From be9a92010c8b686a8b60910a7d492170e6c62e37 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Tue, 1 Sep 2026 10:52:02 -0400 Subject: [PATCH 1/2] build: move the CI toolchain to node 24 Node 20 reached end of life on 2026-04-30 and node 22 (Jod) has been in maintenance-only since 2025-10-21. Node 24 (Krypton) is the active LTS until 2026-10-20 and is supported to 2028-04-30, and the release and publish jobs already pinned 24.19.0 for the npm version it bundles -- so the rest of the repo was building and testing on a line the release never used. Everything that resolves a node now resolves 24: - release.yml build, pr-checks.yml build / test / dist-size / browser-smoke: '22' -> '24' - release.yml release + publish: 24.19.0 -> 24.20.0 - bench.yml codspeed-bench, pr-checks.yml codspeed-walltime: 22.23.1 -> 24.20.0 - root package.json engines.node: >=22.13 -> >=24 - tools/docker/Dockerfile: nodesource setup_22.x -> setup_24.x 24.20.0 (2026-08-26) is the current LTS patch, and one exact version now covers every exact pin in the repo. It bundles npm 11.19.0, still past the 11.5.1 that OIDC trusted publishing needs, so the publish job's no-`npm install --global` property is preserved -- tools/release/README.md updated for the new numbers. THE CODSPEED PINS ARE A BASELINE RE-SEED. 22.23.1 -> 24.20.0 crosses a V8 major, and docs/ci/self-hosted-runner.md is explicit that a node change shifts instruction counts the way a CPU or glibc change does. Expect the first main run after this to report large deltas on every bench, and read none of them as regressions. Noted in that doc alongside the existing pin-exactly rule. Corepack is still bundled in node 24 (verified: deps/corepack is present in the v24.20.0 tree), so `corepack enable pnpm` on the nashua box is unaffected. The existing "node 25 unbundles corepack" caveat stays where it is -- it is about a bump this commit does not make. Published packages keep `"node": ">=0.14"`. That constrains consumers of the wasm codecs, not this repo's build toolchain, and nothing here needs it raised. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/bench.yml | 10 +++++----- .github/workflows/pr-checks.yml | 17 +++++++++-------- .github/workflows/release.yml | 17 +++++++++-------- docs/ci/self-hosted-runner.md | 16 +++++++++------- package.json | 2 +- tools/docker/Dockerfile | 4 ++-- tools/release/README.md | 2 +- 7 files changed, 36 insertions(+), 32 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index e22971a4..8c6708f1 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -242,18 +242,18 @@ jobs: persist-credentials: false - uses: actions/setup-node@v4 with: - # EXACT version, not the '22' range every other job uses. For a range, + # EXACT version, not the '24' range every other job uses. For a range, # setup-node takes any satisfying version already in the runner's tool # cache without checking the network — and on a self-hosted box that - # cache persists, so the bench would silently freeze on whichever 22.x + # cache persists, so the bench would silently freeze on whichever 24.x # landed there first and jump whenever the box is rebuilt. A V8 patch # bump shifts instruction counts the same way a different CPU does. - # 22.23.1 is what the current main baseline was measured on; changing + # 24.20.0 is what the current main baseline was measured on; changing # it is a deliberate re-seed event (see docs/ci/self-hosted-runner.md). - node-version: '22.23.1' + node-version: '24.20.0' # nashua has no package manager beyond npm: setup-node ships node + npm # only, and GitHub's hosted images are not what runs here. Corepack is - # bundled with node 22 and fetches over Node's own https, so it works + # still bundled with node 24 and fetches over Node's own https, so it works # where `npm i -g ` is unreliable on this box — the runner's bundled # node has a corrupted npm ("Cannot find module '../lib/cli.js'"), which # is why OHIF's workflow also went the Corepack route here. `corepack diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index ae18d07f..e445d997 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -196,12 +196,13 @@ jobs: # Shallow checkout: builds only need the working tree (full history # is only required by detect-changes for the merge-base diff). - uses: actions/setup-node@v4 - # The emsdk image bundles node 20.18.0, which fails both pnpm 11's - # engine check (needs >=22.13) and vite 7's (via vitest 3). Put node 22 - # on PATH for pnpm/webpack; emscripten is unaffected — emcc invokes the - # node binary pinned in its own .emscripten config, not the one on PATH. + # The emsdk image bundles node 20.18.0 — end of life since 2026-04-30, + # and it fails pnpm 11's engine check (needs >=22.13) anyway. Put node + # 24 on PATH for pnpm/webpack; emscripten is unaffected — emcc invokes + # the node binary pinned in its own .emscripten config, not the one on + # PATH. with: - node-version: '22' + node-version: '24' - name: Provide pnpm via Corepack # `corepack prepare --activate` with no argument installs exactly the # version in the root package.json "packageManager" field, so CI and @@ -282,7 +283,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24' - name: Provide pnpm via Corepack run: | corepack enable pnpm @@ -370,7 +371,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '22' + node-version: '24' - name: Provide pnpm via Corepack run: | corepack enable pnpm @@ -450,7 +451,7 @@ jobs: # between patch releases move the numbers, and walltime is if anything # more sensitive than instruction counts. Keep this in step with the # codspeed-bench pin so the two instruments stay comparable. - node-version: '22.23.1' + node-version: '24.20.0' - name: Provide pnpm via Corepack run: | corepack enable pnpm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 53b67468..617217b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -110,11 +110,12 @@ jobs: # .git/config cannot push or publish, and the submodule init a few steps # down is the one place in this file that still wants a working remote. - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - # The emsdk image bundles node 20.18.0; pnpm 11 requires >=22.13 and - # vite 7 (via vitest 3) requires >=20.19. emcc is unaffected — it uses - # the node binary pinned in its own .emscripten config, not PATH. + # The emsdk image bundles node 20.18.0, which reached end of life on + # 2026-04-30 and fails pnpm 11's engine check (>=22.13) besides. emcc is + # unaffected — it uses the node binary pinned in its own .emscripten + # config, not PATH, so this only governs pnpm and the build scripts. with: - node-version: '22' + node-version: '24' - name: Provide pnpm via Corepack run: | corepack enable pnpm @@ -173,12 +174,12 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: # Pinned exactly, not floating '24'. Node bundles npm, and this is - # the version that ends up publishing: v24.19.0 ships npm 11.17.0, + # the version that ends up publishing: v24.20.0 ships npm 11.19.0, # comfortably past the 11.5.1 that OIDC trusted publishing needs. So # bumping Node here is also how npm gets bumped — no separate # `npm install --global` step, which would re-download an unpinned # npm on the morning of every release. - node-version: '24.19.0' + node-version: '24.20.0' - name: Provide pnpm via Corepack run: | corepack enable pnpm @@ -426,8 +427,8 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: # Same exact pin as the release job: this is where its bundled npm - # (11.17.0, past the 11.5.1 OIDC floor) actually gets used. - node-version: '24.19.0' + # (11.19.0, past the 11.5.1 OIDC floor) actually gets used. + node-version: '24.20.0' - name: Download built dists uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: diff --git a/docs/ci/self-hosted-runner.md b/docs/ci/self-hosted-runner.md index ad758cd8..89f777cb 100644 --- a/docs/ci/self-hosted-runner.md +++ b/docs/ci/self-hosted-runner.md @@ -37,7 +37,7 @@ and the regression gate is trustworthy. - **flock** (from `util-linux`, present on any stock Linux) — `tools/ci/with-nashua-lock.sh` needs it for the shared-box mutex below. - Node is provisioned per-job by `actions/setup-node@v4`, pinned to an **exact** - version (`22.23.1`) rather than the `'22'` range the other jobs use — see + version (`24.20.0`) rather than the `'24'` range the other jobs use — see [Hardware hygiene](#hardware-hygiene-for-stable-numbers) for why. It does not need to be pre-installed; the runner user only needs write access to the actions tool cache. **The box provides no node on `PATH` of its own**, so anything a @@ -52,8 +52,8 @@ and the regression gate is trustworthy. ``` With no argument, `corepack prepare --activate` installs exactly the version in the root `package.json`'s `packageManager` field, so the bench box can - never drift from the build jobs. Corepack is bundled with node 22 and fetches - over Node's own https. That is deliberate rather than incidental: `npm i -g` + never drift from the build jobs. Corepack is still bundled with node 24 and + fetches over Node's own https. That is deliberate rather than incidental: `npm i -g` is unreliable here because the npm reachable from the GitHub runner's *bundled* node on this box is corrupted (`Cannot find module '../lib/cli.js'`) — the same wall OHIF's workflow hit, which is why it provisions pnpm through @@ -184,14 +184,16 @@ This runner hosts the **simulation** gate only, which changes what matters: - The only hard requirement for cross-run stability is a **fixed CPU model** (don't migrate the box between different physical CPUs), since the modeled cache is derived from it. -- **Pin node exactly, never by range.** Given a range like `'22'`, setup-node uses +- **Pin node exactly, never by range.** Given a range like `'24'`, setup-node uses any satisfying version already in the tool cache *without consulting the network*. On a self-hosted box that cache persists, so the bench silently - freezes on the first 22.x it ever saw and then jumps whenever the box is + freezes on the first 24.x it ever saw and then jumps whenever the box is rebuilt or the cache is cleared — and V8 changes between patch releases move - instruction counts. Both codspeed jobs pin `22.23.1`, the version the current + instruction counts. Both codspeed jobs pin `24.20.0`, the version the current baseline was measured on. Changing it is a deliberate re-seed event, exactly - like a glibc or valgrind bump. + like a glibc or valgrind bump. The move from `22.23.1` to `24.20.0` was one + such event: node 24 is a different V8 major, so expect the first `main` run + after it to show large deltas across every bench and mean nothing by them. - **Don't casually `apt upgrade` the box.** glibc is the sharpest example: different glibc builds dispatch different code paths, so a bump shifts instruction counts much as a different CPU would — that is the subject of diff --git a/package.json b/package.json index 9d84d996..338b2e2e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "playwright-core": "^1.49.0" }, "engines": { - "node": ">=22.13", + "node": ">=24", "pnpm": ">=11" }, "scripts": { diff --git a/tools/docker/Dockerfile b/tools/docker/Dockerfile index c0555e55..99ddc698 100644 --- a/tools/docker/Dockerfile +++ b/tools/docker/Dockerfile @@ -55,9 +55,9 @@ RUN apt-get update \ # what makes that fail now, with a readable message, instead of during a # codec build later. && cmake --version \ - # node 22 on PATH, matching CI's setup-node step. emcc is unaffected: it + # node 24 on PATH, matching CI's setup-node step. emcc is unaffected: it # invokes the node pinned in its own .emscripten config, not this one. - && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \ && apt-get install -y nodejs \ && apt-get autoremove -y \ && apt-get clean -y \ diff --git a/tools/release/README.md b/tools/release/README.md index 90c7d72e..3c729b00 100644 --- a/tools/release/README.md +++ b/tools/release/README.md @@ -69,7 +69,7 @@ token runs nothing but `npm`, the pinned actions and `publish-order.mjs` (node b goes out after the six siblings whose ranges it carries. `--ignore-scripts` is deliberate: `prepublishOnly` re-runs `bash build.sh`, and this job has no emscripten toolchain — the dist being published is the artifact built in step 1 from the same commit. npm's version comes from - the exactly-pinned `node-version` (v24.19.0 → npm 11.17.0, past the 11.5.1 OIDC floor), so there + the exactly-pinned `node-version` (v24.20.0 → npm 11.19.0, past the 11.5.1 OIDC floor), so there is no `npm install --global npm@latest` re-downloading an unpinned publisher every release. 4. **`github-releases`** — a GitHub Release per tag, from the package list `publish` uploaded. It is a separate job so `gh release create`'s `contents: write` never coexists with the OIDC publish From 1f5fc522f80f75df6b30e415902a2d900b4646e6 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 2 Sep 2026 13:09:42 -0400 Subject: [PATCH 2/2] build: finish the node 24 move (devcontainer, cache keys, dist-size) Follow-up on review of #91. The bump left three things behind on 22: - .devcontainer/Dockerfile still ran setup_22.x under a comment reading "node 22 to match CI", which stopped being true in the same commit -- and engines.node became >=24, so pnpm now warns "Unsupported engine" on every install in that container. Bumped to setup_24.x. The comment now says what is actually shared with CI (the node major) and what deliberately is not (the older emsdk pin), so it does not go stale the same way again. - The node_modules cache keys in the test, browser-smoke and codspeed-walltime jobs, plus codspeed-bench in bench.yml, all carried a literal node22 discriminator. bench.yml's key hashes only pnpm-lock.yaml and pnpm-workspace.yaml, so nothing in a node-only bump invalidates it: the bench would have restored a node-22 node_modules tree, skipped the install step (gated on cache-hit), and measured the "clean node 24 re-seed" against dependencies installed by the previous V8 major. Now node24, with the reason for the discriminator written down next to the build job's key so the next bump moves it too. - dist-size had no setup-node at all, contrary to what the PR description claimed, so the size gate ran on whatever node the runner image shipped. check.js only needs fs/path/zlib, so this was harmless rather than broken, but it was unpinned and could sit below the new engines floor. Pinned '24' like every other job. Also corrected two setup-node comments that justified the step with "fails pnpm 11's engine check (>=22.13)" -- that floor is this repo's engines.node and is now >=24. Co-Authored-By: Claude Opus 5 (1M context) --- .devcontainer/Dockerfile | 9 ++++++--- .github/workflows/bench.yml | 6 ++++-- .github/workflows/pr-checks.yml | 22 ++++++++++++++++++---- .github/workflows/release.yml | 2 +- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 6ae5c909..f8542292 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -37,9 +37,12 @@ RUN apt-get update \ && apt-get clean -y \ && rm -rf /var/lib/apt/lists/* -# node 22 to match CI: pnpm 11 requires >=22.13, and vitest 3 (via vite 7) -# requires >=20.19, so the old node 16 here could run neither. -RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - +# node 24 to match CI, and to satisfy the root package.json engines.node +# floor (>=24) — pnpm warns "Unsupported engine" on anything below it. Only +# the node major is shared with CI: the emsdk pin above is deliberately older +# than the one tools/docker/Dockerfile and the build job use, so this image is +# for editing and running the JS tooling, not for reproducing a CI dist. +RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - RUN apt-get install -y nodejs # Corepack reads the pinned pnpm version from the repo's package.json # "packageManager" field on first use, so there is nothing to pin here. diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 8c6708f1..ff0956a0 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -296,8 +296,10 @@ jobs: # pnpm-workspace.yaml is in the key because the lockfile does not # record nodeLinker/allowBuilds/linkWorkspacePackages — without it a # layout change hits the cache and the install step is skipped. - # Keep in step with the cache keys in pr-checks.yml. - key: pnpm-modules-node22-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml') }} + # Keep in step with the cache keys in pr-checks.yml — including the + # node major in the prefix, which is the only part of this key a + # node-only bump changes (see the build job's cache step there). + key: pnpm-modules-node24-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml') }} - name: Install dependencies if: steps.modules-cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index e445d997..83027bb1 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -197,7 +197,7 @@ jobs: # is only required by detect-changes for the merge-base diff). - uses: actions/setup-node@v4 # The emsdk image bundles node 20.18.0 — end of life since 2026-04-30, - # and it fails pnpm 11's engine check (needs >=22.13) anyway. Put node + # and it fails this repo's engines.node check (needs >=24) anyway. Put node # 24 on PATH for pnpm/webpack; emscripten is unaffected — emcc invokes # the node binary pinned in its own .emscripten config, not the one on # PATH. @@ -245,6 +245,14 @@ jobs: # stale cache entry. The root manifest also carries the # packageManager pin, so a pnpm bump invalidates too. # Keep the same hashFiles(...) inputs in all four pnpm-modules keys. + # + # This job's prefix is `build` rather than `node` because it + # installs inside the emsdk container; the other three install on + # ubuntu-latest under setup-node and carry the node major in the key + # so a node bump forces a fresh install instead of restoring a tree + # built against the previous V8/ABI. BUMP THAT DISCRIMINATOR WITH THE + # node-version PINS — bench.yml's key hashes only the lockfile and + # workspace config, so nothing else in a node-only bump invalidates it. key: pnpm-modules-build-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package.json', 'packages/*/package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml') }} - name: Install dependencies if: steps.modules-cache.outputs.cache-hit != 'true' @@ -317,7 +325,7 @@ jobs: packages/*/node_modules # Manifests + workspace config in the key — see the build job's cache # step for why the lockfile alone is not enough. - key: pnpm-modules-node22-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package.json', 'packages/*/package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml') }} + key: pnpm-modules-node24-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package.json', 'packages/*/package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml') }} - name: Install dependencies if: steps.modules-cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile @@ -348,6 +356,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + # check.js needs no dependencies (fs/path/zlib only), so this is purely + # to stop the gate running on whatever node the runner image happens to + # ship — which drifts, and has been below this repo's engines.node floor. + with: + node-version: '24' - name: Download all built dists uses: actions/download-artifact@v4 with: @@ -401,7 +415,7 @@ jobs: packages/*/node_modules # Manifests + workspace config in the key — see the build job's cache # step for why the lockfile alone is not enough. - key: pnpm-modules-node22-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package.json', 'packages/*/package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml') }} + key: pnpm-modules-node24-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package.json', 'packages/*/package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml') }} - name: Install dependencies if: steps.modules-cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile @@ -484,7 +498,7 @@ jobs: # (esbuild/rollup) and break vitest. # Manifests + workspace config in the key — see the build job's cache # step for why the lockfile alone is not enough. - key: pnpm-modules-node22-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package.json', 'packages/*/package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml') }} + key: pnpm-modules-node24-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package.json', 'packages/*/package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml') }} - name: Install dependencies if: steps.modules-cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 617217b4..5542f0c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -111,7 +111,7 @@ jobs: # down is the one place in this file that still wants a working remote. - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 # The emsdk image bundles node 20.18.0, which reached end of life on - # 2026-04-30 and fails pnpm 11's engine check (>=22.13) besides. emcc is + # 2026-04-30 and fails this repo's engines.node check (>=24) besides. emcc is # unaffected — it uses the node binary pinned in its own .emscripten # config, not PATH, so this only governs pnpm and the build scripts. with: