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:
runexecutes one or more shell commands directly on the runner. You write the exact commands; nothing is abstracted away.usesinvokes a packaged action — someone else’s (or your own) reusable unit of automation, referenced by name and version, likeactions/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 -laPush 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 -laPush 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.pyEach 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 -laNeither 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.