Every uses: in .github/workflows is pinned to a SHA with a version comment
beside it, which is the right shape: a tag is mutable and a SHA is not. Nothing
checks that the comment is true, and four of them are not.
FAIL actions/deploy-pages pinned cd2ce8fc but v5 is 368f8252
FAIL taiki-e/install-action pinned 37f7c578 but v2 is 5bf6ce01
FAIL actions/dependency-review-action claims v4, which is not a tag in that repo
FAIL dtolnay/rust-toolchain claims stable, which is not a tag in that repo
That block is the run from 2026-09-04. Recounted at d881d49 on 2026-09-06, the
four entries are the same four and one number has moved: v2 in
taiki-e/install-action now resolves to 84f5ac31, not 5bf6ce01. v2 is a
major alias the maintainers advance, so expect it to differ again by the time you
read this. The pinned SHA 37f7c578 has not moved, which is the point of pinning.
Recounted again at b2c823e6 on 2026-09-16. Two of the four entries have
changed, so read this list rather than the one above. Dependabot bumped three
actions in #405, and upstream moved two major aliases:
FAIL github/codeql-action pinned cdf488f5 but v4 is b96794f0
FAIL taiki-e/install-action pinned e67fa11c but v2 is 26e9283f
NOTAG actions/dependency-review-action claims v4, which is not a tag in that repo
NOTAG dtolnay/rust-toolchain claims stable, which is not a tag in that repo
actions/deploy-pages left the list: #405 moved it to 368f8252, which is the
head of v5, so its comment is now true. github/codeql-action joined it, and
it is the interesting one, because nothing in this repository touched it. The
pinned cdf488f5 is the head of v4.37.9; the v4 alias has since advanced to
b96794f0. A pin under a major-alias comment starts correct and drifts on
somebody else's release schedule, which is the whole argument for the convention
this issue settles.
Nothing is broken by this. Every pinned SHA still resolves and every workflow
still runs what it ran yesterday, which is exactly what pinning is for. What is
wrong is the audit trail: the comment is the only thing telling a reader which
version a forty-character hex string is, and four of them name a version that
SHA is not on. The last two name a moving reference rather than a tag at all,
so there is nothing for a reader to check against.
The check
lacs-project/sysknife has no pin verifier. vladimirrott/maintainer-agent has
scripts/verify-action-pins.sh, which now takes a repository root, so this
output came from pointing it at this checkout:
git clone https://github.com/vladimirrott/maintainer-agent /tmp/maintainer-agent
bash /tmp/maintainer-agent/scripts/verify-action-pins.sh .
Run it from the root of your sysknife clone. It takes a repository root as its
one argument and needs gh authenticated, because it resolves each tag through
the GitHub API.
Copying that script in and adding it to docs-and-hygiene is the whole fix. One
detail matters and is the reason not to write a fresh one: annotated tags point
at a tag object, not a commit. A naive comparison of refs/tags/X.object.sha
against the pin fails for every repository that signs its tags. I wrote that
naive version by hand first and it reported eleven failures instead of four,
including github/codeql-action, which is correctly pinned. A checker with that
flag rate teaches people to ignore it.
Two decisions in it
The two "not a tag" entries need a policy, not a bump. dtolnay/rust-toolchain@stable
and actions/dependency-review-action@v4 are branch or major-alias references.
Either the comment should say so, or they should be pinned to a real tag. The
trufflesecurity/trufflehog pin is the same shape and carries the same bare
comment, # main, so it needs whatever convention this issue settles on.
Bumping a pin is a supply-chain decision. deploy-pages and
install-action have moved by however many commits; updating the pin adopts all
of them. Worth reading what changed rather than taking the new SHA because a
checker asked.
Getting started
Everything here is a workflow file and one shell script. No VM, no daemon, no LLM
provider and no credentials beyond an authenticated gh.
The seventeen pins live in .github/workflows/:
grep -rhoE 'uses:\s*[A-Za-z0-9_.-]+/[A-Za-z0-9_./-]+@[0-9a-f]{40}.*' .github/workflows/ \
| sed 's/^uses:\s*//' | sort -u
Resolve one comment by hand before you write any code, and use the annotated case,
because it is the one that will bite you:
$ gh api repos/ossf/scorecard-action/git/ref/tags/v2.4.4 --jq '.object.type, .object.sha'
tag
55891bbd73f2425e97637d96e306fc9d491d0b21
$ gh api repos/ossf/scorecard-action/git/tags/55891bbd73f2425e97637d96e306fc9d491d0b21 --jq .object.sha
2d1146689b8cda280b9bc96326124645441f03bc
55891bbd is the tag object. 2d114668 is the commit, and it is what the
workflow pins, so this action is correct. Compare against 55891bbd and you
report a failure on a pin that is right.
(This example used github/codeql-action@v4 until 2026-09-16. I swapped it
because the v4 alias moved upstream, so the old block now peels to
b96794f0 and teaches the opposite of what it was written for. v2.4.4 is an
exact version and stays put.) Four of the pinned actions carry annotated tags:
github/codeql-action, ossf/scorecard-action, Swatinem/rust-cache and
anchore/sbom-action. That is five of the seventeen uses: lines, because
github/codeql-action is pinned twice.
Lightweight tags skip the second call. gh api repos/actions/checkout/git/ref/tags/v7
returns commit and the SHA is already the commit, so branch on .object.type.
The new script belongs in scripts/, wired into the docs-and-hygiene job in
.github/workflows/ci.yml, and into scripts/ci-local.sh so a contributor gets
the same answer locally. #346 is the issue about those two lists drifting apart,
so add it to both.
Tests first
Two directions, and the second is the one that matters here.
Break what the guard protects. Add a fixture workflow whose uses: line pins
a real SHA under a version comment that names a different tag, point the checker
at it, and assert it exits non-zero and names that action. Then correct the
comment in the fixture and assert it exits zero.
Break the guard's own input. This checker has a specific way of being useless:
it can pass because it found nothing to check. Assert on the count, not just the
exit code. A fixture directory with three pinned actions must report three
verdicts, so that a regex which stops matching (a uses: written with different
spacing, a pin on its own continuation line) turns into a red test rather than a
silent green run over zero pins.
The annotated-tag case deserves its own test: pin github/codeql-action correctly
in a fixture and assert the checker calls it OK. That is the exact case a naive
implementation gets wrong, and it produced eleven failures instead of four when I
wrote the naive version by hand.
Difficulty
medium. The shell is short. Getting the tag peeling and the "not a tag" case
right is the work, and the fixture design above is most of the thinking.
Every
uses:in.github/workflowsis pinned to a SHA with a version commentbeside it, which is the right shape: a tag is mutable and a SHA is not. Nothing
checks that the comment is true, and four of them are not.
That block is the run from 2026-09-04. Recounted at
d881d49on 2026-09-06, thefour entries are the same four and one number has moved:
v2intaiki-e/install-actionnow resolves to84f5ac31, not5bf6ce01.v2is amajor alias the maintainers advance, so expect it to differ again by the time you
read this. The pinned SHA
37f7c578has not moved, which is the point of pinning.Recounted again at
b2c823e6on 2026-09-16. Two of the four entries havechanged, so read this list rather than the one above. Dependabot bumped three
actions in #405, and upstream moved two major aliases:
actions/deploy-pagesleft the list: #405 moved it to368f8252, which is thehead of
v5, so its comment is now true.github/codeql-actionjoined it, andit is the interesting one, because nothing in this repository touched it. The
pinned
cdf488f5is the head ofv4.37.9; thev4alias has since advanced tob96794f0. A pin under a major-alias comment starts correct and drifts onsomebody else's release schedule, which is the whole argument for the convention
this issue settles.
Nothing is broken by this. Every pinned SHA still resolves and every workflow
still runs what it ran yesterday, which is exactly what pinning is for. What is
wrong is the audit trail: the comment is the only thing telling a reader which
version a forty-character hex string is, and four of them name a version that
SHA is not on. The last two name a moving reference rather than a tag at all,
so there is nothing for a reader to check against.
The check
lacs-project/sysknifehas no pin verifier.vladimirrott/maintainer-agenthasscripts/verify-action-pins.sh, which now takes a repository root, so thisoutput came from pointing it at this checkout:
git clone https://github.com/vladimirrott/maintainer-agent /tmp/maintainer-agent bash /tmp/maintainer-agent/scripts/verify-action-pins.sh .Run it from the root of your
sysknifeclone. It takes a repository root as itsone argument and needs
ghauthenticated, because it resolves each tag throughthe GitHub API.
Copying that script in and adding it to
docs-and-hygieneis the whole fix. Onedetail matters and is the reason not to write a fresh one: annotated tags point
at a tag object, not a commit. A naive comparison of
refs/tags/X.object.shaagainst the pin fails for every repository that signs its tags. I wrote that
naive version by hand first and it reported eleven failures instead of four,
including
github/codeql-action, which is correctly pinned. A checker with thatflag rate teaches people to ignore it.
Two decisions in it
The two "not a tag" entries need a policy, not a bump.
dtolnay/rust-toolchain@stableand
actions/dependency-review-action@v4are branch or major-alias references.Either the comment should say so, or they should be pinned to a real tag. The
trufflesecurity/trufflehogpin is the same shape and carries the same barecomment,
# main, so it needs whatever convention this issue settles on.Bumping a pin is a supply-chain decision.
deploy-pagesandinstall-actionhave moved by however many commits; updating the pin adopts allof them. Worth reading what changed rather than taking the new SHA because a
checker asked.
Getting started
Everything here is a workflow file and one shell script. No VM, no daemon, no LLM
provider and no credentials beyond an authenticated
gh.The seventeen pins live in
.github/workflows/:Resolve one comment by hand before you write any code, and use the annotated case,
because it is the one that will bite you:
55891bbdis the tag object.2d114668is the commit, and it is what theworkflow pins, so this action is correct. Compare against
55891bbdand youreport a failure on a pin that is right.
(This example used
github/codeql-action@v4until 2026-09-16. I swapped itbecause the
v4alias moved upstream, so the old block now peels tob96794f0and teaches the opposite of what it was written for.v2.4.4is anexact version and stays put.) Four of the pinned actions carry annotated tags:
github/codeql-action,ossf/scorecard-action,Swatinem/rust-cacheandanchore/sbom-action. That is five of the seventeenuses:lines, becausegithub/codeql-actionis pinned twice.Lightweight tags skip the second call.
gh api repos/actions/checkout/git/ref/tags/v7returns
commitand the SHA is already the commit, so branch on.object.type.The new script belongs in
scripts/, wired into thedocs-and-hygienejob in.github/workflows/ci.yml, and intoscripts/ci-local.shso a contributor getsthe same answer locally. #346 is the issue about those two lists drifting apart,
so add it to both.
Tests first
Two directions, and the second is the one that matters here.
Break what the guard protects. Add a fixture workflow whose
uses:line pinsa real SHA under a version comment that names a different tag, point the checker
at it, and assert it exits non-zero and names that action. Then correct the
comment in the fixture and assert it exits zero.
Break the guard's own input. This checker has a specific way of being useless:
it can pass because it found nothing to check. Assert on the count, not just the
exit code. A fixture directory with three pinned actions must report three
verdicts, so that a regex which stops matching (a
uses:written with differentspacing, a pin on its own continuation line) turns into a red test rather than a
silent green run over zero pins.
The annotated-tag case deserves its own test: pin
github/codeql-actioncorrectlyin a fixture and assert the checker calls it
OK. That is the exact case a naiveimplementation gets wrong, and it produced eleven failures instead of four when I
wrote the naive version by hand.
Difficulty
medium. The shell is short. Getting the tag peeling and the "not a tag" caseright is the work, and the fixture design above is most of the thinking.