Skip to content

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

Open
vivekchand wants to merge 28 commits into
mainfrom
harden/enforce-sha-pinned-action-refs
Open

Harden CI: keep every action reference pinned to a commit SHA#5305
vivekchand wants to merge 28 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 and its security rationale are 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 and its security rationale are 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 to keep it up to date


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. 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.

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