In a GitHub Actions workflow step, what is the difference between uses: and run:?
answer
- two mutually exclusive step keys
- one invokes packaged code
- the other executes commands
- with: belongs to only one of them
- each run: gets a fresh shell
basics
~20 sA step either invokes packaged code with uses:, which points at an action by repository, local path, or Docker image and takes inputs via with:, or executes shell commands with run:. One step cannot have both keys.
solid answer
~40 sEvery step inside a GitHub Actions job is exactly one of two kinds. A `uses:` step invokes a packaged **action** — `actions/checkout@v4`, `./.github/actions/setup`, or `docker://alpine:3.20` — and passes parameters through `with:`, whose names are defined by that action's `action.yml`. A `run:` step executes shell commands on the runner, one shell process per step, in `github.workspace` by default; a non-zero exit code fails the step. The two keys are mutually exclusive: a step containing both is rejected when GitHub parses the workflow. Keys such as `name`, `id`, `if`, `env`, `continue-on-error` and `timeout-minutes` are valid on both kinds, while `with:` is only meaningful on `uses:` and `shell:`/`working-directory:` only on `run:`. Because each `run:` step is a fresh shell, a `cd` or an exported variable does not survive into the next step.
code
yaml · 20 linesjobs:
build:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: backend
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
id: build
run: |
./gradlew --no-daemon build
echo "artifact=build/libs/app.jar" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
with:
name: app-jar
path: backend/${{ steps.build.outputs.artifact }}go deeper
Recall that a step either calls an action with uses: or runs commands with run:, never both, and that with: only accompanies uses:. Be able to write a two-step job that checks out the repository and then runs a build command.
Explain that each run: step is its own shell process, so directory changes and exports do not persist, and that $GITHUB_ENV and $GITHUB_PATH are the supported way to carry values forward. Know where defaults.run can be declared.
Show judgment about when to inline a script versus depend on a third-party action, and mention the trust boundary that a uses: reference creates along with SHA pinning. Be ready to explain pipefail behaviour biting a build that silently passed.
Own the standard: which steps are allowed to be ad hoc shell, which must go through a reviewed internal composite action, and how defaults and shared env keep dozens of repositories consistent without copy-pasted YAML.
## The two shapes of a step A GitHub Actions job carries an ordered `steps:` list. Every entry in that list runs on the same runner, in the same checkout directory, in the order written — and every entry is one of exactly two shapes: - **`uses:`** — invoke somebody's packaged code (an *action*). - **`run:`** — execute shell commands yourself. They are mutually exclusive within a single step. A step with both keys is not "action first, script second" — GitHub rejects the workflow file when it parses it, and the run never starts. If you need both behaviours, write two steps. ## `uses:` — invoking an action `uses:` takes a reference to a unit of packaged, reusable code: - `owner/repo@ref` — the common case, e.g. `actions/checkout@v4`. The `ref` may be a tag, a branch, or a full commit SHA. - `owner/repo/subdirectory@ref` — an action stored in a subfolder of a repository. - `./path/to/action` — an action living in the *same* repository, resolved relative to the workspace, so a checkout must have happened first. - `docker://image:tag` — a container image run directly as the step. Parameters go in `with:`, and their names are not free-form: the action's `action.yml` declares each input, whether it is required, and its default. Passing a name the action does not declare is silently ignored rather than helpful, which is why a typo in `with:` usually shows up as "the action did nothing". What the action *does* — checkout, cache restore, cloud login — is opaque to your workflow; it just runs and may expose step outputs. ## `run:` — executing commands `run:` takes a command string, or a multi-line block via YAML's `|`: ``` - run: | ./gradlew build ls build/libs ``` Things worth internalising about `run:`: - **One shell process per step.** State that lives only in the shell — the current directory, an exported variable, a shell function — is gone when the step ends. To carry a variable forward, append `NAME=value` to the file named by `$GITHUB_ENV`; to add a directory to `PATH` for later steps, append it to `$GITHUB_PATH`. - **Working directory** defaults to the workspace (`github.workspace`). Override per step with `working-directory:`. - **Shell** defaults to `bash` on Linux and macOS runners and `pwsh` on Windows runners; set it explicitly with `shell:`. Setting `shell: bash` explicitly is not identical to the default — the explicit form runs with `-o pipefail`, so a failing command in the middle of a pipe fails the step. - **Exit code is the verdict.** Non-zero fails the step, and by default every later step in the job is skipped. ## Keys that apply to both `name`, `id`, `if`, `env`, `continue-on-error` and `timeout-minutes` are valid on either shape. `id` matters most: it is how later steps reference this step's outputs through the `steps` context. ## Cutting repetition with defaults and env Rather than repeating `shell:` and `working-directory:` on every command step, set them once: ``` defaults: run: shell: bash working-directory: backend ``` This block is legal at workflow level and at job level, and it affects `run:` steps only — an action invoked with `uses:` decides its own working directory. Similarly, an `env:` map may sit at workflow, job, or step level, with the innermost definition winning. ## Choosing between them Reach for `run:` when the logic is specific to this repository and short: invoking your build tool, moving a file, printing a diagnostic. Reach for `uses:` when the work is generic and someone has already solved the cross-platform and caching details — checking out the repository, setting up a language toolchain, restoring a cache, uploading an artifact. The cost of `uses:` is trust: a third-party action executes with access to the job's workspace and whatever secrets you hand it, which is why teams pin such references to a full commit SHA rather than a moving tag. The cost of `run:` is that you own the portability and the error handling yourself.
- How do you change the shell that a run: step uses in GitHub Actions?Set `shell:` on the step, or set it once for the job or workflow under `defaults.run.shell`. The implicit default is bash on Linux and macOS runners and pwsh on Windows. Note that writing `shell: bash` explicitly is not a no-op: the explicit form adds `-o pipefail`, so a command failing mid-pipeline fails the step instead of being masked by a successful final command.
- Why does an exported variable in one run: step not appear in the next one?Each `run:` step is a separate shell process, so its environment dies with it. To pass a value forward, append `NAME=value` to the file path in `$GITHUB_ENV`; later steps in the same job see it as an environment variable. To extend `PATH`, append the directory to `$GITHUB_PATH`. For a value another *job* needs, use step outputs plus job outputs instead.
- What is the difference between with: and env: on a step?`with:` supplies the declared inputs of the action named in `uses:`, and is only meaningful on such a step; the input names must match the action's `action.yml`. `env:` sets environment variables for the step's process, which works for either shape and is how a `run:` script receives values. Passing an undeclared name under `with:` has no effect.
saying these in an interview costs you the question
- Claims a step can use uses: and run: together
- Thinks a cd in one run step persists to the next
- Uses with: to pass variables into a run: script
- Assumes run: always executes in bash on every runner
- Believes with: keys are free-form rather than declared by the action