1176 lines
37 KiB
Bash
1176 lines
37 KiB
Bash
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
root_dir="$(cd -- "$script_dir/.." && pwd)"
|
|
|
|
local_folder="${LOCAL_FOLDER:-$root_dir}"
|
|
remote_folder="${REMOTE_FOLDER:-/opt/www/virtual/SkinbaseNova}"
|
|
remote_server="${REMOTE_SERVER:-klevze@server3.klevze.si}"
|
|
remote_release_root="${REMOTE_RELEASE_ROOT:-${REMOTE_FOLDER:-/opt/www/virtual/SkinbaseNova}.releases}"
|
|
remote_shared_root="${REMOTE_SHARED_ROOT:-${REMOTE_RELEASE_ROOT:-/opt/www/virtual/SkinbaseNova.releases}/shared}"
|
|
php_bin="${PHP_BIN:-php}"
|
|
composer_bin="${COMPOSER_BIN:-composer}"
|
|
ssh_bin="${SSH_BIN:-ssh}"
|
|
rsync_bin="${RSYNC_BIN:-rsync}"
|
|
local_build_command="${LOCAL_BUILD_COMMAND:-}"
|
|
allow_deploy_from_dot_deploy="${ALLOW_DEPLOY_FROM_DOT_DEPLOY:-0}"
|
|
|
|
run_local_build=1
|
|
run_remote_migrations=1
|
|
run_db_sync=0
|
|
run_meilisearch_setup=0
|
|
auto_detect_meilisearch=0
|
|
dry_run=0
|
|
deploy_mode="normal"
|
|
skip_ssr_restart=0
|
|
db_sync_source=""
|
|
legacy_db_sync_mode=0
|
|
force_db_sync=0
|
|
skip_maintenance=0
|
|
full_upgrade_pre_hook="${FULL_UPGRADE_PRE_HOOK:-}"
|
|
full_upgrade_post_hook="${FULL_UPGRADE_POST_HOOK:-}"
|
|
db_sync_confirm_target="${DB_SYNC_CONFIRM_TARGET:-}"
|
|
db_sync_confirm_phrase="${DB_SYNC_CONFIRM_PHRASE:-}"
|
|
release_retention="${RELEASE_RETENTION:-5}"
|
|
release_id="${RELEASE_ID:-}"
|
|
shared_storage_excludes="${REMOTE_SHARED_STORAGE_EXCLUDES:-}"
|
|
meilisearch_models_csv=""
|
|
readonly all_meilisearch_models_csv='App\Models\Artwork,App\Models\User,App\Models\Group,App\Models\Post,App\Models\Message'
|
|
|
|
declare -a rsync_args=()
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: bash sync.sh [options]
|
|
|
|
Options:
|
|
--mode=normal|full-upgrade
|
|
Choose the deploy mode. Default: normal.
|
|
--full-upgrade Alias for --mode=full-upgrade.
|
|
--skip-build Skip local npm build before rsync.
|
|
--skip-migrate Skip php artisan migrate on the server.
|
|
--dry-run Print the planned rsync/deploy actions without changing the remote server.
|
|
--release-id ID Override the generated release version label used for the remote release directory.
|
|
--keep-releases N Keep the latest N remote releases ready for server-side switching. Default: 5.
|
|
--shared-storage-exclude PATHS
|
|
Comma-separated paths under storage/ to omit from shared storage adoption.
|
|
--with-db-from=local Replace the production database with a dump from the local database.
|
|
--confirm-db-sync-target HOST
|
|
Must match the remote server name when running non-interactively.
|
|
--confirm-db-sync-phrase TEXT
|
|
Must equal 'replace production db from local' when running non-interactively.
|
|
--with-db Legacy alias for --with-db-from=local.
|
|
--force-db-sync Legacy extra confirmation flag for --with-db.
|
|
--with-meilisearch Run Meilisearch settings sync and reimport searchable models.
|
|
--skip-meilisearch Explicitly skip Meilisearch refresh (default behavior).
|
|
--upgrade-pre-hook CMD
|
|
Run a remote shell command before Composer/migrations in full-upgrade mode.
|
|
--upgrade-post-hook CMD
|
|
Run a remote shell command after the deploy completes in full-upgrade mode.
|
|
--no-maintenance Skip php artisan down/up during deploy.
|
|
--skip-ssr-restart Skip restarting the Inertia SSR Node.js process (supervisor: skinbase-ssr).
|
|
--help Show this help.
|
|
|
|
Environment overrides:
|
|
LOCAL_FOLDER, REMOTE_FOLDER, REMOTE_SERVER, REMOTE_RELEASE_ROOT, REMOTE_SHARED_ROOT,
|
|
PHP_BIN, COMPOSER_BIN, SSH_BIN, RSYNC_BIN, LOCAL_BUILD_COMMAND,
|
|
DB_SYNC_CONFIRM_TARGET, DB_SYNC_CONFIRM_PHRASE, RELEASE_RETENTION, RELEASE_ID,
|
|
REMOTE_SHARED_STORAGE_EXCLUDES,
|
|
ALLOW_DEPLOY_FROM_DOT_DEPLOY, FULL_UPGRADE_PRE_HOOK, FULL_UPGRADE_POST_HOOK
|
|
EOF
|
|
}
|
|
|
|
log_step() {
|
|
printf '\n==> %s\n' "$1"
|
|
}
|
|
|
|
log_info() {
|
|
printf ' -> %s\n' "$1"
|
|
}
|
|
|
|
log_warn() {
|
|
printf 'WARN: %s\n' "$1" >&2
|
|
}
|
|
|
|
die() {
|
|
printf 'ERROR: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
require_command() {
|
|
local command_name="$1"
|
|
local description="$2"
|
|
|
|
command -v "$command_name" >/dev/null 2>&1 || die "$description is required but was not found in PATH ($command_name)."
|
|
}
|
|
|
|
validate_positive_integer() {
|
|
local value="$1"
|
|
local label="$2"
|
|
|
|
[[ "$value" =~ ^[1-9][0-9]*$ ]] || die "$label must be a positive integer. Received: $value"
|
|
}
|
|
|
|
sanitize_release_fragment() {
|
|
local value="$1"
|
|
|
|
value="${value//[^A-Za-z0-9._-]/-}"
|
|
value="${value#-}"
|
|
value="${value%-}"
|
|
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
determine_release_id() {
|
|
local timestamp
|
|
local vcs_fragment="manual"
|
|
|
|
[[ -n "$release_id" ]] && return 0
|
|
|
|
timestamp="$(date -u +%Y%m%d-%H%M%S)"
|
|
|
|
if command -v git >/dev/null 2>&1 && git -C "$local_folder" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
vcs_fragment="$(git -C "$local_folder" describe --always --dirty --tags 2>/dev/null || git -C "$local_folder" rev-parse --short HEAD 2>/dev/null || printf 'manual')"
|
|
fi
|
|
|
|
vcs_fragment="$(sanitize_release_fragment "$vcs_fragment")"
|
|
[[ -n "$vcs_fragment" ]] || vcs_fragment="manual"
|
|
release_id="${timestamp}-${vcs_fragment}"
|
|
}
|
|
|
|
remote_release_path() {
|
|
printf '%s' "${remote_release_root}/releases/${release_id}"
|
|
}
|
|
|
|
guard_local_folder() {
|
|
local normalized_local_folder
|
|
|
|
normalized_local_folder="$(cd -- "$local_folder" && pwd)"
|
|
local_folder="$normalized_local_folder"
|
|
|
|
echo "Deploy source: $local_folder"
|
|
echo "Deploy target: $remote_server:$remote_folder"
|
|
|
|
if [[ ! -d "$local_folder" ]]; then
|
|
die "Deploy source folder does not exist: $local_folder"
|
|
fi
|
|
|
|
if [[ "$allow_deploy_from_dot_deploy" != "1" && "$local_folder" == *"/.deploy/"* ]]; then
|
|
log_warn "Refusing to deploy from a .deploy snapshot folder: $local_folder"
|
|
log_warn "This usually means LOCAL_FOLDER is pointing at a stale release snapshot instead of the repo root."
|
|
log_warn "Unset LOCAL_FOLDER or set it to the repository root before running sync.sh."
|
|
die "If you intentionally want to deploy from that folder, set ALLOW_DEPLOY_FROM_DOT_DEPLOY=1."
|
|
fi
|
|
}
|
|
|
|
run_preflight_checks() {
|
|
log_step "Running local preflight checks"
|
|
|
|
require_command "$php_bin" "PHP"
|
|
require_command "$composer_bin" "Composer"
|
|
require_command "$ssh_bin" "SSH"
|
|
require_command "$rsync_bin" "rsync"
|
|
|
|
[[ -f "$local_folder/artisan" ]] || die "Expected Laravel artisan entrypoint at $local_folder/artisan."
|
|
[[ -f "$local_folder/composer.json" ]] || die "Expected composer.json at $local_folder/composer.json."
|
|
validate_positive_integer "$release_retention" "Release retention"
|
|
determine_release_id
|
|
log_info "Release version: $release_id"
|
|
log_info "Remote release root: $remote_release_root"
|
|
log_info "Remote shared root: $remote_shared_root"
|
|
|
|
if [[ "$run_local_build" -eq 1 && -z "$local_build_command" ]]; then
|
|
if is_wsl && command -v wslpath >/dev/null 2>&1 && command -v powershell.exe >/dev/null 2>&1; then
|
|
log_info "WSL frontend build will use Windows npm.cmd via powershell.exe"
|
|
else
|
|
require_command npm "npm"
|
|
fi
|
|
fi
|
|
|
|
log_info "Required local deploy tools are available"
|
|
}
|
|
|
|
is_wsl() {
|
|
[[ -n "${WSL_DISTRO_NAME:-}" || -n "${WSL_INTEROP:-}" ]]
|
|
}
|
|
|
|
run_frontend_build() {
|
|
if [[ -n "$local_build_command" ]]; then
|
|
(
|
|
cd "$local_folder"
|
|
eval "$local_build_command"
|
|
)
|
|
return
|
|
fi
|
|
|
|
if is_wsl && command -v wslpath >/dev/null 2>&1 && command -v powershell.exe >/dev/null 2>&1; then
|
|
local windows_local_folder
|
|
windows_local_folder="$(wslpath -w "$local_folder")"
|
|
|
|
echo "Detected WSL checkout; running frontend build with Windows npm.cmd to match local node_modules..."
|
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \
|
|
"Set-Location -LiteralPath '$windows_local_folder'; npm.cmd run build; npm.cmd run build:ssr"
|
|
return
|
|
fi
|
|
|
|
(
|
|
cd "$local_folder"
|
|
npm run build
|
|
npm run build:ssr
|
|
)
|
|
}
|
|
|
|
build_rsync_args() {
|
|
rsync_args=(
|
|
-rlvz
|
|
--no-perms
|
|
--no-times
|
|
--omit-dir-times
|
|
--delay-updates
|
|
--delete
|
|
--delete-delay
|
|
--exclude ".phpintel/"
|
|
--exclude "bootstrap/cache/"
|
|
--exclude ".env"
|
|
--exclude "public/hot"
|
|
--exclude "public/sitemap.xml"
|
|
--exclude "public/sitemaps/"
|
|
--exclude "node_modules"
|
|
--exclude "public/files/"
|
|
--exclude "resources/lang/"
|
|
--exclude "storage/"
|
|
--exclude ".git/"
|
|
--exclude ".deploy/"
|
|
--exclude ".cursor/"
|
|
--exclude ".venv/"
|
|
--exclude "/var/php-tmp"
|
|
--exclude "/var/php-sessions"
|
|
--exclude "/oldSite"
|
|
--exclude "/vendor"
|
|
-e "$ssh_bin"
|
|
)
|
|
}
|
|
|
|
collect_sync_changed_files() {
|
|
local itemized
|
|
|
|
if ! itemized="$($rsync_bin "${rsync_args[@]}" --dry-run --itemize-changes "$local_folder/" "$remote_server:$remote_folder/" 2>/dev/null)"; then
|
|
return 1
|
|
fi
|
|
|
|
printf '%s\n' "$itemized" | awk '
|
|
/^deleting / {
|
|
sub(/^deleting /, "", $0)
|
|
if ($0 !~ /\/$/) print
|
|
next
|
|
}
|
|
/^[<>ch.*][^ ]* / {
|
|
path = $0
|
|
sub(/^[^ ]+ /, "", path)
|
|
if (path !~ /\/$/) print path
|
|
}
|
|
' | sed '/^$/d' | sort -u
|
|
}
|
|
|
|
detect_meilisearch_models_from_sync() {
|
|
local changed_files
|
|
local file
|
|
local force_full=0
|
|
local -a models=()
|
|
|
|
if ! changed_files="$(collect_sync_changed_files)"; then
|
|
return 1
|
|
fi
|
|
|
|
[[ -n "$changed_files" ]] || return 1
|
|
|
|
while IFS= read -r file; do
|
|
case "$file" in
|
|
config/scout.php|app/Console/Commands/ConfigureMeilisearchIndex.php)
|
|
force_full=1
|
|
;;
|
|
app/Models/Artwork.php)
|
|
models+=("App\\Models\\Artwork")
|
|
;;
|
|
app/Models/User.php)
|
|
models+=("App\\Models\\User")
|
|
;;
|
|
app/Models/Group.php)
|
|
models+=("App\\Models\\Group")
|
|
;;
|
|
app/Models/Post.php)
|
|
models+=("App\\Models\\Post")
|
|
;;
|
|
app/Models/Message.php)
|
|
models+=("App\\Models\\Message")
|
|
;;
|
|
esac
|
|
done <<< "$changed_files"
|
|
|
|
if [[ "$force_full" -eq 1 ]]; then
|
|
printf '%s\n' "$all_meilisearch_models_csv"
|
|
return 0
|
|
fi
|
|
|
|
[[ ${#models[@]} -gt 0 ]] || return 1
|
|
|
|
printf '%s\n' "$(printf '%s\n' "${models[@]}" | awk '!seen[$0]++' | paste -sd, -)"
|
|
}
|
|
|
|
confirm_database_replacement() {
|
|
local expected_phrase="replace production db from local"
|
|
local typed_target=""
|
|
local typed_phrase=""
|
|
|
|
if [[ "$run_db_sync" -ne 1 || "$db_sync_source" != "local" ]]; then
|
|
return
|
|
fi
|
|
|
|
echo "WARNING: this will overwrite the production database on $remote_server using your local database dump."
|
|
|
|
if [[ -n "$db_sync_confirm_target" || -n "$db_sync_confirm_phrase" ]]; then
|
|
if [[ "$db_sync_confirm_target" != "$remote_server" ]]; then
|
|
die "Refusing DB sync: --confirm-db-sync-target must exactly match $remote_server."
|
|
fi
|
|
|
|
if [[ "$db_sync_confirm_phrase" != "$expected_phrase" ]]; then
|
|
die "Refusing DB sync: --confirm-db-sync-phrase must exactly equal '$expected_phrase'."
|
|
fi
|
|
|
|
return
|
|
fi
|
|
|
|
if [[ ! -t 0 ]]; then
|
|
die "Refusing DB sync in non-interactive mode without --confirm-db-sync-target '$remote_server' and --confirm-db-sync-phrase '$expected_phrase'."
|
|
fi
|
|
|
|
read -r -p "Type the remote server to confirm DB replacement [$remote_server]: " typed_target
|
|
[[ "$typed_target" == "$remote_server" ]] || die "Refusing DB sync: remote server confirmation did not match."
|
|
|
|
read -r -p "Type '$expected_phrase' to continue: " typed_phrase
|
|
[[ "$typed_phrase" == "$expected_phrase" ]] || die "Refusing DB sync: confirmation phrase did not match."
|
|
}
|
|
|
|
prepare_remote_release_layout() {
|
|
log_step "Preparing remote release layout"
|
|
|
|
"$ssh_bin" "$remote_server" \
|
|
REMOTE_FOLDER="$(printf '%q' "$remote_folder")" \
|
|
REMOTE_RELEASE_ROOT="$(printf '%q' "$remote_release_root")" \
|
|
REMOTE_SHARED_ROOT="$(printf '%q' "$remote_shared_root")" \
|
|
RELEASE_ID="$(printf '%q' "$release_id")" \
|
|
REMOTE_SHARED_STORAGE_EXCLUDES="$(printf '%q' "$shared_storage_excludes")" \
|
|
'bash -s' <<'EOF'
|
|
set -euo pipefail
|
|
|
|
release_path="${REMOTE_RELEASE_ROOT}/releases/${RELEASE_ID}"
|
|
current_link="${REMOTE_RELEASE_ROOT}/current"
|
|
legacy_release_id="legacy-$(date -u +%Y%m%d-%H%M%S)"
|
|
legacy_release_path="${REMOTE_RELEASE_ROOT}/releases/${legacy_release_id}"
|
|
declare -a storage_exclude_args=()
|
|
|
|
log_info() {
|
|
printf ' -> %s\n' "$1"
|
|
}
|
|
|
|
die() {
|
|
printf 'ERROR: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
ensure_dir() {
|
|
local d
|
|
for d in "$@"; do
|
|
mkdir -p "$d"
|
|
done
|
|
}
|
|
|
|
ensure_laravel_shared_storage_layout() {
|
|
ensure_dir \
|
|
"${REMOTE_SHARED_ROOT}/storage/app/public" \
|
|
"${REMOTE_SHARED_ROOT}/storage/framework/cache/data" \
|
|
"${REMOTE_SHARED_ROOT}/storage/framework/sessions" \
|
|
"${REMOTE_SHARED_ROOT}/storage/framework/views" \
|
|
"${REMOTE_SHARED_ROOT}/storage/logs"
|
|
}
|
|
|
|
build_storage_exclude_args() {
|
|
local detected_excludes=()
|
|
local normalized=""
|
|
|
|
mapfile -t detected_excludes < <(find_auto_storage_excludes "${REMOTE_SHARED_ROOT}/storage")
|
|
|
|
for normalized in "${detected_excludes[@]}"; do
|
|
storage_exclude_args+=(--exclude "$normalized")
|
|
done
|
|
|
|
while IFS= read -r normalized; do
|
|
normalized="${normalized#storage/}"
|
|
normalized="${normalized#/}"
|
|
[[ -n "$normalized" ]] || continue
|
|
storage_exclude_args+=(--exclude "$normalized")
|
|
done < <(printf '%s' "${REMOTE_SHARED_STORAGE_EXCLUDES:-}" | tr ',' '\n')
|
|
}
|
|
|
|
find_auto_storage_excludes() {
|
|
local storage_root="$1"
|
|
|
|
[[ -d "${storage_root}/app" ]] || return 0
|
|
|
|
find "${storage_root}/app" \
|
|
\( -type d -o -type f \) \
|
|
\( -iname '*backup*' -o -iname '*.bak' -o -iname '*.bak.*' \) \
|
|
-printf '%P\n' \
|
|
| sed 's#^#app/#' \
|
|
| awk '!seen[$0]++'
|
|
}
|
|
|
|
prune_storage_excludes() {
|
|
local base_path="$1"
|
|
local detected_excludes=()
|
|
local normalized=""
|
|
|
|
mapfile -t detected_excludes < <(find_auto_storage_excludes "$base_path")
|
|
|
|
for normalized in "${detected_excludes[@]}"; do
|
|
if [[ -e "${base_path}/${normalized}" ]]; then
|
|
log_info "Skipping shared storage backup path ${normalized}"
|
|
rm -rf "${base_path}/${normalized}"
|
|
fi
|
|
done
|
|
|
|
while IFS= read -r normalized; do
|
|
normalized="${normalized#storage/}"
|
|
normalized="${normalized#/}"
|
|
[[ -n "$normalized" ]] || continue
|
|
|
|
if [[ -e "${base_path}/${normalized}" ]]; then
|
|
log_info "Skipping shared storage path ${normalized}"
|
|
rm -rf "${base_path}/${normalized}"
|
|
fi
|
|
done < <(printf '%s' "${REMOTE_SHARED_STORAGE_EXCLUDES:-}" | tr ',' '\n')
|
|
}
|
|
|
|
sync_dir_into_shared() {
|
|
local source_path="$1"
|
|
local shared_path="$2"
|
|
local exclude_scope="${3:-}"
|
|
|
|
ensure_dir "$shared_path"
|
|
|
|
if [[ -d "$source_path" ]]; then
|
|
if [[ "$exclude_scope" == "storage" && ${#storage_exclude_args[@]} -gt 0 ]]; then
|
|
rsync -a "${storage_exclude_args[@]}" "$source_path/" "$shared_path/"
|
|
else
|
|
rsync -a "$source_path/" "$shared_path/"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
adopt_dir_into_shared() {
|
|
local source_path="$1"
|
|
local shared_path="$2"
|
|
local exclude_scope="${3:-}"
|
|
|
|
[[ -d "$source_path" ]] || return 0
|
|
|
|
ensure_dir "$(dirname "$shared_path")"
|
|
|
|
if [[ ! -d "$shared_path" ]] || [[ -z "$(find "$shared_path" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]]; then
|
|
rm -rf "$shared_path"
|
|
mv "$source_path" "$shared_path"
|
|
|
|
if [[ "$exclude_scope" == "storage" ]]; then
|
|
prune_storage_excludes "$shared_path"
|
|
fi
|
|
|
|
return 0
|
|
fi
|
|
|
|
sync_dir_into_shared "$source_path" "$shared_path" "$exclude_scope"
|
|
}
|
|
|
|
adopt_file_into_shared() {
|
|
local source_path="$1"
|
|
local shared_path="$2"
|
|
|
|
[[ -f "$source_path" ]] || return 0
|
|
|
|
ensure_dir "$(dirname "$shared_path")"
|
|
|
|
if [[ ! -e "$shared_path" ]]; then
|
|
mv "$source_path" "$shared_path"
|
|
return 0
|
|
fi
|
|
|
|
cp -a "$source_path" "$shared_path"
|
|
}
|
|
|
|
link_shared_paths() {
|
|
local target_release="$1"
|
|
|
|
ensure_dir "$target_release/bootstrap/cache" "$target_release/public" "$target_release/var"
|
|
|
|
rm -f "$target_release/.env"
|
|
ln -sfn "${REMOTE_SHARED_ROOT}/.env" "$target_release/.env"
|
|
|
|
rm -rf "$target_release/storage"
|
|
ln -sfn "${REMOTE_SHARED_ROOT}/storage" "$target_release/storage"
|
|
|
|
rm -rf "$target_release/public/files"
|
|
ln -sfn "${REMOTE_SHARED_ROOT}/public/files" "$target_release/public/files"
|
|
|
|
rm -f "$target_release/public/sitemap.xml"
|
|
ln -sfn "${REMOTE_SHARED_ROOT}/public/sitemaps/sitemap.xml" "$target_release/public/sitemap.xml"
|
|
|
|
rm -rf "$target_release/public/sitemaps"
|
|
ln -sfn "${REMOTE_SHARED_ROOT}/public/sitemaps" "$target_release/public/sitemaps"
|
|
|
|
rm -rf "$target_release/var/php-tmp"
|
|
ln -sfn "${REMOTE_SHARED_ROOT}/var/php-tmp" "$target_release/var/php-tmp"
|
|
|
|
rm -rf "$target_release/var/php-sessions"
|
|
ln -sfn "${REMOTE_SHARED_ROOT}/var/php-sessions" "$target_release/var/php-sessions"
|
|
}
|
|
|
|
ensure_dir "${REMOTE_RELEASE_ROOT}/releases" "${REMOTE_RELEASE_ROOT}/deployments" "$REMOTE_SHARED_ROOT"
|
|
ensure_dir "${REMOTE_SHARED_ROOT}/storage" "${REMOTE_SHARED_ROOT}/public/files" "${REMOTE_SHARED_ROOT}/public/sitemaps" "${REMOTE_SHARED_ROOT}/var/php-tmp" "${REMOTE_SHARED_ROOT}/var/php-sessions"
|
|
ensure_laravel_shared_storage_layout
|
|
build_storage_exclude_args
|
|
prune_storage_excludes "${REMOTE_SHARED_ROOT}/storage"
|
|
|
|
if [[ -e "$REMOTE_FOLDER" && ! -L "$REMOTE_FOLDER" ]]; then
|
|
[[ -d "$REMOTE_FOLDER" ]] || die "Remote app path exists but is not a directory: ${REMOTE_FOLDER}"
|
|
|
|
log_info "Adopting existing live folder into release layout"
|
|
mv "$REMOTE_FOLDER" "$legacy_release_path"
|
|
|
|
if [[ -f "${legacy_release_path}/.env" && ! -f "${REMOTE_SHARED_ROOT}/.env" ]]; then
|
|
cp -a "${legacy_release_path}/.env" "${REMOTE_SHARED_ROOT}/.env"
|
|
fi
|
|
|
|
adopt_dir_into_shared "${legacy_release_path}/storage" "${REMOTE_SHARED_ROOT}/storage" "storage"
|
|
adopt_dir_into_shared "${legacy_release_path}/public/files" "${REMOTE_SHARED_ROOT}/public/files"
|
|
adopt_dir_into_shared "${legacy_release_path}/public/sitemaps" "${REMOTE_SHARED_ROOT}/public/sitemaps"
|
|
adopt_file_into_shared "${legacy_release_path}/public/sitemap.xml" "${REMOTE_SHARED_ROOT}/public/sitemaps/sitemap.xml"
|
|
adopt_dir_into_shared "${legacy_release_path}/var/php-tmp" "${REMOTE_SHARED_ROOT}/var/php-tmp"
|
|
adopt_dir_into_shared "${legacy_release_path}/var/php-sessions" "${REMOTE_SHARED_ROOT}/var/php-sessions"
|
|
|
|
link_shared_paths "$legacy_release_path"
|
|
ln -sfn "$legacy_release_path" "$current_link"
|
|
fi
|
|
|
|
if [[ -L "$REMOTE_FOLDER" ]]; then
|
|
ln -sfn "$current_link" "$REMOTE_FOLDER"
|
|
elif [[ ! -e "$REMOTE_FOLDER" ]]; then
|
|
ln -sfn "$current_link" "$REMOTE_FOLDER"
|
|
fi
|
|
|
|
rm -rf "$release_path"
|
|
mkdir -p "$release_path"
|
|
|
|
log_info "Release staging path ready at ${release_path}"
|
|
EOF
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--mode)
|
|
shift
|
|
case "${1:?Missing value for --mode}" in
|
|
normal|full-upgrade)
|
|
deploy_mode="$1"
|
|
;;
|
|
*)
|
|
die "Unsupported deploy mode: $1"
|
|
;;
|
|
esac
|
|
;;
|
|
--mode=*)
|
|
case "${1#*=}" in
|
|
normal|full-upgrade)
|
|
deploy_mode="${1#*=}"
|
|
;;
|
|
*)
|
|
die "Unsupported deploy mode: ${1#*=}"
|
|
;;
|
|
esac
|
|
;;
|
|
--full-upgrade)
|
|
deploy_mode="full-upgrade"
|
|
;;
|
|
--skip-build)
|
|
run_local_build=0
|
|
;;
|
|
--skip-migrate)
|
|
run_remote_migrations=0
|
|
;;
|
|
--dry-run)
|
|
dry_run=1
|
|
;;
|
|
--release-id)
|
|
shift
|
|
release_id="$(sanitize_release_fragment "${1:?Missing value for --release-id}")"
|
|
;;
|
|
--release-id=*)
|
|
release_id="$(sanitize_release_fragment "${1#*=}")"
|
|
;;
|
|
--keep-releases)
|
|
shift
|
|
release_retention="${1:?Missing value for --keep-releases}"
|
|
;;
|
|
--keep-releases=*)
|
|
release_retention="${1#*=}"
|
|
;;
|
|
--shared-storage-exclude)
|
|
shift
|
|
shared_storage_excludes="${1:?Missing value for --shared-storage-exclude}"
|
|
;;
|
|
--shared-storage-exclude=*)
|
|
shared_storage_excludes="${1#*=}"
|
|
;;
|
|
--with-db-from=local)
|
|
run_db_sync=1
|
|
db_sync_source="local"
|
|
;;
|
|
--with-db-from=*)
|
|
die "Unsupported DB sync source in option: $1"
|
|
;;
|
|
--with-db)
|
|
run_db_sync=1
|
|
db_sync_source="local"
|
|
legacy_db_sync_mode=1
|
|
;;
|
|
--force-db-sync)
|
|
force_db_sync=1
|
|
;;
|
|
--confirm-db-sync-target)
|
|
shift
|
|
db_sync_confirm_target="${1:?Missing value for --confirm-db-sync-target}"
|
|
;;
|
|
--confirm-db-sync-target=*)
|
|
db_sync_confirm_target="${1#*=}"
|
|
;;
|
|
--confirm-db-sync-phrase)
|
|
shift
|
|
db_sync_confirm_phrase="${1:?Missing value for --confirm-db-sync-phrase}"
|
|
;;
|
|
--confirm-db-sync-phrase=*)
|
|
db_sync_confirm_phrase="${1#*=}"
|
|
;;
|
|
--with-meilisearch)
|
|
run_meilisearch_setup=1
|
|
auto_detect_meilisearch=0
|
|
meilisearch_models_csv="$all_meilisearch_models_csv"
|
|
;;
|
|
--skip-meilisearch)
|
|
run_meilisearch_setup=0
|
|
auto_detect_meilisearch=0
|
|
meilisearch_models_csv=""
|
|
;;
|
|
--upgrade-pre-hook)
|
|
shift
|
|
full_upgrade_pre_hook="${1:?Missing value for --upgrade-pre-hook}"
|
|
;;
|
|
--upgrade-pre-hook=*)
|
|
full_upgrade_pre_hook="${1#*=}"
|
|
;;
|
|
--upgrade-post-hook)
|
|
shift
|
|
full_upgrade_post_hook="${1:?Missing value for --upgrade-post-hook}"
|
|
;;
|
|
--upgrade-post-hook=*)
|
|
full_upgrade_post_hook="${1#*=}"
|
|
;;
|
|
--no-maintenance)
|
|
skip_maintenance=1
|
|
;;
|
|
--skip-ssr-restart)
|
|
skip_ssr_restart=1
|
|
;;
|
|
--help|-h)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
die "Unknown option: $1"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [[ -n "$full_upgrade_pre_hook" || -n "$full_upgrade_post_hook" ]] && [[ "$deploy_mode" != "full-upgrade" ]]; then
|
|
die "Upgrade hooks can only be used with --mode=full-upgrade."
|
|
fi
|
|
|
|
guard_local_folder
|
|
run_preflight_checks
|
|
|
|
if [[ "$run_db_sync" -eq 1 && "$db_sync_source" != "local" ]]; then
|
|
die "Refusing DB sync without an explicit source. Use --with-db-from=local."
|
|
fi
|
|
|
|
if [[ "$run_db_sync" -eq 1 ]] && { [[ -z "$db_sync_confirm_target" && -n "$db_sync_confirm_phrase" ]] || [[ -n "$db_sync_confirm_target" && -z "$db_sync_confirm_phrase" ]]; }; then
|
|
die "Refusing DB sync: both --confirm-db-sync-target and --confirm-db-sync-phrase are required together when provided."
|
|
fi
|
|
|
|
if [[ "$legacy_db_sync_mode" -eq 1 && "$force_db_sync" -ne 1 ]]; then
|
|
die "Refusing legacy --with-db without --force-db-sync. Prefer --with-db-from=local instead."
|
|
fi
|
|
|
|
if [[ "$run_db_sync" -eq 1 ]]; then
|
|
confirm_database_replacement
|
|
fi
|
|
|
|
if [[ "$deploy_mode" == "full-upgrade" && "$auto_detect_meilisearch" -eq 1 && "$run_meilisearch_setup" -eq 0 ]]; then
|
|
run_meilisearch_setup=1
|
|
auto_detect_meilisearch=0
|
|
meilisearch_models_csv="$all_meilisearch_models_csv"
|
|
fi
|
|
|
|
if [[ "$run_local_build" -eq 1 ]]; then
|
|
log_step "Building frontend assets locally"
|
|
run_frontend_build
|
|
fi
|
|
|
|
build_rsync_args
|
|
|
|
if [[ "$dry_run" -eq 1 ]]; then
|
|
log_step "Dry-run deployment preview"
|
|
log_info "Skipping remote changes because --dry-run was requested"
|
|
log_info "Release version: $release_id"
|
|
log_info "Release staging path: $(remote_release_path)"
|
|
log_info "Remote app path will be switched on the server by updating ${remote_release_root}/current"
|
|
log_info "Planned rsync command: $rsync_bin ${rsync_args[*]} --dry-run --itemize-changes $local_folder/ $remote_server:$(remote_release_path)/"
|
|
"$rsync_bin" "${rsync_args[@]}" --dry-run --itemize-changes "$local_folder/" "$remote_server:$(remote_release_path)/"
|
|
|
|
if [[ "$run_db_sync" -eq 1 ]]; then
|
|
log_warn "Dry-run requested with database sync enabled. The database replacement step was not executed."
|
|
fi
|
|
|
|
if [[ "$run_meilisearch_setup" -eq 1 ]]; then
|
|
log_info "Meilisearch refresh would run for: ${meilisearch_models_csv:-$all_meilisearch_models_csv}"
|
|
fi
|
|
|
|
log_step "Dry-run complete"
|
|
exit 0
|
|
fi
|
|
|
|
prepare_remote_release_layout
|
|
|
|
if [[ "$run_meilisearch_setup" -eq 0 && "$auto_detect_meilisearch" -eq 1 ]]; then
|
|
if meilisearch_models_csv="$(detect_meilisearch_models_from_sync)"; then
|
|
run_meilisearch_setup=1
|
|
log_info "Detected Meilisearch-relevant changes in this deployment; will refresh indexes for: $meilisearch_models_csv"
|
|
fi
|
|
fi
|
|
|
|
log_step "Syncing release ${release_id} to $remote_server"
|
|
"$rsync_bin" "${rsync_args[@]}" "$local_folder/" "$remote_server:$(remote_release_path)/"
|
|
|
|
if [[ "$run_db_sync" -eq 1 ]]; then
|
|
log_step "Replacing the production database from the local dump"
|
|
"$script_dir/push-db-to-prod.sh" \
|
|
--force \
|
|
--remote-server "$remote_server" \
|
|
--remote-folder "$remote_folder" \
|
|
$( [[ "$run_remote_migrations" -eq 0 ]] && printf '%s' '--skip-migrate' )
|
|
fi
|
|
|
|
log_step "Running remote Composer and release switch steps"
|
|
"$ssh_bin" "$remote_server" \
|
|
REMOTE_FOLDER="$(printf '%q' "$remote_folder")" \
|
|
REMOTE_RELEASE_ROOT="$(printf '%q' "$remote_release_root")" \
|
|
REMOTE_SHARED_ROOT="$(printf '%q' "$remote_shared_root")" \
|
|
RELEASE_ID="$(printf '%q' "$release_id")" \
|
|
RELEASE_RETENTION="$(printf '%q' "$release_retention")" \
|
|
REMOTE_SHARED_STORAGE_EXCLUDES="$(printf '%q' "$shared_storage_excludes")" \
|
|
PHP_BIN="$(printf '%q' "$php_bin")" \
|
|
COMPOSER_BIN="$(printf '%q' "$composer_bin")" \
|
|
RUN_REMOTE_MIGRATIONS="$run_remote_migrations" \
|
|
SKIP_MAINTENANCE="$skip_maintenance" \
|
|
SKIP_SSR_RESTART="$skip_ssr_restart" \
|
|
DEPLOY_MODE="$(printf '%q' "$deploy_mode")" \
|
|
RUN_MEILISEARCH_SETUP="$run_meilisearch_setup" \
|
|
FULL_UPGRADE_PRE_HOOK="$(printf '%q' "$full_upgrade_pre_hook")" \
|
|
FULL_UPGRADE_POST_HOOK="$(printf '%q' "$full_upgrade_post_hook")" \
|
|
MEILISEARCH_MODELS_CSV="$(printf '%q' "$meilisearch_models_csv")" \
|
|
'bash -s' <<'EOF'
|
|
set -euo pipefail
|
|
|
|
release_path="${REMOTE_RELEASE_ROOT}/releases/${RELEASE_ID}"
|
|
current_link="${REMOTE_RELEASE_ROOT}/current"
|
|
current_app_path="$REMOTE_FOLDER"
|
|
previous_release_id=""
|
|
declare -a storage_exclude_args=()
|
|
|
|
log_step() {
|
|
printf '\n==> %s\n' "$1"
|
|
}
|
|
|
|
log_warn() {
|
|
printf 'WARN: %s\n' "$1" >&2
|
|
}
|
|
|
|
die() {
|
|
printf 'ERROR: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
ensure_dir() {
|
|
local d
|
|
for d in "$@"; do
|
|
mkdir -p "$d"
|
|
done
|
|
}
|
|
|
|
ensure_laravel_shared_storage_layout() {
|
|
ensure_dir \
|
|
"${REMOTE_SHARED_ROOT}/storage/app/public" \
|
|
"${REMOTE_SHARED_ROOT}/storage/framework/cache/data" \
|
|
"${REMOTE_SHARED_ROOT}/storage/framework/sessions" \
|
|
"${REMOTE_SHARED_ROOT}/storage/framework/views" \
|
|
"${REMOTE_SHARED_ROOT}/storage/logs"
|
|
}
|
|
|
|
build_storage_exclude_args() {
|
|
local detected_excludes=()
|
|
local normalized=""
|
|
|
|
mapfile -t detected_excludes < <(find_auto_storage_excludes "${REMOTE_SHARED_ROOT}/storage")
|
|
|
|
for normalized in "${detected_excludes[@]}"; do
|
|
storage_exclude_args+=(--exclude "$normalized")
|
|
done
|
|
|
|
while IFS= read -r normalized; do
|
|
normalized="${normalized#storage/}"
|
|
normalized="${normalized#/}"
|
|
[[ -n "$normalized" ]] || continue
|
|
storage_exclude_args+=(--exclude "$normalized")
|
|
done < <(printf '%s' "${REMOTE_SHARED_STORAGE_EXCLUDES:-}" | tr ',' '\n')
|
|
}
|
|
|
|
find_auto_storage_excludes() {
|
|
local storage_root="$1"
|
|
|
|
[[ -d "${storage_root}/app" ]] || return 0
|
|
|
|
find "${storage_root}/app" \
|
|
\( -type d -o -type f \) \
|
|
\( -iname '*backup*' -o -iname '*.bak' -o -iname '*.bak.*' \) \
|
|
-printf '%P\n' \
|
|
| sed 's#^#app/#' \
|
|
| awk '!seen[$0]++'
|
|
}
|
|
|
|
prune_storage_excludes() {
|
|
local base_path="$1"
|
|
local detected_excludes=()
|
|
local normalized=""
|
|
|
|
mapfile -t detected_excludes < <(find_auto_storage_excludes "$base_path")
|
|
|
|
for normalized in "${detected_excludes[@]}"; do
|
|
if [[ -e "${base_path}/${normalized}" ]]; then
|
|
log_warn "Pruning shared storage backup path ${normalized}"
|
|
rm -rf "${base_path}/${normalized}"
|
|
fi
|
|
done
|
|
|
|
while IFS= read -r normalized; do
|
|
normalized="${normalized#storage/}"
|
|
normalized="${normalized#/}"
|
|
[[ -n "$normalized" ]] || continue
|
|
|
|
if [[ -e "${base_path}/${normalized}" ]]; then
|
|
log_warn "Pruning shared storage path ${normalized}"
|
|
rm -rf "${base_path}/${normalized}"
|
|
fi
|
|
done < <(printf '%s' "${REMOTE_SHARED_STORAGE_EXCLUDES:-}" | tr ',' '\n')
|
|
}
|
|
|
|
ensure_php_runtime_dir() {
|
|
local target_dir="$1"
|
|
local -a privileged_cmd=()
|
|
|
|
if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
|
|
privileged_cmd=(sudo -n)
|
|
elif [[ "$(id -u)" -eq 0 ]]; then
|
|
privileged_cmd=()
|
|
fi
|
|
|
|
if [[ ! -d "$target_dir" ]]; then
|
|
if [[ ${#privileged_cmd[@]} -gt 0 ]]; then
|
|
"${privileged_cmd[@]}" mkdir -p "$target_dir"
|
|
else
|
|
mkdir -p "$target_dir"
|
|
fi
|
|
fi
|
|
|
|
if [[ ${#privileged_cmd[@]} -gt 0 || "$(id -u)" -eq 0 ]]; then
|
|
"${privileged_cmd[@]}" chown -R skinbase:skinbase "$target_dir"
|
|
"${privileged_cmd[@]}" chmod 770 "$target_dir"
|
|
return
|
|
fi
|
|
|
|
chmod 770 "$target_dir" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
ensure_shared_env_readable() {
|
|
local env_path="${REMOTE_SHARED_ROOT}/.env"
|
|
local current_user
|
|
local env_details="details=unavailable"
|
|
|
|
[[ -f "$env_path" ]] || die "Shared production .env is missing at ${env_path}"
|
|
[[ -r "$env_path" ]] && return 0
|
|
|
|
current_user="$(id -un)"
|
|
|
|
if command -v stat >/dev/null 2>&1; then
|
|
env_details="$(stat -c 'owner=%U group=%G mode=%a path=%n' "$env_path" 2>/dev/null || printf 'details=unavailable path=%s' "$env_path")"
|
|
else
|
|
env_details="$(ls -ld "$env_path" 2>/dev/null || printf 'details=unavailable path=%s' "$env_path")"
|
|
fi
|
|
|
|
if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1 && command -v setfacl >/dev/null 2>&1; then
|
|
sudo -n setfacl -m "u:${current_user}:r" "$env_path" >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
[[ -r "$env_path" ]] && return 0
|
|
|
|
die "Shared production .env exists but is not readable by ${current_user}. ${env_details}. Fix permissions or ACLs on ${env_path} before deploy."
|
|
}
|
|
|
|
link_shared_paths() {
|
|
local target_release="$1"
|
|
|
|
ensure_dir "$target_release/bootstrap/cache" "$target_release/public" "$target_release/var"
|
|
|
|
rm -f "$target_release/.env"
|
|
ln -sfn "${REMOTE_SHARED_ROOT}/.env" "$target_release/.env"
|
|
|
|
rm -rf "$target_release/storage"
|
|
ln -sfn "${REMOTE_SHARED_ROOT}/storage" "$target_release/storage"
|
|
|
|
rm -rf "$target_release/public/files"
|
|
ln -sfn "${REMOTE_SHARED_ROOT}/public/files" "$target_release/public/files"
|
|
|
|
rm -f "$target_release/public/sitemap.xml"
|
|
ln -sfn "${REMOTE_SHARED_ROOT}/public/sitemaps/sitemap.xml" "$target_release/public/sitemap.xml"
|
|
|
|
rm -rf "$target_release/public/sitemaps"
|
|
ln -sfn "${REMOTE_SHARED_ROOT}/public/sitemaps" "$target_release/public/sitemaps"
|
|
|
|
rm -rf "$target_release/var/php-tmp"
|
|
ln -sfn "${REMOTE_SHARED_ROOT}/var/php-tmp" "$target_release/var/php-tmp"
|
|
|
|
rm -rf "$target_release/var/php-sessions"
|
|
ln -sfn "${REMOTE_SHARED_ROOT}/var/php-sessions" "$target_release/var/php-sessions"
|
|
}
|
|
|
|
current_release_id() {
|
|
if [[ -L "$current_link" ]]; then
|
|
basename "$(readlink "$current_link")"
|
|
return 0
|
|
fi
|
|
|
|
printf '%s\n' ''
|
|
}
|
|
|
|
bring_app_up() {
|
|
if [[ "$SKIP_MAINTENANCE" -eq 0 && -f "$current_app_path/artisan" ]]; then
|
|
"$PHP_BIN" "$current_app_path/artisan" up >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
run_remote_hook() {
|
|
local hook_name="$1"
|
|
local hook_command="$2"
|
|
|
|
[[ -n "$hook_command" ]] || return 0
|
|
|
|
log_step "Running ${hook_name}"
|
|
bash -lc "$hook_command"
|
|
}
|
|
|
|
prune_old_releases() {
|
|
local -a releases=()
|
|
local current_release
|
|
local prune_count
|
|
local blocked_path=""
|
|
local blocked_details="details=unavailable"
|
|
|
|
mapfile -t releases < <(find "${REMOTE_RELEASE_ROOT}/releases" -mindepth 1 -maxdepth 1 -type d | sort)
|
|
current_release="$(current_release_id)"
|
|
|
|
if (( ${#releases[@]} <= RELEASE_RETENTION )); then
|
|
return
|
|
fi
|
|
|
|
prune_count=$(( ${#releases[@]} - RELEASE_RETENTION ))
|
|
for (( i=0; i<${#releases[@]} && prune_count>0; i++ )); do
|
|
if [[ "$(basename "${releases[$i]}")" == "$current_release" ]]; then
|
|
continue
|
|
fi
|
|
|
|
if rm -rf "${releases[$i]}" 2>/dev/null; then
|
|
rm -f "${REMOTE_RELEASE_ROOT}/deployments/$(basename "${releases[$i]}").json"
|
|
else
|
|
# Some files in this release are owned by another user (e.g. www-data).
|
|
# Remove whatever we can, then warn about the rest.
|
|
rm -rf "${releases[$i]}" 2>&1 | grep -v 'Permission denied' || true
|
|
rm -f "${REMOTE_RELEASE_ROOT}/deployments/$(basename "${releases[$i]}").json" 2>/dev/null || true
|
|
blocked_path="$(find "${releases[$i]}" -mindepth 0 \( ! -user "$(id -un)" -o ! -group "$(id -gn)" \) -print 2>/dev/null | head -n 1)"
|
|
|
|
if [[ -n "$blocked_path" ]]; then
|
|
if command -v stat >/dev/null 2>&1; then
|
|
blocked_details="$(stat -c 'owner=%U group=%G mode=%a path=%n' "$blocked_path" 2>/dev/null || printf 'details=unavailable path=%s' "$blocked_path")"
|
|
else
|
|
blocked_details="$(ls -ld "$blocked_path" 2>/dev/null || printf 'details=unavailable path=%s' "$blocked_path")"
|
|
fi
|
|
fi
|
|
|
|
echo "WARNING: Could not fully remove old release ${releases[$i]} (permission denied on some files). ${blocked_details}. Manual cleanup may be needed." >&2
|
|
fi
|
|
prune_count=$(( prune_count - 1 ))
|
|
done
|
|
}
|
|
|
|
[[ -d "$release_path" ]] || die "Release path does not exist: ${release_path}"
|
|
ensure_dir "$REMOTE_SHARED_ROOT" "${REMOTE_RELEASE_ROOT}/deployments"
|
|
ensure_laravel_shared_storage_layout
|
|
build_storage_exclude_args
|
|
prune_storage_excludes "${REMOTE_SHARED_ROOT}/storage"
|
|
ensure_php_runtime_dir "${REMOTE_SHARED_ROOT}/var/php-tmp"
|
|
ensure_php_runtime_dir "${REMOTE_SHARED_ROOT}/var/php-sessions"
|
|
link_shared_paths "$release_path"
|
|
ensure_shared_env_readable
|
|
|
|
previous_release_id="$(current_release_id)"
|
|
|
|
trap bring_app_up EXIT
|
|
|
|
if [[ "$DEPLOY_MODE" == "full-upgrade" ]]; then
|
|
run_remote_hook "full-upgrade pre-hook" "${FULL_UPGRADE_PRE_HOOK:-}"
|
|
fi
|
|
|
|
log_step "Installing Composer dependencies in staged release"
|
|
cd "$release_path"
|
|
"$COMPOSER_BIN" install --no-dev --prefer-dist --optimize-autoloader --no-interaction
|
|
|
|
if [[ "$SKIP_MAINTENANCE" -eq 0 && -f "$current_app_path/artisan" ]]; then
|
|
log_step "Enabling maintenance mode"
|
|
"$PHP_BIN" "$current_app_path/artisan" down --retry=60 || true
|
|
fi
|
|
|
|
log_step "Switching current release to ${RELEASE_ID}"
|
|
ln -sfn "$release_path" "$current_link"
|
|
ln -sfn "$current_link" "$current_app_path"
|
|
|
|
cd "$current_app_path"
|
|
|
|
if [[ "$RUN_REMOTE_MIGRATIONS" -eq 1 ]]; then
|
|
log_step "Running database migrations"
|
|
"$PHP_BIN" artisan migrate --force
|
|
fi
|
|
|
|
log_step "Refreshing caches"
|
|
"$PHP_BIN" artisan view:clear
|
|
"$PHP_BIN" artisan optimize:clear
|
|
"$PHP_BIN" artisan optimize
|
|
"$PHP_BIN" artisan view:cache
|
|
|
|
if [[ "$SKIP_MAINTENANCE" -eq 0 ]]; then
|
|
log_step "Bringing application back online"
|
|
"$PHP_BIN" artisan up
|
|
trap - EXIT
|
|
fi
|
|
|
|
if ! "$PHP_BIN" artisan homepage:warm-guest-cache; then
|
|
log_warn "Homepage guest cache warm failed during deploy."
|
|
fi
|
|
|
|
if ! "$PHP_BIN" artisan posts:warm-trending; then
|
|
log_warn "Post trending cache warm failed during deploy."
|
|
fi
|
|
|
|
log_step "Restarting queue workers"
|
|
"$PHP_BIN" artisan queue:restart || true
|
|
|
|
if [[ "${SKIP_SSR_RESTART:-0}" -eq 0 ]]; then
|
|
log_step "Restarting Inertia SSR server"
|
|
restart_ssr_with_artisan() {
|
|
"$PHP_BIN" artisan inertia:stop-ssr >/dev/null 2>&1 || true
|
|
nohup "$PHP_BIN" artisan inertia:start-ssr >/tmp/skinbase-inertia-ssr.log 2>&1 < /dev/null &
|
|
}
|
|
|
|
if command -v supervisorctl >/dev/null 2>&1; then
|
|
if supervisorctl status skinbase-ssr >/dev/null 2>&1; then
|
|
supervisorctl restart skinbase-ssr || {
|
|
log_warn "supervisorctl restart skinbase-ssr failed — falling back to artisan SSR restart."
|
|
restart_ssr_with_artisan || log_warn "artisan SSR restart failed — SSR server may need a manual restart."
|
|
}
|
|
else
|
|
log_warn "Supervisor program 'skinbase-ssr' not found — falling back to artisan SSR restart."
|
|
restart_ssr_with_artisan || log_warn "artisan SSR restart failed — add deploy/supervisor/skinbase-ssr.conf to /etc/supervisor/conf.d/ and run 'supervisorctl reread && supervisorctl update'."
|
|
fi
|
|
else
|
|
log_warn "supervisorctl not available — falling back to artisan SSR restart."
|
|
restart_ssr_with_artisan || log_warn "artisan SSR restart failed — SSR server may need a manual restart."
|
|
fi
|
|
fi
|
|
|
|
if [[ "$RUN_MEILISEARCH_SETUP" -eq 1 ]]; then
|
|
if [[ -z "${MEILISEARCH_MODELS_CSV:-}" ]]; then
|
|
MEILISEARCH_MODELS_CSV='App\Models\Artwork,App\Models\User,App\Models\Group,App\Models\Post,App\Models\Message'
|
|
fi
|
|
|
|
IFS=',' read -r -a meilisearch_models <<< "$MEILISEARCH_MODELS_CSV"
|
|
|
|
log_step "Importing searchable models into Meilisearch (auto-creates indexes)"
|
|
for model in "${meilisearch_models[@]}"; do
|
|
[[ -n "$model" ]] || continue
|
|
printf ' -> %s\n' "$model"
|
|
"$PHP_BIN" artisan scout:import "$model"
|
|
done
|
|
log_step "Syncing Meilisearch index settings"
|
|
"$PHP_BIN" artisan scout:sync-index-settings
|
|
log_step "Meilisearch setup complete"
|
|
fi
|
|
|
|
if [[ "$DEPLOY_MODE" == "full-upgrade" ]]; then
|
|
run_remote_hook "full-upgrade post-hook" "${FULL_UPGRADE_POST_HOOK:-}"
|
|
fi
|
|
|
|
cat > "${REMOTE_RELEASE_ROOT}/deployments/${RELEASE_ID}.json" <<JSON
|
|
{
|
|
"release_id": "${RELEASE_ID}",
|
|
"deployed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
"remote_folder": "${REMOTE_FOLDER}",
|
|
"deployment_mode": "${DEPLOY_MODE}",
|
|
"release_path": "${release_path}",
|
|
"previous_release_id": "${previous_release_id}",
|
|
"meilisearch_refreshed": ${RUN_MEILISEARCH_SETUP}
|
|
}
|
|
JSON
|
|
|
|
cat > "${REMOTE_RELEASE_ROOT}/current-release.json" <<JSON
|
|
{
|
|
"release_id": "${RELEASE_ID}",
|
|
"deployed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
"remote_folder": "${REMOTE_FOLDER}",
|
|
"release_path": "${release_path}",
|
|
"previous_release_id": "${previous_release_id}",
|
|
"deployment_mode": "${DEPLOY_MODE}",
|
|
"meilisearch_refreshed": ${RUN_MEILISEARCH_SETUP}
|
|
}
|
|
JSON
|
|
|
|
printf '%s\n' "${RELEASE_ID}" > "${REMOTE_RELEASE_ROOT}/current-release.txt"
|
|
prune_old_releases
|
|
EOF
|
|
|
|
log_step "Deployment complete"
|