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
- uv installed (installation guide)
- A GitHub account
- Familiarity with setting up testing with pytest and uv
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 pytestHere is what each step does:
actions/checkout@v6clones the repository into the runner.astral-sh/setup-uv@v7installs uv. Settingenable-cache: truecaches the uv package store between runs, so subsequent CI builds skip re-downloading dependencies.uv sync --locked --all-extras --devinstalls every dependency fromuv.lockwithout modifying the lockfile. The--lockedflag ensures CI fails if the lockfile is out of date.uv run ruff check .runs the Ruff linter.uv run ruff format --check .checks that all files are formatted according to Ruff’s formatter (without modifying them).uv run pytestruns 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 pytestGitHub 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 pytestPush 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
- See the completed example project with the workflow already in place.
- Read the
setup-uvaction documentation for additional options like pinning a uv version or caching specific directories. - See the publishing tutorial to learn how to publish your package to PyPI, including how to use trusted publishing from GitHub Actions.