Files
shopdb-flask/tools/promote-to-main.cmd
cproudlock 03804bb996 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)
2026-08-30 11:12:09 -04:00

178 lines
5.5 KiB
Batchfile

@echo off
setlocal enabledelayedexpansion
rem Take specific commits from wj to main, from Windows.
rem
rem tools\promote-to-main.cmd <commit> [<commit> ...]
rem tools\promote-to-main.cmd --dry-run <commit> ...
rem
rem Commits are applied in the order given, so give them oldest first.
rem
rem WHY CHERRY-PICK AND NOT A MERGE
rem
rem wj holds this site's work. Some of it is product - the rest of the world
rem should have it - and some belongs to this site alone. Merging the branch
rem takes both; that is how a site branch quietly becomes the product. Naming
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.
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" (
set "DRYRUN=1"
shift
)
if "%~1"=="" (
echo usage: tools\promote-to-main.cmd [--dry-run] ^<commit^> [^<commit^> ...]
echo.
echo Give commits oldest first. Find them with:
echo git log --oneline main..wj
exit /b 1
)
rem The ADR-015 site-literal check is a shell script, and it is the one gate
rem that makes a site branch survivable: it catches a hostname or share path
rem riding along in a commit that looked generic. Running the promotion without
rem it would defeat the point, so this refuses rather than skipping it quietly.
where bash >nul 2>&1
if errorlevel 1 (
echo ERROR: bash was not found on PATH.
echo.
echo The naming/style gate ^(scripts\check-naming-and-style.sh^) needs it, and
echo that gate is what stops a site-specific literal reaching main. Promoting
echo without it is not safe.
echo.
echo Git for Windows ships bash. Add its usr\bin to PATH, or run this from
echo Git Bash using tools/promote-to-main.sh instead.
exit /b 1
)
for /f "delims=" %%b in ('git rev-parse --abbrev-ref HEAD') do set "STARTING_BRANCH=%%b"
for /f "delims=" %%s in ('git status --porcelain') do (
echo Working tree is not clean:
git status --short
echo.
echo Cherry-picking onto main from here would mix uncommitted work into the
echo promotion, or fail halfway and leave you on main with a dirty tree.
exit /b 1
)
rem Resolve and describe first, so a typo is caught before anything moves.
echo About to promote onto main, in this order:
for %%r in (%*) do (
git rev-parse --verify --quiet "%%r^^{commit}" >nul
if errorlevel 1 (
echo ERROR: not a commit: %%r
exit /b 1
)
for /f "delims=" %%l in ('git log --oneline -1 %%r') do echo %%l
)
echo.
if "%DRYRUN%"=="1" (
echo --dry-run: files each commit touches, so you can see what reaches main.
for %%r in (%*) do (
echo.
for /f "delims=" %%l in ('git log --oneline -1 %%r') do echo --- %%l ---
git show --stat --oneline %%r
)
echo.
echo Nothing was changed.
exit /b 0
)
git checkout main
if errorlevel 1 goto :restore
for /f "delims=" %%h in ('git rev-parse HEAD') do set "MAIN_BEFORE=%%h"
for %%r in (%*) do (
echo.
for /f "delims=" %%l in ('git log --oneline -1 %%r') do echo ==^> picking %%l
git cherry-pick -x %%r
if errorlevel 1 (
echo.
echo That commit does not apply cleanly to main.
echo Resolve it by hand if it is worth having:
echo git status
echo git cherry-pick --abort
goto :restore
)
)
echo.
echo ==^> main gates
echo --- naming, style, site literals ^(ADR-015^) ---
bash scripts/check-naming-and-style.sh
if errorlevel 1 goto :restore
echo --- generated project map ---
venv\Scripts\python scripts\gen_project_map.py --check >nul 2>&1
if errorlevel 1 (
echo Project map is stale; regenerating.
venv\Scripts\python scripts\gen_project_map.py
git add docs/PROJECT-MAP.md
git commit -q -m "Regenerate the project map after promotion"
)
echo --- python suite ---
venv\Scripts\python -m pytest -q
if errorlevel 1 goto :restore
echo --- frontend ---
pushd frontend
call npm test --silent
set "FRONTEND_STATUS=!errorlevel!"
popd
if not "!FRONTEND_STATUS!"=="0" goto :restore
echo.
echo main is now:
git log --oneline %MAIN_BEFORE%..HEAD
echo.
echo Gates passed. NOT pushed - review the above, then:
echo tools\git-push.cmd main
echo.
echo You are on main. Return to your branch with:
echo git checkout %STARTING_BRANCH%
exit /b 0
:restore
echo.
echo Promotion did NOT complete.
git rev-parse --verify --quiet CHERRY_PICK_HEAD >nul 2>&1
if not errorlevel 1 (
echo Aborting the in-progress cherry-pick ...
git cherry-pick --abort
)
git checkout "%STARTING_BRANCH%" >nul 2>&1
echo Back on %STARTING_BRANCH%. main is unchanged and nothing was pushed.
exit /b 1