# How to maintain a uv project

A [uv](https://pydevtools.com/handbook/reference/uv.md) project needs regular care, like a dog. Most of that care can run on autopilot: Dependabot for lockfile bumps, `uv audit` in CI for vulnerability scans, and a scheduled workflow for `prek auto-update`. What's left is a short workstation check on the first Monday of the month.

The rest of this guide gives the [GitHub Actions](https://pydevtools.com/handbook/tutorial/setting-up-github-actions-with-uv.md) configs for the three automated jobs, walks through the manual monthly checks, and closes with the one rule that holds the whole thing together: don't bundle a maintenance bump into a release.

> [!TIP]
> All three configs come pre-wired in the [`uv-project-care`](https://github.com/python-developer-tooling-handbook/uv-project-care) GitHub template. Click **Use this template** to scaffold a new repo with Dependabot, `uv audit`, and the scheduled `prek auto-update` workflow already in place.

## Automate the recurring work

These three jobs cover the recurring care. Set them up once per project, then forget them.

### Automate lockfile updates with Dependabot

The manual command is `uv lock --upgrade`, which re-resolves every dependency to the newest version that fits the constraints in [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md):

```console
$ uv lock --upgrade
Resolved 14 packages in 111ms
Updated requests v2.31.0 -> v2.33.1
```

To upgrade one package without disturbing the rest:

```console
$ uv lock --upgrade-package requests
```

Run that on a calendar if you must. The cleaner pattern is letting Dependabot do it for you.

[Dependabot supports uv](https://pydevtools.com/blog/dependabot-uv-support.md) as of March 2025. Add `.github/dependabot.yml`:

```yaml {filename=".github/dependabot.yml"}
version: 2
updates:
  - package-ecosystem: "uv"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 5
```

Dependabot opens scheduled PRs that update `uv.lock` and any matching pinned versions in `pyproject.toml`. CI runs the test suite against each PR; merging the green ones is the entire workflow.

> [!NOTE]
> Dependabot does not relax range constraints. If the project pins `requests>=2.31.0,<2.33` and 2.33.1 is the latest, Dependabot won't propose widening that ceiling. Loosen the constraint manually first, then let Dependabot keep the lock current.

If a release branch needs to stay frozen on the existing lock, exclude it from Dependabot via `target-branch: main` and let `main` carry the bumps.

### Run uv audit in CI

The manual command is `uv audit`, which checks every package in the lockfile against the [OSV](https://osv.dev/) vulnerability database:

```console
$ uv audit
Resolved 14 packages in 3ms
Found 3 known vulnerabilities and no adverse project statuses in 13 packages

requests 2.31.0 has 3 known vulnerabilities:

- GHSA-9hjg-9r4m-mvj7: Requests vulnerable to .netrc credentials leak via malicious URLs
  Fixed in: 2.32.4
```

The cleaner pattern is running it in CI on every push, with a workflow that fails the build on findings. The full setup is in [how to scan Python dependencies for vulnerabilities](https://pydevtools.com/handbook/how-to/how-to-scan-python-dependencies-for-vulnerabilities.md). Wire that workflow up once and every push gets scanned.

> [!IMPORTANT]
> `uv audit` is gated behind a preview-features warning in current uv releases. Pass `--preview-features audit` to silence it, or accept the warning until the command stabilizes.

### Schedule prek auto-update weekly

The manual command is `uvx prek auto-update`, which trims every `rev:` in `.pre-commit-config.yaml` up to the latest release tag:

```console
$ uvx prek auto-update
https://github.com/astral-sh/ruff-pre-commit
  would update rev `v0.6.0` -> `v0.15.12`
```

(Use `pre-commit autoupdate` if the project uses [pre-commit](https://pre-commit.com/) instead of [prek](https://pydevtools.com/handbook/reference/prek.md); the two are command-line compatible apart from prek's hyphenated subcommand name.)

The cleaner pattern is a scheduled workflow that runs autoupdate weekly and opens a PR with the diff. This one uses `peter-evans/create-pull-request` to handle the commit and PR creation:

```yaml {filename=".github/workflows/autoupdate-hooks.yml"}
name: Autoupdate pre-commit hooks
on:
  schedule:
    - cron: "0 9 * * 1"
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

jobs:
  autoupdate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: astral-sh/setup-uv@v8.1.0
      - run: uvx prek auto-update
      - uses: peter-evans/create-pull-request@v8
        with:
          title: "chore: autoupdate pre-commit hooks"
          commit-message: "chore: autoupdate pre-commit hooks"
          branch: chore/autoupdate-hooks
          delete-branch: true
```

Monday 9am UTC, weekly. The PR is empty when nothing changed and gets merged with one click otherwise. For tighter supply-chain protection, pin the action versions to a [full SHA](https://pydevtools.com/handbook/how-to/how-to-pin-github-actions-by-sha-for-python-projects.md) instead of a tag.

> [!IMPORTANT]
> The workflow's `permissions:` block is necessary but not sufficient on a fresh repo. Also enable **Settings → Actions → General → Workflow permissions → "Allow GitHub Actions to create and approve pull requests"**. Without that toggle, `peter-evans/create-pull-request` fails with `GitHub Actions is not permitted to create or approve pull requests`.

## Handle what's left manually each month

Once the three CI jobs are running, this is what's left for a human. Block time on the first Monday of the month; if nothing's broken it's five minutes.

### Merge the bot PRs

Open the repo and burn down the queue:

- Dependabot's stack of `uv.lock` bumps.
- The autoupdate-hooks PR from last Monday.
- Any other dependency PRs that have accumulated.

Each one already has a green test run. Squash-merge the green ones; investigate the red ones.

### Review the audit ignore list

Every `--ignore` in your audit configuration is a deliberate exception. Once a month, walk the list and check which exceptions still apply and which now have an upstream fix you can pull in.

```console
$ uv audit --ignore GHSA-9hjg-9r4m-mvj7
```

`--ignore-until-fixed <ID>` is a softer form that automatically un-ignores once the advisory lists a fixed version. Convert long-standing ignores to that form when you can.

### Update uv and Python on your workstation

CI runners are ephemeral; they install fresh uv and Python on every run. Only the workstation needs these:

```console
$ uv self update
$ uv python upgrade
```

The full mechanics, including how virtual environments transparently follow patch upgrades, are in [how to upgrade uv](https://pydevtools.com/handbook/how-to/how-to-upgrade-uv.md) and [how to keep Python up to date with uv python upgrade](https://pydevtools.com/handbook/how-to/how-to-keep-python-up-to-date-with-uv-python-upgrade.md).

Prune the patch directories that have accumulated since last month:

```console
$ uv python list --only-installed
$ uv python uninstall 3.11.8 3.12.3
```

Each retained patch is roughly 100&nbsp;MB. Two years of unmanaged upgrades are a noticeable disk hit on a laptop.

## Don't bundle maintenance with releases

A release should pin to a known-good `uv.lock` that the test suite and CI have already exercised. Maintenance days bump the lock forward; release days don't. The two belong on different days, on purpose.

Schedule maintenance for the Monday after a release, not the Friday before. Mid-cycle Dependabot PRs get tested and age before they ship; bumps that arrive the day before a release wait for the next cycle.

## Related

- [uv: A Complete Guide](https://pydevtools.com/handbook/explanation/uv-complete-guide.md) covers what uv does, how fast it is, the core workflows, and recent releases.
- [How to use a uv lockfile for reproducible Python environments](https://pydevtools.com/handbook/how-to/how-to-use-a-uv-lockfile-for-reproducible-python-environments.md)
- [How to scan Python dependencies for vulnerabilities](https://pydevtools.com/handbook/how-to/how-to-scan-python-dependencies-for-vulnerabilities.md)
- [How to upgrade uv](https://pydevtools.com/handbook/how-to/how-to-upgrade-uv.md)
- [How to keep Python up to date with uv python upgrade](https://pydevtools.com/handbook/how-to/how-to-keep-python-up-to-date-with-uv-python-upgrade.md)
- [How to set up prek hooks for a Python project](https://pydevtools.com/handbook/how-to/how-to-set-up-prek-hooks-for-a-python-project.md)
- [Setting up GitHub Actions with uv](https://pydevtools.com/handbook/tutorial/setting-up-github-actions-with-uv.md)
- [How to pin GitHub Actions by SHA for Python projects](https://pydevtools.com/handbook/how-to/how-to-pin-github-actions-by-sha-for-python-projects.md)
