Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/node-repair.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,14 @@ jobs:
expected-base: ${{ inputs.expected-base }}
selections: ${{ inputs.selections }}
prepare:
name: Node repair preparation
needs: guard
runs-on: ubuntu-latest
timeout-minutes: 50
permissions:
contents: read
actions: read
checks: read
env:
npm_config_ignore_scripts: 'true'
pnpm_config_ignore_scripts: 'true'
Expand All @@ -79,6 +82,7 @@ jobs:
with:
ref: ${{ inputs.expected-head }}
persist-credentials: false
fetch-depth: 0
- name: Prepare the selected candidate using trusted workflow code
id: prepare
uses: $/node/repair
Expand All @@ -89,3 +93,12 @@ jobs:
expected-head: ${{ inputs.expected-head }}
expected-base: ${{ inputs.expected-base }}
selections: ${{ inputs.selections }}
- name: Seal uploaded repair evidence
env:
ARTIFACT_ID: ${{ steps.prepare.outputs.candidate-artifact-id }}
ARCHIVE_SHA256: ${{ steps.prepare.outputs.candidate-archive-sha256 }}
CONTENT_SHA256: ${{ steps.prepare.outputs.candidate-report-sha256 }}
run: |
set -euo pipefail
[[ "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ && "$ARCHIVE_SHA256" =~ ^[a-f0-9]{64}$ && "$CONTENT_SHA256" =~ ^[a-f0-9]{64}$ ]]
printf '::notice title=Node repair evidence v1::v1:%s:%s:%s\n' "$ARTIFACT_ID" "$ARCHIVE_SHA256" "$CONTENT_SHA256"
29 changes: 29 additions & 0 deletions docs/node-maintenance-operator.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,32 @@ Before the cumulative repair consumer begins, verify a distinct-run hosted
readback against the reviewed source and prove a sibling-job forgery rejects.
Local API fixtures do not satisfy that hosted gate. Missing or expired history
must never be reported as an empty original advisory list.

## Cumulative repair preparation

Selected repair preparation reads the nearest native producer receipt on the
PR's first-parent history. The initial receipt names the immutable normal audit;
later receipts name the original repair preparation artifact. Editable PR text
is never used as historical audit evidence. The consumer verifies the producer
job, its Check annotation, workflow revision, artifact ID and archive/report
hashes, then checks the exact project and dependency-state binding.

Each sealed `result.md` retains the earlier canonical audit records and renders
one cumulative table. Earlier rows keep their recorded outcomes with a historical
evidence qualifier. A current unresolved finding takes precedence. An advisory
missing from today's audit is only "not observed in the current audit"; absence
is not evidence of withdrawal or a newly confirmed repair. The preparation
notice names only the currently selected repair.

The preparation runner needs Contents, Actions and Checks read access. It emits
one `Node repair evidence v1` notice after uploading exactly `candidate.patch`,
`audit.json` and `result.md`. The notice binds the immutable artifact and report
hashes. Project validation runs on a separate runner and cannot replace this
artifact. Missing, expired, ambiguous or mismatched history stops preparation
before sealing. Inspect and finish the existing proposal, or explicitly close it
and create a new normal-audit proposal; do not reset a branch containing repairs.

This checkpoint prepares cumulative evidence. It does not publish dependency
changes or authorize merging. The later publisher must retain the validated
repair marker and exact native `Node-Repair-*` locator trailers for another
read-only attempt; producer job outputs alone do not persist across runs.
15 changes: 15 additions & 0 deletions node/repair/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ outputs:
candidate-artifact-id:
description: Immutable preparation artifact ID from this run.
value: ${{ steps.candidate.outputs.artifact-id }}
candidate-archive-sha256:
description: Preparation archive SHA256 returned by immutable upload.
value: ${{ steps.candidate.outputs.artifact-digest }}
candidate-report-sha256:
description: Exact sealed cumulative result SHA256.
value: ${{ steps.report-digest.outputs.sha256 }}
runs:
using: composite
steps:
Expand Down Expand Up @@ -88,12 +94,21 @@ runs:
if: ${{ inputs.operation == 'prepare' }}
shell: bash
env:
GH_TOKEN: ${{ github.token }}
REPAIR_REQUEST: ${{ inputs.request-id }}
REPAIR_PR: ${{ inputs.pull-request }}
REPAIR_HEAD: ${{ inputs.expected-head }}
REPAIR_BASE: ${{ inputs.expected-base }}
REPAIR_SELECTIONS: ${{ inputs.selections }}
run: python "$GITHUB_ACTION_PATH/../runtime/repair.py" prepare --artifact "$RUNNER_TEMP/node-repair-candidate"
- name: Hash the sealed cumulative report
id: report-digest
if: ${{ inputs.operation == 'prepare' }}
shell: bash
run: |
set -euo pipefail
digest=$(sha256sum "$RUNNER_TEMP/node-repair-candidate/result.md")
printf 'sha256=%s\n' "${digest%% *}" >> "$GITHUB_OUTPUT"
- name: Seal the preparation artifact
id: candidate
if: ${{ inputs.operation == 'prepare' }}
Expand Down
14 changes: 13 additions & 1 deletion node/runtime/node_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,19 @@ def security_releases(major, *, read=None):
raise ValueError(
"Cannot verify Node runtime security: invalid release record"
)
version = version_tuple(release.get("version"))
value = release.get("version")
if (
isinstance(value, str)
and re.fullmatch(
r"v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)-"
r"(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)"
r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*",
value,
)
and release.get("security") is False
):
continue
version = version_tuple(value)
if version[0] != int(major):
continue
if type(release.get("security")) is not bool:
Expand Down
Loading
Loading