Using and Pinning Actions
The uses: keyword pulls in someone else's code and runs it inside your pipeline, with access to your GITHUB_TOKEN and your secrets. That is enormously convenient — checkout, language setup, deploys, and notifications are all one line — and it is also a trust decision every time. The reference you put after the @ is what decides how much trust.
A mutable tag like @v4 reads cleanly but can be silently repointed at different code; an immutable 40-character commit SHA freezes exactly what runs. The gap between those two is the difference between a reproducible build and handing a third party the ability to change your pipeline without your knowing.
The uses Syntax
The common form is owner/repo@ref. There are variants: owner/repo/path@ref targets an action in a subdirectory of a repo, ./path runs an action stored inside your own repository, and docker://image:tag runs a container action directly. The @ref at the end is the part that matters most for security.
steps: - uses: actions/checkout@v4 # marketplace, tag - uses: actions/checkout@8f4b7f... # marketplace, SHA-pinned - uses: ./.github/actions/build # local, in-repo action
Tag vs Branch vs SHA
Three kinds of ref, three risk levels. @v4 is a mutable major tag — the maintainer can move it to a new commit at will, which is how patch fixes reach you automatically and also how a compromise reaches you automatically. @main is the worst choice: it changes on every push to the action's default branch. @<full-sha> is immutable — whatever ran is what ran, and it cannot change underneath you.
Inputs and Outputs
with: supplies the action's inputs; the action declares what it accepts. Many actions also produce outputs, which a later step reads as steps.<id>.outputs.<name> once you give the uses: step an id. This is the same output channel jobs use, scoped to steps within a job.
steps:
- uses: actions/setup-node@1a4442c... # v4.0.3
with:
node-version: '20'
cache: npm
The Supply-Chain Reality
A popular action's repository — or merely its tag — is a single point of compromise for every workflow that consumes it. The 2025 tj-actions incident drove the point home: attackers moved widely used tags to a malicious commit, and thousands of repositories that pinned to those mutable tags executed the injected code with their own secrets. SHA pinning is the defense, because a SHA cannot be repointed.
Dependabot for Actions
The cost of SHA pinning is staleness — a frozen SHA never receives the upstream patch. Dependabot closes that gap: enable version-updates for the github-actions ecosystem and it opens pull requests bumping each pinned SHA to the latest release, with the version in the PR. You get immutability and a steady, reviewed update cadence at the same time.
Tag pinning (@v4) — reads cleanly and auto-receives patch fixes, but the tag is a mutable Git ref. The maintainer, or an attacker who compromises the repo, can repoint it at any commit, and your next run executes whatever it now points to.
SHA pinning (@<40-char-sha>) — immutable and auditable: what ran is exactly what ran. The trade is that it freezes you on that code until something updates it, which is why you pair it with Dependabot. For anything touching secrets or the deploy path, pin to SHA.
- Pinning a third-party action to
@mainor a branch — every push by the maintainer changes the code that executes in your pipeline with your secrets. - Trusting
@v3as if it were immutable, when major and minor tags are mutable Git refs that can be force-moved at any time. - Granting a marketplace action broad
GITHUB_TOKENpermissions when it only needscontents: read, widening the blast radius if it is compromised. - Copy-pasting a workflow with an action version from a years-old blog post, inheriting known-vulnerable code.
- SHA-pinning everything and then never enabling Dependabot, so the pins silently rot on stale, unpatched code.
- Pin third-party actions to a full commit SHA with a trailing comment naming the version (
# v4.1.7). - Trust GitHub-authored
actions/*to tags if you must, but still prefer SHA pinning on security-sensitive paths. - Enable Dependabot
version-updatesfor thegithub-actionsecosystem to keep pinned SHAs fresh through reviewed PRs. - Use a local
./action or a reusable workflow for logic you control instead of re-pulling a third party. - Restrict allowed actions org-wide via Settings → Actions policy so only vetted owners and selected actions can run.
include with a ref, CI/CD componentsCircleCI orbs, version-pinnedJenkins shared libraries by Git refAzure Pipelines tasks and templates by refKnowledge Check
Why is referencing a third-party action by @v4 a supply-chain risk?
- A tag is a mutable ref, so a maintainer or attacker can repoint it at malicious code
- Tags are slower for the runner to resolve and download than a commit SHA
- Major tags are frozen and never updated, so you are stuck running stale, unpatched action code
- GitHub forbids tag references on any security-sensitive deploy path
What does pinning to a full commit SHA protect against, and what does it cost?
- The exact code that runs cannot change under you; the cost is staleness until you bump it
- It speeds the action up by skipping ref resolution on every run, and it costs you nothing at all
- It hides the action's source from the run logs, at the cost of auditability
- It auto-updates to the newest release, at the cost of build reproducibility
How does Dependabot complement SHA pinning?
- It opens PRs bumping each pinned SHA to the latest release, so pins do not rot
- It rewrites every one of your SHA pins back into mutable major tags automatically
- It blocks any workflow that references an action by a SHA pin
- It runs the pinned action inside a sandbox at build time
A widely used action's tag is moved to a malicious commit. What is the blast radius?
- Every repo pinned to that tag runs the injected code with its own secrets next run
- Only the action's own repository is affected; consumers stay untouched
- Only the repos that SHA-pinned the action end up running the injected code on their next run
- Nothing, because GitHub detects and blocks a moved tag before any run
You got correct