Actions, their inputs, and pinning a version in GitHub Actions
Using somebody else's step, and choosing which version of it you trust. The three shapes of uses, the inputs an action declares under with, and what a tag, a branch, or a commit each costs you.
Most of a workflow is steps somebody else wrote. This page is the uses line that names one, the with block that feeds it, and the ref after the @ that says how far you trust it.
🧩 Action
A step someone else wrote, named by uses. It runs on your job's machine with your token and your secrets, so uses is you trusting a stranger's code with both.
- Reach for it when the step is a solved problem: checking out the code, installing a toolchain, caching, uploading an artifact, deploying. Someone wrote it, tested it, and keeps it working.
- Lives in a step, as
useswhere a command would haverun. A step is one or the other, and what comes afterusesis the next term.
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "22"
- run: npm test
Gets you in trouble when
- You pick an action on its stars. It runs with your
GITHUB_TOKENand every secret you hand it; read itsaction.yml, at least, before it reads your repository. - A step has both
usesandrun. It is one or the other, and GitHub refuses the file; the editor flips a step between the two kinds instead. - You expect
shellordefaults.runto reach it. An action brought its own runtime; those keys touchrunsteps only. - A Docker action lands on Windows or macOS. Container actions run on Linux only, and the job fails before the image is pulled.
- It needs the code and nobody checked it out. The runner starts empty;
actions/checkoutis the first step of nearly every job for that reason.
In the editor it is Uses an action on a step: Action names it, Version pins it, and What it is given holds its inputs.
🔗 uses
Where the action comes from: a repository, a path, or a Docker image.
- Reach for it when you add an action step; it is the one key the step cannot do without. What you write after it is where GitHub fetches the code from.
- Lives on the step. The same key on a job calls a whole reusable workflow instead of one action; this page is the step's.
steps:
- uses: actions/checkout@v6
- uses: actions/github-script@v7
with:
script: core.info("a public action, pinned to its major tag")
- uses: ./.github/actions/notify
- uses: docker://alpine:3.20
with:
args: echo "a Docker image, straight from Docker Hub"
The three shapes, and where each one sends GitHub looking:
| Written as | What it is | Where GitHub finds it |
|---|---|---|
actions/checkout@v6 |
A repository at a ref; owner/repo/path@ref reaches an action in a subfolder of it |
That repository on GitHub, at that tag, branch, or commit |
./.github/actions/notify |
A path inside your own repository, with no ref | The folder in your checkout, so actions/checkout has to run first |
docker://alpine:3.20 |
A Docker image, pinned by its own tag or digest | Docker Hub, or the registry the image names; the job must run on Linux |
A local action is read from the checkout, so its code is whatever the branch the run is on holds, and nothing else pins it.
Gets you in trouble when
- There is no
@at all. GitHub takes the repository's default branch, whatever it holds today, and the editor warns that the step pins nothing. ./comes beforeactions/checkout. The runner's folder is empty until then, so the path is not there and the step fails at once.- You misspell the owner or the repository. GitHub says so only when the run starts; the editor asks GitHub as you type and says so first, naming the ref.
- The action's repository is private. GitHub fetches an action from a private repository only when that repository's Actions settings allow it.
- The image has no tag.
docker://alpineis whateverlatestmeans today, which is the branch problem again under a different name.
In the editor it is the Action field on a step: the actions this repository already runs come first, and a typed one is checked against GitHub as you go.
🎛️ with
The inputs an action takes, and what happens when you get one wrong.
- Reach for it when the action has knobs: which Node version, which folder, how deep to fetch. Its
action.ymldeclares every one, andwithis where you turn them. - Lives under the step, as a mapping of
name: value. Every value arrives as a string, so"22"and22are the same thing to the action.
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
An action declares its inputs like a form: some required, some with a default, now and then one deprecated. The editor renders exactly what the action declares, nothing more.
| The action declares | What it means for you | What the editor shows |
|---|---|---|
description |
What the input is for, in the author's words | The hint under the field |
required: true |
Leave it out and the action has nothing to work with | A required badge, and an error until you fill it in |
default |
What the action takes when you say nothing | Left out it is …, under the field |
deprecationMessage |
The input still works, and the author wants you off it | The message under the field, and a warning |
Gets you in trouble when
- You leave a required input out. GitHub does not check
requiredfor you, so most actions fail the run on their first line,Input required and not supplied, and a few carry on with an empty value, which is worse. - You misspell an input. GitHub only warns in the log,
Unexpected input(s), and the action runs as if you had said nothing; the editor names it an error before you save. - You pass
trueand the action reads"true". Every input is a string; the action decides what to make of it, andyesmay well be nothing. - You bump the version and the inputs move with it. A new major can rename or drop an input; the editor re-reads what the new ref declares, so look at the block again.
- A deprecated input still works, which is how it stays in the file. It logs a warning on every run until the author removes it, and then it does nothing at all.
In the editor it is What it is given on an action step or a calling job: a field per declared input, a checkbox for a yes or no, a free row for the rest.
📌 Pinning
Choosing a tag, a branch, or a commit, and what each one means for you.
- Reach for it when you write any
usesat all. The ref after the@is the pin, and no ref is a pin to the default branch; the only question is how tight. - Lives after the
@:@v6is a tag,@maina branch,@d23441a…a commit. A comment beside a commit says which version it was, for the humans.
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- uses: actions/setup-node@v6
Every ref is one of three things, and each one moves on its own terms:
| You write | It is | It moves when | What it costs you |
|---|---|---|---|
actions/checkout@v6 |
A tag | Its author moves it; a major tag moves on every release | Nothing to update, and every change the author ships, good or bad |
actions/checkout@main |
A branch | Anyone pushes to it, finished or not | A run that breaks on a Tuesday for a change you never made |
actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 |
A commit | Never | Updating it by hand, or letting Dependabot open the pull request |
v4 is a tag, and its author can repoint it at new code tomorrow. A commit is the one ref that never moves, which makes it the safe answer for anything that touches your token.
Gets you in trouble when
- A major tag moves under you.
@v4is whatever its author calls v4 today; a fix lands, a bug lands with it, and nothing in your file changed. - A branch.
@maintakes every push, the half-finished ones included; the editor names the ref a branch and warns. - A patch tag.
@v4.2.2never gets the fix that ships in v4.2.3; the editor warns, and says to pin@v4for fixes or a commit for an exact build. - A SHA from a fork. Check the commit is in the action's own repository, not a copy with the same history and a surprise on top.
- A commit nobody updates. It is safe and it is old, and the
# v6.1.0beside it lies once someone bumps one and not the other; Dependabot'sgithub-actionsupdates move both.
In the editor the Action field says whether the ref is a tag, a branch, or a commit; Version beside it offers the released ones, and picking one rewrites the ref alone.
📚 Straight from GitHub
- uses on a step: every shape with an example, and versioned actions side by side.
- with, and inputs in the action's metadata, where required, default, and deprecationMessage are declared.
- Using third-party actions: the security hardening on pinning to a full-length commit SHA, and Dependabot version updates to keep the pins fresh.
- A Docker Hub action, and an action in the same repository as the workflow.