Skip to content

Steps Deep Dive


Every step you’ve written so far has used run — a raw shell command. But there’s a second, arguably more important step type: uses, which calls a prepackaged, reusable action instead of writing the equivalent shell yourself. Understanding when a runner actually has your repository’s files and when it doesn’t is the whole point of this chapter.

Two Kinds Of Step

A step does exactly one of two things:

  • run executes one or more shell commands directly on the runner. You write the exact commands; nothing is abstracted away.
  • uses invokes a packaged action — someone else’s (or your own) reusable unit of automation, referenced by name and version, like actions/checkout@v4.

Think of run as “do this shell command” and uses as “run this program someone already built.” Both live in the same steps list and execute in the same order, top to bottom.

The Runner Starts Empty

Here’s something that trips up almost everyone the first time: a fresh runner does not have your repository’s code on it. It’s a clean virtual machine with nothing but the OS and preinstalled tools — no clone, no checkout, nothing of yours.

Prove it. Add a new step to hello.yml that tries to list your repository’s files:

jobs:
  greet:
    runs-on: ubuntu-latest
    steps:
      - name: Say hello
        run: echo "Hello from GitHub Actions!"
      - name: List files
        run: ls -la

Push it and check the List files step in the logs. You’ll see the runner’s default working directory — empty, or close to it. No app.py, no .github/, nothing that looks like your repository. The workflow file itself got pulled in to start the job, but the rest of your repository’s contents were never fetched.

Fixing It With uses

This is exactly what actions/checkout exists to solve. Add it as the first step, before anything that needs your files:

jobs:
  greet:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
      - name: Say hello
        run: echo "Hello from GitHub Actions!"
      - name: List files
        run: ls -la

Push this version, and check List files again. Now app.py and .github/workflows/hello.yml show up, because actions/checkout@v4 cloned your repository onto the runner before the later steps ran.

Tip

actions/checkout is so close to universal that if a workflow is doing anything with your repository’s actual files, its first step is almost always a checkout. The next chapter covers where actions like this come from and how to pick versions of them safely.

Steps Share The Runner’s Filesystem

Within a single job, steps run sequentially, and each one can see whatever the previous steps left behind on disk — that’s how checkout’s clone becomes visible to later run steps at all. This only holds within one job, though. As you’ll see once this section covers multiple jobs, a different job gets an entirely separate, brand-new runner with none of that context carried over.

Multi-Line run And Working Directory

Two small but useful run capabilities worth knowing now:

      - name: Inspect the checkout
        run: |
          pwd
          ls -la
          cat app.py

Each line in a | block runs as its own shell command in sequence, sharing the same working directory and environment for the whole step.

If you need a step to operate somewhere other than the repository root, working-directory changes it for just that step, without affecting any other step:

      - name: Inspect from a subdirectory
        working-directory: .github/workflows
        run: ls -la

Neither of these needs to go into your actual hello.yml right now — they’re worth knowing exist for when a real workflow needs them.

Clean Up

Leave hello.yml with the checkout step in place — you’ll build directly on it in the next chapter, where actions/setup-python gets added alongside actions/checkout to actually run app.py.

Last updated on