Skip to content

Setting up GitHub Actions with uv

Without continuous integration, catching bugs depends on developers remembering to run tests and linters locally before every push. Formatting inconsistencies slip through, broken tests land on main, and nobody notices until the next person pulls the code. A CI pipeline removes that manual step: every push and every pull request is automatically tested and linted, so problems surface before they merge.

Setting up CI for Python projects used to mean juggling separate installation steps for the interpreter, pip, virtualenvs, and pinned dependencies. With uv, a single tool handles Python installation, dependency resolution, and locked installs, which keeps the workflow file short and fast.

This tutorial walks you through setting up a CI pipeline using GitHub Actions and uv. By the end, pushes to main and all pull requests will automatically run pytest and Ruff across multiple Python versions.

Prerequisites

Clone the Example Project

This tutorial uses a small temperature-converter project with code and tests already written but no CI configured. Clone it and move into the directory:

$ git clone https://github.com/python-developer-tooling-handbook/demo-uv-github-actions.git
$ cd demo-uv-github-actions

Verify the project works locally:

$ uv sync --locked --dev
$ uv run pytest

You should see all tests passing.

Create the Workflow File

GitHub Actions workflows live in .github/workflows/. Create the directory and a new file called ci.yml:

$ mkdir -p .github/workflows

Open .github/workflows/ci.yml in your editor and add the following:

name: CI

on:
  push:
    branches: [main]
  pull_request:

The on block tells GitHub to run this workflow on every push to main and on every pull request.

Add the Test Job

Below the on block, add a jobs section:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: astral-sh/setup-uv@v7
        with:
          enable-cache: true
      - run: uv sync --locked --all-extras --dev
      - run: uv run ruff check .
      - run: uv run ruff format --check .
      - run: uv run pytest

Here is what each step does:

  1. actions/checkout@v6 clones the repository into the runner.
  2. astral-sh/setup-uv@v7 installs uv. Setting enable-cache: true caches the uv package store between runs, so subsequent CI builds skip re-downloading dependencies.
  3. uv sync --locked --all-extras --dev installs every dependency from uv.lock without modifying the lockfile. The --locked flag ensures CI fails if the lockfile is out of date.
  4. uv run ruff check . runs the Ruff linter.
  5. uv run ruff format --check . checks that all files are formatted according to Ruff’s formatter (without modifying them).
  6. uv run pytest runs the test suite.

Test Across Multiple Python Versions

To verify your code works on more than one Python version, add a strategy.matrix to the job and pass the version to setup-uv:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.12", "3.13", "3.14"]
    steps:
      - uses: actions/checkout@v6
      - uses: astral-sh/setup-uv@v7
        with:
          python-version: ${{ matrix.python-version }}
          enable-cache: true
      - run: uv sync --locked --all-extras --dev
      - run: uv run ruff check .
      - run: uv run ruff format --check .
      - run: uv run pytest

GitHub Actions will now run a parallel job for each Python version in the matrix.

The Complete Workflow

Here is the full .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.12", "3.13", "3.14"]
    steps:
      - uses: actions/checkout@v6
      - uses: astral-sh/setup-uv@v7
        with:
          python-version: ${{ matrix.python-version }}
          enable-cache: true
      - run: uv sync --locked --all-extras --dev
      - run: uv run ruff check .
      - run: uv run ruff format --check .
      - run: uv run pytest

Push and Verify

Push the workflow to GitHub:

$ git add .github/workflows/ci.yml
$ git commit -m "Add CI workflow"
$ git push

Open your repository on GitHub and click the Actions tab. You should see a workflow run with one job per Python version. Each job checks out the code, installs dependencies, lints, and runs tests.

If all checks pass, you’ll see green checkmarks. If any step fails, click into the job to read the logs and identify the issue.

Next Steps

Last updated on

Please submit corrections and feedback...