Plainwork

Plainwork / Pinfix / Fix workflows failing GitHub's require SHA pinning policy

GitHub's "require actions pinned to a full-length commit SHA" setting: fix the failing workflows

Since August 2025, GitHub lets an enterprise, organisation or repository administrator require that every action a workflow uses is pinned to a full-length commit SHA. Once it is switched on, a workflow that says uses: actions/checkout@v4 fails. This is a checklist to find every offending line, fix it, and keep the pins from going stale.

What the setting does

The setting is in the Actions policies of the enterprise, organisation or repository (Settings, Actions, General). With the SHA-pinning requirement on, GitHub refuses to run a workflow that references an action by a tag or branch such as @v4 or @main. The reference must be the 40-character commit SHA. The announcement is in the GitHub changelog of 2025-08-15, and the reasoning is in GitHub's secure use reference: a full-length commit SHA is the only way to use an action as an immutable release, because tags can be moved or deleted by anyone who gets control of the action's repository.

That is not hypothetical. In March 2025 the tags of the widely used tj-actions/changed-files action were repointed at malicious code (CVE-2025-30066), and every workflow that referenced it by tag ran that code. Workflows that pinned a SHA did not.

Step 1: list every unpinned reference

Do not trust a search for @v. Unpinned references also hide behind branches (@main), short SHAs, reusable workflows and composite actions. Paste a workflow into the free pinning checker to see each unpinned line, or run the offline check from the repository root:

npx @plainworkdigital/pinfix check .

It reads .github/workflows and any action.yml, prints every uses: line that is not a full SHA, and exits with code 1 if it finds any, so you can also use it as a CI step that stops new unpinned lines from arriving.

Step 2: pin each line to the commit, not the tag object

The pin must be the SHA of the commit. For an annotated tag, the tag object has its own SHA, and that is not the one GitHub wants. The reliable way is to ask GitHub for the commit the tag resolves to:

gh api repos/actions/checkout/commits/v4 --jq .sha

Write it with the human-readable version as a trailing comment so you and your update bot can still tell what the pin means:

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

Pinfix does this for you: pinfix pin .github/workflows/ci.yml --write rewrites one file and picks the most specific version tag for the comment. The VS Code extension offers the same as a quick fix on the line.

Step 3: the cases that trip people up

  • Reusable workflows. uses: org/repo/.github/workflows/build.yml@main needs a SHA too, and the actions used inside the called workflow are only as safe as that workflow. If you do not own it, ask the owner to pin them or copy the workflow into your repository.
  • Composite actions in your own repository. Pin the actions used inside your action.yml as well. Pin them regardless: it is the same risk, and it keeps your own action safe for others to use.
  • Local actions. uses: ./.github/actions/setup points at files in the same commit, so there is nothing to pin.
  • docker:// references. Pin these to an image digest (@sha256:...) instead. See how to find the right digest.
  • Variables. A reference built from ${{ ... }} cannot be pinned by a tool. Replace it with a literal.
  • Private actions. A tool without access to the private repository cannot look up its SHA. Use gh api with your own token.

Step 4: keep the pins current

A pin also freezes security fixes. Left alone, a SHA from last year runs last year's bugs. Two ways to keep moving forward: let Dependabot or Renovate open update pull requests (both understand the # v4.2.2 comment convention and update the SHA and the comment together), or refresh by hand on a schedule. Either way, check the result now and then. A version comment that no longer matches its SHA is a sign that someone edited one half by hand. Pinfix Pro includes pinfix verify for that drift check and pinfix update to move pins to the newest release in the same major version.

What a pin does not do

A SHA proves you run the same code tomorrow as today. It does not prove the code was safe in the first place. Pin from a version you have looked at, prefer actions from owners you trust, and grant the workflow only the permissions it needs.

A short rollout plan for an organisation

  1. Run the check across your repositories and count the unpinned lines per repository.
  2. Pin the busiest repositories first and merge those pull requests.
  3. Turn on Dependabot or Renovate for the github-actions ecosystem so the pins get updated.
  4. Switch on the SHA-pinning requirement once the counts are zero, so no team is surprised by a failing build.
  5. Keep the offline check in CI so new unpinned lines are caught in the pull request, not on the next run.

Try it

Start with the free checker, or install Pinfix from the VS Code Extensions view (search Pinfix) or with npx @plainworkdigital/pinfix check. Finding unpinned lines and pinning one file at a time are free; Pro ($9, one-time) pins a whole repository in one command.

More from the catalogue

Other small tools

See everything →