Skip to content

Harden CI: keep every action reference pinned to a commit SHA - #5305

Merged
vivekchand merged 30 commits into
mainfrom
harden/enforce-sha-pinned-action-refs
Sep 10, 2026
Merged

Harden CI: keep every action reference pinned to a commit SHA#5305
vivekchand merged 30 commits into
mainfrom
harden/enforce-sha-pinned-action-refs

Conversation

@vivekchand

@vivekchand vivekchand commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Product record: No-PRD: CI-only change confined to scripts/, tests/ and .github/, all PRD-exempt paths.

Risk: Low, and it is a gate rather than behaviour. Nothing in the shipped package changes — no runtime code, no dependency. The failure mode is a future PR that adds a floating tag now going red in the ordinary CI matrix instead of merging quietly; that is the intent. Undone by reverting. All existing references are already pinned, so both checks pass on this tree today — this cannot turn main red on merge.

Summary

  • Pinning this repo's action references was done over a series of changes, one workflow family at a time. Nothing kept it done. A job runs perfectly well against a floating tag, so one convenient uses: actions/checkout@v4 in a later PR reverts a slice of that work with nothing going red. This turns the finished cleanup into a one-way ratchet, the same shape as the existing acceptance-criteria gate: it catches "untouched code stopped satisfying a property", not "this diff is wrong".
  • Why it matters: a tag is mutable. The repository that owns @v4 can repoint it at new code, and that code then runs inside our jobs with our token. Several of these workflows hold contents: write, publish to PyPI, or deploy — a commit SHA cannot be repointed.
  • Closes a blind spot found while doing this: collect_refs() globbed only .github/workflows/, so the two uses: lines inside .github/actions/setup-openclaw/action.yml were never checked by anything, despite running with the calling job's token. Discovery now covers composite actions too, and reports repo-relative paths so multiple action.yml files stay distinguishable.

Where the check runs

Enforcement Runs on
scripts/check_action_refs.py SHA pinning added to the offline half, beside the existing shape check supply-chain.yml
tests/test_action_refs_pinned.py (new) Same check, auto-discovered and parametrised per reference Every PR, via the lint job in ci.yml

The script's resolution half needs a token and only runs in supply-chain.yml, so the test is what makes this gate every pull request. Local (./) and docker:// references are out of scope by construction — not third-party, and the shared _USES pattern does not match them.

Test plan

  • python3 scripts/check_action_refs.py → exit 0: "all N reference(s) are pinned to a commit SHA" across every workflow/composite-action file
  • pytest tests/test_action_refs_pinned.py → all pass (the case count tracks the reference count, since discovery is automatic)
  • Negative test — the gate actually bites. Temporarily un-pinned one workflow ref (ci.ymlactions/checkout@v4) and one composite-action ref (setup-openclaw/action.ymlactions/cache@v4). Script exits 1 naming both; the test reports one failure per un-pinned reference. The composite-action ref is one the previous scanner could not see. Both files restored, git diff clean.
  • The guard is wired into CI. This repo runs explicit FILE LISTS, not pytest tests/, so a test named in no workflow runs in no job at all. tests/test_action_refs_pinned.py is named in the lint job of .github/workflows/ci.yml, beside the other FILE-LIST guards — that job is aggregated by scripts/e2e_gate.py, so a failure there blocks the merge.
  • yaml.safe_load over all 36 workflow files — all parse
  • Existing guards unaffected: tests/test_workflow_yaml_valid.py + tests/test_ci_workflow_invocations_are_real.py → 537 passed, 318 skipped; tests/test_e2e_gate.py + tests/test_verification_matrix.py + tests/test_c6_required_checks_single_source.py → 54 passed
  • make lint-py reports the same error count on this branch as on clean main — pre-existing in cli.py and unrelated; this change adds none

Note for reviewers

This is the follow-up proposed on clawmetry-cloud#2163, where a Drift Bot finding correctly identified that check_action_refs.py accepted a tag, a branch, or a commit — so pinning could regress with CI staying green. That finding pointed at this file, which could not be fixed from the cloud repo.


Generated by Claude Code

@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 2 potential drift finding(s)

1. Blueprint: Release Verification and Merge Gating

File: tests/test_action_refs_pinned.py:1

The new test file implements an automated gate for GitHub action SHA pinning that runs on every PR, but the Release Verification and Merge Gating blueprint does not document this component or its discovery mechanism. The blueprint's SecurityAuditScanner is documented to scan for unpinned action references, but there is no documented test component that auto-discovers and parametrizes each reference for testing on every pull request.

2. Blueprint: Release Verification and Merge Gating

File: tests/test_action_refs_pinned.py:63

The test calls check_action_refs.source_files() on line 63 to discover composite action files in .github/actions/, but this function does not exist in the current scripts/check_action_refs.py. The test's dependency on a non-existent function will cause it to fail at runtime.

