Every Python project deserves a safety net. Continuous integration (CI) is that net: every time you push code, a machine checks it for you. It runs your linter, runs your tests, and fails loudly the moment something breaks — before a bad commit sneaks into your shared main branch. Today you'll set up your first pipeline, and it takes only a few minutes.
What you're building
A GitHub Actions workflow is a YAML file that lives in your repository at .github/workflows/ci.yml. GitHub reads it, watches for the events you name, and runs your jobs on a fresh virtual machine every time.
The pipeline you'll write does two things:
- Lint your code with Ruff, a fast Python linter written in Rust.
- Run your test suite with pytest.
If either step fails, the whole run turns red and your commit or pull request shows the failure right where it matters.
How the workflow fits together
Every workflow has the same skeleton: a name, an on block that says when to run, and a jobs section listing what to do. Each job declares a runs-on — the kind of machine it uses, here ubuntu-latest — and a list of steps. A step either uses a prebuilt action or runs a shell command.
Two official actions do the heavy lifting: actions/checkout@v4 copies your repository onto the runner, and actions/setup-python@v5 installs the Python version you ask for.
The workflow
Drop this into .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- name: Check out the code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with Ruff
run: ruff check --output-format=github .
- name: Test with pytest
run: pytest
Here's what each piece does:
on.push.branchesruns the pipeline on every push tomain;on.pull_requestruns it whenever someone opens or updates a pull request.cache: "pip"tells setup-python to reuse downloaded dependencies between runs, so later runs are faster.- The install step upgrades pip, installs Ruff and pytest, and loads your own dependencies from
requirements.txtif you have one. ruff check --output-format=github .lints your whole codebase. The--output-format=githubflag renders each finding as an inline annotation on the offending line, not just a line in the logs.pytestruns your tests. Both Ruff and pytest exit with a non-zero code when they find problems — and a non-zero exit is exactly what makes a step, and the whole workflow, fail.
Try it
Push the file to your repository, open the Actions tab, and watch the run. When it's green, you're set: every future push and pull request gets the same check automatically.
From here, natural next steps are testing on multiple Python versions with a strategy matrix, running ruff format --check to enforce consistent formatting, or publishing test reports as artifacts. But start simple — a pipeline that runs on every change and catches the obvious mistakes is worth more than an elaborate one you never finish.