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 e22971a4..ff0956a0 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 @@ -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 ae18d07f..83027bb1 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 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. 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 @@ -244,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' @@ -282,7 +291,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 @@ -316,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 @@ -347,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: @@ -370,7 +385,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 @@ -400,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 @@ -450,7 +465,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 @@ -483,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 53b67468..5542f0c2 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 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: - 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