@@ -0,0 +1,85 @@
"""Every `uses:` reference must stay pinned to a full commit SHA.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The new test file implements an automated gate for GitHub action SHA pinning that runs on every PR, but the Release Verification and Merge Gating blueprint does not document this component or its discovery mechanism. The blueprint's SecurityAuditScanner is documented to scan for unpinned action references, but there is no documented test component that auto-discovers and parametrizes each reference for testing on every pull request.

"""
scanned = {os.path.relpath(p, REPO_ROOT) for p in check_action_refs.source_files()}
actions_dir = os.path.join(REPO_ROOT, ".github", "actions")
if not os.path.isdir(actions_dir):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The test calls check_action_refs.source_files() on line 63 to discover composite action files in .github/actions/, but this function does not exist in the current scripts/check_action_refs.py. The test's dependency on a non-existent function will cause it to fail at runtime.

Copy link
Copy Markdown
Owner Author

Drift Bot finding 2 was correct and has been fixed in d432451. Finding 1 needs someone with Factory access.

Finding 2 — source_files() did not exist. Correct, and my mistake.

Not a false positive: the first commit landed tests/test_action_refs_pinned.py without the scripts/check_action_refs.py changes it calls. A git stash/pop while I was measuring the lint-py baseline unstaged the script, and the commit took only what was staged. My verification ran against the working tree, which had both halves — so it passed locally while the pushed commit could not have.

Reproduced and fixed:

Commit pytest tests/test_action_refs_pinned.py
e7d0e25 (was pushed) 1 failed, 18 passed — AttributeError on source_files, at the exact line Drift Bot named
d432451 (now) 21 passed

Both runs in a clean git worktree of the commit itself rather than my working tree, which is the check that would have caught this the first time. python3 scripts/check_action_refs.py also exits 0 from that same clean tree: 19 reference(s), all pinned, across 35 files.

E2E Gate (required) was not a second failure — its log shows it failed solely on 'drift-bot': failure, so it should clear with the gate it aggregates.

Finding 1 — blueprint does not document the new test component

Accurate, and I can't close it from here: the Blueprint lives in Software Factory, and this repository has no file that would satisfy it. Recording what it would need to say, so whoever updates it doesn't have to re-derive it:

tests/test_action_refs_pinned.py enforces SHA pinning on every pull request via the ordinary CI matrix. It auto-discovers .github/workflows/*.{yml,yaml} plus .github/actions/**/action.{yml,yaml} through check_action_refs.source_files() and parametrises one test per distinct owner/repo@ref. It complements SecurityAuditScanner/check_action_refs.py, which enforces the same property but runs only in supply-chain.yml and whose resolution half needs a token.

Worth noting the two findings point the same way: the blueprint documents SecurityAuditScanner as scanning for unpinned references, but until this PR nothing actually enforced pinning — check_action_refs.py accepted a tag, a branch, or a commit. That gap is what clawmetry-cloud#2163 hit. So the blueprint described a control the code did not implement; this PR implements it, and the blueprint now needs to describe how.

Happy to adjust naming or placement if the Blueprint would rather this live somewhere other than tests/.


Generated by Claude Code

@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 2 potential drift finding(s)

1. Blueprint: Release Verification and Merge Gating

File: tests/test_action_refs_pinned.py:1

The new test file implements an automated parametrized gate for GitHub action SHA pinning that runs on every PR and covers both workflows and composite actions. The SecurityAuditScanner component in the blueprint documents scanning for unpinned action references but does not document the test-based enforcement mechanism, per-reference parametrization, composite action coverage, or that the check runs on every pull request.

2. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:60

