Skip to content

ci: run the bootstrap from pwsh and git bash - #307

Merged
eirikb merged 3 commits into
mainfrom
feature/eirikb/cold-bootstrap-ci
Jul 29, 2026
Merged

ci: run the bootstrap from pwsh and git bash#307
eirikb merged 3 commits into
mainfrom
feature/eirikb/cold-bootstrap-ci

Conversation

@eirikb

@eirikb eirikb commented Jul 29, 2026

Copy link
Copy Markdown
Owner

gg.cmd always ends up executing through cmd.exe on Windows, but it inherits the PATH and PSModulePath of whatever shell you started it from. Two bugs shipped through that seam this week:

  • crash when using git bash on windows #291 — from Git Bash, C:\Program Files\Git\usr\bin sits ahead of System32, so a bare tar resolved to Git's GNU tar instead of bsdtar. Unpacking died.
  • crash on powershell 7 #292 — from PowerShell 7, the child Windows PowerShell inherits pwsh's PSModulePath and loads its Core-only Microsoft.PowerShell.Utility. Binary cmdlets kept working; Get-FileHash vanished.

Neither was caught, and not for the reason I first assumed. The cold path was already tested — has_secret is true on same-repo PRs, so "Pre-populate cache with stage4" is skipped there. The actual gap is the parent shell:

Job Shell Invocation Branch exercised
test-OS1 bash sh gg.cmd the shell half — sh is explicit, cmd.exe never sees it
test-batch cmd .\gg.cmd the batch half, but parent is plain cmd.exe

So the batch half was covered and Git Bash was covered, but never Git Bash as parent of the batch half. Both bugs live in that cell.

What this adds

test-parent-shells: one job per parent shell on windows-2022, running .\gg.cmd from pwsh and ./gg.cmd from Git Bash — deliberately not sh gg.cmd, since MSYS dispatching a .cmd to cmd.exe by extension is the whole point.

Each leg runs twice (cold, then the cached branch at stage1.bat:15, which nothing else covered) and asserts what node printed, not just the exit code — stage2.ps1 runs Start-Process -ErrorAction SilentlyContinue and its catch defaults $exitCode = 0, so gg can exit 0 having launched nothing.

The job always runs. When the Azure secret is absent it seeds stage4 from the artifact like the sibling jobs, so fork PRs still cover stage1's unpack — where #291 lives — with only the blob download dropping out. The gatekeeper is strict; no skip exemption.

Verified

Dispatched on real runners: 178 jobs green, 0 failures. Both legs cold-bootstrapped (node not found in cache → download → v26.5.0), then ran warm. The Git Bash leg's own PATH dump confirms the hazard is live there:

PATH: …;C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\usr\bin;…;C:\Windows\system32;…

actionlint reports the same findings as main (7 pre-existing shellcheck items) — nothing new.

Honest limits

  • The pwsh leg is a real regression test — revert the .NET SHA512 to Get-FileHash and it goes red deterministically. The bash leg is weaker: it proves Git's GNU tar is first on PATH, but with the System32 pin in place GNU tar is never invoked, so reverting the pin might leave it green. It guards the environment more than that specific bug.
  • The fork path is unexercised by these runs — a same-repo dispatch has the secret, so the seeding steps skip. Only a real fork PR exercises them.
  • The assertions are unproven as detectors — green shows they don't false-positive; it doesn't show they'd catch the swallowed-exit-code case.
  • Not done, worth a follow-up: timeout-minutes (the job hits the Azure blob cold every run, and stage2.ps1:92 has no -TimeoutSec or retry), and preconditions asserting the job still reproduces its own premise — Git's tar still shadowing System32, pwsh still ≥ 7.

eirikb added 3 commits July 29, 2026 11:21
gg.cmd always ends up in cmd.exe, but it inherits the PATH and PSModulePath of
whatever shell you started it from, and that is where #291 and #292 lived - Git's
GNU tar sitting ahead of the System32 one, and PowerShell 7 shadowing the module
Get-FileHash is defined in. Both only bite on the first run, and both shipped.

Nothing in CI was ever that parent. test-batch starts from cmd, and test-OS1 says
`sh gg.cmd`, which takes the shell branch and never touches the bat one. So this
adds a job per parent shell that wipes the cache and runs ./gg.cmd from git bash
and .\gg.cmd from pwsh - no `sh`, since handing a .cmd to cmd.exe is the point.

Only runs where the secret is, or there is no stage4 blob to bootstrap from, so
the gatekeeper takes skipped for this one job.

The pwsh half is a real regression test - that mechanism reproduces every time.
The git bash half is weaker: GNU tar coped on the VM I reproduced #291 on, so it
guards the environment rather than that exact bug.
Every other test job runs its command twice, and this one did not - so the
cached branch at the top of stage1.bat, which is what every run after the first
takes, was never reached. Two lines.

pwsh does not stop on a failing native command, only on cmdlet errors, so the
cold run gets its exit code checked before the warm one can paper over it. Bash
is already -e.

Also fixes the comment on the wipe: %UserProfile% and $HOME both land on
C:\Users\runneradmin on a runner, and the VM is fresh anyway, so it is belt and
braces rather than the two different places I claimed. And bash does not hand
the .cmd to cmd.exe itself - MSYS does that by extension.
Two holes, both found by review rather than by the job failing.

The job asserted an exit code, and stage2.ps1 hands back 0 when it never got a
process at all - Start-Process runs with -ErrorAction SilentlyContinue and the
catch below it defaults $exitCode to 0. So gg could have launched nothing and
the step would still pass. Both legs now check what node printed, the way the
#289 test already does.

And the job skipped itself without the Azure secret, with the gatekeeper told
that skipping was fine. Fork PRs therefore got no parent-shell coverage at all
and a green tick. It now seeds stage4 from the artifact like the other jobs do,
so it always runs: stage1 still unpacks, which is where #291 lives, and only the
blob download drops out. The gatekeeper is strict again.

The cache wipe went too. It deleted a directory that cannot exist on a fresh VM,
and claimed %UserProfile% and $HOME were different places, which they are not on
a runner. What actually matters is that stage2.ps1 is absent, since that is what
stage1.bat keys off - so remove that and assert it, and the job fails loudly if
anything ever seeds a full cache.
@eirikb
eirikb enabled auto-merge (squash) July 29, 2026 11:52
@eirikb
eirikb merged commit 3940ae2 into main Jul 29, 2026
358 checks passed
@eirikb
eirikb deleted the feature/eirikb/cold-bootstrap-ci branch July 29, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant