How to Cache uvx Tools in GitHub Actions
setup-uv caches standalone uvx tools without a lockfile. For a tools-only workflow, a date-based cache key controls when to refresh those tools. This guide uses the same date to limit package releases and refresh the cache.
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 and the directory-by-directory breakdown in how to cache uv dependencies in CI instead. This guide covers the other case: running tools like ruff or sqlite-utils with no project to key on.
Control when the tool cache refreshes
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.lockA tools-only job has none of these files to invalidate its cache when tool requirements change. Use a date in the cache key to control refreshes. Keep prune-cache: false, the default, so pre-built wheels remain available for later runs.
Add the caching workflow
This workflow caches every uvx tool it runs, keyed to a single date. Copy it into .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@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
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 --versionThe 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, also change the pin date to create a fresh cache key. That run downloads the tools and stores them. 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.