From e149e2ec423a0e3c666d7e5327df919257e9660a Mon Sep 17 00:00:00 2001 From: Ryota Murakami Date: Sun, 31 May 2026 17:19:21 +0900 Subject: [PATCH] fix(deploy): anchor rsync excludes to transfer root to prevent exit 23 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three artifact-sync / rollback rsyncs used unanchored excludes (--exclude='logs/' --exclude='run/'), which match a directory of that name at ANY depth. rsync protects excluded paths on the receiver from --delete, so deep node_modules dirs named logs/ (e.g. node_modules/@sentry/core/build/esm/logs) shielded stale nested @sentry trees from deletion. rsync could then not remove the now-non-empty parents and aborted with exit 23 before the PM2 restart — the new bundle synced to disk but the old code kept serving. Anchor all three excludes with a leading '/' so they protect ONLY the top-level runtime state (/.env, /logs/, /run/) and let --delete prune stale nested node_modules. Verified on the production host (rsync 3.2.7): the unanchored pattern leaves the nested @sentry tree undeletable ("cannot delete non-empty directory"); the anchored pattern removes it while preserving the top-level logs/ directory. --- .github/workflows/deploy.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6382ce2a6..f57ebb69c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -228,7 +228,9 @@ jobs: return 1 fi - rsync -a --delete --exclude='.env' --exclude='logs/' --exclude='run/' "$rollback_dir"/ "$remote_dir"/ + # Anchored excludes ('/'-prefixed) protect only the top-level .env/logs/run, so + # --delete can still prune stale nested node_modules (see artifact-sync rsync, exit 23). + rsync -a --delete --exclude='/.env' --exclude='/logs/' --exclude='/run/' "$rollback_dir"/ "$remote_dir"/ cd "$remote_dir" if sudo pm2 describe server >/dev/null 2>&1; then sudo pm2 reload ecosystem.config.js --update-env @@ -243,7 +245,9 @@ jobs: capture_rollback_artifacts() { set +e - rsync -a --delete --exclude='.env' --exclude='logs/' --exclude='run/' "$remote_dir"/ "$rollback_dir"/ + # Anchored excludes ('/'-prefixed) protect only the top-level .env/logs/run, so + # --delete can still prune stale nested node_modules (see artifact-sync rsync, exit 23). + rsync -a --delete --exclude='/.env' --exclude='/logs/' --exclude='/run/' "$remote_dir"/ "$rollback_dir"/ rollback_status=$? set -e @@ -337,7 +341,10 @@ jobs: reset_staging_dir || fail_deployment "Failed to clear migration staging directory" tar -xzf "$remote_archive" -C "$staging_dir" || fail_deployment "Failed to re-extract clean deployment artifact" - rsync -a --delete --exclude='.env' --exclude='logs/' --exclude='run/' "$staging_dir"/ "$remote_dir"/ || fail_deployment "Failed to sync deployment artifact" + # Anchor excludes with a leading '/' so they shield ONLY top-level runtime state + # (.env, logs/, run/). Unanchored patterns also matched deep node_modules dirs + # named logs/, protecting stale nested @sentry trees from --delete → rsync exit 23. + rsync -a --delete --exclude='/.env' --exclude='/logs/' --exclude='/run/' "$staging_dir"/ "$remote_dir"/ || fail_deployment "Failed to sync deployment artifact" deployment_applied="true" cd "$remote_dir"