Repository navigation
chore(docker): pin Node.js base image via NODE22_VERSION/NODE24_VERSION - #1570
Conversation
Replace the floating node:22-trixie-slim and node:24-trixie-slim bases with global build ARGs that hold the full Node.js version (22.23.4 and 24.22.1, the October 2026 security releases). The pin makes the Node.js version shipped in each image visible in the repo and changes it only through a reviewed bump, as decided in openabdev#1567. The smoke tests now check that node --version in each Node-based image matches the ARG default. A new assert_line keeps the ARG value identical across every Dockerfile that declares it. Nightly keeps floating as the Node.js canary. It passes only the major version (NODE22_VERSION=22, NODE24_VERSION=24), so its base resolves to the floating tags, and it does not run the version assertion. Also fix docs/opencode.md, which still named node:22-bookworm-slim. Refs openabdev#1567 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
…ly Node version Address round-1 review findings on openabdev#1570: - F1: Replace the hardcoded 8-agent allowlist and kimi case mapping in both smoke-test workflows with .github/scripts/node_base_arg.sh, which resolves the NODE<n>_VERSION ARG from the (target) stage's base image, following stage inheritance. Non-Node images skip; an unpinned FROM node: fails. validate-packaged-pins now also requires every Dockerfile built FROM node:${NODE<n>_VERSION} to carry the same pin as Dockerfile.unified, so a new Node-based Dockerfile cannot fail open or ship a different version. - F2: Nightly records the Node.js version that actually shipped for each Node-based variant/arch in the job summary (warn-only; the float is intentional). - F3: Generalize the Dockerfile.opencode base-image comment instead of listing a partial set of sibling Dockerfiles.
|
Thanks for the review. 5ae1753 covers all three findings (F1, F2, F3), so I kept it as the round-1 fix. I had a parallel fix in progress and dropped it in favor of yours, so the branch carries a single implementation.
|
This comment has been minimized.
This comment has been minimized.
The derived loop in validate-packaged-pins already enforces that every
Dockerfile built FROM node:${NODE<n>_VERSION} carries the same pin as
Dockerfile.unified. The two literal assert_line lines added only the
version itself, which forced a workflow edit on every Node bump. That
contradicts the Dockerfile-only bump described in RELEASING.md (openabdev#1572)
and in the node-security-check issue text (openabdev#1571).
Verified with the extracted step script: the tree as committed passes,
bumping all nine NODE22 Dockerfiles to 22.23.5 passes, and bumping only
Dockerfile.pi fails.
Refs openabdev#1567
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round 2, F1: 1f1fec4 removes the two hard-coded #1571 is already on |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
validate-packaged-pins found Node-based Dockerfiles and unpinned bases with its own ^FROM node: regex. node_base_arg.sh already skips --platform and matches FROM in any case, and the parity gate's regex did neither. An unpinned FROM --platform=... node:<tag> or from node:<tag> in Dockerfile.package, which has no PR-time smoke test, therefore passed silently. node_base_arg.sh --all now checks every FROM line in a file. It prints each NODE<n>_VERSION in use and fails on any unpinned node: base. The parity gate runs it on every tracked Dockerfile, including those in subdirectories, so the gate and the runtime check share one parser. The file list is assigned before the loop, so a git failure stops the step instead of skipping every check. Inline fixtures pin the two reported forms and a pinned --platform base as regression cases. Verified with the extracted step in ubuntu:24.04: - passes as committed and after a full NODE22 bump - fails on a partial bump, a missing Dockerfile.unified pin, an unpinned --platform or lowercase from base in Dockerfile.package, an unpinned base in openshell/Dockerfile, and outside a git repo Refs openabdev#1567 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round 3, F1: f18717d applies option 2. I extracted the step and ran it in ubuntu:24.04. It passes as committed and after a full bump. It fails on a partial bump, a missing unified pin, the two reported |
…arser Address round-3 F1 on openabdev#1570: the parity gate kept its own `^FROM node:` regex, so `FROM --platform=... node:<tag>`, lowercase `from node:<tag>`, digest-only, registry-qualified, or build-arg Node bases passed silently, and the resolver itself skipped several of those forms. - node_base_arg.sh classifies every FROM in the file. Any reference to a Node.js image (node:, node@, library/node, docker.io/library/node) that is not node:${NODE<n>_VERSION}-<suffix> fails, as does FROM ${VAR} unless VAR is allowlisted (BUILDER_IMAGE). A missing tag suffix gets its own message. Stage names are matched case-insensitively, as Docker does. - New --check mode prints "<file> NODE<n>_VERSION" per pin used. validate-packaged-pins now uses it over every tracked Dockerfile (including nested ones) instead of a second grep parser, fails if no Node-based Dockerfile is found, requires a full x.y.z pin in Dockerfile.unified, and names the file and expected pin on mismatch. - Add test_node_base_arg.sh (21 fixtures plus the current repository targets) and run it in validate-packaged-pins.
Address round-4 findings on openabdev#1570: node_base_arg.sh --check still passed FROM index.docker.io/library/node:22, FROM mirror.gcr.io/library/node:22, and a FROM split across lines with "\" (parsed as an empty image). - Treat any image whose repository name is "node", under any registry or namespace, as a Node.js base that must be node:${NODE<n>_VERSION}-<suffix>. - Join "\" continuation lines (skipping comment lines) before parsing, as Docker does; reject a FROM with no image and the "# escape=" parser directive, which would change the continuation character. - Add 9 fixtures (30 total) covering these forms.
…n nested Dockerfiles Address round-4 findings on openabdev#1570: - node_base_arg.sh: any build arg other than ${NODE<n>_VERSION} inside a base image reference (node${TAG}-slim, ${IMG}:22, ${REG}/node:...) now fails, since it cannot be resolved statically and could hide an unpinned Node.js base. Document the RUN-heredoc limitation (false error only). - docker-smoke-test.yml: also trigger on '**/Dockerfile*', so a PR that only changes a nested Dockerfile still runs the validate-packaged-pins check that scans every tracked Dockerfile. - Add 4 fixtures (34 total).
… non-zero The repository-target checks compared command-substitution output only, so a non-Node target whose lookup crashed (empty output, non-zero exit) passed as "not Node-based". Check the exit status explicitly.
…ld args
FROM ${BUILDER_IMAGE} was trusted by name, so ARG BUILDER_IMAGE=node:22
would pass. Classify the declared ARG default as well (CI overrides are not
statically known), and guard against a self-referencing default. Add
fixtures for the default, parameter-expansion and bare-composite forms
(39 total).
This comment has been minimized.
This comment has been minimized.
|
Note LGTM ✅ - Round 3 F1 is fixed. What This PR DoesNode-based agent images were built from floating How It Works
Findings
Finding Details🟢 F1-F2: Unpinned and unresolvable Node.js bases now fail closedRound 3 found that the parity gate used its own
The pinned forms still resolve: The
🟢 F3: Regression suite
🟢 F6: CIAt Non-blocking Notes
Baseline Check
Addressing External Reviewer Feedback@thepagent✅ Approved at @antigenius0910 (
|
Picks up the NODE<major>_VERSION pins from openabdev#1570 so the PR-time dry-run job can parse Dockerfile.unified.
What problem does this solve?
Every Node.js-based agent image uses a floating
FROM node:22-trixie-slim(kimi:node:24-trixie-slim). The repo does not record which Node.js version an image ships, and a Node.js security release reaches users only when an image happens to be rebuilt. #1567 measuredstable-*on Node.js 22.23.1 andbeta-*on 22.23.3, both behind the October 2026 security release (22.23.4 / 24.22.1). The maintainers decided to pin the version with a build ARG and keep nightly floating.Part of #1567 (step 3, first PR). Merge order: #1570, then #1571, then #1572.
Discord Discussion URL: https://discord.com/channels/1491295327620169908/1491365158868619404/1562955032012398712
Review Contract
Goal
Every release, pre-beta, and per-agent Dockerfile builds its Node.js base from one explicit full version per major. Changing that version takes a reviewed one-line bump per file. CI fails if an image's
node --versiondiffers from the pin, or if the pin differs between files.Non-goals
node-security-check.ymlfor that.Accepted Residual Risks
node:<version>-trixie-slimtag is re-published by Docker for Debian updates, so a rebuild is not byte-reproducible. The Node.js version is. This is the intended trade-off.assert_lineindocker-smoke-test.ymlkeeps the values identical, so a partial bump fails CI rather than shipping mixed versions.Acceptance Criteria
git grep -n 'FROM node:' -- 'Dockerfile*'shows onlynode:${NODE22_VERSION}-...ornode:${NODE24_VERSION}-....validate-packaged-pinspasses with the newNODE22_VERSIONandNODE24_VERSIONassert_linechecks.NODE22_VERSION=22andNODE24_VERSION=24.Follow-ups
node-security-check.yml(companion PR) and the RELEASING.md runbook plus SECURITY.md (companion PR).At a Glance
Prior Art & Industry Research
Not applicable. This is a build and CI change to how the base image version is declared. It does not change agent runtime behavior.
Proposed Solution
NODE22_VERSION=22.23.4andNODE24_VERSION=24.22.1before the firstFROMinDockerfile.unified,Dockerfile.package, and the per-agent Dockerfiles (claude, codex, copilot, gemini, mimocode, opencode, pi, kimi). Each file declares only the ARG it uses.docker-smoke-test.ymlanddocker-smoke-test-unified.ymlgain a runtime step that comparesdocker run --entrypoint node <image> --versionwith the pinned ARG. After review round 1,.github/scripts/node_base_arg.shresolves that ARG from the stage's base image instead of a hard-coded agent list. Non-Node images skip the step, and an unpinnedFROM node:fails it.validate-packaged-pinschecks that every Dockerfile builtFROM node:${NODE<n>_VERSION}carries the same pin asDockerfile.unified. A Node bump therefore edits only Dockerfiles.nightly-build.ymlappendsNODE22_VERSION=22 NODE24_VERSION=24to the resolved build-args, sonode:${NODE22_VERSION}-trixie-slimresolves to the floatingnode:22-trixie-slim.docs/opencode.mdnamednode:22-bookworm-slim. It now describes the pinned trixie base.Why this approach?
A full-version ARG puts the shipped Node.js version in the diff and makes the security bump a reviewable PR. Because the major alone is also a valid tag, nightly floats with a single build-arg and no second Dockerfile. The maintainers chose this in #1567.
Alternatives Considered
FROM, and the gap is the release step. RFC: rebuild agent images for Node.js security releases (Node 22.23.4 / 24.22.1) #1567 has the details.Validation
docker buildx build --checkon all 10 edited Dockerfiles. Warning counts match upstreammain(only the existingSecretsUsedInArgOrEnv).Dockerfile.unified --target base-nodebuilt and printedv22.23.4. With--build-arg NODE22_VERSION=22it resolvednode:22-trixie-slim.Dockerfile.package --target kimibuilt with a stubbinscontext and printedv24.22.1. With the nightly args it resolvednode:24-trixie-slim.run:blocks were extracted from the YAML and run against the built images. claude and kimi, per-agent and unified, passed. A forced mismatch (ARG22.0.0,24.0.0) failed with::error::Node.js version mismatch: expected v22.0.0 (NODE22_VERSION), got v22.23.4. A missing ARG failed with::error::Failed to extract NODE24_VERSION from Dockerfile.kimi.validate-packaged-pinsscript passed as committed and failed afterDockerfile.piwas changed to22.23.3.🤖 Generated with Claude Code