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
uvand 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"}]underon:to catch breakage from new dependency releases. - Cancel old runs: add a
concurrencygroup 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 useruns-on: ${{ matrix.os }}. - Secrets: store keys in repository Settings → Secrets and read them as
${{ secrets.MY_KEY }}; never commit them.
Checklist
- Push
ci.ymland 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