Compare commits

..

No commits in common. "main" and "v7.3.1" have entirely different histories.
main ... v7.3.1

50 changed files with 272037 additions and 214375 deletions

View file

@ -1,48 +0,0 @@
---
name: dependabot-pr-rollup
description: Find open Dependabot PRs for the current GitHub repo, compare each PR head to its base branch, replay only the net dependency changes in a fresh worktree and branch, run npm validation, and optionally commit, push, and open a PR. Use when you want to batch or manually replicate active Dependabot updates.
license: MIT
compatibility: Requires git, git worktree, gh CLI auth, npm, and a GitHub repo with an origin remote.
---
# Dependabot PR Rollup
## When to use
Use this skill when the user wants to:
- find all open Dependabot PRs in the current repo
- reproduce their net effect in one local branch
- validate the result with the repo's standard npm checks
- optionally commit, push, and open a PR
## Workflow
1. Inspect the current checkout state, but do not reuse a dirty worktree.
2. List open Dependabot PRs with `gh pr list --state open --author app/dependabot`.
3. For each PR, collect the title, base branch, head branch, changed files, and relevant diffs.
4. Compare each PR head against `origin/<base>` instead of trusting the PR title. Dependabot PRs can already be partially merged, superseded by newer versions, or have no remaining net effect.
5. Create a new worktree and branch from `origin/<base>`.
6. Reproduce only the remaining dependency changes in the new worktree.
- Inspect `package.json` before editing.
- Run `npm ci --ignore-scripts` before applying updates.
- Use `npm install ... --ignore-scripts` for direct dependency changes so `package-lock.json` stays in sync.
7. Run `npm run all`.
8. If requested, commit the changed source, lockfile, and generated artifacts, then push and open a PR.
## Repo-specific notes
- Use `gh` for GitHub operations.
- Keep the user's original checkout untouched by working in a separate worktree.
- In this repo, `npm run all` is the safest validation command because it runs build, check, package, and test.
- If dependency changes affect bundled output, include the regenerated `dist/` files.
## Report back
Always report:
- open Dependabot PRs found
- which PRs required no net changes
- new branch name
- new worktree path
- files changed
- `npm run all` result
- if applicable, commit SHA and PR URL

263
.github/copilot-instructions.md vendored Normal file
View file

@ -0,0 +1,263 @@
# Copilot Instructions for setup-uv
This document provides essential information for GitHub Copilot coding agents working on the `astral-sh/setup-uv` repository.
## Repository Overview
**setup-uv** is a GitHub Action that sets up the [uv](https://docs.astral.sh/uv/)
Python package installer in GitHub Actions workflows.
It's a TypeScript-based action that downloads uv binaries, manages caching, handles version resolution,
and configures the environment for subsequent workflow steps.
### Key Features
- Downloads and installs specific uv versions from GitHub releases
- Supports version resolution from config files (pyproject.toml, uv.toml, .tool-versions)
- Implements intelligent caching for both uv cache and Python installations
- Provides cross-platform support (Linux, macOS, Windows, including ARM architectures)
- Includes problem matchers for Python error reporting
- Supports environment activation and custom tool directories
## Repository Structure
**Size**: Small-medium repository (~50 source files, ~400 total files including dependencies)
**Languages**: TypeScript (primary), JavaScript (compiled output), JSON (configuration)
**Runtime**: Node.js 24 (GitHub Actions runtime)
**Key Dependencies**: @actions/core, @actions/cache, @actions/tool-cache, @octokit/core
### Core Architecture
```
src/
├── setup-uv.ts # Main entry point and orchestration
├── save-cache.ts # Post-action cache saving logic
├── update-known-versions.ts # Maintenance script for version updates
├── cache/ # Cache management functionality
├── download/ # Version resolution and binary downloading
├── utils/ # Input parsing, platform detection, configuration
└── version/ # Version resolution from various file formats
```
### Key Files and Locations
- **Action Definition**: `action.yml` - Defines all inputs/outputs and entry points
- **Main Source**: `src/setup-uv.ts` - Primary action logic
- **Configuration**: `biome.json` (linting), `tsconfig.json` (TypeScript), `jest.config.js` (testing)
- **Compiled Output**: `dist/` - Contains compiled Node.js bundles (auto-generated, committed)
- **Test Fixtures**: `__tests__/fixtures/` - Sample projects for different configuration scenarios
- **Workflows**: `.github/workflows/test.yml` - Comprehensive CI/CD pipeline
## Build and Development Process
### Prerequisites
- Node.js 24+ (matches GitHub Actions runtime)
- npm (included with Node.js)
### Essential Commands (ALWAYS run in this order)
#### 1. Install Dependencies
```bash
npm ci --ignore-scripts
```
**Timing**: ~20-30 seconds
**Note**: Always run this first after cloning or when package.json changes
#### 2. Build TypeScript
```bash
npm run build
```
**Timing**: ~5-10 seconds
**Purpose**: Compiles TypeScript source to JavaScript in `lib/` directory
#### 3. Lint and Format Code
```bash
npm run check
```
**Timing**: ~2-5 seconds
**Tool**: Biome (replaces ESLint/Prettier)
**Auto-fixes**: Formatting, import organization, basic linting issues
#### 4. Package for Distribution
```bash
npm run package
```
**Timing**: ~20-30 seconds
**Purpose**: Creates bundled distributions in `dist/` using @vercel/ncc
**Critical**: This step MUST be run before committing - the `dist/` files are used by GitHub Actions
#### 5. Run Tests
```bash
npm test
```
**Timing**: ~10-15 seconds
**Framework**: Jest with TypeScript support
**Coverage**: Unit tests for version resolution, input parsing, checksum validation
#### 6. Complete Validation (Recommended)
```bash
npm run all
```
**Timing**: ~60-90 seconds
**Purpose**: Runs build → check → package → test in sequence
**Use**: Before making pull requests or when unsure about build state
### Important Build Notes
**CRITICAL**: Always run `npm run package` after making code changes. The `dist/` directory contains the compiled bundles that GitHub Actions actually executes. Forgetting this step will cause your changes to have no effect.
**TypeScript Warnings**: You may see ts-jest warnings about "isolatedModules" - these are harmless and don't affect functionality.
**Biome**: This project uses Biome instead of ESLint/Prettier. Run `npm run check` to fix formatting and linting issues automatically.
## Testing Strategy
### Unit Tests
- **Location**: `__tests__/` directory
- **Framework**: Jest with ts-jest transformer
- **Coverage**: Version resolution, input parsing, checksum validation, utility functions
### Integration Tests
- **Location**: `.github/workflows/test.yml`
- **Scope**: Full end-to-end testing across multiple platforms and scenarios
- **Key Test Categories**:
- Version installation (specific, latest, semver ranges)
- Cache behavior (setup, restore, invalidation)
- Cross-platform compatibility (Ubuntu, macOS, Windows, ARM)
- Configuration file parsing (pyproject.toml, uv.toml, requirements.txt)
- Error handling and edge cases
### Test Fixtures
Located in `__tests__/fixtures/`, these provide sample projects with different configurations:
- `pyproject-toml-project/` - Standard Python project with uv version specification
- `uv-toml-project/` - Project using uv.toml configuration
- `requirements-txt-project/` - Legacy requirements.txt format
- `cache-dir-defined-project/` - Custom cache directory configuration
## Continuous Integration
### GitHub Workflows
#### Primary Test Suite (`.github/workflows/test.yml`)
- **Triggers**: PRs, pushes to main, manual dispatch
- **Matrix**: Multiple OS (Ubuntu, macOS, Windows), architecture (x64, ARM), and configuration combinations
- **Duration**: ~5 minutes for full matrix
- **Key Validations**:
- Cross-platform installation and functionality
- Cache behavior and performance
- Version resolution from various sources
- Tool directory configurations
- Problem matcher functionality
#### Maintenance Workflows
- **CodeQL Analysis**: Security scanning on pushes/PRs
- **Update Known Versions**: Daily job to sync with latest uv releases
- **Dependabot**: Automated dependency updates
### Pre-commit Validation
The CI runs these checks that you should run locally:
1. `npm run all` - Complete build and test suite
2. ActionLint - GitHub Actions workflow validation
3. Change detection - Ensures no uncommitted build artifacts
## Key Configuration Files
### Action Configuration (`action.yml`)
Defines 20+ inputs including version specifications,
cache settings, tool directories, and environment options.
This file is the authoritative source for understanding available action parameters.
### TypeScript Configuration (`tsconfig.json`)
- Target: ES2024
- Module: nodenext (Node.js modules)
- Strict mode enabled
- Output directory: `lib/`
### Linting Configuration (`biome.json`)
- Formatter and linter combined
- Enforces consistent code style
- Automatically organizes imports and sorts object keys
## Common Development Patterns
### Making Code Changes
1. Edit TypeScript source files in `src/`
2. Run `npm run build` to compile
3. Run `npm run check` to format and lint
4. Run `npm run package` to update distribution bundles
5. Run `npm test` to verify functionality
6. Commit all changes including `dist/` files
### Adding New Features
- Follow existing patterns in `src/utils/inputs.ts` for new action inputs
- Update `action.yml` to declare new inputs/outputs
- Add corresponding tests in `__tests__/`
- Add a test in `.github/workflows/test.yml` if it affects integration
- Update README.md with usage examples
### Cache-Related Changes
- Cache logic is complex and affects performance significantly
- Test with multiple cache scenarios (hit, miss, invalidation)
- Consider impact on both GitHub-hosted and self-hosted runners
- Validate cache key generation and dependency detection
### Version Resolution Changes
- Version resolution supports multiple file formats and precedence rules
- Test with fixtures in `__tests__/fixtures/`
- Consider backward compatibility with existing projects
- Validate semver and PEP 440 specification handling
## Troubleshooting
### Build Failures
- **"Module not found"**: Run `npm ci --ignore-scripts` to ensure dependencies are installed
- **TypeScript errors**: Check `tsconfig.json` and ensure all imports are valid
- **Test failures**: Check if test fixtures have been modified or if logic changes broke assumptions
### Action Failures in Workflows
- **Changes not taking effect**: Ensure `npm run package` was run and `dist/` files committed
- **Version resolution issues**: Check version specification format and file existence
- **Cache problems**: Verify cache key generation and dependency glob patterns
### Common Gotchas
- **Forgetting to package**: Code changes won't work without running `npm run package`
- **Platform differences**: Windows paths use backslashes, test cross-platform behavior
- **Cache invalidation**: Cache keys are sensitive to dependency file changes
- **Tool directory permissions**: Some platforms require specific directory setups
## Trust These Instructions
These instructions are comprehensive and current. Only search for additional information if:
- You encounter specific error messages not covered here
- You need to understand implementation details of specific functions
- The instructions appear outdated (check repository commit history)
For most development tasks, following the build process and development patterns outlined above will be sufficient.

View file

@ -19,7 +19,7 @@ categories:
labels:
- "maintenance"
- "ci"
- "update-known-checksums"
- "update-known-versions"
- title: "📚 Documentation"
labels:
- "documentation"

View file

@ -1,9 +0,0 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "es2022",
"types": ["node"]
},
"include": ["check-all-tests-passed-needs.ts"]
}

View file

@ -19,6 +19,6 @@ jobs:
pull-requests: read
steps:
- name: 🚀 Run Release Drafter
uses: release-drafter/release-drafter@139054aeaa9adc52ab36ddf67437541f039b88e2 # v7.1.1
uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -25,10 +25,10 @@ jobs:
with:
persist-credentials: false
- name: Actionlint
uses: eifinger/actionlint-action@7802e0cc3ab3f81cbffb36fb0bf1a3621d994b89 # v1.10.1
uses: eifinger/actionlint-action@447fbfe7533062b7a9ea55f790f2396fba6d052a # v1.10.0
- name: Run zizmor
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: .nvmrc
cache: npm
@ -38,7 +38,7 @@ jobs:
npm run all
- name: Check all jobs are in all-tests-passed.needs
run: |
tsc -p tsconfig.json
tsc check-all-tests-passed-needs.ts
node check-all-tests-passed-needs.js
working-directory: .github/scripts
- name: Make sure no changes from linters are detected
@ -164,22 +164,10 @@ jobs:
- name: Latest version gets installed
run: |
LATEST_VERSION=$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/astral-sh/uv/releases/latest | jq -r '.tag_name')
UV_VERSION_OUTPUT=$(uv --version)
if [[ ! "$UV_VERSION_OUTPUT" =~ ^uv[[:space:]]+([^[:space:]]+) ]]; then
echo "Could not parse uv version from: $UV_VERSION_OUTPUT"
exit 1
fi
UV_VERSION="${BASH_REMATCH[1]}"
echo "Latest version is $LATEST_VERSION"
echo "uv --version output is $UV_VERSION_OUTPUT"
echo "Parsed uv version is $UV_VERSION"
if [ "$UV_VERSION" != "$LATEST_VERSION" ]; then
echo "Wrong uv version: $UV_VERSION_OUTPUT"
exit 1
if [ "$(uv --version)" != "uv $LATEST_VERSION" ]; then
echo "Wrong uv version: $(uv --version)"
exit 1
fi
env:
GH_TOKEN: ${{ github.token }}
@ -808,12 +796,12 @@ jobs:
- name: Install from custom manifest file
uses: ./
with:
manifest-file: "https://raw.githubusercontent.com/astral-sh/setup-uv/${{ github.ref }}/__tests__/download/custom-manifest.ndjson"
manifest-file: "https://raw.githubusercontent.com/astral-sh/setup-uv/${{ github.ref }}/__tests__/download/custom-manifest.json"
- run: uv sync
working-directory: __tests__/fixtures/uv-project
- name: Correct version gets installed
run: |
if [ "$(uv --version)" != "uv 0.9.26" ]; then
if [ "$(uv --version)" != "uv 0.7.12-alpha.1" ]; then
echo "Wrong uv version: $(uv --version)"
exit 1
fi

View file

@ -1,4 +1,4 @@
name: "Update known checksums"
name: "Update known versions"
on:
workflow_dispatch:
schedule:
@ -18,15 +18,16 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: true
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: .nvmrc
cache: npm
- name: Update known checksums
id: update-known-checksums
node-version: "20"
- name: Update known versions
id: update-known-versions
run:
node dist/update-known-checksums/index.cjs
node dist/update-known-versions/index.js
src/download/checksum/known-checksums.ts
version-manifest.json
${{ secrets.GITHUB_TOKEN }}
- name: Check for changes
id: changes-exist
run: |
@ -47,10 +48,10 @@ jobs:
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
git add .
git commit -m "chore: update known checksums for $LATEST_VERSION"
git commit -m "chore: update known versions for $LATEST_VERSION"
git push origin HEAD:refs/heads/main
env:
LATEST_VERSION: ${{ steps.update-known-checksums.outputs.latest-version }}
LATEST_VERSION: ${{ steps.update-known-versions.outputs.latest-version }}
- name: Create Pull Request
if: ${{ steps.changes-exist.outputs.changes-exist == 'true' && steps.commit-and-push.outcome != 'success' }}
@ -59,11 +60,11 @@ jobs:
commit-message: "chore: update known checksums"
title:
"chore: update known checksums for ${{
steps.update-known-checksums.outputs.latest-version }}"
steps.update-known-versions.outputs.latest-version }}"
body:
"chore: update known checksums for ${{
steps.update-known-checksums.outputs.latest-version }}"
steps.update-known-versions.outputs.latest-version }}"
base: main
labels: "automated-pr,update-known-checksums"
branch: update-known-checksums-pr
labels: "automated-pr,update-known-versions"
branch: update-known-versions-pr
delete-branch: true

View file

@ -0,0 +1,51 @@
---
name: Update Major Minor Tags
on:
push:
branches-ignore:
- "**"
tags:
- "v*.*.*"
permissions: {}
jobs:
update_major_minor_tags:
name: Make sure major and minor tags are up to date on a patch release
runs-on: ubuntu-24.04-arm
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: true # needed for git push below
- name: Update Major Minor Tags
run: |
set -x
cd "${GITHUB_WORKSPACE}" || exit
# Set up variables.
TAG="${GITHUB_REF#refs/tags/}" # v1.2.3
MINOR="${TAG%.*}" # v1.2
MAJOR="${MINOR%.*}" # v1
if [ "${GITHUB_REF}" = "${TAG}" ]; then
echo "This workflow is not triggered by tag push: GITHUB_REF=${GITHUB_REF}"
exit 1
fi
MESSAGE="Release ${TAG}"
# Set up Git.
git config user.name "${GITHUB_ACTOR}"
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
# Update MAJOR/MINOR tag
git tag -fa "${MAJOR}" -m "${MESSAGE}"
git tag -fa "${MINOR}" -m "${MESSAGE}"
# Push
git push --force origin "${MINOR}"
git push --force origin "${MAJOR}"

View file

@ -1,18 +0,0 @@
# setup-uv agent notes
This repository is a TypeScript-based GitHub Action for installing `uv` in GitHub Actions workflows. It also supports restoring/saving the `uv` cache and optional managed-Python caching.
- The published action runs the committed bundles in `dist/`, not the TypeScript in `src/`. After any code change, run `npm run package` and commit the resulting `dist/` updates.
- Standard local validation is:
1. `npm ci --ignore-scripts`
2. `npm run all`
- `npm run check` uses Biome (not ESLint/Prettier) and rewrites files in place.
- User-facing changes are usually multi-file changes. If you add or change inputs, outputs, or behavior, update `action.yml`, the implementation in `src/`, tests in `__tests__/`, relevant docs/README, and then re-package.
- The easiest areas to regress are version resolution and caching. When touching them, add or update tests for precedence, cache invalidation, and cross-platform path behavior.
- Workflow edits have extra CI-only checks (`actionlint` and `zizmor`); `npm run all` does not cover them.
- Source is authored with bundler-friendly TypeScript, but published action artifacts in `dist/` are bundled as CommonJS for maximum GitHub Actions runtime compatibility with `@actions/*` dependencies.
- Keep these concerns separate when changing module formats:
- `src/` and tests may use modern ESM-friendly TypeScript patterns.
- `dist/` should prioritize runtime reliability over format purity.
- Do not switch published bundles to ESM without validating the actual committed artifacts under the target Node runtime.
- Before finishing, make sure validation does not leave generated or formatting-only diffs behind.