The SecurityAuditScanner in the blueprint specifies scanning "workflow definitions" for unpinned action references, but the implementation now includes composite actions under .github/actions/ as well. The code explicitly documents why (composite actions run with the calling job's token) but this expanded scope is not reflected in the blueprint's responsibilities.

@@ -0,0 +1,85 @@
"""Every `uses:` reference must stay pinned to a full commit SHA.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The new test file implements an automated parametrized gate for GitHub action SHA pinning that runs on every PR and covers both workflows and composite actions. The SecurityAuditScanner component in the blueprint documents scanning for unpinned action references but does not document the test-based enforcement mechanism, per-reference parametrization, composite action coverage, or that the check runs on every pull request.

@@ -46,20 +59,33 @@
_SHA = re.compile(r"^[0-9a-f]{40}$")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The SecurityAuditScanner in the blueprint specifies scanning "workflow definitions" for unpinned action references, but the implementation now includes composite actions under .github/actions/ as well. The code explicitly documents why (composite actions run with the calling job's token) but this expanded scope is not reflected in the blueprint's responsibilities.

Copy link
Copy Markdown
Owner Author

Status: green everywhere except drift-bot, which needs a Blueprint edit I can't make

Correcting my earlier comment before it misleads anyone: it was written at 18:43:03 against the first Drift Bot round, and a second round landed 17 seconds later against d432451. So "finding 2 was correct and has been fixed" is now stale — that was round 1's finding 2 (the missing source_files()), and it is fixed. Round 2 has a different finding 2.

Current head d432451: 34 of 35 checks green, no merge conflict. E2E Gate (required) fails on exactly one aggregated input — drift-bot: 2 findings — and both of those findings are now Blueprint-documentation drift against Release Verification and Merge Gating. Neither is a code defect, and neither can be closed from this repository.

The two edits the Blueprint needs

1 — SecurityAuditScanner's scope is wider than documented. It is specified as scanning "workflow definitions". It now also scans .github/actions/**/action.{yml,yaml}. That was the point of the change: a composite action's own uses: lines run with the calling job's token, so scanning only .github/workflows/ left the shorter path uninspected.

2 — the per-PR enforcement component is undocumented. Suggested wording:

tests/test_action_refs_pinned.py enforces SHA pinning on every pull request through the ordinary CI matrix. It auto-discovers .github/workflows/*.{yml,yaml} plus .github/actions/**/action.{yml,yaml} via check_action_refs.source_files() and parametrises one test per distinct owner/repo@ref. It complements SecurityAuditScanner / check_action_refs.py, which enforces the same property but runs only in supply-chain.yml and whose resolution half requires a token.

Both findings point the same direction, which is worth stating plainly: the Blueprint has described SecurityAuditScanner as scanning for unpinned references, but until this PR nothing actually enforced pinning — check_action_refs.py accepted a tag, a branch, or a commit alike. That gap is what clawmetry-cloud#2163 ran into. The Blueprint documented a control the code did not implement; this PR implements it, and the Blueprint now has to describe how.

Unblocking

Per FLYWHEEL §1f, drift-bot is a commit status from the 8090-software-factory App, not an Actions run — there is no re-run button, and merging red leaves this PR permanently red even after the Blueprint is corrected. So the order is: update the Blueprint, then push a real commit here so a fresh round runs against a new head. I'm not pushing an empty commit to force that, and I'm not narrowing the composite-action coverage to dodge finding 1 — that would drop a control that exists for a reason and would not clear finding 2 anyway.

Standing down on this PR until someone with Factory access makes those edits. Happy to adjust naming or placement if the Blueprint would rather the per-PR check live somewhere other than tests/.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

blocked on author decision — skipping (auto-mergeability sweep)

Drift Bot found 2 drift finding(s) on the head commit — requires blueprint alignment before this can merge.


Generated by Claude Code

@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 3 potential drift finding(s)

1. Blueprint: Release Verification and Merge Gating

File: tests/test_action_refs_pinned.py:1

A new automated test-based gate for GitHub action SHA pinning runs on every PR and parametrizes tests per action reference. The SecurityAuditScanner component in the blueprint documents scanning for unpinned action references but does not document this test-based enforcement mechanism, per-reference parametrization, or that the check runs on every pull request.

2. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:60

The SecurityAuditScanner's responsibilities document scanning "workflow definitions" for unpinned action references, but the implementation now includes composite actions under .github/actions/ as well. The code explicitly documents why (composite actions run with the calling job's token) but this expanded scope is not reflected in the blueprint.

3. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:120

The updated offline check now enforces that every action reference must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4. The blueprint documents scanning for "unpinned action references" but does not document this SHA pinning requirement or the specific failure mode and remediation message shown to users.

@@ -0,0 +1,85 @@
"""Every `uses:` reference must stay pinned to a full commit SHA.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

A new automated test-based gate for GitHub action SHA pinning runs on every PR and parametrizes tests per action reference. The SecurityAuditScanner component in the blueprint documents scanning for unpinned action references but does not document this test-based enforcement mechanism, per-reference parametrization, or that the check runs on every pull request.

@@ -46,20 +59,33 @@
_SHA = re.compile(r"^[0-9a-f]{40}$")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The SecurityAuditScanner's responsibilities document scanning "workflow definitions" for unpinned action references, but the implementation now includes composite actions under .github/actions/ as well. The code explicitly documents why (composite actions run with the calling job's token) but this expanded scope is not reflected in the blueprint.

Copy link
Copy Markdown
Owner Author

New head 9ac0d98: code still green, but the Blueprint now needs a third edit

Updating the standing-down comment above rather than repeating it — acting on its two-edit list would leave this PR red, because round 3 of Drift Bot added one.

The merge of main is verified safe. Re-ran everything in a clean worktree of 9ac0d98:

  • python3 scripts/check_action_refs.py → exit 0, 19 references, all SHA-pinned, across 35 files
  • pytest tests/test_action_refs_pinned.py → 21 passed
  • yaml.safe_load over all 35 workflow + composite-action files → parse

