Skip to content

Four action pins name a version they are not, and nothing checks #369

Description

@vladimirrott

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is neededmediumDifficulty: needs familiarity with one subsystem

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions