Hermetic web builds, one Node version, and parity in CI (attn-6q7b) - #20
Merged
Conversation
The web staging deploy was blocked on 2026-09-03 by two build-infrastructure failures unrelated to product code, and the fix that unblocked it (pinning npm 11 in the deploy job, PR #18) is a workaround. This epic resolves the causes so they stop recurring, before the next release is cut. attn-6q7b Hermetic web builds and one Node version across CI and deploy attn-6q7b.1 Vendor the icon packs and drop the three git dependencies attn-6q7b.2 Run one Node version: align ci.yml with the deploy job attn-6q7b.3 Retire the npm@11 pin from the deploy job (after .1) attn-6q7b.4 Run the native-hosted parity suite in CI (after .2) attn-6q7b.5 Prove it on a fresh clone and write it down (after all) Not one git dependency but three: eyecons, catppuccin-vsc-icons and vscode-icons are all git+ssh:// in the lockfile, and none has a usable registry package (eyecons on npm is a 0.0.1 placeholder; the other two are unpublished), so vendoring is the only hermetic option. Node 22 is a hard floor, not a preference — wrangler 4 in web/ refuses anything older — yet ci.yml still runs 20 and the root engines field still says >=18. This commit carries only the beads export. .beads/issues.jsonl had not been exported since 2026-07-23 (448 issues); the DB now holds 767. The diff is that resync — 199 issues added, 27 updated, none removed — plus the six records above. Implementation lands on this branch behind it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TnncCnVNhggoZ9QLQiqeW
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…3 .4) The staging deploy broke on 2026-09-03 for two reasons that had nothing to do with product code, and the pin that unblocked it was a workaround. This removes the causes. Installs no longer touch GitHub. web/package.json depended on three icon packs as `github:` refs — eyecons, catppuccin-vsc-icons, vscode-icons. The lockfile pinned each repo's commit, but npm still cloned each one and resolved THAT project's floating package.json against the live registry on every install, so an unrelated upstream publish (eslint 10 landing beside plugins capped at 9) could walk npm 10's arborist into a null node. None of the three is usable from npm: `eyecons` there is an unrelated 0.0.1 placeholder and the other two are unpublished. So the GENERATED output is now committed — 2,776 SVGs, 4.5 MB — and the packs are fetched as pinned source tarballs only when someone refreshes them (`npm run refresh:icons`). The build reads committed files and needs no pack at all, which is why `generate:icons` leaves `build`, `build:browser` and `check`. Committing output rather than vendoring sources costs ~1 MB more and takes generation off the build path entirely. Verified byte- identical to what the git-dep build produced at the same pins: every SVG, every pack module and the manifest are unchanged, and only the README differs because it now records each pack's commit and licence. Since generation no longer guards itself, `check:icons` verifies the committed tree is internally consistent — every icon a pack module imports exists, no orphans, manifest loads every pack. It deliberately does not re-run the generator: it has to pass with nothing fetched. One Node version. ci.yml ran all four jobs on 20 while the deploy ran 22 (wrangler 4 refuses older), so CI never exercised the npm the deploy uses — which is precisely why the arborist crash was invisible until a deploy. CI moves to 22, .nvmrc declares it, and web/package.json says engines >=22. The root package.json stays >=18 on purpose: `attnmd` is the end-user CLI wrapper and its consumers should not inherit our build floor. The npm@11 pin is gone from the deploy job, now that the thing it worked around is gone. Parity now gates PRs. scripts/test-hosted-review-e2e.sh only ever ran inside the dispatch-only deploy workflow, which is how an owner-side regression (empty review rail on uncurated shares, #19) survived two merges and a version bump. It runs in ci.yml in local mode — its own relay, Vite and owner — so it needs no deployed environment. Verified it catches the thing it exists for: re-introducing the #19 bug fails that case through every retry while the rest pass. Two reliability fixes found while getting there. `kill_pid` killed the npm wrapper but not the vite child it forks, so a run leaked a server that the next run then silently tested against — it now kills the tree, and refuses to start when a port it needs is already held. And the suite retries in CI only: everything shares one box there, and three local runs failed three different assertions, none reproducible. A deterministic break still fails every attempt, as the #19 revert confirms. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TnncCnVNhggoZ9QLQiqeW
…q7b.5) Three things a contributor now has to know and could not learn from the tree: Node 22 is the floor and why (wrangler 4) — including why the root package.json deliberately stays at 18; the icon packs are committed output with a refresh command rather than dependencies, and adding a `github:` dependency would undo the hermetic install; and the parity suite gates PRs, owns its ports, and retries in CI for a reason that is written down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TnncCnVNhggoZ9QLQiqeW
…-6q7b.5) Both found by actually running the fresh clone the task asks for, and neither was visible from the main checkout, where node_modules already had everything. The dev servers still generated icons. `dev`, `dev:browser` and `dev:browser:shares` each ran `generate:icons` first, so in a fresh clone they died on packs that are no longer installed — and the parity suite starts the hosted app with `dev:browser`, which is how this surfaced. Generation is now reachable only through `refresh:icons`, which is the point: the committed output is what everything else reads. The local relay ran on an unpinned wrangler. Local mode starts the relay from relay/, whose dependencies nothing installed — so `npx` fetched wrangler 4.129 at run time against relay's pinned ^3.80. A suite that downloads a different major of its own server on every run is the exact non-hermeticity this epic removes, so CI installs relay deps (and caches that lockfile too), and the script refuses to start rather than silently reaching for npx. Fresh clone of this branch, nothing preinstalled: npm ci (web) 5.8s, no PUPPETEER_SKIP_DOWNLOAD needed npm run check:icons 2776 svgs, 5 pack modules npm run check 1635 files, 0 errors npm run build ok npm run build:browser ok npm ci (relay) ok cargo build --locked ok parity suite 3 passed, 1 flaky, suite passed The PUPPETEER_SKIP_DOWNLOAD workaround that fresh worktrees used to need is retired with the git dependencies that caused it; the memory is updated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TnncCnVNhggoZ9QLQiqeW
Five planned tasks closed. attn-6q7b.6 stays open: the parity suite's tier case is timing-sensitive and, on a loaded runner, can lose all three retries together — seen once on a staging deploy, with the same commit then passing 4/4 twice. Worth fixing properly rather than leaning on retries, but it is not a build-hermeticity problem, so it does not gate this work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TnncCnVNhggoZ9QLQiqeW
angusbezzina
marked this pull request as ready for review
September 3, 2026 23:55
attn-6q7b.2 claimed `npm ci` on Node 20 would fail fast with an engines
error. It did not: `engines` in package.json only warns, so Node 20 would
install anyway and die later inside wrangler with a message that does not
name the cause. engine-strict makes the declaration real.
npm error code EBADENGINE
npm error notsup Required: {"node":">=22"}
npm error notsup Actual: {"node":"v20.20.2","npm":"11.14.1"}
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TnncCnVNhggoZ9QLQiqeW
Everything else in this repo is driven through `task`, so `task refresh:icons` is what someone reaches for after changing a pin. It did not exist — the commands were npm-only and undiscoverable from `task --list`. Verified from a cold cache: refetching all three packs at the current pins and regenerating produces no diff at all, which is the property that makes the committed output trustworthy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015TnncCnVNhggoZ9QLQiqeW
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.
Resolves the build-infrastructure failures that blocked the staging deploy on 2026-09-03. PR #18 unblocked it by pinning npm 11; this removes the causes and retires the pin.
Epic
attn-6q7b— 5 of 6 tasks closed;attn-6q7b.6is a tracked follow-up that does not gate this.1. Installs no longer touch GitHub (
attn-6q7b.1)web/package.jsondepended on three icon packs asgithub:refs —eyecons,catppuccin-vsc-icons,vscode-icons. The lockfile pinned each commit, but npm still cloned each repo and resolved that project's floatingpackage.jsonagainst the live registry on every install. That is how an unrelated upstream publish (eslint 10 beside plugins capped at 9) walked npm 10's arborist intoCannot read properties of null (reading 'edgesOut')with no change in this repo.None of the three is usable from npm —
eyeconsthere is an unrelated0.0.1placeholder, the other two are unpublished — so the generated output is committed (2,776 SVGs, 4.5 MB) and the packs are fetched as pinned source tarballs only bynpm run refresh:icons. Generation therefore leavesbuild,build:browser,check, and thedevservers: nothing in the normal path needs a pack.Committing output rather than vendoring sources costs ~1 MB more and takes generation off the build path entirely. Verified byte-identical to the git-dep build at the same pins — every SVG, every pack module, the manifest; only the README differs, because it now records each pack's commit and licence.
npm run check:iconsreplaces the guarantee generation used to give: every icon a pack module imports exists, no orphans, the manifest loads every pack. It never re-runs the generator — it must pass with nothing fetched.2. One Node version (
attn-6q7b.2)ci.ymlran all four jobs on Node 20 while the deploy ran 22 (wrangler 4 refuses older), so CI never exercised the npm the deploy uses — precisely why the crash was invisible until a deploy. CI moves to 22,.nvmrcpins it,web/package.jsondeclaresengines.node >=22. The rootpackage.jsonstays>=18deliberately:attnmdis the end-user CLI wrapper and its consumers should not inherit our build floor.3. The npm pin is gone (
attn-6q7b.3)Removed from the deploy job, now that the thing it worked around is gone.
4. Parity gates PRs (
attn-6q7b.4)scripts/test-hosted-review-e2e.shran only inside the dispatch-only deploy workflow — which is how a regression that left the owner's review rail empty on uncurated shares survived two merges and a version bump (#19). It now runs inci.ymlin local mode (its own relay, Vite and owner), needing no deployed environment.Verified it catches what it exists for: re-introducing the #19 bug fails that case through every retry while the rest pass.
Three reliability fixes found on the way:
kill_pidkilled thenpmwrapper but not thevitechild it forks, so a run leaked a server the next run silently tested against. It now kills the tree, and refuses to start if a port it needs is held.relay/with nothing installed, sonpxfetched wrangler 4.129 against relay's pinned ^3.80 — the same non-hermeticity, inside the test harness. CI installs relay deps; the script refuses rather than reaching for npx.Verification
Fresh clone, nothing preinstalled:
npm ci(web)PUPPETEER_SKIP_DOWNLOADneedednpm run check:iconsnpm run checknpm run build/build:browsernpm ci(relay)cargo build --lockedgrep -c 'git+' web/package-lock.json→ 033817511711)33818979937, 4/4 parity)Known follow-up:
attn-6q7b.6The tier case (
v3 browser tiers…) is timing-sensitive. One staging-deploy run failed it 3/3 ontoolbarCount: 0; the same commit then passed 4/4 twice — from a laptop against that deployment, and on a re-dispatched deploy. Retries run back-to-back, so sustained load on a runner defeats them. The issue carries the three observed failure modes and a likely fix (an explicit readiness signal instead of relying on the first assertion as a barrier). Not a hermeticity problem, so it does not gate this.Note on the diff
.beads/issues.jsonlhad not been exported since 2026-07-23 (448 issues; the DB holds 768). Part of the diff is that resync — none removed — plus this epic's records. The 2,776 committed SVGs are the rest.🤖 Generated with Claude Code
https://claude.ai/code/session_015TnncCnVNhggoZ9QLQiqeW