5
0
mirror of https://github.com/astral-sh/setup-uv.git synced 2025-12-15 11:07:14 +00:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Kevin Stillhammer
2233977af9 leave uv.exe in D: 2024-09-18 20:49:20 +02:00
Kevin Stillhammer
363497d0ae mv uv.exe onto D: 2024-09-18 18:17:51 +02:00
Kevin Stillhammer
561bff6f70 Add tool bin dir to PATH on windows 2024-09-18 18:00:23 +02:00
34 changed files with 12610 additions and 28964 deletions

4
.eslintignore Normal file
View File

@@ -0,0 +1,4 @@
dist/
lib/
node_modules/
jest.config.js

61
.eslintrc.json Normal file
View File

@@ -0,0 +1,61 @@
{
"plugins": ["jest", "@typescript-eslint"],
"extends": ["plugin:github/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
"i18n-text/no-en": "off",
"eslint-comments/no-use": "off",
"import/no-namespace": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-member-accessibility": [
"error",
{ "accessibility": "no-public" }
],
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-comment": "error",
"camelcase": "off",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/explicit-function-return-type": [
"error",
{ "allowExpressions": true }
],
"@typescript-eslint/func-call-spacing": ["error", "never"],
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unbound-method": "error"
},
"env": {
"node": true,
"es6": true,
"jest/globals": true
}
}

View File

@@ -11,7 +11,10 @@ concurrency:
jobs:
test-setup-cache:
runs-on: windows-latest
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest]
steps:
- uses: actions/checkout@v4
- name: Setup with cache
@@ -22,7 +25,10 @@ jobs:
- run: uv sync
working-directory: __tests__\fixtures\uv-project
test-restore-cache:
runs-on: windows-latest
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest]
needs: test-setup-cache
steps:
- uses: actions/checkout@v4

View File

@@ -14,23 +14,21 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
enable-cache: [ "true", "false", "auto" ]
os: ["ubuntu-latest", "selfhosted-ubuntu-arm64"]
os: [ubuntu-latest, macos-latest, macos-14]
steps:
- uses: actions/checkout@v4
- name: Setup with cache
uses: ./
with:
enable-cache: ${{ matrix.enable-cache }}
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-${{ matrix.os }}-${{ matrix.enable-cache }}
enable-cache: true
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}
- run: uv sync
working-directory: __tests__/fixtures/uv-project
test-restore-cache:
runs-on: ${{ matrix.os }}
strategy:
matrix:
enable-cache: [ "true", "false", "auto" ]
os: [ "ubuntu-latest", "selfhosted-ubuntu-arm64" ]
os: [ubuntu-latest, macos-latest, macos-14]
needs: test-setup-cache
steps:
- uses: actions/checkout@v4
@@ -38,61 +36,17 @@ jobs:
id: restore
uses: ./
with:
enable-cache: ${{ matrix.enable-cache }}
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-${{ matrix.os }}-${{ matrix.enable-cache }}
enable-cache: true
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}
- name: Cache was hit
if: ${{ matrix.enable-cache == 'true' || (matrix.enable-cache == 'auto' && matrix.os == 'ubuntu-latest') }}
run: |
if [ "$CACHE_HIT" != "true" ]; then
exit 1
fi
env:
CACHE_HIT: ${{ steps.restore.outputs.cache-hit }}
- name: Cache was not hit
if: ${{ matrix.enable-cache == 'false' || (matrix.enable-cache == 'auto' && matrix.os == 'selfhosted-ubuntu-arm64') }}
run: |
if [ "$CACHE_HIT" == "true" ]; then
exit 1
fi
env:
CACHE_HIT: ${{ steps.restore.outputs.cache-hit }}
- run: uv sync
working-directory: __tests__/fixtures/uv-project
test-setup-cache-requirements-txt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup with cache
uses: ./
with:
enable-cache: true
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-requirements-txt
- run: |
uv venv
uv pip install -r requirements.txt
working-directory: __tests__/fixtures/requirements-txt-project
test-restore-cache-requirements-txt:
runs-on: ubuntu-latest
needs: test-setup-cache
steps:
- uses: actions/checkout@v4
- name: Restore with cache
id: restore
uses: ./
with:
enable-cache: true
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-requirements-txt
- name: Cache was hit
run: |
if [ "$CACHE_HIT" != "true" ]; then
exit 1
fi
env:
CACHE_HIT: ${{ steps.restore.outputs.cache-hit }}
- run: |
uv venv
uv pip install -r requirements.txt
working-directory: __tests__/fixtures/requirements-txt-project
test-setup-cache-dependency-glob:
runs-on: ubuntu-latest
@@ -105,7 +59,7 @@ jobs:
cache-dependency-glob: |
__tests__/fixtures/uv-project/uv.lock
**/pyproject.toml
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-dependency-glob
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}
- run: uv sync
working-directory: __tests__/fixtures/uv-project
test-restore-cache-dependency-glob:
@@ -125,8 +79,7 @@ jobs:
cache-dependency-glob: |
__tests__/fixtures/uv-project/uv.lock
**/pyproject.toml
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-dependency-glob
ignore-nothing-to-cache: true
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}
- name: Cache was not hit
run: |
if [ "$CACHE_HIT" == "true" ]; then
@@ -136,19 +89,19 @@ jobs:
CACHE_HIT: ${{ steps.restore.outputs.cache-hit }}
test-setup-cache-local:
runs-on: selfhosted-ubuntu-arm64
runs-on: oracle-aarch64
steps:
- uses: actions/checkout@v4
- name: Setup with cache
uses: ./
with:
enable-cache: true
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-local
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}
cache-local-path: /tmp/uv-cache
- run: uv sync
working-directory: __tests__/fixtures/uv-project
test-restore-cache-local:
runs-on: selfhosted-ubuntu-arm64
runs-on: oracle-aarch64
needs: test-setup-cache-local
steps:
- uses: actions/checkout@v4
@@ -157,7 +110,7 @@ jobs:
uses: ./
with:
enable-cache: true
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-local
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}
cache-local-path: /tmp/uv-cache
- name: Cache was hit
run: |
@@ -168,62 +121,3 @@ jobs:
CACHE_HIT: ${{ steps.restore.outputs.cache-hit }}
- run: uv sync
working-directory: __tests__/fixtures/uv-project
test-tilde-expansion-cache-local-path:
runs-on: selfhosted-ubuntu-arm64
steps:
- uses: actions/checkout@v4
- name: Create cache directory
run: mkdir -p ~/uv-cache
shell: bash
- name: Setup with cache
uses: ./
with:
cache-local-path: ~/uv-cache/cache-local-path
- run: uv sync
working-directory: __tests__/fixtures/uv-project
test-tilde-expansion-cache-dependency-glob:
runs-on: selfhosted-ubuntu-arm64
steps:
- uses: actions/checkout@v4
- name: Create cache directory
run: mkdir -p ~/uv-cache
shell: bash
- name: Create cache dependency glob file
run: touch ~/uv-cache.glob
shell: bash
- name: Setup with cache
uses: ./
with:
enable-cache: true
cache-local-path: ~/uv-cache/cache-dependency-glob
cache-dependency-glob: "~/uv-cache.glob"
- run: uv sync
working-directory: __tests__/fixtures/uv-project
cleanup-tilde-expansion-tests:
needs:
- test-tilde-expansion-cache-local-path
- test-tilde-expansion-cache-dependency-glob
runs-on: selfhosted-ubuntu-arm64
steps:
- name: Remove cache directory
run: rm -rf ~/uv-cache
shell: bash
- name: Remove cache dependency glob file
run: rm -f ~/uv-cache.glob
shell: bash
test-no-python-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fake pyproject.toml at root
run: cp __tests__/fixtures/old-python-constraint-project/pyproject.toml pyproject.toml
- name: Setup with cache
uses: ./
with:
enable-cache: true
- run: uv sync
working-directory: __tests__/fixtures/old-python-constraint-project

