GitHub Actions for Python – CI/CD Guide (2026)

Run your tests on every push, lint with Ruff, and publish to PyPI without storing a token.

What you will build

  • A test workflow that runs on Python 3.12, 3.13 and 3.14
  • Fast installs with uv and dependency caching
  • Linting with Ruff and tests with pytest
  • A release workflow that publishes to PyPI with trusted publishing

1. Project layout


my-package/
├── pyproject.toml
├── src/my_package/__init__.py
├── tests/test_basic.py
└── .github/workflows/
    ├── ci.yml
    └── release.yml
            

GitHub runs every YAML file inside .github/workflows/. Each file is one workflow.

2. The CI workflow (tests + lint)


# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.12", "3.13", "3.14"]

    steps:
      - uses: actions/checkout@v5

      - name: Install uv
        uses: astral-sh/setup-uv@v6
        with:
          enable-cache: true

      - name: Set up Python ${{ matrix.python-version }}
        run: uv python install ${{ matrix.python-version }}

      - name: Install project and dev tools
        run: uv sync --all-extras --dev

      - name: Lint with Ruff
        run: uv run ruff check .

      - name: Run tests
        run: uv run pytest --cov=my_package --cov-report=term-missing
            

The matrix runs the same job three times, once per Python version. fail-fast: false lets every version finish, so you can see exactly which one breaks.

3. Dev dependencies in pyproject.toml


[project]
name = "my-package"
version = "0.1.0"
requires-python = ">=3.12"

[dependency-groups]
dev = ["pytest", "pytest-cov", "ruff"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
            

4. A test to run


# tests/test_basic.py
from my_package import add

def test_add():
    assert add(2, 3) == 5
            

5. Publish to PyPI on release (no token)


# .github/workflows/release.yml
name: Release

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest
    environment: pypi
    permissions:
      id-token: write   # required for trusted publishing
    steps:
      - uses: actions/checkout@v5
      - uses: astral-sh/setup-uv@v6
      - name: Build
        run: uv build
      - name: Publish
        uses: pypa/gh-action-pypi-publish@release/v1
            

On PyPI, open your project's Publishing settings and add a trusted publisher with your GitHub owner, repository, workflow file name (release.yml) and environment (pypi). PyPI then accepts uploads from that workflow without any API token stored in GitHub.

6. Useful extras

  • Run on a schedule: add schedule: [{cron: "0 3 * * 1"}] under on: to catch breakage from new dependency releases.
  • Cancel old runs: add a concurrency group so a new push stops the previous run on the same branch.
  • Test on Windows and macOS: add os: [ubuntu-latest, windows-latest, macos-latest] to the matrix and use runs-on: ${{ matrix.os }}.
  • Secrets: store keys in repository Settings → Secrets and read them as ${{ secrets.MY_KEY }}; never commit them.

Checklist

  • Push ci.yml and open the Actions tab to watch the first run
  • Add a status badge to your README
  • Configure trusted publishing before your first release

Related: Dockerfile generator · pyproject.toml creator