fix(build): one builder per package, so the required gates stop racing - #464
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
Vercel preview readyPreview: https://pretable-s4l0sopmb-cacheplane.vercel.app Updated automatically by the |
`@pretable/core` and `@pretable-internal/renderer-dom` are topological
siblings -- both depend on `grid-core`, neither on the other -- so
`pnpm -r` starts them at the same instant. Both scripts then rebuilt
packages pnpm had already built: core ran `pnpm --filter grid-core
build`, renderer-dom ran three such prefixes. A cold
`pnpm -r --filter './packages/*' build` shows it directly:
20:33:43 grid-core build$ tsc -b
20:33:45 grid-core build: Done <- pnpm already built it
20:33:45 core build$ pnpm --filter grid-core build && tsup
20:33:45 renderer-dom build$ pnpm --filter text-core build && ...
20:33:46 renderer-dom build: > layout-core > tsc -b
Two `tsc -b` processes emitting one `dist/` is unsafe by construction:
`writeFileSync` truncates before it writes, so a concurrent reader can
see `layout-core/dist/index.d.ts` empty and report "is not a module".
Measured directly, two cold `tsc -b` runs launched together emitted that
file twice in 2 of 3 runs and once in the third -- the nondeterminism is
the race. It failed the required `typecheck` gate intermittently, which
is the corrosive kind of flake: it teaches people to re-run until green.
The graph was declared twice -- in `dependencies`, and again by hand
inside the script strings. pnpm already orders `-r` runs topologically,
so the second copy bought nothing and was what created the overlap.
This deletes it. Each package builds only itself; the root `test` and
`typecheck` scripts build the packages once, up front, before running
either. Apps keep their standalone `prepare:deps` but derive the closure
with pnpm's `<pkg>^...` filter instead of listing siblings, so it stays
correct when the graph changes. The two CI jobs that build only the
published packages now ask for their dependency closures (`<pkg>...`).
After the fix the same cold build shows every package built exactly once,
by one process, with no nested `pnpm --filter` at all.
`scripts/__tests__/workspace-scripts-own-one-package.test.mjs` holds the
invariant. It discovers the workspace rather than listing it, and it is
mutation-tested: re-adding the exact prefix this commit removes fails it,
and so does a filter naming a package that does not exist. Its first
draft passed both mutations -- it skipped any argument containing "/",
which is every scoped package name -- so the blind spot is now called out
in the code.
Verified cold, on this branch: typecheck, test (react 1216, website 554,
row-model 326, renderer-dom 127, grid-core 124, bench 155), build
including the Next site, lint, format, api:check, lint:packaging,
typecheck:public, typecheck:performance, the rewritten CI build command,
and a standalone `prepare:deps` from an empty tree -- all exit 0.
No changeset: this changes how the workspace builds, not what any
published package does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
blove
force-pushed
the
blove/fix-workspace-build-race
branch
from
August 17, 2026 04:18
83a4485 to
a4b8c95
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The workspace graph was declared twice: once in
dependencies, and again by hand inside the script strings. pnpm already orders-rruns topologically, so the second copy bought nothing — and it let two processes build the same package at the same time.The overlap, as measured
At the base I measured (
fca7a789),@pretable/coreand@pretable-internal/renderer-domwere topological siblings — both depended ongrid-core, neither on the other — sopnpm -rstarted them at the same instant. A coldpnpm -r --filter './packages/*' buildshows it directly:Two
tsc -bprocesses emitting onedist/is unsafe by construction:writeFileSynctruncates before it writes, so a concurrent reader can seelayout-core/dist/index.d.tsempty and report "is not a module".I could not catch the corrupted read on this machine (0 failures in 10 cold races — APFS writes a 1 KB
.d.tstoo fast). So I measured the precondition instead. Two coldtsc -bruns launched together emitted that file twice in 2 of 3 runs, once in the third. That split is the race: whether the second process's up-to-date check lands before or after the first process's write is a coin flip, and on a 2-core runner with a cold page cache the window is wider than here.Correction, after rebasing
That particular sibling pair no longer exists. #448 landed while this branch was open and made
renderer-domdepend on@pretable/core, which linearizes the tail of the graph:Levels 2–4 are now single-occupancy, and the two packages that share level 1 (
grid-core,bench-runner) have disjoint project references. So on today'smainthe race is dormant, not fixed — it was real and measured at the commit I measured it on, and what made it possible is still in the tree. It comes back the moment a second package joins any level, which is an ordinary thing to do and carries no warning that it re-arms a flake in a required gate.I'd rather say that plainly than leave the PR claiming a live failure.
The change
Each package builds only itself. The root
testandtypecheckbuild the packages once, up front, before running either.Apps keep their standalone
prepare:deps—pnpm --filter @pretable/app-bench devstill works from an empty tree — but derive the closure with pnpm's<pkg>^...filter instead of listing siblings, so it stays correct when the graph changes. (It already has: this branch needed no edit to track #448's rewiring, while the hand-written chain it replaced named@pretable/reactand would have kept working only by luck.) The two CI jobs that build only the published packages now ask for their dependency closures (<pkg>...).Wall clock locally is unchanged (8s both ways) — the redundant builds were mostly up-to-date no-ops here. This is a correctness fix, not a speed one.
The guard
scripts/__tests__/workspace-scripts-own-one-package.test.mjsholds the invariant: a workspace script builds its own package and nothing else. It discovers the workspace rather than listing it, so a package added tomorrow is covered with no edit.It is mutation-tested. Re-adding the exact prefix this PR removes fails it; so does a filter naming a package that does not exist; and the sanctioned derived form still passes. Its first draft passed both mutations — it skipped any
--filterargument containing/, which is every scoped package name, so it was checking nothing. That blind spot is now called out in the code so the next person doesn't reintroduce it.Verification
Cold, rebased onto
cf3ff2b4:typecheck,test(react 1227, website 556, bench 156, renderer-dom 127),buildincluding the Next site,lint,format,api:check,lint:packaging,typecheck:public,typecheck:performance, the rewritten CI build command, and a standaloneprepare:depsfrom an empty tree — all exit 0.One note for reviewers pulling this branch: #448 added a workspace dependency, so a
node_modulespredating it will failrenderer-dom's build withCannot find module '@pretable/core'. That is a stale install, not this change —pnpm installclears it.No changeset: this changes how the workspace builds, not what any published package does.
🤖 Generated with Claude Code