View File

@@ -28,18 +28,19 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, macos-14]
os: [ubuntu-latest, macos-latest, macos-14, oracle-aarch64]
steps:
- uses: actions/checkout@v4
- name: Install latest version
- name: Install default version
uses: ./
- run: uv sync
working-directory: __tests__/fixtures/uv-project
test-specific-version:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
matrix:
uv-version: ["0.3.0", "0.3.2", "0.3", "0.3.x", ">=0.3.0"]
os: [ubuntu-latest, macos-latest, macos-14, oracle-aarch64]
uv-version: ["latest", "0.3.0", "0.3.2", "0.3", "0.3.x", ">=0.3.0"]
steps:
- uses: actions/checkout@v4
- name: Install version ${{ matrix.uv-version }}
@@ -49,7 +50,10 @@ jobs:
- run: uv sync
working-directory: __tests__/fixtures/uv-project
test-semver-range:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, oracle-aarch64]
steps:
- uses: actions/checkout@v4
- name: Install version 0.3
@@ -68,15 +72,15 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest, oracle-aarch64]
checksum:
["4d9279ad5ca596b1e2d703901d508430eb07564dc4d8837de9e2fca9c90f8ecd"]
exclude:
- os: macos-latest
- os: oracle-aarch64
checksum: "4d9279ad5ca596b1e2d703901d508430eb07564dc4d8837de9e2fca9c90f8ecd"
include:
- os: macos-latest
checksum: "a70cbfbf3bb5c08b2f84963b4f12c94e08fbb2468ba418a3bfe1066fbe9e7218"
- os: oracle-aarch64
checksum: "e11b01402ab645392c7ad6044db63d37e4fd1e745e015306993b07695ea5f9f8"
steps:
- uses: actions/checkout@v4
- name: Checksum matches expected
@@ -84,8 +88,6 @@ jobs:
with:
version: "0.3.2"
checksum: ${{ matrix.checksum }}
- run: uv sync
working-directory: __tests__/fixtures/uv-project
test-with-explicit-token:
runs-on: ubuntu-latest
steps:
@@ -107,63 +109,10 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
[
ubuntu-latest,
macos-latest,
macos-14,
windows-latest,
]
os: [windows-latest]
steps:
- uses: actions/checkout@v4
- name: Install default version
uses: ./
- run: uv tool install ruff
- run: ruff --version
test-tilde-expansion-tool-dirs:
runs-on: selfhosted-ubuntu-arm64
steps:
- uses: actions/checkout@v4
- name: Setup with cache
uses: ./
with:
tool-bin-dir: "~/tool-bin-dir"
tool-dir: "~/tool-dir"
- name: "Check if tool dirs are expanded"
run: |
if ! echo "$PATH" | grep -q "/home/ubuntu/tool-bin-dir"; then
echo "PATH does not contain /home/ubuntu/tool-bin-dir: $PATH"
exit 1
fi
if [ "$UV_TOOL_DIR" != "/home/ubuntu/tool-dir" ]; then
echo "UV_TOOL_DIR does not contain /home/ubuntu/tool-dir: $UV_TOOL_DIR"
exit 1
fi
test-python-version:
runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- name: Install latest version
uses: ./
with:
python-version: 3.13.1t
- name: Verify UV_PYTHON is set to correct version
run: |
echo "$UV_PYTHON"
if [ "$UV_PYTHON" != "3.13.1t" ]; then
exit 1
fi
shell: bash
- name: Verify packages can be installed
run: uv pip install --python=3.13.1t pip
shell: bash
- name: Verify python version is correct
run: |
python --version
if [ "$(python --version)" != "Python 3.13.1" ]; then
exit 1
fi
shell: bash

View File

@@ -17,7 +17,7 @@ jobs:
src/download/checksum/known-checksums.ts ${{ secrets.GITHUB_TOKEN }}
- run: npm install && npm run all
- name: Create Pull Request
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
uses: peter-evans/create-pull-request@6cd32fd93684475c31847837f87bb135d40a2b79 # v7.0.3
with:
commit-message: "chore: update known checksums"
title:

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
dist/
lib/
node_modules/

4
.prettierrc.json Normal file
View File

@@ -0,0 +1,4 @@
{
"trailingComma": "all",
"proseWrap": "always"
}

205
README.md
View File