1
AGENTS.md Symbolic link
View file

@ -0,0 +1 @@
.github/copilot-instructions.md

View file

@ -68,7 +68,7 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed
# The checksum of the uv version to install
checksum: ""
# Used when downloading uv from GitHub releases
# Used to increase the rate limit when retrieving versions and downloading uv
github-token: ${{ github.token }}
# Enable uploading of the uv cache: true, false, or auto (enabled on GitHub-hosted runners, disabled on self-hosted runners)
@ -114,7 +114,7 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed
# Custom path to set UV_TOOL_BIN_DIR to
tool-bin-dir: ""
# URL to a custom manifest file in the astral-sh/versions format
# URL to the manifest file containing available versions and download URLs
manifest-file: ""
# Add problem matchers
@ -190,12 +190,10 @@ For more advanced configuration options, see our detailed documentation:
## How it works
By default, this action resolves uv versions from the
[`astral-sh/versions`](https://github.com/astral-sh/versions) manifest and downloads uv from the
official [GitHub Releases](https://github.com/astral-sh/uv).
It then uses the [GitHub Actions Toolkit](https://github.com/actions/toolkit) to cache uv as a
tool to speed up consecutive runs on self-hosted runners.
This action downloads uv from the uv repo's official
[GitHub Releases](https://github.com/astral-sh/uv) and uses the
[GitHub Actions Toolkit](https://github.com/actions/toolkit) to cache it as a tool to speed up
consecutive runs on self-hosted runners.
The installed version of uv is then added to the runner PATH, enabling later steps to invoke it
by name (`uv`).

View file

@ -4,11 +4,10 @@ import {
validateChecksum,
} from "../../../src/download/checksum/checksum";
const validChecksum =
"f3da96ec7e995debee7f5d52ecd034dfb7074309a1da42f76429ecb814d813a3";
const filePath = "__tests__/fixtures/checksumfile";
test("checksum should match", async () => {
const validChecksum =
"f3da96ec7e995debee7f5d52ecd034dfb7074309a1da42f76429ecb814d813a3";
const filePath = "__tests__/fixtures/checksumfile";
// string params don't matter only test the checksum mechanism, not known checksums
await validateChecksum(
validChecksum,
@ -19,16 +18,6 @@ test("checksum should match", async () => {
);
});
test("provided checksum beats known checksums", async () => {
await validateChecksum(
validChecksum,
filePath,
"x86_64",
"unknown-linux-gnu",
"0.3.0",
);
});
type KnownVersionFixture = { version: string; known: boolean };
it.each<KnownVersionFixture>([

View file

@ -0,0 +1,9 @@
[
{
"arch": "x86_64",
"artifactName": "uv-x86_64-unknown-linux-gnu.tar.gz",
"downloadUrl": "https://release.pyx.dev/0.7.12-alpha.1/uv-x86_64-unknown-linux-gnu.tar.gz",
"platform": "unknown-linux-gnu",
"version": "0.7.12-alpha.1"
}
]

View file

@ -1 +0,0 @@
{"version":"0.9.26","artifacts":[{"platform":"x86_64-unknown-linux-gnu","variant":"default","url":"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz","archive_format":"tar.gz","sha256":"30ccbf0a66dc8727a02b0e245c583ee970bdafecf3a443c1686e1b30ec4939e8"}]}

View file

@ -1,353 +0,0 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import * as semver from "semver";
const mockInfo = jest.fn();
const mockWarning = jest.fn();
jest.unstable_mockModule("@actions/core", () => ({
debug: jest.fn(),
info: mockInfo,
warning: mockWarning,
}));
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockDownloadTool = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockExtractTar = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockExtractZip = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockCacheDir = jest.fn<any>();
jest.unstable_mockModule("@actions/tool-cache", () => ({
cacheDir: mockCacheDir,
downloadTool: mockDownloadTool,
evaluateVersions: (versions: string[], range: string) =>
semver.maxSatisfying(versions, range) ?? "",
extractTar: mockExtractTar,
extractZip: mockExtractZip,
find: () => "",
findAllVersions: () => [],
isExplicitVersion: (version: string) => semver.valid(version) !== null,
}));
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockGetLatestVersion = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockGetAllVersions = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockGetArtifact = jest.fn<any>();
jest.unstable_mockModule("../../src/download/manifest", () => ({
getAllVersions: mockGetAllVersions,
getArtifact: mockGetArtifact,
getLatestVersion: mockGetLatestVersion,
}));
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockValidateChecksum = jest.fn<any>();
jest.unstable_mockModule("../../src/download/checksum/checksum", () => ({
validateChecksum: mockValidateChecksum,
}));
const { downloadVersion, resolveVersion, rewriteToMirror } = await import(
"../../src/download/download-version"
);
describe("download-version", () => {
beforeEach(() => {
mockInfo.mockReset();
mockWarning.mockReset();
mockDownloadTool.mockReset();
mockExtractTar.mockReset();
mockExtractZip.mockReset();
mockCacheDir.mockReset();
mockGetLatestVersion.mockReset();
mockGetAllVersions.mockReset();
mockGetArtifact.mockReset();
mockValidateChecksum.mockReset();
mockDownloadTool.mockResolvedValue("/tmp/downloaded");
mockExtractTar.mockResolvedValue("/tmp/extracted");
mockExtractZip.mockResolvedValue("/tmp/extracted");
mockCacheDir.mockResolvedValue("/tmp/cached");
});
describe("resolveVersion", () => {
it("uses the default manifest to resolve latest", async () => {
mockGetLatestVersion.mockResolvedValue("0.9.26");
const version = await resolveVersion("latest", undefined);
expect(version).toBe("0.9.26");
expect(mockGetLatestVersion).toHaveBeenCalledTimes(1);
expect(mockGetLatestVersion).toHaveBeenCalledWith(undefined);
});
it("uses the default manifest to resolve available versions", async () => {
mockGetAllVersions.mockResolvedValue(["0.9.26", "0.9.25"]);
const version = await resolveVersion("^0.9.0", undefined);
expect(version).toBe("0.9.26");
expect(mockGetAllVersions).toHaveBeenCalledTimes(1);
expect(mockGetAllVersions).toHaveBeenCalledWith(undefined);
});
it("uses manifest-file when provided", async () => {
mockGetAllVersions.mockResolvedValue(["0.9.26", "0.9.25"]);
const version = await resolveVersion(
"^0.9.0",
"https://example.com/custom.ndjson",
);
expect(version).toBe("0.9.26");
expect(mockGetAllVersions).toHaveBeenCalledWith(
"https://example.com/custom.ndjson",
);
});
});
describe("downloadVersion", () => {
it("fails when manifest lookup fails", async () => {
mockGetArtifact.mockRejectedValue(new Error("manifest unavailable"));
await expect(
downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
undefined,
"token",
),
).rejects.toThrow("manifest unavailable");
expect(mockDownloadTool).not.toHaveBeenCalled();
expect(mockValidateChecksum).not.toHaveBeenCalled();
});
it("fails when no matching artifact exists in the default manifest", async () => {
mockGetArtifact.mockResolvedValue(undefined);
await expect(
downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
undefined,
"token",
),
).rejects.toThrow(
"Could not find artifact for version 0.9.26, arch x86_64, platform unknown-linux-gnu in https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson .",
);
expect(mockDownloadTool).not.toHaveBeenCalled();
expect(mockValidateChecksum).not.toHaveBeenCalled();
});
it("uses built-in checksums for default manifest downloads", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "manifest-checksum-that-should-be-ignored",
downloadUrl: "https://example.com/uv.tar.gz",
});
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
undefined,
"token",
);
expect(mockValidateChecksum).toHaveBeenCalledWith(
undefined,
"/tmp/downloaded",
"x86_64",
"unknown-linux-gnu",
"0.9.26",
);
});
it("rewrites GitHub Releases URLs to the Astral mirror", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "abc123",
downloadUrl:
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
});
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
undefined,
"token",
);
expect(mockDownloadTool).toHaveBeenCalledWith(
"https://releases.astral.sh/github/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
undefined,
undefined,
);
});
it("does not rewrite non-GitHub URLs", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "abc123",
downloadUrl: "https://example.com/uv.tar.gz",
});
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
undefined,
"token",
);
expect(mockDownloadTool).toHaveBeenCalledWith(
"https://example.com/uv.tar.gz",
undefined,
"token",
);
});
it("falls back to GitHub Releases when the mirror fails", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "abc123",
downloadUrl:
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
});
mockDownloadTool
.mockRejectedValueOnce(new Error("mirror unavailable"))
.mockResolvedValueOnce("/tmp/downloaded");
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
undefined,
"token",
);
expect(mockDownloadTool).toHaveBeenCalledTimes(2);
expect(mockDownloadTool).toHaveBeenNthCalledWith(
1,
"https://releases.astral.sh/github/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
undefined,
undefined,
);
expect(mockDownloadTool).toHaveBeenNthCalledWith(
2,
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
undefined,
"token",
);
expect(mockWarning).toHaveBeenCalledWith(
"Failed to download from mirror, falling back to GitHub Releases: mirror unavailable",
);
});
it("does not fall back for non-GitHub URLs", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "abc123",
downloadUrl: "https://example.com/uv.tar.gz",
});
mockDownloadTool.mockRejectedValue(new Error("download failed"));
await expect(
downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
undefined,
"token",
),
).rejects.toThrow("download failed");
expect(mockDownloadTool).toHaveBeenCalledTimes(1);
});
it("uses manifest-file checksum metadata when checksum input is unset", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "manifest-checksum",
downloadUrl: "https://example.com/custom-uv.tar.gz",
});
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
"",
"token",
"https://example.com/custom.ndjson",
);
expect(mockValidateChecksum).toHaveBeenCalledWith(
"manifest-checksum",
"/tmp/downloaded",
"x86_64",
"unknown-linux-gnu",
"0.9.26",
);
});
it("prefers checksum input over manifest-file checksum metadata", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "manifest-checksum",
downloadUrl: "https://example.com/custom-uv.tar.gz",
});
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
"user-checksum",
"token",
"https://example.com/custom.ndjson",
);
expect(mockValidateChecksum).toHaveBeenCalledWith(
"user-checksum",
"/tmp/downloaded",
"x86_64",
"unknown-linux-gnu",
"0.9.26",
);
});
});
describe("rewriteToMirror", () => {
it("rewrites a GitHub Releases URL to the Astral mirror", () => {
expect(
rewriteToMirror(
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
),
).toBe(
"https://releases.astral.sh/github/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
);
});
it("returns undefined for non-GitHub URLs", () => {
expect(rewriteToMirror("https://example.com/uv.tar.gz")).toBeUndefined();
});
it("returns undefined for a different GitHub repo", () => {
expect(
rewriteToMirror(
"https://github.com/other/repo/releases/download/v1.0/file.tar.gz",
),
).toBeUndefined();
});
});
});

View file

@ -1,180 +0,0 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockFetch = jest.fn<any>();
jest.unstable_mockModule("@actions/core", () => ({
debug: jest.fn(),
info: jest.fn(),
}));
jest.unstable_mockModule("../../src/utils/fetch", () => ({
fetch: mockFetch,
}));
const {
clearManifestCache,
fetchManifest,
getAllVersions,
getArtifact,
getLatestVersion,
parseManifest,
} = await import("../../src/download/manifest");
const sampleManifestResponse = `{"version":"0.9.26","artifacts":[{"platform":"aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-aarch64-apple-darwin.tar.gz","archive_format":"tar.gz","sha256":"fcf0a9ea6599c6ae28a4c854ac6da76f2c889354d7c36ce136ef071f7ab9721f"},{"platform":"x86_64-pc-windows-msvc","variant":"default","url":"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-pc-windows-msvc.zip","archive_format":"zip","sha256":"eb02fd95d8e0eed462b4a67ecdd320d865b38c560bffcda9a0b87ec944bdf036"}]}
{"version":"0.9.25","artifacts":[{"platform":"aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/uv/releases/download/0.9.25/uv-aarch64-apple-darwin.tar.gz","archive_format":"tar.gz","sha256":"606b3c6949d971709f2526fa0d9f0fd23ccf60e09f117999b406b424af18a6a6"}]}`;
const multiVariantManifestResponse = `{"version":"0.9.26","artifacts":[{"platform":"aarch64-apple-darwin","variant":"python-managed","url":"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-aarch64-apple-darwin-managed.tar.gz","archive_format":"tar.gz","sha256":"managed-checksum"},{"platform":"aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-aarch64-apple-darwin.zip","archive_format":"zip","sha256":"default-checksum"}]}`;
function createMockResponse(
ok: boolean,
status: number,
statusText: string,
data: string,
) {
return {
ok,
status,
statusText,
text: async () => data,
};
}
describe("manifest", () => {
beforeEach(() => {
clearManifestCache();
mockFetch.mockReset();
});
describe("fetchManifest", () => {
it("fetches and parses manifest data", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleManifestResponse),
);
const versions = await fetchManifest();
expect(versions).toHaveLength(2);
expect(versions[0]?.version).toBe("0.9.26");
expect(versions[1]?.version).toBe("0.9.25");
});
it("throws on a failed fetch", async () => {
mockFetch.mockResolvedValue(
createMockResponse(false, 500, "Internal Server Error", ""),
);
await expect(fetchManifest()).rejects.toThrow(
"Failed to fetch manifest data: 500 Internal Server Error",
);
});
it("caches results per URL", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleManifestResponse),
);
await fetchManifest("https://example.com/custom.ndjson");
await fetchManifest("https://example.com/custom.ndjson");
expect(mockFetch).toHaveBeenCalledTimes(1);
});
});
describe("getAllVersions", () => {
it("returns all version strings", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleManifestResponse),
);
const versions = await getAllVersions(
"https://example.com/custom.ndjson",
);
expect(versions).toEqual(["0.9.26", "0.9.25"]);
});
});
describe("getLatestVersion", () => {
it("returns the first version string", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleManifestResponse),
);
await expect(
getLatestVersion("https://example.com/custom.ndjson"),
).resolves.toBe("0.9.26");
});
});
describe("getArtifact", () => {
beforeEach(() => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleManifestResponse),
);
});
it("finds an artifact by version and platform", async () => {
const artifact = await getArtifact("0.9.26", "aarch64", "apple-darwin");
expect(artifact).toEqual({
archiveFormat: "tar.gz",
checksum:
"fcf0a9ea6599c6ae28a4c854ac6da76f2c889354d7c36ce136ef071f7ab9721f",
downloadUrl:
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-aarch64-apple-darwin.tar.gz",
});
});
it("finds a windows artifact", async () => {
const artifact = await getArtifact("0.9.26", "x86_64", "pc-windows-msvc");
expect(artifact).toEqual({
archiveFormat: "zip",
checksum:
"eb02fd95d8e0eed462b4a67ecdd320d865b38c560bffcda9a0b87ec944bdf036",
downloadUrl:
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-pc-windows-msvc.zip",
});
});
it("prefers the default variant when multiple artifacts share a platform", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", multiVariantManifestResponse),
);
const artifact = await getArtifact("0.9.26", "aarch64", "apple-darwin");
expect(artifact).toEqual({
archiveFormat: "zip",
checksum: "default-checksum",
downloadUrl:
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-aarch64-apple-darwin.zip",
});
});
it("returns undefined for an unknown version", async () => {
const artifact = await getArtifact("0.0.1", "aarch64", "apple-darwin");
expect(artifact).toBeUndefined();
});
it("returns undefined for an unknown platform", async () => {
const artifact = await getArtifact(
"0.9.26",
"aarch64",
"unknown-linux-musl",
);
expect(artifact).toBeUndefined();
});
});
describe("parseManifest", () => {
it("throws for malformed manifest data", () => {
expect(() => parseManifest('{"version":"0.1.0"', "test-source")).toThrow(
"Failed to parse manifest data from test-source",
);
});
});
});

View file

@ -1,6 +1,14 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
jest.mock("@actions/core", () => {
return {
debug: jest.fn(),
getBooleanInput: jest.fn(
(name: string) => (mockInputs[name] ?? "") === "true",
),
getInput: jest.fn((name: string) => mockInputs[name] ?? ""),
warning: jest.fn(),
};
});
import {
afterEach,
beforeEach,
@ -10,244 +18,120 @@ import {
jest,
} from "@jest/globals";
// Will be mutated per test before (re-)importing the module under test
let mockInputs: Record<string, string> = {};
const tempDirs: string[] = [];
const ORIGINAL_HOME = process.env.HOME;
const ORIGINAL_RUNNER_ENVIRONMENT = process.env.RUNNER_ENVIRONMENT;
const ORIGINAL_RUNNER_TEMP = process.env.RUNNER_TEMP;
const ORIGINAL_UV_CACHE_DIR = process.env.UV_CACHE_DIR;
const ORIGINAL_UV_PYTHON_INSTALL_DIR = process.env.UV_PYTHON_INSTALL_DIR;
const mockDebug = jest.fn();
const mockGetBooleanInput = jest.fn(
(name: string) => (mockInputs[name] ?? "") === "true",
);
const mockGetInput = jest.fn((name: string) => mockInputs[name] ?? "");
const mockInfo = jest.fn();
const mockWarning = jest.fn();
jest.unstable_mockModule("@actions/core", () => ({
debug: mockDebug,
getBooleanInput: mockGetBooleanInput,
getInput: mockGetInput,
info: mockInfo,
warning: mockWarning,
}));
const { CacheLocalSource, loadInputs } = await import("../../src/utils/inputs");
function createTempProject(files: Record<string, string> = {}): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "setup-uv-inputs-test-"));
tempDirs.push(dir);
for (const [relativePath, content] of Object.entries(files)) {
const filePath = path.join(dir, relativePath);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content);
}
return dir;
}
function resetEnvironment(): void {
jest.clearAllMocks();
mockInputs = {};
process.env.HOME = "/home/testuser";
delete process.env.RUNNER_ENVIRONMENT;
delete process.env.RUNNER_TEMP;
delete process.env.UV_CACHE_DIR;
delete process.env.UV_PYTHON_INSTALL_DIR;
}
function restoreEnvironment(): void {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { force: true, recursive: true });
}
process.env.HOME = ORIGINAL_HOME;
process.env.RUNNER_ENVIRONMENT = ORIGINAL_RUNNER_ENVIRONMENT;
process.env.RUNNER_TEMP = ORIGINAL_RUNNER_TEMP;
process.env.UV_CACHE_DIR = ORIGINAL_UV_CACHE_DIR;
process.env.UV_PYTHON_INSTALL_DIR = ORIGINAL_UV_PYTHON_INSTALL_DIR;
}
beforeEach(resetEnvironment);
afterEach(restoreEnvironment);
describe("loadInputs", () => {
it("loads defaults for a github-hosted runner", () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["enable-cache"] = "auto";
process.env.RUNNER_ENVIRONMENT = "github-hosted";
process.env.RUNNER_TEMP = "/runner-temp";
const inputs = loadInputs();
expect(inputs.enableCache).toBe(true);
expect(inputs.cacheLocalPath).toEqual({
path: "/runner-temp/setup-uv-cache",
source: CacheLocalSource.Default,
});
expect(inputs.pythonDir).toBe("/runner-temp/uv-python-dir");
expect(inputs.venvPath).toBe("/workspace/.venv");
expect(inputs.manifestFile).toBeUndefined();
expect(inputs.resolutionStrategy).toBe("highest");
});
it("uses cache-dir from pyproject.toml when present", () => {
mockInputs["working-directory"] = createTempProject({
"pyproject.toml": `[project]
name = "uv-project"
version = "0.1.0"
[tool.uv]
cache-dir = "/tmp/pyproject-toml-defined-cache-path"
`,
});
const inputs = loadInputs();
expect(inputs.cacheLocalPath).toEqual({
path: "/tmp/pyproject-toml-defined-cache-path",
source: CacheLocalSource.Config,
});
expect(mockInfo).toHaveBeenCalledWith(
expect.stringContaining("Found cache-dir in"),
);
});
it("uses UV_CACHE_DIR from the environment", () => {
mockInputs["working-directory"] = createTempProject();
process.env.UV_CACHE_DIR = "/env/cache-dir";
const inputs = loadInputs();
expect(inputs.cacheLocalPath).toEqual({
path: "/env/cache-dir",
source: CacheLocalSource.Env,
});
expect(mockInfo).toHaveBeenCalledWith(
"UV_CACHE_DIR is already set to /env/cache-dir",
);
});
it("uses UV_PYTHON_INSTALL_DIR from the environment", () => {
mockInputs["working-directory"] = "/workspace";
process.env.UV_PYTHON_INSTALL_DIR = "/env/python-dir";
const inputs = loadInputs();
expect(inputs.pythonDir).toBe("/env/python-dir");
expect(mockInfo).toHaveBeenCalledWith(
"UV_PYTHON_INSTALL_DIR is already set to /env/python-dir",
);
});
it("warns when parsing a malformed pyproject.toml for cache-dir", () => {
mockInputs["working-directory"] = createTempProject({
"pyproject.toml": `[project]
name = "malformed-pyproject-toml-project"
version = "0.1.0"
[malformed-toml
`,
});
const inputs = loadInputs();
expect(inputs.cacheLocalPath).toBeUndefined();
expect(mockWarning).toHaveBeenCalledWith(
expect.stringContaining("Error while parsing pyproject.toml:"),
);
});
it("throws for an invalid resolution strategy", () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["resolution-strategy"] = "middle";
expect(() => loadInputs()).toThrow(
"Invalid resolution-strategy: middle. Must be 'highest' or 'lowest'.",
);
});
});
describe("cacheDependencyGlob", () => {
it("returns empty string when input not provided", () => {
mockInputs["working-directory"] = "/workspace";
const inputs = loadInputs();
expect(inputs.cacheDependencyGlob).toBe("");
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
mockInputs = {};
process.env.HOME = "/home/testuser";
});
it.each([
["requirements.txt", "/workspace/requirements.txt"],
["./uv.lock", "/workspace/uv.lock"],
])("resolves %s to %s", (globInput, expected) => {
mockInputs["working-directory"] = "/workspace";
mockInputs["cache-dependency-glob"] = globInput;
const inputs = loadInputs();
expect(inputs.cacheDependencyGlob).toBe(expected);
afterEach(() => {
process.env.HOME = ORIGINAL_HOME;
});
it("handles multiple lines, trimming whitespace, tilde expansion and absolute paths", () => {
it("returns empty string when input not provided", async () => {
mockInputs["working-directory"] = "/workspace";
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
expect(cacheDependencyGlob).toBe("");
});
it("resolves a single relative path", async () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["cache-dependency-glob"] = "requirements.txt";
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
expect(cacheDependencyGlob).toBe("/workspace/requirements.txt");
});
it("strips leading ./ from relative path", async () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["cache-dependency-glob"] = "./uv.lock";
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
expect(cacheDependencyGlob).toBe("/workspace/uv.lock");
});
it("handles multiple lines, trimming whitespace, tilde expansion and absolute paths", async () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["cache-dependency-glob"] =
" ~/.cache/file1\n ./rel/file2 \nfile3.txt";
const inputs = loadInputs();
expect(inputs.cacheDependencyGlob).toBe(
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
expect(cacheDependencyGlob).toBe(
[
"/home/testuser/.cache/file1",
"/workspace/rel/file2",
"/workspace/file3.txt",
"/home/testuser/.cache/file1", // expanded tilde, absolute path unchanged
"/workspace/rel/file2", // ./ stripped and resolved
"/workspace/file3.txt", // relative path resolved
].join("\n"),
);
});
it.each([
[
"/abs/path.lock\nrelative.lock",
["/abs/path.lock", "/workspace/relative.lock"].join("\n"),
],
[
"!/abs/path.lock\n!relative.lock",
["!/abs/path.lock", "!/workspace/relative.lock"].join("\n"),
],
])("normalizes multiline glob %s", (globInput, expected) => {
it("keeps absolute path unchanged in multiline input", async () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["cache-dependency-glob"] = globInput;
mockInputs["cache-dependency-glob"] = "/abs/path.lock\nrelative.lock";
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
expect(cacheDependencyGlob).toBe(
["/abs/path.lock", "/workspace/relative.lock"].join("\n"),
);
});
const inputs = loadInputs();
expect(inputs.cacheDependencyGlob).toBe(expected);
it("handles exclusions in relative paths correct", async () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["cache-dependency-glob"] = "!/abs/path.lock\n!relative.lock";
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
expect(cacheDependencyGlob).toBe(
["!/abs/path.lock", "!/workspace/relative.lock"].join("\n"),
);
});
});
describe("tool directories", () => {
it("expands tilde for tool-bin-dir and tool-dir", () => {
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
mockInputs = {};
process.env.HOME = "/home/testuser";
});
afterEach(() => {
process.env.HOME = ORIGINAL_HOME;
});
it("expands tilde for tool-bin-dir and tool-dir", async () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["tool-bin-dir"] = "~/tool-bin-dir";
mockInputs["tool-dir"] = "~/tool-dir";
const inputs = loadInputs();
const { toolBinDir, toolDir } = await import("../../src/utils/inputs");
expect(inputs.toolBinDir).toBe("/home/testuser/tool-bin-dir");
expect(inputs.toolDir).toBe("/home/testuser/tool-dir");
expect(toolBinDir).toBe("/home/testuser/tool-bin-dir");
expect(toolDir).toBe("/home/testuser/tool-dir");
});
});
describe("cacheLocalPath", () => {
it("expands tilde in cache-local-path", () => {
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
mockInputs = {};
process.env.HOME = "/home/testuser";
});
afterEach(() => {
process.env.HOME = ORIGINAL_HOME;
});
it("expands tilde in cache-local-path", async () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["cache-local-path"] = "~/uv-cache/cache-local-path";
const inputs = loadInputs();
const { CacheLocalSource, cacheLocalPath } = await import(
"../../src/utils/inputs"
);
expect(inputs.cacheLocalPath).toEqual({
expect(cacheLocalPath).toEqual({
path: "/home/testuser/uv-cache/cache-local-path",
source: CacheLocalSource.Input,
});
@ -255,38 +139,71 @@ describe("cacheLocalPath", () => {
});
describe("venvPath", () => {
it("defaults to .venv in the working directory", () => {
mockInputs["working-directory"] = "/workspace";
const inputs = loadInputs();
expect(inputs.venvPath).toBe("/workspace/.venv");
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
mockInputs = {};
process.env.HOME = "/home/testuser";
});
it.each([
["custom-venv", "/workspace/custom-venv"],
["custom-venv/", "/workspace/custom-venv"],
["/tmp/custom-venv", "/tmp/custom-venv"],
["~/.venv", "/home/testuser/.venv"],
])("resolves venv-path %s to %s", (venvPathInput, expected) => {
afterEach(() => {
process.env.HOME = ORIGINAL_HOME;
});
it("defaults to .venv in the working directory", async () => {
mockInputs["working-directory"] = "/workspace";
const { venvPath } = await import("../../src/utils/inputs");
expect(venvPath).toBe("/workspace/.venv");
});
it("resolves a relative venv-path", async () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["activate-environment"] = "true";
mockInputs["venv-path"] = venvPathInput;
const inputs = loadInputs();
expect(inputs.venvPath).toBe(expected);
mockInputs["venv-path"] = "custom-venv";
const { venvPath } = await import("../../src/utils/inputs");
expect(venvPath).toBe("/workspace/custom-venv");
});
it("warns when venv-path is set but activate-environment is false", () => {
it("normalizes venv-path with trailing slash", async () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["activate-environment"] = "true";
mockInputs["venv-path"] = "custom-venv/";
const { venvPath } = await import("../../src/utils/inputs");
expect(venvPath).toBe("/workspace/custom-venv");
});
it("keeps an absolute venv-path unchanged", async () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["activate-environment"] = "true";
mockInputs["venv-path"] = "/tmp/custom-venv";
const { venvPath } = await import("../../src/utils/inputs");
expect(venvPath).toBe("/tmp/custom-venv");
});
it("expands tilde in venv-path", async () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["activate-environment"] = "true";
mockInputs["venv-path"] = "~/.venv";
const { venvPath } = await import("../../src/utils/inputs");
expect(venvPath).toBe("/home/testuser/.venv");
});
it("warns when venv-path is set but activate-environment is false", async () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["venv-path"] = "custom-venv";
const inputs = loadInputs();
const { activateEnvironment, venvPath } = await import(
"../../src/utils/inputs"
);
expect(inputs.activateEnvironment).toBe(false);
expect(inputs.venvPath).toBe("/workspace/custom-venv");
expect(mockWarning).toHaveBeenCalledWith(
expect(activateEnvironment).toBe(false);
expect(venvPath).toBe("/workspace/custom-venv");
const mockedCore = jest.requireMock("@actions/core") as {
warning: jest.Mock;
};
expect(mockedCore.warning).toHaveBeenCalledWith(
"venv-path is only used when activate-environment is true",
);
});

View file

@ -1,121 +1,113 @@
jest.mock("node:fs");
jest.mock("@actions/core", () => ({
warning: jest.fn(),
}));
import fs from "node:fs";
import * as core from "@actions/core";
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { getUvVersionFromToolVersions } from "../../src/version/tool-versions-file";
const mockReadFileSync = jest.fn();
const mockWarning = jest.fn();
jest.unstable_mockModule("node:fs", () => ({
default: {
readFileSync: mockReadFileSync,
},
}));
jest.unstable_mockModule("@actions/core", () => ({
warning: mockWarning,
}));
async function getVersionFromToolVersions(filePath: string) {
const { getUvVersionFromToolVersions } = await import(
"../../src/version/tool-versions-file"
);
return getUvVersionFromToolVersions(filePath);
}
const mockedFs = fs as jest.Mocked<typeof fs>;
const mockedCore = core as jest.Mocked<typeof core>;
describe("getUvVersionFromToolVersions", () => {
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
});
it("should return undefined for non-.tool-versions files", async () => {
const result = await getVersionFromToolVersions("package.json");
it("should return undefined for non-.tool-versions files", () => {
const result = getUvVersionFromToolVersions("package.json");
expect(result).toBeUndefined();
expect(mockReadFileSync).not.toHaveBeenCalled();
expect(mockedFs.readFileSync).not.toHaveBeenCalled();
});
it("should return version for valid uv entry", async () => {
it("should return version for valid uv entry", () => {
const fileContent = "python 3.11.0\nuv 0.1.0\nnodejs 18.0.0";
mockReadFileSync.mockReturnValue(fileContent);
mockedFs.readFileSync.mockReturnValue(fileContent);
const result = await getVersionFromToolVersions(".tool-versions");
const result = getUvVersionFromToolVersions(".tool-versions");
expect(result).toBe("0.1.0");
expect(mockReadFileSync).toHaveBeenCalledWith(".tool-versions", "utf8");
expect(mockedFs.readFileSync).toHaveBeenCalledWith(
".tool-versions",
"utf8",
);
});
it("should return version for uv entry with v prefix", async () => {
it("should return version for uv entry with v prefix", () => {
const fileContent = "uv v0.2.0";
mockReadFileSync.mockReturnValue(fileContent);
mockedFs.readFileSync.mockReturnValue(fileContent);
const result = await getVersionFromToolVersions(".tool-versions");
const result = getUvVersionFromToolVersions(".tool-versions");
expect(result).toBe("0.2.0");
});
it("should handle whitespace around uv entry", async () => {
it("should handle whitespace around uv entry", () => {
const fileContent = " uv 0.3.0 ";
mockReadFileSync.mockReturnValue(fileContent);
mockedFs.readFileSync.mockReturnValue(fileContent);
const result = await getVersionFromToolVersions(".tool-versions");
const result = getUvVersionFromToolVersions(".tool-versions");
expect(result).toBe("0.3.0");
});
it("should skip commented lines", async () => {
it("should skip commented lines", () => {
const fileContent = "# uv 0.1.0\npython 3.11.0\nuv 0.2.0";
mockReadFileSync.mockReturnValue(fileContent);
mockedFs.readFileSync.mockReturnValue(fileContent);
const result = await getVersionFromToolVersions(".tool-versions");
const result = getUvVersionFromToolVersions(".tool-versions");
expect(result).toBe("0.2.0");
});
it("should return first matching uv version", async () => {
it("should return first matching uv version", () => {
const fileContent = "uv 0.1.0\npython 3.11.0\nuv 0.2.0";
mockReadFileSync.mockReturnValue(fileContent);
mockedFs.readFileSync.mockReturnValue(fileContent);
const result = await getVersionFromToolVersions(".tool-versions");
const result = getUvVersionFromToolVersions(".tool-versions");
expect(result).toBe("0.1.0");
});
it("should return undefined when no uv entry found", async () => {
it("should return undefined when no uv entry found", () => {
const fileContent = "python 3.11.0\nnodejs 18.0.0";
mockReadFileSync.mockReturnValue(fileContent);
mockedFs.readFileSync.mockReturnValue(fileContent);
const result = await getVersionFromToolVersions(".tool-versions");
const result = getUvVersionFromToolVersions(".tool-versions");
expect(result).toBeUndefined();
});
it("should return undefined for empty file", async () => {
mockReadFileSync.mockReturnValue("");
it("should return undefined for empty file", () => {
mockedFs.readFileSync.mockReturnValue("");
const result = await getVersionFromToolVersions(".tool-versions");
const result = getUvVersionFromToolVersions(".tool-versions");
expect(result).toBeUndefined();
});
it("should warn and return undefined for ref syntax", async () => {
it("should warn and return undefined for ref syntax", () => {
const fileContent = "uv ref:main";
mockReadFileSync.mockReturnValue(fileContent);
mockedFs.readFileSync.mockReturnValue(fileContent);
const result = await getVersionFromToolVersions(".tool-versions");
const result = getUvVersionFromToolVersions(".tool-versions");
expect(result).toBeUndefined();
expect(mockWarning).toHaveBeenCalledWith(
expect(mockedCore.warning).toHaveBeenCalledWith(
"The ref syntax of .tool-versions is not supported. Please use a released version instead.",
);
});
it("should handle file path with .tool-versions extension", async () => {
it("should handle file path with .tool-versions extension", () => {
const fileContent = "uv 0.1.0";
mockReadFileSync.mockReturnValue(fileContent);
mockedFs.readFileSync.mockReturnValue(fileContent);
const result = await getVersionFromToolVersions("path/to/.tool-versions");
const result = getUvVersionFromToolVersions("path/to/.tool-versions");
expect(result).toBe("0.1.0");
expect(mockReadFileSync).toHaveBeenCalledWith(
expect(mockedFs.readFileSync).toHaveBeenCalledWith(
"path/to/.tool-versions",
"utf8",
);

View file

@ -26,7 +26,7 @@ inputs:
required: false
github-token:
description:
"Used when downloading uv from GitHub releases."
"Used to increase the rate limit when retrieving versions and downloading uv."
required: false
default: ${{ github.token }}
enable-cache:
@ -75,7 +75,7 @@ inputs:
description: "Custom path to set UV_TOOL_BIN_DIR to."
required: false
manifest-file:
description: "URL to a custom manifest file in the astral-sh/versions format."
description: "URL to the manifest file containing available versions and download URLs."
required: false
add-problem-matchers:
description: "Add problem matchers."
@ -102,8 +102,8 @@ outputs:
description: "A boolean value to indicate the Python cache entry was found"
runs:
using: "node24"
main: "dist/setup/index.cjs"
post: "dist/save-cache/index.cjs"
main: "dist/setup/index.js"
post: "dist/save-cache/index.js"
post-if: success()
branding:
icon: "package"

View file

@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.7/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.7/schema.json",
"assist": {
"actions": {
"source": {

63360
dist/save-cache/index.cjs generated vendored

File diff suppressed because one or more lines are too long

94318
dist/save-cache/index.js generated vendored Normal file

File diff suppressed because one or more lines are too long

97161
dist/setup/index.cjs generated vendored

File diff suppressed because one or more lines are too long

104601
dist/setup/index.js generated vendored Normal file

File diff suppressed because one or more lines are too long

49632
dist/update-known-checksums/index.cjs generated vendored

File diff suppressed because one or more lines are too long

39017
dist/update-known-versions/index.js generated vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -18,35 +18,41 @@ are automatically verified by this action. The sha256 hashes can be found on the
## Manifest file
By default, setup-uv reads version metadata from
[`astral-sh/versions`](https://github.com/astral-sh/versions).
The `manifest-file` input allows you to specify a JSON manifest that lists available uv versions,
architectures, and their download URLs. By default, this action uses the manifest file contained
in this repository, which is automatically updated with each release of uv.
The `manifest-file` input lets you override that source with your own URL, for example to test
custom uv builds or alternate download locations.
### Format
The manifest file must use the same format as `astral-sh/versions`: one JSON object per line, where each object represents a version and its artifacts. The versions must be sorted in descending order. For example:
The manifest file contains an array of objects, each describing a version,
architecture, platform, and the corresponding download URL. For example:
```json
{"version":"0.10.7","artifacts":[{"platform":"x86_64-unknown-linux-gnu","variant":"default","url":"https://example.com/uv-x86_64-unknown-linux-gnu.tar.gz","archive_format":"tar.gz","sha256":"..."}]}
{"version":"0.10.6","artifacts":[{"platform":"x86_64-unknown-linux-gnu","variant":"default","url":"https://example.com/uv-x86_64-unknown-linux-gnu.tar.gz","archive_format":"tar.gz","sha256":"..."}]}
[
{
"version": "0.7.13",
"artifactName": "uv-aarch64-apple-darwin.tar.gz",
"arch": "aarch64",
"platform": "apple-darwin",
"downloadUrl": "https://github.com/astral-sh/uv/releases/download/0.7.13/uv-aarch64-apple-darwin.tar.gz"
},
...
]
```
setup-uv currently only supports `default` as the `variant`.
The `archive_format` field is currently ignored.
You can supply a custom manifest file URL to define additional versions,
architectures, or different download URLs.
This is useful if you maintain your own uv builds or want to override the default sources.
```yaml
- name: Use a custom manifest file
uses: astral-sh/setup-uv@v7
with:
manifest-file: "https://example.com/my-custom-manifest.ndjson"
manifest-file: "https://example.com/my-custom-manifest.json"
```
> [!NOTE]
> When you use a custom manifest file and do not set the `version` input, setup-uv installs the
> latest version from that custom manifest.
> When you use a custom manifest file and do not set the `version` input, its default value is `latest`.
> This means the action will install the latest version available in the custom manifest file.
> This is different from the default behavior of installing the latest version from the official uv releases.
## Add problem matchers

View file

@ -38,12 +38,9 @@ You can customize the venv location with `venv-path`, for example to place it in
## GitHub authentication token
By default, this action resolves available uv versions from
[`astral-sh/versions`](https://github.com/astral-sh/versions), then downloads uv artifacts from
GitHub Releases.
You can provide a token via `github-token` to authenticate those downloads. By default, the
`GITHUB_TOKEN` secret is used, which is automatically provided by GitHub Actions.
This action uses the GitHub API to fetch the uv release artifacts. To avoid hitting the GitHub API
rate limit too quickly, an authentication token can be provided via the `github-token` input. By
default, the `GITHUB_TOKEN` secret is used, which is automatically provided by GitHub Actions.
If the default
[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)

9
jest.config.js Normal file
View file

@ -0,0 +1,9 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ["js", "ts"],
testMatch: ["**/*.test.ts"],
transform: {
"^.+\\.ts$": "ts-jest",
},
verbose: true,
};

View file

@ -1,14 +0,0 @@
import { createDefaultEsmPreset } from "ts-jest";
const esmPreset = createDefaultEsmPreset({
tsconfig: "./tsconfig.json",
});
export default {
...esmPreset,
clearMocks: true,
moduleFileExtensions: ["js", "mjs", "ts"],
testEnvironment: "node",
testMatch: ["**/*.test.ts"],
verbose: true,
};

4432
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,18 +2,16 @@
"name": "setup-uv",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Set up your GitHub Actions workflow with a specific version of uv",
"main": "dist/setup/index.cjs",
"main": "dist/index.js",
"scripts": {
"build": "tsc --noEmit",
"build": "tsc",
"check": "biome check --write",
"package": "node scripts/build-dist.mjs",
"test:unit": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
"test": "npm run build && npm run test:unit",
"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",
"test": "jest",
"act": "act pull_request -W .github/workflows/test.yml --container-architecture linux/amd64 -s GITHUB_TOKEN=\"$(gh auth token)\"",
"update-known-checksums": "RUNNER_TEMP=known_versions node dist/update-known-checksums/index.cjs src/download/checksum/known-checksums.ts",
"all": "npm run build && npm run check && npm run package && npm run test:unit"
"update-known-versions": "RUNNER_TEMP=known_versions node dist/update-known-versions/index.js src/download/checksum/known-versions.ts \"$(gh auth token)\"",
"all": "npm run build && npm run check && npm run package && npm test"
},
"repository": {
"type": "git",
@ -28,26 +26,28 @@
"author": "@eifinger",
"license": "MIT",
"dependencies": {
"@actions/cache": "^6.0.0",
"@actions/core": "^3.0.0",
"@actions/exec": "^3.0.0",
"@actions/glob": "^0.6.1",
"@actions/io": "^3.0.2",
"@actions/tool-cache": "^4.0.0",
"@renovatebot/pep440": "^4.2.2",
"smol-toml": "^1.6.0",
"undici": "^7.24.2"
"@actions/cache": "^4.1.0",
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/glob": "^0.5.0",
"@actions/io": "^1.1.3",
"@actions/tool-cache": "^2.0.2",
"@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": "^14.0.0",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"@renovatebot/pep440": "^4.2.1",
"smol-toml": "^1.4.2",
"undici": "5.28.5"
},
"devDependencies": {
"@biomejs/biome": "^2.4.7",
"@biomejs/biome": "2.3.7",
"@types/js-yaml": "^4.0.9",
"@types/node": "^25.5.0",
"@types/node": "^24.10.1",
"@types/semver": "^7.7.1",
"@vercel/ncc": "^0.38.4",
"esbuild": "^0.27.4",
"jest": "^30.3.0",
"js-yaml": "^4.1.1",
"ts-jest": "^29.4.6",
"jest": "^30.2.0",
"js-yaml": "^4.1.0",
"ts-jest": "^29.4.5",
"typescript": "^5.9.3"
}
}

View file

@ -1,33 +0,0 @@
import { rm } from "node:fs/promises";
import { build } from "esbuild";
const builds = [
{
entryPoints: ["src/setup-uv.ts"],
outfile: "dist/setup/index.cjs",
staleOutfiles: ["dist/setup/index.mjs"],
},
{
entryPoints: ["src/save-cache.ts"],
outfile: "dist/save-cache/index.cjs",
staleOutfiles: ["dist/save-cache/index.mjs"],
},
{
entryPoints: ["src/update-known-checksums.ts"],
outfile: "dist/update-known-checksums/index.cjs",
staleOutfiles: ["dist/update-known-checksums/index.mjs"],
},
];
for (const { staleOutfiles, ...options } of builds) {
await Promise.all(
staleOutfiles.map((outfile) => rm(outfile, { force: true })),
);
await build({
bundle: true,
format: "cjs",
platform: "node",
target: "node24",
...options,
});
}

View file

@ -1,7 +1,15 @@
import * as cache from "@actions/cache";
import * as core from "@actions/core";
import { hashFiles } from "../hash/hash-files";
import type { SetupInputs } from "../utils/inputs";
import {
cacheDependencyGlob,
cacheLocalPath,
cachePython,
cacheSuffix,
pruneCache,
pythonDir,
restoreCache as shouldRestoreCache,
} from "../utils/inputs";
import { getArch, getOSNameVersion, getPlatform } from "../utils/platforms";
export const STATE_CACHE_KEY = "cache-key";
@ -10,21 +18,18 @@ export const STATE_PYTHON_CACHE_MATCHED_KEY = "python-cache-matched-key";
const CACHE_VERSION = "2";
export async function restoreCache(
inputs: SetupInputs,
pythonVersion?: string,
): Promise<void> {
const cacheKey = await computeKeys(inputs, pythonVersion);
export async function restoreCache(pythonVersion?: string): Promise<void> {
const cacheKey = await computeKeys(pythonVersion);
core.saveState(STATE_CACHE_KEY, cacheKey);
core.setOutput("cache-key", cacheKey);
if (!inputs.restoreCache) {
if (!shouldRestoreCache) {
core.info("restore-cache is false. Skipping restore cache step.");
core.setOutput("python-cache-hit", false);
return;
}
if (inputs.cacheLocalPath === undefined) {
if (cacheLocalPath === undefined) {
throw new Error(
"cache-local-path is not set. Cannot restore cache without a valid cache path.",
);
@ -32,15 +37,15 @@ export async function restoreCache(
await restoreCacheFromKey(
cacheKey,
inputs.cacheLocalPath.path,
cacheLocalPath.path,
STATE_CACHE_MATCHED_KEY,
"cache-hit",
);
if (inputs.cachePython) {
if (cachePython) {
await restoreCacheFromKey(
`${cacheKey}-python`,
inputs.pythonDir,
pythonDir,
STATE_PYTHON_CACHE_MATCHED_KEY,
"python-cache-hit",
);
@ -71,34 +76,28 @@ async function restoreCacheFromKey(
handleMatchResult(matchedKey, cacheKey, stateKey, outputKey);
}
async function computeKeys(
inputs: SetupInputs,
pythonVersion?: string,
): Promise<string> {
async function computeKeys(pythonVersion?: string): Promise<string> {
let cacheDependencyPathHash = "-";
if (inputs.cacheDependencyGlob !== "") {
if (cacheDependencyGlob !== "") {
core.info(
`Searching files using cache dependency glob: ${inputs.cacheDependencyGlob.split("\n").join(",")}`,
);
cacheDependencyPathHash += await hashFiles(
inputs.cacheDependencyGlob,
true,
`Searching files using cache dependency glob: ${cacheDependencyGlob.split("\n").join(",")}`,
);
cacheDependencyPathHash += await hashFiles(cacheDependencyGlob, true);
if (cacheDependencyPathHash === "-") {
core.warning(
`No file matched to [${inputs.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.`,
`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.`,
);
}
}
if (cacheDependencyPathHash === "-") {
cacheDependencyPathHash = "-no-dependency-glob";
}
const suffix = inputs.cacheSuffix ? `-${inputs.cacheSuffix}` : "";
const suffix = cacheSuffix ? `-${cacheSuffix}` : "";
const version = pythonVersion ?? "unknown";
const platform = await getPlatform();
const osNameVersion = getOSNameVersion();
const pruned = inputs.pruneCache ? "-pruned" : "";
const python = inputs.cachePython ? "-py" : "";
const pruned = pruneCache ? "-pruned" : "";
const python = cachePython ? "-py" : "";
return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform}-${osNameVersion}-${version}${pruned}${python}${cacheDependencyPathHash}${suffix}`;
}

View file

@ -6,35 +6,33 @@ import type { Architecture, Platform } from "../../utils/platforms";
import { KNOWN_CHECKSUMS } from "./known-checksums";
export async function validateChecksum(
checksum: string | undefined,
checkSum: string | undefined,
downloadPath: string,
arch: Architecture,
platform: Platform,
version: string,
): Promise<void> {
const key = `${arch}-${platform}-${version}`;
const hasProvidedChecksum = checksum !== undefined && checksum !== "";
const checksumToUse = hasProvidedChecksum ? checksum : KNOWN_CHECKSUMS[key];
if (checksumToUse === undefined) {
core.debug(`No checksum found for ${key}.`);
return;
let isValid: boolean | undefined;
if (checkSum !== undefined && checkSum !== "") {
isValid = await validateFileCheckSum(downloadPath, checkSum);
} else {
core.debug("Checksum not provided. Checking known checksums.");
const key = `${arch}-${platform}-${version}`;
if (key in KNOWN_CHECKSUMS) {
const knownChecksum = KNOWN_CHECKSUMS[`${arch}-${platform}-${version}`];
core.debug(`Checking checksum for ${arch}-${platform}-${version}.`);
isValid = await validateFileCheckSum(downloadPath, knownChecksum);
} else {
core.debug(`No known checksum found for ${key}.`);
}
}
const checksumSource = hasProvidedChecksum
? "provided checksum"
: `KNOWN_CHECKSUMS entry for ${key}`;
core.debug(`Validating checksum using ${checksumSource}.`);
const isValid = await validateFileCheckSum(downloadPath, checksumToUse);
if (!isValid) {
throw new Error(
`Checksum for ${downloadPath} did not match ${checksumToUse}.`,
);
if (isValid === false) {
throw new Error(`Checksum for ${downloadPath} did not match ${checkSum}.`);
}
if (isValid === true) {
core.debug(`Checksum for ${downloadPath} is valid.`);
}
core.debug(`Checksum for ${downloadPath} is valid.`);
}
async function validateFileCheckSum(

View file

@ -1,315 +1,5 @@
// AUTOGENERATED_DO_NOT_EDIT
export const KNOWN_CHECKSUMS: { [key: string]: string } = {
"aarch64-apple-darwin-0.11.2":
"4beaa9550f93ef7f0fc02f7c28c9c48cd61fe30db00f5ac8947e0a425c3fb282",
"aarch64-pc-windows-msvc-0.11.2":
"ffdded8338205f53727b51d404563a5ac8eaa9aea53279a7b7c42177e11d478c",
"aarch64-unknown-linux-gnu-0.11.2":
"04792cac761c4a6ba78267f36f2af541b7f92196d42ac55d21d3ff6b0f5ab6a5",
"aarch64-unknown-linux-musl-0.11.2":
"275d91dd1f1955136591e7ec5e1fa21e84d0d37ead7da7c35c3683df748d9855",
"arm-unknown-linux-musleabihf-0.11.2":
"ce572dac1a8f9a92960f89e99351352fae068d34b24bed86fb88e75fd5dd67d9",
"armv7-unknown-linux-gnueabihf-0.11.2":
"3e90d7de9e3a4e2d8d1bd9ce164362fce22248474986e712039479fb6fd73136",
"armv7-unknown-linux-musleabihf-0.11.2":
"5222cdd7c7dd3263f8c243831606a9f01a1a07a40ffc3c26c03afb34491075c2",
"i686-pc-windows-msvc-0.11.2":
"506f8274b253b2386881a121f3b7d915b637019bda15876bbd1357235305cf12",
"i686-unknown-linux-gnu-0.11.2":
"c7ec378bab887443a70786382e58d76489da14a7e33b155915d648cca4bdb46c",
"i686-unknown-linux-musl-0.11.2":
"ade8714be45457899568c5b03ef885a0cc94476c07a0bdbe34531ba84231bab2",
"powerpc64le-unknown-linux-gnu-0.11.2":
"3f3a50e99364efc8ff7add10e79757a2b8458700a38180ec5f313524481b9fbc",
"riscv64gc-unknown-linux-gnu-0.11.2":
"e56a93f0ff21d6908461a6ecbf465beae19ae22719f900284abb7680bd07ec41",
"riscv64gc-unknown-linux-musl-0.11.2":
"4f263571bb457a16a31cb38fba4fcc9cf1059d1d32c5b2e54c43175fcd59205d",
"s390x-unknown-linux-gnu-0.11.2":
"42ebe40775f2a77a514fa47399fde86473bf35bd33b6896c6410a0309fc4d205",
"x86_64-apple-darwin-0.11.2":
"a9c3653245031304c50dd60ac0301bf6c112e12c38c32302a71d4fa6a63ba2cb",
"x86_64-pc-windows-msvc-0.11.2":
"171b7ccda1bbd562da6babeffcf533a1c6cc7862cf998da826e1db534fc43e48",
"x86_64-unknown-linux-gnu-0.11.2":
"7ac2ca0449c8d68dae9b99e635cd3bc9b22a4cb1de64b7c43716398447d42981",
"x86_64-unknown-linux-musl-0.11.2":
"4700d9fc75734247587deb3e25dd2c6c24f4ac69e8fe91d6acad4a6013115c06",
"aarch64-apple-darwin-0.11.1":
"f7815f739ed5d0e4202e6292acedb8659b9ae7de663d07188d8c6cbd7f96303f",
"aarch64-pc-windows-msvc-0.11.1":
"b789db0c1504dd3b02c090bd5783487497cc46cc2eb71754874cdd1ef59eb52a",
"aarch64-unknown-linux-gnu-0.11.1":
"1340e62da1ee3c1109764340e1247e8a1a232c30dde4a0f0548976dcaa90f06d",
"aarch64-unknown-linux-musl-0.11.1":
"bd04ffce77ee8d77f39823c13606183581847c2f5dcd704f2ea0f15e376b1a27",
"arm-unknown-linux-musleabihf-0.11.1":
"625c0e756e2374fce864ceaa6beedd5821e276e2b6307f2b719f2d62b449b89c",
"armv7-unknown-linux-gnueabihf-0.11.1":
"baf8daaab20b0502d1853dbfd916afb0762c024ae7f0df1c2deb2a1a1c1c3467",
"armv7-unknown-linux-musleabihf-0.11.1":
"684c25b74e83bcb1b177152379cfe2c974ba731aa5af278e1d161e41709f8bcf",
"i686-pc-windows-msvc-0.11.1":
"3c07858a08c54e4e5753239354c7b07ae69071b2b6f5aa2cc970e612adcb4740",
"i686-unknown-linux-gnu-0.11.1":
"6e83167c05708570563b10b6cc7e8c289daef5f51fde0b152e41af2a7ef70813",
"i686-unknown-linux-musl-0.11.1":
"b0d5152635c257fec76f95cb9268112b47ff70bd33a23866295a4f2ed9f46b7f",
"powerpc64le-unknown-linux-gnu-0.11.1":
"e42d2abfac46f57564789e2bfa6dbea4ae3135892e36ae066ba0ae77b69bb676",
"riscv64gc-unknown-linux-gnu-0.11.1":
"5e2c757b35dab015ad37f74ee3e060208390b5f4defb6684876f1be0664f3f6e",
"riscv64gc-unknown-linux-musl-0.11.1":
"6f590a824aed363cbec4079f7ddab87b5685119e0f5f0e71cd114c7b7c326199",
"s390x-unknown-linux-gnu-0.11.1":
"4208173c74e29572b799178709b5ed5828b24888659f944a4b47c0aaf78b42d2",
"x86_64-apple-darwin-0.11.1":
"2103670e8e949605e51926c7b953923ff6f6befbfb55aee928f5e760c9c910f8",
"x86_64-pc-windows-msvc-0.11.1":
"6659250cebbd3bb6ee48bcb21a3f0c6656450d63fb97f0f069bcb532bdb688ed",
"x86_64-unknown-linux-gnu-0.11.1":
"7c0c8069053e6e99e5911ff32b916be571f3419cd8e11bd28fb7da2c7dcaa553",
"x86_64-unknown-linux-musl-0.11.1":
"4e949471a95b37088a1ff1a585f69abed4d3cd3f921f50709a46b6ba62986d38",
"aarch64-apple-darwin-0.11.0":
"0c0f32c6a3473c5928aff96c3233715edfc79290e892f255cac93710cde7b91a",
"aarch64-pc-windows-msvc-0.11.0":
"95419e04a3ef5f13fb2a06bd6d787ba80a9d8981d6f097780e5a979817a2879d",
"aarch64-unknown-linux-gnu-0.11.0":
"8e179ca110343a17f801444ff9ef117dba56ef5fc9f6a4c9bb77b318ddba5f24",
"aarch64-unknown-linux-musl-0.11.0":
"658be4b8ec905635f1295468d4d5120d9e1ab1722eec9a104473ce993590babe",
"arm-unknown-linux-musleabihf-0.11.0":
"bfdcbd5fa41c8a9877a72c2b55a95da2bc79933885ef56c699b65bb2ed9cea91",
"armv7-unknown-linux-gnueabihf-0.11.0":
"0cad4e1b6769e48aa1e80cf639ddcc7c1bfe9ed017e95868fed185a8d818c949",
"armv7-unknown-linux-musleabihf-0.11.0":
"2aa9da83c6c0cf8a06bc9df14d51056284fa067ef5390b4db79998ff12f3bee7",
"i686-pc-windows-msvc-0.11.0":
"3b09d70e686087e096dbd8a2af21b922a2cac7d613dc053c3281c3ddbb961961",
"i686-unknown-linux-gnu-0.11.0":
"59928a0267501c20d9f9942f5f1d81a991ec55e29a19e002ae3d5c178c674c89",
"i686-unknown-linux-musl-0.11.0":
"1f438d6f6f851f0dabad3307ce7fd46541ecc5c42ebb664f382eb6c9a424a67d",
"powerpc64le-unknown-linux-gnu-0.11.0":
"29f17fb43595492b1a36cda57df7adad74183132df32799d32897268ff4e26dd",
"riscv64gc-unknown-linux-gnu-0.11.0":
"84ef37dda1003c5b65fa6c8f84242d35a7fcc84cc5ea9490d702edc36cad1f67",
"s390x-unknown-linux-gnu-0.11.0":
"b25be62f3b642348a2fece5c658624586661b8d1103891ab6903768b0529edc4",
"x86_64-apple-darwin-0.11.0":
"31aaec764166af8885cf99321fd6ed24fef80225a6f26ed1ae8ce04111688a7e",
"x86_64-pc-windows-msvc-0.11.0":
"e21d00b172df83531564a95e75a2bdc0c59b471dbb3515f0c1b4d6ef657dc451",
"x86_64-unknown-linux-gnu-0.11.0":
"cc0fbb42b3642125f600a55b0b095bea65cddaadb94c6ea2b6ba5d79c5825089",
"x86_64-unknown-linux-musl-0.11.0":
"bf6b0757c73d1726faa2a819b155d4d864919a95766720215d78fdcd09d42d26",
"aarch64-apple-darwin-0.10.12":
"ae738b5661a900579ec621d3918c0ef17bdec0da2a8a6d8b161137cd15f25414",
"aarch64-pc-windows-msvc-0.10.12":
"e79881e2c4f98a0f3a37b8770bf224e8fee70f6dcf8fc17055d8291bb1b0b867",
"aarch64-unknown-linux-gnu-0.10.12":
"0ed7d20f49f6b9b60d45fdfcac28f3ac01a671a6ef08672401ed2833423fea2a",
"aarch64-unknown-linux-musl-0.10.12":
"55bd1c1c10ec8b95a8c184f5e18b566703c6ab105f0fc118aaa4d748aabf28e4",
"arm-unknown-linux-musleabihf-0.10.12":
"9714e5059b05110a1c7ddbc18c971c13e0260e10551b7b77d82cbf907a4ebd9b",
"armv7-unknown-linux-gnueabihf-0.10.12":
"eaa02f36d5112029601b18ac3d1a0c03a83bb20cb4154c2f5345f777fa6c4101",
"armv7-unknown-linux-musleabihf-0.10.12":
"bd735652298c6e62cdd2ac939babe176a3356613e6803baa33d0bc10e8d9e4ed",
"i686-pc-windows-msvc-0.10.12":
"2312e75b9c77befdc1bff30da18f16df03083452852952553bee91da362c1a1d",
"i686-unknown-linux-gnu-0.10.12":
"8501844b34e3a28cfbba5a4b857eebd696d952e0bb4160357451ad80f3f49db8",
"i686-unknown-linux-musl-0.10.12":
"56cad78abcf5b710d2f7b9f774fcfd6bbed340d2aa9d9fc9e3b515542ec5e953",
"powerpc64le-unknown-linux-gnu-0.10.12":
"3c8017d9112221c83f43e8a15a58099663c0b2bdeabc8b43bb800413dfa21218",
"riscv64gc-unknown-linux-gnu-0.10.12":
"b1ca482b6b5dd7bf6ab733a3695cb0ab5b8e992ca96527efae93aa78fcc52a9b",
"s390x-unknown-linux-gnu-0.10.12":
"e1a0345eefe6fd3300948cd6f18aab092f9b88a243782113e645ce96530a6693",
"x86_64-apple-darwin-0.10.12":
"17443e293f2ae407bb2d8d34b875ebfe0ae01cf1296de5647e69e7b2e2b428f0",
"x86_64-pc-windows-msvc-0.10.12":
"4c1d55501869b3330d4aabf45ad6024ce2367e0f3af83344395702d272c22e88",
"x86_64-unknown-linux-gnu-0.10.12":
"ec72570c9d1f33021aa80b176d7baba390de2cfeb1abcbefca346d563bf17484",
"x86_64-unknown-linux-musl-0.10.12":
"adccf40b5d1939a5e0093081ec2307ea24235adf7c2d96b122c561fa37711c46",
"aarch64-apple-darwin-0.10.11":
"437a7d498dd6564d5bf986074249ba1fc600e73da55ae04d7bd4c24d5f149b95",
"aarch64-pc-windows-msvc-0.10.11":
"6a3eec4105c775dd87c11ef8ec41564648273751ff807c8955c24ddbcc636d03",
"aarch64-unknown-linux-gnu-0.10.11":
"23003df007937dd607409c8ddf010baa82bad2673e60e254632ca5b04edcce13",
"aarch64-unknown-linux-musl-0.10.11":
"5d80a7f6343d2676dfde1e5126582070a2bbc62df6f60d5527a169be3788532a",
"arm-unknown-linux-musleabihf-0.10.11":
"d3c248497c450d22a39c1d43a4a358c0c852e6056f5f49be96495eea41afb96c",
"armv7-unknown-linux-gnueabihf-0.10.11":
"7895a6470dfba051af4e74253599482fc0b37141b5d229956b383365e1a22902",
"armv7-unknown-linux-musleabihf-0.10.11":
"d2880c08acfdaef0985488972c8b14969f7139c27545046e2f6202f0e0f4d9d8",
"i686-pc-windows-msvc-0.10.11":
"c17f3dc3b2c47490057f17a1f0c37270f11a7b7cedf9bf2c0f841ce02bc7001b",
"i686-unknown-linux-gnu-0.10.11":
"1ab69ff7dd104a902731758ee05b782dfd9bdb263384e61650de638f33f586df",
"i686-unknown-linux-musl-0.10.11":
"cffb80d303fc1655e259d0b769c489f452e97425a6b6d3393d766413783a1d8c",
"powerpc64le-unknown-linux-gnu-0.10.11":
"ddc6a20670e60219e947b1b04813be80d7e9f4c4a0234231c8ed9298eec04aa6",
"riscv64gc-unknown-linux-gnu-0.10.11":
"c0719473cf5f8b475e917b8dfef6ae5d876b86a00a82ef91e47a02f561399f4f",
"s390x-unknown-linux-gnu-0.10.11":
"305ee734c585918515a22fe43b7cf253c38d468771373a0c02364d67498e07b2",
"x86_64-apple-darwin-0.10.11":
"ff90020b554cf02ef8008535c9aab6ef27bb7be6b075359300dec79c361df897",
"x86_64-pc-windows-msvc-0.10.11":
"9ee74df98582f37fdd6069e1caac80d2616f9a489f5dbb2b1c152f30be69c58e",
"x86_64-unknown-linux-gnu-0.10.11":
"5a360b0de092ddf4131f5313d0411b48c4e95e8107e40c3f8f2e9fcb636b3583",
"x86_64-unknown-linux-musl-0.10.11":
"d78246139dc6cf3ed6d03c84da762686bced7ad1de67977ee372a45b95a1f6d0",
"aarch64-apple-darwin-0.10.10":
"8a09f0ef51ee7f7170731b4cb8bde5bf9ba6da5304f49a7df6cdab42a1f37b5d",
"aarch64-pc-windows-msvc-0.10.10":
"2c6fe113f14574bc27f085751c68d3485589fcc3c3c64ed85dd1eecc2f87cffc",
"aarch64-unknown-linux-gnu-0.10.10":
"2b80457b950deda12e8d5dc3b9b7494ac143eae47f1fb11b1c6e5a8495a6421e",
"aarch64-unknown-linux-musl-0.10.10":
"d08c08b82cdcaf2bd3d928ffe844d3558dda53f90066db6ef9174157cc763252",
"arm-unknown-linux-musleabihf-0.10.10":
"ccc3c4dd5eeea4b2be829ef9bc0b8d9882389c0f303f7ec5ba668065d57e2673",
"armv7-unknown-linux-gnueabihf-0.10.10":
"032786622b52f8d0232b5ad16e25342a64f9e43576652db7bf607231021902f3",
"armv7-unknown-linux-musleabihf-0.10.10":
"f6f67b190eb28b473917c97210f89fd11d9b9393d774acd093ea738fcee68864",
"i686-pc-windows-msvc-0.10.10":
"980d7ea368cc4883f572bb85c285a647eddfc23539064d2bfaf8fbfefcc2112b",
"i686-unknown-linux-gnu-0.10.10":
"5260fbef838f8cfec44697064a5cfae08a27c6ab7ed7feab7fc946827e896952",
"i686-unknown-linux-musl-0.10.10":
"a6683ade964f8d8623098ca0c96b4311d8388b44a56a386cd795974f39fb5bd2",
"powerpc64le-unknown-linux-gnu-0.10.10":
"78939dc4fc905aca8af4be19b6c6ecc306f04c6ca9f98d144372595d9397fd0d",
"riscv64gc-unknown-linux-gnu-0.10.10":
"5eff670bf80fce9d9e50df5b4d46c415a9c0324eadf7059d97c76f89ffc33c3f",
"s390x-unknown-linux-gnu-0.10.10":
"a32d2be5600f7f42f82596ffe9d3115f020974ca7fb4f15251c5625c5481ea5e",
"x86_64-apple-darwin-0.10.10":
"dd18420591d625f9b4ca2b57a7a6fe3cce43910f02e02d90e47a4101428de14a",
"x86_64-pc-windows-msvc-0.10.10":
"d31a30f1dfb96e630a08d5a9b3f3f551254b7ed6e9b7e495f46a4232661c7252",
"x86_64-unknown-linux-gnu-0.10.10":
"3e1027f26ce8c7e4c32e2277a7fed2cb410f2f1f9320d3df97653d40e21f415b",
"x86_64-unknown-linux-musl-0.10.10":
"74544e8755fbc27559e22e29fd561bdc48f91b8bd8323e760a1130f32433bea4",
"aarch64-apple-darwin-0.10.9":
"a92f61e9ac9b0f29668c15f56152e4a60143fca148ff5bfadb86718472c3f376",
"aarch64-pc-windows-msvc-0.10.9":
"5c2526844acf978eab784161c21604343141aa6c9ed22c237ae2f315648f049d",
"aarch64-unknown-linux-gnu-0.10.9":
"cc0c5a8573e7d6d78aecb954e0a62b5c0d18217bb81f1e19363b428c57a9962a",
"aarch64-unknown-linux-musl-0.10.9":
"05b0d3087e913ebe11756365a90dd47c05d6728752fdbe129ad4c3ccd769826d",
"arm-unknown-linux-musleabihf-0.10.9":
"6220fa3eb5f8212cae4ec3a5053060914aaa829549cf706dde9f9cc344f75f61",
"armv7-unknown-linux-gnueabihf-0.10.9":
"0076eac165c2f7129627e2297478e7ffbb9465d9ae6a8961b2f53dcbd807473d",
"armv7-unknown-linux-musleabihf-0.10.9":
"f702e821b80e371e14987a886d58ee103c5948b7b096fa49a552624c24d7e073",
"i686-pc-windows-msvc-0.10.9":
"034bf6b91390b9adc5f41a5946fdb618ebc8cef1574f3d95af9c12fe2bf9aaf3",
"i686-unknown-linux-gnu-0.10.9":
"90d9168a4e7900463f9fd79a32eb1890081fb1e238d803404f6e17b2dcdcca7b",
"i686-unknown-linux-musl-0.10.9":
"1d42b0d0a037b3d658b11ec889154686db3ab269ba2b789bdbc45d36e3549f34",
"powerpc64le-unknown-linux-gnu-0.10.9":
"e804f4a7d0659e09ef806365f04bdd33c940603fab903e925402748d05dd109a",
"riscv64gc-unknown-linux-gnu-0.10.9":
"1541596da45855e34202130027a613a2ace7d441e04d747cb4dd9f2590461c9a",
"s390x-unknown-linux-gnu-0.10.9":
"a589d4a8930c82fa7225daec19c632651b3c84f50f770efe758056b387e5f0dd",
"x86_64-apple-darwin-0.10.9":
"9cc2de7d195fa157f98b306a8a1cb151ded93f488939b93363cebc8b9d598c28",
"x86_64-pc-windows-msvc-0.10.9":
"f58dc40896000229db7c52b8bdd931394040ef2ad59abd1eda841f6d70b13d7a",
"x86_64-unknown-linux-gnu-0.10.9":
"20d79708222611fa540b5c9ed84f352bcd3937740e51aacc0f8b15b271c57594",
"x86_64-unknown-linux-musl-0.10.9":
"433e56874739e92c7cfd661ba9e5f287b376ca612c08c8194a41a98a13158aea",
"aarch64-apple-darwin-0.10.8":
"c3a6fff5b6b4abddff863117878194e35dbc6b0267d61ad259ab9896f9b8dcbb",
"aarch64-pc-windows-msvc-0.10.8":
"20db25dc446f9a75d1cfde0a5f4b021e1b2eb266e600a610d32c7ca5d7ff83bf",
"aarch64-unknown-linux-gnu-0.10.8":
"661860e954f87dcd823251191866af3486484d1a9df60eed56f4586ed7559e3d",
"aarch64-unknown-linux-musl-0.10.8":
"2ef0d0489e9e2a32f134ca80097fa36be4b486c4ab004706a1d6d0d57980ff07",
"arm-unknown-linux-musleabihf-0.10.8":
"f6dfca333c566024f6feaef19adf7ce06675a1bc2fcadc2de640dd805112a518",
"armv7-unknown-linux-gnueabihf-0.10.8":
"1bee8f88a7129f7922c43b0e091a7065d4e13a2934e599aa8a48f162cf9739aa",
"armv7-unknown-linux-musleabihf-0.10.8":
"ad0ca78991518fde1c4c42f8590e86f29db1f746cedb637f9dac1bb7de2e28da",
"i686-pc-windows-msvc-0.10.8":
"db40952a0c16eb647cb3a06c8cc13712b72e5b6a2501bc080c7e00c0f0e4ad88",
"i686-unknown-linux-gnu-0.10.8":
"3a78c54ffedce8eafd59a19a32eaec538924169fa4bf9d28d2d5841a7f604210",
"i686-unknown-linux-musl-0.10.8":
"25cf70c12abded06c4c18db8fdba253776bc115ce28f849af6f6ef771e67d730",
"powerpc64le-unknown-linux-gnu-0.10.8":
"3a4a158e645d04825872eb59ca60dd5026529e4f9fe5dd88987a45478301724d",
"riscv64gc-unknown-linux-gnu-0.10.8":
"2349e786d2de14fbd72386f42ed9f398cad52f47f6cdd78e05f338a1faf1321c",
"s390x-unknown-linux-gnu-0.10.8":
"21de0f86838b06e6ebcc3cb6a079d49d3d3886e5b49822ae58e5758eb08a6710",
"x86_64-apple-darwin-0.10.8":
"e0a1b22b039f8155765f5bc8c13df03a5f994a901901179791572e8e5f053281",
"x86_64-pc-windows-msvc-0.10.8":
"2e70ecd22196cbd9d14eefb700814bcafc5b75a0d8275b52e8402e5fe256d928",
"x86_64-unknown-linux-gnu-0.10.8":
"f0c566b55683395a62fefb9261a060fa09824914b5682c3b9629fa154762ae2f",
"x86_64-unknown-linux-musl-0.10.8":
"a4e6ad1aecac61077de548d2cc9ccf2c2f1848863312b3b59fb0d2eb8d8a043c",
"aarch64-apple-darwin-0.10.7":
"1eb4dcc5e0fc8669fa0b33cf1151b64ba3b8c26b60dceff4f7a686129e2af22b",
"aarch64-pc-windows-msvc-0.10.7":
"45ba7b72a7435343d650c73d21d65d2e8bdda47f6bd39af00e37f3cb70aa79ef",
"aarch64-unknown-linux-gnu-0.10.7":
"20efc27d946860093650bcf26096a016b10fdaf03b13c33b75fbde02962beea9",
"aarch64-unknown-linux-musl-0.10.7":
"115291f9943531a3b63db3a2eabda8b74b8da4831551679382cb309c9debd9f7",
"arm-unknown-linux-musleabihf-0.10.7":
"3ea331cd68f28235e13639d5400341a3893d0455f2473a74a9926b7d62cb739c",
"armv7-unknown-linux-gnueabihf-0.10.7":
"2e2f88cc5a7b49282c9aa05cfe03e3b8b0a044e90981062fbeb60a7aeba188ca",
"armv7-unknown-linux-musleabihf-0.10.7":
"27319e842d802c5c73be52f3774999d79d0f28f37984090998560fd925133375",
"i686-pc-windows-msvc-0.10.7":
"a7960473a473ee5907a55fccb8c645e24c1da7d39076aaef652b819e3a26a28b",
"i686-unknown-linux-gnu-0.10.7":
"1a22aa0d2268a9a6fb2e5f092ca3d1ef7c14f96c3b4fd546226814f376e59d73",
"i686-unknown-linux-musl-0.10.7":
"75c2cc60675fb6f846b394c3f7b51f77c08f0981abf5cfcb5e27cfbb2f5837e0",
"powerpc64le-unknown-linux-gnu-0.10.7":
"7398686962b966959c32e7fbfd2868fbac38491ff0d86033d7c8bbb826a04026",
"riscv64gc-unknown-linux-gnu-0.10.7":
"39abc60403fdcf5c681b63c967059d42aea58a81ffb092d6dda767390222a4b0",
"s390x-unknown-linux-gnu-0.10.7":
"281ae4c1343e0c5f9775358690d40e00edbf63ca788b4d8b6574a0b5cba624f4",
"x86_64-apple-darwin-0.10.7":
"4fed9d4f4608fb3850db714ee37244436f850a2b6e485bc510795679c2d08866",
"x86_64-pc-windows-msvc-0.10.7":
"8881afb877996a1373a12e816395122a8d39a3ac06cd066272acdb49510cf0fe",
"x86_64-unknown-linux-gnu-0.10.7":
"9ac6cee4e379a5abfca06e78a777b26b7ba1f81cb7935b97054d80d85ac00774",
"x86_64-unknown-linux-musl-0.10.7":
"992529add6024e67135b1c80617abd2eca7be2cf0b99b3911f923de815bd8dc1",
"aarch64-apple-darwin-0.10.6":
"3993249d8f51deaf34cfce037e57e294e82267ff1f9dc45b7983a17afaf065b4",
"aarch64-pc-windows-msvc-0.10.6":

View file

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

View file

@ -2,17 +2,20 @@ import { promises as fs } from "node:fs";
import * as path from "node:path";
import * as core from "@actions/core";
import * as tc from "@actions/tool-cache";
import type { Endpoints } from "@octokit/types";
import * as pep440 from "@renovatebot/pep440";
import * as semver from "semver";
import {
ASTRAL_MIRROR_PREFIX,
GITHUB_RELEASES_PREFIX,
TOOL_CACHE_NAME,
VERSIONS_MANIFEST_URL,
} from "../utils/constants";
import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants";
import { Octokit } from "../utils/octokit";
import type { Architecture, Platform } from "../utils/platforms";
import { validateChecksum } from "./checksum/checksum";
import { getAllVersions, getArtifact, getLatestVersion } from "./manifest";
import {
getDownloadUrl,
getLatestKnownVersion as getLatestVersionInManifest,
} from "./version-manifest";
type Release =
Endpoints["GET /repos/{owner}/{repo}/releases"]["response"]["data"][number];
export function tryGetFromToolCache(
arch: Architecture,
@ -29,85 +32,72 @@ export function tryGetFromToolCache(
return { installedPath, version: resolvedVersion };
}
export async function downloadVersion(
export async function downloadVersionFromGithub(
platform: Platform,
arch: Architecture,
version: string,
checksum: string | undefined,
checkSum: string | undefined,
githubToken: string,
manifestUrl?: string,
): Promise<{ version: string; cachedToolDir: string }> {
const artifact = await getArtifact(version, arch, platform, manifestUrl);
const artifact = `uv-${arch}-${platform}`;
const extension = getExtension(platform);
const downloadUrl = `https://github.com/${OWNER}/${REPO}/releases/download/${version}/${artifact}${extension}`;
return await downloadVersion(
downloadUrl,
artifact,
platform,
arch,
version,
checkSum,
githubToken,
);
}
if (!artifact) {
throw new Error(
getMissingArtifactMessage(version, arch, platform, manifestUrl),
export async function downloadVersionFromManifest(
manifestUrl: string | undefined,
platform: Platform,
arch: Architecture,
version: string,
checkSum: string | undefined,
githubToken: string,
): Promise<{ version: string; cachedToolDir: string }> {
const downloadUrl = await getDownloadUrl(
manifestUrl,
version,
arch,
platform,
);
if (!downloadUrl) {
core.info(
`manifest-file does not contain version ${version}, arch ${arch}, platform ${platform}. Falling back to GitHub releases.`,
);
}
// For the default astral-sh/versions source, checksum validation relies on
// user input or the built-in KNOWN_CHECKSUMS table, not manifest sha256 values.
const resolvedChecksum =
manifestUrl === undefined
? checksum
: resolveChecksum(checksum, artifact.checksum);
const mirrorUrl = rewriteToMirror(artifact.downloadUrl);
const downloadUrl = mirrorUrl ?? artifact.downloadUrl;
// Don't send the GitHub token to the Astral mirror.
const downloadToken = mirrorUrl !== undefined ? undefined : githubToken;
try {
return await downloadArtifact(
downloadUrl,
`uv-${arch}-${platform}`,
return await downloadVersionFromGithub(
platform,
arch,
version,
resolvedChecksum,
downloadToken,
);
} catch (err) {
if (mirrorUrl === undefined) {
throw err;
}
core.warning(
`Failed to download from mirror, falling back to GitHub Releases: ${(err as Error).message}`,
);
return await downloadArtifact(
artifact.downloadUrl,
`uv-${arch}-${platform}`,
platform,
arch,
version,
resolvedChecksum,
checkSum,
githubToken,
);
}
return await downloadVersion(
downloadUrl,
`uv-${arch}-${platform}`,
platform,
arch,
version,
checkSum,
githubToken,
);
}
/**
* Rewrite a GitHub Releases URL to the Astral mirror.
* Returns `undefined` if the URL does not match the expected GitHub prefix.
*/
export function rewriteToMirror(url: string): string | undefined {
if (!url.startsWith(GITHUB_RELEASES_PREFIX)) {
return undefined;
}
return ASTRAL_MIRROR_PREFIX + url.slice(GITHUB_RELEASES_PREFIX.length);
}
async function downloadArtifact(
async function downloadVersion(
downloadUrl: string,
artifactName: string,
platform: Platform,
arch: Architecture,
version: string,
checksum: string | undefined,
githubToken: string | undefined,
checkSum: string | undefined,
githubToken: string,
): Promise<{ version: string; cachedToolDir: string }> {
core.info(`Downloading uv from "${downloadUrl}" ...`);
const downloadPath = await tc.downloadTool(
@ -115,14 +105,14 @@ async function downloadArtifact(
undefined,
githubToken,
);
await validateChecksum(checksum, downloadPath, arch, platform, version);
await validateChecksum(checkSum, downloadPath, arch, platform, version);
let uvDir: string;
if (platform === "pc-windows-msvc") {
// On windows extracting the zip does not create an intermediate directory.
// On windows extracting the zip does not create an intermediate directory
try {
// Try tar first as it's much faster, but only bsdtar supports zip files,
// so this may fail if another tar, like gnu tar, ends up being used.
// so this my fail if another tar, like gnu tar, ends up being used.
uvDir = await tc.extractTar(downloadPath, undefined, "x");
} catch (err) {
core.info(
@ -137,36 +127,13 @@ async function downloadArtifact(
const extractedDir = await tc.extractTar(downloadPath);
uvDir = path.join(extractedDir, artifactName);
}
const cachedToolDir = await tc.cacheDir(
uvDir,
TOOL_CACHE_NAME,
version,
arch,
);
return { cachedToolDir, version };
}
function getMissingArtifactMessage(
version: string,
arch: Architecture,
platform: Platform,
manifestUrl?: string,
): string {
if (manifestUrl === undefined) {
return `Could not find artifact for version ${version}, arch ${arch}, platform ${platform} in ${VERSIONS_MANIFEST_URL} .`;
}
return `manifest-file does not contain version ${version}, arch ${arch}, platform ${platform}.`;
}
function resolveChecksum(
checksum: string | undefined,
manifestChecksum: string,
): string {
return checksum !== undefined && checksum !== ""
? checksum
: manifestChecksum;
return { cachedToolDir, version: version };
}
function getExtension(platform: Platform): string {
@ -175,61 +142,124 @@ function getExtension(platform: Platform): string {
export async function resolveVersion(
versionInput: string,
manifestUrl: string | undefined,
manifestFile: string | undefined,
githubToken: string,
resolutionStrategy: "highest" | "lowest" = "highest",
): Promise<string> {
core.debug(`Resolving version: ${versionInput}`);
let version: string;
const isSimpleMinimumVersionSpecifier =
versionInput.includes(">") && !versionInput.includes(",");
const resolveVersionSpecifierToLatest =
isSimpleMinimumVersionSpecifier && resolutionStrategy === "highest";
if (resolveVersionSpecifierToLatest) {
core.info("Found minimum version specifier, using latest version");
}
const version =
versionInput === "latest" || resolveVersionSpecifierToLatest
? await getLatestVersion(manifestUrl)
: versionInput;
if (manifestFile) {
version =
versionInput === "latest" || resolveVersionSpecifierToLatest
? await getLatestVersionInManifest(manifestFile)
: versionInput;
} else {
version =
versionInput === "latest" || resolveVersionSpecifierToLatest
? await getLatestVersion(githubToken)
: versionInput;
}
if (tc.isExplicitVersion(version)) {
core.debug(`Version ${version} is an explicit version.`);
if (
resolveVersionSpecifierToLatest &&
!pep440.satisfies(version, versionInput)
) {
throw new Error(`No version found for ${versionInput}`);
if (resolveVersionSpecifierToLatest) {
if (!pep440.satisfies(version, versionInput)) {
throw new Error(`No version found for ${versionInput}`);
}
}
return version;
}
const availableVersions = await getAvailableVersions(manifestUrl);
const availableVersions = await getAvailableVersions(githubToken);
core.debug(`Available versions: ${availableVersions}`);
const resolvedVersion =
resolutionStrategy === "lowest"
? minSatisfying(availableVersions, version)
: maxSatisfying(availableVersions, version);
if (resolvedVersion === undefined) {
throw new Error(`No version found for ${version}`);
}
return resolvedVersion;
}
async function getAvailableVersions(
manifestUrl: string | undefined,
): Promise<string[]> {
if (manifestUrl !== undefined) {
core.info(
`Getting available versions from manifest-file ${manifestUrl} ...`,
async function getAvailableVersions(githubToken: string): Promise<string[]> {
core.info("Getting available versions from GitHub API...");
try {
const octokit = new Octokit({
auth: githubToken,
});
return await getReleaseTagNames(octokit);
} catch (err) {
if ((err as Error).message.includes("Bad credentials")) {
core.info(
"No (valid) GitHub token provided. Falling back to anonymous. Requests might be rate limited.",
);
const octokit = new Octokit();
return await getReleaseTagNames(octokit);
}
throw err;
}
}
async function getReleaseTagNames(octokit: Octokit): Promise<string[]> {
const response: Release[] = await octokit.paginate(
octokit.rest.repos.listReleases,
{
owner: OWNER,
repo: REPO,
},
);
const releaseTagNames = response.map((release) => release.tag_name);
if (releaseTagNames.length === 0) {
throw Error(
"Github API request failed while getting releases. Check the GitHub status page for outages. Try again later.",
);
} else {
core.info(`Getting available versions from ${VERSIONS_MANIFEST_URL} ...`);
}
return releaseTagNames;
}
async function getLatestVersion(githubToken: string) {
core.info("Getting latest version from GitHub API...");
const octokit = new Octokit({
auth: githubToken,
});
let latestRelease: { tag_name: string } | undefined;
try {
latestRelease = await getLatestRelease(octokit);
} catch (err) {
if ((err as Error).message.includes("Bad credentials")) {
core.info(
"No (valid) GitHub token provided. Falling back to anonymous. Requests might be rate limited.",
);
const octokit = new Octokit();
latestRelease = await getLatestRelease(octokit);
} else {
core.error(
"Github API request failed while getting latest release. Check the GitHub status page for outages. Try again later.",
);
throw err;
}
}
return await getAllVersions(manifestUrl);
if (!latestRelease) {
throw new Error("Could not determine latest release.");
}
core.debug(`Latest version: ${latestRelease.tag_name}`);
return latestRelease.tag_name;
}
async function getLatestRelease(octokit: Octokit) {
const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({
owner: OWNER,
repo: REPO,
});
return latestRelease;
}
function maxSatisfying(

View file

@ -1,199 +0,0 @@
import * as core from "@actions/core";
import { VERSIONS_MANIFEST_URL } from "../utils/constants";
import { fetch } from "../utils/fetch";
import { selectDefaultVariant } from "./variant-selection";
export interface ManifestArtifact {
platform: string;
variant?: string;
url: string;
archive_format: string;
sha256: string;
}
export interface ManifestVersion {
version: string;
artifacts: ManifestArtifact[];
}
export interface ArtifactResult {
archiveFormat: string;
checksum: string;
downloadUrl: string;
}
const cachedManifestData = new Map<string, ManifestVersion[]>();
export async function fetchManifest(
manifestUrl: string = VERSIONS_MANIFEST_URL,
): Promise<ManifestVersion[]> {
const cachedVersions = cachedManifestData.get(manifestUrl);
if (cachedVersions !== undefined) {
core.debug(`Using cached manifest data from ${manifestUrl}`);
return cachedVersions;
}
core.info(`Fetching manifest data from ${manifestUrl} ...`);
const response = await fetch(manifestUrl, {});
if (!response.ok) {
throw new Error(
`Failed to fetch manifest data: ${response.status} ${response.statusText}`,
);
}
const body = await response.text();
const versions = parseManifest(body, manifestUrl);
cachedManifestData.set(manifestUrl, versions);
return versions;
}
export function parseManifest(
data: string,
sourceDescription: string,
): ManifestVersion[] {
const trimmed = data.trim();
if (trimmed === "") {
throw new Error(`Manifest at ${sourceDescription} is empty.`);
}
if (trimmed.startsWith("[")) {
throw new Error(
`Legacy JSON array manifests are no longer supported in ${sourceDescription}. Use the astral-sh/versions manifest format instead.`,
);
}
const versions: ManifestVersion[] = [];
for (const [index, line] of data.split("\n").entries()) {
const record = line.trim();
if (record === "") {
continue;
}
let parsed: unknown;
try {
parsed = JSON.parse(record);
} catch (error) {
throw new Error(
`Failed to parse manifest data from ${sourceDescription} at line ${index + 1}: ${(error as Error).message}`,
);
}
if (!isManifestVersion(parsed)) {
throw new Error(
`Invalid manifest record in ${sourceDescription} at line ${index + 1}.`,
);
}
versions.push(parsed);
}
if (versions.length === 0) {
throw new Error(`No manifest data found in ${sourceDescription}.`);
}
return versions;
}
export async function getLatestVersion(
manifestUrl: string = VERSIONS_MANIFEST_URL,
): Promise<string> {
const latestVersion = (await fetchManifest(manifestUrl))[0]?.version;
if (latestVersion === undefined) {
throw new Error("No versions found in manifest data");
}
core.debug(`Latest version from manifest: ${latestVersion}`);
return latestVersion;
}
export async function getAllVersions(
manifestUrl: string = VERSIONS_MANIFEST_URL,
): Promise<string[]> {
const versions = await fetchManifest(manifestUrl);
return versions.map((versionData) => versionData.version);
}
export async function getArtifact(
version: string,
arch: string,
platform: string,
manifestUrl: string = VERSIONS_MANIFEST_URL,
): Promise<ArtifactResult | undefined> {
const versions = await fetchManifest(manifestUrl);
const versionData = versions.find(
(candidate) => candidate.version === version,
);
if (!versionData) {
core.debug(`Version ${version} not found in manifest ${manifestUrl}`);
return undefined;
}
const targetPlatform = `${arch}-${platform}`;
const matchingArtifacts = versionData.artifacts.filter(
(candidate) => candidate.platform === targetPlatform,
);
if (matchingArtifacts.length === 0) {
core.debug(
`Artifact for ${targetPlatform} not found in version ${version}. Available platforms: ${versionData.artifacts
.map((candidate) => candidate.platform)
.join(", ")}`,
);
return undefined;
}
const artifact = selectDefaultVariant(
matchingArtifacts,
`Multiple artifacts found for ${targetPlatform} in version ${version}`,
);
return {
archiveFormat: artifact.archive_format,
checksum: artifact.sha256,
downloadUrl: artifact.url,
};
}
export function clearManifestCache(manifestUrl?: string): void {
if (manifestUrl === undefined) {
cachedManifestData.clear();
return;
}
cachedManifestData.delete(manifestUrl);
}
function isManifestVersion(value: unknown): value is ManifestVersion {
if (!isRecord(value)) {
return false;
}
if (typeof value.version !== "string" || !Array.isArray(value.artifacts)) {
return false;
}
return value.artifacts.every(isManifestArtifact);
}
function isManifestArtifact(value: unknown): value is ManifestArtifact {
if (!isRecord(value)) {
return false;
}
const variantIsValid =
typeof value.variant === "string" || value.variant === undefined;
return (
typeof value.archive_format === "string" &&
typeof value.platform === "string" &&
typeof value.sha256 === "string" &&
typeof value.url === "string" &&
variantIsValid
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

View file

@ -1,39 +0,0 @@
interface VariantAwareEntry {
variant?: string;
}
export function selectDefaultVariant<T extends VariantAwareEntry>(
entries: T[],
duplicateEntryDescription: string,
): T {
const firstEntry = entries[0];
if (firstEntry === undefined) {
throw new Error("selectDefaultVariant requires at least one candidate.");
}
if (entries.length === 1) {
return firstEntry;
}
const defaultEntries = entries.filter((entry) =>
isDefaultVariant(entry.variant),
);
if (defaultEntries.length === 1) {
return defaultEntries[0];
}
throw new Error(
`${duplicateEntryDescription} with variants ${formatVariants(entries)}. setup-uv currently requires a single default variant for duplicate platform entries.`,
);
}
function isDefaultVariant(variant: string | undefined): boolean {
return variant === undefined || variant === "default";
}
function formatVariants<T extends VariantAwareEntry>(entries: T[]): string {
return entries
.map((entry) => entry.variant ?? "default")
.sort((left, right) => left.localeCompare(right))
.join(", ");
}

View file

@ -0,0 +1,91 @@
import { promises as fs } from "node:fs";
import { join } from "node:path";
import * as core from "@actions/core";
import * as semver from "semver";
import { fetch } from "../utils/fetch";
const localManifestFile = join(__dirname, "..", "..", "version-manifest.json");
interface ManifestEntry {
version: string;
artifactName: string;
arch: string;
platform: string;
downloadUrl: string;
}
export async function getLatestKnownVersion(
manifestUrl: string | undefined,
): Promise<string> {
const manifestEntries = await getManifestEntries(manifestUrl);
return manifestEntries.reduce((a, b) =>
semver.gt(a.version, b.version) ? a : b,
).version;
}
export async function getDownloadUrl(
manifestUrl: string | undefined,
version: string,
arch: string,
platform: string,
): Promise<string | undefined> {
const manifestEntries = await getManifestEntries(manifestUrl);
const entry = manifestEntries.find(
(entry) =>
entry.version === version &&
entry.arch === arch &&
entry.platform === platform,
);
return entry ? entry.downloadUrl : undefined;
}
async function getManifestEntries(
manifestUrl: string | undefined,
): Promise<ManifestEntry[]> {
let data: string;
if (manifestUrl !== undefined) {
core.info(`Fetching manifest-file from: ${manifestUrl}`);
const response = await fetch(manifestUrl, {});
if (!response.ok) {
throw new Error(
`Failed to fetch manifest-file: ${response.status} ${response.statusText}`,
);
}
data = await response.text();
} else {
core.info("manifest-file not provided, reading from local file.");
const fileContent = await fs.readFile(localManifestFile);
data = fileContent.toString();
}
return JSON.parse(data);
}
export async function updateVersionManifest(
manifestUrl: string,
downloadUrls: string[],
): Promise<void> {
const manifest: ManifestEntry[] = [];
for (const downloadUrl of downloadUrls) {
const urlParts = downloadUrl.split("/");
const version = urlParts[urlParts.length - 2];
const artifactName = urlParts[urlParts.length - 1];
if (!artifactName.startsWith("uv-")) {
continue;
}
if (artifactName.startsWith("uv-installer")) {
continue;
}
const artifactParts = artifactName.split(".")[0].split("-");
manifest.push({
arch: artifactParts[1],
artifactName: artifactName,
downloadUrl: downloadUrl,
platform: artifactName.split(`uv-${artifactParts[1]}-`)[1].split(".")[0],
version: version,
});
}
core.debug(`Updating manifest-file: ${JSON.stringify(manifest)}`);
await fs.writeFile(manifestUrl, JSON.stringify(manifest));
}

View file

@ -9,14 +9,21 @@ import {
STATE_PYTHON_CACHE_MATCHED_KEY,
} from "./cache/restore-cache";
import { STATE_UV_PATH, STATE_UV_VERSION } from "./utils/constants";
import { loadInputs, type SetupInputs } from "./utils/inputs";
import {
cacheLocalPath,
cachePython,
enableCache,
ignoreNothingToCache,
pythonDir,
pruneCache as shouldPruneCache,
saveCache as shouldSaveCache,
} from "./utils/inputs";
export async function run(): Promise<void> {
try {
const inputs = loadInputs();
if (inputs.enableCache) {
if (inputs.saveCache) {
await saveCache(inputs);
if (enableCache) {
if (shouldSaveCache) {
await saveCache();
} else {
core.info("save-cache is false. Skipping save cache step.");
}
@ -36,7 +43,7 @@ export async function run(): Promise<void> {
}
}
async function saveCache(inputs: SetupInputs): Promise<void> {
async function saveCache(): Promise<void> {
const cacheKey = core.getState(STATE_CACHE_KEY);
const matchedKey = core.getState(STATE_CACHE_MATCHED_KEY);
@ -47,13 +54,13 @@ async function saveCache(inputs: SetupInputs): Promise<void> {
if (matchedKey === cacheKey) {
core.info(`Cache hit occurred on key ${cacheKey}, not saving cache.`);
} else {
if (inputs.pruneCache) {
if (shouldPruneCache) {
await pruneCache();
}
const actualCachePath = getUvCachePath(inputs);
const actualCachePath = getUvCachePath();
if (!fs.existsSync(actualCachePath)) {
if (inputs.ignoreNothingToCache) {
if (ignoreNothingToCache) {
core.info(
"No cacheable uv cache paths were found. Ignoring because ignore-nothing-to-cache is enabled.",
);
@ -72,10 +79,10 @@ async function saveCache(inputs: SetupInputs): Promise<void> {
}
}
if (inputs.cachePython) {
if (!fs.existsSync(inputs.pythonDir)) {
if (cachePython) {
if (!fs.existsSync(pythonDir)) {
core.warning(
`Python cache path ${inputs.pythonDir} does not exist on disk. Skipping Python cache save because no managed Python installation was found. If you want uv to install managed Python instead of using a system interpreter, set UV_PYTHON_PREFERENCE=only-managed.`,
`Python cache path ${pythonDir} does not exist on disk. Skipping Python cache save because no managed Python installation was found. If you want uv to install managed Python instead of using a system interpreter, set UV_PYTHON_PREFERENCE=only-managed.`,
);
return;
}
@ -83,7 +90,7 @@ async function saveCache(inputs: SetupInputs): Promise<void> {
const pythonCacheKey = `${cacheKey}-python`;
await saveCacheToKey(
pythonCacheKey,
inputs.pythonDir,
pythonDir,
STATE_PYTHON_CACHE_MATCHED_KEY,
"Python cache",
);
@ -106,22 +113,22 @@ async function pruneCache(): Promise<void> {
await exec.exec(uvPath, execArgs, options);
}
function getUvCachePath(inputs: SetupInputs): string {
if (inputs.cacheLocalPath === undefined) {
function getUvCachePath(): string {
if (cacheLocalPath === undefined) {
throw new Error(
"cache-local-path is not set. Cannot save cache without a valid cache path.",
);
}
if (
process.env.UV_CACHE_DIR &&
process.env.UV_CACHE_DIR !== inputs.cacheLocalPath.path
process.env.UV_CACHE_DIR !== cacheLocalPath.path
) {
core.warning(
`The environment variable UV_CACHE_DIR has been changed to "${process.env.UV_CACHE_DIR}", by an action or step running after astral-sh/setup-uv. This can lead to unexpected behavior. If you expected this to happen set the cache-local-path input to "${process.env.UV_CACHE_DIR}" instead of "${inputs.cacheLocalPath.path}".`,
`The environment variable UV_CACHE_DIR has been changed to "${process.env.UV_CACHE_DIR}", by an action or step running after astral-sh/setup-uv. This can lead to unexpected behavior. If you expected this to happen set the cache-local-path input to "${process.env.UV_CACHE_DIR}" instead of "${cacheLocalPath.path}".`,
);
return process.env.UV_CACHE_DIR;
}
return inputs.cacheLocalPath.path;
return cacheLocalPath.path;
}
async function saveCacheToKey(

View file

@ -4,12 +4,31 @@ import * as core from "@actions/core";
import * as exec from "@actions/exec";
import { restoreCache } from "./cache/restore-cache";
import {
downloadVersion,
downloadVersionFromManifest,
resolveVersion,
tryGetFromToolCache,
} from "./download/download-version";
import { STATE_UV_PATH, STATE_UV_VERSION } from "./utils/constants";
import { CacheLocalSource, loadInputs, type SetupInputs } from "./utils/inputs";
import {
activateEnvironment as activateEnvironmentInput,
addProblemMatchers,
CacheLocalSource,
cacheLocalPath,
checkSum,
enableCache,
githubToken,
ignoreEmptyWorkdir,
manifestFile,
pythonDir,
pythonVersion,
resolutionStrategy,
toolBinDir,
toolDir,
venvPath,
versionFile as versionFileInput,
version as versionInput,
workingDirectory,
} from "./utils/inputs";
import {
type Architecture,
getArch,
@ -18,11 +37,9 @@ import {
} from "./utils/platforms";
import { getUvVersionFromFile } from "./version/resolve";
const sourceDir = __dirname;
async function getPythonVersion(inputs: SetupInputs): Promise<string> {
if (inputs.pythonVersion !== "") {
return inputs.pythonVersion;
async function getPythonVersion(): Promise<string> {
if (pythonVersion !== "") {
return pythonVersion;
}
let output = "";
@ -36,7 +53,7 @@ async function getPythonVersion(inputs: SetupInputs): Promise<string> {
};
try {
const execArgs = ["python", "find", "--directory", inputs.workingDirectory];
const execArgs = ["python", "find", "--directory", workingDirectory];
await exec.exec("uv", execArgs, options);
const pythonPath = output.trim();
@ -52,38 +69,37 @@ async function getPythonVersion(inputs: SetupInputs): Promise<string> {
}
async function run(): Promise<void> {
try {
const inputs = loadInputs();
detectEmptyWorkdir(inputs);
const platform = await getPlatform();
const arch = getArch();
detectEmptyWorkdir();
const platform = await getPlatform();
const arch = getArch();
try {
if (platform === undefined) {
throw new Error(`Unsupported platform: ${process.platform}`);
}
if (arch === undefined) {
throw new Error(`Unsupported architecture: ${process.arch}`);
}
const setupResult = await setupUv(inputs, platform, arch);
const setupResult = await setupUv(platform, arch, checkSum, githubToken);
addToolBinToPath(inputs);
addToolBinToPath();
addUvToPathAndOutput(setupResult.uvDir);
setToolDir(inputs);
addPythonDirToPath(inputs);
setupPython(inputs);
await activateEnvironment(inputs);
addMatchers(inputs);
setCacheDir(inputs);
setToolDir();
addPythonDirToPath();
setupPython();
await activateEnvironment();
addMatchers();
setCacheDir();
core.setOutput("uv-version", setupResult.version);
core.saveState(STATE_UV_VERSION, setupResult.version);
core.info(`Successfully installed uv version ${setupResult.version}`);
const detectedPythonVersion = await getPythonVersion(inputs);
core.setOutput("python-version", detectedPythonVersion);
const pythonVersion = await getPythonVersion();
core.setOutput("python-version", pythonVersion);
if (inputs.enableCache) {
await restoreCache(inputs, detectedPythonVersion);
if (enableCache) {
await restoreCache(pythonVersion);
}
// https://github.com/nodejs/node/issues/56645#issuecomment-3077594952
await new Promise((resolve) => setTimeout(resolve, 50));
@ -93,9 +109,9 @@ async function run(): Promise<void> {
}
}
function detectEmptyWorkdir(inputs: SetupInputs): void {
if (fs.readdirSync(inputs.workingDirectory).length === 0) {
if (inputs.ignoreEmptyWorkdir) {
function detectEmptyWorkdir(): void {
if (fs.readdirSync(workingDirectory).length === 0) {
if (ignoreEmptyWorkdir) {
core.info(
"Empty workdir detected. Ignoring because ignore-empty-workdir is enabled",
);
@ -108,11 +124,12 @@ function detectEmptyWorkdir(inputs: SetupInputs): void {
}
async function setupUv(
inputs: SetupInputs,
platform: Platform,
arch: Architecture,
checkSum: string | undefined,
githubToken: string,
): Promise<{ uvDir: string; version: string }> {
const resolvedVersion = await determineVersion(inputs);
const resolvedVersion = await determineVersion(manifestFile);
const toolCacheResult = tryGetFromToolCache(arch, resolvedVersion);
if (toolCacheResult.installedPath) {
core.info(`Found uv in tool-cache for ${toolCacheResult.version}`);
@ -122,58 +139,63 @@ async function setupUv(
};
}
const downloadResult = await downloadVersion(
const downloadVersionResult = await downloadVersionFromManifest(
manifestFile,
platform,
arch,
resolvedVersion,
inputs.checksum,
inputs.githubToken,
inputs.manifestFile,
checkSum,
githubToken,
);
return {
uvDir: downloadResult.cachedToolDir,
version: downloadResult.version,
uvDir: downloadVersionResult.cachedToolDir,
version: downloadVersionResult.version,
};
}
async function determineVersion(inputs: SetupInputs): Promise<string> {
return await resolveVersion(
getRequestedVersion(inputs),
inputs.manifestFile,
inputs.resolutionStrategy,
);
}
function getRequestedVersion(inputs: SetupInputs): string {
if (inputs.version !== "") {
return inputs.version;
async function determineVersion(
manifestFile: string | undefined,
): Promise<string> {
if (versionInput !== "") {
return await resolveVersion(
versionInput,
manifestFile,
githubToken,
resolutionStrategy,
);
}
if (inputs.versionFile !== "") {
const versionFromFile = getUvVersionFromFile(inputs.versionFile);
if (versionFileInput !== "") {
const versionFromFile = getUvVersionFromFile(versionFileInput);
if (versionFromFile === undefined) {
throw new Error(
`Could not determine uv version from file: ${inputs.versionFile}`,
`Could not determine uv version from file: ${versionFileInput}`,
);
}
return versionFromFile;
return await resolveVersion(
versionFromFile,
manifestFile,
githubToken,
resolutionStrategy,
);
}
const versionFromUvToml = getUvVersionFromFile(
`${inputs.workingDirectory}${path.sep}uv.toml`,
`${workingDirectory}${path.sep}uv.toml`,
);
const versionFromPyproject = getUvVersionFromFile(
`${inputs.workingDirectory}${path.sep}pyproject.toml`,
`${workingDirectory}${path.sep}pyproject.toml`,
);
if (versionFromUvToml === undefined && versionFromPyproject === undefined) {
core.info(
"Could not determine uv version from uv.toml or pyproject.toml. Falling back to latest.",
);
}
return versionFromUvToml || versionFromPyproject || "latest";
return await resolveVersion(
versionFromUvToml || versionFromPyproject || "latest",
manifestFile,
githubToken,
resolutionStrategy,
);
}
function addUvToPathAndOutput(cachedPath: string): void {
@ -188,17 +210,15 @@ function addUvToPathAndOutput(cachedPath: string): void {
}
}
function addToolBinToPath(inputs: SetupInputs): void {
if (inputs.toolBinDir !== undefined) {
core.exportVariable("UV_TOOL_BIN_DIR", inputs.toolBinDir);
core.info(`Set UV_TOOL_BIN_DIR to ${inputs.toolBinDir}`);
function addToolBinToPath(): void {
if (toolBinDir !== undefined) {
core.exportVariable("UV_TOOL_BIN_DIR", toolBinDir);
core.info(`Set UV_TOOL_BIN_DIR to ${toolBinDir}`);
if (process.env.UV_NO_MODIFY_PATH !== undefined) {
core.info(
`UV_NO_MODIFY_PATH is set, not adding ${inputs.toolBinDir} to path`,
);
core.info(`UV_NO_MODIFY_PATH is set, not adding ${toolBinDir} to path`);
} else {
core.addPath(inputs.toolBinDir);
core.info(`Added ${inputs.toolBinDir} to the path`);
core.addPath(toolBinDir);
core.info(`Added ${toolBinDir} to the path`);
}
} else {
if (process.env.UV_NO_MODIFY_PATH !== undefined) {
@ -218,74 +238,74 @@ function addToolBinToPath(inputs: SetupInputs): void {
}
}
function setToolDir(inputs: SetupInputs): void {
if (inputs.toolDir !== undefined) {
core.exportVariable("UV_TOOL_DIR", inputs.toolDir);
core.info(`Set UV_TOOL_DIR to ${inputs.toolDir}`);
function setToolDir(): void {
if (toolDir !== undefined) {
core.exportVariable("UV_TOOL_DIR", toolDir);
core.info(`Set UV_TOOL_DIR to ${toolDir}`);
}
}
function addPythonDirToPath(inputs: SetupInputs): void {
core.exportVariable("UV_PYTHON_INSTALL_DIR", inputs.pythonDir);
core.info(`Set UV_PYTHON_INSTALL_DIR to ${inputs.pythonDir}`);
function addPythonDirToPath(): void {
core.exportVariable("UV_PYTHON_INSTALL_DIR", pythonDir);
core.info(`Set UV_PYTHON_INSTALL_DIR to ${pythonDir}`);
if (process.env.UV_NO_MODIFY_PATH !== undefined) {
core.info("UV_NO_MODIFY_PATH is set, not adding python dir to path");
} else {
core.addPath(inputs.pythonDir);
core.info(`Added ${inputs.pythonDir} to the path`);
core.addPath(pythonDir);
core.info(`Added ${pythonDir} to the path`);
}
}
function setupPython(inputs: SetupInputs): void {
if (inputs.pythonVersion !== "") {
core.exportVariable("UV_PYTHON", inputs.pythonVersion);
core.info(`Set UV_PYTHON to ${inputs.pythonVersion}`);
function setupPython(): void {
if (pythonVersion !== "") {
core.exportVariable("UV_PYTHON", pythonVersion);
core.info(`Set UV_PYTHON to ${pythonVersion}`);
}
}
async function activateEnvironment(inputs: SetupInputs): Promise<void> {
if (inputs.activateEnvironment) {
async function activateEnvironment(): Promise<void> {
if (activateEnvironmentInput) {
if (process.env.UV_NO_MODIFY_PATH !== undefined) {
throw new Error(
"UV_NO_MODIFY_PATH and activate-environment cannot be used together.",
);
}
core.info(`Creating and activating python venv at ${inputs.venvPath}...`);
core.info(`Creating and activating python venv at ${venvPath}...`);
await exec.exec("uv", [
"venv",
inputs.venvPath,
venvPath,
"--directory",
inputs.workingDirectory,
workingDirectory,
"--clear",
]);
let venvBinPath = `${inputs.venvPath}${path.sep}bin`;
let venvBinPath = `${venvPath}${path.sep}bin`;
if (process.platform === "win32") {
venvBinPath = `${inputs.venvPath}${path.sep}Scripts`;
venvBinPath = `${venvPath}${path.sep}Scripts`;
}
core.addPath(path.resolve(venvBinPath));
core.exportVariable("VIRTUAL_ENV", inputs.venvPath);
core.setOutput("venv", inputs.venvPath);
core.exportVariable("VIRTUAL_ENV", venvPath);
core.setOutput("venv", venvPath);
}
}
function setCacheDir(inputs: SetupInputs): void {
if (inputs.cacheLocalPath !== undefined) {
if (inputs.cacheLocalPath.source === CacheLocalSource.Config) {
function setCacheDir(): void {
if (cacheLocalPath !== undefined) {
if (cacheLocalPath.source === CacheLocalSource.Config) {
core.info(
"Using cache-dir from uv config file, not modifying UV_CACHE_DIR",
);
return;
}
core.exportVariable("UV_CACHE_DIR", inputs.cacheLocalPath.path);
core.info(`Set UV_CACHE_DIR to ${inputs.cacheLocalPath.path}`);
core.exportVariable("UV_CACHE_DIR", cacheLocalPath.path);
core.info(`Set UV_CACHE_DIR to ${cacheLocalPath.path}`);
}
}
function addMatchers(inputs: SetupInputs): void {
if (inputs.addProblemMatchers) {
const matchersPath = path.join(sourceDir, "..", "..", ".github");
function addMatchers(): void {
if (addProblemMatchers) {
const matchersPath = path.join(__dirname, `..${path.sep}..`, ".github");
core.info(`##[add-matcher]${path.join(matchersPath, "python.json")}`);
}
}

View file

@ -1,81 +0,0 @@
import * as core from "@actions/core";
import * as semver from "semver";
import { KNOWN_CHECKSUMS } from "./download/checksum/known-checksums";
import {
type ChecksumEntry,
updateChecksums,
} from "./download/checksum/update-known-checksums";
import {
fetchManifest,
getLatestVersion,
type ManifestVersion,
} from "./download/manifest";
const VERSION_IN_CHECKSUM_KEY_PATTERN =
/-(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/;
async function run(): Promise<void> {
const checksumFilePath = process.argv.slice(2)[0];
if (!checksumFilePath) {
throw new Error(
"Missing checksum file path. Usage: node dist/update-known-checksums/index.cjs <checksum-file-path>",
);
}
const latestVersion = await getLatestVersion();
const latestKnownVersion = getLatestKnownVersionFromChecksums();
if (semver.lte(latestVersion, latestKnownVersion)) {
core.info(
`Latest release (${latestVersion}) is not newer than the latest known version (${latestKnownVersion}). Skipping update.`,
);
return;
}
const versions = await fetchManifest();
const checksumEntries = extractChecksumsFromManifest(versions);
await updateChecksums(checksumFilePath, checksumEntries);
core.setOutput("latest-version", latestVersion);
}
function getLatestKnownVersionFromChecksums(): string {
const versions = new Set<string>();
for (const key of Object.keys(KNOWN_CHECKSUMS)) {
const version = extractVersionFromChecksumKey(key);
if (version !== undefined) {
versions.add(version);
}
}
const latestVersion = [...versions].sort(semver.rcompare)[0];
if (!latestVersion) {
throw new Error("Could not determine latest known version from checksums.");
}
return latestVersion;
}
function extractVersionFromChecksumKey(key: string): string | undefined {
return key.match(VERSION_IN_CHECKSUM_KEY_PATTERN)?.[1];
}
function extractChecksumsFromManifest(
versions: ManifestVersion[],
): ChecksumEntry[] {
const checksums: ChecksumEntry[] = [];
for (const version of versions) {
for (const artifact of version.artifacts) {
checksums.push({
checksum: artifact.sha256,
key: `${artifact.platform}-${version.version}`,
});
}
}
return checksums;
}
run();

View file

@ -0,0 +1,63 @@
import * as core from "@actions/core";
import type { Endpoints } from "@octokit/types";
import * as semver from "semver";
import { updateChecksums } from "./download/checksum/update-known-checksums";
import {
getLatestKnownVersion,
updateVersionManifest,
} from "./download/version-manifest";
import { OWNER, REPO } from "./utils/constants";
import { Octokit } from "./utils/octokit";
type Release =
Endpoints["GET /repos/{owner}/{repo}/releases"]["response"]["data"][number];
async function run(): Promise<void> {
const checksumFilePath = process.argv.slice(2)[0];
const versionsManifestFile = process.argv.slice(2)[1];
const githubToken = process.argv.slice(2)[2];
const octokit = new Octokit({
auth: githubToken,
});
const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({
owner: OWNER,
repo: REPO,
});
const latestKnownVersion = await getLatestKnownVersion(undefined);
if (semver.lte(latestRelease.tag_name, latestKnownVersion)) {
core.info(
`Latest release (${latestRelease.tag_name}) is not newer than the latest known version (${latestKnownVersion}). Skipping update.`,
);
return;
}
const releases: Release[] = await octokit.paginate(
octokit.rest.repos.listReleases,
{
owner: OWNER,
repo: REPO,
},
);
const checksumDownloadUrls: string[] = releases.flatMap((release) =>
release.assets
.filter((asset) => asset.name.endsWith(".sha256"))
.map((asset) => asset.browser_download_url),
);
await updateChecksums(checksumFilePath, checksumDownloadUrls);
const artifactDownloadUrls: string[] = releases.flatMap((release) =>
release.assets
.filter((asset) => !asset.name.endsWith(".sha256"))
.map((asset) => asset.browser_download_url),
);
await updateVersionManifest(versionsManifestFile, artifactDownloadUrls);
core.setOutput("latest-version", latestRelease.tag_name);
}
run();

View file

@ -1,13 +1,5 @@
export const REPO = "uv";
export const OWNER = "astral-sh";
export const TOOL_CACHE_NAME = "uv";
export const STATE_UV_PATH = "uv-path";
export const STATE_UV_VERSION = "uv-version";
export const VERSIONS_MANIFEST_URL =
"https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson";
/** GitHub Releases URL prefix for uv artifacts. */
export const GITHUB_RELEASES_PREFIX =
"https://github.com/astral-sh/uv/releases/download/";
/** Astral mirror URL prefix that fronts GitHub Releases for uv artifacts. */
export const ASTRAL_MIRROR_PREFIX =
"https://releases.astral.sh/github/uv/releases/download/";

View file

@ -9,121 +9,53 @@ export enum CacheLocalSource {
Default,
}
export interface CacheLocalPath {
path: string;
source: CacheLocalSource;
}
export const workingDirectory = core.getInput("working-directory");
export const version = core.getInput("version");
export const versionFile = getVersionFile();
export const pythonVersion = core.getInput("python-version");
export const activateEnvironment = core.getBooleanInput("activate-environment");
export const venvPath = getVenvPath();
export const checkSum = core.getInput("checksum");
export const enableCache = getEnableCache();
export const restoreCache = core.getInput("restore-cache") === "true";
export const saveCache = core.getInput("save-cache") === "true";
export const cacheSuffix = core.getInput("cache-suffix") || "";
export const cacheLocalPath = getCacheLocalPath();
export const cacheDependencyGlob = getCacheDependencyGlob();
export const pruneCache = core.getInput("prune-cache") === "true";
export const cachePython = core.getInput("cache-python") === "true";
export const ignoreNothingToCache =
core.getInput("ignore-nothing-to-cache") === "true";
export const ignoreEmptyWorkdir =
core.getInput("ignore-empty-workdir") === "true";
export const toolBinDir = getToolBinDir();
export const toolDir = getToolDir();
export const pythonDir = getUvPythonDir();
export const githubToken = core.getInput("github-token");
export const manifestFile = getManifestFile();
export const addProblemMatchers =
core.getInput("add-problem-matchers") === "true";
export const resolutionStrategy = getResolutionStrategy();
export type ResolutionStrategy = "highest" | "lowest";
export interface SetupInputs {
workingDirectory: string;
version: string;
versionFile: string;
pythonVersion: string;
activateEnvironment: boolean;
venvPath: string;
checksum: string;
enableCache: boolean;
restoreCache: boolean;
saveCache: boolean;
cacheSuffix: string;
cacheLocalPath?: CacheLocalPath;
cacheDependencyGlob: string;
pruneCache: boolean;
cachePython: boolean;
ignoreNothingToCache: boolean;
ignoreEmptyWorkdir: boolean;
toolBinDir?: string;
toolDir?: string;
pythonDir: string;
githubToken: string;
manifestFile?: string;
addProblemMatchers: boolean;
resolutionStrategy: ResolutionStrategy;
}
export function loadInputs(): SetupInputs {
const workingDirectory = core.getInput("working-directory");
const version = core.getInput("version");
const versionFile = getVersionFile(workingDirectory);
const pythonVersion = core.getInput("python-version");
const activateEnvironment = core.getBooleanInput("activate-environment");
const venvPath = getVenvPath(workingDirectory, activateEnvironment);
const checksum = core.getInput("checksum");
const enableCache = getEnableCache();
const restoreCache = core.getInput("restore-cache") === "true";
const saveCache = core.getInput("save-cache") === "true";
const cacheSuffix = core.getInput("cache-suffix") || "";
const cacheLocalPath = getCacheLocalPath(
workingDirectory,
versionFile,
enableCache,
);
const cacheDependencyGlob = getCacheDependencyGlob(workingDirectory);
const pruneCache = core.getInput("prune-cache") === "true";
const cachePython = core.getInput("cache-python") === "true";
const ignoreNothingToCache =
core.getInput("ignore-nothing-to-cache") === "true";
const ignoreEmptyWorkdir = core.getInput("ignore-empty-workdir") === "true";
const toolBinDir = getToolBinDir(workingDirectory);
const toolDir = getToolDir(workingDirectory);
const pythonDir = getUvPythonDir();
const githubToken = core.getInput("github-token");
const manifestFile = getManifestFile();
const addProblemMatchers = core.getInput("add-problem-matchers") === "true";
const resolutionStrategy = getResolutionStrategy();
return {
activateEnvironment,
addProblemMatchers,
cacheDependencyGlob,
cacheLocalPath,
cachePython,
cacheSuffix,
checksum,
enableCache,
githubToken,
ignoreEmptyWorkdir,
ignoreNothingToCache,
manifestFile,
pruneCache,
pythonDir,
pythonVersion,
resolutionStrategy,
restoreCache,
saveCache,
toolBinDir,
toolDir,
venvPath,
version,
versionFile,
workingDirectory,
};
}
function getVersionFile(workingDirectory: string): string {
function getVersionFile(): string {
const versionFileInput = core.getInput("version-file");
if (versionFileInput !== "") {
const tildeExpanded = expandTilde(versionFileInput);
return resolveRelativePath(workingDirectory, tildeExpanded);
return resolveRelativePath(tildeExpanded);
}
return versionFileInput;
}
function getVenvPath(
workingDirectory: string,
activateEnvironment: boolean,
): string {
function getVenvPath(): string {
const venvPathInput = core.getInput("venv-path");
if (venvPathInput !== "") {
if (!activateEnvironment) {
core.warning("venv-path is only used when activate-environment is true");
}
const tildeExpanded = expandTilde(venvPathInput);
return normalizePath(resolveRelativePath(workingDirectory, tildeExpanded));
return normalizePath(resolveRelativePath(tildeExpanded));
}
return normalizePath(resolveRelativePath(workingDirectory, ".venv"));
return normalizePath(resolveRelativePath(".venv"));
}
function getEnableCache(): boolean {
@ -134,11 +66,11 @@ function getEnableCache(): boolean {
return enableCacheInput === "true";
}
function getToolBinDir(workingDirectory: string): string | undefined {
function getToolBinDir(): string | undefined {
const toolBinDirInput = core.getInput("tool-bin-dir");
if (toolBinDirInput !== "") {
const tildeExpanded = expandTilde(toolBinDirInput);
return resolveRelativePath(workingDirectory, tildeExpanded);
return resolveRelativePath(tildeExpanded);
}
if (process.platform === "win32") {
if (process.env.RUNNER_TEMP !== undefined) {
@ -151,11 +83,11 @@ function getToolBinDir(workingDirectory: string): string | undefined {
return undefined;
}
function getToolDir(workingDirectory: string): string | undefined {
function getToolDir(): string | undefined {
const toolDirInput = core.getInput("tool-dir");
if (toolDirInput !== "") {
const tildeExpanded = expandTilde(toolDirInput);
return resolveRelativePath(workingDirectory, tildeExpanded);
return resolveRelativePath(tildeExpanded);
}
if (process.platform === "win32") {
if (process.env.RUNNER_TEMP !== undefined) {
@ -168,23 +100,21 @@ function getToolDir(workingDirectory: string): string | undefined {
return undefined;
}
function getCacheLocalPath(
workingDirectory: string,
versionFile: string,
enableCache: boolean,
): CacheLocalPath | undefined {
function getCacheLocalPath():
| {
path: string;
source: CacheLocalSource;
}
| undefined {
const cacheLocalPathInput = core.getInput("cache-local-path");
if (cacheLocalPathInput !== "") {
const tildeExpanded = expandTilde(cacheLocalPathInput);
return {
path: resolveRelativePath(workingDirectory, tildeExpanded),
path: resolveRelativePath(tildeExpanded),
source: CacheLocalSource.Input,
};
}
const cacheDirFromConfig = getCacheDirFromConfig(
workingDirectory,
versionFile,
);
const cacheDirFromConfig = getCacheDirFromConfig();
if (cacheDirFromConfig !== undefined) {
return { path: cacheDirFromConfig, source: CacheLocalSource.Config };
}
@ -192,7 +122,7 @@ function getCacheLocalPath(
core.info(`UV_CACHE_DIR is already set to ${process.env.UV_CACHE_DIR}`);
return { path: process.env.UV_CACHE_DIR, source: CacheLocalSource.Env };
}
if (enableCache) {
if (getEnableCache()) {
if (process.env.RUNNER_ENVIRONMENT === "github-hosted") {
if (process.env.RUNNER_TEMP !== undefined) {
return {
@ -217,12 +147,9 @@ function getCacheLocalPath(
}
}
function getCacheDirFromConfig(
workingDirectory: string,
versionFile: string,
): string | undefined {
function getCacheDirFromConfig(): string | undefined {
for (const filePath of [versionFile, "uv.toml", "pyproject.toml"]) {
const resolvedPath = resolveRelativePath(workingDirectory, filePath);
const resolvedPath = resolveRelativePath(filePath);
try {
const cacheDir = getConfigValueFromTomlFile(resolvedPath, "cache-dir");
if (cacheDir !== undefined) {
@ -248,8 +175,9 @@ export function getUvPythonDir(): string {
if (process.env.RUNNER_ENVIRONMENT !== "github-hosted") {
if (process.platform === "win32") {
return `${process.env.APPDATA}${path.sep}uv${path.sep}python`;
} else {
return `${process.env.HOME}${path.sep}.local${path.sep}share${path.sep}uv${path.sep}python`;
}
return `${process.env.HOME}${path.sep}.local${path.sep}share${path.sep}uv${path.sep}python`;
}
if (process.env.RUNNER_TEMP !== undefined) {
return `${process.env.RUNNER_TEMP}${path.sep}uv-python-dir`;
@ -259,14 +187,14 @@ export function getUvPythonDir(): string {
);
}
function getCacheDependencyGlob(workingDirectory: string): string {
function getCacheDependencyGlob(): string {
const cacheDependencyGlobInput = core.getInput("cache-dependency-glob");
if (cacheDependencyGlobInput !== "") {
return cacheDependencyGlobInput
.split("\n")
.map((part) => part.trim())
.map((part) => expandTilde(part))
.map((part) => resolveRelativePath(workingDirectory, part))
.map((part) => resolveRelativePath(part))
.join("\n");
}
return cacheDependencyGlobInput;
@ -292,10 +220,7 @@ function normalizePath(inputPath: string): string {
return trimmed;
}
function resolveRelativePath(
workingDirectory: string,
inputPath: string,
): string {
function resolveRelativePath(inputPath: string): string {
const hasNegation = inputPath.startsWith("!");
const pathWithoutNegation = hasNegation ? inputPath.substring(1) : inputPath;
@ -315,7 +240,7 @@ function getManifestFile(): string | undefined {
return undefined;
}
function getResolutionStrategy(): ResolutionStrategy {
function getResolutionStrategy(): "highest" | "lowest" {
const resolutionStrategyInput = core.getInput("resolution-strategy");
if (resolutionStrategyInput === "lowest") {
return "lowest";

34
src/utils/octokit.ts Normal file
View file

@ -0,0 +1,34 @@
import type { OctokitOptions } from "@octokit/core";
import { Octokit as Core } from "@octokit/core";
import {
type PaginateInterface,
paginateRest,
} from "@octokit/plugin-paginate-rest";
import { legacyRestEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
import { fetch as customFetch } from "./fetch";
export type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
const DEFAULTS = {
baseUrl: "https://api.github.com",
userAgent: "setup-uv",
};
const OctokitWithPlugins = Core.plugin(paginateRest, legacyRestEndpointMethods);
export const Octokit = OctokitWithPlugins.defaults(function buildDefaults(
options: OctokitOptions,
): OctokitOptions {
return {
...DEFAULTS,
...options,
request: {
fetch: customFetch,
...options.request,
},
};
});
export type Octokit = InstanceType<typeof OctokitWithPlugins> & {
paginate: PaginateInterface;
};

View file

@ -13,7 +13,6 @@ export type Architecture =
| "x86_64"
| "aarch64"
| "s390x"
| "riscv64gc"
| "powerpc64le";
export function getArch(): Architecture | undefined {
@ -22,7 +21,6 @@ export function getArch(): Architecture | undefined {
arm64: "aarch64",
ia32: "i686",
ppc64: "powerpc64le",
riscv64: "riscv64gc",
s390x: "s390x",
x64: "x86_64",
};

View file

@ -1,12 +1,12 @@
{
"compilerOptions": {
"esModuleInterop": true,
"isolatedModules": true,
"module": "esnext",
"moduleResolution": "bundler",
"noImplicitAny": true,
"strict": true,
"target": "ES2022"
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"module": "nodenext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"outDir": "./lib" /* Redirect output structure to the directory. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
"strict": true /* Enable all strict type-checking options. */,
"target": "ES2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
},
"include": ["src/**/*.ts"]
"exclude": ["node_modules", "**/*.test.ts"]
}

30515
version-manifest.json Normal file

File diff suppressed because it is too large Load diff