#!/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}" 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:-}" run_local_build=1 run_remote_migrations=1 run_db_sync=0 db_sync_source="" legacy_db_sync_mode=0 force_db_sync=0 skip_maintenance=0 db_sync_confirm_target="${DB_SYNC_CONFIRM_TARGET:-}" db_sync_confirm_phrase="${DB_SYNC_CONFIRM_PHRASE:-}" usage() { cat <<'EOF' Usage: bash sync.sh [options] Options: --skip-build Skip local npm build before rsync. --skip-migrate Skip php artisan migrate on the server. --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. --no-maintenance Skip php artisan down/up during deploy. --help Show this help. Environment overrides: LOCAL_FOLDER, REMOTE_FOLDER, REMOTE_SERVER, PHP_BIN, COMPOSER_BIN, SSH_BIN, RSYNC_BIN, LOCAL_BUILD_COMMAND, DB_SYNC_CONFIRM_TARGET, DB_SYNC_CONFIRM_PHRASE EOF } 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 echo "Refusing DB sync: --confirm-db-sync-target must exactly match $remote_server." >&2 exit 1 fi if [[ "$db_sync_confirm_phrase" != "$expected_phrase" ]]; then echo "Refusing DB sync: --confirm-db-sync-phrase must exactly equal '$expected_phrase'." >&2 exit 1 fi return fi if [[ ! -t 0 ]]; then echo "Refusing DB sync in non-interactive mode without --confirm-db-sync-target \"$remote_server\" and --confirm-db-sync-phrase \"$expected_phrase\"." >&2 exit 1 fi read -r -p "Type the remote server to confirm DB replacement [$remote_server]: " typed_target if [[ "$typed_target" != "$remote_server" ]]; then echo "Refusing DB sync: remote server confirmation did not match." >&2 exit 1 fi read -r -p "Type '$expected_phrase' to continue: " typed_phrase if [[ "$typed_phrase" != "$expected_phrase" ]]; then echo "Refusing DB sync: confirmation phrase did not match." >&2 exit 1 fi } 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" return fi ( cd "$local_folder" npm run build ) } while [[ $# -gt 0 ]]; do case "$1" in --skip-build) run_local_build=0 ;; --skip-migrate) run_remote_migrations=0 ;; --with-db-from=local) run_db_sync=1 db_sync_source="local" ;; --with-db-from=*) echo "Unsupported DB sync source in option: $1" >&2 echo "Only --with-db-from=local is supported." >&2 exit 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#*=}" ;; --no-maintenance) skip_maintenance=1 ;; --help|-h) usage exit 0 ;; *) echo "Unknown option: $1" >&2 usage >&2 exit 1 ;; esac shift done if [[ "$run_db_sync" -eq 1 && "$db_sync_source" != "local" ]]; then echo "Refusing DB sync without an explicit source. Use --with-db-from=local." >&2 exit 1 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 echo "Refusing DB sync: both --confirm-db-sync-target and --confirm-db-sync-phrase are required together when provided." >&2 exit 1 fi if [[ "$legacy_db_sync_mode" -eq 1 && "$force_db_sync" -ne 1 ]]; then echo "Refusing legacy --with-db without --force-db-sync. Prefer --with-db-from=local instead." >&2 exit 1 fi if [[ "$run_db_sync" -eq 1 ]]; then confirm_database_replacement fi if [[ "$run_local_build" -eq 1 ]]; then echo "Building frontend assets locally..." run_frontend_build fi echo "Syncing application files to $remote_server..." "$rsync_bin" -avz \ --delete \ --delete-delay \ --chmod=D755,F644 \ --exclude ".phpintel/" \ --exclude "bootstrap/cache/" \ --exclude ".env" \ --exclude "public/hot" \ --exclude "node_modules" \ --exclude "public/files/" \ --exclude "resources/lang/" \ --exclude "storage/" \ --exclude ".git/" \ --exclude ".cursor/" \ --exclude ".venv/" \ --exclude "/oldSite" \ --exclude "/vendor" \ -e ssh \ "$local_folder/" \ "$remote_server:$remote_folder/" if [[ "$run_db_sync" -eq 1 ]]; then echo "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 echo "Running remote Composer and Artisan steps..." "$ssh_bin" "$remote_server" \ REMOTE_FOLDER="$(printf '%q' "$remote_folder")" \ PHP_BIN="$(printf '%q' "$php_bin")" \ COMPOSER_BIN="$(printf '%q' "$composer_bin")" \ RUN_REMOTE_MIGRATIONS="$run_remote_migrations" \ SKIP_MAINTENANCE="$skip_maintenance" \ 'bash -s' <<'EOF' set -euo pipefail cd "$REMOTE_FOLDER" bring_app_up() { if [[ "$SKIP_MAINTENANCE" -eq 0 ]]; then "$PHP_BIN" artisan up >/dev/null 2>&1 || true fi } trap bring_app_up EXIT if [[ "$SKIP_MAINTENANCE" -eq 0 ]]; then "$PHP_BIN" artisan down --retry=60 || true fi "$COMPOSER_BIN" install --no-dev --prefer-dist --optimize-autoloader --no-interaction if [[ "$RUN_REMOTE_MIGRATIONS" -eq 1 ]]; then "$PHP_BIN" artisan migrate --force fi "$PHP_BIN" artisan optimize:clear "$PHP_BIN" artisan config:cache "$PHP_BIN" artisan view:cache "$PHP_BIN" artisan queue:restart || true if [[ "$SKIP_MAINTENANCE" -eq 0 ]]; then "$PHP_BIN" artisan up trap - EXIT fi EOF echo "Deployment complete."