# setup-uv: pinning uv in a GitHub Actions workflow

> The setup-uv action installs a chosen version of the uv package manager and adds it to PATH in a GitHub Actions job, with optional caching of the uv binary, of the uv cache, of managed Python installations, checksum verification and problem matchers. Its most useful behaviour is the one nobody configures: if you give it no version, it reads the requirement out of your uv.toml or pyproject.toml.

**astral-sh/setup-uv** — Set up your GitHub Actions workflow with a specific version of https://docs.astral.sh/uv/

- Repository: https://github.com/astral-sh/setup-uv
- Stars: 892 · Forks: 118
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/astral-sh-setup-uv

## The version comes from your config file if you do not name one

The action does one main thing, which is install uv and put it on PATH, and the design decision worth knowing about is the default. If you do not specify a version, the action looks for a required-version in a uv.toml or pyproject.toml file in the repository root, and if none is found it installs the latest version. That means the version pin can live in the same file that declares your dependencies, and the workflow does not have to be the place where the version is written down. The shortest possible usage is two lines:

```yaml
- name: Install the latest version of uv
  uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
```

The action reference in that example is a commit hash with the version in a trailing comment, not a tag, and the documentation is consistent about that. It is a deliberate choice for a security reason, since a tag is a mutable pointer and a commit hash is not, and it is the convention you would be expected to follow if you copy the example. The named release in the comment tells a reader which version the hash corresponds to without giving up the immutability.

## Caching is three separate things, and they behave differently

There are three caches in play and they are easy to conflate. The first caches the installed version of uv itself, which is what speeds up consecutive runs on self-hosted runners, where the toolchain is not preinstalled. The second is the uv cache itself, which is optional and can be persisted in the GitHub Actions cache. The third is managed Python installations, uploaded to the Actions cache, and that one is off by default. The default behaviour of the uv cache input is the value auto, which means enabled on GitHub-hosted runners except for release, tag push, pull_request_target and workflow_run events, and disabled on self-hosted runners. Saving after the run is separately controlled, and the default there is also auto, but disabled for merge_group events. The two auto rules exist for good reasons, since a cache write in those event types either wastes quota or risks cache poisoning, but they are worth knowing before you file a bug that the cache never appears. A merge queue is exactly the situation where you would most want a cache, and exactly the situation where the default turns saving off.

## The cache key is driven by a glob over your project files

What gets cached is decided by a glob pattern matched against files relative to the working directory, and the default pattern is the list of files whose contents affect your dependency resolution:

```yaml
    # Glob pattern to match files relative to the working directory to control the cache
    cache-dependency-glob: |
      **/*requirements*.txt
      **/*requirements*.in
      **/*constraints*.txt
      **/*constraints*.in
      **/pyproject.toml
      **/uv.lock
      **/*.py.lock
```

A change to any matched file changes the cache key, and that is the whole mechanism. Two inputs control what happens around the cache entry: whether to restore it if found, which defaults to true, and whether to save it after the run, which defaults to auto. There is also a prune-cache input, off by default, and a cache-suffix for when you want two different cache entries for the same lock file, which is the escape hatch for a job that changes the environment without changing the dependencies. The cache-local-path input decides where the cache is written, and the default is the system temporary directory, which matters if you expected it to live somewhere durable.

## Python version selection has one supported form and several ignored ones

The python-version input sets the environment variable UV_PYTHON for the rest of the workflow, and it overrides any Python version specification in pyproject.toml, in .python-version, and in an explicitly selected .tool-versions file. There is one more route: when version-file points to .tool-versions, its python entry is used if neither python-version nor UV_PYTHON is set. The example of that file shows the two lines involved:

```text
uv 0.12.3
python 3.13
```

and the workflow that selects it:

```yaml
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
  with:
    version-file: ".tool-versions"
```

The limits are stated just as plainly. Only a single Python version is supported. Filesystem paths are not supported for uv or Python. Multiple Python fallback versions and the asdf ref, path and system forms are ignored with a warning. That is a real constraint rather than a footnote, because a team that wants to test against a matrix of interpreters with fallback has to build that matrix itself, and the action will not do it. The architecture input, added in the 10.3.0 release, sets UV_PYTHON_ARCH to values such as x86_64 or aarch64, which is what you need on a runner where the Python build has to match the machine.

## A matrix of Python versions is the standard shape of use

The documented way to test several Python versions is a matrix, with the action setting the version per job and the test step reading the same matrix value:

```yaml
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12", "3.13"]
    steps:
      - uses: actions/checkout@v5
      - name: Install the latest version of uv and set the python version
        uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
        with:
          python-version: ${{ matrix.python-version }}
      - name: Test with python ${{ matrix.python-version }}
        run: uv run --
```

The free-threaded build is a separate case, because 3.13t is a different kind of interpreter rather than a version number. The example sets it explicitly and then installs pip for that interpreter with uv pip install and an explicit --python flag:

```yaml
- name: Install the latest version of uv and set the python version to 3.13t
  uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
  with:
    python-version: 3.13t
- run: uv pip install --python=3.13t pip
```

