Repository navigation
the lockfile's node floor (>=22.23.4) is above the node CI installs (v22.23.3): the deploy job installs the site's dependencies on a node they declare unsupported, twice, in the install log #188
Description
Problem
The lockfile that #187 landed raises the node engine floor to >=22.23.4 (@astrojs/mdx 8.1.1 and @astrojs/prism 4.1.1), and the repo's CI installs it on v22.23.3 — so the repo's own deploy job installs its dependencies on a node the dependencies declare unsupported, and says so, twice, in the install log:
2026-10-26T07:00:38.5218457Z npm warn EBADENGINE Unsupported engine {
2026-10-26T07:00:38.5219244Z npm warn EBADENGINE package: '@astrojs/mdx@8.1.1',
2026-10-26T07:00:38.5219875Z npm warn EBADENGINE required: { node: '>=22.23.4' },
2026-10-26T07:00:38.5220594Z npm warn EBADENGINE current: { node: 'v22.23.3', npm: '10.9.9' }
2026-10-26T07:00:38.5221148Z npm warn EBADENGINE }
2026-10-26T07:00:38.5224905Z npm warn EBADENGINE Unsupported engine {
2026-10-26T07:00:38.5225645Z npm warn EBADENGINE package: '@astrojs/prism@4.1.1',
2026-10-26T07:00:38.5226297Z npm warn EBADENGINE required: { node: '>=22.23.4' },
2026-10-26T07:00:38.5227062Z npm warn EBADENGINE current: { node: 'v22.23.3', npm: '10.9.9' }
2026-10-26T07:00:38.5227609Z npm warn EBADENGINE }
(from the merged run's deploy job, npm ci --no-audit --no-fund, run 40282211214). The install succeeds — npm warns rather than fails, because the repo sets no engine-strict — so the failure is latent, not loud.
Mechanism (measured)
The floor. The landed lockfile carries the floor on two packages:
$ python3 -c "import json; pk=json.load(open('package-lock.json'))['packages']; \
print({n: (pk[n]['version'], pk[n].get('engines')) for n in ('node_modules/@astrojs/mdx','node_modules/@astrojs/prism')})"
{'node_modules/@astrojs/mdx': ('8.1.1', {'node': '>=22.23.4'}),
'node_modules/@astrojs/prism': ('4.1.1', {'node': '>=22.23.4'})}
The base tree's floor was >=22.12.0 (@astrojs/mdx 7.0.8, @astrojs/prism 4.0.2) — so #187 raised it.
Why CI landed below it. The repo declares its node in one place, .node-version, and that file holds a floating major:
$ cat .node-version
22
The deploy workflow reads it with node-version-file: .node-version, and actions/setup-node@v7 resolved it through its own version manifest, which lagged the release:
2026-10-26T06:51:06.3696190Z node-version-file: .node-version
2026-10-26T06:51:06.5072437Z Resolved .node-version as 22
2026-10-26T06:51:06.9531008Z node: v22.23.3
v22.23.4 was already released when the run happened — it shipped on 2026-10-20, six days before this run, and it is the newest 22.x:
$ curl -s https://nodejs.org/dist/index.json | jq -r '.[] | select(.version|startswith("v22.2")) | "\(.version) \(.date)"' | head -5
v22.23.4 2026-10-20
v22.23.3 2026-09-23
v22.23.2 2026-07-28
So a floating major cannot guarantee a floor: the floor was published ahead of the alias's resolution, and nothing in the repo declared the requirement that would have caught it.
The missed control
- Nothing declares the floor.
package.jsonhas noenginesfield, and.node-versionnames a major, not a floor. The tree's declared toolchain and the tree's declared dependency requirements are two files that never compare themselves. - The install step's output is not a gate. The
npm cistep's warnings are not checked by anything: the deploy job asserts the site's shape, not the install's cleanliness, and npm's exit status is 0.
Blast radius
- This repo's CI — the merged run warned twice, and every future run warns twice until the declared node moves.
- The org's docs-site image —
layer-docs-siterequires@github.com/opencharly/layer-nodejs:v2026.272.0322, and that layer's own recorded measurement of its build image is node 22.22.2 (layer-nodejs'scharly.yml, thePNPM_ALLOWBUILDS_*comment: "MEASURED in the real build image (Fedora 43, node 22.22.2)"). That image installs this lockfile, so thecheck-docsbed inherits the same warning once the umbrella advances the docs pin. That surface is owned by another repo and is filed there.
Root fix
Declare the node version the tree's dependencies actually require, exactly, where the repo already declares it. .node-version becomes 22.23.4:
- the CI installs a node that satisfies the floor deterministically, instead of trusting a floating alias whose resolution lagged the release that set the floor;
- the declared toolchain and the declared requirement stop being two files that never compare themselves — and there is still exactly one source of the node version, so nothing has to be declared twice.
The floor is not raised by hand: it is read out of the lockfile (>=22.23.4), so the pin and the requirement move together.
Agent: umbrella-prose-truth
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (analysed on a live system)
Activity
aitrawog commentedon Oct 26, 2026
STATUS — #188 — PR #189 is open against
main, branchfix/node-engine-floor, head8817842593640d38f9e307e98c5fabe910503c6e.Done. The defect is measured and the fix is on its branch: the lockfile declares
>=22.23.4, the repo declared the floating major22, andactions/setup-node@v7resolved that to v22.23.3 — one patch below the floor, six days afterv22.23.4was released. The same twoEBADENGINEwarnings are in this issue's own merged-run log.The change is one line:
.node-version22→22.23.4, the exact version the tree's dependencies require.The counter-measurement and the proof, same npm (10.9.9) on both sides, one patch of node apart, on the tree before the change:
npm ci>=22.23.4required)Next. The PR's own
deploycheck is the leg that proves the CI's node is the pinned one — a realwrangler pages deployagainst the real service, with no local Cloudflare credential and nothing mocked. Then the validator's full report is read and every finding is fixed in one commit per round.Not in this PR, and filed for it. The docs-site image installs this lockfile on a node below the same floor (
layer-docs-sitepinslayer-nodejs:v2026.272.0322, whose measured build image is node 22.22.2; the newest published node layer, v2026.292.2324, is one day short ofv22.23.4's release, so a pin bump alone does not reach the floor). That surface belongs to another repo and is filed there with the measurement: opencharly/layer-nodejs#8, unclaimed. The umbrella'sdocs-pin leg follows this landing, and itscheck-docsbed is where the image's node becomes visible.Agent: umbrella-prose-truth
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
fix(build): pin the node version to 22.23.4 — the floor the lockfile …
aitrawog commentedon Oct 26, 2026
RESOLVED — #188 — the successor has landed: #189, squash
fe0819975f59c0d75736c7e0fc396be5a056190d, tagv2026.299.0734, changelogCHANGELOG/2026.299.0734.md.What the fix was. One line:
.node-version22→22.23.4— the exact version the tree's dependencies require, read out of the lockfile's ownenginesfloor (>=22.23.4on@astrojs/mdx8.1.1 and@astrojs/prism4.1.1).What the CI's own merged run now says. The deploy job of the merge run, on the merged tree:
The whole log carries
0EBADENGINE lines. The same log on the base tree carried two — the exact two this issue reported, on the CI's own node. The install is now warning-free because the node satisfies the floor, not because anything was silenced: noengine-strict, no--force, no suppression, and the deploy ran on the pinned node (/opt/hostedtoolcache/node/22.23.4/x64/bin/npx wrangler).The counter-measurement, kept in the issue's record. Same npm on both sides (10.9.9), one patch of node apart, on the tree before the change: node 22.23.3 →
npm ciwarns EBADENGINE twice and exits 0; node 22.23.4 → 0 warnings, and the site builds 1699 pages with 0 warning lines. The coverage fails without the change, because without the change the install warns.What this issue did not close, and where it went. The docs-site image installs this same lockfile on a node below the same floor:
layer-docs-siterequireslayer-nodejs:v2026.272.0322, whose own measured build image is node 22.22.2, and the newest published node layer (v2026.292.2324, 2026-10-19) is one day short ofv22.23.4's release — so a pin bump alone does not reach the floor. That surface is owned by another repo and is filed there with the measurement: opencharly/layer-nodejs#8, unclaimed, and open for a taker.Also filed from this RCA, and not fixed here: the repo declares its node but nothing about the npm that resolves its lockfile, and the two npm versions in play disagree — a nested
overridesentry is honoured by npm 11 and ignored by npm 10.9.9 (the npm CI runs), which makes the CI'snpm ciREFUSE the lockfile. Measured and written up as #190, unclaimed.Agent: umbrella-prose-truth
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)