Skip to content

How to Cache uvx Tools in GitHub Actions

uv

A workflow that runs uvx some-tool fetches that tool from PyPI on every run. setup-uv’s cache is built for projects with a lockfile, so it does not help a job that only runs standalone tools. This guide keys the cache to a pinned date instead, so uvx tools restore from cache with no pyproject.toml or uv.lock in the repository.

If your job installs a project (uv sync against a committed lockfile), you want the lockfile-keyed cache in Setting up GitHub Actions with uv instead. This guide covers the other case: running tools like ruff or sqlite-utils with no project to key on.

Why the default cache misses uvx tools

enable-cache: true invalidates the cache when a dependency file changes. The files it watches are fixed:

**/*requirements*.txt
**/*requirements*.in
**/*constraints*.txt
**/*constraints*.in
**/pyproject.toml
**/uv.lock
**/*.py.lock

A tools-only job has none of them, so the key never changes and the restored cache is empty. setup-uv compounds the problem by pruning pre-built wheels after each run, keeping only source-built wheels. Since uvx tools arrive almost entirely as pre-built wheels, the default prune discards exactly what you wanted to keep.

Add the caching workflow

This workflow caches every uvx tool it runs, keyed to a single date. Copy it into .github/workflows/tools.yml:

.github/workflows/tools.yml
name: Run tools

on:
  workflow_dispatch:

env:
  # Pins every uvx tool to versions published on or before this date,
  # and drives the cache key below. Bump it to refresh both.
  UV_EXCLUDE_NEWER: "2026-07-12"

jobs:
  tools:
    runs-on: ubuntu-latest
    steps:
      - name: Install uv and restore the tool cache
        id: setup-uv
        uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990  # v8.3.2
        with:
          enable-cache: true
          cache-dependency-glob: ""                              # ignore lockfiles; there are none
          cache-suffix: "tools-${{ env.UV_EXCLUDE_NEWER }}"      # key the cache on the pin date
          prune-cache: false                                     # keep pre-built wheels

      - name: Run cached-only on a cache hit
        if: steps.setup-uv.outputs.cache-hit == 'true'
        run: echo "UV_OFFLINE=1" >> "$GITHUB_ENV"

      - name: Run the tools
        run: |
          uvx sqlite-utils --version
          uvx ruff --version

The action is pinned to a commit SHA with the version in a trailing comment, the pattern from how to pin GitHub Actions by SHA for Python projects. Check the setup-uv releases page for the current SHA before committing.

How the date pins tools and keys the cache

UV_EXCLUDE_NEWER is the environment-variable form of uv’s --exclude-newer flag, covered in how to use --exclude-newer for reproducible Python environments. It tells uv to ignore any package version published after 2026-07-12, so each uvx call resolves to the same versions on every run.

cache-dependency-glob: "" switches off the default lockfile keying so the cache no longer looks for files that do not exist. cache-suffix: "tools-${{ env.UV_EXCLUDE_NEWER }}" puts the date into the cache key directly, so the key changes only when the date does. prune-cache: false keeps the downloaded wheels in the cache instead of stripping them after the run.

Fail the build when a tool is not cached

The second step exports UV_OFFLINE=1 whenever the cache was restored (steps.setup-uv.outputs.cache-hit == 'true'). On a cache hit, uv runs offline: a tool that was already cached runs from disk, and a tool that was not cached fails at once instead of quietly downloading from PyPI.

That failure is the signal you want. When you add a new uvx line, the first run misses the cache, downloads the tool, and stores it. If a later run still cannot find it offline, the cache key and the pinned tools have drifted apart, and the build tells you so rather than hiding a slow, uncached install.

Refresh the tools

Change one line to pick up newer tool versions:

env:
  UV_EXCLUDE_NEWER: "2026-08-15"

The new date re-pins every tool to versions published on or before it and changes cache-suffix, which invalidates the old cache. The next run downloads the updated tools once and caches them under the new key.

Learn More

Last updated on