12 · CI with GitHub Actions
'It works on my machine' is where bugs hide. Continuous Integration runs your tests and linter on every push and pull request, on a clean machine — so a broken change is caught at the PR, not in production. GitHub Actions defines this as a YAML workflow.
Continuous Integration runs your test suite and linter AUTOMATICALLY on every push and pull request, on a fresh machine. A GitHub Actions workflow (`.github/workflows/ci.yml`) declares WHEN to run (`on:` push/PR), WHERE (`runs-on:` a clean runner), and WHAT (`jobs:` → `steps:`: checkout, set up Python, install deps, run pytest + ruff) — turning 'it works on my machine' into a verified, green-or-red check on every change.
Without this:
Without CI, tests only run when someone remembers to run them locally — so broken code merges silently, and the bug surfaces in production or in a teammate's clone instead of at the pull request.
You have tests for your training and serving code. The problem is when they run: if running them is a manual ritual someone has to remember, they'll be skipped under deadline pressure, and a broken change will sail into main. Continuous Integration (CI) removes the human from the loop: the tests and checks run automatically, on a clean machine, on every push and pull request. A change is verified before it merges, not after it breaks something.
GitHub Actions is GitHub's built-in CI system. You add a YAML file under .github/workflows/ and GitHub runs it for you on its servers. A workflow has a small, consistent anatomy:
on:— the triggers.on: [push, pull_request]means "run this on every push and every PR". This is the heart of CI — the workflow fires by itself when code changes.jobs:— one or more units of work that run (by default) in parallel, each on its own fresh virtual machine.runs-on:— the machine each job uses, e.g.ubuntu-latest. Crucially this is a clean environment — nothing of "your machine" is there, which is exactly what catches "but it works locally" bugs (a missing dependency, an uncommitted file).steps:— the ordered commands inside a job: check out the code, set up Python, install dependencies, runpytest, runruff. Each step is either a reusable action (uses: actions/checkout@v4) or a shell command (run: pytest).
Two refinements make CI fast and thorough:
Dependency caching. Installing dependencies from scratch on every run is slow. A cache step keeps the pip download cache between runs keyed by a hash of requirements.txt, so installs are near-instant until the dependencies actually change. (The setup-python action can do this with cache: pip.)
A test matrix. strategy.matrix runs the same job across several configurations — e.g. Python 3.10, 3.11, 3.12 — in parallel, so you verify your code on every supported version at once instead of hoping it works beyond the one you developed on.
The outcome is a green ✓ or red ✗ on every pull request. A red check blocks the merge: the broken change is caught at the door. GitHub Actions runs on GitHub's servers (not the browser), so the workflow below is read-along — but on / jobs / runs-on / steps is the whole skeleton.
A `.github/workflows/ci.yml`: trigger `on` push/PR, a `test` job `runs-on: ubuntu-latest` across a Python version matrix, with steps to checkout, set up Python (with pip caching), install deps, run ruff, and run pytest.
CI/CD pipeline: each stage must pass before the next runs — a failure halts the deploy.
When does a GitHub Actions CI workflow with `on: [push, pull_request]` run?
- CI runs your tests and linter automatically on every push and pull request, on a clean machine — catching broken changes at the PR instead of in production.
- A GitHub Actions workflow has a simple anatomy: `on:` (triggers), `jobs:` (parallel units), `runs-on:` (a clean runner), and `steps:` (checkout, setup, install, run).
- Dependency caching (`cache: pip`) keeps installs fast across runs, and a `strategy.matrix` runs the job across multiple Python versions in parallel.
Every collaborative ML repo gates merges on a CI workflow: tests, linting, and often data/model checks run on each PR, so a regression is blocked before it reaches main — the same discipline that makes software teams ship safely.
If you remove it: Tests would run only when someone remembers locally, so broken code merges silently and 'works on my machine' bugs surface in a teammate's clone or in production.