Worth noting the ratchet did its job here: main brought changes to ci.yml, conformance-heartbeat.yml and release-canary.yml into this branch, and every reference in them is pinned. That is the check passing against code it did not grow up with. The PR's diff against main is still exactly the same two files, byte-identical to d432451.

The third edit

scripts/check_action_refs.py:120 — the pinning requirement itself is undocumented. The Blueprint describes scanning for "unpinned action references" but never states that a reference must be a full 40-character commit SHA, nor what the check tells a developer when it rejects one. Suggested wording:

The offline check rejects any uses: reference whose ref is not a full 40-character commit SHA, floating tags such as @v4 included. On failure it names each offending reference with its file, and prints the remediation: pin to the SHA with the version in a trailing comment (uses: owner/action@<sha> # vX.Y.Z), resolved via git ls-remote.

So the full set is now three: scanner scope widened to composite actions, the per-PR test component, and this.

The pattern worth your attention

Round 1: 2 findings, one a genuine code bug (fixed). Round 2: 2 findings, reshaped. Round 3: 3 findings. Each round against a green tree has reworded the existing findings and now added one. That is not a bug report converging on a fix — it reads as the Blueprint being materially behind this component rather than off by a sentence, and there is no guarantee a fourth round against the same code stops at three.

I am not going to keep chasing it round by round. Flagging it so whoever edits the Blueprint knows to reconcile the whole SecurityAuditScanner section against the code as it now stands, rather than patching in three paragraphs and hoping the next round is empty.

Everything else is unchanged: no merge conflict, drift-bot is the only failing status, and E2E Gate (required) fails solely on it.


Generated by Claude Code

@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 3 potential drift finding(s)

1. Blueprint: Release Verification and Merge Gating

File: tests/test_action_refs_pinned.py:1

A new automated parametrized test-based gate for GitHub action SHA pinning runs on every PR, but the blueprint's SecurityAuditScanner component does not document this test component, its per-reference parametrization, or that this check is enforced on every pull request rather than only during supply-chain verification.

2. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:60

The SecurityAuditScanner's responsibilities document scanning "workflow definitions" for unpinned action references, but the implementation now includes composite actions under .github/actions/ as well, with explicit documentation of why (composite actions run with the calling job's token). This expanded scope is not reflected in the blueprint.

3. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:145

The offline check now enforces that every action reference must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4. The blueprint documents scanning for "unpinned action references" but does not document this specific SHA pinning requirement, the remediation message, or that tags are mutable and pose a security risk.

@@ -0,0 +1,85 @@
"""Every `uses:` reference must stay pinned to a full commit SHA.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

A new automated parametrized test-based gate for GitHub action SHA pinning runs on every PR, but the blueprint's SecurityAuditScanner component does not document this test component, its per-reference parametrization, or that this check is enforced on every pull request rather than only during supply-chain verification.

@@ -46,20 +59,33 @@
_SHA = re.compile(r"^[0-9a-f]{40}$")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The SecurityAuditScanner's responsibilities document scanning "workflow definitions" for unpinned action references, but the implementation now includes composite actions under .github/actions/ as well, with explicit documentation of why (composite actions run with the calling job's token). This expanded scope is not reflected in the blueprint.

unpinned = []

# Offline shape check: a ref must be non-empty and not obviously templated.
# Offline checks, so these run on every PR rather than only where a token

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The offline check now enforces that every action reference must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4. The blueprint documents scanning for "unpinned action references" but does not document this specific SHA pinning requirement, the remediation message, or that tags are mutable and pose a security risk.

Copy link
Copy Markdown
Owner Author

PR sweep update (automated): Drift Bot is still reporting 3 findings on this PR after a update_pull_request_branch merge from main was applied. The branch is now current with eb264a7e, but the blueprint contradictions detected by Drift Bot persist on the new HEAD (8447304c).

This means the drift findings are in this PR's own diff, not in stale base commits. Manual review is needed: the PR's changes likely conflict with a product requirement recorded in the blueprints. Please check the Drift Bot findings and either update the implementation to match the blueprints, or update the blueprint if the requirement has legitimately changed.

Status: drift-bot: failure — blocked from merging until resolved.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

PR janitor (CI triage): This PR is BLOCKED. The E2E Gate (required) check fails because the drift-bot commit status reports 3 drift finding(s) on head commit 8447304 (took 32s to analyse). All other CI checks pass.

The drift-bot finding is a real code-review result, not a transient failure — rerunnning won't clear it. Resolve the drift-bot findings to unblock the E2E Gate.


Generated by Claude Code

@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 3 potential drift finding(s)

1. Blueprint: Release Verification and Merge Gating

File: tests/test_action_refs_pinned.py:1

A new parametrized test gate for GitHub action SHA pinning runs on every PR and auto-discovers action references, but the SecurityAuditScanner component does not document this test-based enforcement mechanism, its per-reference parametrization, or that the check runs on every pull request rather than only during supply-chain verification.

2. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:60

The SecurityAuditScanner's responsibilities document scanning "workflow definitions" for unpinned action references, but the implementation now includes composite actions under .github/actions/ as well, with explicit documentation of why (composite actions run with the calling job's token). This expanded scope is not reflected in the blueprint.

3. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:145

The offline check now enforces that every action reference must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4. The blueprint documents scanning for "unpinned action references" but does not specify this SHA requirement, the security rationale (tags are mutable), or the remediation guidance provided to users.

@@ -0,0 +1,85 @@
"""Every `uses:` reference must stay pinned to a full commit SHA.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

