From 5259e027c6203f684f8efad2fb3ef51be30e841a Mon Sep 17 00:00:00 2001 From: "Timur Kh." Date: Mon, 19 Oct 2026 15:11:01 +0300 Subject: [PATCH] deploy: re-exec from a copy, so the script cannot rewrite itself mid-run deploy.sh lives in the repo it updates. `git reset --hard` therefore replaced the file bash was reading; bash keeps a byte offset into the script and resumed at that offset in the new file, skipping whatever the diff had shifted. That is how the previous commit's renewal hook ended up present on the server and never executed: `grep` found it in deploy.sh, the hook directory was empty. Stage one now updates the checkout and re-execs a copy under /tmp, which no later `git reset` can touch; stage two does the work and removes the copy. Co-Authored-By: Claude Opus 5 --- deploy/deploy.sh | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 73dfd8b..5954b87 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -26,11 +26,31 @@ warn() { printf '\033[1;33m !! %s\033[0m\n' "$*"; } cd "$REPO_DIR" # ---- 1. code --------------------------------------------------------------- -say "Fetching $BRANCH from $(git remote get-url origin)" -git fetch --prune origin -git checkout -q "$BRANCH" -git reset --hard "origin/$BRANCH" -echo "now at $(git log --oneline -1)" +# This script lives in the repo it updates, so `git reset --hard` rewrites the +# file bash is in the middle of reading. Bash reads a script incrementally and +# keeps a byte offset, so after the reset it resumes at that offset in the *new* +# file — silently skipping or garbling whatever the diff shifted. (That is not +# hypothetical: it is how the certbot renewal hook came to be committed, present +# on disk, and never executed.) +# +# So: stage one updates the checkout and re-execs a *copy* outside the repo, +# which nothing can rewrite underneath it. Stage two does the actual work. +if [[ ${KADENCE_DEPLOY_STAGE:-} != run ]]; then + say "Fetching $BRANCH from $(git remote get-url origin)" + git fetch --prune origin + git checkout -q "$BRANCH" + git reset --hard "origin/$BRANCH" + echo "now at $(git log --oneline -1)" + + staged=$(mktemp /tmp/kadence-deploy.XXXXXX.sh) + cat "$REPO_DIR/deploy/deploy.sh" > "$staged" + chmod +x "$staged" + export KADENCE_DEPLOY_STAGE=run KADENCE_DEPLOY_STAGED="$staged" + exec "$staged" "$@" +fi + +# Stage two: started from the copy, so clean it up on the way out. +trap '[[ -n ${KADENCE_DEPLOY_STAGED:-} ]] && rm -f "$KADENCE_DEPLOY_STAGED"' EXIT # ---- 2. secrets ------------------------------------------------------------ setkv() {