Skip to content
Open
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
61 changes: 58 additions & 3 deletions .github/workflows/node-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,33 @@ name: Node dependency audit
outputs:
validation_passed:
description: Whether candidate validation passed.
value: ${{ jobs.audit.outputs.validation_passed }}
value: ${{ jobs.validation.outputs.validation_passed }}
proposal_complete:
description: Whether proposal resolution and audit evidence completed.
value: ${{ jobs.audit.outputs.proposal_complete }}
permissions:
contents: read
jobs:
audit:
name: Node audit evidence (${{ inputs.artifact-prefix || 'dependency-audit' }})
env:
npm_config_ignore_scripts: 'true'
pnpm_config_ignore_scripts: 'true'
npm_config_ignore_pnpmfile: 'true'
pnpm_config_ignore_pnpmfile: 'true'
npm_config_lockfile_only: 'true'
pnpm_config_lockfile_only: 'true'
npm_config_package_lock_only: 'true'
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 60
outputs:
validation_passed: ${{ steps.proposal.outputs.validation_passed }}
prepared_node: ${{ steps.seal.outputs.prepared_node }}
proposal_complete: ${{ steps.proposal.outputs.proposal_complete }}
report_artifact_id: ${{ steps.proposal.outputs.report_artifact_id }}
dependencies_artifact_id: ${{ steps.proposal.outputs.dependencies_artifact_id }}
node_update_artifact_id: ${{ steps.proposal.outputs.node_update_artifact_id }}
steps:
- name: Validate manual dispatch ref
env:
Expand All @@ -54,7 +66,7 @@ jobs:
with:
ref: ${{ inputs.audit-ref || github.sha }}
persist-credentials: false
- name: Audit, propose upgrades, and validate
- name: Prepare and seal audit evidence
id: proposal
uses: $/node/audit
with:
Expand All @@ -63,6 +75,49 @@ jobs:
refresh: ${{ inputs.refresh }}
operation: ${{ inputs.operation }}
proposal-kind: ${{ inputs.proposal-kind }}
- name: Seal uploaded audit evidence
id: seal
env:
ARTIFACT_ID: ${{ steps.proposal.outputs.report_artifact_id }}
ARCHIVE_SHA256: ${{ steps.proposal.outputs.report_archive_sha256 }}
CONTENT_SHA256: ${{ steps.proposal.outputs.report_content_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 audit evidence v1::v1:%s:%s:%s\n' "$ARTIFACT_ID" "$ARCHIVE_SHA256" "$CONTENT_SHA256"
printf 'prepared_node=%s\n' "$NODE_AUDIT_NODE_VERSION" >> "$GITHUB_OUTPUT"
validation:
needs: audit
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 60
outputs:
validation_passed: ${{ steps.validation.outputs.validation_passed }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ inputs.audit-ref || github.sha }}
persist-credentials: false
- uses: $/node/project
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
artifact-ids: ${{ needs.audit.outputs.dependencies_artifact_id }}
merge-multiple: true
path: ${{ env.NPM_PROJECT }}
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
artifact-ids: ${{ needs.audit.outputs.node_update_artifact_id }},${{ needs.audit.outputs.report_artifact_id }}
merge-multiple: true
path: ${{ runner.temp }}
- name: Check the frozen candidate on a separate runner
id: validation
continue-on-error: ${{ inputs.operation != 'validate' }}
uses: $/node/audit
with:
operation: check-prepared
prepared-node: ${{ needs.audit.outputs.prepared_node }}
artifact-prefix: ${{ inputs.artifact-prefix || 'dependency-audit' }}-validation
configuration-issue:
needs: audit
if: ${{ always() && !cancelled() }}
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/node-maintenance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ jobs:
permissions:
contents: write
pull-requests: write
actions: read
checks: read
uses: ./.github/workflows/node-publish.yml
with:
validation-passed: ${{ needs.security.outputs.validation_passed }}
Expand Down Expand Up @@ -110,6 +112,8 @@ jobs:
permissions:
contents: write
pull-requests: write
actions: read
checks: read
uses: ./.github/workflows/node-publish.yml
with:
validation-passed: ${{ needs.upgrades.outputs.validation_passed }}
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/node-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ jobs:
permissions:
contents: write
pull-requests: write
actions: read
checks: read
Comment thread
omry marked this conversation as resolved.
steps:
- name: Create repository-scoped publisher token
if: ${{ inputs.app-client-id != '' }}
Expand All @@ -53,6 +55,8 @@ jobs:
repositories: ${{ github.event.repository.name }}
permission-contents: write
permission-pull-requests: write
permission-actions: read
permission-checks: read
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
token: ${{ steps.app-token.outputs.token || github.token }}
Expand Down Expand Up @@ -113,6 +117,8 @@ jobs:
PR_FOOTER: |
[Workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
PROPOSAL_KIND: ${{ inputs.proposal-kind }}
TRUSTED_WORKFLOW_SHA: ${{ job.workflow_sha }}
AUDIT_ARTIFACT_PREFIX: ${{ inputs.artifact-prefix }}
REFRESH_ONLY: ${{ inputs.refresh-only }}
EXPECTED_PR: ${{ inputs.expected-pr }}
PROPOSAL_COMPLETE: ${{ inputs.proposal-complete }}
Expand Down
42 changes: 42 additions & 0 deletions docs/node-maintenance-operator.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,45 @@ it does not promise permanent deduplication after a record is deleted.
Keep App credentials in operator storage and Worker secrets. Keep verification
receipts outside the repository, without credentials or webhook payloads. A
local test or dry-run bundle does not replace live delivery evidence.

## Sealed normal-audit evidence

The normal audit producer prepares dependency changes on a fresh read-only
runner. It disables lifecycle scripts, local/global pnpmfile hooks and dependency
materialization. Its parser installer runs in Python isolated mode. Project
validation runs later on another read-only runner with the exact prepared files.
That runner can retain check diagnostics; it cannot replace the producer artifact.
A failed proposal check still permits a PR report with failed validation. A
committed-state validation run fails when its project checks fail.

The existing `dependency-audit-pr.md` report includes one bounded canonical
version1 record with original audit advisories, severities, links, package rows,
before/candidate dependency hashes and deduplicated counts. After the report
upload succeeds, the producer emits one `Node audit evidence v1` notice on its
existing Actions job Check. The notice binds the numeric artifact ID and archive
and content SHA256 hashes. It requires no Checks write permission.

The normal publisher resolves the expected producer job in the exact current
run, then verifies its server-derived Check URL, immutable workflow revision,
completed seal steps, unique matching annotation and immutable artifact digest.
The provider run, job, Check and artifact heads must agree on the triggering
revision. The sealed checkout revision is checked separately against the planned
audit revision, which can advance before the coordinated planning checkout.
It rejects sibling-job notices, duplicate/ambiguous producer jobs, expired or
altered artifacts and mismatched project/dependency state. Native commit trailers
retain exact run/job/Check/artifact IDs and hashes for the later read-only repair
consumer. Editable PR prose is not historical audit authority.
The immutable artifact retains the canonical record while the human PR body omits
that opaque record.

Only the publisher reads Actions metadata/artifacts and Check annotations; grant
`actions: read` and `checks: read` to its calling job and any publisher App token.
The producer and project-validation jobs keep Contents read only. The isolated
pilot must provide these read grants. Both supported caller templates include
them, and fleet setup checks require them. Updating callers in existing
repositories and the installation rollout remain later delivery work.

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.
2 changes: 2 additions & 0 deletions docs/node-maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ jobs:
permissions:
contents: write
pull-requests: write
actions: read
checks: read
uses: hydra-ecosystem/github-actions/.github/workflows/node-publish.yml@v1
with:
validation-passed: ${{ needs.audit.outputs.validation_passed }}
Expand Down
65 changes: 52 additions & 13 deletions node/audit/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,47 @@ inputs:
description: Select an existing proposal channel on a default-branch push.
default: 'false'
operation:
description: propose or validate (read-only dependency validation).
description: propose, validate committed state, or check-prepared on a separate runner.
default: propose
proposal-kind:
description: Verify resolved policy agrees with the selected channel.
default: legacy
prepared-node:
description: The producer's selected Node runtime for isolated validation.
default: ''
outputs:
validation_passed:
description: Whether frozen installation and project checks passed.
value: ${{ steps.proposal.outputs.validation_passed }}
value: ${{ steps.prepared-validation.outputs.validation_passed || steps.proposal.outputs.validation_passed }}
proposal_complete:
description: Whether proposal resolution and audit evidence completed.
value: ${{ steps.proposal.outputs.proposal_complete }}
report_artifact_id:
description: Exact immutable report artifact ID.
value: ${{ steps.report.outputs.artifact-id }}
dependencies_artifact_id:
description: Exact immutable prepared dependency artifact ID.
value: ${{ steps.dependencies.outputs.artifact-id }}
node_update_artifact_id:
description: Exact immutable prepared Node update artifact ID.
value: ${{ steps.node-update.outputs.artifact-id }}
report_archive_sha256:
description: Uploaded report archive digest.
value: ${{ steps.report.outputs.artifact-digest }}
report_content_sha256:
description: Sealed report content digest.
value: ${{ steps.proposal.outputs.report_sha256 }}
runs:
using: composite
steps:
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
with:
python-version: '3.13'
- name: Install configuration and audit report parser
run: python -m pip install PyYAML==6.0.3
run: python -I -m pip install PyYAML==6.0.3
shell: bash
- name: Read project configuration
if: ${{ inputs.operation != 'check-prepared' }}
env:
AUDIT_KIND: ${{ inputs.audit-kind }}
REFRESH: ${{ inputs.refresh }}
Expand All @@ -48,14 +67,19 @@ runs:
esac
python "$GITHUB_ACTION_PATH/../runtime/project.py" --github-env "$GITHUB_ENV" --resolved-config "$RUNNER_TEMP/node-audit-resolved-config.json" --configuration-report "$RUNNER_TEMP/node-audit-configuration.json" --audit-kind "$AUDIT_KIND" --node-update "$RUNNER_TEMP/node-audit-node-update.json" "${node_args[@]}" "${refresh_args[@]}"
shell: bash
- name: Read the sealed prepared toolchain
if: ${{ inputs.operation == 'check-prepared' }}
shell: bash
run: python "$GITHUB_ACTION_PATH/../runtime/templates/dependency_audit.py" --directory "$NPM_PROJECT" --report "$RUNNER_TEMP/dependency-audit-pr.md" --check-prepared --node-update "$RUNNER_TEMP/node-audit-node-update.json" --github-env "$GITHUB_ENV"
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: ${{ env.NODE_AUDIT_NODE_VERSION }}
node-version: ${{ inputs.prepared-node || env.NODE_AUDIT_NODE_VERSION }}
check-latest: true
- name: Enable the project's pinned package manager
env:
PACKAGE_MANAGER: ${{ env.NODE_AUDIT_MANAGER }}
PACKAGE_MANAGER_VERSION: ${{ env.NODE_AUDIT_MANAGER_VERSION }}
npm_config_package_lock_only: 'false'
run: |
set -euo pipefail
if [ "$PACKAGE_MANAGER" = pnpm ]; then
Expand All @@ -65,6 +89,7 @@ runs:
fi
shell: bash
- name: Verify the declared toolchain before dependency changes
if: ${{ inputs.operation != 'check-prepared' }}
env:
AUDIT_KIND: ${{ inputs.audit-kind }}
REFRESH: ${{ inputs.refresh }}
Expand All @@ -73,15 +98,18 @@ runs:
refresh_args=()
if [ "${REFRESH:-false}" = true ]; then refresh_args=(--refresh); fi
python "$GITHUB_ACTION_PATH/../runtime/project.py" --verify-runtime --resolved-config "$RUNNER_TEMP/node-audit-resolved-config.json" --configuration-report "$RUNNER_TEMP/node-audit-configuration.json" --audit-kind "$AUDIT_KIND" "${refresh_args[@]}"
installed_node=$(node --version)
printf 'NODE_AUDIT_NODE_VERSION=%s\n' "${installed_node#v}" >> "$GITHUB_ENV"
shell: bash
- name: Retain configuration status even when preflight fails
if: ${{ always() && !cancelled() }}
if: ${{ always() && !cancelled() && inputs.operation != 'check-prepared' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ${{ inputs.artifact-prefix || 'node-audit' }}-configuration
if-no-files-found: ignore
path: ${{ runner.temp }}/node-audit-configuration.json
- name: Verify the selected proposal channel
if: ${{ inputs.operation != 'check-prepared' }}
env:
PROPOSAL_KIND: ${{ inputs.proposal-kind }}
MODE: ${{ env.NODE_AUDIT_MAINTENANCE_MODE }}
Expand All @@ -93,32 +121,42 @@ runs:
esac
shell: bash
- name: Audit, propose upgrades, and validate
if: ${{ inputs.operation != 'check-prepared' }}
id: proposal
run: |
set -euo pipefail
operation_args=()
operation_args=(--prepare-only)
case "${OPERATION:-propose}" in
propose) ;;
validate) operation_args=(--validate-only) ;;
validate) operation_args+=(--validate-only) ;;
*) echo "::error::Unknown maintenance operation"; exit 1 ;;
esac
python "$GITHUB_ACTION_PATH/../runtime/templates/dependency_audit.py" --directory "$NPM_PROJECT" --report "$RUNNER_TEMP/dependency-audit-pr.md" --github-output "$GITHUB_OUTPUT" --validation-script "$NODE_AUDIT_VALIDATION_SCRIPT" --maintenance-mode "$NODE_AUDIT_MAINTENANCE_MODE" --upgrade-scope "$NODE_AUDIT_UPGRADE_SCOPE" --minimum-release-age-days "$NODE_AUDIT_MINIMUM_RELEASE_AGE_DAYS" --resolved-config "$RUNNER_TEMP/node-audit-resolved-config.json" --node-update "$RUNNER_TEMP/node-audit-node-update.json" --local-patches-report "$RUNNER_TEMP/node-audit-local-patches.json" "${operation_args[@]}"
shell: bash
env:
OPERATION: ${{ inputs.operation }}
- name: Validate the exact prepared dependencies
id: prepared-validation
if: ${{ inputs.operation == 'check-prepared' }}
shell: bash
run: python "$GITHUB_ACTION_PATH/../runtime/templates/dependency_audit.py" --directory "$NPM_PROJECT" --report "$RUNNER_TEMP/dependency-audit-pr.md" --check-prepared --node-update "$RUNNER_TEMP/node-audit-node-update.json" --github-output "$GITHUB_OUTPUT"
- name: Retain the audit summary even when the audit fails
if: ${{ always() && !cancelled() }}
run: |
if [ -f "$RUNNER_TEMP/dependency-audit-pr.md" ]; then
cat "$RUNNER_TEMP/dependency-audit-pr.md" >> "$GITHUB_STEP_SUMMARY"
fi
shell: bash
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
- if: ${{ inputs.operation != 'check-prepared' }}
id: node-update
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ${{ inputs.artifact-prefix || 'dependency-audit' }}-node-update
if-no-files-found: error
path: ${{ runner.temp }}/node-audit-node-update.json
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
- if: ${{ inputs.operation != 'check-prepared' }}
id: dependencies
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ${{ inputs.artifact-prefix || 'dependency-audit' }}-dependencies
if-no-files-found: error
Expand All @@ -127,21 +165,22 @@ runs:
${{ env.NPM_PROJECT }}/package-lock.json
${{ env.NPM_PROJECT }}/pnpm-lock.yaml
${{ env.NPM_PROJECT }}/pnpm-workspace.yaml
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
if: ${{ always() && !cancelled() }}
- if: ${{ always() && !cancelled() }}
id: report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ${{ inputs.artifact-prefix || 'dependency-audit' }}-report
if-no-files-found: ignore
path: ${{ runner.temp }}/dependency-audit-pr.md
- name: Retain local patch status for the issue reporter
if: ${{ always() && !cancelled() }}
if: ${{ always() && !cancelled() && inputs.operation != 'check-prepared' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ${{ inputs.artifact-prefix || 'node-audit' }}-local-patches
if-no-files-found: ignore
path: ${{ runner.temp }}/node-audit-local-patches.json
- name: Fail committed-state validation when project checks fail
if: ${{ inputs.operation == 'validate' && steps.proposal.outputs.validation_passed != 'true' }}
if: ${{ inputs.operation == 'check-prepared' && steps.prepared-validation.outputs.validation_passed != 'true' }}
run: |
echo "::error::Default-branch validation failed; see the retained report."
exit 1
Expand Down
2 changes: 2 additions & 0 deletions node/caller.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ jobs:
permissions:
contents: write
pull-requests: write
actions: read
checks: read
uses: __CENTRAL_REPOSITORY__/.github/workflows/node-publish.yml@__CENTRAL_REVISION__
with:
validation-passed: ${{ needs.audit.outputs.validation_passed }}
Expand Down
2 changes: 2 additions & 0 deletions node/coordinated-caller.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
contents: write
pull-requests: write
issues: write
actions: read
checks: read
uses: __CENTRAL_REPOSITORY__/.github/workflows/node-maintenance.yml@__CENTRAL_REVISION__
with:
audit-kind: ${{ inputs.audit-kind || 'auto' }}
Expand Down
3 changes: 3 additions & 0 deletions node/publish/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@ runs:
- name: Verify and stage the Node security update
shell: bash
run: python "$GITHUB_ACTION_PATH/../runtime/node_proposal.py" --plan "$RUNNER_TEMP/node-audit-node-update.json" --github-env "$GITHUB_ENV"
- name: Verify immutable normal-audit history and retain exact native locators
shell: bash
run: python "$GITHUB_ACTION_PATH/../runtime/templates/dependency_audit.py" --directory "$NPM_PROJECT" --report "$RUNNER_TEMP/dependency-audit-pr.md" --locate-history --trusted-workflow-sha "$TRUSTED_WORKFLOW_SHA" --artifact-prefix "$AUDIT_ARTIFACT_PREFIX" --github-env "$GITHUB_ENV"
- shell: bash
run: bash "$GITHUB_ACTION_PATH/publish.sh"
Loading
Loading