A new parametrized test gate for GitHub action SHA pinning runs on every PR and auto-discovers action references, but the SecurityAuditScanner component does not document this test-based enforcement mechanism, its per-reference parametrization, or that the check runs on every pull request rather than only during supply-chain verification.

@@ -46,20 +59,33 @@
_SHA = re.compile(r"^[0-9a-f]{40}$")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The SecurityAuditScanner's responsibilities document scanning "workflow definitions" for unpinned action references, but the implementation now includes composite actions under .github/actions/ as well, with explicit documentation of why (composite actions run with the calling job's token). This expanded scope is not reflected in the blueprint.

unpinned = []

# Offline shape check: a ref must be non-empty and not obviously templated.
# Offline checks, so these run on every PR rather than only where a token

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The offline check now enforces that every action reference must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4. The blueprint documents scanning for "unpinned action references" but does not specify this SHA requirement, the security rationale (tags are mutable), or the remediation guidance provided to users.

@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 3 potential drift finding(s)

1. Blueprint: Release Verification and Merge Gating

File: tests/test_action_refs_pinned.py:1

A new parametrized test gate for GitHub action SHA pinning runs on every PR and auto-discovers action references, but the SecurityAuditScanner component in the blueprint does not document this test-based enforcement mechanism, its per-reference parametrization, or that the check runs on every pull request rather than only during supply-chain verification.

2. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:60

The SecurityAuditScanner component describes scanning "workflow definitions" for unpinned action references, but the implementation now includes composite actions under .github/actions/, with explicit code-level documentation that composite actions run with the calling job's token. This expanded scope is not reflected in the blueprint's responsibilities.

3. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:145

The offline check now enforces that every action reference must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4, with security rationale (tags are mutable) and remediation guidance. The blueprint documents scanning for "unpinned action references" but does not specify this SHA requirement or security context.

@@ -0,0 +1,85 @@
"""Every `uses:` reference must stay pinned to a full commit SHA.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

A new parametrized test gate for GitHub action SHA pinning runs on every PR and auto-discovers action references, but the SecurityAuditScanner component in the blueprint does not document this test-based enforcement mechanism, its per-reference parametrization, or that the check runs on every pull request rather than only during supply-chain verification.

@@ -46,20 +59,33 @@
_SHA = re.compile(r"^[0-9a-f]{40}$")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The SecurityAuditScanner component describes scanning "workflow definitions" for unpinned action references, but the implementation now includes composite actions under .github/actions/, with explicit code-level documentation that composite actions run with the calling job's token. This expanded scope is not reflected in the blueprint's responsibilities.

Copy link
Copy Markdown
Owner Author

blocked on author decision — skipping (auto-mergeability sweep)

The E2E Gate required check is failing because the Drift Bot (8090-software-factory[bot]) found Blueprint documentation gaps. These cannot be resolved by editing code in this repository — they require updating the relevant Blueprints in Software Factory (factory.8090.ai).


Generated by Claude Code

@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 3 potential drift finding(s)

1. Blueprint: Release Verification and Merge Gating

File: tests/test_action_refs_pinned.py:1

A new parametrized test gate for GitHub action SHA pinning runs on every PR and auto-discovers action references, but the SecurityAuditScanner component in the blueprint does not document this test-based enforcement mechanism, its per-reference parametrization, or that the check runs on every pull request rather than only during supply-chain verification.

2. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:60

The SecurityAuditScanner component describes scanning "workflow definitions" for unpinned action references, but the implementation now includes composite actions under .github/actions/ with explicit documentation that composite actions run with the calling job's token. This expanded scope is not reflected in the blueprint's responsibilities.

3. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:145

The offline check now enforces that every action reference must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4, with security rationale (tags are mutable) and remediation guidance. The blueprint documents scanning for "unpinned action references" but does not specify this SHA requirement or security context.

@@ -0,0 +1,85 @@
"""Every `uses:` reference must stay pinned to a full commit SHA.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

