Skip to content

chore(ci): add daily node-security-check workflow - #1571

Merged
thepagent merged 2 commits into
openabdev:mainfrom
antigenius0910:chore/issue-1567-node-security-check
Oct 23, 2026
Merged

thepagent merged 2 commits into
openabdev:mainfrom
antigenius0910:chore/issue-1567-node-security-check

Conversation

@antigenius0910

@antigenius0910 antigenius0910 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

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>_VERSION ARGs from #1570, so the PR-time dry-run job fails on this PR until #1570 is on main. 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-security lists each Node.js pin and each published stable-<agent>/beta-<agent> image that is behind. The same issue is updated while the gap persists and closed when everything is current.

Non-goals

  • Bumping pins or cutting releases automatically. A maintainer follows the RELEASING.md runbook (companion PR).
  • Tracking non-security Node.js patch releases, Debian CVEs, or agent CLI versions.
  • Checking nightly-* or pre-beta-* images, which are not release channels.

Accepted Residual Risks

  • The check depends on nodejs.org/dist/index.json and 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.
  • Only linux/amd64 image configs are read. Both architectures are built from the same FROM line, so a mismatch is unlikely.
  • A new intermediate Node.js stage whose name does not start with base- would be checked as an image tag and reported not published. It shows up in the table and does not break the run.
  • The issue body carries a "Last checked" timestamp, so the body is edited daily while the issue is open. Body edits do not notify.

Acceptance Criteria

  • actionlint and shellcheck are clean on the new files.
  • DRY_RUN=1 against a pinned Dockerfile reports pins current and today's stable-*/beta-* images stale, and exits 0.
  • A Dockerfile without NODE<major>_VERSION ARGs exits 1 with an ::error:: line.
  • The create, update, close, and no-op decisions are each exercised with a stubbed gh.
  • After merge, a manual workflow_dispatch run on openabdev/openab creates or updates exactly one node-security issue.

Follow-ups

  • A row for node-security-check.yml in the RELEASING.md workflow table once both PRs are merged.

At a Glance

daily cron / workflow_dispatch (openabdev/openab only)
        │
        ▼
node_security_check.sh
  ├─ Dockerfile.unified ──► pins {22: 22.23.4, 24: 24.22.1} + Node agents per major
  ├─ nodejs.org/dist/index.json ──► latest security release per major
  └─ ghcr.io configs ──► NODE_VERSION of stable-<agent>, beta-<agent> (404 = not published)
        │
        ▼
 anything stale? × open node-security issue?
   yes × no  ──► create          yes × yes ──► update in place
   no  × yes ──► comment + close no  × no  ──► nothing
 exit 0 in all four; non-zero only if the check itself breaks

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.yml runs daily at 07:47 UTC and on workflow_dispatch, with contents: read and issues: write. It is guarded with if: github.repository == 'openabdev/openab' so forks stay quiet. A concurrency group prevents a cron run and a dispatch from both creating an issue.
  • .github/scripts/node_security_check.sh (bash, curl, jq):
    • It derives each Node.js major and its agents from 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.
    • It finds the latest security: true release per major in index.json.
    • It reads NODE_VERSION from each published image config.
    • It reconciles one issue through an explicit four-state decision.
    • It writes the same tables to the job summary.
  • A pull_request dry-run job (contents: read, DRY_RUN=1) runs when the script, the workflow, or Dockerfile.unified changes. A parser break fails in its own PR.
  • The tracker issue carries a <!-- 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 as major mismatch. Every curl call has timeouts.
  • issue-check.yml and close-unlinked-stale-issues.yml skip node-security issues. 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

Validation

All runs used ubuntu:24.04 (bash 5.2, jq 1.7) in Docker as a stand-in for ubuntu-latest, against live nodejs.org and ghcr.io data.

  • Pinned copy of Dockerfile.unified (22.23.4 / 24.22.1). Both pins are current. Every stable-* image is 22.23.1 stale, every beta-* image is 22.23.3 stale, beta-kimi is 24.21.0 stale, and stable-kimi is not published. The decision is "create or update issue" and the exit code is 0.
  • Pin at 22.23.3. The 22 row is reported stale, and the exit code is 0.
  • Unmodified upstream Dockerfile.unified. Exits 1 with ::error::no 'ARG NODE<major>_VERSION=<x.y.z>' found in Dockerfile.unified.
  • An unpinned extra FROM node:24-trixie-slim AS foo exits 1. No network (--network none) exits 1 after retries.
  • With gh stubbed, the decisions came out as follows. Not stale with issue feat: /close slash command to manually terminate a thread session #42 open gives close (#42). Stale with feat: /close slash command to manually terminate a thread session #42 open gives update (#42). Stale with no issue gives create, plus a label create when the label is missing. Not stale with no issue gives none.
  • Read-only gh issue list and gh label list against openabdev/openab behave as the script expects.
  • shellcheck (stable) and actionlint 1.7.7 are clean, including on close-unlinked-stale-issues.yml.

