Overview of GitHub Actions
GitHub Actions runs the same checks contributors run locally (Ruff, mypy, the test suite) every time someone opens a pull request or pushes to main.
Red CI build is a signal that a change shouldn't be merged.
Green shows that the default branch deployable.
Anatomy of a Workflow
A workflow lives at .github/workflows/<name>.yml and is plain YAML. A workflow has:
- Triggers (
on:): which events run the workflow (push, pull request, schedule, manual dispatch). - Jobs: independent units of work, each running on its own runner (a fresh VM or container).
- Steps: ordered actions inside a job. Each step is either a shell command (
run:) or a reusable action (uses:).
The release tracker's CI workflow runs one job that lints, type-checks, applies migrations, and runs tests under coverage:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
checks:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_DB: release_tracker
POSTGRES_USER: release_tracker
POSTGRES_PASSWORD: release_tracker
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U release_tracker -d release_tracker"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgresql+psycopg://release_tracker:release_tracker@localhost:5432/release_tracker
JWT_SECRET_KEY: ci-secret-key-32-bytes-or-longer-1234
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "0.11.6"
- name: Set up Python
run: uv python install 3.14
- name: Sync dependencies
run: uv sync --frozen
- name: Lint
run: uv run ruff check --no-cache .
- name: Format check
run: uv run ruff format --check .
- name: Type check
run: uv run mypy .
- name: Apply migrations
run: uv run alembic upgrade head
- name: Test with coverage
run: |
uv run coverage run -m pytest
uv run coverage report
services: Provides a Real Postgres
The test suite uses SQLite, but alembic upgrade head needs a real Postgres because the production migrations target Postgres-specific types like the taskstatus enum. GitHub Actions' services: block spins up a Postgres container alongside the runner and exposes it on localhost:5432. The runner waits for the pg_isready healthcheck to pass before the steps start.
For projects that need integration tests against Postgres in addition to SQLite-based unit tests, the same services block makes that affordable.
env: Sets the Same Variables the App Reads Locally
The env: block at the job level supplies the environment variables pydantic-settings reads on a developer machine. JWT_SECRET_KEY is set inline because it's a CI-only test value, not a real secret. A real key would live in GitHub's repository secrets:
- Settings → Secrets and variables → Actions → New repository secret.
- Reference it in the workflow as
${{ secrets.JWT_SECRET_KEY }}.
The pattern shows up like this:
env:
DATABASE_URL: postgresql+psycopg://...
JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
The actual value never appears in workflow logs; GitHub redacts it automatically.
astral-sh/setup-uv Pins the Tool
The uses: astral-sh/setup-uv@v3 step installs uv on the runner. Pinning to version: "0.11.6" matches the Dockerfile's pinned uv. The same uv version everywhere (local, CI, production image) keeps the lockfile resolution deterministic.
uv python install 3.14 then installs the right Python version. uv handles Python downloads and caching the same way it handles package downloads, so a CI run doesn't depend on whatever Python the runner happens to ship with.
Out of Scope
What we won't cover:
- Building and publishing Docker images on tag pushes.
- Deploy steps. Once the image is published, a deployment platform pulls and runs it. The shape varies wildly per platform, and the tooling depends on which platform the team is on.
- Test parallelism. A larger test suite would split across multiple jobs (or use
pytest-xdistinside one job) to keep wall-clock time manageable.
Note
This workflow is enough to ship our demo API, your production configuration will probably be different.