Your first workflow file
GitHub's built-in CI is GitHub Actions, enabled by committing a YAML file, the same format as compose in lesson 5-1, under .github/workflows/ in the repo. That is the entire setup, with no server to install.
name: ci
on:
push:
branches: [main]
pull_request:| Key | Meaning |
|---|---|
name: ci | the label shown in the repo's Actions tab |
on: | the events that trigger this workflow |
push: branches: [main] | every push to main |
pull_request: | every pull request |
When a trigger fires, GitHub boots a fresh virtual machine called a runner and executes the jobs on it.
A fresh machine on every run is the point. There is no leftover state and no works-on-my-runner, which solves the lesson 1-1 problem by brute force.
Jobs and steps
The rest of the file:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: pytest| Key | Meaning |
|---|---|
jobs: | one or more jobs, each on its own runner |
runs-on: ubuntu-latest | which kind of runner VM to use |
steps: | commands executed top to bottom |
uses: | a reusable action from the marketplace |
with: | parameters for an action |
run: | a plain shell command |
Each step stops the job if it exits non-zero, exactly as lesson 7-1 described. actions/checkout@v4 is the essential one, cloning the repo onto the runner, and without it the runner is an empty machine.
That is the whole vocabulary: events, jobs, steps, uses for shared actions, and run for shell.
Why checkout comes first
- uses: actions/checkout@v4 is almost always the first step because the runner starts as an empty fresh machine, and checkout clones the repository onto it.
Every run gets a brand-new runner VM with no trace of the project on it, and checkout performs the git clone.
| Step order | Outcome |
|---|---|
checkout, then pytest | works |
pytest, then checkout | fails, there are no files yet |
Any step that needs the code, which is nearly all of them, must come after it. The failure mode when it is forgotten is confusing, because the error looks like a missing test file rather than a missing repository.
Where workflow files live
Workflow files live in .github/workflows/.
It is a hidden folder at the repo root, starting with a dot, with two levels: .github, then a folder named after what these files define. Any .yml file committed there becomes a workflow.
| Path | Role |
|---|---|
.github/workflows/ci.yml | a workflow |
.github/workflows/release.yml | another, independent workflow |
This means CI configuration is version-controlled and code-reviewed like everything else, and a broken pipeline can be fixed with git revert. It also means a pull request can change the pipeline that tests it, which is worth remembering when reviewing.
A trigger filtered to one branch
With on: push: branches: [main], a push to a feature branch called new-login does not run the workflow. The trigger is filtered to pushes on main only.
The on: block is a filter, and push events on other branches do not match branches: [main].
| Event | Matches the filter |
|---|---|
push to main | yes |
push to new-login | no |
pull request from new-login | only if pull_request is listed |
Teams usually add pull_request as a second trigger, as in this lesson's example, so feature branches still get tested through their pull requests. Without it, problems are only found after a merge, which defeats the purpose.