🤖 Generated with Claude Code

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>
@chaodu-obk

chaodu-obk Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Suggestion (not a formal review): please widen what the check covers. Two cases this week would not be caught with CHANNELS=(stable beta) and the fixed REPO=openabdev/openab:

  1. pre-beta-<agent>: openab:pre-beta-* is still on Node 22.23.3 / 24.21.0, last built 2026-10-03. The cause is the pre-beta schedule gate (fix in ci(pre-beta): gate scheduled builds on main HEAD SHA #1573; a manual rebuild is running). Adding pre-beta to CHANNELS would have flagged it.
  2. Downstream ghcr.io/openabdev/openab-pty: every openab-pty image, nightly included, is built FROM openab:pre-beta-<variant>. So openab-pty:pre-beta-claude and openab-pty:nightly-claude (built 2026-10-22) are both on 22.23.3. This would need the registry repo to be a per-row parameter rather than the global REPO. Its channels are pre-beta and nightly, and it has no kimi variant. A tag that is not published already renders as "not published", which fits here.

Both are cheap additions to assess(). If you'd rather keep this PR scoped to the release channels, a follow-up is fine too. Thanks for turning these PRs around so quickly.

@chaodu-agent

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.
@chaodu-agent

Copy link
Copy Markdown
Collaborator

Note

LGTM ✅ - All four round-1 blockers are resolved in d9566724. Merge only in the declared order #1570 -> #1572 -> #1571; the new dry-run check stays red on this PR until #1570 is on main.

What This PR Does

Nothing tells maintainers when Node.js publishes a security release that openab's pins or published images are behind on. #1567 found stable-* images behind two consecutive Node.js security releases. This PR adds a daily workflow that keeps one node-security issue open while any pin or published stable-*/beta-* image is behind, and closes it when everything is current. A behind image does not fail the workflow; only a broken check does.

How It Works

  • node_security_check.sh reads the ARG NODE<major>_VERSION pins and Node stages from Dockerfile.unified. Child stages inherit their parent's major, and base-* stages are excluded.
  • It takes the latest security: true release per major from nodejs.org/dist/index.json.
  • It reads NODE_VERSION from the linux/amd64 config of each stable-<agent>/beta-<agent> image through an anonymous ghcr.io token. A 404 means not published.
  • It reconciles a single issue through four states: create, update, close, none.
  • New in this round: a dry-run job runs the script with DRY_RUN=1 on pull requests that touch the script, the workflow, or Dockerfile.unified. It has only contents: read and makes no gh calls.
  • The reconcile check job keeps the schedule and dispatch triggers, the repository guard, and a job-scoped issues: write.
  • close-unlinked-stale-issues.yml skips node-security issues.

Verified at d9566724:

  • actionlint 1.7.7 reports no issues on both workflows, and bash -n passes on the script.
  • DRY_RUN=1 against chore(docker): pin Node.js base image via NODE22_VERSION/NODE24_VERSION #1570's Dockerfile.unified and live data exits 0:
    • pins 22.23.4/24.22.1 are current;
    • all seven stable-* images are 22.23.1 stale;
    • all beta-* images are current;
    • stable-kimi is not published.
  • Against main's Dockerfile the script exits 1 with ::error::.
  • The PR dry-run check (run 39889877367) fails for exactly that reason, as intended.

Findings

# Severity Finding Location
1 🟢 Resolved: merge order is now declared in the PR body and enforced by the red dry-run check until #1570 lands PR body, .github/workflows/node-security-check.yml
2 🟢 Resolved: the #1572 dependency for the RELEASING.md runbook anchor is now declared PR body
3 🟢 Resolved: PR-time DRY_RUN=1 regression gate with least-privilege permissions .github/workflows/node-security-check.yml:9-34
4 🟢 Resolved: remediation hint derives majors (NODE[0-9]+_VERSION) .github/scripts/node_security_check.sh:149
5 🟢 Follow-up suggestions carried from round 1 (non-blocking) -
6 🟢 Optional: move concurrency to the check job to simplify the workflow-level expression .github/workflows/node-security-check.yml:18-22
7 🟢 Sound design and correct permission isolation -
Finding Details

🟢 F1: Merge order with #1570 - resolved

The PR body now has an [!IMPORTANT] block that states "#1570 -> #1572 -> #1571 (this PR last), or all three together" and gives the reason for each dependency. The new dry-run job makes the dependency visible on GitHub. On a merge ref without #1570's ARGs it fails with no 'ARG NODE<major>_VERSION=<x.y.z>' found, and it turns green on re-run once #1570 is on main. A pull_request workflow runs from the PR's merge commit, so the gate is already active on this PR.

🟢 F2: RELEASING.md anchor - resolved

The link to RELEASING.md#base-image-security-release is unchanged. The PR body now declares #1572, which adds that section, as a prerequisite. With the declared order, the link is live before this workflow can create an issue.

🟢 F3: PR-time regression gate - resolved

The trigger, permissions, and concurrency were checked event by event:

  • Trigger: pull_request, not pull_request_target, with paths: covering the script, the workflow, and Dockerfile.unified, which is everything the parser reads.
  • dry-run permissions: It inherits workflow-level contents: read and sets no GH_TOKEN. The DRY_RUN branch returns before reconcile/ensure_label, so it makes no gh calls.
  • check permissions: check holds issues: write at job level and is guarded by github.event_name != 'pull_request' && github.repository == 'openabdev/openab'. No PR event, including one from a fork, can reach it.
  • Concurrency:
    • PR runs use node-security-check-pr-<N> with cancel-in-progress: true.
    • Schedule and dispatch runs share node-security-check-reconcile without cancellation, which keeps the original create-race guard.
    • The &&/|| expression short-circuits safely on non-PR events.

Known trade-offs:

  • A PR touching Dockerfile.unified now depends on nodejs.org and ghcr.io being reachable. A brief outage fails that PR's check until a re-run, which is acceptable.
  • The gate exercises parsing and assessment only. The decide/reconcile paths remain covered by the stubbed-gh validation in the PR description, not by CI.

🟢 F4: Derived majors in the remediation hint - resolved

Line 149 now renders git grep -lE 'ARG NODE[0-9]+_VERSION' -- 'Dockerfile*'. The pattern is not expanded inside the unquoted heredoc. On #1570's tree it matches exactly the 10 Dockerfiles that declare the ARG.

🟢 F5: Follow-up suggestions (non-blocking)

Carried from round 1 for a follow-up PR or issue:

  • Cross-major mismatch. Compare ${node%%.*} with the expected major and report major mismatch. Also extend the STALE_MAJOR condition at line 112, or decide still returns none.
  • Expected-but-absent tags. Treat a beta-<agent> 404 as missing. A stable-<agent> 404 stays not published. Whether missing opens the issue is a maintainer policy choice. Opening it catches deleted tags, at the cost of a short false positive after a new Node agent is added.
  • Timeouts. Add --connect-timeout/--max-time to curl.
  • Ownership. Add an ownership marker or --author filter in reconcile, a header comment documenting the ARG/stage/tag/base-* contract, and a LABEL cross-reference to close-unlinked-stale-issues.yml.
  • Issue checks. Exempt node-security in issue-check.yml for human edits.
  • Simplification. Optional: drop the daily "Last checked" edit, use plain if branches instead of the decide() code concatenation, and label the image table "stable/beta".

🟢 F6: Optional concurrency simplification

Only check touches issues. Moving concurrency: { group: node-security-check } onto that job would replace the workflow-level ternary group: and the cancel-in-progress: expression. The trade-off is that PR runs are no longer cancelled on a new push. Not required.

🟢 F7: What is done well

See "What's Good" below.

Addressing External Reviewer Feedback

chaodu-obk[bot] (issue comment, 2026-10-23)

Widen coverage: (1) add pre-beta-<agent> to CHANNELS, since openab:pre-beta-* is still on Node 22.23.3 / 24.21.0; (2) check downstream ghcr.io/openabdev/openab-pty, which is built FROM openab:pre-beta-<variant>, by making the registry repo a per-row parameter.

ℹ️ Accepted as a follow-up; unchanged from round 1. This PR's Non-goals exclude pre-beta-* and nightly-*. Its goal is met without them. Because openab-pty ships on top of pre-beta, its staleness does reach users, so both additions are worth a follow-up issue linked to #1567 and #1573. They fit the existing design, since unpublished tags already render as not published.

Round 1 -> Round 2 Resolution
Round 1 Status Resolution
F1 merge order with #1570 not enforced ✅ Resolved Declared in the PR body and enforced by the red dry-run check
F2 runbook anchor depends on undeclared #1572 ✅ Resolved #1572 declared as a prerequisite in the PR body
F3 no PR-time regression gate ✅ Resolved pull_request + paths + DRY_RUN=1 job, contents: read only (commit d9566724)
F4 hard-coded majors in remediation hint ✅ Resolved NODE[0-9]+_VERSION (commit d9566724)
F5-F10 suggestions Open, non-blocking Carried as F5 above
Baseline Check
What's Good (🟢)
  • The check fails closed: set -euo pipefail with inherit_errexit, ::error:: annotations, and a 404-versus-other-error split. A broken check goes red, while a behind image never does.
  • Agents and majors are derived from Dockerfile.unified, so new Node agents and majors are covered without editing the script.
  • Image configs are read through the registry API, with no image pulls and no stored credentials.
  • Permissions are least privilege and improved in this round: contents: read at the workflow level, issues: write only on the guarded check job, and pull_request rather than pull_request_target.
  • The issue lifecycle is a small, explicit four-state reconcile, and DRY_RUN and the job summary make it easy to inspect.

5️⃣ Three Reasons We Might Not Need This PR

  1. Detection is not remediation. Stable images are still re-tagged rather than rebuilt, so a human must act on the issue. A tracker that stays open for weeks can be ignored.
  2. Another source of truth. chore(docker): pin Node.js base image via NODE22_VERSION/NODE24_VERSION #1570's CI assertions, the runbook, and this issue all describe whether Node.js is current. The script is about 226 lines of bash, including a small OCI client.
  3. External coupling. The check depends on the ARG convention, nodejs.org, and anonymous ghcr.io reads. A change to any of them turns the check red until someone fixes it.

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.

@thepagent
thepagent merged commit 5c6d24c into openabdev:main Oct 23, 2026
8 of 9 checks passed
@antigenius0910

Copy link
Copy Markdown
Contributor Author

Thanks for the review. The branch now has your d956672 (F3 dry-run gate, F4 generic hint) and acf845a on top of it.

Finding Status
F1 merge order with #1570 Declared in the body: #1570, then #1571, then #1572. The dry-run job reads this branch's Dockerfile.unified, so it stays red until #1570 is on main. That red check is the gate.
F2 runbook anchor from #1572 Declared in the body. Until #1572 merges, the link opens the top of RELEASING.md, which is harmless.
F3 PR-time gate d956672.
F4 hard-coded majors d956672. acf845a also states that each Dockerfile declares only its own major and that a bump needs no workflow edit.
F5 cross-major acf845a. An image or pin on a different major reports major mismatch and counts as behind. Tested with a beta-claude on 24.x: HEAD reported current, the fix reports major mismatch.
F6 missing tags Deferred to a follow-up, together with the pre-beta-* / openab-pty coverage that chaodu-obk suggested.
F7 curl timeouts acf845a (--connect-timeout 10 --max-time 30).
F8 ownership and contracts acf845a. The issue body carries <!-- node-security-check -->, and reconcile acts only on labeled issues with that marker. The label and Dockerfile contracts are documented in the script.
F9 issue-check.yml acf845a. Tracker issues are skipped. A stubbed run confirmed that without the exemption the tracker gets incomplete/needs-triage and a comment.
F10 simplifications Not taken in this PR.

Verification of acf845a: shellcheck and actionlint are clean. A DRY_RUN=1 run against a pinned Dockerfile.unified exited 0, reported stable-* as 22.23.1 stale and beta-* as current, and printed the marker. A reconcile test with stubbed gh covered marked, unmarked, and mixed issues.

antigenius0910 added a commit to antigenius0910/openab that referenced this pull request Oct 23, 2026
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>
@antigenius0910

Copy link
Copy Markdown
Contributor Author

Correction to my earlier comment: this PR was merged at d956672, before acf845a reached the branch. So F5, F7, F8, and F9 are not on main. They are now in #1574. Merge it after #1570.

thepagent pushed a commit that referenced this pull request Oct 23, 2026
…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>
thepagent pushed a commit that referenced this pull request Oct 23, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants