├── .editorconfig ├── .git-blame-ignore-revs ├── .gitattributes ├── .github ├── actionlint.yaml ├── dependabot.yml ├── python.json ├── release-drafter.yml ├── scripts │ └── check-all-tests-passed-needs.ts └── workflows │ ├── codeql-analysis.yml │ ├── release-drafter.yml │ ├── test.yml │ ├── update-known-versions.yml │ └── update-major-minor-tags.yml ├── .gitignore ├── CODEOWNERS ├── LICENSE ├── README.md ├── __tests__ ├── download │ └── checksum │ │ └── checksum.test.ts └── fixtures │ ├── checksumfile │ ├── malformed-pyproject-toml-project │ ├── .python-version │ ├── README.md │ ├── hello.py │ └── pyproject.toml │ ├── old-python-constraint-project │ ├── README.md │ ├── pyproject.toml │ ├── src │ │ └── old_python_constraint_project │ │ │ └── __init__.py │ └── uv.lock │ ├── pyproject-toml-project │ ├── .python-version │ ├── README.md │ ├── hello.py │ └── pyproject.toml │ ├── requirements-txt-project │ ├── hello_world.py │ └── requirements.txt │ ├── uv-project │ ├── README.md │ ├── pyproject.toml │ ├── src │ │ └── uv_project │ │ │ └── __init__.py │ └── uv.lock │ └── uv-toml-project │ ├── .python-version │ ├── README.md │ ├── hello.py │ ├── pyproject.toml │ └── uv.toml ├── action.yml ├── biome.json ├── dist ├── save-cache │ └── index.js ├── setup │ └── index.js └── update-known-versions │ └── index.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── cache │ └── restore-cache.ts ├── download │ ├── checksum │ │ ├── checksum.ts │ │ ├── known-checksums.ts │ │ └── update-known-checksums.ts │ ├── download-version.ts │ └── version-manifest.ts ├── hash │ └── hash-files.ts ├── save-cache.ts ├── setup-uv.ts ├── update-known-versions.ts └── utils │ ├── config-file.ts │ ├── constants.ts │ ├── inputs.ts │ ├── octokit.ts │ └── platforms.ts ├── tsconfig.json └── version-manifest.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Check http://editorconfig.org for more information 2 | # This is the main config file for this project: 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | indent_style = space 10 | insert_final_newline = true 11 | indent_size = 2 12 | 13 | [*.{rs,py,pyi}] 14 | indent_size = 4 15 | 16 | [*.snap] 17 | trim_trailing_whitespace = false 18 | 19 | [*.md] 20 | max_line_length = 100 21 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astral-sh/setup-uv/d44461ea9f6b6d77b25a3b196dc2cdb60b5d29eb/.git-blame-ignore-revs -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | dist/** -diff linguist-generated=true 3 | -------------------------------------------------------------------------------- /.github/actionlint.yaml: -------------------------------------------------------------------------------- 1 | self-hosted-runner: 2 | # Custom labels of self-hosted or large GitHub hosted runners 3 | # so that actionlint knows that they are not a typo 4 | labels: 5 | - selfhosted-ubuntu-arm64 6 | # Configuration variables in array of strings defined in your repository or 7 | # organization. `null` means disabling configuration variables check. 8 | # Empty array means no configuration variable is allowed. 9 | config-variables: null 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | 8 | - package-ecosystem: npm 9 | directory: / 10 | schedule: 11 | interval: daily 12 | -------------------------------------------------------------------------------- /.github/python.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "python", 5 | "pattern": [ 6 | { 7 | "regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$", 8 | "file": 1, 9 | "line": 2 10 | }, 11 | { 12 | "regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$", 13 | "message": 2 14 | } 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: "v$RESOLVED_VERSION 🌈" 2 | tag-template: "v$RESOLVED_VERSION" 3 | categories: 4 | - title: "🚨 Breaking changes" 5 | labels: 6 | - "breaking-change" 7 | - title: "✨ New features" 8 | labels: 9 | - "new-feature" 10 | - title: "🐛 Bug fixes" 11 | labels: 12 | - "bugfix" 13 | - title: "🚀 Enhancements" 14 | labels: 15 | - "enhancement" 16 | - "refactor" 17 | - "performance" 18 | - title: "🧰 Maintenance" 19 | labels: 20 | - "maintenance" 21 | - "ci" 22 | - "update-known-versions" 23 | - title: "📚 Documentation" 24 | labels: 25 | - "documentation" 26 | - title: "⬆️ Dependency updates" 27 | labels: 28 | - "dependencies" 29 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)" 30 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 31 | version-resolver: 32 | major: 33 | labels: 34 | - "major" 35 | - "breaking-change" 36 | minor: 37 | labels: 38 | - "minor" 39 | - "new-feature" 40 | - "enhancement" 41 | patch: 42 | labels: 43 | - "patch" 44 | - "bugfix" 45 | - "default-version-update" 46 | default: patch 47 | template: | 48 | ## Changes 49 | 50 | $CHANGES 51 | -------------------------------------------------------------------------------- /.github/scripts/check-all-tests-passed-needs.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import * as yaml from "js-yaml"; 3 | 4 | interface WorkflowJob { 5 | needs?: string[]; 6 | [key: string]: unknown; 7 | } 8 | 9 | interface Workflow { 10 | jobs: Record; 11 | [key: string]: unknown; 12 | } 13 | 14 | const workflow = yaml.load( 15 | fs.readFileSync("../workflows/test.yml", "utf8"), 16 | ) as Workflow; 17 | const jobs = Object.keys(workflow.jobs); 18 | const allTestsPassed = workflow.jobs["all-tests-passed"]; 19 | const needs: string[] = allTestsPassed.needs || []; 20 | 21 | const expectedNeeds = jobs.filter((j) => j !== "all-tests-passed"); 22 | const missing = expectedNeeds.filter((j) => !needs.includes(j)); 23 | 24 | if (missing.length > 0) { 25 | console.error( 26 | `Missing jobs in all-tests-passed needs: ${missing.join(", ")}`, 27 | ); 28 | console.info( 29 | "Please add the missing jobs to the needs section of all-tests-passed in test.yml.", 30 | ); 31 | process.exit(1); 32 | } 33 | console.log( 34 | "All jobs in test.yml are in the needs section of all-tests-passed.", 35 | ); 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | workflow_dispatch: 16 | push: 17 | branches: 18 | - main 19 | pull_request: 20 | # The branches below must be a subset of the branches above 21 | branches: 22 | - main 23 | 24 | jobs: 25 | analyze: 26 | name: Analyze 27 | runs-on: ubuntu-latest 28 | permissions: 29 | actions: read 30 | contents: read 31 | security-events: write 32 | 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | language: ["TypeScript"] 37 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 38 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | source-root: src 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@v3 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 https://git.io/JvXDl 62 | 63 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 64 | # and modify them (or add more) to build your code if your project 65 | # uses a compiled language 66 | 67 | #- run: | 68 | # make bootstrap 69 | # make release 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release Drafter 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | update_release_draft: 13 | name: ✏️ Draft release 14 | runs-on: ubuntu-24.04-arm 15 | permissions: 16 | contents: write 17 | pull-requests: read 18 | steps: 19 | - name: 🚀 Run Release Drafter 20 | uses: release-drafter/release-drafter@v6.1.0 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "test" 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | lint: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Actionlint 24 | uses: eifinger/actionlint-action@23c85443d840cd73bbecb9cddfc933cc21649a38 # v1.9.1 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: "20" 28 | - run: | 29 | npm install 30 | - run: | 31 | npm run all 32 | - name: Check all jobs are in all-tests-passed.needs 33 | run: | 34 | tsc check-all-tests-passed-needs.ts 35 | node check-all-tests-passed-needs.js 36 | working-directory: .github/scripts 37 | - name: Make sure no changes from linters are detected 38 | run: | 39 | git diff --exit-code || (echo "::error::Please run 'npm run all' to fix the issues" && exit 1) 40 | 41 | test-default-version: 42 | runs-on: ${{ matrix.os }} 43 | strategy: 44 | matrix: 45 | os: [ubuntu-latest, macos-latest, macos-14, windows-latest] 46 | steps: 47 | - uses: actions/checkout@v4 48 | - name: Install latest version 49 | id: setup-uv 50 | uses: ./ 51 | - run: uv sync 52 | working-directory: __tests__/fixtures/uv-project 53 | shell: bash 54 | - name: Check uv-path is set 55 | run: ${{ steps.setup-uv.outputs.uv-path }} --version 56 | - name: Check uvx-path is set 57 | run: ${{ steps.setup-uv.outputs.uvx-path }} --version 58 | 59 | test-specific-version: 60 | runs-on: ubuntu-latest 61 | strategy: 62 | matrix: 63 | uv-version: ["0.3.0", "0.3.2", "0.3", "0.3.x", ">=0.3.0"] 64 | steps: 65 | - uses: actions/checkout@v4 66 | - name: Install version ${{ matrix.uv-version }} 67 | uses: ./ 68 | with: 69 | version: ${{ matrix.uv-version }} 70 | - run: uv sync 71 | working-directory: __tests__/fixtures/uv-project 72 | 73 | test-semver-range: 74 | strategy: 75 | matrix: 76 | os: [ ubuntu-latest, selfhosted-ubuntu-arm64 ] 77 | runs-on: ${{ matrix.os }} 78 | steps: 79 | - uses: actions/checkout@v4 80 | - name: Install version 0.3 81 | id: setup-uv 82 | uses: ./ 83 | with: 84 | version: "0.3" 85 | - name: Correct version gets installed 86 | run: | 87 | if [ "$(uv --version)" != "uv 0.3.5" ]; then 88 | echo "Wrong uv version: $(uv --version)" 89 | exit 1 90 | fi 91 | - name: Output has correct version 92 | run: | 93 | if [ "$UV_VERSION" != "0.3.5" ]; then 94 | exit 1 95 | fi 96 | env: 97 | UV_VERSION: ${{ steps.setup-uv.outputs.uv-version }} 98 | 99 | test-pep440-version: 100 | runs-on: ubuntu-latest 101 | steps: 102 | - uses: actions/checkout@v4 103 | - name: Install version 0.4.30 104 | id: setup-uv 105 | uses: ./ 106 | with: 107 | version: ">=0.4.25,<0.5" 108 | - name: Correct version gets installed 109 | run: | 110 | if [ "$(uv --version)" != "uv 0.4.30" ]; then 111 | echo "Wrong uv version: $(uv --version)" 112 | exit 1 113 | fi 114 | 115 | test-pyproject-file-version: 116 | runs-on: ubuntu-latest 117 | steps: 118 | - uses: actions/checkout@v4 119 | - name: Install version 0.5.14 120 | id: setup-uv 121 | uses: ./ 122 | with: 123 | working-directory: "__tests__/fixtures/pyproject-toml-project" 124 | - name: Correct version gets installed 125 | run: | 126 | if [ "$(uv --version)" != "uv 0.5.14" ]; then 127 | echo "Wrong uv version: $(uv --version)" 128 | exit 1 129 | fi 130 | 131 | test-malformed-pyproject-file-fallback: 132 | runs-on: ubuntu-latest 133 | steps: 134 | - uses: actions/checkout@v4 135 | - name: Install using malformed pyproject.toml 136 | id: setup-uv 137 | uses: ./ 138 | with: 139 | working-directory: "__tests__/fixtures/malformed-pyproject-toml-project" 140 | - run: uv --help 141 | 142 | test-uv-file-version: 143 | runs-on: ubuntu-latest 144 | steps: 145 | - uses: actions/checkout@v4 146 | - name: Install version 0.5.15 147 | id: setup-uv 148 | uses: ./ 149 | with: 150 | working-directory: "__tests__/fixtures/uv-toml-project" 151 | - name: Correct version gets installed 152 | run: | 153 | if [ "$(uv --version)" != "uv 0.5.15" ]; then 154 | echo "Wrong uv version: $(uv --version)" 155 | exit 1 156 | fi 157 | 158 | test-checksum: 159 | runs-on: ${{ matrix.inputs.os }} 160 | strategy: 161 | matrix: 162 | inputs: 163 | - os: ubuntu-latest 164 | checksum: "4d9279ad5ca596b1e2d703901d508430eb07564dc4d8837de9e2fca9c90f8ecd" 165 | - os: macos-latest 166 | checksum: "a70cbfbf3bb5c08b2f84963b4f12c94e08fbb2468ba418a3bfe1066fbe9e7218" 167 | steps: 168 | - uses: actions/checkout@v4 169 | - name: Checksum matches expected 170 | uses: ./ 171 | with: 172 | version: "0.3.2" 173 | checksum: ${{ matrix.inputs.checksum }} 174 | - run: uv sync 175 | working-directory: __tests__/fixtures/uv-project 176 | 177 | test-with-explicit-token: 178 | runs-on: ubuntu-latest 179 | steps: 180 | - uses: actions/checkout@v4 181 | - name: Install default version 182 | uses: ./ 183 | with: 184 | github-token: ${{ secrets.GITHUB_TOKEN }} 185 | - run: uv sync 186 | working-directory: __tests__/fixtures/uv-project 187 | 188 | test-uvx: 189 | runs-on: ubuntu-latest 190 | steps: 191 | - uses: actions/checkout@v4 192 | - name: Install default version 193 | uses: ./ 194 | - run: uvx ruff --version 195 | 196 | test-tool-install: 197 | runs-on: ${{ matrix.os }} 198 | strategy: 199 | matrix: 200 | os: 201 | [ 202 | ubuntu-latest, 203 | macos-latest, 204 | macos-14, 205 | windows-latest, 206 | ] 207 | steps: 208 | - uses: actions/checkout@v4 209 | - name: Install default version 210 | uses: ./ 211 | - run: uv tool install ruff 212 | - run: ruff --version 213 | 214 | test-tilde-expansion-tool-dirs: 215 | runs-on: selfhosted-ubuntu-arm64 216 | steps: 217 | - uses: actions/checkout@v4 218 | - name: Setup with cache 219 | uses: ./ 220 | with: 221 | tool-bin-dir: "~/tool-bin-dir" 222 | tool-dir: "~/tool-dir" 223 | - name: "Check if tool dirs are expanded" 224 | run: | 225 | if ! echo "$PATH" | grep -q "/home/ubuntu/tool-bin-dir"; then 226 | echo "PATH does not contain /home/ubuntu/tool-bin-dir: $PATH" 227 | exit 1 228 | fi 229 | if [ "$UV_TOOL_DIR" != "/home/ubuntu/tool-dir" ]; then 230 | echo "UV_TOOL_DIR does not contain /home/ubuntu/tool-dir: $UV_TOOL_DIR" 231 | exit 1 232 | fi 233 | 234 | test-python-version: 235 | runs-on: ${{ matrix.os }} 236 | strategy: 237 | matrix: 238 | os: [ubuntu-latest, macos-latest, windows-latest] 239 | steps: 240 | - uses: actions/checkout@v4 241 | - name: Install latest version 242 | uses: ./ 243 | with: 244 | python-version: 3.13.1t 245 | - name: Verify UV_PYTHON is set to correct version 246 | run: | 247 | echo "$UV_PYTHON" 248 | if [ "$UV_PYTHON" != "3.13.1t" ]; then 249 | exit 1 250 | fi 251 | shell: bash 252 | - run: uv sync 253 | working-directory: __tests__/fixtures/uv-project 254 | 255 | test-activate-environment: 256 | runs-on: ${{ matrix.os }} 257 | strategy: 258 | matrix: 259 | os: [ ubuntu-latest, macos-latest, windows-latest ] 260 | steps: 261 | - uses: actions/checkout@v4 262 | - name: Install latest version 263 | uses: ./ 264 | with: 265 | python-version: 3.13.1t 266 | activate-environment: true 267 | - name: Verify packages can be installed 268 | run: uv pip install pip 269 | shell: bash 270 | - name: Verify python version is correct 271 | run: | 272 | python --version 273 | if [ "$(python --version)" != "Python 3.13.1" ]; then 274 | exit 1 275 | fi 276 | shell: bash 277 | 278 | test-musl: 279 | runs-on: ubuntu-latest 280 | container: alpine 281 | steps: 282 | - uses: actions/checkout@v4 283 | - name: Install latest version 284 | uses: ./ 285 | - run: uv sync 286 | working-directory: __tests__/fixtures/uv-project 287 | 288 | test-setup-cache: 289 | runs-on: ${{ matrix.os }} 290 | strategy: 291 | matrix: 292 | enable-cache: [ "true", "false", "auto" ] 293 | os: [ "ubuntu-latest", "selfhosted-ubuntu-arm64", "windows-latest" ] 294 | steps: 295 | - uses: actions/checkout@v4 296 | - name: Setup with cache 297 | uses: ./ 298 | with: 299 | enable-cache: ${{ matrix.enable-cache }} 300 | cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-${{ matrix.os }}-${{ matrix.enable-cache }} 301 | - run: uv sync 302 | working-directory: __tests__/fixtures/uv-project 303 | shell: bash 304 | test-restore-cache: 305 | runs-on: ${{ matrix.os }} 306 | strategy: 307 | matrix: 308 | enable-cache: [ "true", "false", "auto" ] 309 | os: [ "ubuntu-latest", "selfhosted-ubuntu-arm64", "windows-latest" ] 310 | needs: test-setup-cache 311 | steps: 312 | - uses: actions/checkout@v4 313 | - name: Restore with cache 314 | id: restore 315 | uses: ./ 316 | with: 317 | enable-cache: ${{ matrix.enable-cache }} 318 | cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-${{ matrix.os }}-${{ matrix.enable-cache }} 319 | - name: Cache was hit 320 | if: ${{ matrix.enable-cache == 'true' || (matrix.enable-cache == 'auto' && matrix.os == 'ubuntu-latest') }} 321 | run: | 322 | if [ "$CACHE_HIT" != "true" ]; then 323 | exit 1 324 | fi 325 | env: 326 | CACHE_HIT: ${{ steps.restore.outputs.cache-hit }} 327 | shell: bash 328 | - name: Cache was not hit 329 | if: ${{ matrix.enable-cache == 'false' || (matrix.enable-cache == 'auto' && matrix.os == 'selfhosted-ubuntu-arm64') }} 330 | run: | 331 | if [ "$CACHE_HIT" == "true" ]; then 332 | exit 1 333 | fi 334 | env: 335 | CACHE_HIT: ${{ steps.restore.outputs.cache-hit }} 336 | shell: bash 337 | - run: uv sync 338 | working-directory: __tests__/fixtures/uv-project 339 | shell: bash 340 | 341 | test-setup-cache-requirements-txt: 342 | runs-on: ubuntu-latest 343 | steps: 344 | - uses: actions/checkout@v4 345 | - name: Setup with cache 346 | uses: ./ 347 | with: 348 | enable-cache: true 349 | cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-requirements-txt 350 | - run: | 351 | uv venv 352 | uv pip install -r requirements.txt 353 | working-directory: __tests__/fixtures/requirements-txt-project 354 | test-restore-cache-requirements-txt: 355 | runs-on: ubuntu-latest 356 | needs: test-setup-cache 357 | steps: 358 | - uses: actions/checkout@v4 359 | - name: Restore with cache 360 | id: restore 361 | uses: ./ 362 | with: 363 | enable-cache: true 364 | cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-requirements-txt 365 | - name: Cache was hit 366 | run: | 367 | if [ "$CACHE_HIT" != "true" ]; then 368 | exit 1 369 | fi 370 | env: 371 | CACHE_HIT: ${{ steps.restore.outputs.cache-hit }} 372 | - run: | 373 | uv venv 374 | uv pip install -r requirements.txt 375 | working-directory: __tests__/fixtures/requirements-txt-project 376 | 377 | test-setup-cache-dependency-glob: 378 | runs-on: ubuntu-latest 379 | steps: 380 | - uses: actions/checkout@v4 381 | - name: Setup with cache 382 | uses: ./ 383 | with: 384 | enable-cache: true 385 | cache-dependency-glob: | 386 | __tests__/fixtures/uv-project/uv.lock 387 | **/pyproject.toml 388 | cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-dependency-glob 389 | - run: uv sync 390 | working-directory: __tests__/fixtures/uv-project 391 | test-restore-cache-dependency-glob: 392 | runs-on: ubuntu-latest 393 | needs: test-setup-cache-dependency-glob 394 | steps: 395 | - uses: actions/checkout@v4 396 | - name: Change pyproject.toml 397 | run: | 398 | echo '[tool.uv]' >> __tests__/fixtures/uv-project/pyproject.toml 399 | echo 'dev-dependencies = []' >> __tests__/fixtures/uv-project/pyproject.toml 400 | - name: Restore with cache 401 | id: restore 402 | uses: ./ 403 | with: 404 | enable-cache: true 405 | cache-dependency-glob: | 406 | __tests__/fixtures/uv-project/uv.lock 407 | **/pyproject.toml 408 | cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-dependency-glob 409 | ignore-nothing-to-cache: true 410 | - name: Cache was not hit 411 | run: | 412 | if [ "$CACHE_HIT" == "true" ]; then 413 | exit 1 414 | fi 415 | env: 416 | CACHE_HIT: ${{ steps.restore.outputs.cache-hit }} 417 | 418 | test-cache-local: 419 | strategy: 420 | matrix: 421 | inputs: 422 | - os: ubuntu-latest 423 | expected-cache-dir: "/home/runner/work/_temp/setup-uv-cache" 424 | - os: windows-latest 425 | expected-cache-dir: "D:\\a\\_temp\\setup-uv-cache" 426 | - os: selfhosted-ubuntu-arm64 427 | expected-cache-dir: "/home/ubuntu/.cache/uv" 428 | runs-on: ${{ matrix.inputs.os }} 429 | steps: 430 | - uses: actions/checkout@v4 431 | - name: Setup with cache 432 | uses: ./ 433 | with: 434 | cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-cache-local 435 | - run: | 436 | if [ "$UV_CACHE_DIR" != "${{ matrix.inputs.expected-cache-dir }}" ]; then 437 | echo "UV_CACHE_DIR is not set to the expected value: $UV_CACHE_DIR" 438 | exit 1 439 | fi 440 | shell: bash 441 | 442 | test-setup-cache-local: 443 | runs-on: selfhosted-ubuntu-arm64 444 | steps: 445 | - uses: actions/checkout@v4 446 | - name: Setup with cache 447 | uses: ./ 448 | with: 449 | enable-cache: true 450 | cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-local 451 | cache-local-path: /tmp/uv-cache 452 | - run: uv sync 453 | working-directory: __tests__/fixtures/uv-project 454 | test-restore-cache-local: 455 | runs-on: selfhosted-ubuntu-arm64 456 | needs: test-setup-cache-local 457 | steps: 458 | - uses: actions/checkout@v4 459 | - name: Restore with cache 460 | id: restore 461 | uses: ./ 462 | with: 463 | enable-cache: true 464 | cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-setup-cache-local 465 | cache-local-path: /tmp/uv-cache 466 | - name: Cache was hit 467 | run: | 468 | if [ "$CACHE_HIT" != "true" ]; then 469 | exit 1 470 | fi 471 | env: 472 | CACHE_HIT: ${{ steps.restore.outputs.cache-hit }} 473 | - run: uv sync 474 | working-directory: __tests__/fixtures/uv-project 475 | 476 | test-tilde-expansion-cache-local-path: 477 | runs-on: selfhosted-ubuntu-arm64 478 | steps: 479 | - uses: actions/checkout@v4 480 | - name: Create cache directory 481 | run: mkdir -p ~/uv-cache 482 | shell: bash 483 | - name: Setup with cache 484 | uses: ./ 485 | with: 486 | cache-local-path: ~/uv-cache/cache-local-path 487 | - run: uv sync 488 | working-directory: __tests__/fixtures/uv-project 489 | 490 | test-tilde-expansion-cache-dependency-glob: 491 | runs-on: selfhosted-ubuntu-arm64 492 | steps: 493 | - uses: actions/checkout@v4 494 | - name: Create cache directory 495 | run: mkdir -p ~/uv-cache 496 | shell: bash 497 | - name: Create cache dependency glob file 498 | run: touch ~/uv-cache.glob 499 | shell: bash 500 | - name: Setup with cache 501 | uses: ./ 502 | with: 503 | enable-cache: true 504 | cache-local-path: ~/uv-cache/cache-dependency-glob 505 | cache-dependency-glob: "~/uv-cache.glob" 506 | - run: uv sync 507 | working-directory: __tests__/fixtures/uv-project 508 | 509 | cleanup-tilde-expansion-tests: 510 | needs: 511 | - test-tilde-expansion-cache-local-path 512 | - test-tilde-expansion-cache-dependency-glob 513 | if: always() 514 | runs-on: selfhosted-ubuntu-arm64 515 | steps: 516 | - name: Remove cache directory 517 | run: rm -rf ~/uv-cache 518 | shell: bash 519 | - name: Remove cache dependency glob file 520 | run: rm -f ~/uv-cache.glob 521 | shell: bash 522 | 523 | test-no-python-version: 524 | runs-on: ubuntu-latest 525 | steps: 526 | - uses: actions/checkout@v4 527 | - name: Fake pyproject.toml at root 528 | run: cp __tests__/fixtures/old-python-constraint-project/pyproject.toml pyproject.toml 529 | - name: Setup with cache 530 | uses: ./ 531 | with: 532 | enable-cache: true 533 | - run: uv sync 534 | working-directory: __tests__/fixtures/old-python-constraint-project 535 | 536 | all-tests-passed: 537 | runs-on: ubuntu-latest 538 | needs: 539 | - lint 540 | - test-default-version 541 | - test-specific-version 542 | - test-semver-range 543 | - test-pep440-version 544 | - test-pyproject-file-version 545 | - test-malformed-pyproject-file-fallback 546 | - test-uv-file-version 547 | - test-checksum 548 | - test-with-explicit-token 549 | - test-uvx 550 | - test-tool-install 551 | - test-tilde-expansion-tool-dirs 552 | - test-python-version 553 | - test-activate-environment 554 | - test-musl 555 | - test-cache-local 556 | - test-setup-cache 557 | - test-restore-cache 558 | - test-setup-cache-requirements-txt 559 | - test-restore-cache-requirements-txt 560 | - test-setup-cache-dependency-glob 561 | - test-restore-cache-dependency-glob 562 | - test-setup-cache-local 563 | - test-restore-cache-local 564 | - test-tilde-expansion-cache-local-path 565 | - test-tilde-expansion-cache-dependency-glob 566 | - cleanup-tilde-expansion-tests 567 | - test-no-python-version 568 | if: always() 569 | steps: 570 | - name: All tests passed 571 | run: | 572 | echo "All jobs passed: ${{ !contains(needs.*.result, 'failure') }}" 573 | # shellcheck disable=SC2242 574 | exit ${{ contains(needs.*.result, 'failure') && 1 || 0 }} 575 | -------------------------------------------------------------------------------- /.github/workflows/update-known-versions.yml: -------------------------------------------------------------------------------- 1 | name: "Update known versions" 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 4 * * *" # Run every day at 4am UTC 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-24.04-arm 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: "20" 18 | - name: Update known versions 19 | id: update-known-versions 20 | run: 21 | node dist/update-known-versions/index.js 22 | src/download/checksum/known-checksums.ts 23 | version-manifest.json 24 | ${{ secrets.GITHUB_TOKEN }} 25 | - run: npm install && npm run all 26 | - name: Create Pull Request 27 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 28 | with: 29 | commit-message: "chore: update known versions" 30 | title: 31 | "chore: update known versions for ${{ 32 | steps.update-known-versions.outputs.latest-version }}" 33 | body: 34 | "chore: update known versions for ${{ 35 | steps.update-known-versions.outputs.latest-version }}" 36 | base: main 37 | labels: "automated-pr,update-known-versions" 38 | branch: update-known-versions-pr 39 | delete-branch: true 40 | -------------------------------------------------------------------------------- /.github/workflows/update-major-minor-tags.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Update Major Minor Tags 3 | 4 | on: 5 | push: 6 | branches-ignore: 7 | - "**" 8 | tags: 9 | - "v*.*.*" 10 | 11 | jobs: 12 | update_major_minor_tags: 13 | name: Make sure major and minor tags are up to date on a patch release 14 | runs-on: ubuntu-24.04-arm 15 | permissions: 16 | contents: write 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Update Major Minor Tags 20 | run: | 21 | set -x 22 | 23 | cd "${GITHUB_WORKSPACE}" || exit 24 | 25 | # Set up variables. 26 | TAG="${GITHUB_REF#refs/tags/}" # v1.2.3 27 | MINOR="${TAG%.*}" # v1.2 28 | MAJOR="${MINOR%.*}" # v1 29 | 30 | if [ "${GITHUB_REF}" = "${TAG}" ]; then 31 | echo "This workflow is not triggered by tag push: GITHUB_REF=${GITHUB_REF}" 32 | exit 1 33 | fi 34 | 35 | MESSAGE="Release ${TAG}" 36 | 37 | # Set up Git. 38 | git config user.name "${GITHUB_ACTOR}" 39 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 40 | 41 | # Update MAJOR/MINOR tag 42 | git tag -fa "${MAJOR}" -m "${MESSAGE}" 43 | git tag -fa "${MINOR}" -m "${MESSAGE}" 44 | 45 | # Push 46 | git push --force origin "${MINOR}" 47 | git push --force origin "${MAJOR}" 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | # OS metadata 94 | .DS_Store 95 | Thumbs.db 96 | 97 | # Ignore built ts files 98 | __tests__/runner/* 99 | lib/**/* 100 | 101 | # Idea IDEs (PyCharm, WebStorm, IntelliJ, etc) 102 | .idea/ 103 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @eifinger 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Kevin Stillhammer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # setup-uv 2 | 3 | Set up your GitHub Actions workflow with a specific version of [uv](https://docs.astral.sh/uv/). 4 | 5 | - Install a version of uv and add it to PATH 6 | - Cache the installed version of uv to speed up consecutive runs on self-hosted runners 7 | - Register problem matchers for error output 8 | - (Optional) Persist the uv's cache in the GitHub Actions Cache 9 | - (Optional) Verify the checksum of the downloaded uv executable 10 | 11 | ## Contents 12 | 13 | - [Usage](#usage) 14 | - [Install a required-version or latest (default)](#install-a-required-version-or-latest-default) 15 | - [Install the latest version](#install-the-latest-version) 16 | - [Install a specific version](#install-a-specific-version) 17 | - [Install a version by supplying a semver range or pep440 specifier](#install-a-version-by-supplying-a-semver-range-or-pep440-specifier) 18 | - [Python version](#python-version) 19 | - [Activate environment](#activate-environment) 20 | - [Working directory](#working-directory) 21 | - [Validate checksum](#validate-checksum) 22 | - [Enable Caching](#enable-caching) 23 | - [Cache dependency glob](#cache-dependency-glob) 24 | - [Local cache path](#local-cache-path) 25 | - [Disable cache pruning](#disable-cache-pruning) 26 | - [Ignore nothing to cache](#ignore-nothing-to-cache) 27 | - [GitHub authentication token](#github-authentication-token) 28 | - [UV_TOOL_DIR](#uv_tool_dir) 29 | - [UV_TOOL_BIN_DIR](#uv_tool_bin_dir) 30 | - [Tilde Expansion](#tilde-expansion) 31 | - [How it works](#how-it-works) 32 | - [FAQ](#faq) 33 | 34 | ## Usage 35 | 36 | ### Install a required-version or latest (default) 37 | 38 | ```yaml 39 | - name: Install the latest version of uv 40 | uses: astral-sh/setup-uv@v6 41 | ``` 42 | 43 | If you do not specify a version, this action will look for a [required-version](https://docs.astral.sh/uv/reference/settings/#required-version) 44 | in a `uv.toml` or `pyproject.toml` file in the repository root. If none is found, the latest version will be installed. 45 | 46 | For an example workflow, see 47 | [here](https://github.com/charliermarsh/autobot/blob/e42c66659bf97b90ca9ff305a19cc99952d0d43f/.github/workflows/ci.yaml). 48 | 49 | ### Install the latest version 50 | 51 | ```yaml 52 | - name: Install the latest version of uv 53 | uses: astral-sh/setup-uv@v6 54 | with: 55 | version: "latest" 56 | ``` 57 | 58 | ### Install a specific version 59 | 60 | ```yaml 61 | - name: Install a specific version of uv 62 | uses: astral-sh/setup-uv@v6 63 | with: 64 | version: "0.4.4" 65 | ``` 66 | 67 | ### Install a version by supplying a semver range or pep440 specifier 68 | 69 | You can specify a [semver range](https://github.com/npm/node-semver?tab=readme-ov-file#ranges) 70 | or [pep440 specifier](https://peps.python.org/pep-0440/#version-specifiers) 71 | to install the latest version that satisfies the range. 72 | 73 | ```yaml 74 | - name: Install a semver range of uv 75 | uses: astral-sh/setup-uv@v6 76 | with: 77 | version: ">=0.4.0" 78 | ``` 79 | 80 | ```yaml 81 | - name: Pinning a minor version of uv 82 | uses: astral-sh/setup-uv@v6 83 | with: 84 | version: "0.4.x" 85 | ``` 86 | 87 | ```yaml 88 | - name: Install a pep440-specifier-satisfying version of uv 89 | uses: astral-sh/setup-uv@v6 90 | with: 91 | version: ">=0.4.25,<0.5" 92 | ``` 93 | 94 | ### Python version 95 | 96 | You can use the input `python-version` to set the environment variable `UV_PYTHON` for the rest of your workflow 97 | 98 | This will override any python version specifications in `pyproject.toml` and `.python-version` 99 | 100 | ```yaml 101 | - name: Install the latest version of uv and set the python version to 3.13t 102 | uses: astral-sh/setup-uv@v6 103 | with: 104 | python-version: 3.13t 105 | - run: uv pip install --python=3.13t pip 106 | ``` 107 | 108 | You can combine this with a matrix to test multiple python versions: 109 | 110 | ```yaml 111 | jobs: 112 | test: 113 | runs-on: ubuntu-latest 114 | strategy: 115 | matrix: 116 | python-version: ["3.9", "3.10", "3.11", "3.12"] 117 | steps: 118 | - uses: actions/checkout@v4 119 | - name: Install the latest version of uv and set the python version 120 | uses: astral-sh/setup-uv@v6 121 | with: 122 | python-version: ${{ matrix.python-version }} 123 | - name: Test with python ${{ matrix.python-version }} 124 | run: uv run --frozen pytest 125 | ``` 126 | 127 | ### Activate environment 128 | 129 | You can set `activate-environment` to `true` to automatically activate a venv. 130 | This allows directly using it in later steps: 131 | 132 | ```yaml 133 | - name: Install the latest version of uv and activate the environment 134 | uses: astral-sh/setup-uv@v6 135 | with: 136 | activate-environment: true 137 | - run: uv pip install pip 138 | ``` 139 | 140 | ### Working directory 141 | 142 | You can set the working directory with the `working-directory` input. 143 | This controls where we look for `pyproject.toml`, `uv.toml` and `.python-version` files 144 | which are used to determine the version of uv and python to install. 145 | 146 | It also controls where [the venv gets created](#activate-environment). 147 | 148 | ```yaml 149 | - name: Install uv based on the config files in the working-directory 150 | uses: astral-sh/setup-uv@v6 151 | with: 152 | working-directory: my/subproject/dir 153 | ``` 154 | 155 | ### Validate checksum 156 | 157 | You can specify a checksum to validate the downloaded executable. Checksums up to the default version 158 | are automatically verified by this action. The sha256 hashes can be found on the 159 | [releases page](https://github.com/astral-sh/uv/releases) of the uv repo. 160 | 161 | ```yaml 162 | - name: Install a specific version and validate the checksum 163 | uses: astral-sh/setup-uv@v6 164 | with: 165 | version: "0.3.1" 166 | checksum: "e11b01402ab645392c7ad6044db63d37e4fd1e745e015306993b07695ea5f9f8" 167 | ``` 168 | 169 | ### Enable caching 170 | 171 | If you enable caching, the [uv cache](https://docs.astral.sh/uv/concepts/cache/) will be uploaded to 172 | the GitHub Actions cache. This can speed up runs that reuse the cache by several minutes. 173 | Caching is enabled by default on GitHub-hosted runners. 174 | 175 | > [!TIP] 176 | > 177 | > On self-hosted runners this is usually not needed since the cache generated by uv on the runner's 178 | > filesystem is not removed after a run. For more details see [Local cache path](#local-cache-path). 179 | 180 | You can optionally define a custom cache key suffix. 181 | 182 | ```yaml 183 | - name: Enable caching and define a custom cache key suffix 184 | id: setup-uv 185 | uses: astral-sh/setup-uv@v6 186 | with: 187 | enable-cache: true 188 | cache-suffix: "optional-suffix" 189 | ``` 190 | 191 | When the cache was successfully restored, the output `cache-hit` will be set to `true` and you can 192 | use it in subsequent steps. For example, to use the cache in the above case: 193 | 194 | ```yaml 195 | - name: Do something if the cache was restored 196 | if: steps.setup-uv.outputs.cache-hit == 'true' 197 | run: echo "Cache was restored" 198 | ``` 199 | 200 | #### Cache dependency glob 201 | 202 | If you want to control when the GitHub Actions cache is invalidated, specify a glob pattern with the 203 | `cache-dependency-glob` input. The GitHub Actions cache will be invalidated if any file matching the glob pattern 204 | changes. If you use relative paths, they are relative to the repository root. 205 | 206 | > [!NOTE] 207 | > 208 | > You can look up supported patterns [here](https://github.com/actions/toolkit/tree/main/packages/glob#patterns) 209 | > 210 | > The default is 211 | > ```yaml 212 | > cache-dependency-glob: | 213 | > **/*requirements*.txt 214 | > **/*requirements*.in 215 | > **/*constraints*.txt 216 | > **/*constraints*.in 217 | > **/pyproject.toml 218 | > **/uv.lock 219 | > ``` 220 | 221 | ```yaml 222 | - name: Define a cache dependency glob 223 | uses: astral-sh/setup-uv@v6 224 | with: 225 | enable-cache: true 226 | cache-dependency-glob: "**/pyproject.toml" 227 | ``` 228 | 229 | ```yaml 230 | - name: Define a list of cache dependency globs 231 | uses: astral-sh/setup-uv@v6 232 | with: 233 | enable-cache: true 234 | cache-dependency-glob: | 235 | **/requirements*.txt 236 | **/pyproject.toml 237 | ``` 238 | 239 | ```yaml 240 | - name: Define an absolute cache dependency glob 241 | uses: astral-sh/setup-uv@v6 242 | with: 243 | enable-cache: true 244 | cache-dependency-glob: "/tmp/my-folder/requirements*.txt" 245 | ``` 246 | 247 | ```yaml 248 | - name: Never invalidate the cache 249 | uses: astral-sh/setup-uv@v6 250 | with: 251 | enable-cache: true 252 | cache-dependency-glob: "" 253 | ``` 254 | 255 | ### Local cache path 256 | 257 | This action controls where uv stores its cache on the runner's filesystem by setting `UV_CACHE_DIR`. 258 | It defaults to `setup-uv-cache` in the `TMP` dir, `D:\a\_temp\uv-tool-dir` on Windows and 259 | `/tmp/setup-uv-cache` on Linux/macOS. You can change the default by specifying the path with the 260 | `cache-local-path` input. 261 | 262 | ```yaml 263 | - name: Define a custom uv cache path 264 | uses: astral-sh/setup-uv@v6 265 | with: 266 | cache-local-path: "/path/to/cache" 267 | ``` 268 | 269 | ### Disable cache pruning 270 | 271 | By default, the uv cache is pruned after every run, removing pre-built wheels, but retaining any 272 | wheels that were built from source. On GitHub-hosted runners, it's typically faster to omit those 273 | pre-built wheels from the cache (and instead re-download them from the registry on each run). 274 | However, on self-hosted or local runners, preserving the cache may be more efficient. See 275 | the [documentation](https://docs.astral.sh/uv/concepts/cache/#caching-in-continuous-integration) for 276 | more information. 277 | 278 | If you want to persist the entire cache across runs, disable cache pruning with the `prune-cache` 279 | input. 280 | 281 | ```yaml 282 | - name: Don't prune the cache before saving it 283 | uses: astral-sh/setup-uv@v6 284 | with: 285 | enable-cache: true 286 | prune-cache: false 287 | ``` 288 | 289 | ### Ignore nothing to cache 290 | 291 | By default, the action will fail if caching is enabled but there is nothing to upload (the uv cache directory does not exist). 292 | If you want to ignore this, set the `ignore-nothing-to-cache` input to `true`. 293 | 294 | ```yaml 295 | - name: Ignore nothing to cache 296 | uses: astral-sh/setup-uv@v6 297 | with: 298 | enable-cache: true 299 | ignore-nothing-to-cache: true 300 | ``` 301 | 302 | ### Ignore empty workdir 303 | 304 | By default, the action will warn if the workdir is empty, because this is usually the case when 305 | `actions/checkout` is configured to run after `setup-uv`, which is not supported. 306 | 307 | If you want to ignore this, set the `ignore-empty-workdir` input to `true`. 308 | 309 | ```yaml 310 | - name: Ignore empty workdir 311 | uses: astral-sh/setup-uv@v6 312 | with: 313 | ignore-empty-workdir: true 314 | ``` 315 | 316 | ### GitHub authentication token 317 | 318 | This action uses the GitHub API to fetch the uv release artifacts. To avoid hitting the GitHub API 319 | rate limit too quickly, an authentication token can be provided via the `github-token` input. By 320 | default, the `GITHUB_TOKEN` secret is used, which is automatically provided by GitHub Actions. 321 | 322 | If the default 323 | [permissions for the GitHub token](https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) 324 | are not sufficient, you can provide a custom GitHub token with the necessary permissions. 325 | 326 | ```yaml 327 | - name: Install the latest version of uv with a custom GitHub token 328 | uses: astral-sh/setup-uv@v6 329 | with: 330 | github-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} 331 | ``` 332 | 333 | ### UV_TOOL_DIR 334 | 335 | On Windows `UV_TOOL_DIR` is set to `uv-tool-dir` in the `TMP` dir (e.g. `D:\a\_temp\uv-tool-dir`). 336 | On GitHub hosted runners this is on the much faster `D:` drive. 337 | 338 | On all other platforms the tool environments are placed in the 339 | [default location](https://docs.astral.sh/uv/concepts/tools/#tools-directory). 340 | 341 | If you want to change this behaviour (especially on self-hosted runners) you can use the `tool-dir` 342 | input: 343 | 344 | ```yaml 345 | - name: Install the latest version of uv with a custom tool dir 346 | uses: astral-sh/setup-uv@v6 347 | with: 348 | tool-dir: "/path/to/tool/dir" 349 | ``` 350 | 351 | ### UV_TOOL_BIN_DIR 352 | 353 | On Windows `UV_TOOL_BIN_DIR` is set to `uv-tool-bin-dir` in the `TMP` dir (e.g. 354 | `D:\a\_temp\uv-tool-bin-dir`). On GitHub hosted runners this is on the much faster `D:` drive. This 355 | path is also automatically added to the PATH. 356 | 357 | On all other platforms the tool binaries get installed to the 358 | [default location](https://docs.astral.sh/uv/concepts/tools/#the-bin-directory). 359 | 360 | If you want to change this behaviour (especially on self-hosted runners) you can use the 361 | `tool-bin-dir` input: 362 | 363 | ```yaml 364 | - name: Install the latest version of uv with a custom tool bin dir 365 | uses: astral-sh/setup-uv@v6 366 | with: 367 | tool-bin-dir: "/path/to/tool-bin/dir" 368 | ``` 369 | 370 | ### Tilde Expansion 371 | 372 | This action supports expanding the `~` character to the user's home directory for the following inputs: 373 | 374 | - `cache-local-path` 375 | - `tool-dir` 376 | - `tool-bin-dir` 377 | - `cache-dependency-glob` 378 | 379 | ```yaml 380 | - name: Expand the tilde character 381 | uses: astral-sh/setup-uv@v6 382 | with: 383 | cache-local-path: "~/path/to/cache" 384 | tool-dir: "~/path/to/tool/dir" 385 | tool-bin-dir: "~/path/to/tool-bin/dir" 386 | cache-dependency-glob: "~/my-cache-buster" 387 | ``` 388 | 389 | ## How it works 390 | 391 | This action downloads uv from the uv repo's official 392 | [GitHub Releases](https://github.com/astral-sh/uv) and uses the 393 | [GitHub Actions Toolkit](https://github.com/actions/toolkit) to cache it as a tool to speed up 394 | consecutive runs on self-hosted runners. 395 | 396 | The installed version of uv is then added to the runner PATH, enabling later steps to invoke it 397 | by name (`uv`). 398 | 399 | ## FAQ 400 | 401 | ### Do I still need `actions/setup-python` alongside `setup-uv`? 402 | 403 | With `setup-uv`, you can install a specific version of Python using `uv python install` rather than 404 | relying on `actions/setup-python`. 405 | 406 | Using `actions/setup-python` can be faster, because GitHub caches the Python versions alongside the runner. 407 | 408 | For example: 409 | 410 | ```yaml 411 | - name: Checkout the repository 412 | uses: actions/checkout@main 413 | - name: Install the latest version of uv 414 | uses: astral-sh/setup-uv@v6 415 | with: 416 | enable-cache: true 417 | - name: Test 418 | run: uv run --frozen pytest # Uses the Python version automatically installed by uv 419 | ``` 420 | 421 | To install a specific version of Python, use 422 | [`uv python install`](https://docs.astral.sh/uv/guides/install-python/): 423 | 424 | ```yaml 425 | - name: Install the latest version of uv 426 | uses: astral-sh/setup-uv@v6 427 | with: 428 | enable-cache: true 429 | - name: Install Python 3.12 430 | run: uv python install 3.12 431 | ``` 432 | 433 | ### What is the default version? 434 | 435 | By default, this action installs the latest version of uv. 436 | 437 | If you require the installed version in subsequent steps of your workflow, use the `uv-version` 438 | output: 439 | 440 | ```yaml 441 | - name: Checkout the repository 442 | uses: actions/checkout@main 443 | - name: Install the default version of uv 444 | id: setup-uv 445 | uses: astral-sh/setup-uv@v6 446 | - name: Print the installed version 447 | run: echo "Installed uv version is ${{ steps.setup-uv.outputs.uv-version }}" 448 | ``` 449 | 450 | ### Should I include the resolution strategy in the cache key? 451 | 452 | **Yes!** 453 | 454 | The cache key gets computed by using the [cache-dependency-glob](#cache-dependency-glob). 455 | 456 | If you 457 | have jobs which use the same dependency definitions from `requirements.txt` or 458 | `pyproject.toml` but different 459 | [resolution strategies](https://docs.astral.sh/uv/concepts/resolution/#resolution-strategy), 460 | each job will have different dependencies or dependency versions. 461 | But if you do not add the resolution strategy as a [cache-suffix](#enable-caching), 462 | they will have the same cache key. 463 | 464 | This means the first job which starts uploading its cache will win and all other job will fail 465 | uploading the cache, 466 | because they try to upload with the same cache key. 467 | 468 | You might see errors like 469 | `Failed to save: Failed to CreateCacheEntry: Received non-retryable error: Failed request: (409) Conflict: cache entry with the same key, version, and scope already exists` 470 | 471 | ### Why do I see warnings like `No GitHub Actions cache found for key` 472 | 473 | When a workflow runs for the first time on a branch and has a new cache key, because the 474 | [cache-dependency-glob](#cache-dependency-glob) found changed files (changed dependencies), 475 | the cache will not be found and the warning `No GitHub Actions cache found for key` will be printed. 476 | 477 | While this might be irritating at first, it is expected behaviour and the cache will be created 478 | and reused in later workflows. 479 | 480 | The reason for the warning is, that we have to way to know if this is the first run of a new 481 | cache key or the user accidentally misconfigured the [cache-dependency-glob](#cache-dependency-glob) 482 | or [cache-suffix](#enable-caching) and the cache never gets used. 483 | 484 | ### Do I have to run `actions/checkout` before or after `setup-uv`? 485 | 486 | Some workflows need uv but do not need to access the repository content. 487 | 488 | But **if** you need to access the repository content, you have run `actions/checkout` before running `setup-uv`. 489 | Running `actions/checkout` after `setup-uv` **is not supported**. 490 | 491 | ### Does `setup-uv` also install my project or its dependencies automatically? 492 | 493 | No, `setup-uv` alone wont install any libraries from your `pyproject.toml` or `requirements.txt`, it only sets up `uv`. 494 | You should run `uv sync` or `uv pip install .` separately, or use `uv run ...` to ensure necessary dependencies are installed. 495 | 496 | ## Acknowledgements 497 | 498 | `setup-uv` was initially written and published by [Kevin Stillhammer](https://github.com/eifinger) 499 | before moving under the official [Astral](https://github.com/astral-sh) GitHub organization. You can 500 | support Kevin's work in open source on [Buy me a coffee](https://www.buymeacoffee.com/eifinger) or 501 | [PayPal](https://paypal.me/kevinstillhammer). 502 | 503 | ## License 504 | 505 | MIT 506 | 507 |
508 | 509 | Made by Astral 510 | 511 |
512 | -------------------------------------------------------------------------------- /__tests__/download/checksum/checksum.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test, it } from "@jest/globals"; 2 | import { 3 | isknownVersion, 4 | validateChecksum, 5 | } from "../../../src/download/checksum/checksum"; 6 | 7 | test("checksum should match", async () => { 8 | const validChecksum = 9 | "f3da96ec7e995debee7f5d52ecd034dfb7074309a1da42f76429ecb814d813a3"; 10 | const filePath = "__tests__/fixtures/checksumfile"; 11 | // string params don't matter only test the checksum mechanism, not known checksums 12 | await validateChecksum( 13 | validChecksum, 14 | filePath, 15 | "aarch64", 16 | "pc-windows-msvc", 17 | "1.2.3", 18 | ); 19 | }); 20 | 21 | type KnownVersionFixture = { version: string; known: boolean }; 22 | 23 | it.each([ 24 | { 25 | version: "0.3.0", 26 | known: true, 27 | }, 28 | { 29 | version: "0.0.15", 30 | known: false, 31 | }, 32 | ])( 33 | "isknownVersion should return $known for version $version", 34 | ({ version, known }) => { 35 | expect(isknownVersion(version)).toBe(known); 36 | }, 37 | ); 38 | -------------------------------------------------------------------------------- /__tests__/fixtures/checksumfile: -------------------------------------------------------------------------------- 1 | Random file content -------------------------------------------------------------------------------- /__tests__/fixtures/malformed-pyproject-toml-project/.python-version: -------------------------------------------------------------------------------- 1 | 3.11 2 | -------------------------------------------------------------------------------- /__tests__/fixtures/malformed-pyproject-toml-project/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astral-sh/setup-uv/d44461ea9f6b6d77b25a3b196dc2cdb60b5d29eb/__tests__/fixtures/malformed-pyproject-toml-project/README.md -------------------------------------------------------------------------------- /__tests__/fixtures/malformed-pyproject-toml-project/hello.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | print("Hello from malformed-pyproject-toml-project!") 3 | 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /__tests__/fixtures/malformed-pyproject-toml-project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "malformed-pyproject-toml-project" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [] 8 | 9 | [malformed-toml 10 | -------------------------------------------------------------------------------- /__tests__/fixtures/old-python-constraint-project/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astral-sh/setup-uv/d44461ea9f6b6d77b25a3b196dc2cdb60b5d29eb/__tests__/fixtures/old-python-constraint-project/README.md -------------------------------------------------------------------------------- /__tests__/fixtures/old-python-constraint-project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "old-python-constraint-project" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.8,<=3.9" 7 | dependencies = [ 8 | "ruff>=0.6.2", 9 | ] 10 | 11 | [build-system] 12 | requires = ["hatchling"] 13 | build-backend = "hatchling.build" 14 | -------------------------------------------------------------------------------- /__tests__/fixtures/old-python-constraint-project/src/old_python_constraint_project/__init__.py: -------------------------------------------------------------------------------- 1 | def hello() -> str: 2 | return "Hello from uv-project!" 3 | -------------------------------------------------------------------------------- /__tests__/fixtures/old-python-constraint-project/uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.12" 3 | 4 | [[package]] 5 | name = "ruff" 6 | version = "0.6.2" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/23/f4/279d044f66b79261fd37df76bf72b64471afab5d3b7906a01499c4451910/ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be", size = 2460281 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/72/4b/47dd7a69287afb4069fa42c198e899463605460a58120196711bfcf0446b/ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c", size = 9695871 }, 11 | { 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 }, 12 | { 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 }, 13 | { 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 }, 14 | { 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 }, 15 | { 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 }, 16 | { 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 }, 17 | { 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 }, 18 | { 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 }, 19 | { 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 }, 20 | { 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 }, 21 | { 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 }, 22 | { 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 }, 23 | { 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 }, 24 | { url = "https://files.pythonhosted.org/packages/79/a9/0f2f21fe15ba537c46598f96aa9ae4a3d4b9ec64926664617ca6a8c772f4/ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da", size = 7961901 }, 25 | { url = "https://files.pythonhosted.org/packages/b0/80/fff12ffe11853d9f4ea3e5221e6dd2e93640a161c05c9579833e09ad40a7/ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2", size = 8783320 }, 26 | { url = "https://files.pythonhosted.org/packages/56/91/577cdd64cce5e74d3f8b5ecb93f29566def569c741eb008aed4f331ef821/ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9", size = 8225886 }, 27 | ] 28 | 29 | [[package]] 30 | name = "uv-project" 31 | version = "0.1.0" 32 | source = { editable = "." } 33 | dependencies = [ 34 | { name = "ruff" }, 35 | ] 36 | 37 | [package.metadata] 38 | requires-dist = [{ name = "ruff" }] 39 | -------------------------------------------------------------------------------- /__tests__/fixtures/pyproject-toml-project/.python-version: -------------------------------------------------------------------------------- 1 | 3.11 2 | -------------------------------------------------------------------------------- /__tests__/fixtures/pyproject-toml-project/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astral-sh/setup-uv/d44461ea9f6b6d77b25a3b196dc2cdb60b5d29eb/__tests__/fixtures/pyproject-toml-project/README.md -------------------------------------------------------------------------------- /__tests__/fixtures/pyproject-toml-project/hello.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | print("Hello from pyproject-toml-project!") 3 | 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /__tests__/fixtures/pyproject-toml-project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pyproject-toml-project" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [] 8 | 9 | [dependency-groups] 10 | dev = [ 11 | "reuse==5.0.2", 12 | {include-group = "lint"}, 13 | ] 14 | lint = [ 15 | "flake8==4.0.1", 16 | ] 17 | 18 | [tool.uv] 19 | required-version = "==0.5.14" 20 | -------------------------------------------------------------------------------- /__tests__/fixtures/requirements-txt-project/hello_world.py: -------------------------------------------------------------------------------- 1 | print("Hello world") 2 | -------------------------------------------------------------------------------- /__tests__/fixtures/requirements-txt-project/requirements.txt: -------------------------------------------------------------------------------- 1 | ruff>=0.6.2 2 | -------------------------------------------------------------------------------- /__tests__/fixtures/uv-project/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astral-sh/setup-uv/d44461ea9f6b6d77b25a3b196dc2cdb60b5d29eb/__tests__/fixtures/uv-project/README.md -------------------------------------------------------------------------------- /__tests__/fixtures/uv-project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "uv-project" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "ruff>=0.6.2", 9 | ] 10 | 11 | [build-system] 12 | requires = ["hatchling"] 13 | build-backend = "hatchling.build" 14 | -------------------------------------------------------------------------------- /__tests__/fixtures/uv-project/src/uv_project/__init__.py: -------------------------------------------------------------------------------- 1 | def hello() -> str: 2 | return "Hello from uv-project!" 3 | -------------------------------------------------------------------------------- /__tests__/fixtures/uv-project/uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.12" 3 | 4 | [[package]] 5 | name = "ruff" 6 | version = "0.6.2" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/23/f4/279d044f66b79261fd37df76bf72b64471afab5d3b7906a01499c4451910/ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be", size = 2460281 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/72/4b/47dd7a69287afb4069fa42c198e899463605460a58120196711bfcf0446b/ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c", size = 9695871 }, 11 | { 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 }, 12 | { 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 }, 13 | { 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 }, 14 | { 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 }, 15 | { 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 }, 16 | { 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 }, 17 | { 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 }, 18 | { 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 }, 19 | { 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 }, 20 | { 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 }, 21 | { 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 }, 22 | { 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 }, 23 | { 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 }, 24 | { url = "https://files.pythonhosted.org/packages/79/a9/0f2f21fe15ba537c46598f96aa9ae4a3d4b9ec64926664617ca6a8c772f4/ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da", size = 7961901 }, 25 | { url = "https://files.pythonhosted.org/packages/b0/80/fff12ffe11853d9f4ea3e5221e6dd2e93640a161c05c9579833e09ad40a7/ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2", size = 8783320 }, 26 | { url = "https://files.pythonhosted.org/packages/56/91/577cdd64cce5e74d3f8b5ecb93f29566def569c741eb008aed4f331ef821/ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9", size = 8225886 }, 27 | ] 28 | 29 | [[package]] 30 | name = "uv-project" 31 | version = "0.1.0" 32 | source = { editable = "." } 33 | dependencies = [ 34 | { name = "ruff" }, 35 | ] 36 | 37 | [package.metadata] 38 | requires-dist = [{ name = "ruff" }] 39 | -------------------------------------------------------------------------------- /__tests__/fixtures/uv-toml-project/.python-version: -------------------------------------------------------------------------------- 1 | 3.11 2 | -------------------------------------------------------------------------------- /__tests__/fixtures/uv-toml-project/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astral-sh/setup-uv/d44461ea9f6b6d77b25a3b196dc2cdb60b5d29eb/__tests__/fixtures/uv-toml-project/README.md -------------------------------------------------------------------------------- /__tests__/fixtures/uv-toml-project/hello.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | print("Hello from uv-toml-project!") 3 | 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /__tests__/fixtures/uv-toml-project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "uv-toml-project" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [] 8 | 9 | [tool.uv] 10 | required-version = "==0.5.14" 11 | -------------------------------------------------------------------------------- /__tests__/fixtures/uv-toml-project/uv.toml: -------------------------------------------------------------------------------- 1 | required-version = "==0.5.15" 2 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "astral-sh/setup-uv" 2 | description: 3 | "Set up your GitHub Actions workflow with a specific version of uv." 4 | author: "astral-sh" 5 | inputs: 6 | version: 7 | description: "The version of uv to install e.g., `0.5.0` Defaults to the version in pyproject.toml or 'latest'." 8 | default: "" 9 | python-version: 10 | description: "The version of Python to set UV_PYTHON to" 11 | required: false 12 | activate-environment: 13 | description: "Use uv venv to activate a venv ready to be used by later steps. " 14 | default: "false" 15 | working-directory: 16 | description: "The directory to execute all commands in and look for files such as pyproject.toml" 17 | default: ${{ github.workspace }} 18 | checksum: 19 | description: "The checksum of the uv version to install" 20 | required: false 21 | server-url: 22 | description: "The server url to use when downloading uv" 23 | required: false 24 | default: "https://github.com" 25 | github-token: 26 | description: 27 | "Used to increase the rate limit when retrieving versions and downloading uv." 28 | required: false 29 | default: ${{ github.token }} 30 | enable-cache: 31 | description: "Enable uploading of the uv cache" 32 | default: "auto" 33 | cache-dependency-glob: 34 | description: 35 | "Glob pattern to match files relative to the repository root to control 36 | the cache." 37 | default: | 38 | **/*requirements*.txt 39 | **/*requirements*.in 40 | **/*constraints*.txt 41 | **/*constraints*.in 42 | **/pyproject.toml 43 | **/uv.lock 44 | cache-suffix: 45 | description: "Suffix for the cache key" 46 | required: false 47 | cache-local-path: 48 | description: "Local path to store the cache." 49 | default: "" 50 | prune-cache: 51 | description: "Prune cache before saving." 52 | default: "true" 53 | ignore-nothing-to-cache: 54 | description: "Ignore when nothing is found to cache." 55 | default: "false" 56 | ignore-empty-workdir: 57 | description: "Ignore when the working directory is empty." 58 | default: "false" 59 | tool-dir: 60 | description: "Custom path to set UV_TOOL_DIR to." 61 | required: false 62 | tool-bin-dir: 63 | description: "Custom path to set UV_TOOL_BIN_DIR to." 64 | required: false 65 | outputs: 66 | uv-version: 67 | description: "The installed uv version. Useful when using latest." 68 | uv-path: 69 | description: "The path to the installed uv binary." 70 | uvx-path: 71 | description: "The path to the installed uvx binary." 72 | cache-hit: 73 | description: "A boolean value to indicate a cache entry was found" 74 | runs: 75 | using: "node20" 76 | main: "dist/setup/index.js" 77 | post: "dist/save-cache/index.js" 78 | post-if: success() 79 | branding: 80 | icon: "package" 81 | color: "black" 82 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": ["dist", "lib", "node_modules"] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "space" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true 23 | } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "quoteStyle": "double", 28 | "trailingCommas": "all" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ["js", "ts"], 4 | testMatch: ["**/*.test.ts"], 5 | transform: { 6 | "^.+\\.ts$": "ts-jest", 7 | }, 8 | verbose: true, 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setup-uv", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Set up your GitHub Actions workflow with a specific version of uv", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "build": "tsc", 9 | "format": "biome format --fix", 10 | "format-check": "biome format", 11 | "lint": "biome lint --fix", 12 | "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-versions src/update-known-versions.ts", 13 | "test": "jest", 14 | "act": "act pull_request -W .github/workflows/test.yml --container-architecture linux/amd64 -s GITHUB_TOKEN=\"$(gh auth token)\"", 15 | "update-known-versions": "RUNNER_TEMP=known_versions node dist/update-known-versions/index.js src/download/checksum/known-versions.ts \"$(gh auth token)\"", 16 | "all": "npm run build && npm run format && npm run lint && npm run package && npm test" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/astral-sh/setup-uv.git" 21 | }, 22 | "keywords": ["actions", "python", "setup", "uv"], 23 | "author": "@eifinger", 24 | "license": "MIT", 25 | "dependencies": { 26 | "@actions/cache": "^4.0.3", 27 | "@actions/core": "^1.11.1", 28 | "@actions/exec": "^1.1.1", 29 | "@actions/glob": "^0.5.0", 30 | "@actions/io": "^1.1.3", 31 | "@actions/tool-cache": "^2.0.2", 32 | "@octokit/core": "^7.0.2", 33 | "@octokit/plugin-paginate-rest": "^12.0.0", 34 | "@octokit/plugin-rest-endpoint-methods": "^14.0.0", 35 | "@renovatebot/pep440": "^4.1.0", 36 | "smol-toml": "^1.3.4", 37 | "undici": "^7.10.0" 38 | }, 39 | "devDependencies": { 40 | "@biomejs/biome": "1.9.4", 41 | "@types/js-yaml": "^4.0.9", 42 | "@types/node": "^22.15.21", 43 | "@types/semver": "^7.7.0", 44 | "@vercel/ncc": "^0.38.3", 45 | "jest": "^29.7.0", 46 | "js-yaml": "^4.1.0", 47 | "ts-jest": "^29.3.2", 48 | "typescript": "^5.8.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/cache/restore-cache.ts: -------------------------------------------------------------------------------- 1 | import * as cache from "@actions/cache"; 2 | import * as core from "@actions/core"; 3 | import { 4 | cacheDependencyGlob, 5 | cacheLocalPath, 6 | cacheSuffix, 7 | pruneCache, 8 | pythonVersion as pythonVersionInput, 9 | workingDirectory, 10 | } from "../utils/inputs"; 11 | import { getArch, getPlatform } from "../utils/platforms"; 12 | import { hashFiles } from "../hash/hash-files"; 13 | import * as exec from "@actions/exec"; 14 | 15 | export const STATE_CACHE_KEY = "cache-key"; 16 | export const STATE_CACHE_MATCHED_KEY = "cache-matched-key"; 17 | const CACHE_VERSION = "1"; 18 | 19 | export async function restoreCache(): Promise { 20 | const cacheKey = await computeKeys(); 21 | 22 | let matchedKey: string | undefined; 23 | core.info( 24 | `Trying to restore uv cache from GitHub Actions cache with key: ${cacheKey}`, 25 | ); 26 | try { 27 | matchedKey = await cache.restoreCache([cacheLocalPath], cacheKey); 28 | } catch (err) { 29 | const message = (err as Error).message; 30 | core.warning(message); 31 | core.setOutput("cache-hit", false); 32 | return; 33 | } 34 | 35 | core.saveState(STATE_CACHE_KEY, cacheKey); 36 | 37 | handleMatchResult(matchedKey, cacheKey); 38 | } 39 | 40 | async function computeKeys(): Promise { 41 | let cacheDependencyPathHash = "-"; 42 | if (cacheDependencyGlob !== "") { 43 | core.info( 44 | `Searching files using cache dependency glob: ${cacheDependencyGlob.split("\n").join(",")}`, 45 | ); 46 | cacheDependencyPathHash += await hashFiles(cacheDependencyGlob, true); 47 | if (cacheDependencyPathHash === "-") { 48 | core.warning( 49 | `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.`, 50 | ); 51 | } 52 | } 53 | if (cacheDependencyPathHash === "-") { 54 | cacheDependencyPathHash = "-no-dependency-glob"; 55 | } 56 | const suffix = cacheSuffix ? `-${cacheSuffix}` : ""; 57 | const pythonVersion = await getPythonVersion(); 58 | const platform = await getPlatform(); 59 | const pruned = pruneCache ? "-pruned" : ""; 60 | return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform}-${pythonVersion}${pruned}${cacheDependencyPathHash}${suffix}`; 61 | } 62 | 63 | async function getPythonVersion(): Promise { 64 | if (pythonVersionInput !== "") { 65 | return pythonVersionInput; 66 | } 67 | 68 | let output = ""; 69 | const options: exec.ExecOptions = { 70 | silent: !core.isDebug(), 71 | listeners: { 72 | stdout: (data: Buffer) => { 73 | output += data.toString(); 74 | }, 75 | }, 76 | }; 77 | 78 | try { 79 | const execArgs = ["python", "find", "--directory", workingDirectory]; 80 | await exec.exec("uv", execArgs, options); 81 | const pythonPath = output.trim(); 82 | 83 | output = ""; 84 | await exec.exec(pythonPath, ["--version"], options); 85 | // output is like "Python 3.8.10" 86 | return output.split(" ")[1].trim(); 87 | } catch (error) { 88 | const err = error as Error; 89 | core.debug(`Failed to get python version from uv. Error: ${err.message}`); 90 | return "unknown"; 91 | } 92 | } 93 | 94 | function handleMatchResult( 95 | matchedKey: string | undefined, 96 | primaryKey: string, 97 | ): void { 98 | if (!matchedKey) { 99 | core.info(`No GitHub Actions cache found for key: ${primaryKey}`); 100 | core.setOutput("cache-hit", false); 101 | return; 102 | } 103 | 104 | core.saveState(STATE_CACHE_MATCHED_KEY, matchedKey); 105 | core.info( 106 | `uv cache restored from GitHub Actions cache with key: ${matchedKey}`, 107 | ); 108 | core.setOutput("cache-hit", true); 109 | } 110 | -------------------------------------------------------------------------------- /src/download/checksum/checksum.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import * as crypto from "node:crypto"; 3 | 4 | import * as core from "@actions/core"; 5 | import { KNOWN_CHECKSUMS } from "./known-checksums"; 6 | import type { Architecture, Platform } from "../../utils/platforms"; 7 | 8 | export async function validateChecksum( 9 | checkSum: string | undefined, 10 | downloadPath: string, 11 | arch: Architecture, 12 | platform: Platform, 13 | version: string, 14 | ): Promise { 15 | let isValid: boolean | undefined = undefined; 16 | if (checkSum !== undefined && checkSum !== "") { 17 | isValid = await validateFileCheckSum(downloadPath, checkSum); 18 | } else { 19 | core.debug("Checksum not provided. Checking known checksums."); 20 | const key = `${arch}-${platform}-${version}`; 21 | if (key in KNOWN_CHECKSUMS) { 22 | const knownChecksum = KNOWN_CHECKSUMS[`${arch}-${platform}-${version}`]; 23 | core.debug(`Checking checksum for ${arch}-${platform}-${version}.`); 24 | isValid = await validateFileCheckSum(downloadPath, knownChecksum); 25 | } else { 26 | core.debug(`No known checksum found for ${key}.`); 27 | } 28 | } 29 | 30 | if (isValid === false) { 31 | throw new Error(`Checksum for ${downloadPath} did not match ${checkSum}.`); 32 | } 33 | if (isValid === true) { 34 | core.debug(`Checksum for ${downloadPath} is valid.`); 35 | } 36 | } 37 | 38 | async function validateFileCheckSum( 39 | filePath: string, 40 | expected: string, 41 | ): Promise { 42 | return new Promise((resolve, reject) => { 43 | const hash = crypto.createHash("sha256"); 44 | const stream = fs.createReadStream(filePath); 45 | stream.on("error", (err) => reject(err)); 46 | stream.on("data", (chunk) => hash.update(chunk)); 47 | stream.on("end", () => { 48 | const actual = hash.digest("hex"); 49 | resolve(actual === expected); 50 | }); 51 | }); 52 | } 53 | 54 | export function isknownVersion(version: string): boolean { 55 | const pattern = new RegExp(`^.*-.*-${version}$`); 56 | return Object.keys(KNOWN_CHECKSUMS).some((key) => pattern.test(key)); 57 | } 58 | -------------------------------------------------------------------------------- /src/download/checksum/update-known-checksums.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "node:fs"; 2 | import * as tc from "@actions/tool-cache"; 3 | import { KNOWN_CHECKSUMS } from "./known-checksums"; 4 | export async function updateChecksums( 5 | filePath: string, 6 | downloadUrls: string[], 7 | ): Promise { 8 | await fs.rm(filePath); 9 | await fs.appendFile( 10 | filePath, 11 | "// AUTOGENERATED_DO_NOT_EDIT\nexport const KNOWN_CHECKSUMS: { [key: string]: string } = {\n", 12 | ); 13 | let firstLine = true; 14 | for (const downloadUrl of downloadUrls) { 15 | const key = getKey(downloadUrl); 16 | if (key === undefined) { 17 | continue; 18 | } 19 | const checksum = await getOrDownloadChecksum(key, downloadUrl); 20 | if (!firstLine) { 21 | await fs.appendFile(filePath, ",\n"); 22 | } 23 | await fs.appendFile(filePath, ` "${key}":\n "${checksum}"`); 24 | firstLine = false; 25 | } 26 | await fs.appendFile(filePath, ",\n};\n"); 27 | } 28 | 29 | function getKey(downloadUrl: string): string | undefined { 30 | // https://github.com/astral-sh/uv/releases/download/0.3.2/uv-aarch64-apple-darwin.tar.gz.sha256 31 | const parts = downloadUrl.split("/"); 32 | const fileName = parts[parts.length - 1]; 33 | if (fileName.startsWith("source")) { 34 | return undefined; 35 | } 36 | const name = fileName.split(".")[0].split("uv-")[1]; 37 | const version = parts[parts.length - 2]; 38 | return `${name}-${version}`; 39 | } 40 | 41 | async function getOrDownloadChecksum( 42 | key: string, 43 | downloadUrl: string, 44 | ): Promise { 45 | let checksum = ""; 46 | if (key in KNOWN_CHECKSUMS) { 47 | checksum = KNOWN_CHECKSUMS[key]; 48 | } else { 49 | const content = await downloadAssetContent(downloadUrl); 50 | checksum = content.split(" ")[0].trim(); 51 | } 52 | return checksum; 53 | } 54 | 55 | async function downloadAssetContent(downloadUrl: string): Promise { 56 | const downloadPath = await tc.downloadTool(downloadUrl); 57 | const content = await fs.readFile(downloadPath, "utf8"); 58 | return content; 59 | } 60 | -------------------------------------------------------------------------------- /src/download/download-version.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import * as tc from "@actions/tool-cache"; 3 | import * as path from "node:path"; 4 | import * as pep440 from "@renovatebot/pep440"; 5 | import { promises as fs } from "node:fs"; 6 | import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants"; 7 | import type { Architecture, Platform } from "../utils/platforms"; 8 | import { validateChecksum } from "./checksum/checksum"; 9 | import { Octokit } from "../utils/octokit"; 10 | 11 | export function tryGetFromToolCache( 12 | arch: Architecture, 13 | version: string, 14 | ): { version: string; installedPath: string | undefined } { 15 | core.debug(`Trying to get uv from tool cache for ${version}...`); 16 | const cachedVersions = tc.findAllVersions(TOOL_CACHE_NAME, arch); 17 | core.debug(`Cached versions: ${cachedVersions}`); 18 | let resolvedVersion = tc.evaluateVersions(cachedVersions, version); 19 | if (resolvedVersion === "") { 20 | resolvedVersion = version; 21 | } 22 | const installedPath = tc.find(TOOL_CACHE_NAME, resolvedVersion, arch); 23 | return { version: resolvedVersion, installedPath }; 24 | } 25 | 26 | export async function downloadVersion( 27 | serverUrl: string, 28 | platform: Platform, 29 | arch: Architecture, 30 | version: string, 31 | checkSum: string | undefined, 32 | githubToken: string, 33 | ): Promise<{ version: string; cachedToolDir: string }> { 34 | const resolvedVersion = await resolveVersion(version, githubToken); 35 | const artifact = `uv-${arch}-${platform}`; 36 | let extension = ".tar.gz"; 37 | if (platform === "pc-windows-msvc") { 38 | extension = ".zip"; 39 | } 40 | const downloadUrl = `${serverUrl}/${OWNER}/${REPO}/releases/download/${resolvedVersion}/${artifact}${extension}`; 41 | core.info(`Downloading uv from "${downloadUrl}" ...`); 42 | 43 | const downloadPath = await tc.downloadTool( 44 | downloadUrl, 45 | undefined, 46 | githubToken, 47 | ); 48 | await validateChecksum( 49 | checkSum, 50 | downloadPath, 51 | arch, 52 | platform, 53 | resolvedVersion, 54 | ); 55 | 56 | let uvDir: string; 57 | if (platform === "pc-windows-msvc") { 58 | const fullPathWithExtension = `${downloadPath}${extension}`; 59 | await fs.copyFile(downloadPath, fullPathWithExtension); 60 | uvDir = await tc.extractZip(fullPathWithExtension); 61 | // On windows extracting the zip does not create an intermediate directory 62 | } else { 63 | const extractedDir = await tc.extractTar(downloadPath); 64 | uvDir = path.join(extractedDir, artifact); 65 | } 66 | const cachedToolDir = await tc.cacheDir( 67 | uvDir, 68 | TOOL_CACHE_NAME, 69 | resolvedVersion, 70 | arch, 71 | ); 72 | return { version: resolvedVersion, cachedToolDir }; 73 | } 74 | 75 | export async function resolveVersion( 76 | versionInput: string, 77 | githubToken: string, 78 | ): Promise { 79 | core.debug(`Resolving version: ${versionInput}`); 80 | const version = 81 | versionInput === "latest" 82 | ? await getLatestVersion(githubToken) 83 | : versionInput; 84 | if (tc.isExplicitVersion(version)) { 85 | core.debug(`Version ${version} is an explicit version.`); 86 | return version; 87 | } 88 | const availableVersions = await getAvailableVersions(githubToken); 89 | core.debug(`Available versions: ${availableVersions}`); 90 | const resolvedVersion = maxSatisfying(availableVersions, version); 91 | if (resolvedVersion === undefined) { 92 | throw new Error(`No version found for ${version}`); 93 | } 94 | return resolvedVersion; 95 | } 96 | 97 | async function getAvailableVersions(githubToken: string): Promise { 98 | try { 99 | const octokit = new Octokit({ 100 | auth: githubToken, 101 | }); 102 | return await getReleaseTagNames(octokit); 103 | } catch (err) { 104 | if ((err as Error).message.includes("Bad credentials")) { 105 | core.info( 106 | "No (valid) GitHub token provided. Falling back to anonymous. Requests might be rate limited.", 107 | ); 108 | const octokit = new Octokit(); 109 | return await getReleaseTagNames(octokit); 110 | } 111 | throw err; 112 | } 113 | } 114 | 115 | async function getReleaseTagNames( 116 | octokit: InstanceType, 117 | ): Promise { 118 | const response = await octokit.paginate(octokit.rest.repos.listReleases, { 119 | owner: OWNER, 120 | repo: REPO, 121 | }); 122 | return response.map((release) => release.tag_name); 123 | } 124 | 125 | async function getLatestVersion(githubToken: string) { 126 | core.debug("Getting latest version..."); 127 | const octokit = new Octokit({ 128 | auth: githubToken, 129 | }); 130 | 131 | let latestRelease: { tag_name: string } | undefined; 132 | try { 133 | latestRelease = await getLatestRelease(octokit); 134 | } catch (err) { 135 | core.info( 136 | "No (valid) GitHub token provided. Falling back to anonymous. Requests might be rate limited.", 137 | ); 138 | if (err instanceof Error) { 139 | core.debug(err.message); 140 | } 141 | const octokit = new Octokit(); 142 | latestRelease = await getLatestRelease(octokit); 143 | } 144 | 145 | if (!latestRelease) { 146 | throw new Error("Could not determine latest release."); 147 | } 148 | core.debug(`Latest version: ${latestRelease.tag_name}`); 149 | return latestRelease.tag_name; 150 | } 151 | 152 | async function getLatestRelease(octokit: InstanceType) { 153 | const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({ 154 | owner: OWNER, 155 | repo: REPO, 156 | }); 157 | return latestRelease; 158 | } 159 | 160 | function maxSatisfying( 161 | versions: string[], 162 | version: string, 163 | ): string | undefined { 164 | const maxSemver = tc.evaluateVersions(versions, version); 165 | if (maxSemver !== "") { 166 | core.debug(`Found a version that satisfies the semver range: ${maxSemver}`); 167 | return maxSemver; 168 | } 169 | const maxPep440 = pep440.maxSatisfying(versions, version); 170 | if (maxPep440 !== null) { 171 | core.debug( 172 | `Found a version that satisfies the pep440 specifier: ${maxPep440}`, 173 | ); 174 | return maxPep440; 175 | } 176 | return undefined; 177 | } 178 | -------------------------------------------------------------------------------- /src/download/version-manifest.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "node:fs"; 2 | import * as core from "@actions/core"; 3 | import * as semver from "semver"; 4 | 5 | interface VersionManifestEntry { 6 | version: string; 7 | artifactName: string; 8 | arch: string; 9 | platform: string; 10 | downloadUrl: string; 11 | } 12 | 13 | export async function getLatestKnownVersion( 14 | versionManifestFile: string, 15 | ): Promise { 16 | const data = await fs.readFile(versionManifestFile); 17 | const versionManifestEntries: VersionManifestEntry[] = JSON.parse( 18 | data.toString(), 19 | ); 20 | return versionManifestEntries.reduce((a, b) => 21 | semver.gt(a.version, b.version) ? a : b, 22 | ).version; 23 | } 24 | 25 | export async function updateVersionManifest( 26 | versionManifestFile: string, 27 | downloadUrls: string[], 28 | ): Promise { 29 | const versionManifest: VersionManifestEntry[] = []; 30 | 31 | for (const downloadUrl of downloadUrls) { 32 | const urlParts = downloadUrl.split("/"); 33 | const version = urlParts[urlParts.length - 2]; 34 | const artifactName = urlParts[urlParts.length - 1]; 35 | if (!artifactName.startsWith("uv-")) { 36 | continue; 37 | } 38 | if (artifactName.startsWith("uv-installer")) { 39 | continue; 40 | } 41 | const artifactParts = artifactName.split(".")[0].split("-"); 42 | versionManifest.push({ 43 | version: version, 44 | artifactName: artifactName, 45 | arch: artifactParts[1], 46 | platform: artifactName.split(`uv-${artifactParts[1]}-`)[1].split(".")[0], 47 | downloadUrl: downloadUrl, 48 | }); 49 | } 50 | core.debug(`Updating version manifest: ${JSON.stringify(versionManifest)}`); 51 | await fs.writeFile(versionManifestFile, JSON.stringify(versionManifest)); 52 | } 53 | -------------------------------------------------------------------------------- /src/hash/hash-files.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from "node:crypto"; 2 | import * as core from "@actions/core"; 3 | import * as fs from "node:fs"; 4 | import * as stream from "node:stream"; 5 | import * as util from "node:util"; 6 | import { create } from "@actions/glob"; 7 | 8 | /** 9 | * Hashes files matching the given glob pattern. 10 | * 11 | * Copied from https://github.com/actions/toolkit/blob/20ed2908f19538e9dfb66d8083f1171c0a50a87c/packages/glob/src/internal-hash-files.ts#L9-L49 12 | * But supports hashing files outside the GITHUB_WORKSPACE. 13 | * @param pattern The glob pattern to match files. 14 | * @param verbose Whether to log the files being hashed. 15 | */ 16 | export async function hashFiles( 17 | pattern: string, 18 | verbose = false, 19 | ): Promise { 20 | const globber = await create(pattern); 21 | let hasMatch = false; 22 | const writeDelegate = verbose ? core.info : core.debug; 23 | const result = crypto.createHash("sha256"); 24 | let count = 0; 25 | for await (const file of globber.globGenerator()) { 26 | writeDelegate(file); 27 | if (fs.statSync(file).isDirectory()) { 28 | writeDelegate(`Skip directory '${file}'.`); 29 | continue; 30 | } 31 | const hash = crypto.createHash("sha256"); 32 | const pipeline = util.promisify(stream.pipeline); 33 | await pipeline(fs.createReadStream(file), hash); 34 | result.write(hash.digest()); 35 | count++; 36 | if (!hasMatch) { 37 | hasMatch = true; 38 | } 39 | } 40 | result.end(); 41 | 42 | if (hasMatch) { 43 | writeDelegate(`Found ${count} files to hash.`); 44 | return result.digest("hex"); 45 | } 46 | writeDelegate("No matches found for glob"); 47 | return ""; 48 | } 49 | -------------------------------------------------------------------------------- /src/save-cache.ts: -------------------------------------------------------------------------------- 1 | import * as cache from "@actions/cache"; 2 | import * as core from "@actions/core"; 3 | import * as exec from "@actions/exec"; 4 | import * as fs from "node:fs"; 5 | import { 6 | STATE_CACHE_MATCHED_KEY, 7 | STATE_CACHE_KEY, 8 | } from "./cache/restore-cache"; 9 | import { 10 | cacheLocalPath, 11 | enableCache, 12 | ignoreNothingToCache, 13 | pruneCache as shouldPruneCache, 14 | } from "./utils/inputs"; 15 | 16 | export async function run(): Promise { 17 | try { 18 | if (enableCache) { 19 | await saveCache(); 20 | // node will stay alive if any promises are not resolved, 21 | // which is a possibility if HTTP requests are dangling 22 | // due to retries or timeouts. We know that if we got here 23 | // that all promises that we care about have successfully 24 | // resolved, so simply exit with success. 25 | process.exit(0); 26 | } 27 | } catch (error) { 28 | const err = error as Error; 29 | core.setFailed(err.message); 30 | } 31 | } 32 | 33 | async function saveCache(): Promise { 34 | const cacheKey = core.getState(STATE_CACHE_KEY); 35 | const matchedKey = core.getState(STATE_CACHE_MATCHED_KEY); 36 | 37 | if (!cacheKey) { 38 | core.warning("Error retrieving cache key from state."); 39 | return; 40 | } 41 | if (matchedKey === cacheKey) { 42 | core.info(`Cache hit occurred on key ${cacheKey}, not saving cache.`); 43 | return; 44 | } 45 | 46 | if (shouldPruneCache) { 47 | await pruneCache(); 48 | } 49 | 50 | core.info(`Saving cache path: ${cacheLocalPath}`); 51 | if (!fs.existsSync(cacheLocalPath) && !ignoreNothingToCache) { 52 | throw new Error( 53 | `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.`, 54 | ); 55 | } 56 | try { 57 | await cache.saveCache([cacheLocalPath], cacheKey); 58 | core.info(`cache saved with the key: ${cacheKey}`); 59 | } catch (e) { 60 | if ( 61 | e instanceof Error && 62 | e.message === 63 | "Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved." 64 | ) { 65 | core.info( 66 | "No cacheable paths were found. Ignoring because ignore-nothing-to-save is enabled.", 67 | ); 68 | } else { 69 | throw e; 70 | } 71 | } 72 | } 73 | 74 | async function pruneCache(): Promise { 75 | const options: exec.ExecOptions = { 76 | silent: !core.isDebug(), 77 | }; 78 | const execArgs = ["cache", "prune", "--ci"]; 79 | 80 | core.info("Pruning cache..."); 81 | await exec.exec("uv", execArgs, options); 82 | } 83 | 84 | run(); 85 | -------------------------------------------------------------------------------- /src/setup-uv.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import * as path from "node:path"; 3 | import { 4 | downloadVersion, 5 | tryGetFromToolCache, 6 | resolveVersion, 7 | } from "./download/download-version"; 8 | import { restoreCache } from "./cache/restore-cache"; 9 | 10 | import { 11 | type Architecture, 12 | getArch, 13 | getPlatform, 14 | type Platform, 15 | } from "./utils/platforms"; 16 | import { 17 | activateEnvironment as activateEnvironmentInput, 18 | cacheLocalPath, 19 | checkSum, 20 | ignoreEmptyWorkdir, 21 | enableCache, 22 | githubToken, 23 | pythonVersion, 24 | toolBinDir, 25 | toolDir, 26 | version as versionInput, 27 | workingDirectory, 28 | serverUrl, 29 | } from "./utils/inputs"; 30 | import * as exec from "@actions/exec"; 31 | import fs from "node:fs"; 32 | import { getUvVersionFromConfigFile } from "./utils/config-file"; 33 | 34 | async function run(): Promise { 35 | detectEmptyWorkdir(); 36 | const platform = await getPlatform(); 37 | const arch = getArch(); 38 | 39 | try { 40 | if (platform === undefined) { 41 | throw new Error(`Unsupported platform: ${process.platform}`); 42 | } 43 | if (arch === undefined) { 44 | throw new Error(`Unsupported architecture: ${process.arch}`); 45 | } 46 | const setupResult = await setupUv(platform, arch, checkSum, githubToken); 47 | 48 | addToolBinToPath(); 49 | addUvToPathAndOutput(setupResult.uvDir); 50 | setToolDir(); 51 | setupPython(); 52 | await activateEnvironment(); 53 | addMatchers(); 54 | setCacheDir(cacheLocalPath); 55 | 56 | core.setOutput("uv-version", setupResult.version); 57 | core.info(`Successfully installed uv version ${setupResult.version}`); 58 | 59 | if (enableCache) { 60 | await restoreCache(); 61 | } 62 | process.exit(0); 63 | } catch (err) { 64 | core.setFailed((err as Error).message); 65 | } 66 | } 67 | 68 | function detectEmptyWorkdir(): void { 69 | if (fs.readdirSync(".").length === 0) { 70 | if (ignoreEmptyWorkdir) { 71 | core.info( 72 | "Empty workdir detected. Ignoring because ignore-empty-workdir is enabled", 73 | ); 74 | } else { 75 | core.warning( 76 | "Empty workdir detected. This may cause unexpected behavior. You can enable ignore-empty-workdir to mute this warning.", 77 | ); 78 | } 79 | } 80 | } 81 | 82 | async function setupUv( 83 | platform: Platform, 84 | arch: Architecture, 85 | checkSum: string | undefined, 86 | githubToken: string, 87 | ): Promise<{ uvDir: string; version: string }> { 88 | const resolvedVersion = await determineVersion(); 89 | const toolCacheResult = tryGetFromToolCache(arch, resolvedVersion); 90 | if (toolCacheResult.installedPath) { 91 | core.info(`Found uv in tool-cache for ${toolCacheResult.version}`); 92 | return { 93 | uvDir: toolCacheResult.installedPath, 94 | version: toolCacheResult.version, 95 | }; 96 | } 97 | 98 | const downloadVersionResult = await downloadVersion( 99 | serverUrl, 100 | platform, 101 | arch, 102 | resolvedVersion, 103 | checkSum, 104 | githubToken, 105 | ); 106 | 107 | return { 108 | uvDir: downloadVersionResult.cachedToolDir, 109 | version: downloadVersionResult.version, 110 | }; 111 | } 112 | 113 | async function determineVersion(): Promise { 114 | if (versionInput !== "") { 115 | return await resolveVersion(versionInput, githubToken); 116 | } 117 | const versionFromUvToml = getUvVersionFromConfigFile( 118 | `${workingDirectory}${path.sep}uv.toml`, 119 | ); 120 | const versionFromPyproject = getUvVersionFromConfigFile( 121 | `${workingDirectory}${path.sep}pyproject.toml`, 122 | ); 123 | if (versionFromUvToml === undefined && versionFromPyproject === undefined) { 124 | core.info( 125 | "Could not determine uv version from uv.toml or pyproject.toml. Falling back to latest.", 126 | ); 127 | } 128 | return await resolveVersion( 129 | versionFromUvToml || versionFromPyproject || "latest", 130 | githubToken, 131 | ); 132 | } 133 | 134 | function addUvToPathAndOutput(cachedPath: string): void { 135 | core.setOutput("uv-path", `${cachedPath}${path.sep}uv`); 136 | core.setOutput("uvx-path", `${cachedPath}${path.sep}uvx`); 137 | core.addPath(cachedPath); 138 | core.info(`Added ${cachedPath} to the path`); 139 | } 140 | 141 | function addToolBinToPath(): void { 142 | if (toolBinDir !== undefined) { 143 | core.exportVariable("UV_TOOL_BIN_DIR", toolBinDir); 144 | core.info(`Set UV_TOOL_BIN_DIR to ${toolBinDir}`); 145 | core.addPath(toolBinDir); 146 | core.info(`Added ${toolBinDir} to the path`); 147 | } else { 148 | if (process.env.XDG_BIN_HOME !== undefined) { 149 | core.addPath(process.env.XDG_BIN_HOME); 150 | core.info(`Added ${process.env.XDG_BIN_HOME} to the path`); 151 | } else if (process.env.XDG_DATA_HOME !== undefined) { 152 | core.addPath(`${process.env.XDG_DATA_HOME}/../bin`); 153 | core.info(`Added ${process.env.XDG_DATA_HOME}/../bin to the path`); 154 | } else { 155 | core.addPath(`${process.env.HOME}/.local/bin`); 156 | core.info(`Added ${process.env.HOME}/.local/bin to the path`); 157 | } 158 | } 159 | } 160 | 161 | function setToolDir(): void { 162 | if (toolDir !== undefined) { 163 | core.exportVariable("UV_TOOL_DIR", toolDir); 164 | core.info(`Set UV_TOOL_DIR to ${toolDir}`); 165 | } 166 | } 167 | 168 | function setupPython(): void { 169 | if (pythonVersion !== "") { 170 | core.exportVariable("UV_PYTHON", pythonVersion); 171 | core.info(`Set UV_PYTHON to ${pythonVersion}`); 172 | } 173 | } 174 | 175 | async function activateEnvironment(): Promise { 176 | if (activateEnvironmentInput) { 177 | const execArgs = ["venv", ".venv", "--directory", workingDirectory]; 178 | 179 | core.info("Activating python venv..."); 180 | await exec.exec("uv", execArgs); 181 | 182 | let venvBinPath = `${workingDirectory}${path.sep}.venv${path.sep}bin`; 183 | if (process.platform === "win32") { 184 | venvBinPath = `${workingDirectory}${path.sep}.venv${path.sep}Scripts`; 185 | } 186 | core.addPath(path.resolve(venvBinPath)); 187 | core.exportVariable( 188 | "VIRTUAL_ENV", 189 | path.resolve(`${workingDirectory}${path.sep}.venv`), 190 | ); 191 | } 192 | } 193 | 194 | function setCacheDir(cacheLocalPath: string): void { 195 | core.exportVariable("UV_CACHE_DIR", cacheLocalPath); 196 | core.info(`Set UV_CACHE_DIR to ${cacheLocalPath}`); 197 | } 198 | 199 | function addMatchers(): void { 200 | const matchersPath = path.join(__dirname, `..${path.sep}..`, ".github"); 201 | core.info(`##[add-matcher]${path.join(matchersPath, "python.json")}`); 202 | } 203 | 204 | run(); 205 | -------------------------------------------------------------------------------- /src/update-known-versions.ts: -------------------------------------------------------------------------------- 1 | import * as semver from "semver"; 2 | import * as core from "@actions/core"; 3 | import { Octokit } from "./utils/octokit"; 4 | 5 | import { OWNER, REPO } from "./utils/constants"; 6 | 7 | import { updateChecksums } from "./download/checksum/update-known-checksums"; 8 | import { 9 | updateVersionManifest, 10 | getLatestKnownVersion, 11 | } from "./download/version-manifest"; 12 | 13 | async function run(): Promise { 14 | const checksumFilePath = process.argv.slice(2)[0]; 15 | const versionsManifestFilePath = process.argv.slice(2)[1]; 16 | const githubToken = process.argv.slice(2)[2]; 17 | 18 | const octokit = new Octokit({ 19 | auth: githubToken, 20 | }); 21 | 22 | const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({ 23 | owner: OWNER, 24 | repo: REPO, 25 | }); 26 | 27 | const latestKnownVersion = await getLatestKnownVersion( 28 | versionsManifestFilePath, 29 | ); 30 | 31 | if (semver.lte(latestRelease.tag_name, latestKnownVersion)) { 32 | core.info( 33 | `Latest release (${latestRelease.tag_name}) is not newer than the latest known version (${latestKnownVersion}). Skipping update.`, 34 | ); 35 | return; 36 | } 37 | 38 | const releases = await octokit.paginate(octokit.rest.repos.listReleases, { 39 | owner: OWNER, 40 | repo: REPO, 41 | }); 42 | const checksumDownloadUrls: string[] = releases.flatMap((release) => 43 | release.assets 44 | .filter((asset) => asset.name.endsWith(".sha256")) 45 | .map((asset) => asset.browser_download_url), 46 | ); 47 | await updateChecksums(checksumFilePath, checksumDownloadUrls); 48 | 49 | const artifactDownloadUrls: string[] = releases.flatMap((release) => 50 | release.assets 51 | .filter((asset) => !asset.name.endsWith(".sha256")) 52 | .map((asset) => asset.browser_download_url), 53 | ); 54 | 55 | await updateVersionManifest(versionsManifestFilePath, artifactDownloadUrls); 56 | 57 | core.setOutput("latest-version", latestRelease.tag_name); 58 | } 59 | 60 | run(); 61 | -------------------------------------------------------------------------------- /src/utils/config-file.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import * as core from "@actions/core"; 3 | import * as toml from "smol-toml"; 4 | 5 | export function getUvVersionFromConfigFile( 6 | filePath: string, 7 | ): string | undefined { 8 | core.info(`Trying to find required-version for uv in: ${filePath}`); 9 | if (!fs.existsSync(filePath)) { 10 | core.info(`Could not find file: ${filePath}`); 11 | return undefined; 12 | } 13 | let requiredVersion: string | undefined; 14 | try { 15 | requiredVersion = getRequiredVersion(filePath); 16 | } catch (err) { 17 | const message = (err as Error).message; 18 | core.warning(`Error while parsing ${filePath}: ${message}`); 19 | return undefined; 20 | } 21 | 22 | if (requiredVersion?.startsWith("==")) { 23 | requiredVersion = requiredVersion.slice(2); 24 | } 25 | if (requiredVersion !== undefined) { 26 | core.info( 27 | `Found required-version for uv in ${filePath}: ${requiredVersion}`, 28 | ); 29 | } 30 | return requiredVersion; 31 | } 32 | 33 | function getRequiredVersion(filePath: string): string | undefined { 34 | const fileContent = fs.readFileSync(filePath, "utf-8"); 35 | 36 | if (filePath.endsWith("pyproject.toml")) { 37 | const tomlContent = toml.parse(fileContent) as { 38 | tool?: { uv?: { "required-version"?: string } }; 39 | }; 40 | return tomlContent?.tool?.uv?.["required-version"]; 41 | } 42 | const tomlContent = toml.parse(fileContent) as { 43 | "required-version"?: string; 44 | }; 45 | return tomlContent["required-version"]; 46 | } 47 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const REPO = "uv"; 2 | export const OWNER = "astral-sh"; 3 | export const TOOL_CACHE_NAME = "uv"; 4 | -------------------------------------------------------------------------------- /src/utils/inputs.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import path from "node:path"; 3 | 4 | export const version = core.getInput("version"); 5 | export const pythonVersion = core.getInput("python-version"); 6 | export const activateEnvironment = core.getBooleanInput("activate-environment"); 7 | export const workingDirectory = core.getInput("working-directory"); 8 | export const checkSum = core.getInput("checksum"); 9 | export const enableCache = getEnableCache(); 10 | export const cacheSuffix = core.getInput("cache-suffix") || ""; 11 | export const cacheLocalPath = getCacheLocalPath(); 12 | export const cacheDependencyGlob = core.getInput("cache-dependency-glob"); 13 | export const pruneCache = core.getInput("prune-cache") === "true"; 14 | export const ignoreNothingToCache = 15 | core.getInput("ignore-nothing-to-cache") === "true"; 16 | export const ignoreEmptyWorkdir = 17 | core.getInput("ignore-empty-workdir") === "true"; 18 | export const toolBinDir = getToolBinDir(); 19 | export const toolDir = getToolDir(); 20 | export const serverUrl = core.getInput("server-url"); 21 | export const githubToken = core.getInput("github-token"); 22 | 23 | function getEnableCache(): boolean { 24 | const enableCacheInput = core.getInput("enable-cache"); 25 | if (enableCacheInput === "auto") { 26 | return process.env.RUNNER_ENVIRONMENT === "github-hosted"; 27 | } 28 | return enableCacheInput === "true"; 29 | } 30 | 31 | function getToolBinDir(): string | undefined { 32 | const toolBinDirInput = core.getInput("tool-bin-dir"); 33 | if (toolBinDirInput !== "") { 34 | return expandTilde(toolBinDirInput); 35 | } 36 | if (process.platform === "win32") { 37 | if (process.env.RUNNER_TEMP !== undefined) { 38 | return `${process.env.RUNNER_TEMP}${path.sep}uv-tool-bin-dir`; 39 | } 40 | throw Error( 41 | "Could not determine UV_TOOL_BIN_DIR. Please make sure RUNNER_TEMP is set or provide the tool-bin-dir input", 42 | ); 43 | } 44 | return undefined; 45 | } 46 | 47 | function getToolDir(): string | undefined { 48 | const toolDirInput = core.getInput("tool-dir"); 49 | if (toolDirInput !== "") { 50 | return expandTilde(toolDirInput); 51 | } 52 | if (process.platform === "win32") { 53 | if (process.env.RUNNER_TEMP !== undefined) { 54 | return `${process.env.RUNNER_TEMP}${path.sep}uv-tool-dir`; 55 | } 56 | throw Error( 57 | "Could not determine UV_TOOL_DIR. Please make sure RUNNER_TEMP is set or provide the tool-dir input", 58 | ); 59 | } 60 | return undefined; 61 | } 62 | 63 | function getCacheLocalPath(): string { 64 | const cacheLocalPathInput = core.getInput("cache-local-path"); 65 | if (cacheLocalPathInput !== "") { 66 | return expandTilde(cacheLocalPathInput); 67 | } 68 | if (process.env.RUNNER_ENVIRONMENT === "github-hosted") { 69 | if (process.env.RUNNER_TEMP !== undefined) { 70 | return `${process.env.RUNNER_TEMP}${path.sep}setup-uv-cache`; 71 | } 72 | throw Error( 73 | "Could not determine UV_CACHE_DIR. Please make sure RUNNER_TEMP is set or provide the cache-local-path input", 74 | ); 75 | } 76 | if (process.platform === "win32") { 77 | return `${process.env.APPDATA}${path.sep}uv${path.sep}cache`; 78 | } 79 | return `${process.env.HOME}${path.sep}.cache${path.sep}uv`; 80 | } 81 | 82 | function expandTilde(input: string): string { 83 | if (input.startsWith("~")) { 84 | return `${process.env.HOME}${input.substring(1)}`; 85 | } 86 | return input; 87 | } 88 | -------------------------------------------------------------------------------- /src/utils/octokit.ts: -------------------------------------------------------------------------------- 1 | import { Octokit as Core } from "@octokit/core"; 2 | import type { 3 | Constructor, 4 | OctokitOptions, 5 | } from "@octokit/core/dist-types/types"; 6 | import { 7 | paginateRest, 8 | type PaginateInterface, 9 | } from "@octokit/plugin-paginate-rest"; 10 | import { legacyRestEndpointMethods } from "@octokit/plugin-rest-endpoint-methods"; 11 | import { fetch as undiciFetch, ProxyAgent, type RequestInit } from "undici"; 12 | 13 | export type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods"; 14 | 15 | const DEFAULTS = { 16 | baseUrl: "https://api.github.com", 17 | userAgent: "setup-uv", 18 | }; 19 | 20 | export function getProxyAgent() { 21 | const httpProxy = process.env.HTTP_PROXY || process.env.http_prox; 22 | if (httpProxy) { 23 | return new ProxyAgent(httpProxy); 24 | } 25 | 26 | const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; 27 | if (httpsProxy) { 28 | return new ProxyAgent(httpsProxy); 29 | } 30 | 31 | return undefined; 32 | } 33 | 34 | export const customFetch = async (url: string, opts: RequestInit) => 35 | await undiciFetch(url, { 36 | dispatcher: getProxyAgent(), 37 | ...opts, 38 | }); 39 | 40 | export const Octokit: typeof Core & 41 | Constructor< 42 | { 43 | paginate: PaginateInterface; 44 | } & ReturnType 45 | > = Core.plugin(paginateRest, legacyRestEndpointMethods).defaults( 46 | function buildDefaults(options: OctokitOptions): OctokitOptions { 47 | return { 48 | ...DEFAULTS, 49 | ...options, 50 | request: { 51 | fetch: customFetch, 52 | ...options.request, 53 | }, 54 | }; 55 | }, 56 | ); 57 | 58 | export type Octokit = InstanceType; 59 | -------------------------------------------------------------------------------- /src/utils/platforms.ts: -------------------------------------------------------------------------------- 1 | import * as exec from "@actions/exec"; 2 | import * as core from "@actions/core"; 3 | export type Platform = 4 | | "unknown-linux-gnu" 5 | | "unknown-linux-musl" 6 | | "unknown-linux-musleabihf" 7 | | "apple-darwin" 8 | | "pc-windows-msvc"; 9 | export type Architecture = 10 | | "i686" 11 | | "x86_64" 12 | | "aarch64" 13 | | "s390x" 14 | | "powerpc64le"; 15 | 16 | export function getArch(): Architecture | undefined { 17 | const arch = process.arch; 18 | const archMapping: { [key: string]: Architecture } = { 19 | ia32: "i686", 20 | x64: "x86_64", 21 | arm64: "aarch64", 22 | s390x: "s390x", 23 | ppc64: "powerpc64le", 24 | }; 25 | 26 | if (arch in archMapping) { 27 | return archMapping[arch]; 28 | } 29 | } 30 | 31 | export async function getPlatform(): Promise { 32 | const processPlatform = process.platform; 33 | const platformMapping: { [key: string]: Platform } = { 34 | linux: "unknown-linux-gnu", 35 | darwin: "apple-darwin", 36 | win32: "pc-windows-msvc", 37 | }; 38 | 39 | if (processPlatform in platformMapping) { 40 | const platform = platformMapping[processPlatform]; 41 | if (platform === "unknown-linux-gnu") { 42 | const isMusl = await isMuslOs(); 43 | return isMusl ? "unknown-linux-musl" : platform; 44 | } 45 | return platform; 46 | } 47 | } 48 | 49 | async function isMuslOs(): Promise { 50 | let stdOutput = ""; 51 | let errOutput = ""; 52 | const options: exec.ExecOptions = { 53 | silent: !core.isDebug(), 54 | listeners: { 55 | stdout: (data: Buffer) => { 56 | stdOutput += data.toString(); 57 | }, 58 | stderr: (data: Buffer) => { 59 | errOutput += data.toString(); 60 | }, 61 | }, 62 | ignoreReturnCode: true, 63 | }; 64 | 65 | try { 66 | const execArgs = ["--version"]; 67 | await exec.exec("ldd", execArgs, options); 68 | return stdOutput.includes("musl") || errOutput.includes("musl"); 69 | } catch (error) { 70 | const err = error as Error; 71 | core.warning( 72 | `Failed to determine glibc or musl. Falling back to glibc. Error: ${err.message}`, 73 | ); 74 | return false; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 4 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 5 | "outDir": "./lib" /* Redirect output structure to the directory. */, 6 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 7 | "strict": true /* Enable all strict type-checking options. */, 8 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 9 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 10 | }, 11 | "exclude": ["node_modules", "**/*.test.ts"] 12 | } 13 | --------------------------------------------------------------------------------