Skip to content

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:

.github/workflows/ci.yml
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:

  1. Settings → Secrets and variables → Actions → New repository secret.
  2. Reference it in the workflow as ${{ secrets.JWT_SECRET_KEY }}.

The pattern shows up like this:

YAML
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-xdist inside 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.