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
(cherry picked from commit 6d42110a91)
This commit is contained in:
cproudlock
2026-08-30 09:53:04 -04:00
parent cd86c27969
commit 03804bb996
2 changed files with 45 additions and 1 deletions

View File

@@ -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" (

View File

@@ -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