Skip to content

ci(ngit): build the stage-1 portal from the pinned toolchain, not floating main/node - #477

Merged
Amperstrand merged 2 commits into
mainfrom
fix/stage1-portal-pin
Sep 22, 2026
Merged

Amperstrand merged 2 commits into
mainfrom
fix/stage1-portal-pin

Conversation

@Amperstrand

@Amperstrand Amperstrand commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

What

Stage 1's build-portal is still red on every main push. #514 landed the PORTAL_REF half of this fix; this branch is now exactly the remaining half — the toolchain pin — rebased on current main (merge of d622688b by a kind reviewer; delta vs main is only the node changes below).

Live failure this fixes (kind-9841, d622688b, today)

  • setup-node resolves node 20.20.2 from the runner cache (main still pins node-version: '20', line 333)
  • portal-build.sh enforces the manifest toolchain (node 22.17.0 / npm 10.9.2)
  • Build captive portal assets fails its gate in 1.35 s — a pre-build refusal, not a build failure

Change

  • node-version: '22.17.0' (the portal's declared toolchain, same as the GitHub twin) with the twin's comment
  • the twin's Verify pinned npm version step

Evidence (unchanged from the original run, still applicable)

  • PORTAL_REF=main + system node → gate refusals reproduced locally (rc=1 each)
  • pinned toolchain (manifest tarball, sha256-verified) → full portal build from the pinned commit, rc=0
  • make go-battery 16 modules green; js-schema-lint PASS; build-purity 3/3

Together with #519 (repro-check's audit epoch), this clears both repo-side reds on main. Both exist because their PRs merged without runner evidence — #473 remains the forcing function for that class.

@Amperstrand

Copy link
Copy Markdown
Collaborator Author

Technical review (evidence-gathering pass, not an approval).

What it does. Aligns the stage-1 build-portal job with the reproducibility gates #466 added to packaging/portal-build.sh: node pinned to 22.17.0 (the manifest's node), an explicit npm-vs-build-inputs.json verification step, and — most importantly — the PORTAL_REF=main override is dropped, because portal-build.sh has refused floating refs since #466 and defaults to the manifest pin on its own.

The finding that makes this urgent rather than cosmetic: the dropped override means stage 1 has been red on every push since #466 landed — which is exactly why no tollgate-build/aac36282/* records exist for the alpha3 tag (the job fails before uploading). So this PR doesn't just polish determinism; it unbreaks stage 1 for every future push, including alpha4's. That also sharpens the sequencing note on #469: #477 must merge before the alpha4 tag is cut, or stage 1 fails again at the tag.

Verified on this head (a65f1247): the modified workflow parses as valid YAML; the new npm step's jq usage matches the file's existing convention (the cross-compile, upload and record steps already run jq with no install step anywhere in the file — the runner provides it); portal-build.sh's own node/npm enforcement and floating-ref rejection are the gates this change aligns with, so the workflow and the script now enforce the same toolchain twice — deliberately, with the earlier, clearer failure in the workflow. Changelog entry present and accurate (including the "red since #466" framing). Two commits, clean; base fresh.

Two optional notes, neither blocking. (1) test.yml (both twins) still floats node-version: '22' for the schema-lint job — CI-check only, no artifact bytes, but the same pin discipline would close it. (2) Together with #474 (pinned UPX), this PR is what makes #469's "first release reproducible byte-for-byte" sentence true — landing both before the alpha4 tag keeps the release notes honest.

Disposition: land — ideally ahead of the alpha4 cut; it is stage-1-unblocking, CI-only, and behavior-preserving on the packaging path.

@felixfelix-bot

Copy link
Copy Markdown
Contributor

Verified live on the ngit lane: your stage-1 portal fix is the second half of the stage-1 red state. I merged it with my epoch fix into branch ci/stage1-verify-both (18cab0c9) and the full build-package-binaries.yml ran GREEN on our DQ05 coordinator — including your build-portal job building from the manifest pin 4f74a6dd… with node 22.17.0/npm 10.9.2 (9842 7a65aa1a…, all three jobs success). Thanks for having already fixed the portal half.

@Amperstrand

Copy link
Copy Markdown
Collaborator Author

Technical review (evidence-gathering pass, not an approval).

What it does. Un-reds stage 1's build-portal job by removing its two deliberate violations of the #466/#383 gates: the PORTAL_REF="${PORTAL_REF:-main}" override is dropped (so portal-build.sh defaults to the manifest SHA, as the GitHub twin already does), and setup-node moves from floating '20' to the pinned 22.17.0 with the twin's npm-verification step copied alongside. 23 lines in one workflow + the CHANGELOG entry — nothing else.

Verified on this head (a65f1247, own worktree, no rebase):

  • Both touched-adjacent workflows parse (yaml.safe_load: .ngit/act/workflows/build-package-binaries.yml, .github/workflows/build-package.yml); the two new run: blocks pass bash -n (rc=0 each). Diff walked line by line: .ngit/act/workflows/build-package-binaries.yml +21/−2, CHANGELOG.md +7.
  • Drift sentinels at this head: sh scripts/check-version-sync.sh → 18× ok, "Version v0.6.0-alpha3 is consistent", rc=0; python3 tests/contract/check-mirror-sync.py → "BLOSSOM_SERVERS: 13 copies identical", "relay list: 13 copies identical", rc=0.
  • The exact CI failure reproduced: PORTAL_REF=main bash packaging/portal-build.sh → ERROR: PORTAL_REF='main' does not match the pinned portal commit '4f74a6dd…', rc=1 — byte-for-byte the kind-9842 failure tail. And with no override but this host's node v22.22.3 on PATH: ERROR: node v22.22.3 != pinned v22.17.0, rc=1 — which is the load-bearing point: the workflow's 22.17.0 literal is enforced by portal-build.sh's toolchain gate, not trusted.
  • Three-way toolchain agreement confirmed: packaging/build-inputs.json (node 22.17.0 / npm 10.9.2) ↔ the workflow literal ↔ the portal repo's engines at the pinned commit 4f74a6dd (fetched depth-1: {"node": "v22.17.0", "npm": "10.9.2"}). The npm check reads the manifest at runtime via jq; jq is already a hard dependency of this lane's neighboring steps (blossom-upload, publish-record), so no new tool assumption.
  • Portal source pin stays the single source: after this PR the only PORTAL_REF occurrence in any workflow is the new explanatory comment — the floating override is gone from the whole lane (grep -rn PORTAL_REF .ngit/ .github/), and portal-build.sh's default is $PORTAL_COMMIT ← packaging/build-env.sh ← build-inputs.json.
  • Hermeticity of the touched job: no container image, no floating node, no floating ref; actions/*@v5/@v6 and runs-on: ubuntu-latest are the lane's pre-existing conventions, untouched here. The npm step now matches .github/workflows/build-package.yml step-for-step (compared side by side — identical bodies).
  • Hygiene: two clean commits (implementation + changelog), no assistant attribution, one logical change; CHANGELOG bullet under ### Changed / Internal with bold lead-in and PR link, correct category for a CI fix. src/ untouched, so the Go battery was deliberately not run (sentinels above cover this diff's surface).

On the "new hardcoded literal" question. node-version: '22.17.0' is a literal, but it is not a new literal class — it byte-matches the already-merged GitHub twin (.github/workflows/build-package.yml:361), and unlike the #448 Go drift (silent disagreement), a stale literal here cannot ship wrong bytes: portal-build.sh refuses any node/npm that disagrees with the manifest, so drift surfaces as a red lane, never as a different artifact. That matches the precedent set on #474 (pins whose runtime consumer verifies against the manifest don't require check-version-sync extension). Optional hardening follow-up, not a blocker: a sentinel grep tying both workflow node-version: literals to build-inputs.json would move that failure from build time to pre-commit time and cover both copies at once.

Composition. With #462 (artifact-over-merge ADR): reinforces it — the portal stays a hash-pinned CI-built artifact; nothing here re-introduces merging or committing built JS. With #464 (pin advance to 4f74a6d): this PR closes exactly the "known fragility, deliberately left" #464 called out — the lane invoking PORTAL_REF=main meant every portal-main advance re-drifted stage 1 red; that loop is now gone. With open #488 (epoch from source commit): same file, disjoint regions — #488 rewrites determine-versioning, this PR touches only build-portal, and they're semantically orthogonal (epoch stamping vs toolchain/ref pinning); both are fail-closed, so they layer cleanly in either merge order. The PR body's flagged follow-up (main's committed portal bundle snapshot predating its own pin) is correctly out of scope as a separate logical change. Pre-existing and also out of scope: node-version: '22' still floats in test.yml in both trees — test-only, ships no bytes.

Disposition: land — with the optional node-literal sentinel extension and the stale committed-bundle regeneration noted as follow-ups, neither blocking.

…ating main/node

build-portal overrode PORTAL_REF with a floating 'main' and setup-node
with a floating '20'. Both are refused by portal-build.sh's
reproducibility gates (portal commit and node/npm pins live in
packaging/build-inputs.json), so every stage-1 run on main since the

  ERROR: PORTAL_REF='main' does not match the pinned portal commit '…'.

Drop the override — portal-build.sh already defaults PORTAL_REF to the
manifest pin — and pin node 22.17.0 plus the twin's npm verification
step, matching .github/workflows/build-package.yml.

Evidence, run locally at this commit:
- PORTAL_REF=main bash packaging/portal-build.sh  -> gate error, rc=1
- system node v22.22.3                            -> toolchain gate, rc=1
- pinned toolchain (manifest tarball, sha256-verified) + no override
  -> full portal build from the pinned commit, rc=0
@Amperstrand

Copy link
Copy Markdown
Collaborator Author

Rebased onto the new main tip bf4ca69d (cf1955e6, replacing 230095e6) after #517 landed: the workflow fix replayed cleanly, and the changelog entry folds alongside #517's wave. While resolving, I also fixed the three ([#N](…pull/N)) placeholder links #517 left in its changelog entries (the keyset-agnostic decode fix, the portal-bundle regeneration, and the production-mint guard) — they all point at #517 now, so the release fold won't ship broken links. Zero markers; node 22.17.0 + npm verification + no PORTAL_REF override intact.

@Amperstrand
Amperstrand force-pushed the fix/stage1-portal-pin branch 2 times, most recently from 230095e to cf1955e Compare September 22, 2026 15:57
@Amperstrand
Amperstrand merged commit 22edca2 into main Sep 22, 2026
Amperstrand added a commit that referenced this pull request Sep 22, 2026
The [Unreleased] entries accumulated since the team's alpha4
redefinition all belong to this release: the v4 (cashuB) short-keyset
token fix (#517's portal repin to d699367 — coinos/minibits customers
could not pay), the stage-1 toolchain pin (#477), the portal-bundle
regeneration + contract guard (#517), the production-mint guard (#517),
and the shard SOURCE_DATE_EPOCH fix (#491). Folded into the
[v0.6.0-alpha4] section (24 entries), fresh empty [Unreleased] left on
top. RELEASE-NOTES gains the v4 token-fix bullet in Notable bug fixes.

VERSION unchanged (v0.6.0-alpha4); check-version-sync green.

After merge: delete the stale v0.6.0-alpha4 tag (fd58297 — never
mirrored, never announced; it predates this wave and carries the
node-pin defect), re-tag this merge commit, then DQ05 (mirror push +
drive) per #339.

Co-authored-by: Amperstrand <amperstrand@localhost>
@Amperstrand
Amperstrand deleted the fix/stage1-portal-pin branch October 11, 2026 11:23
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.

2 participants