Skip to content

Setting up GitHub Actions with uv

uv

Every push to main and every pull request should be tested and linted automatically. This tutorial sets up that pipeline using GitHub Actions and uv, running 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...