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
178 lines
5.5 KiB
Batchfile
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
|