From 6d42110a919f71358b2f9dc5df3c153a26ffc41a Mon Sep 17 00:00:00 2001 From: cproudlock Date: Sun, 30 Aug 2026 09:53:04 -0400 Subject: [PATCH] Stop the promote script from deleting itself mid-run promote-to-main.sh checks out main. The tooling lives only on the site branch, so that checkout removes tools/promote-to-main.sh while the script is still running - and bash reads a script lazily, by file offset, so it would carry on executing whatever bytes happen to land there. While manipulating branches. Not theoretical: main does not have tools/ right now, so the first real use would have hit it. Both versions now copy themselves to a temp file and re-exec from there. The bash copy unlinks itself immediately, since the open handle keeps working. The Windows copy has the same hazard for the same reason - cmd.exe re-seeks the batch file after every command - and two extra traps worth naming: the copy runs from %TEMP%, so the repository root has to travel with it rather than being derived from %~dp0, and %errorlevel% inside a parenthesised block is expanded when the block is PARSED, before the call it is meant to report on. Scope: product --- tools/promote-to-main.cmd | 25 ++++++++++++++++++++++++- tools/promote-to-main.sh | 21 +++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/tools/promote-to-main.cmd b/tools/promote-to-main.cmd index b141020d..7f96abfd 100644 --- a/tools/promote-to-main.cmd +++ b/tools/promote-to-main.cmd @@ -17,7 +17,30 @@ rem the commits keeps the decision explicit and per change. rem rem It does NOT push. Read what it prints, then run tools\git-push.cmd main. -cd /d "%~dp0.." +rem RUN FROM A COPY OF OURSELF. +rem +rem This checks out main. The tooling may live only on the site branch, in which +rem case that checkout DELETES this file while cmd.exe is still reading it - and +rem cmd.exe reads a batch file line by line, by file offset, re-seeking after +rem every command. It would carry on reading whatever bytes are at that offset +rem afterwards, while manipulating branches. The bash version has the same +rem hazard and the same fix. +if not "%PROMOTE_SELFCOPY%"=="1" ( + rem The copy runs from %TEMP%, where %~dp0 is the temp directory and not the + rem checkout, so the repository root has to travel with it. + set "PROMOTE_REPO=%~dp0.." + set "PROMOTE_SELFCOPY=1" + copy /y "%~f0" "%TEMP%\promote-to-main-copy.cmd" >nul + call "%TEMP%\promote-to-main-copy.cmd" %* + rem Delayed expansion: a plain %errorlevel% here is expanded when the block is + rem parsed, which is BEFORE the call above has run, so it would always report + rem the code from whatever ran previously. + set "EXITCODE=!errorlevel!" + del /q "%TEMP%\promote-to-main-copy.cmd" >nul 2>&1 + exit /b !EXITCODE! +) + +cd /d "%PROMOTE_REPO%" set "DRYRUN=0" if "%~1"=="--dry-run" ( diff --git a/tools/promote-to-main.sh b/tools/promote-to-main.sh index aef79a66..885ed73c 100755 --- a/tools/promote-to-main.sh +++ b/tools/promote-to-main.sh @@ -26,6 +26,27 @@ # main yourself. set -euo pipefail +# RUN FROM A COPY OF OURSELF. +# +# This script checks out main. The tooling may live only on the site branch, in +# which case that checkout DELETES this file while bash is still reading it - +# and bash reads a script lazily, by offset, so it would carry on executing +# whatever bytes happen to be at that position afterwards. While manipulating +# branches. This is not theoretical: main did not have tools/ when this was +# written, so the very first real use would have hit it. +# +# Copy to a temp file, re-exec from there, and delete the copy immediately. On +# Unix the open file handle keeps working after the unlink, so the running +# script survives whatever git does to the tree. +if [ "${PROMOTE_SELFCOPY:-}" != "1" ]; then + SELF="$(mktemp)" + cat "$0" > "$SELF" + chmod 700 "$SELF" + export PROMOTE_SELFCOPY=1 + exec bash "$SELF" "$@" +fi +rm -f "$0" 2>/dev/null || true + cd "$(git rev-parse --show-toplevel)" DRYRUN=0