From 82108ebd9774ec198dba6b239616f68e67ad381d Mon Sep 17 00:00:00 2001 From: John Coffey Date: Wed, 26 Aug 2026 10:26:58 -0700 Subject: [PATCH] Stop the deploy script rewriting itself mid-run Moving the deploy script into the repo put it inside the checkout it resets, and bash does not read a script all at once -- it reads as it goes, by byte offset. `git reset --hard` replacing the file underneath a running shell makes it stop wherever it had reached. Silently, and with exit status 0. A three-line demonstration: echo "line A" cat > "$0" <<'NEW' echo "REWRITTEN" NEW echo "line B" prints "line A" and nothing else, and exits 0. In a deploy that means building the image, then stopping before the container is replaced, and reporting success -- so the old container keeps serving while everything says the new one shipped. It only bites when a deploy carries a change to the deploy script itself, which is rare enough to be baffling when it happens and exactly the sort of quiet failure this project keeps paying for. The script now re-execs from a copy outside the tree before touching git, so the file being run cannot change while it runs, and removes the copy on exit. Running it from outside the checkout -- as the host did before this moved into the repo -- skips all of that. The trap is an `if` rather than `[ -n ... ] && trap`, which would leave the not-re-exec'd path resting on errexit ignoring a failed left operand of &&. It does ignore it, but a deploy script is a poor place to depend on knowing that. Verified: with the guard, a script that overwrites itself mid-run completes every later line and cleans up its copy; without it, the lines after the rewrite never run. --- deploy.example.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/deploy.example.sh b/deploy.example.sh index 1b28581..63c223c 100755 --- a/deploy.example.sh +++ b/deploy.example.sh @@ -49,6 +49,27 @@ IMAGE_REPO="${IHASMAIL_IMAGE:-ihasmail}" # How long to wait for the new container to report healthy, in seconds. HEALTH_TIMEOUT="${IHASMAIL_HEALTH_TIMEOUT:-30}" +# --- run from a copy, if this script lives in the checkout it resets --------- +# `git reset --hard` below rewrites the working tree, and this script may be +# part of it. Bash does not read a script all at once -- it reads as it goes, +# by byte offset -- so a file replaced underneath it makes the shell stop +# wherever it had reached. Silently, and with exit status 0: a deploy that +# stopped halfway would report success. Re-exec from a copy outside the tree so +# the file being run cannot change while it runs. +SELF="$(readlink -f "$0")" +APP_REAL="$(readlink -f "$APP" 2>/dev/null || printf '%s' "$APP")" +if [ -z "${IHASMAIL_REEXEC:-}" ] && [ "${SELF#"$APP_REAL"/}" != "$SELF" ]; then + COPY="$(mktemp "${TMPDIR:-/tmp}/ihasmail-deploy.XXXXXX")" + cat "$SELF" > "$COPY" + chmod +x "$COPY" + IHASMAIL_REEXEC=1 exec "$COPY" "$@" +fi +# The copy has served its purpose once we exit; the shell has finished reading +# it by then. +if [ -n "${IHASMAIL_REEXEC:-}" ]; then + trap 'rm -f "$SELF"' EXIT +fi + REF="" ASSUME_YES=0 DRY_RUN=0