Radar · 14/07/2026 · coding

uvx in GitHub Actions cache-friendly

Simon Willison published a recipe for using uvx (the uv tool runner) in GitHub Actions without having every workflow download everything from PyPI each time. The trick comes in two parts: you set a UV_EXCLUDE_NEWER environment variable with a fixed date at the start of the workflow, and use that date as part of the cache key. Each uvx tool-name will resolve to the most recent version up to that date, and the cache will survive across runs. To update tools, you move the date forward.

This matters if you automate Python workflows with GitHub Actions: instead of waiting for the runner to download dependencies each time, you reuse what’s already in cache. The result is faster builds and less traffic to PyPI. The pattern is small but immediately actionable: two lines of config and you know your workflows won’t start from scratch every time.

If you want to try it. Add UV_EXCLUDE_NEWER: "2026-07-12" (or whatever date you want) at the start of your workflow and use that variable in the cache action key. The full recipe is documented with examples.

In detail

The problem

When you use uvx tool-name in a GitHub Actions workflow, by default each run contacts PyPI to download the tool and its dependencies. This adds latency to every build and wastes bandwidth: if the tool hasn’t changed, you’re doing unnecessary work.

GitHub Actions caching exists precisely for this, but there was a need for a way to make uvx respect that cache instead of always downloading everything fresh. The problem is that uvx by default tries to resolve to the most recent available version, which means the cache key changes every time there’s a new tool release, even if that release doesn’t matter to you.

Willison’s solution

The recipe relies on UV_EXCLUDE_NEWER, an environment variable for uv that says: “ignore everything published after this date.” If you set UV_EXCLUDE_NEWER: "2026-07-12" at the start of the workflow, each call to uvx will resolve to the most recent version published through July 12, 2026, and that version will stay stable until you change the date.

You use that date in the cache action key as well (actions/cache@v4), so GitHub knows the cache is valid as long as the date doesn’t change. When you want to update the tools, you move the date forward and the cache invalidates itself.

The advantage is that you have explicit control over when to update, instead of being subject to every new tool release without warning. It’s a form of soft pinning: you don’t lock a specific version, but you lock a time window, and that’s enough to keep the cache coherent.

Why it matters

If you have workflows that run frequently (CI on every commit, automated deploys, scheduled jobs), the time saved on each run accumulates. More importantly: you eliminate a source of instability. A workflow that passes today and fails tomorrow because a new version of a tool broke something is a problem this recipe prevents, because the date gives you an explicit control point.

There’s also an open issue against astral-sh/setup-uv requesting a change to default in favor of caching instead of wheel purge. If that proposal lands, some of this work could become automatic. In the meantime, Willison’s recipe works today.

Type to search across course, playbooks, skills, papers…