@@ -14,17 +14,12 @@ Set up your GitHub Actions workflow with a specific version of [uv](https://docs
- [Install the latest version (default)](#install-the-latest-version-default)
- [Install a specific version](#install-a-specific-version)
- [Install a version by supplying a semver range](#install-a-version-by-supplying-a-semver-range)
- [Python version](#python-version)
- [Validate checksum](#validate-checksum)
- [Enable Caching](#enable-caching)
- [Cache dependency glob](#cache-dependency-glob)
- [Local cache path](#local-cache-path)
- [Disable cache pruning](#disable-cache-pruning)
- [Ignore nothing to cache](#ignore-nothing-to-cache)
- [GitHub authentication token](#github-authentication-token)
- [UV_TOOL_DIR](#uv_tool_dir)
- [UV_TOOL_BIN_DIR](#uv_tool_bin_dir)
- [Tilde Expansion](#tilde-expansion)
- [How it works](#how-it-works)
- [FAQ](#faq)
@@ -34,7 +29,7 @@ Set up your GitHub Actions workflow with a specific version of [uv](https://docs
```yaml
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
with:
version: "latest"
```
@@ -42,80 +37,49 @@ Set up your GitHub Actions workflow with a specific version of [uv](https://docs
For an example workflow, see
[here](https://github.com/charliermarsh/autobot/blob/e42c66659bf97b90ca9ff305a19cc99952d0d43f/.github/workflows/ci.yaml).
> [!TIP]
>
> Using `latest` requires that uv download the executable on every run, which incurs a cost
> (especially on self-hosted runners). As a best practice, consider pinning the version to a
> specific release.
### Install a specific version
```yaml
- name: Install a specific version of uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
with:
version: "0.4.4"
```
### Install a version by supplying a semver range
You can specify a [semver range](https://github.com/npm/node-semver?tab=readme-ov-file#ranges)
You can also specify a [semver range](https://github.com/npm/node-semver?tab=readme-ov-file#ranges)
to install the latest version that satisfies the range.
```yaml
- name: Install a semver range of uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
with:
version: ">=0.4.0"
version: ">=0.3.0"
```
```yaml
- name: Pinning a minor version of uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
with:
version: "0.4.x"
```
### Python version
You can use the input `python-version` to
- set the environment variable `UV_PYTHON` for the rest of your workflow
- create a new virtual environment with the specified python version
- activate the virtual environment for the rest of your workflow
This will override any python version specifications in `pyproject.toml` and `.python-version`
```yaml
- name: Install the latest version of uv and set the python version to 3.13t
uses: astral-sh/setup-uv@v4
with:
python-version: 3.13t
- run: uv pip install --python=3.13t pip
```
You can combine this with a matrix to test multiple python versions:
```yaml
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Install the latest version of uv and set the python version
uses: astral-sh/setup-uv@v4
with:
python-version: ${{ matrix.python-version }}
- name: Test with python ${{ matrix.python-version }}
run: uv run --frozen pytest
version: "0.3.x"
```
### Validate checksum
You can specify a checksum to validate the downloaded executable. Checksums up to the default version
are automatically verified by this action. The sha256 hashes can be found on the
You can also specify a checksum to validate the downloaded file. Checksums up to the default version
are automatically verified by this action. The sha265 hashes can be found on the
[releases page](https://github.com/astral-sh/uv/releases) of the uv repo.
```yaml
- name: Install a specific version and validate the checksum
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
with:
version: "0.3.1"
checksum: "e11b01402ab645392c7ad6044db63d37e4fd1e745e015306993b07695ea5f9f8"
@@ -123,8 +87,8 @@ are automatically verified by this action. The sha256 hashes can be found on the
### Enable caching
If you enable caching, the [uv cache](https://docs.astral.sh/uv/concepts/cache/) will be uploaded to
the GitHub Actions cache. This can speed up runs that reuse the cache by several minutes.
If you enable caching, the [uv cache](https://docs.astral.sh/uv/concepts/cache/) will be cached to
the GitHub Actions Cache. This can speed up runs that reuse the cache by several minutes.
> [!TIP]
>
@@ -136,7 +100,7 @@ You can optionally define a custom cache key suffix.
```yaml
- name: Enable caching and define a custom cache key suffix
id: setup-uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
cache-suffix: "optional-suffix"
@@ -153,30 +117,25 @@ use it in subsequent steps. For example, to use the cache in the above case:
#### Cache dependency glob
If you want to control when the GitHub Actions cache is invalidated, specify a glob pattern with the
`cache-dependency-glob` input. The GitHub Actions cache will be invalidated if any file matching the glob pattern
changes. If you use relative paths, they are relative to the repository root.
If you want to control when the cache is invalidated, specify a glob pattern with the
`cache-dependency-glob` input. The cache will be invalidated if any file matching the glob pattern
changes. The glob matches files relative to the repository root.
> [!NOTE]
>
> The default is
> ```yaml
> cache-dependency-glob: |
> **/requirements*.txt
> **/uv.lock
> ```
> The default is `**/uv.lock`.
```yaml
- name: Define a cache dependency glob
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
cache-dependency-glob: "**/requirements*.txt"
```
```yaml
- name: Define a list of cache dependency globs
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
cache-dependency-glob: |
@@ -184,17 +143,9 @@ changes. If you use relative paths, they are relative to the repository root.
**/pyproject.toml
```
```yaml
- name: Define an absolute cache dependency glob
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "/tmp/my-folder/requirements*.txt"
```
```yaml
- name: Never invalidate the cache
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
cache-dependency-glob: ""
@@ -202,51 +153,16 @@ changes. If you use relative paths, they are relative to the repository root.
### Local cache path
This action controls where uv stores its cache on the runner's filesystem by setting `UV_CACHE_DIR`.
It defaults to `setup-uv-cache` in the `TMP` dir, `D:\a\_temp\uv-tool-dir` on Windows and
`/tmp/setup-uv-cache` on Linux/macOS. You can change the default by specifying the path with the
`cache-local-path` input.
This action controls where uv stores its cache on the runner's filesystem. You can change the
default (`/tmp/setup-uv-cache`) by specifying the path with the `cache-local-path` input.
```yaml
- name: Define a custom uv cache path
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
with:
cache-local-path: "/path/to/cache"
```
### Disable cache pruning
By default, the uv cache is pruned after every run, removing pre-built wheels, but retaining any
wheels that were built from source. On GitHub-hosted runners, it's typically faster to omit those
pre-built wheels from the cache (and instead re-download them from the registry on each run).
However, on self-hosted or local runners, preserving the cache may be more efficient. See
the [documentation](https://docs.astral.sh/uv/concepts/cache/#caching-in-continuous-integration) for
more information.
If you want to persist the entire cache across runs, disable cache pruning with the `prune-cache`
input.
```yaml
- name: Don't prune the cache before saving it
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
prune-cache: false
```
### Ignore nothing to cache
By default, the action will fail if caching is enabled but there is nothing to upload (the uv cache directory does not exist).
If you want to ignore this, set the `ignore-nothing-to-cache` input to `true`.
```yaml
- name: Ignore nothing to cache
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
ignore-nothing-to-cache: true
```
### GitHub authentication token
This action uses the GitHub API to fetch the uv release artifacts. To avoid hitting the GitHub API
@@ -259,34 +175,15 @@ are not sufficient, you can provide a custom GitHub token with the necessary per
```yaml
- name: Install the latest version of uv with a custom GitHub token
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
with:
github-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
```
### UV_TOOL_DIR
On Windows `UV_TOOL_DIR` is set to `uv-tool-dir` in the `TMP` dir (e.g. `D:\a\_temp\uv-tool-dir`).
On GitHub hosted runners this is on the much faster `D:` drive.
On all other platforms the tool environments are placed in the
[default location](https://docs.astral.sh/uv/concepts/tools/#tools-directory).
If you want to change this behaviour (especially on self-hosted runners) you can use the `tool-dir`
input:
```yaml
- name: Install the latest version of uv with a custom tool dir
uses: astral-sh/setup-uv@v4
with:
tool-dir: "/path/to/tool/dir"
```
### UV_TOOL_BIN_DIR
On Windows `UV_TOOL_BIN_DIR` is set to `uv-tool-bin-dir` in the `TMP` dir (e.g.
`D:\a\_temp\uv-tool-bin-dir`). On GitHub hosted runners this is on the much faster `D:` drive. This
path is also automatically added to the PATH.
On Windows `UV_TOOL_BIN_DIR` is set to the `TMP` dir. On GitHub hosted runners this is on the much
faster `D:` drive. This path is also automatically added to the PATH.
On all other platforms the tool binaries get installed to the
[default location](https://docs.astral.sh/uv/concepts/tools/#the-bin-directory).
@@ -296,28 +193,9 @@ If you want to change this behaviour (especially on self-hosted runners) you can
```yaml
- name: Install the latest version of uv with a custom tool bin dir
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
with:
tool-bin-dir: "/path/to/tool-bin/dir"
```
### Tilde Expansion
This action supports expanding the `~` character to the user's home directory for the following inputs:
- `cache-local-path`
- `tool-dir`
- `tool-bin-dir`
- `cache-dependency-glob`
```yaml
- name: Expand the tilde character
uses: astral-sh/setup-uv@v4
with:
cache-local-path: "~/path/to/cache"
tool-dir: "~/path/to/tool/dir"
tool-bin-dir: "~/path/to/tool-bin/dir"
cache-dependency-glob: "~/my-cache-buster"
tool-bin-dir: "/path/to/tool/bin"
```
## How it works
@@ -334,22 +212,21 @@ by name (`uv`).
### Do I still need `actions/setup-python` alongside `setup-uv`?
With `setup-uv`, you can install a specific version of Python using `uv python install` rather than
No. This action is modelled as a drop-in replacement for `actions/setup-python` when using uv. With
`setup-uv`, you can install a specific version of Python using `uv python install` rather than
relying on `actions/setup-python`.
Using `actions/setup-python` can be faster, because GitHub caches the Python versions alongside the runner.
For example:
```yaml
- name: Checkout the repository
uses: actions/checkout@main
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- name: Test
run: uv run --frozen pytest # Uses the Python version automatically installed by uv
run: uv run --frozen pytest
```
To install a specific version of Python, use
@@ -357,7 +234,7 @@ To install a specific version of Python, use
```yaml
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- name: Install Python 3.12
@@ -376,7 +253,7 @@ output:
uses: actions/checkout@main
- name: Install the default version of uv
id: setup-uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v3
- name: Print the installed version
run: echo "Installed uv version is ${{ steps.setup-uv.outputs.uv-version }}"
```

View File

@@ -1,13 +0,0 @@
[project]
name = "old-python-constraint-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.8,<=3.9"
dependencies = [
"ruff>=0.6.2",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

View File

@@ -1,2 +0,0 @@
def hello() -> str:
return "Hello from uv-project!"

View File

@@ -1,38 +0,0 @@
version = 1
requires-python = ">=3.12"
[[package]]
name = "ruff"
version = "0.6.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/23/f4/279d044f66b79261fd37df76bf72b64471afab5d3b7906a01499c4451910/ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be", size = 2460281 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/72/4b/47dd7a69287afb4069fa42c198e899463605460a58120196711bfcf0446b/ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c", size = 9695871 },
{ url = "https://files.pythonhosted.org/packages/ae/c3/8aac62ac4638c14a740ee76a755a925f2d0d04580ab790a9887accb729f6/ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570", size = 9459354 },
{ url = "https://files.pythonhosted.org/packages/2f/cf/77fbd8d4617b9b9c503f9bffb8552c4e3ea1a58dc36975e7a9104ffb0f85/ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158", size = 9163871 },
{ url = "https://files.pythonhosted.org/packages/05/1c/765192bab32b79efbb498b06f0b9dcb3629112b53b8777ae1d19b8209e09/ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534", size = 10096250 },
{ url = "https://files.pythonhosted.org/packages/08/d0/86f3cb0f6934c99f759c232984a5204d67a26745cad2d9edff6248adf7d2/ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b", size = 9475376 },
{ url = "https://files.pythonhosted.org/packages/cd/cc/4c8d0e225b559a3fae6092ec310d7150d3b02b4669e9223f783ef64d82c0/ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d", size = 10295634 },
{ url = "https://files.pythonhosted.org/packages/db/96/d2699cfb1bb5a01c68122af43454c76c31331e1c8a9bd97d653d7c82524b/ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66", size = 11024941 },
{ url = "https://files.pythonhosted.org/packages/8b/a9/6ecd66af8929e0f2a1ed308a4137f3521789f28f0eb97d32c2ca3aa7000c/ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8", size = 10606894 },
{ url = "https://files.pythonhosted.org/packages/e4/73/2ee4cd19f44992fedac1cc6db9e3d825966072f6dcbd4032f21cbd063170/ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1", size = 11552886 },
{ url = "https://files.pythonhosted.org/packages/60/4c/c0f1cd35ce4a93c54a6bb1ee6934a3a205fa02198dd076678193853ceea1/ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1", size = 10264945 },
{ url = "https://files.pythonhosted.org/packages/c4/89/e45c9359b9cdd4245512ea2b9f2bb128a997feaa5f726fc9e8c7a66afadf/ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23", size = 10100007 },
{ url = "https://files.pythonhosted.org/packages/06/74/0bd4e0a7ed5f6908df87892f9bf60a2356c0fd74102d8097298bd9b4f346/ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a", size = 9559267 },
{ url = "https://files.pythonhosted.org/packages/54/03/3dc6dc9419f276f05805bf888c279e3e0b631284abd548d9e87cebb93aec/ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c", size = 9905304 },
{ url = "https://files.pythonhosted.org/packages/5c/5b/d6a72a6a6bbf097c09de468326ef5fa1c9e7aa5e6e45979bc0d984b0dbe7/ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56", size = 10341480 },
{ url = "https://files.pythonhosted.org/packages/79/a9/0f2f21fe15ba537c46598f96aa9ae4a3d4b9ec64926664617ca6a8c772f4/ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da", size = 7961901 },
{ url = "https://files.pythonhosted.org/packages/b0/80/fff12ffe11853d9f4ea3e5221e6dd2e93640a161c05c9579833e09ad40a7/ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2", size = 8783320 },
{ url = "https://files.pythonhosted.org/packages/56/91/577cdd64cce5e74d3f8b5ecb93f29566def569c741eb008aed4f331ef821/ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9", size = 8225886 },
]
[[package]]
name = "uv-project"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "ruff" },
]
[package.metadata]
requires-dist = [{ name = "ruff" }]

View File

@@ -1 +0,0 @@
print("Hello world")

View File

@@ -1 +0,0 @@
ruff>=0.6.2

View File

@@ -6,42 +6,29 @@ inputs:
version:
description: "The version of uv to install"
default: "latest"
python-version:
description: "The version of Python to set UV_PYTHON to"
required: false
checksum:
description: "The checksum of the uv version to install"
required: false
github-token:
description:
"Used to increase the rate limit when retrieving versions and downloading uv."
"Used to increase the rate limit when retrieving versions and downloading
uv."
required: false
default: ${{ github.token }}
enable-cache:
description: "Enable caching of the uv cache"
default: "auto"
default: "false"
cache-dependency-glob:
description:
"Glob pattern to match files relative to the repository root to control
the cache."
default: |
**/uv.lock
**/requirements*.txt
default: "**/uv.lock"
cache-suffix:
description: "Suffix for the cache key"
required: false
cache-local-path:
description: "Local path to store the cache."
default: ""
prune-cache:
description: "Prune cache before saving."
default: "true"
ignore-nothing-to-cache:
description: "Ignore when nothing is found to cache."
default: "false"
tool-dir:
description: "Custom path to set UV_TOOL_DIR to."
required: false
tool-bin-dir:
description: "Custom path to set UV_TOOL_BIN_DIR to."
required: false

View File

@@ -1,31 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.2/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": ["dist", "lib", "node_modules"]
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"trailingCommas": "all"
}
}
}

13369
dist/save-cache/index.js generated vendored

File diff suppressed because one or more lines are too long

14340
dist/setup/index.js generated vendored

File diff suppressed because one or more lines are too long

4804
dist/update-known-checksums/index.js generated vendored

File diff suppressed because one or more lines are too long

6683
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,25 +6,30 @@
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"format": "biome format --fix",
"format-check": "biome format",
"lint": "biome lint --fix",
"format": "prettier --write .",
"format-check": "prettier --check .",
"lint": "eslint src/**/*.ts --fix",
"package": "ncc build -o dist/setup src/setup-uv.ts && ncc build -o dist/save-cache src/save-cache.ts && ncc build -o dist/update-known-checksums src/update-known-checksums.ts",
"test": "jest",
"act": "act pull_request -W .github/workflows/test.yml --container-architecture linux/amd64 -s GITHUB_TOKEN=\"$(gh auth token)\"",
"update-known-checksums": "RUNNER_TEMP=known_checksums node dist/update-known-checksums/index.js src/download/checksum/known-checksums.ts \"$(gh auth token)\"",
"update-known-checksums": "node dist/update-known-checksums/index.js src/download/checksum/known-checksums.ts \"$(gh auth token)\"",
"all": "npm run build && npm run format && npm run lint && npm run package && npm test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/astral-sh/setup-uv.git"
},
"keywords": ["actions", "python", "setup", "uv"],
"keywords": [
"actions",
"python",
"setup",
"uv"
],
"author": "@eifinger",
"license": "MIT",
"dependencies": {
"@actions/cache": "^4.0.0",
"@actions/core": "^1.11.1",
"@actions/cache": "^3.2.4",
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"@actions/glob": "^0.5.0",
@@ -33,13 +38,20 @@
"@octokit/rest": "^21.0.2"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/node": "^22.10.2",
"@types/node": "^22.5.5",
"@types/semver": "^7.5.8",
"@vercel/ncc": "^0.38.3",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.18.0",
"@vercel/ncc": "^0.38.1",
"eslint": "^8.57.1",
"eslint-plugin-github": "^5.0.2",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jest": "^28.8.3",
"eslint-plugin-prettier": "^5.2.1",
"jest": "^29.7.0",
"js-yaml": "^4.1.0",
"prettier": "^3.3.3",
"ts-jest": "^29.2.5",
"typescript": "^5.7.2"
"typescript": "^5.6.2"
}
}

View File

@@ -1,21 +1,19 @@
import * as cache from "@actions/cache";
import * as glob from "@actions/glob";
import * as core from "@actions/core";
import {
cacheDependencyGlob,
cacheLocalPath,
cacheSuffix,
pythonVersion as pythonVersionInput,
} from "../utils/inputs";
import { getArch, getPlatform } from "../utils/platforms";
import { hashFiles } from "../hash/hash-files";
import * as exec from "@actions/exec";
export const STATE_CACHE_KEY = "cache-key";
export const STATE_CACHE_MATCHED_KEY = "cache-matched-key";
const CACHE_VERSION = "1";
export async function restoreCache(): Promise<void> {
const cacheKey = await computeKeys();
export async function restoreCache(version: string): Promise<void> {
const cacheKey = await computeKeys(version);
let matchedKey: string | undefined;
core.info(
@@ -35,56 +33,28 @@ export async function restoreCache(): Promise<void> {
handleMatchResult(matchedKey, cacheKey);
}
async function computeKeys(): Promise<string> {
async function computeKeys(version: string): Promise<string> {
let cacheDependencyPathHash = "-";
if (cacheDependencyGlob !== "") {
core.info(
`Searching files using cache dependency glob: ${cacheDependencyGlob.split("\n").join(",")}`,
);
cacheDependencyPathHash += await hashFiles(cacheDependencyGlob, true);
cacheDependencyPathHash += await glob.hashFiles(
cacheDependencyGlob,
undefined,
undefined,
true,
);
if (cacheDependencyPathHash === "-") {
core.warning(
`No file matched to [${cacheDependencyGlob.split("\n").join(",")}]. The cache will never get invalidated. Make sure you have checked out the target repository and configured the cache-dependency-glob input correctly.`,
throw new Error(
`No file in ${process.cwd()} matched to [${cacheDependencyGlob.split("\n").join(",")}], make sure you have checked out the target repository`,
);
}
}
if (cacheDependencyPathHash === "-") {
cacheDependencyPathHash = "-no-dependency-glob";
} else {
cacheDependencyPathHash += "no-dependency-glob";
}
const suffix = cacheSuffix ? `-${cacheSuffix}` : "";
const pythonVersion = await getPythonVersion();
return `setup-uv-${CACHE_VERSION}-${getArch()}-${getPlatform()}-${pythonVersion}${cacheDependencyPathHash}${suffix}`;
}
async function getPythonVersion(): Promise<string> {
if (pythonVersionInput !== "") {
return pythonVersionInput;
}
let output = "";
const options: exec.ExecOptions = {
silent: !core.isDebug(),
listeners: {
stdout: (data: Buffer) => {
output += data.toString();
},
},
};
try {
const execArgs = ["python", "find"];
await exec.exec("uv", execArgs, options);
const pythonPath = output.trim();
output = "";
await exec.exec(pythonPath, ["--version"], options);
// output is like "Python 3.8.10"
return output.split(" ")[1].trim();
} catch (error) {
const err = error as Error;
core.debug(`Failed to get python version from uv. Error: ${err.message}`);
return "unknown";
}
return `setup-uv-${CACHE_VERSION}-${getArch()}-${getPlatform()}-${version}${cacheDependencyPathHash}${suffix}`;
}
function handleMatchResult(

View File

@@ -1,9 +1,9 @@
import * as fs from "node:fs";
import * as crypto from "node:crypto";
import * as fs from "fs";
import * as crypto from "crypto";
import * as core from "@actions/core";
import { KNOWN_CHECKSUMS } from "./known-checksums";
import type { Architecture, Platform } from "../../utils/platforms";
import { Architecture, Platform } from "../../utils/platforms";
export async function validateChecksum(
checkSum: string | undefined,
@@ -12,11 +12,11 @@ export async function validateChecksum(
platform: Platform,
version: string,
): Promise<void> {
let isValid: boolean | undefined = undefined;
let isValid = true;
if (checkSum !== undefined && checkSum !== "") {
isValid = await validateFileCheckSum(downloadPath, checkSum);
} else {
core.debug("Checksum not provided. Checking known checksums.");
core.debug(`Checksum not provided. Checking known checksums.`);
const key = `${arch}-${platform}-${version}`;
if (key in KNOWN_CHECKSUMS) {
const knownChecksum = KNOWN_CHECKSUMS[`${arch}-${platform}-${version}`];
@@ -27,12 +27,10 @@ export async function validateChecksum(
}
}
if (isValid === false) {
if (!isValid) {
throw new Error(`Checksum for ${downloadPath} did not match ${checkSum}.`);
}
if (isValid === true) {
core.debug(`Checksum for ${downloadPath} is valid.`);
}
core.debug(`Checksum for ${downloadPath} is valid.`);
}
async function validateFileCheckSum(

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
import { promises as fs } from "node:fs";
import { promises as fs } from "fs";
import * as tc from "@actions/tool-cache";
import { KNOWN_CHECKSUMS } from "./known-checksums";
export async function updateChecksums(
filePath: string,
downloadUrls: string[],
@@ -8,50 +7,31 @@ export async function updateChecksums(
await fs.rm(filePath);
await fs.appendFile(
filePath,
"// AUTOGENERATED_DO_NOT_EDIT\nexport const KNOWN_CHECKSUMS: { [key: string]: string } = {\n",
"// AUTOGENERATED_DO_NOT_EDIT\nexport const KNOWN_CHECKSUMS: {[key: string]: string} = {\n",
);
let firstLine = true;
for (const downloadUrl of downloadUrls) {
const content = await downloadAssetContent(downloadUrl);
const checksum = content.split(" ")[0].trim();
const key = getKey(downloadUrl);
if (key === undefined) {
continue;
}
const checksum = await getOrDownloadChecksum(key, downloadUrl);
if (!firstLine) {
await fs.appendFile(filePath, ",\n");
}
await fs.appendFile(filePath, ` "${key}":\n "${checksum}"`);
await fs.appendFile(filePath, ` '${key}':\n '${checksum}'`);
firstLine = false;
}
await fs.appendFile(filePath, ",\n};\n");
await fs.appendFile(filePath, "}\n");
}
function getKey(downloadUrl: string): string | undefined {
function getKey(downloadUrl: string): string {
// https://github.com/astral-sh/uv/releases/download/0.3.2/uv-aarch64-apple-darwin.tar.gz.sha256
const parts = downloadUrl.split("/");
const fileName = parts[parts.length - 1];
if (fileName.startsWith("source")) {
return undefined;
}
const name = fileName.split(".")[0].split("uv-")[1];
const version = parts[parts.length - 2];
return `${name}-${version}`;
}
async function getOrDownloadChecksum(
key: string,
downloadUrl: string,
): Promise<string> {
let checksum = "";
if (key in KNOWN_CHECKSUMS) {
checksum = KNOWN_CHECKSUMS[key];
} else {
const content = await downloadAssetContent(downloadUrl);
checksum = content.split(" ")[0].trim();
}
return checksum;
}
async function downloadAssetContent(downloadUrl: string): Promise<string> {
const downloadPath = await tc.downloadTool(downloadUrl);
const content = await fs.readFile(downloadPath, "utf8");

View File

@@ -0,0 +1,65 @@
import * as core from "@actions/core";
import * as tc from "@actions/tool-cache";
import * as exec from "@actions/exec";
import * as path from "path";
import { Architecture, Platform } from "../utils/platforms";
import { validateChecksum } from "./checksum/checksum";
import { OWNER, REPO } from "../utils/constants";
export async function downloadLatest(
platform: Platform,
arch: Architecture,
checkSum: string | undefined,
githubToken: string | undefined,
): Promise<{ cachedToolDir: string; version: string }> {
const artifact = `uv-${arch}-${platform}`;
let downloadUrl = `https://github.com/${OWNER}/${REPO}/releases/latest/download/${artifact}`;
if (platform === "pc-windows-msvc") {
downloadUrl += ".zip";
} else {
downloadUrl += ".tar.gz";
}
core.info(`Downloading uv from "${downloadUrl}" ...`);
const downloadPath = await tc.downloadTool(
downloadUrl,
undefined,
githubToken,
);
let uvExecutablePath: string;
let uvDir: string;
if (platform === "pc-windows-msvc") {
uvDir = await tc.extractZip(downloadPath);
// On windows extracting the zip does not create an intermediate directory
uvExecutablePath = path.join(uvDir, "uv.exe");
} else {
const extractedDir = await tc.extractTar(downloadPath);
uvDir = path.join(extractedDir, artifact);
uvExecutablePath = path.join(uvDir, "uv");
}
const version = await getVersion(uvExecutablePath);
await validateChecksum(checkSum, downloadPath, arch, platform, version);
return { cachedToolDir: uvDir, version };
}
async function getVersion(uvExecutablePath: string): Promise<string> {
// Parse the output of `uv --version` to get the version
// The output looks like
// uv 0.3.1 (be17d132a 2024-08-21)
const options: exec.ExecOptions = {
silent: !core.isDebug(),
};
const execArgs = ["--version"];
let output = "";
options.listeners = {
stdout: (data: Buffer) => {
output += data.toString();
},
};
await exec.exec(uvExecutablePath, execArgs, options);
const parts = output.split(" ");
return parts[1];
}

View File

@@ -1,14 +1,8 @@
import * as core from "@actions/core";
import * as tc from "@actions/tool-cache";
import * as path from "node:path";
import { promises as fs } from "node:fs";
import {
GITHUB_COM_API,
OWNER,
REPO,
TOOL_CACHE_NAME,
} from "../utils/constants";
import type { Architecture, Platform } from "../utils/platforms";
import * as path from "path";
import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants";
import { Architecture, Platform } from "../utils/platforms";
import { validateChecksum } from "./checksum/checksum";
import * as github from "@actions/github";
@@ -36,11 +30,12 @@ export async function downloadVersion(
): Promise<{ version: string; cachedToolDir: string }> {
const resolvedVersion = await resolveVersion(version, githubToken);
const artifact = `uv-${arch}-${platform}`;
let extension = ".tar.gz";
let downloadUrl = `https://github.com/${OWNER}/${REPO}/releases/download/${resolvedVersion}/${artifact}`;
if (platform === "pc-windows-msvc") {
extension = ".zip";
downloadUrl += ".zip";
} else {
downloadUrl += ".tar.gz";
}
const downloadUrl = `https://github.com/${OWNER}/${REPO}/releases/download/${resolvedVersion}/${artifact}${extension}`;
core.info(`Downloading uv from "${downloadUrl}" ...`);
const downloadPath = await tc.downloadTool(
@@ -58,9 +53,7 @@ export async function downloadVersion(
let uvDir: string;
if (platform === "pc-windows-msvc") {
const fullPathWithExtension = `${downloadPath}${extension}`;
await fs.copyFile(downloadPath, fullPathWithExtension);
uvDir = await tc.extractZip(fullPathWithExtension);
uvDir = await tc.extractZip(downloadPath);
// On windows extracting the zip does not create an intermediate directory
} else {
const extractedDir = await tc.extractTar(downloadPath);
@@ -75,14 +68,10 @@ export async function downloadVersion(
return { version: resolvedVersion, cachedToolDir };
}
export async function resolveVersion(
versionInput: string,
async function resolveVersion(
version: string,
githubToken: string,
): Promise<string> {
const version =
versionInput === "latest"
? await getLatestVersion(githubToken)
: versionInput;
if (tc.isExplicitVersion(version)) {
core.debug(`Version ${version} is an explicit version.`);
return version;
@@ -96,7 +85,7 @@ export async function resolveVersion(
}
async function getAvailableVersions(githubToken: string): Promise<string[]> {
const octokit = github.getOctokit(githubToken, { baseUrl: GITHUB_COM_API });
const octokit = github.getOctokit(githubToken);
const response = await octokit.paginate(octokit.rest.repos.listReleases, {
owner: OWNER,
@@ -104,17 +93,3 @@ async function getAvailableVersions(githubToken: string): Promise<string[]> {
});
return response.map((release) => release.tag_name);
}
async function getLatestVersion(githubToken: string) {
const octokit = github.getOctokit(githubToken, { baseUrl: GITHUB_COM_API });
const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({
owner: OWNER,
repo: REPO,
});
if (!latestRelease) {
throw new Error("Could not determine latest release.");
}
return latestRelease.tag_name;
}

View File

@@ -1,48 +0,0 @@
import * as crypto from "node:crypto";
import * as core from "@actions/core";
import * as fs from "node:fs";
import * as stream from "node:stream";
import * as util from "node:util";
import { create } from "@actions/glob";
/**
* Hashes files matching the given glob pattern.
*
* Copied from https://github.com/actions/toolkit/blob/20ed2908f19538e9dfb66d8083f1171c0a50a87c/packages/glob/src/internal-hash-files.ts#L9-L49
* But supports hashing files outside the GITHUB_WORKSPACE.
* @param pattern The glob pattern to match files.
* @param verbose Whether to log the files being hashed.
*/
export async function hashFiles(
pattern: string,
verbose = false,
): Promise<string> {
const globber = await create(pattern);
let hasMatch = false;
const writeDelegate = verbose ? core.info : core.debug;
const result = crypto.createHash("sha256");
let count = 0;
for await (const file of globber.globGenerator()) {
writeDelegate(file);
if (fs.statSync(file).isDirectory()) {
writeDelegate(`Skip directory '${file}'.`);
continue;
}
const hash = crypto.createHash("sha256");
const pipeline = util.promisify(stream.pipeline);
await pipeline(fs.createReadStream(file), hash);
result.write(hash.digest());
count++;
if (!hasMatch) {
hasMatch = true;
}
}
result.end();
if (hasMatch) {
writeDelegate(`Found ${count} files to hash.`);
return result.digest("hex");
}
writeDelegate("No matches found for glob");
return "";
}

View File

@@ -1,33 +1,22 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import * as fs from "node:fs";
import {
STATE_CACHE_MATCHED_KEY,
STATE_CACHE_KEY,
} from "./cache/restore-cache";
import {
cacheLocalPath,
enableCache,
ignoreNothingToCache,
pruneCache as shouldPruneCache,
} from "./utils/inputs";
import { cacheLocalPath, enableCache } from "./utils/inputs";
export async function run(): Promise<void> {
try {
if (enableCache) {
await saveCache();
// node will stay alive if any promises are not resolved,
// which is a possibility if HTTP requests are dangling
// due to retries or timeouts. We know that if we got here
// that all promises that we care about have successfully
// resolved, so simply exit with success.
process.exit(0);
}
} catch (error) {
const err = error as Error;
core.setFailed(err.message);
}
process.exit(0);
}
async function saveCache(): Promise<void> {
@@ -37,38 +26,17 @@ async function saveCache(): Promise<void> {
if (!cacheKey) {
core.warning("Error retrieving cache key from state.");
return;
}
if (matchedKey === cacheKey) {
} else if (matchedKey === cacheKey) {
core.info(`Cache hit occurred on key ${cacheKey}, not saving cache.`);
return;
}
if (shouldPruneCache) {
await pruneCache();
}
await pruneCache();
core.info(`Saving cache path: ${cacheLocalPath}`);
if (!fs.existsSync(cacheLocalPath) && !ignoreNothingToCache) {
throw new Error(
`Cache path ${cacheLocalPath} does not exist on disk. This likely indicates that there are no dependencies to cache. Consider disabling the cache input if it is not needed.`,
);
}
try {
await cache.saveCache([cacheLocalPath], cacheKey);
core.info(`cache saved with the key: ${cacheKey}`);
} catch (e) {
if (
e instanceof Error &&
e.message ===
"Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved."
) {
core.info(
"No cacheable paths were found. Ignoring because ignore-nothing-to-save is enabled.",
);
} else {
throw e;
}
}
await cache.saveCache([cacheLocalPath], cacheKey);
core.info(`cache saved with the key: ${cacheKey}`);
}
async function pruneCache(): Promise<void> {

View File

@@ -1,29 +1,26 @@
import * as core from "@actions/core";
import * as path from "node:path";
import * as path from "path";
import {
downloadVersion,
tryGetFromToolCache,
resolveVersion,
} from "./download/download-version";
import { restoreCache } from "./cache/restore-cache";
import { downloadLatest } from "./download/download-latest";
import {
type Architecture,
Architecture,
getArch,
getPlatform,
type Platform,
Platform,
} from "./utils/platforms";
import {
cacheLocalPath,
checkSum,
enableCache,
githubToken,
pythonVersion,
toolBinDir,
toolDir,
version,
} from "./utils/inputs";
import * as exec from "@actions/exec";
async function run(): Promise<void> {
const platform = getPlatform();
@@ -46,16 +43,14 @@ async function run(): Promise<void> {
addUvToPath(setupResult.uvDir);
addToolBinToPath();
setToolDir();
await setupPython();
addMatchers();
setCacheDir(cacheLocalPath);
core.setOutput("uv-version", setupResult.version);
core.info(`Successfully installed uv version ${setupResult.version}`);
addMatchers();
setCacheDir(cacheLocalPath);
if (enableCache) {
await restoreCache();
await restoreCache(setupResult.version);
}
process.exit(0);
} catch (err) {
@@ -70,28 +65,38 @@ async function setupUv(
checkSum: string | undefined,
githubToken: string,
): Promise<{ uvDir: string; version: string }> {
const resolvedVersion = await resolveVersion(versionInput, githubToken);
const toolCacheResult = tryGetFromToolCache(arch, resolvedVersion);
if (toolCacheResult.installedPath) {
core.info(`Found uv in tool-cache for ${toolCacheResult.version}`);
return {
uvDir: toolCacheResult.installedPath,
version: toolCacheResult.version,
};
let installedPath: string | undefined;
let cachedToolDir: string;
let version: string;
if (versionInput === "latest") {
const latestResult = await downloadLatest(
platform,
arch,
checkSum,
githubToken,
);
version = latestResult.version;
cachedToolDir = latestResult.cachedToolDir;
} else {
const toolCacheResult = tryGetFromToolCache(arch, versionInput);
version = toolCacheResult.version;
installedPath = toolCacheResult.installedPath;
if (installedPath) {
core.info(`Found uv in tool-cache for ${versionInput}`);
return { uvDir: installedPath, version };
}
const versionResult = await downloadVersion(
platform,
arch,
versionInput,
checkSum,
githubToken,
);
cachedToolDir = versionResult.cachedToolDir;
version = versionResult.version;
}
const downloadVersionResult = await downloadVersion(
platform,
arch,
resolvedVersion,
checkSum,
githubToken,
);
return {
uvDir: downloadVersionResult.cachedToolDir,
version: downloadVersionResult.version,
};
return { uvDir: cachedToolDir, version };
}
function addUvToPath(cachedPath: string): void {
@@ -105,45 +110,6 @@ function addToolBinToPath(): void {
core.info(`Set UV_TOOL_BIN_DIR to ${toolBinDir}`);
core.addPath(toolBinDir);
core.info(`Added ${toolBinDir} to the path`);
} else {
if (process.env.XDG_BIN_HOME !== undefined) {
core.addPath(process.env.XDG_BIN_HOME);
core.info(`Added ${process.env.XDG_BIN_HOME} to the path`);
} else if (process.env.XDG_DATA_HOME !== undefined) {
core.addPath(`${process.env.XDG_DATA_HOME}/../bin`);
core.info(`Added ${process.env.XDG_DATA_HOME}/../bin to the path`);
} else {
core.addPath(`${process.env.HOME}/.local/bin`);
core.info(`Added ${process.env.HOME}/.local/bin to the path`);
}
}
}
function setToolDir(): void {
if (toolDir !== undefined) {
core.exportVariable("UV_TOOL_DIR", toolDir);
core.info(`Set UV_TOOL_DIR to ${toolDir}`);
}
}
async function setupPython(): Promise<void> {
if (pythonVersion !== "") {
core.exportVariable("UV_PYTHON", pythonVersion);
core.info(`Set UV_PYTHON to ${pythonVersion}`);
const options: exec.ExecOptions = {
silent: !core.isDebug(),
};
const execArgs = ["venv", "--python", pythonVersion];
core.info("Activating python venv...");
await exec.exec("uv", execArgs, options);
let venvBinPath = ".venv/bin";
if (process.platform === "win32") {
venvBinPath = ".venv/Scripts";
}
core.addPath(venvBinPath);
core.exportVariable("VIRTUAL_ENV", ".venv");
}
}

View File

@@ -1,7 +1,7 @@
import * as github from "@actions/github";
import * as core from "@actions/core";
import { GITHUB_COM_API, OWNER, REPO } from "./utils/constants";
import { OWNER, REPO } from "./utils/constants";
import * as semver from "semver";
import { updateChecksums } from "./download/checksum/update-known-checksums";
@@ -10,7 +10,7 @@ async function run(): Promise<void> {
const checksumFilePath = process.argv.slice(2)[0];
const github_token = process.argv.slice(2)[1];
const octokit = github.getOctokit(github_token, { baseUrl: GITHUB_COM_API });
const octokit = github.getOctokit(github_token);
const response = await octokit.paginate(octokit.rest.repos.listReleases, {
owner: OWNER,

View File

@@ -1,4 +1,3 @@
export const REPO = "uv";
export const OWNER = "astral-sh";
export const TOOL_CACHE_NAME = "uv";
export const GITHUB_COM_API = "https://api.github.com";

View File

@@ -1,56 +1,22 @@
import * as core from "@actions/core";
import path from "node:path";
import path from "path";
export const version = core.getInput("version");
export const pythonVersion = core.getInput("python-version");
export const checkSum = core.getInput("checksum");
export const enableCache = getEnableCache();
export const enableCache = core.getInput("enable-cache") === "true";
export const cacheSuffix = core.getInput("cache-suffix") || "";
export const cacheLocalPath = getCacheLocalPath();
export const cacheDependencyGlob = core.getInput("cache-dependency-glob");
export const pruneCache = core.getInput("prune-cache") === "true";
export const ignoreNothingToCache =
core.getInput("ignore-nothing-to-cache") === "true";
export const toolBinDir = getToolBinDir();
export const toolDir = getToolDir();
export const githubToken = core.getInput("github-token");
function getEnableCache(): boolean {
const enableCacheInput = core.getInput("enable-cache");
if (enableCacheInput === "auto") {
return process.env.RUNNER_ENVIRONMENT === "github-hosted";
}
return enableCacheInput === "true";
}
function getToolBinDir(): string | undefined {
const toolBinDirInput = core.getInput("tool-bin-dir");
if (toolBinDirInput !== "") {
return expandTilde(toolBinDirInput);
return toolBinDirInput;
}
if (process.platform === "win32") {
if (process.env.RUNNER_TEMP !== undefined) {
return `${process.env.RUNNER_TEMP}${path.sep}uv-tool-bin-dir`;
}
throw Error(
"Could not determine UV_TOOL_BIN_DIR. Please make sure RUNNER_TEMP is set or provide the tool-bin-dir input",
);
}
return undefined;
}
function getToolDir(): string | undefined {
const toolDirInput = core.getInput("tool-dir");
if (toolDirInput !== "") {
return expandTilde(toolDirInput);
}
if (process.platform === "win32") {
if (process.env.RUNNER_TEMP !== undefined) {
return `${process.env.RUNNER_TEMP}${path.sep}uv-tool-dir`;
}
throw Error(
"Could not determine UV_TOOL_DIR. Please make sure RUNNER_TEMP is set or provide the tool-dir input",
);
return "D:\\a\\_temp\\uv-tool-bin-dir";
}
return undefined;
}
@@ -58,19 +24,13 @@ function getToolDir(): string | undefined {
function getCacheLocalPath(): string {
const cacheLocalPathInput = core.getInput("cache-local-path");
if (cacheLocalPathInput !== "") {
return expandTilde(cacheLocalPathInput);
return cacheLocalPathInput;
}
if (process.env.RUNNER_TEMP !== undefined) {
return `${process.env.RUNNER_TEMP}${path.sep}setup-uv-cache`;
}
throw Error(
"Could not determine UV_CACHE_DIR. Please make sure RUNNER_TEMP is set or provide the cache-local-path input",
);
}
function expandTilde(input: string): string {
if (input.startsWith("~")) {
return `${process.env.HOME}${input.substring(1)}`;
if (process.platform === "win32") {
return "D:\\a\\_temp\\setup-uv-cache";
}
return input;
return "/tmp/setup-uv-cache";
}