Skip to content

feat(governance): fail the linter on action pins that do not exist - #558

Merged
hyperpolymath merged 3 commits into
mainfrom
feat/pin-existence-check
Jul 28, 2026
Merged

feat(governance): fail the linter on action pins that do not exist#558
hyperpolymath merged 3 commits into
mainfrom
feat/pin-existence-check

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

The gap

The governance linter's Check SHA-pinned actions step 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:

Unique (action, SHA) pins 613
Unresolvable 112 (18%)
— real repo, invented SHA 80
— action repo itself is gone 32 (7 actions)
Committed workflow files affected 876
Repo roots affected ~310

Why 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 checks shows 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

  • New scripts/check-action-pins-resolve.sh — dedupes (repo, sha) pairs from the caller's workflows and asks the GitHub API whether each resolves.
  • Wired into the workflow-lint job, using the established idiom from the allowlist preflight above it (sparse-checkout standards → copy script to $RUNNER_TEMPrm -rf the 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) from REPO-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.sh documents. They are instead counted and reported loudly as UNVERIFIED. 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_TOKEN allows 1,000 req/hr/repo and only unique pairs are queried (largest estate repo is well under 100).

Verification

Tested against three real repos:

Repo Result
tangle 9/9 resolve → exit 0
0patch-lsa-sentinel catches phantom github/codeql-action@29b1f65cexit 1
aerie catches both dead a2ml-validate-action + k9-validate-actionexit 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 -n and shellcheck -S warning clean.

Propagation caveat

Consumers pin governance-reusable.yml by 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 from main, so its logic stays current without a re-pin — but the step itself needs the newer workflow.

🤖 Generated with Claude Code

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>
Comment thread scripts/check-action-pins-resolve.sh
Comment thread scripts/check-action-pins-resolve.sh
Comment thread scripts/check-action-pins-resolve.sh
@gitar-bot

gitar-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

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

Code Review ✅ Approved 3 resolved / 3 findings

Adds 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 uses: lines are scanned and can hard-fail the gate

📄 scripts/check-action-pins-resolve.sh:66 📄 scripts/check-action-pins-resolve.sh:104-118 📄 scripts/check-action-pins-resolve.sh:138-149
The pair-extraction regex \buses:[[:space:]]*...@[0-9a-f]{40} (line 66) is not YAML-aware and matches any line containing uses:, including commented examples such as # uses: some/action@<40hex>. The pre-existing "Check SHA-pinned actions" step anchors on ^[[:space:]]+uses:, so the two gates disagree: a fabricated example SHA sitting in a comment resolves to 404/422 and triggers an exit 1 hard fail even though it is never executed. Given the gate runs estate-wide, anchor the match to a real step key (e.g. require leading indentation ^[[:space:]]*(-[[:space:]]+)?uses:) or strip comments before extraction.

Edge Case: Quoted or uppercase uses: pins silently escape the check

📄 scripts/check-action-pins-resolve.sh:66-70
The extraction regex only matches an unquoted ref beginning with [A-Za-z0-9_.-] and a lowercase [0-9a-f]{40} SHA. A quoted pin (uses: "owner/repo@<sha>") fails to match because " follows the whitespace, and an uppercase-hex SHA is skipped entirely. Such pins are silently omitted from the resolve check — the exact silent-green blind spot this gate is meant to close. Broaden the pattern to allow an optional leading quote and [0-9a-fA-F]{40} (normalizing case before the API call).

Edge Case: Private/inaccessible action repo 404 causes false REPO-NOT-FOUND fail

📄 scripts/check-action-pins-resolve.sh:107-113
GitHub returns 404 for a repository the token cannot see, indistinguishably from a genuinely deleted repo. A legitimately private third-party action pin (where github.token lacks cross-repo access) therefore hits the HTTP = 404 branch (line 111) and is reported as REPO-NOT-FOUND, forcing exit 1. This contradicts the script's stated fail-only-on-determinate-negative philosophy, since access-denied is not evidence the pin is bad. Consider treating a repo-endpoint 404 as UNVERIFIED when no auth token access to that repo can be confirmed, or documenting that private cross-org actions are unsupported.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@hyperpolymath
hyperpolymath merged commit e354431 into main Jul 28, 2026
15 of 20 checks passed
@hyperpolymath
hyperpolymath deleted the feat/pin-existence-check branch July 28, 2026 19:44
@sonarqubecloud

Copy link
Copy Markdown

hyperpolymath added a commit that referenced this pull request Jul 28, 2026
…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>
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.

1 participant