Skip to content

Pinning Actions To A Commit SHA


Every uses: actions/checkout@v5 line you’ve written this course has trusted a moving target. v5 isn’t a fixed piece of code — it’s a label that the action’s maintainer can point at a different commit whenever they choose. Usually that’s harmless, a maintainer pushing a patch release. But it means the exact code your workflow runs on every trigger isn’t something you’ve verified — it’s whatever v5 happens to mean today, which might be different from what it meant yesterday.

Why A Version Tag isn’t A Guarantee

A git tag is just a pointer. git tag v5 <commit> creates it, and git tag -f v5 <different-commit> moves it — nothing stops a repository owner from doing that. Most action maintainers won’t ever do this maliciously. But “most maintainers are trustworthy” isn’t the same guarantee as “this exact code is what runs in my pipeline,” and the difference matters a lot if an action’s repository — or a maintainer’s account — is ever compromised.

If that happens, everyone pinned to @v5 starts running whatever the attacker pushed to that tag, the next time their workflow triggers, with no warning and no diff to review. This isn’t hypothetical: it’s the exact mechanism behind real supply-chain incidents in the Actions ecosystem, where a popular action’s tag was quietly repointed to a commit containing credential-stealing code, and every workflow using that floating tag pulled it in automatically.

A commit SHA doesn’t have this problem, because a SHA isn’t a pointer to a commit — it is the commit, mathematically. Git computes it from the commit’s content, so actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd will always resolve to that exact code, forever. There’s no version of history where that SHA means something different.

    flowchart LR
    subgraph Tag["@v5 (a moving pointer)"]
        direction TB
        T1["Today: commit A"] -.->|maintainer can repoint| T2["Tomorrow: commit B"]
    end
    subgraph SHA["@93cb6ef... (a fixed commit)"]
        direction TB
        S1["Always: commit A"]
    end
  

Pinning actions/checkout

You’ve used actions/checkout@v5 in nearly every workflow this course. Here’s the current commit SHA behind the latest v5 release, v5.0.1:

93cb6efe18208431cddfb8368fd83d5badbf9bfd

Note

This chapter was written and verified against the real actions/checkout repository at the time of writing. Actions release new versions constantly — by the time you’re reading this, v5.0.1 may no longer be the newest release. That’s fine; the technique is what matters, not this specific SHA. A later chapter covers how to keep pinned SHAs current without doing this lookup by hand every time.

Swap the tag for the SHA, and add the version number back as a trailing comment so the workflow file stays readable:

steps:
  - name: Checkout
    uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1

That comment isn’t optional decoration — without it, actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd tells a reviewer nothing about what version is actually running. With it, anyone reading the workflow can see both the exact pinned commit and which human-readable release it corresponds to.

How to Find The SHA For Any Action Yourself

You won’t always have this course to hand you a SHA, so it’s worth knowing how to look one up. The most reliable way, from any machine with git installed, is git ls-remote against the action’s repository — no GitHub account, no API token, no rate limit:

git ls-remote --tags https://github.com/actions/checkout v5.0.1

That prints the commit SHA that tag currently points to. Two things to watch for:

  • If the action has an annotated tag, git ls-remote will show two lines for it — one for the tag object itself, and one suffixed ^{} for the commit it actually points to. You want the ^{} line’s SHA; pinning to a tag-object SHA instead of a commit SHA produces a confusing “unable to resolve action” error.
  • Run the command again before you actually commit the pin. Tags occasionally get new patch releases between when you looked something up and when you finish writing the workflow.

If you’d rather use the GitHub CLI, this does the same thing:

gh api repos/actions/checkout/commits/v5.0.1 --jq '.sha'

What This Costs You

Pinning to a SHA does have a real downside: you stop getting automatic updates. With @v5, a bug fix or security patch released under that tag reaches your workflow the next time it runs. With a SHA pin, your workflow keeps running that exact commit until you manually update the pin — even if the maintainer ships something important.

This isn’t a reason to avoid pinning. It’s a reason to pair it with a way to manage updates deliberately instead of receiving them silently. That’s exactly the gap a couple of chapters later closes: Dependabot can watch your pinned SHAs and open a pull request whenever a newer version is available, bumping both the SHA and its version comment together. You get the review step a floating tag skips, without losing updates entirely — you just see them as a diff instead of a surprise.

Tip

You don’t need to pin every action in your workflows to feel a benefit — start with the ones you’d be most worried about if compromised. actions/checkout runs in nearly every job and has access to your repository contents, which makes it a reasonable place to start.

Last updated on