Repository navigation
Conversation
Add node-security-check.yml and .github/scripts/node_security_check.sh. Once a day the check compares Node.js security releases from nodejs.org/dist/index.json with two sources: - the NODE<major>_VERSION pins in Dockerfile.unified - NODE_VERSION in the published stable-<agent> and beta-<agent> images, read anonymously from the ghcr.io image configs It derives the Node majors and the Node-based agents from the Dockerfile, so Node 22 and Node 24 (kimi) are covered without a hard-coded list. While a pin or a published image is behind, the check keeps one open issue labeled node-security, creating it or updating it in place. It closes the issue once everything is current. Stale data does not fail the workflow, as requested in openabdev#1567. Only a broken check fails it, such as a parse error, a network error, or an unexpected registry status. close-unlinked-stale-issues.yml now skips node-security issues. Without that exclusion it would close the tracking issue after 14 days and the next daily run would open a new one. Depends on the NODE22_VERSION/NODE24_VERSION pins from the companion PR. Without them the script fails with a clear error. Refs openabdev#1567 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Suggestion (not a formal review): please widen what the check covers. Two cases this week would not be caught with
Both are cheap additions to |
This comment has been minimized.
This comment has been minimized.
- node-security-check.yml: add a pull_request trigger (paths: script, workflow, Dockerfile.unified) that runs the script with DRY_RUN=1 and contents: read only. Parser-breaking Dockerfile changes now fail in their own PR instead of the next scheduled run. issues: write moves to the scheduled/dispatch job, which keeps the repository guard. PR runs use a per-PR concurrency group so they never queue behind reconcile. - node_security_check.sh: the remediation hint greps 'ARG NODE[0-9]+_VERSION' instead of hard-coding 22|24, matching the script's dynamic major discovery. Addresses review findings F3 and F4 on openabdev#1571.
|
Note LGTM ✅ - All four round-1 blockers are resolved in What This PR DoesNothing tells maintainers when Node.js publishes a security release that openab's pins or published images are behind on. #1567 found How It Works
Verified at
Findings
Finding Details🟢 F1: Merge order with #1570 - resolvedThe PR body now has an 🟢 F2: RELEASING.md anchor - resolvedThe link to 🟢 F3: PR-time regression gate - resolvedThe trigger, permissions, and concurrency were checked event by event:
Known trade-offs:
🟢 F4: Derived majors in the remediation hint - resolvedLine 149 now renders 🟢 F5: Follow-up suggestions (non-blocking)Carried from round 1 for a follow-up PR or issue:
🟢 F6: Optional concurrency simplificationOnly 🟢 F7: What is done wellSee "What's Good" below. Addressing External Reviewer Feedbackchaodu-obk[bot] (issue comment, 2026-10-23)
ℹ️ Accepted as a follow-up; unchanged from round 1. This PR's Non-goals exclude Round 1 -> Round 2 Resolution
Baseline Check
What's Good (🟢)
5️⃣ Three Reasons We Might Not Need This PR
All three are outweighed by two consecutive security releases that went unnoticed, and by the fact that no off-the-shelf tool sees re-tagged stale images. |
|
Thanks for the review. The branch now has your d956672 (F3 dry-run gate, F4 generic hint) and acf845a on top of it.
Verification of acf845a: shellcheck and actionlint are clean. A |
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>
…ON (#1570) * chore(docker): pin Node.js base image via NODE22_VERSION/NODE24_VERSION 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 #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 #1567 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(review): derive Node.js pin checks from Dockerfiles, record nightly Node version Address round-1 review findings on #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. * chore(docker): drop hard-coded Node pin assertions 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 (#1572) and in the node-security-check issue text (#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 #1567 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(docker): make node_base_arg.sh the only Node FROM parser 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 #1567 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(review): make node_base_arg.sh the single fail-closed Node base parser Address round-3 F1 on #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. * fix(review): recognize any registry Node base and continued FROM lines Address round-4 findings on #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. * fix(review): reject unresolvable build args in base images; trigger on nested Dockerfiles Address round-4 findings on #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). * fix(review): fail repository-target tests when node_base_arg.sh exits 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. * fix(review): check the declared default of allowlisted base-image build 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). --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
* chore(ci): harden node-security-check per #1571 review #1571 was merged before these review follow-ups landed. This applies the remaining suggestions from its round-1 review. - F5: report an image or pin on a different Node major than its line as "major mismatch" and count it as behind. Before this change, an image on a higher major than its Dockerfile line was reported "current". - F7: bound every curl call with connect and total timeouts. - F8: mark the tracker issue with <!-- node-security-check -->, act only on labeled issues that carry the marker, and document the label and Dockerfile contracts next to the code that relies on them. - F9: exempt node-security issues from issue-check.yml, so a maintainer edit or relabel does not mark the tracker incomplete. Refs #1567 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(ci): address #1574 review — mismatch title/next steps, reserved label - issue_title: fall back to 'Node <m> major mismatch' when the line has no security release, so the title never ends empty or with ' / ' - issue body: intro and Next steps cover major-mismatch rows - LABEL comment: label is reserved for this script's tracker - reconcile: null-safe body match, explicit --limit - issue-check.yml: note GITHUB_TOKEN writes do not retrigger it --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
What problem does this solve?
No job notices when Node.js ships a security release. #1567 found
stable-*images behind two consecutive Node.js security releases (22.23.2 in July, 22.23.4 in October). The maintainers asked for a daily check that keeps one labeled issue open while anything is behind, without failing the workflow every day, for both the Node.js 22 and Node.js 24 (kimi) lines.Part of #1567 (step 3, second PR). Merge order: #1570, then #1571, then #1572. The script reads the
NODE<major>_VERSIONARGs from #1570, so the PR-timedry-runjob fails on this PR until #1570 is onmain. That red check is the merge gate. The issue body links the RELEASING.md runbook anchor from #1572. Until #1572 merges, the link opens the top of RELEASING.md.Discord Discussion URL: https://discord.com/channels/1491295327620169908/1491365158868619404/1562955032012398712
Review Contract
Goal
Within a day of a Node.js security release, one open issue labeled
node-securitylists each Node.js pin and each publishedstable-<agent>/beta-<agent>image that is behind. The same issue is updated while the gap persists and closed when everything is current.Non-goals
nightly-*orpre-beta-*images, which are not release channels.Accepted Residual Risks
nodejs.org/dist/index.jsonand anonymous ghcr.io reads. If either is down, the run fails (red) for that day, and the next run recovers. A broken check is red on purpose, and stale data is not.FROMline, so a mismatch is unlikely.base-would be checked as an image tag and reportednot published. It shows up in the table and does not break the run.Acceptance Criteria
DRY_RUN=1against a pinned Dockerfile reports pins current and today'sstable-*/beta-*images stale, and exits 0.NODE<major>_VERSIONARGs exits 1 with an::error::line.gh.workflow_dispatchrun onopenabdev/openabcreates or updates exactly onenode-securityissue.Follow-ups
node-security-check.ymlin the RELEASING.md workflow table once both PRs are merged.At a Glance
Prior Art & Industry Research
Not applicable. This is a repository CI job for release hygiene. It does not change agent runtime behavior.
Proposed Solution
.github/workflows/node-security-check.ymlruns daily at 07:47 UTC and onworkflow_dispatch, withcontents: readandissues: write. It is guarded withif: github.repository == 'openabdev/openab'so forks stay quiet. Aconcurrencygroup prevents a cron run and a dispatch from both creating an issue..github/scripts/node_security_check.sh(bash, curl, jq):Dockerfile.unified.FROM base-node AS <agent>inherits base-node's major. A Node.js stage that is not pinned through an ARG is an error.security: truerelease per major inindex.json.NODE_VERSIONfrom each published image config.pull_requestdry-runjob (contents: read,DRY_RUN=1) runs when the script, the workflow, orDockerfile.unifiedchanges. A parser break fails in its own PR.<!-- node-security-check -->marker. Only labeled issues with that marker are updated or closed. An image or pin on a different Node major than its line counts asmajor mismatch. Every curl call has timeouts.issue-check.ymlandclose-unlinked-stale-issues.ymlskipnode-securityissues. Without that, the stale bot would close the tracking issue after 14 days, and the next daily run would open a new one.Why this approach?
An issue that stays open and updates itself is visible and actionable. A daily red check gets ignored, as the maintainers noted. Deriving the agent list from the Dockerfile means a new Node.js agent or a new Node.js major is covered without editing the script. Reading image configs through the registry API costs a few small HTTP requests and needs no image pulls.
Alternatives Considered
FROMand does not detect published images that lag behind. Renovate would add an app dependency for the same partial coverage.Dockerfile.unified.Validation
All runs used
ubuntu:24.04(bash 5.2, jq 1.7) in Docker as a stand-in forubuntu-latest, against live nodejs.org and ghcr.io data.Dockerfile.unified(22.23.4 / 24.22.1). Both pins arecurrent. Everystable-*image is22.23.1 stale, everybeta-*image is22.23.3 stale,beta-kimiis24.21.0 stale, andstable-kimiisnot published. The decision is "create or update issue" and the exit code is 0.stale, and the exit code is 0.Dockerfile.unified. Exits 1 with::error::no 'ARG NODE<major>_VERSION=<x.y.z>' found in Dockerfile.unified.FROM node:24-trixie-slim AS fooexits 1. No network (--network none) exits 1 after retries.ghstubbed, the decisions came out as follows. Not stale with issue feat: /close slash command to manually terminate a thread session #42 open givesclose (#42). Stale with feat: /close slash command to manually terminate a thread session #42 open givesupdate (#42). Stale with no issue givescreate, plus a label create when the label is missing. Not stale with no issue givesnone.gh issue listandgh label listagainst openabdev/openab behave as the script expects.close-unlinked-stale-issues.yml.🤖 Generated with Claude Code