The two things to notice are that the test step is a plain uv run with no virtual environment activation, and that the checkout action is pinned to a major version tag in the example, unlike the setup action which is pinned to a hash. That inconsistency in the example is a small reminder that the pinning discipline applies to the action you trust with your build, not to every step in the file.

## Outputs and the environment you get back

The action reports what it did through a set of outputs. uv-version is the installed version, which is what you want when you asked for latest, and uv-path and uvx-path are the paths of the two installed binaries. cache-hit is a boolean saying whether a cache entry was found, and python-cache-hit does the same for the Python cache. venv is the path to the activated venv when activate-environment is on, and python-version is the version that was set. The newest of these is python-runtime-id, an opaque identifier reported by uv for the activated venv's Python runtime, and it is empty when activate-environment is false, which is a detail that matters if you key anything on it. The activate-environment input uses uv venv to create a ready to use environment for later steps, with a venv-path input to override the default location of .venv in the working directory, and a no-project input that passes --no-project when creating it. There are also tool-dir and tool-bin-dir inputs to set UV_TOOL_DIR and UV_TOOL_BIN_DIR, and a manifest-file input for a custom manifest in the astral-sh versions format.

## Supply chain details: checksums, the mirror and the token

Three inputs concern where the binary comes from, and they are the part of this action worth reading rather than copying. The checksum input takes the checksum of the uv version to install, and when it is set the downloaded executable is verified, which is the difference between trusting GitHub Releases and trusting a byte string you pinned yourself. The github-token input is used when downloading uv from GitHub releases and defaults to the workflow's own token. The download-from-astral-mirror input defaults to true, meaning uv is fetched from the Astral mirror rather than directly from GitHub Releases, which is a speed and availability decision for GitHub-hosted runners. The problem matchers input defaults to true as well, and it registers matchers so that error output in a log is annotated the way a compiler error would be rather than being a wall of text. Two more inputs are about behaviour rather than plumbing: quiet suppresses info-level log output so only warnings and errors are shown, and ignore-nothing-to-cache and ignore-empty-workdir let the action skip a cache step when there is nothing to cache or the working directory is empty.

## How the action is built and released

The repository is a TypeScript GitHub Action with a dist directory committed, which is the standard shape for a JavaScript action that has to run on a runner without a build step. The package manifest names the project setup-uv, marks it private, and requires npm 11.10.0 or newer, with the package manager pinned to the same version and set to fail on error. The scripts show the workflow: a build that runs tsc in no-emit mode as a type check, a check that runs biome with the write flag, a package step that runs a script in scripts to build the distribution, and unit tests run through jest with the experimental VM modules flag, plus a combined all target that runs everything in order. The end to end tests run act against a pull request workflow inside a container, using the GitHub CLI to supply a token, which is how you exercise an action locally without pushing to a branch. There is also a script for updating the known checksums, pointed at a file under src/download/checksum, which is how the set of trusted checksums is maintained as new uv versions ship. The licence is MIT, and the last push was on 2026-10-20, with v10.3.0 released on 2026-10-09 and v10.2.0 on 2026-09-21.

## Conclusion

setup-uv fits a team running uv in GitHub Actions that wants the version in one place, the cache in the Actions cache, and the Python version pinned without a second action. It does not suit a workflow that needs several fallback Python versions, since only a single version is supported and the asdf ref, path and system forms are ignored with a warning. Before you adopt it, decide whether you want the version pinned by commit hash, as the examples in the documentation do, or by tag, and read the advanced configuration for the cache keys, because the default glob and the auto mode for merge queues are where surprises come from.

## FAQ

### How do I install uv in a GitHub Actions workflow with setup-uv?

Add the action as a step, for example - name: Install the latest version of uv followed by uses: astral-sh/setup-uv with a commit hash and the version in a comment. If you do not set a version, the action looks for a required-version in uv.toml or pyproject.toml in the repository root and installs the latest version if there is none.

### What does the enable-cache input do in setup-uv?

It controls the GitHub Actions cache for uv and takes true, false or auto. The default of auto enables the cache on GitHub-hosted runners except for release, tag push, pull_request_target and workflow_run events, and disables it on self-hosted runners. Saving after the run is a separate input, also defaulting to auto but disabled for merge_group events.

### How do I set the Python version for a setup-uv job?

Use the python-version input, which sets UV_PYTHON for the rest of the workflow and overrides any version in pyproject.toml, .python-version or an explicitly selected .tool-versions file. Only a single Python version is supported, and multiple fallback versions and the asdf ref, path and system forms are ignored with a warning.

### Can setup-uv verify the uv binary it downloads?

Yes. The checksum input takes the checksum of the uv version to install, and the action verifies the downloaded executable against it. The action also downloads uv from the Astral mirror by default rather than directly from GitHub Releases, and registers problem matchers so error output is annotated in the log.

## Sources

- [astral-sh/setup-uv on GitHub](https://github.com/astral-sh/setup-uv)
- [Issues](https://github.com/astral-sh/setup-uv/issues)
- [License: MIT](https://github.com/astral-sh/setup-uv/blob/main/LICENSE)
- [README](https://github.com/astral-sh/setup-uv/blob/main/README.md)
- [Releases](https://github.com/astral-sh/setup-uv/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/astral-sh-setup-uv
