fix(deploy): refuse deploy when rsync destination overlaps the source
deploy-to-target.sh rsyncs with --delete to TARGET_DIR=/home/archipelago/archy, which is a symlink to /home/archipelago/Projects/archy. archi-dev-box is this same machine over loopback SSH, so deploying from the main checkout is a no-op (source and destination resolve identically) — but deploying from a git worktree nested under it mirrored that worktree onto the main checkout and deleted everything else: ~1810 tracked files, the deploying worktree itself mid-run, a running dev server, and two concurrent sessions' uncommitted work. Guard compares /etc/machine-id across the SSH hop and, when source and destination are on the same host, refuses if either path contains the other. Identical paths still pass, so normal local deploys are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
afc17c4e43
commit
cf7a603ee5
@ -564,6 +564,41 @@ fi
|
||||
|
||||
# Sync code
|
||||
section_start
|
||||
|
||||
# GUARD (2026-07-31 incident): the rsync below is `--delete`, and TARGET_DIR
|
||||
# (/home/archipelago/archy) is a SYMLINK to /home/archipelago/Projects/archy on
|
||||
# archi-dev-box — which is this same machine over loopback SSH. Deploying from
|
||||
# the main checkout is a harmless no-op (source and destination resolve to the
|
||||
# same directory), but deploying from anywhere else on this host — a git
|
||||
# worktree under .claude/worktrees/, a copy, a subdirectory — makes rsync mirror
|
||||
# that source ONTO the main checkout and delete everything not present in it.
|
||||
# That is exactly what happened: ~1810 tracked files deleted, the deploying
|
||||
# worktree destroyed mid-run, a running dev server killed, and two concurrent
|
||||
# sessions' uncommitted work lost permanently.
|
||||
#
|
||||
# Refuse the deploy when source and destination are on the same host AND either
|
||||
# path contains the other. Identical paths are allowed (the normal local case).
|
||||
_LOCAL_SRC="$(readlink -f "$PROJECT_DIR")"
|
||||
_REMOTE_ID="$(ssh $SSH_OPTS "$TARGET_HOST" 'cat /etc/machine-id 2>/dev/null' 2>/dev/null || true)"
|
||||
_LOCAL_ID="$(cat /etc/machine-id 2>/dev/null || true)"
|
||||
if [ -n "$_REMOTE_ID" ] && [ "$_REMOTE_ID" = "$_LOCAL_ID" ]; then
|
||||
_REMOTE_DST="$(ssh $SSH_OPTS "$TARGET_HOST" "readlink -f '$TARGET_DIR'" 2>/dev/null || true)"
|
||||
if [ -n "$_REMOTE_DST" ] && [ "$_REMOTE_DST" != "$_LOCAL_SRC" ]; then
|
||||
case "$_LOCAL_SRC/" in
|
||||
"$_REMOTE_DST"/*)
|
||||
echo "FATAL: refusing to deploy. Source '$_LOCAL_SRC' is INSIDE the deploy destination '$_REMOTE_DST' on this same host." >&2
|
||||
echo " 'rsync --delete' would delete the destination's contents (including this source) — the 2026-07-31 data-loss incident." >&2
|
||||
echo " Run this script from the main checkout ($_REMOTE_DST), not from a worktree or copy." >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
case "$_REMOTE_DST/" in
|
||||
"$_LOCAL_SRC"/*)
|
||||
echo "FATAL: refusing to deploy. Destination '$_REMOTE_DST' is INSIDE the source '$_LOCAL_SRC' on this same host." >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
progress "Syncing code"
|
||||
rsync -avz --delete \
|
||||
-e "ssh $SSH_OPTS" \
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user