Knowledgebase

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 uses where a command would have run. A step is one or the other, and what comes after uses is 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_TOKEN and every secret you hand it; read its action.yml, at least, before it reads your repository.
  • A step has both uses and run. It is one or the other, and GitHub refuses the file; the editor flips a step between the two kinds instead.
  • You expect shell or defaults.run to reach it. An action brought its own runtime; those keys touch run steps 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/checkout is 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 before actions/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://alpine is whatever latest means 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.yml declares every one, and with is where you turn them.
  • Lives under the step, as a mapping of name: value. Every value arrives as a string, so "22" and 22 are 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 required for 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 true and the action reads "true". Every input is a string; the action decides what to make of it, and yes may 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 uses at 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 @: @v6 is a tag, @main a 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. @v4 is whatever its author calls v4 today; a fix lands, a bug lands with it, and nothing in your file changed.
  • A branch. @main takes every push, the half-finished ones included; the editor names the ref a branch and warns.
  • A patch tag. @v4.2.2 never gets the fix that ships in v4.2.3; the editor warns, and says to pin @v4 for 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.0 beside it lies once someone bumps one and not the other; Dependabot's github-actions updates 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