A new parametrized test gate for GitHub action SHA pinning runs on every PR and auto-discovers action references, but the SecurityAuditScanner component in the blueprint does not document this test-based enforcement mechanism, its per-reference parametrization, or that the check runs on every pull request rather than only during supply-chain verification.

@@ -46,20 +59,33 @@
_SHA = re.compile(r"^[0-9a-f]{40}$")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The SecurityAuditScanner component describes scanning "workflow definitions" for unpinned action references, but the implementation now includes composite actions under .github/actions/ with explicit documentation that composite actions run with the calling job's token. This expanded scope is not reflected in the blueprint's responsibilities.

@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 3 potential drift finding(s)

1. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:59

The blueprint specifies SecurityAuditScanner scans "workflow definitions" for unpinned action references, but the implementation now also scans composite actions under .github/actions/ because their uses: lines run with the calling job's token. This expanded scope is not documented in the blueprint.

2. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:149

The blueprint specifies SecurityAuditScanner scans for "unpinned action references," but the implementation now enforces that references must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4 due to tag mutability risk. This stricter SHA-pinning requirement is not documented in the blueprint.

3. Blueprint: Release Verification and Merge Gating

File: .github/workflows/ci.yml:203

The blueprint specifies SecurityAuditScanner performs "Reporting rather than gating," but the new test_action_refs_pinned.py test runs in the merge-gating lint job on every pull request, enforcing SHA pinning and blocking merges rather than reporting findings as artifacts.

@@ -46,20 +59,33 @@
_SHA = re.compile(r"^[0-9a-f]{40}$")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The blueprint specifies SecurityAuditScanner scans "workflow definitions" for unpinned action references, but the implementation now also scans composite actions under .github/actions/ because their uses: lines run with the calling job's token. This expanded scope is not documented in the blueprint.

# is available: a ref must be non-empty, not templated, and pinned to a
# full commit SHA.
for (action, ref), files in sorted(refs.items()):
if not ref or "${{" in ref:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The blueprint specifies SecurityAuditScanner scans for "unpinned action references," but the implementation now enforces that references must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4 due to tag mutability risk. This stricter SHA-pinning requirement is not documented in the blueprint.

Comment thread .github/workflows/ci.yml
# supply-chain.yml; this line is what applies the ratchet to every PR.
# Named explicitly because this job runs FILE LISTS.
- name: Action references stay pinned to a commit SHA
run: python3 -m pytest tests/test_action_refs_pinned.py -q

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The blueprint specifies SecurityAuditScanner performs "Reporting rather than gating," but the new test_action_refs_pinned.py test runs in the merge-gating lint job on every pull request, enforcing SHA pinning and blocking merges rather than reporting findings as artifacts.

Copy link
Copy Markdown
Owner Author

✨ auto-fixed: merged latest main into branch (was BEHIND; no conflicts)


Generated by Claude Code

@vivekchand vivekchand left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test plan & review notes

Repo: vivekchand/clawmetry

What changed

  • scripts/check_action_refs.py now covers composite actions under .github/actions/ (previously only workflows/), and adds an offline SHA-pin check (previously only the live-resolution path enforced pinning). New tests/test_action_refs_pinned.py wires the offline check into the ordinary CI matrix so any future @v4-style reference fails the PR, not just supply-chain scans.

Smoke commands

# Fast: the new CI gate test
python3 -m pytest tests/test_action_refs_pinned.py -v

# Offline script check (should print "OK ... all references pinned")
python3 scripts/check_action_refs.py

# Guard-the-guard: confirm composite actions are discovered
python3 -c "
import sys; sys.path.insert(0, 'scripts')
import check_action_refs
files = check_action_refs.source_files()
actions = [f for f in files if '.github/actions' in f]
print(f'Discovered {len(files)} total files, {len(actions)} composite action file(s)')
print('Workflow+action refs:', len(check_action_refs.collect_refs()))
"

# Full CI suite
make test

