feat(governance): fail the linter on action pins that do not exist - #558
Conversation
The existing "Check SHA-pinned actions" step verifies a pin's SHAPE (@ + 40 hex). It cannot tell a real commit from an invented one, because a fabricated SHA is a well-formed 40-hex string. Measured across the estate 2026-07-28: 613 unique (action, SHA) pins, of which 112 (18%) do not resolve — 80 invented SHAs plus 32 pins to seven action repos that no longer exist — present in 876 COMMITTED workflow files across ~310 repo roots. The failure mode is silent: Actions resolves a `uses:` ref only at run time, and an unresolvable ref produces NO check run rather than a red one. So `gh pr checks` shows nothing, the board reads green, and the job never ran — a repo can be "fully green" with its security scanning entirely absent. Adds scripts/check-action-pins-resolve.sh and wires it into workflow-lint. Failure semantics are deliberate: HARD FAIL only on a determinate negative (GitHub answered "does not exist"), distinguishing a missing SHA from a dead action repo. Rate limits, 5xx and network loss say nothing about a pin, so they are reported LOUDLY as UNVERIFIED rather than failing — that avoids turning a GitHub incident into an estate-wide red treadmill (cf. check-workflow-staleness.sh), while keeping the gap visible instead of silently green. Verified against three repos: tangle (9/9 resolve, passes), 0patch-lsa-sentinel (catches phantom codeql SHA 29b1f65c), and aerie (catches both dead a2ml/k9-validate-action repos). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Note Automatic reviews are paused because your trial's included automatic processing has been used for this period. Upgrade now, or comment "Gitar review" to run a review anytime. Code Review ✅ Approved 3 resolved / 3 findingsAdds a governance linter check to verify that action SHA pins actually exist on GitHub, but fails due to comment-matching regex issues, quote/case-sensitivity blind spots in extraction, and 404 false positives on private repos. ✅ 3 resolved✅ Edge Case: Commented-out
|
| Compact |
|
Was this helpful? React with 👍 / 👎 | Gitar
|
…branch (#559) ## Urgent follow-up to #558 #558 merged while the pin-gate's standards checkout was still temporarily pointed at `feat/pin-existence-check`: ```yaml repository: hyperpolymath/standards ref: feat/pin-existence-check # ← should be main ``` ### Why that ref existed A bootstrap problem. The gate fetches `check-action-pins-resolve.sh` from standards, but while the PR was open the script did not yet exist on `main` — so `cp` failed with *"No such file or directory"* and the step could never be exercised. Temporarily pointing at the branch proved the gate works end-to-end in real CI: **21 pins checked, all resolved, 5 seconds, no UNVERIFIED**. The revert back to `main` was pushed, but landed *after* the merge. ### Why it must be fixed now Every consumer's Governance run currently checks out a **feature branch**. Deleting that branch — routine post-merge hygiene — breaks the gate across the estate. The branch is deliberately still alive until this merges. The bootstrap problem is gone: the script is on `main` as of #558, so `ref: main` now resolves correctly. Verified: YAML re-parses with all 11 jobs, `ref = main`, zero remaining references to the feature branch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>



The gap
The governance linter's
Check SHA-pinned actionsstep verifies a pin's shape (@+ 40 hex). It cannot verify the SHA exists — a fabricated 40-hex string passes it.That is not theoretical. Measured across the estate on 2026-07-28:
(action, SHA)pinsWhy it stayed invisible: Actions resolves a
uses:ref only at run time, and an unresolvable ref produces no check run at all — not a red one.gh pr checksshows nothing, the board reads green, and the job never ran. A repo can be "fully green" with its security scanning entirely absent.Full report:
dev-notes/estate-unresolvable-action-pins-2026-07-28.md.The change
scripts/check-action-pins-resolve.sh— dedupes(repo, sha)pairs from the caller's workflows and asks the GitHub API whether each resolves.workflow-lintjob, using the established idiom from the allowlist preflight above it (sparse-checkout standards → copy script to$RUNNER_TEMP→rm -rfthe checkout before scanning, so the standards tree is never part of the caller's workspace).Failure semantics (deliberate)
HARD FAIL only on a determinate negative — GitHub answered and the answer was "does not exist". The script distinguishes
SHA-NOT-FOUND(repin it) fromREPO-NOT-FOUND(the action is gone — vendor it, per hyperpolymath/tangle#84).Does NOT fail on indeterminate answers (rate limit, 5xx, network). Those say nothing about a pin, and failing on them would turn any GitHub incident into an estate-wide red treadmill — the exact trap
check-workflow-staleness.shdocuments. They are instead counted and reported loudly asUNVERIFIED. A fail-open that announces itself is not a fake gate; a fail-open that hides is.Rate limiting is not expected to bite:
GITHUB_TOKENallows 1,000 req/hr/repo and only unique pairs are queried (largest estate repo is well under 100).Verification
Tested against three real repos:
tangle0patch-lsa-sentinelgithub/codeql-action@29b1f65c→ exit 1 ✅aeriea2ml-validate-action+k9-validate-action→ exit 1 ✅standards' own 21 pins all resolve, so this repo passes its own new gate.YAML re-parsed (11 jobs preserved, step order correct); script passes
bash -nandshellcheck -S warningclean.Propagation caveat
Consumers pin
governance-reusable.ymlby SHA, so this step only starts running for a consumer once it re-pins to a standards SHA at or after this merge (scripts/propagate-workflow-pins.sh/ the staleness gate drive that). The script is always fetched frommain, so its logic stays current without a re-pin — but the step itself needs the newer workflow.🤖 Generated with Claude Code