fix(build-hooks): atomic rename to prevent race with concurrent install reads

scripts/build-hooks.js used fs.copyFileSync (truncate-then-write, non-atomic).
Under --test-concurrency=4, multiple builder invocations raced; a parallel
install.js subprocess could readFileSync between truncate and write and
observe an empty file, then write that emptiness into the install target.
Surfaced as the release-blocking bug-2136-sh-hook-version part 4 failure on
main even though the same SHA passed every install-smoke matrix entry.

Fix: stage outputs to hooks/.dist-staging/ then fs.renameSync into hooks/dist/.
POSIX rename(2) is atomic, so concurrent readers always observe a complete
file. The existing bug-2136 part 4 test locks the post-fix invariant.

Failing run: https://github.com/gsd-build/get-shit-done/actions/runs/25472202941/job/74738276687

Closes #3214

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-06 23:34:29 -04:00
parent 304c1a1302
commit c4f11db5e9
4 changed files with 46 additions and 4 deletions

View File

@@ -0,0 +1,4 @@
---
type: Fixed
---
**Atomic writes in `scripts/build-hooks.js` to fix flaky release CI** — nine test files invoke `build-hooks.js` from their `before()` hooks, and `scripts/run-tests.cjs` runs test files with `--test-concurrency=4`, so multiple builders raced to rewrite the same files in `hooks/dist/`. `fs.copyFileSync(src, dest)` truncates `dest` then writes it; a parallel `bin/install.js` subprocess (spawned by another install test) could `fs.readFileSync` between the truncate and the write and observe an empty file. install.js then wrote that empty content into the install target, so installed `.sh` hooks lacked their `# gsd-hook-version:` header. This surfaced as the release-blocking failure in `tests/bug-2136-sh-hook-version.test.cjs` part 4 even though the same SHA passed on every other Node-22/Node-24 install-smoke matrix run. `build-hooks.js` now stages each output to a sibling `hooks/.dist-staging/` directory (same filesystem as `hooks/dist/`) and uses `fs.renameSync` to swap into place — POSIX `rename(2)` is atomic, so concurrent readers always observe a complete file. The existing `tests/bug-2136-sh-hook-version.test.cjs` part 4 already locks the post-fix invariant. (Failing run: https://github.com/gsd-build/get-shit-done/actions/runs/25472202941/job/74738276687)