Skip to content

ci: fail workflow-lint on unpinned action references - #710

Merged
av1155 merged 1 commit into
mainfrom
ci/enforce-action-pin-check
Aug 4, 2026
Merged

ci: fail workflow-lint on unpinned action references#710
av1155 merged 1 commit into
mainfrom
ci/enforce-action-pin-check

Conversation

@av1155

@av1155 av1155 commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

#699 pinned all 71 action references to commit SHAs and #707 wrote the
convention down, but nothing enforced it. An edit reverting a pin to
uses: actions/checkout@v7 still merged cleanly. That revert is the natural
thing to type: it is what upstream READMEs show and what a copied snippet
contains, so it reads as normal and reviews as harmless while the guarantee
quietly goes away for that action.

workflow-lint.yml already triggers on paths: [".github/workflows/**"], which
is exactly when a pin can change and never otherwise, so the check lives there.

Closes #709

Changes

One new job in .github/workflows/workflow-lint.yml. Nothing else changes.

Why a grep rather than actionlint config. actionlint has no pinning rule.
Its checks doc lists two uses:-related rules, "Action format in uses:" and
"Outdated popular actions detection", and format validation accepts any ref
including @v4 and @main. The existing reviewdog/action-actionlint step
will never catch this, so there is no knob to turn.

Why a separate job. Given the above, a pin failure surfacing under the check
named Lint GitHub Actions workflows (actionlint) would send whoever hits it
looking for an actionlint config that does not exist. A distinctly named job
attributes the failure correctly. workflow-lint is not one of the 11 required
checks, so adding a job name here carries no branch-protection risk.

Why not a dedicated action. zgosalvez/github-actions-ensure-sha-pinned-actions
is maintained but small, and using a third-party action to enforce supply-chain
hygiene means pinning and Dependabot-tracking one more dependency in order to
police dependencies. The grep has no supply-chain surface of its own and matches
the hand-rolled precedent in version-check.yml.

Local uses: ./ composite actions are exempt because they have no SHA to pin.
None exist today (.github/actions/ is absent); the exclusion is there before
it is needed rather than after it blocks someone.

Testing

Red and green were both proven by extracting the check body straight out of the
workflow and running it, so the test exercises the shipped code rather than a
retyped copy of it.

Against the real tree:

All action references are SHA-pinned
EXIT: 0

Against a fixture copy with one pin reverted to actions/checkout@v7:

::error::Actions must be pinned as 'owner/repo@<40-char-sha> # vX.Y.Z'
.github/workflows/version-check.yml:28:        uses: actions/checkout@v7
EXIT: 1

The predicate was also checked against the cases that could bite it. Tag refs,
branch refs, and a bare SHA with no version comment are all flagged. Correctly
pinned refs pass, subpath actions like github/codeql-action/upload-sarif pass,
uses: ./ passes, and a comment line mentioning uses: x@v1 passes because the
pattern anchors on the start of the line.

actionlint reports the same single pre-existing shellcheck warning it reports
on main and nothing new, so the new run: block is shellcheck-clean. The YAML
parses with both jobs present.

No CHANGELOG entry: CI-only changes do not get one.

Type of Change

  • Bug fix (fix:)
  • New feature (feat:)
  • Refactoring (refactor:)
  • Documentation (docs:)
  • CI/CD (ci:)
  • Chore (chore:)

Checklist

  • Issue exists and is linked above with Closes #N
  • Linked issue has exactly one type:* and one priority:* label
  • Linked issue has at most one phase:* label (or none when not roadmap work)
  • Branch name matches issue scope (feat/<slug>, fix/<slug>, etc.)
  • Tests added/updated for all changes
  • Type check passes (mypy src/)
  • Lint passes (ruff check .)
  • Format passes (ruff format --check .)
  • Documentation updated (if applicable)
  • No secrets or sensitive data committed
  • Scope check: This change helps search for missing or cutoff-unmet media in a controlled way

#707 documented the SHA-pin convention but nothing checked it, so an
edit reverting a pin to `@v7` still merged. That revert is the natural
thing to type: it is what upstream READMEs show.

actionlint cannot cover this. Its two `uses:` rules validate format
and flag outdated popular actions, and both accept `@v4` and `@main`.
Hence a grep, styled after version-check.yml, rather than a config
knob that does not exist.

Its own job rather than a step in the actionlint one: a pin failure
reported under a check named actionlint would send whoever hits it
hunting for a config that is not there.

Local `uses: ./` composite actions are exempt since they have no SHA
to pin. None exist yet; the exclusion is there before it is needed.

Closes #709
@av1155
av1155 merged commit c53b9ce into main Aug 4, 2026
22 checks passed
@av1155
av1155 deleted the ci/enforce-action-pin-check branch August 4, 2026 00:00
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.

ci: fail workflow-lint on unpinned action references

1 participant