Skip to content

fix(build): pin the node version to 22.23.4 — the floor the lockfile declares - #189

Merged
github-actions[bot] merged 1 commit into
mainfrom
fix/node-engine-floor
Oct 26, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
fix/node-engine-floor

Conversation

@aitrawog

Copy link
Copy Markdown
Contributor

Summary

Head 8817842593640d38f9e307e98c5fabe910503c6e — 1 file changed, 1 insertion(+), 1 deletion(-).

The lockfile this repo now ships declares a node floor of >=22.23.4, and the repo declared its node as the floating major 22 — which the CI resolved to v22.23.3, one patch below the floor. The deploy job was therefore installing the site's dependencies on a node they declare unsupported, and npm said so twice, in the install log, while still exiting 0.

This change pins the exact version the tree's dependencies require, so the install is clean and the declared toolchain and the declared requirement are the same fact.

file what it carries
.node-version 22 → 22.23.4 — the exact node the lockfile's engines floors require

Why this is a defect and not a nicety. A floating major cannot guarantee a floor: the floor was published ahead of the alias's resolution. v22.23.4 was released on 2026-10-20; the merged deploy run of #187 executed on 2026-10-26 and actions/setup-node@v7 still resolved 22 to v22.23.3 — six days later — because it resolves through its own version manifest, not through the release feed. The requirement was met by luck, and the luck ran out on the day the floor was published.

Why the exact version, and not a newer major. The floor is >=22.23.4; v22.23.4 is the newest 22.x and satisfies it exactly. The repo stays on the node major it was already on — the change is the declared version, not the node line.

Why there is still exactly one place that declares the node. .node-version is where this repo has always declared it, and the deploy workflow reads it with node-version-file: .node-version. package.json gains no engines field: that would be a second source for the same fact, and the two would drift — the defect this issue is about, in a different file.

How tested

The floor, read out of the lockfile — the requirement this change satisfies, and the base's:

$ 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'})}

$ git show origin/main:package-lock.json | python3 -c "import json,sys; pk=json.load(sys.stdin)['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': ('7.0.8', {'node': '>=22.12.0'}), 'node_modules/@astrojs/prism': ('4.0.2', {'node': '>=22.12.0'})}

The counter-measurement — the defect, reproduced, with the change absent. The same npm on both sides (10.9.9), one patch of node apart, on the tree before this change:

$ mise exec node@22.23.3 -- npm ci --no-audit --no-fund
npm warn EBADENGINE Unsupported engine {
npm warn EBADENGINE   package: '@astrojs/mdx@8.1.1',
npm warn EBADENGINE   required: { node: '>=22.23.4' },
npm warn EBADENGINE   current: { node: 'v22.23.3', npm: '10.9.9' }
npm warn EBADENGINE }
npm warn EBADENGINE Unsupported engine {
npm warn EBADENGINE   package: '@astrojs/prism@4.1.1',
npm warn EBADENGINE   required: { node: '>=22.23.4' },
npm warn EBADENGINE   current: { node: 'v22.23.3', npm: '10.9.9' }
npm warn EBADENGINE }

added 306 packages in 2s
CI_RC=0

$ grep -c EBADENGINE /tmp/pin904/nodefloor-ci-2233.log
10

The same two warnings are in the merged run's own install step, on the CI's own node — the same defect, in the org's own log, and the install still exited 0:

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 }

The proof — the same tree, on the pinned node. The install is clean, and the site builds:

$ mise exec node@22.23.4 -- npm ci --no-audit --no-fund
added 306 packages in 2s
CI_RC=0

$ grep -c EBADENGINE /tmp/pin904/nodefloor-ci-2234.log
0

$ mise exec node@22.23.4 -- npm run build
...
08:21:01 [starlight:pagefind] Found 1699 HTML files.
08:21:01 [build] 1699 page(s) built in 1m 14s
08:21:01 [build] Complete!
BUILD_RC=0

$ grep -icE '\b(error|warn|deprecat|fail)\b' /tmp/pin904/nodefloor-build-2234.log
0

The artefact that is actually shipped, built by that node:

$ mise exec node@22.23.4 -- node --version
v22.23.4

$ grep -rl 'popovertarget="starlight__sidebar"' dist --include='*.html' | wc -l
1698

$ grep -rl 'aria-expanded' dist --include='*.html' | wc -l
0

$ find dist -name '*.html' | wc -l
1699

The version exists, and it is the newest of its line — so the pin is a released version and not a guess:

$ curl -s https://nodejs.org/dist/index.json | python3 -c "import json,sys; [print(r['version'], r['date']) for r in json.load(sys.stdin) if r['version'] in ('v22.23.2','v22.23.3','v22.23.4')]"
v22.23.2 2026-07-28
v22.23.3 2026-09-23
v22.23.4 2026-10-20

What was not run here, stated plainly. The Cloudflare Pages deploy is this repo's own CI deploy check on this PR — a real wrangler pages deploy against the real service, in the job that runs on every push, and it is the leg that proves the CI's node is the pinned one. No local Cloudflare credential exists, so no deploy output is claimed here and nothing was mocked. The check-docs bed — a disposable: true pod deploy of docs-site-app — clones the docs repo at an immutable DOCS_REF and asserts that pin is the docs repo's main head, so it cannot be pointed at an unmerged docs commit: it is the charly-side leg that follows this landing, not a step this change could run.

Rulebook compliance

  • R0 — the dispatcher's two skills for this surface were loaded before any edit: /charly-tools:docs-site (the Astro/Starlight toolchain candy and the check-docs bed) and /charly-build:docs (the generation half, whose drift gate this change leaves a no-op). This repo's own AGENTS.md was read first.
  • R1 — the defect was measured before the fix was written: the floor out of the lockfile, the two warnings reproduced locally on node 22.23.3, and the same two warnings found in the merged run's own log. The finding this change does not clear — the docs-site image carries a node below the floor — is filed as the node the image installs is a floating major (node_22.x at build time), so the image carries a node below the floors its consumers' lockfiles now declare — and the newest published image is one day short of v22.23.4 layer-nodejs#8 rather than left implied, and it belongs to another repo. The finding was raised by the validator on fix(deps): adopt the Starlight 0.42 line and the upstream selector-parser fix, with no forced override #187 and is fixed here rather than filed away.
  • R2 — no duplication: the node version is declared in exactly one file, the one the workflow already reads. package.json gains no engines field, so there is no second source to drift.
  • R3 — no workaround: no engine-strict toggle to turn the warning into a different error, no --force, no suppression. The install is clean because the node satisfies the requirement.
  • R4 — product before prose: the fix is in the toolchain declaration the build reads; no documentation was edited to route around the defect.
  • R5 — nothing legacy survives: the floating major 22 is gone, replaced by the exact version the floor names. There is no second place left declaring a node major.
  • R6 — git status was read before the change; one session worktree, one fresh branch off the landed origin/main (1c2b7bab6393a0748698bb565408c08c1ba2757e), no force-push, no amend after the push, no rebase, no hook bypass, no direct push to main.
  • R7 — the coverage fails without the change: on node 22.23.3 the same install warns EBADENGINE twice, and with the pinned node it warns zero times. The changed path (the install on the declared node, and the build that follows it) was executed live, and the shipped artefact was inspected in the build that produced it.
  • R8 — the one service boundary, the Cloudflare Pages deploy, has no local credential, so it skips visibly: it is the CI deploy check on this PR, and no deploy output is claimed here. Nothing was mocked or faked.
  • R9 — no binary and no runtime OS dependency in this repo, so there is no build stamp to match and no packaging: entry to add.
  • R10 — the change class is a build-toolchain declaration, and its gate is this repo's own generation workflow, run on the committed tree above and again on this PR. The disposable: true bed for this surface (check-docs) pins the docs repo's main head and therefore cannot exercise an unmerged docs commit; it is the charly-side leg that follows this landing, as stated under How tested.
  • HC — one cutover: the declared version, the requirement it satisfies, the install proof and the build proof travel together, and no half of it is deferred.
  • COORD — the scope was claimed on 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 with the measurement that decided it, and the sibling surface (the image's node) is filed in its own owning repo, the node the image installs is a floating major (node_22.x at build time), so the image carries a node below the floors its consumers' lockfiles now declare — and the newest published image is one day short of v22.23.4 layer-nodejs#8, rather than pulled into this repo's cutover.
  • QUAL — the install output carries no warning and no error line, and the build output carries no warning and no error line, as pasted above.

Change classification

A build-toolchain change, not a documentation one. The diff edits the file the deploy workflow reads to choose the node that installs and builds the site, so the documentation reviewed tier is forbidden for it.

Tier: fully tested and validated. The change-class gate — the install and the site build on the committed tree — passed with zero warning lines, the counter-measurement is red without the change, and the changed path (the install on the declared node) was executed live with its output retained above. The live Cloudflare deploy is the CI's own check on this PR and is not claimed as local proof.

What this change touches, and the issue it closes:

change files issues
the declared node version, pinned to the floor the lockfile declares .node-version Closes #188

Filed issues


Agent: umbrella-prose-truth
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

…declares

#187's lockfile carries "engines": {"node": ">=22.23.4"} on
@astrojs/mdx 8.1.1 and @astrojs/prism 4.1.1. The repo declared its node as the
floating major "22", and actions/setup-node@v7 resolved that through its own
version manifest to v22.23.3 — one patch below the floor, although v22.23.4 had
been released six days earlier (2026-10-20). The deploy job therefore installed
the site's dependencies on a node they declare unsupported, and npm said so
twice (EBADENGINE) in the install log; the install still exits 0, so that warning
was the only signal.

A floating major cannot guarantee a floor: the floor was published ahead of the
alias's resolution. Pin the exact version the tree's dependencies require, so CI
installs a compliant node deterministically, and the declared toolchain and the
declared requirement stop being two files that never compare themselves.

Measured on the committed tree, same npm on both sides (10.9.9): node 22.23.3 ->
npm ci warns EBADENGINE twice (>=22.23.4 required, v22.23.3 current); node
22.23.4 -> 0 warnings, and the site builds 1699 pages with 0 warning lines.

Refs #188

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
@github-actions

Copy link
Copy Markdown

Review — PASS

What the diff actually is

.node-version: 22 → 22.23.4 (1 file, +1/−1). This exactly matches the body's stated change ("the declared node version, pinned to the floor the lockfile declares") and its metadata line ("1 file changed, 1 insertion(+), 1 deletion(-)"). Head SHA, branch, and base in the header are consistent with the body.

Correctness

  • The change is a pure toolchain pin: it replaces the floating major 22 with a concrete patch 22.23.4. The diff is coherent and complete — there is no second source of the version, and the body explicitly (and correctly, per the diff) states package.json gains no engines field, so no drift is introduced.
  • The rationale (floating major can't guarantee a >=22.23.4 floor; pinning the exact floor makes the declared toolchain match the declared requirement) is internally consistent with the one-line change and with the "no duplication" argument.
  • The body's supporting evidence (lockfile engines, EBADENGINE reproduction on 22.23.3, clean install + build on 22.23.4, curl release listing) is internally consistent and not contradicted by the diff. These are external facts I cannot independently verify from the provided context (no package-lock.json in the diff, no live execution), but they do not conflict with anything in the diff, and the kind of change (pin to a specific, claimed-released version) is sound.

Tests

  • No test file ships, which is expected for a .node-version change — there is no framework that asserts a version file's contents in this repo. The body correctly frames the CI install/build as the gate and honestly notes the failure mode is a warning (npm exits 0 with EBADENGINE) rather than a hard failure. That is a real, if soft, coverage gap, but it is disclosed and is not something a one-line pin could reasonably ship as a hard-failing test. Not a blocker for this change class.

Security / scope / cleanliness

  • No secrets, no injection surface, no dependency or privilege change — it edits a plain version string.
  • No leftover legacy path: the floating 22 is fully replaced; there is no duplicate declaration. No dead code introduced.

Comments

  • Zero comments in the thread; nothing to disposition.

Notes (non-blocking)

  • The body's release-date and curl claims (e.g., v22.23.4 released 2026-10-20) and the lockfile-floor evidence are unverifiable from the supplied context. They are internally coherent and consistent with the diff, so I accept them at face value rather than blocking on absent external verification; if a maintainer has doubts about the exact floor value, the lockfile is the place to confirm it before landing.
  • The body is unusually heavy on compliance boilerplate (R0–R10, HC/COORD/QUAL) relative to a one-line change; it reads as over-claimed prose but does not contradict the diff.

The change is minimal, honest about what it does and does not prove, and correctly fixes the declared-toolchain/declared-floor mismatch.

Verdict: PASS


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

@github-actions
github-actions Bot enabled auto-merge (squash) October 26, 2026 07:33
@github-actions
github-actions Bot merged commit fe08199 into main Oct 26, 2026
4 checks passed
charly-auto-merge Bot added a commit that referenced this pull request Oct 26, 2026
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.

1 participant