Skip to content

ci: give Node one source of truth, and move it to 26 - #346

Open
FrameAutomata wants to merge 1 commit into
mainfrom
ci/333-node-single-source
Open

ci: give Node one source of truth, and move it to 26#346
FrameAutomata wants to merge 1 commit into
mainfrom
ci/333-node-single-source

Conversation

@FrameAutomata

Copy link
Copy Markdown
Collaborator

Closes #333. You asked to lock it down and tighten it "to like 26" — this does both: .nvmrc becomes the single source of truth, and the pin moves 22 → 26.

Before

Ten sites naming a Node major, nothing asserting they agree — and two of them not naming one at all:

site was
release-traceway.yml node-version: 22
benchmark-processor.yml node-version: 22
release-docs.yml, release-website.yml node-version: "latest"
Dockerfile, .minimal, .sqlite, .duckdb, .browser (×2) node:22-alpine / node:22-bookworm-slim
flake.nix regex over engines.node

"latest" resolves to Node Current, which the runner tool cache never ships — so every docs and website deploy was re-downloading a Node tarball before it could start.

After

.nvmrc  ->  4× setup-node    node-version-file: .nvmrc
        ->  flake.nix        dev shells
        ->  release-traceway --build-arg NODE_VERSION, all 5 image builds

engines.node stays ">=22". It is deliberately not the pin. That field is the floor npm enforces on consumers, and pointing node-version-file at it is exactly what f95424cc was fixing — node-version-file resolves a range to the newest satisfying release, so ">=22" silently meant "latest". Reading .nvmrc restores the mechanism f95424cc had to abandon, without reintroducing the bug that made it abandon it.

The Dockerfiles keep an ARG NODE_VERSION=26 default, because a bare docker build has no way to read .nvmrc. That default is now the only value in the repo that can drift — so scripts/check-node-pins.sh asserts it matches, and release-traceway.yml runs it before it builds anything. It also fails on a literal node:<major> tag reintroduced into a FROM, which would ignore both .nvmrc and the ARG.

I confirmed it actually catches both, rather than assuming:

$ sed -i 's/ARG NODE_VERSION=26/ARG NODE_VERSION=22/' Dockerfile.sqlite
check-node-pins: Dockerfile.sqlite pins Node 22, .nvmrc says 26          exit 1

$ # reintroduce node:24-alpine in Dockerfile.duckdb
check-node-pins: hardcoded node tag, use ${NODE_VERSION}:
Dockerfile.duckdb:20:FROM --platform=$BUILDPLATFORM node:24-alpine ...   exit 1

One thing worth your call: 26 is not LTS until October

Node is a build-time dependency in every image except :browser, which copies the node binary into its runtime stage to execute Playwright specs (Dockerfile.browser:80). So that image is the one place the major is load-bearing at runtime, and Node 26 reaches LTS in October 2026 — about five weeks out.

I've shipped 26 as asked. If you'd rather be on the current LTS, 24 is a one-line change to .nvmrc plus the five ARG defaults, and the check script tells you if you miss one. That is the point of the change more than the number is.

Verification

npm ci / build / test / check on nodejs 26.7.0 pass — 25 tests, 0 svelte-check errors
All four nix dev shells evaluate, hand out v26.7.0
scripts/check-node-pins.sh passes clean, fails on both drift shapes above
Workflows no literal node-version: remains

Not verified: no container runtime available here, so the five image builds were not run. The ARGs are global — declared before the first FROM in every file, which I checked mechanically — which is what makes them usable in a FROM.

Out of scope, deliberately

  • testing/devtesting-nestjs/Dockerfile stays on node:20-alpine. The issue names it as existing drift, but it pins the runtime of a sample third-party app rather than anything Traceway builds; coupling it to our frontend's pin would be wrong. (It is on an EOL major, which is its own question.)
  • docs/pages/client/js-sdk/sourcemap-upload.mdx keeps a literal — it is a copy-pasteable recipe for readers who have no .nvmrc.
  • ci: test the frontend on every labelled PR #315's frontend.yml uses node-version-file: frontend/package.json, which has the resolve-to-latest behaviour described above. Different file, so no conflict either way; whichever of the two lands second should switch it to .nvmrc. I'll do that on ci: test the frontend on every labelled PR #315 if this goes first.

🤖 Generated with Claude Code

Node's major was written out at ten sites with nothing asserting they
agree, and the two release-site values did not even name a major --
node-version: "latest" resolves to Node Current, which the runner tool
cache never ships, so every docs/website deploy re-downloaded a tarball.

.nvmrc now holds the pin and everything that can read it, does:

  4x setup-node        node-version: 22 | "latest" -> node-version-file
  flake.nix            engines.node regex -> .nvmrc
  5 Dockerfiles        node:22-* -> node:${NODE_VERSION}-*
  release-traceway     --build-arg NODE_VERSION read from .nvmrc,
                       passed to all five image builds

engines.node stays a floor (">=22") rather than becoming the pin. That
is what npm enforces on consumers, and it is also what f95424c was
fixing: node-version-file pointed at a range resolves to the newest
satisfying release, so ">=22" there silently meant "latest". Reading
.nvmrc gives the mechanism back without reintroducing that.

The Dockerfiles keep an ARG default because a bare `docker build` has no
way to read .nvmrc. That default is now the only value that can drift,
so scripts/check-node-pins.sh asserts it matches -- and also fails on a
literal node:<major> tag reintroduced in a FROM, which would ignore both
.nvmrc and the ARG. release-traceway.yml runs it before it builds.

The bump itself is 22 -> 26. Node is a build-time dependency everywhere
except the :browser image, which copies the binary into its runtime
stage to execute Playwright specs -- so that image is the one place the
major is load-bearing at runtime, and worth naming since Node 26 does
not reach LTS until October 2026. If you would rather ship the current
LTS, 24 is a one-line change to .nvmrc plus the five ARG defaults, and
the script tells you if you miss one.

Verified: frontend npm ci, build, test (25 passed) and check (0 errors)
all pass on nodejs 26.7.0; all four nix dev shells evaluate and hand out
v26.7.0; the check script passes and was confirmed to fail on both drift
shapes (a changed ARG default, a reintroduced literal tag).

Not verified: no container runtime here, so the five image builds were
not run. The ARGs are global (declared before the first FROM in every
file, confirmed) which is what makes them usable in FROM.

Closes #333.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FrameAutomata added a commit that referenced this pull request Aug 28, 2026
This PR changed release-docs.yml and release-website.yml from
node-version: "latest" to a literal 22, and documented "Node is pinned
per site rather than read from one file" as the intended shape. #346
(closing #333) makes .nvmrc the single source of truth instead, so both
are superseded: the two workflows now take node-version-file: .nvmrc,
which fixes "latest" for the same reason and without adding a third and
fourth hand-maintained literal.

Dropping them here rather than reconciling them, since they touched the
exact lines #346 does -- the two PRs would otherwise conflict, and this
one is about Dependabot.

What remains is the watcher and the docs recipe's node20 actions, which
is what the PR was for.

Refs #330, #333.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

Node's version lives in six hardcoded places; the single source of truth was removed to unblock a floor bump

1 participant