Likely failure modes from the diff

  • If any newly added workflow (merged after this PR's head) contains a tag reference (@v4) instead of a SHA, test_action_refs_pinned.py will fail parametrically with a clear message. This is the intended behavior — just means whoever added it needs to pin it.
  • test_composite_actions_are_scanned will pytest.skip() if .github/actions/ doesn't exist — correct behavior on a repo without composite actions, but worth verifying the repo actually has composite actions so the test isn't a no-op.
  • The path-relative reporting change (os.path.relpath(path, REPO_ROOT) vs os.path.basename) means error output now shows full relative paths — cosmetic improvement, verify nothing parses the old basename format.

Issue link

  • No Closes #N in the PR body. PR is marked No-PRD: CI-only. Status looks clean; the remaining blocked state is likely a required-reviewer gate (owner cannot self-approve on this repo) rather than a CI failure. Worth confirming with another team member if you need a second approval.

Generated by Claude Code

@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 3 potential drift finding(s)

1. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:59

The blueprint specifies SecurityAuditScanner scans "workflow definitions" for unpinned action references, but the implementation now also scans composite actions under .github/actions/ because their uses: lines run with the calling job's token.

2. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:149

The blueprint specifies SecurityAuditScanner scans for "unpinned action references," but the implementation now enforces that references must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4 due to tag mutability risk.

3. Blueprint: Release Verification and Merge Gating

File: .github/workflows/ci.yml:203

The blueprint specifies SecurityAuditScanner performs "Reporting rather than gating," but the new test_action_refs_pinned.py test runs in the merge-gating lint job on every pull request, enforcing SHA pinning and blocking merges rather than reporting findings.

@@ -46,20 +59,33 @@
_SHA = re.compile(r"^[0-9a-f]{40}$")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The blueprint specifies SecurityAuditScanner scans "workflow definitions" for unpinned action references, but the implementation now also scans composite actions under .github/actions/ because their uses: lines run with the calling job's token.

# is available: a ref must be non-empty, not templated, and pinned to a
# full commit SHA.
for (action, ref), files in sorted(refs.items()):
if not ref or "${{" in ref:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The blueprint specifies SecurityAuditScanner scans for "unpinned action references," but the implementation now enforces that references must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4 due to tag mutability risk.

Comment thread .github/workflows/ci.yml
# supply-chain.yml; this line is what applies the ratchet to every PR.
# Named explicitly because this job runs FILE LISTS.
- name: Action references stay pinned to a commit SHA
run: python3 -m pytest tests/test_action_refs_pinned.py -q

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The blueprint specifies SecurityAuditScanner performs "Reporting rather than gating," but the new test_action_refs_pinned.py test runs in the merge-gating lint job on every pull request, enforcing SHA pinning and blocking merges rather than reporting findings.

@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 3 potential drift finding(s)

1. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:59

The blueprint specifies SecurityAuditScanner scans "workflow definitions" for unpinned action references, but the implementation now also scans composite actions under .github/actions/ because their uses: lines run with the calling job's token.

2. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:149

The blueprint specifies SecurityAuditScanner scans for "unpinned action references", but the implementation now enforces that references must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4 due to tag mutability risk.

3. Blueprint: Release Verification and Merge Gating

File: .github/workflows/ci.yml:211

The blueprint specifies SecurityAuditScanner performs "Reporting rather than gating", but the new test_action_refs_pinned.py test runs in the merge-gating lint job on every pull request, enforcing SHA pinning and blocking merges rather than reporting findings.

@@ -46,20 +59,33 @@
_SHA = re.compile(r"^[0-9a-f]{40}$")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The blueprint specifies SecurityAuditScanner scans "workflow definitions" for unpinned action references, but the implementation now also scans composite actions under .github/actions/ because their uses: lines run with the calling job's token.

# is available: a ref must be non-empty, not templated, and pinned to a
# full commit SHA.
for (action, ref), files in sorted(refs.items()):
if not ref or "${{" in ref:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The blueprint specifies SecurityAuditScanner scans for "unpinned action references", but the implementation now enforces that references must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4 due to tag mutability risk.

Comment thread .github/workflows/ci.yml
# several of these workflows hold `contents: write` or publish to PyPI.
# scripts/check_action_refs.py makes the same check, but only in
# supply-chain.yml; this line is what applies the ratchet to every PR.
# Named explicitly because this job runs FILE LISTS.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The blueprint specifies SecurityAuditScanner performs "Reporting rather than gating", but the new test_action_refs_pinned.py test runs in the merge-gating lint job on every pull request, enforcing SHA pinning and blocking merges rather than reporting findings.

@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 3 potential drift finding(s)

1. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:50

The blueprint specifies SecurityAuditScanner scans "workflow definitions" for unpinned action references, but the implementation now also scans composite actions under .github/actions/ because their uses: lines run with the calling job's token.

2. Blueprint: Release Verification and Merge Gating

File: scripts/check_action_refs.py:149

The blueprint specifies SecurityAuditScanner scans for "unpinned action references", but the implementation now enforces that references must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4 due to tag mutability risk.

3. Blueprint: Release Verification and Merge Gating

File: .github/workflows/ci.yml:211

The blueprint specifies SecurityAuditScanner performs "Reporting rather than gating", but the new test_action_refs_pinned.py test runs in the merge-gating lint job on every pull request, enforcing SHA pinning and blocking merges rather than reporting findings.

@@ -38,6 +50,7 @@

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The blueprint specifies SecurityAuditScanner scans "workflow definitions" for unpinned action references, but the implementation now also scans composite actions under .github/actions/ because their uses: lines run with the calling job's token.

# is available: a ref must be non-empty, not templated, and pinned to a
# full commit SHA.
for (action, ref), files in sorted(refs.items()):
if not ref or "${{" in ref:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The blueprint specifies SecurityAuditScanner scans for "unpinned action references", but the implementation now enforces that references must be pinned to a full 40-character commit SHA, rejecting floating tags like @v4 due to tag mutability risk.

Comment thread .github/workflows/ci.yml
@@ -209,6 +209,17 @@ jobs:
- name: Architecture docs name modules that exist
run: python3 -m pytest tests/test_module_map_drift.py -q

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Release Verification and Merge Gating

The blueprint specifies SecurityAuditScanner performs "Reporting rather than gating", but the new test_action_refs_pinned.py test runs in the merge-gating lint job on every pull request, enforcing SHA pinning and blocking merges rather than reporting findings.

vivekchand and others added 2 commits September 10, 2026 10:50
Drift Bot found three, all correct and all the same omission: the code
enforces rules the product record does not state.

  1. scope now includes composite actions under .github/actions/, because
     their uses: lines run with the CALLING job's token;
  2. "pinned" now means a full 40-character SHA, not any non-empty ref, since
     a tag is mutable and can be repointed after review;
  3. sharpest: this GATES, while the blueprint specifies SecurityAuditScanner
     as reporting rather than gating.

The third is a real distinction, not a wording fix. SecurityAuditScanner is
non-gating on purpose -- it audits against a vulnerability feed, where a new
disclosure must not redden an unrelated PR. Pinning is different in kind: a
property of the diff under review, decidable offline, with no external feed
that can flip a green PR overnight. So it is its own check and the scanner's
posture is unchanged -- the same scanner-reports/guard-gates split already
recorded for WorkflowIntegrityGuard.

The blueprint now carries four contracts and an ADR for why the gate checks
pin SHAPE offline and leaves target resolution behind CLAWMETRY_LIVE_CHECKS=1:
a required check that depends on a third-party API fails for reasons unrelated
to the diff, and a gate that fails for unrelated reasons is one people learn
to satisfy without reading (ADR-012).

Verified the guard actually goes red rather than merely existing: unpinning one
actions/checkout ref fails both the script and the test, with a message naming
the file and giving the git ls-remote command to resolve the SHA. Clean again
on restore -- 17 references across 40 workflow and composite-action files, all
pinned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xb6A5G74JiMe3zHFs1JZEP
@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

@vivekchand
vivekchand merged commit 5d63f1c into main Sep 10, 2026
38 checks passed
@vivekchand
vivekchand deleted the harden/enforce-sha-pinned-action-refs branch September 10, 2026 09:09
@vivekchand

Copy link
Copy Markdown
Owner Author

Merged and live on main. CI-only (scripts/, tests/, .github/) — nothing ships in the wheel, so no [RELEASE] is involved.

Verified against origin/main after merge:

$ python3 scripts/check_action_refs.py
Found 17 distinct action reference(s) across 40 workflow/composite-action file(s).
OK (offline checks only): all 17 reference(s) are pinned to a commit SHA.

$ python3 -m pytest tests/test_action_refs_pinned.py -q
19 passed

The ratchet is applied to every PR, not just supply-chain.ymlci.yml:221 names the test in the lint job, which matters here because CI runs explicit file lists and an unlisted test runs in no job at all.

The guard was checked for going red, not just for existing. Unpinning one actions/checkout reference fails both the script and the test, with a message that names the file and hands over the resolution command:

actions/checkout@v4 used by .github/workflows/ci.yml

A tag is mutable; the repository that owns it can repoint it at new code, which
then runs in our jobs with our token. Pin to the full 40-character commit SHA
and keep the version in a trailing comment:
    uses: owner/action@<40-hex-sha> # vX.Y.Z
Resolve the SHA with: git ls-remote https://github.com/owner/action refs/tags/vX.Y.Z

On Drift Bot's three findings — all correct, and the third was a real distinction rather than a wording problem. The blueprint specifies SecurityAuditScanner as reporting rather than gating, and this PR gates. Those are different in kind: the scanner audits against a vulnerability feed, where a new disclosure must not redden an unrelated PR, whereas pinning is a property of the diff under review, decidable offline, with no external feed that can flip a green PR overnight. So it is enforced by its own check and the scanner's posture is unchanged — the same scanner-reports/guard-gates split already recorded for WorkflowIntegrityGuard.

The Release Verification and Merge Gating blueprint now carries four contracts (a full 40-char SHA; composite actions under .github/actions/ are in scope because their uses: lines run with the calling job's token; this gate is not SecurityAuditScanner; the live check stays opt-in) and an ADR for why the gate checks pin shape offline — a required check that depends on a third-party API fails for reasons unrelated to the diff, and a gate that fails for unrelated reasons is one people learn to satisfy without reading.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants