Compare commits

..

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

45 changed files with 232553 additions and 213916 deletions

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 pull-requests: read
steps: steps:
- name: 🚀 Run Release Drafter - name: 🚀 Run Release Drafter
uses: release-drafter/release-drafter@139054aeaa9adc52ab36ddf67437541f039b88e2 # v7.1.1 uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6.2.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -27,7 +27,7 @@ jobs:
- name: Actionlint - name: Actionlint
uses: eifinger/actionlint-action@7802e0cc3ab3f81cbffb36fb0bf1a3621d994b89 # v1.10.1 uses: eifinger/actionlint-action@7802e0cc3ab3f81cbffb36fb0bf1a3621d994b89 # v1.10.1
- name: Run zizmor - name: Run zizmor
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: .nvmrc node-version-file: .nvmrc
@ -38,7 +38,7 @@ jobs:
npm run all npm run all
- name: Check all jobs are in all-tests-passed.needs - name: Check all jobs are in all-tests-passed.needs
run: | run: |
tsc -p tsconfig.json tsc check-all-tests-passed-needs.ts
node check-all-tests-passed-needs.js node check-all-tests-passed-needs.js
working-directory: .github/scripts working-directory: .github/scripts
- name: Make sure no changes from linters are detected - name: Make sure no changes from linters are detected
@ -164,21 +164,9 @@ jobs:
- name: Latest version gets installed - name: Latest version gets installed
run: | 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') 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 "Latest version is $LATEST_VERSION"
echo "uv --version output is $UV_VERSION_OUTPUT" if [ "$(uv --version)" != "uv $LATEST_VERSION" ]; then
echo "Parsed uv version is $UV_VERSION" echo "Wrong uv version: $(uv --version)"
if [ "$UV_VERSION" != "$LATEST_VERSION" ]; then
echo "Wrong uv version: $UV_VERSION_OUTPUT"
exit 1 exit 1
fi fi
env: env:
@ -808,12 +796,12 @@ jobs:
- name: Install from custom manifest file - name: Install from custom manifest file
uses: ./ uses: ./
with: 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 - run: uv sync
working-directory: __tests__/fixtures/uv-project working-directory: __tests__/fixtures/uv-project
- name: Correct version gets installed - name: Correct version gets installed
run: | 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)" echo "Wrong uv version: $(uv --version)"
exit 1 exit 1
fi fi

View file

@ -20,12 +20,11 @@ jobs:
persist-credentials: true persist-credentials: true
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version-file: .nvmrc node-version: "20"
cache: npm
- name: Update known checksums - name: Update known checksums
id: update-known-checksums id: update-known-checksums
run: run:
node dist/update-known-checksums/index.cjs node dist/update-known-checksums/index.js
src/download/checksum/known-checksums.ts src/download/checksum/known-checksums.ts
- name: Check for changes - name: Check for changes
id: changes-exist id: changes-exist

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

@ -10,9 +10,4 @@ This repository is a TypeScript-based GitHub Action for installing `uv` in GitHu
- 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. - 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. - 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. - 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. - Before finishing, make sure validation does not leave generated or formatting-only diffs behind.

View file

@ -26,7 +26,7 @@ Set up your GitHub Actions workflow with a specific version of [uv](https://docs
```yaml ```yaml
- name: Install the latest version of uv - name: Install the latest version of uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
``` ```
If you do not specify a version, this action will look for a [required-version](https://docs.astral.sh/uv/reference/settings/#required-version) If you do not specify a version, this action will look for a [required-version](https://docs.astral.sh/uv/reference/settings/#required-version)
@ -42,7 +42,7 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed
```yaml ```yaml
- name: Install uv with all available options - name: Install uv with all available options
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
# The version of uv to install (default: searches for version in config files, then latest) # The version of uv to install (default: searches for version in config files, then latest)
version: "" version: ""
@ -114,7 +114,7 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed
# Custom path to set UV_TOOL_BIN_DIR to # Custom path to set UV_TOOL_BIN_DIR to
tool-bin-dir: "" tool-bin-dir: ""
# URL to a custom manifest file in the astral-sh/versions format # URL to a custom manifest file (NDJSON preferred, legacy JSON array is deprecated)
manifest-file: "" manifest-file: ""
# Add problem matchers # Add problem matchers
@ -139,7 +139,7 @@ This will override any python version specifications in `pyproject.toml` and `.p
```yaml ```yaml
- name: Install the latest version of uv and set the python version to 3.13t - name: Install the latest version of uv and set the python version to 3.13t
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
python-version: 3.13t python-version: 3.13t
- run: uv pip install --python=3.13t pip - run: uv pip install --python=3.13t pip
@ -157,7 +157,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Install the latest version of uv and set the python version - name: Install the latest version of uv and set the python version
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Test with python ${{ matrix.python-version }} - name: Test with python ${{ matrix.python-version }}
@ -174,7 +174,7 @@ It also controls where [the venv gets created](#activate-environment), unless `v
```yaml ```yaml
- name: Install uv based on the config files in the working-directory - name: Install uv based on the config files in the working-directory
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
working-directory: my/subproject/dir working-directory: my/subproject/dir
``` ```
@ -190,8 +190,8 @@ For more advanced configuration options, see our detailed documentation:
## How it works ## How it works
By default, this action resolves uv versions from the By default, this action resolves uv versions from
[`astral-sh/versions`](https://github.com/astral-sh/versions) manifest and downloads uv from the [`astral-sh/versions`](https://github.com/astral-sh/versions) (NDJSON) and downloads uv from the
official [GitHub Releases](https://github.com/astral-sh/uv). 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 It then uses the [GitHub Actions Toolkit](https://github.com/actions/toolkit) to cache uv as a
@ -216,7 +216,7 @@ For example:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@main uses: actions/checkout@main
- name: Install the latest version of uv - name: Install the latest version of uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
enable-cache: true enable-cache: true
- name: Test - name: Test
@ -228,7 +228,7 @@ To install a specific version of Python, use
```yaml ```yaml
- name: Install the latest version of uv - name: Install the latest version of uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
enable-cache: true enable-cache: true
- name: Install Python 3.12 - name: Install Python 3.12
@ -247,7 +247,7 @@ output:
uses: actions/checkout@main uses: actions/checkout@main
- name: Install the default version of uv - name: Install the default version of uv
id: setup-uv id: setup-uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
- name: Print the installed version - name: Print the installed version
run: echo "Installed uv version is ${{ steps.setup-uv.outputs.uv-version }}" run: echo "Installed uv version is ${{ steps.setup-uv.outputs.uv-version }}"
``` ```

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,10 +1,9 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import * as semver from "semver";
const mockInfo = jest.fn(); const mockInfo = jest.fn();
const mockWarning = jest.fn(); const mockWarning = jest.fn();
jest.unstable_mockModule("@actions/core", () => ({ jest.mock("@actions/core", () => ({
debug: jest.fn(), debug: jest.fn(),
info: mockInfo, info: mockInfo,
warning: mockWarning, warning: mockWarning,
@ -19,41 +18,59 @@ const mockExtractZip = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests. // biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockCacheDir = jest.fn<any>(); const mockCacheDir = jest.fn<any>();
jest.unstable_mockModule("@actions/tool-cache", () => ({ jest.mock("@actions/tool-cache", () => {
const actual = jest.requireActual("@actions/tool-cache") as Record<
string,
unknown
>;
return {
...actual,
cacheDir: mockCacheDir, cacheDir: mockCacheDir,
downloadTool: mockDownloadTool, downloadTool: mockDownloadTool,
evaluateVersions: (versions: string[], range: string) =>
semver.maxSatisfying(versions, range) ?? "",
extractTar: mockExtractTar, extractTar: mockExtractTar,
extractZip: mockExtractZip, extractZip: mockExtractZip,
find: () => "", };
findAllVersions: () => [], });
isExplicitVersion: (version: string) => semver.valid(version) !== null,
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockGetLatestVersionFromNdjson = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockGetAllVersionsFromNdjson = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockGetArtifactFromNdjson = jest.fn<any>();
jest.mock("../../src/download/versions-client", () => ({
getAllVersions: mockGetAllVersionsFromNdjson,
getArtifact: mockGetArtifactFromNdjson,
getLatestVersion: mockGetLatestVersionFromNdjson,
})); }));
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests. // biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockGetLatestVersion = jest.fn<any>(); const mockGetAllManifestVersions = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests. // biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockGetAllVersions = jest.fn<any>(); const mockGetLatestVersionInManifest = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests. // biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockGetArtifact = jest.fn<any>(); const mockGetManifestArtifact = jest.fn<any>();
jest.unstable_mockModule("../../src/download/manifest", () => ({ jest.mock("../../src/download/version-manifest", () => ({
getAllVersions: mockGetAllVersions, getAllVersions: mockGetAllManifestVersions,
getArtifact: mockGetArtifact, getLatestKnownVersion: mockGetLatestVersionInManifest,
getLatestVersion: mockGetLatestVersion, getManifestArtifact: mockGetManifestArtifact,
})); }));
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests. // biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockValidateChecksum = jest.fn<any>(); const mockValidateChecksum = jest.fn<any>();
jest.unstable_mockModule("../../src/download/checksum/checksum", () => ({ jest.mock("../../src/download/checksum/checksum", () => ({
validateChecksum: mockValidateChecksum, validateChecksum: mockValidateChecksum,
})); }));
const { downloadVersion, resolveVersion, rewriteToMirror } = await import( import {
"../../src/download/download-version" downloadVersionFromManifest,
); downloadVersionFromNdjson,
resolveVersion,
} from "../../src/download/download-version";
describe("download-version", () => { describe("download-version", () => {
beforeEach(() => { beforeEach(() => {
@ -63,9 +80,12 @@ describe("download-version", () => {
mockExtractTar.mockReset(); mockExtractTar.mockReset();
mockExtractZip.mockReset(); mockExtractZip.mockReset();
mockCacheDir.mockReset(); mockCacheDir.mockReset();
mockGetLatestVersion.mockReset(); mockGetLatestVersionFromNdjson.mockReset();
mockGetAllVersions.mockReset(); mockGetAllVersionsFromNdjson.mockReset();
mockGetArtifact.mockReset(); mockGetArtifactFromNdjson.mockReset();
mockGetAllManifestVersions.mockReset();
mockGetLatestVersionInManifest.mockReset();
mockGetManifestArtifact.mockReset();
mockValidateChecksum.mockReset(); mockValidateChecksum.mockReset();
mockDownloadTool.mockResolvedValue("/tmp/downloaded"); mockDownloadTool.mockResolvedValue("/tmp/downloaded");
@ -75,28 +95,36 @@ describe("download-version", () => {
}); });
describe("resolveVersion", () => { describe("resolveVersion", () => {
it("uses the default manifest to resolve latest", async () => { it("uses astral-sh/versions to resolve latest", async () => {
mockGetLatestVersion.mockResolvedValue("0.9.26"); mockGetLatestVersionFromNdjson.mockResolvedValue("0.9.26");
const version = await resolveVersion("latest", undefined); const version = await resolveVersion("latest", undefined);
expect(version).toBe("0.9.26"); expect(version).toBe("0.9.26");
expect(mockGetLatestVersion).toHaveBeenCalledTimes(1); expect(mockGetLatestVersionFromNdjson).toHaveBeenCalledTimes(1);
expect(mockGetLatestVersion).toHaveBeenCalledWith(undefined);
}); });
it("uses the default manifest to resolve available versions", async () => { it("uses astral-sh/versions to resolve available versions", async () => {
mockGetAllVersions.mockResolvedValue(["0.9.26", "0.9.25"]); mockGetAllVersionsFromNdjson.mockResolvedValue(["0.9.26", "0.9.25"]);
const version = await resolveVersion("^0.9.0", undefined); const version = await resolveVersion("^0.9.0", undefined);
expect(version).toBe("0.9.26"); expect(version).toBe("0.9.26");
expect(mockGetAllVersions).toHaveBeenCalledTimes(1); expect(mockGetAllVersionsFromNdjson).toHaveBeenCalledTimes(1);
expect(mockGetAllVersions).toHaveBeenCalledWith(undefined); });
it("does not fall back when astral-sh/versions fails", async () => {
mockGetLatestVersionFromNdjson.mockRejectedValue(
new Error("NDJSON unavailable"),
);
await expect(resolveVersion("latest", undefined)).rejects.toThrow(
"NDJSON unavailable",
);
}); });
it("uses manifest-file when provided", async () => { it("uses manifest-file when provided", async () => {
mockGetAllVersions.mockResolvedValue(["0.9.26", "0.9.25"]); mockGetAllManifestVersions.mockResolvedValue(["0.9.26", "0.9.25"]);
const version = await resolveVersion( const version = await resolveVersion(
"^0.9.0", "^0.9.0",
@ -104,35 +132,37 @@ describe("download-version", () => {
); );
expect(version).toBe("0.9.26"); expect(version).toBe("0.9.26");
expect(mockGetAllVersions).toHaveBeenCalledWith( expect(mockGetAllManifestVersions).toHaveBeenCalledWith(
"https://example.com/custom.ndjson", "https://example.com/custom.ndjson",
); );
}); });
}); });
describe("downloadVersion", () => { describe("downloadVersionFromNdjson", () => {
it("fails when manifest lookup fails", async () => { it("fails when NDJSON metadata lookup fails", async () => {
mockGetArtifact.mockRejectedValue(new Error("manifest unavailable")); mockGetArtifactFromNdjson.mockRejectedValue(
new Error("NDJSON unavailable"),
);
await expect( await expect(
downloadVersion( downloadVersionFromNdjson(
"unknown-linux-gnu", "unknown-linux-gnu",
"x86_64", "x86_64",
"0.9.26", "0.9.26",
undefined, undefined,
"token", "token",
), ),
).rejects.toThrow("manifest unavailable"); ).rejects.toThrow("NDJSON unavailable");
expect(mockDownloadTool).not.toHaveBeenCalled(); expect(mockDownloadTool).not.toHaveBeenCalled();
expect(mockValidateChecksum).not.toHaveBeenCalled(); expect(mockValidateChecksum).not.toHaveBeenCalled();
}); });
it("fails when no matching artifact exists in the default manifest", async () => { it("fails when no matching artifact exists in NDJSON metadata", async () => {
mockGetArtifact.mockResolvedValue(undefined); mockGetArtifactFromNdjson.mockResolvedValue(undefined);
await expect( await expect(
downloadVersion( downloadVersionFromNdjson(
"unknown-linux-gnu", "unknown-linux-gnu",
"x86_64", "x86_64",
"0.9.26", "0.9.26",
@ -147,14 +177,14 @@ describe("download-version", () => {
expect(mockValidateChecksum).not.toHaveBeenCalled(); expect(mockValidateChecksum).not.toHaveBeenCalled();
}); });
it("uses built-in checksums for default manifest downloads", async () => { it("uses built-in checksums for default NDJSON downloads", async () => {
mockGetArtifact.mockResolvedValue({ mockGetArtifactFromNdjson.mockResolvedValue({
archiveFormat: "tar.gz", archiveFormat: "tar.gz",
checksum: "manifest-checksum-that-should-be-ignored", sha256: "ndjson-checksum-that-should-be-ignored",
downloadUrl: "https://example.com/uv.tar.gz", url: "https://example.com/uv.tar.gz",
}); });
await downloadVersion( await downloadVersionFromNdjson(
"unknown-linux-gnu", "unknown-linux-gnu",
"x86_64", "x86_64",
"0.9.26", "0.9.26",
@ -170,126 +200,23 @@ describe("download-version", () => {
"0.9.26", "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);
}); });
describe("downloadVersionFromManifest", () => {
it("uses manifest-file checksum metadata when checksum input is unset", async () => { it("uses manifest-file checksum metadata when checksum input is unset", async () => {
mockGetArtifact.mockResolvedValue({ mockGetManifestArtifact.mockResolvedValue({
archiveFormat: "tar.gz", archiveFormat: "tar.gz",
checksum: "manifest-checksum", checksum: "manifest-checksum",
downloadUrl: "https://example.com/custom-uv.tar.gz", downloadUrl: "https://example.com/custom-uv.tar.gz",
}); });
await downloadVersion( await downloadVersionFromManifest(
"https://example.com/custom.ndjson",
"unknown-linux-gnu", "unknown-linux-gnu",
"x86_64", "x86_64",
"0.9.26", "0.9.26",
"", "",
"token", "token",
"https://example.com/custom.ndjson",
); );
expect(mockValidateChecksum).toHaveBeenCalledWith( expect(mockValidateChecksum).toHaveBeenCalledWith(
@ -302,19 +229,19 @@ describe("download-version", () => {
}); });
it("prefers checksum input over manifest-file checksum metadata", async () => { it("prefers checksum input over manifest-file checksum metadata", async () => {
mockGetArtifact.mockResolvedValue({ mockGetManifestArtifact.mockResolvedValue({
archiveFormat: "tar.gz", archiveFormat: "tar.gz",
checksum: "manifest-checksum", checksum: "manifest-checksum",
downloadUrl: "https://example.com/custom-uv.tar.gz", downloadUrl: "https://example.com/custom-uv.tar.gz",
}); });
await downloadVersion( await downloadVersionFromManifest(
"https://example.com/custom.ndjson",
"unknown-linux-gnu", "unknown-linux-gnu",
"x86_64", "x86_64",
"0.9.26", "0.9.26",
"user-checksum", "user-checksum",
"token", "token",
"https://example.com/custom.ndjson",
); );
expect(mockValidateChecksum).toHaveBeenCalledWith( expect(mockValidateChecksum).toHaveBeenCalledWith(
@ -326,28 +253,4 @@ describe("download-version", () => {
); );
}); });
}); });
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

@ -0,0 +1,136 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
const mockWarning = jest.fn();
jest.mock("@actions/core", () => ({
debug: jest.fn(),
info: jest.fn(),
warning: mockWarning,
}));
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockFetch = jest.fn<any>();
jest.mock("../../src/utils/fetch", () => ({
fetch: mockFetch,
}));
import {
clearManifestCache,
getAllVersions,
getLatestKnownVersion,
getManifestArtifact,
} from "../../src/download/version-manifest";
const legacyManifestResponse = JSON.stringify([
{
arch: "x86_64",
artifactName: "uv-x86_64-unknown-linux-gnu.tar.gz",
downloadUrl:
"https://example.com/releases/download/0.7.12-alpha.1/uv-x86_64-unknown-linux-gnu.tar.gz",
platform: "unknown-linux-gnu",
version: "0.7.12-alpha.1",
},
{
arch: "x86_64",
artifactName: "uv-x86_64-unknown-linux-gnu.tar.gz",
downloadUrl:
"https://example.com/releases/download/0.7.13/uv-x86_64-unknown-linux-gnu.tar.gz",
platform: "unknown-linux-gnu",
version: "0.7.13",
},
]);
const ndjsonManifestResponse = `{"version":"0.10.0","artifacts":[{"platform":"x86_64-unknown-linux-gnu","variant":"default","url":"https://example.com/releases/download/0.10.0/uv-x86_64-unknown-linux-gnu.tar.gz","archive_format":"tar.gz","sha256":"checksum-100"}]}
{"version":"0.9.30","artifacts":[{"platform":"x86_64-unknown-linux-gnu","variant":"default","url":"https://example.com/releases/download/0.9.30/uv-x86_64-unknown-linux-gnu.tar.gz","archive_format":"tar.gz","sha256":"checksum-0930"}]}`;
const multiVariantManifestResponse = `{"version":"0.10.0","artifacts":[{"platform":"x86_64-unknown-linux-gnu","variant":"managed-python","url":"https://example.com/releases/download/0.10.0/uv-x86_64-unknown-linux-gnu-managed-python.tar.gz","archive_format":"tar.gz","sha256":"checksum-managed"},{"platform":"x86_64-unknown-linux-gnu","variant":"default","url":"https://example.com/releases/download/0.10.0/uv-x86_64-unknown-linux-gnu-default.zip","archive_format":"zip","sha256":"checksum-default"}]}`;
function createMockResponse(
ok: boolean,
status: number,
statusText: string,
data: string,
) {
return {
ok,
status,
statusText,
text: async () => data,
};
}
describe("version-manifest", () => {
beforeEach(() => {
clearManifestCache();
mockFetch.mockReset();
mockWarning.mockReset();
});
it("supports the legacy JSON manifest format", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", legacyManifestResponse),
);
const latest = await getLatestKnownVersion(
"https://example.com/legacy.json",
);
const artifact = await getManifestArtifact(
"https://example.com/legacy.json",
"0.7.13",
"x86_64",
"unknown-linux-gnu",
);
expect(latest).toBe("0.7.13");
expect(artifact).toEqual({
archiveFormat: undefined,
checksum: undefined,
downloadUrl:
"https://example.com/releases/download/0.7.13/uv-x86_64-unknown-linux-gnu.tar.gz",
});
expect(mockWarning).toHaveBeenCalledTimes(1);
});
it("supports NDJSON manifests", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", ndjsonManifestResponse),
);
const versions = await getAllVersions("https://example.com/custom.ndjson");
const artifact = await getManifestArtifact(
"https://example.com/custom.ndjson",
"0.10.0",
"x86_64",
"unknown-linux-gnu",
);
expect(versions).toEqual(["0.10.0", "0.9.30"]);
expect(artifact).toEqual({
archiveFormat: "tar.gz",
checksum: "checksum-100",
downloadUrl:
"https://example.com/releases/download/0.10.0/uv-x86_64-unknown-linux-gnu.tar.gz",
});
expect(mockWarning).not.toHaveBeenCalled();
});
it("prefers the default variant when a manifest contains multiple variants", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", multiVariantManifestResponse),
);
const artifact = await getManifestArtifact(
"https://example.com/multi-variant.ndjson",
"0.10.0",
"x86_64",
"unknown-linux-gnu",
);
expect(artifact).toEqual({
archiveFormat: "zip",
checksum: "checksum-default",
downloadUrl:
"https://example.com/releases/download/0.10.0/uv-x86_64-unknown-linux-gnu-default.zip",
});
});
});

View file

@ -0,0 +1,169 @@
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.mock("../../src/utils/fetch", () => ({
fetch: mockFetch,
}));
import {
clearCache,
fetchVersionData,
getAllVersions,
getArtifact,
getLatestVersion,
parseVersionData,
} from "../../src/download/versions-client";
const sampleNdjsonResponse = `{"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 multiVariantNdjsonResponse = `{"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("versions-client", () => {
beforeEach(() => {
clearCache();
mockFetch.mockReset();
});
describe("fetchVersionData", () => {
it("should fetch and parse NDJSON data", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleNdjsonResponse),
);
const versions = await fetchVersionData();
expect(versions).toHaveLength(2);
expect(versions[0].version).toBe("0.9.26");
expect(versions[1].version).toBe("0.9.25");
});
it("should throw error on failed fetch", async () => {
mockFetch.mockResolvedValue(
createMockResponse(false, 500, "Internal Server Error", ""),
);
await expect(fetchVersionData()).rejects.toThrow(
"Failed to fetch version data: 500 Internal Server Error",
);
});
it("should cache results", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleNdjsonResponse),
);
await fetchVersionData();
await fetchVersionData();
expect(mockFetch).toHaveBeenCalledTimes(1);
});
});
describe("getLatestVersion", () => {
it("should return the first version (newest)", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleNdjsonResponse),
);
const latest = await getLatestVersion();
expect(latest).toBe("0.9.26");
});
});
describe("getAllVersions", () => {
it("should return all version strings", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleNdjsonResponse),
);
const versions = await getAllVersions();
expect(versions).toEqual(["0.9.26", "0.9.25"]);
});
});
describe("getArtifact", () => {
beforeEach(() => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleNdjsonResponse),
);
});
it("should find artifact by version and platform", async () => {
const artifact = await getArtifact("0.9.26", "aarch64", "apple-darwin");
expect(artifact).toEqual({
archiveFormat: "tar.gz",
sha256:
"fcf0a9ea6599c6ae28a4c854ac6da76f2c889354d7c36ce136ef071f7ab9721f",
url: "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-aarch64-apple-darwin.tar.gz",
});
});
it("should find windows artifact", async () => {
const artifact = await getArtifact("0.9.26", "x86_64", "pc-windows-msvc");
expect(artifact).toEqual({
archiveFormat: "zip",
sha256:
"eb02fd95d8e0eed462b4a67ecdd320d865b38c560bffcda9a0b87ec944bdf036",
url: "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-pc-windows-msvc.zip",
});
});
it("should prefer the default variant when multiple artifacts share a platform", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", multiVariantNdjsonResponse),
);
const artifact = await getArtifact("0.9.26", "aarch64", "apple-darwin");
expect(artifact).toEqual({
archiveFormat: "zip",
sha256: "default-checksum",
url: "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-aarch64-apple-darwin.zip",
});
});
it("should return undefined for unknown version", async () => {
const artifact = await getArtifact("0.0.1", "aarch64", "apple-darwin");
expect(artifact).toBeUndefined();
});
it("should return undefined for unknown platform", async () => {
const artifact = await getArtifact(
"0.9.26",
"aarch64",
"unknown-linux-musl",
);
expect(artifact).toBeUndefined();
});
});
describe("parseVersionData", () => {
it("should throw for malformed NDJSON", () => {
expect(() =>
parseVersionData('{"version":"0.1.0"', "test-source"),
).toThrow("Failed to parse version data from test-source");
});
});
});

View file

@ -1,6 +1,14 @@
import fs from "node:fs"; jest.mock("@actions/core", () => {
import os from "node:os"; return {
import path from "node:path"; debug: jest.fn(),
getBooleanInput: jest.fn(
(name: string) => (mockInputs[name] ?? "") === "true",
),
getInput: jest.fn((name: string) => mockInputs[name] ?? ""),
warning: jest.fn(),
};
});
import { import {
afterEach, afterEach,
beforeEach, beforeEach,
@ -10,244 +18,120 @@ import {
jest, jest,
} from "@jest/globals"; } from "@jest/globals";
// Will be mutated per test before (re-)importing the module under test
let mockInputs: Record<string, string> = {}; let mockInputs: Record<string, string> = {};
const tempDirs: string[] = [];
const ORIGINAL_HOME = process.env.HOME; 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(); describe("cacheDependencyGlob", () => {
const mockGetBooleanInput = jest.fn( beforeEach(() => {
(name: string) => (mockInputs[name] ?? "") === "true", jest.resetModules();
);
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(); jest.clearAllMocks();
mockInputs = {}; mockInputs = {};
process.env.HOME = "/home/testuser"; 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 });
}
afterEach(() => {
process.env.HOME = ORIGINAL_HOME; 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); it("returns empty string when input not provided", async () => {
afterEach(restoreEnvironment);
describe("loadInputs", () => {
it("loads defaults for a github-hosted runner", () => {
mockInputs["working-directory"] = "/workspace"; mockInputs["working-directory"] = "/workspace";
mockInputs["enable-cache"] = "auto"; const { cacheDependencyGlob } = await import("../../src/utils/inputs");
process.env.RUNNER_ENVIRONMENT = "github-hosted"; expect(cacheDependencyGlob).toBe("");
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", () => { it("resolves a single relative path", async () => {
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"; mockInputs["working-directory"] = "/workspace";
process.env.UV_PYTHON_INSTALL_DIR = "/env/python-dir"; mockInputs["cache-dependency-glob"] = "requirements.txt";
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
const inputs = loadInputs(); expect(cacheDependencyGlob).toBe("/workspace/requirements.txt");
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", () => { it("strips leading ./ from relative path", async () => {
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["working-directory"] = "/workspace";
mockInputs["resolution-strategy"] = "middle"; mockInputs["cache-dependency-glob"] = "./uv.lock";
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
expect(() => loadInputs()).toThrow( expect(cacheDependencyGlob).toBe("/workspace/uv.lock");
"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("");
}); });
it.each([ it("handles multiple lines, trimming whitespace, tilde expansion and absolute paths", async () => {
["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);
});
it("handles multiple lines, trimming whitespace, tilde expansion and absolute paths", () => {
mockInputs["working-directory"] = "/workspace"; mockInputs["working-directory"] = "/workspace";
mockInputs["cache-dependency-glob"] = mockInputs["cache-dependency-glob"] =
" ~/.cache/file1\n ./rel/file2 \nfile3.txt"; " ~/.cache/file1\n ./rel/file2 \nfile3.txt";
const { cacheDependencyGlob } = await import("../../src/utils/inputs");
const inputs = loadInputs(); expect(cacheDependencyGlob).toBe(
expect(inputs.cacheDependencyGlob).toBe(
[ [
"/home/testuser/.cache/file1", "/home/testuser/.cache/file1", // expanded tilde, absolute path unchanged
"/workspace/rel/file2", "/workspace/rel/file2", // ./ stripped and resolved
"/workspace/file3.txt", "/workspace/file3.txt", // relative path resolved
].join("\n"), ].join("\n"),
); );
}); });
it.each([ it("keeps absolute path unchanged in multiline input", async () => {
[
"/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) => {
mockInputs["working-directory"] = "/workspace"; 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(); it("handles exclusions in relative paths correct", async () => {
mockInputs["working-directory"] = "/workspace";
expect(inputs.cacheDependencyGlob).toBe(expected); 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", () => { 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["working-directory"] = "/workspace";
mockInputs["tool-bin-dir"] = "~/tool-bin-dir"; mockInputs["tool-bin-dir"] = "~/tool-bin-dir";
mockInputs["tool-dir"] = "~/tool-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(toolBinDir).toBe("/home/testuser/tool-bin-dir");
expect(inputs.toolDir).toBe("/home/testuser/tool-dir"); expect(toolDir).toBe("/home/testuser/tool-dir");
}); });
}); });
describe("cacheLocalPath", () => { 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["working-directory"] = "/workspace";
mockInputs["cache-local-path"] = "~/uv-cache/cache-local-path"; 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", path: "/home/testuser/uv-cache/cache-local-path",
source: CacheLocalSource.Input, source: CacheLocalSource.Input,
}); });
@ -255,38 +139,71 @@ describe("cacheLocalPath", () => {
}); });
describe("venvPath", () => { describe("venvPath", () => {
it("defaults to .venv in the working directory", () => { beforeEach(() => {
mockInputs["working-directory"] = "/workspace"; jest.resetModules();
jest.clearAllMocks();
const inputs = loadInputs(); mockInputs = {};
process.env.HOME = "/home/testuser";
expect(inputs.venvPath).toBe("/workspace/.venv");
}); });
it.each([ afterEach(() => {
["custom-venv", "/workspace/custom-venv"], process.env.HOME = ORIGINAL_HOME;
["custom-venv/", "/workspace/custom-venv"], });
["/tmp/custom-venv", "/tmp/custom-venv"],
["~/.venv", "/home/testuser/.venv"], it("defaults to .venv in the working directory", async () => {
])("resolves venv-path %s to %s", (venvPathInput, expected) => { 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["working-directory"] = "/workspace";
mockInputs["activate-environment"] = "true"; mockInputs["activate-environment"] = "true";
mockInputs["venv-path"] = venvPathInput; mockInputs["venv-path"] = "custom-venv";
const { venvPath } = await import("../../src/utils/inputs");
const inputs = loadInputs(); expect(venvPath).toBe("/workspace/custom-venv");
expect(inputs.venvPath).toBe(expected);
}); });
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["working-directory"] = "/workspace";
mockInputs["venv-path"] = "custom-venv"; mockInputs["venv-path"] = "custom-venv";
const inputs = loadInputs(); const { activateEnvironment, venvPath } = await import(
"../../src/utils/inputs"
);
expect(inputs.activateEnvironment).toBe(false); expect(activateEnvironment).toBe(false);
expect(inputs.venvPath).toBe("/workspace/custom-venv"); expect(venvPath).toBe("/workspace/custom-venv");
expect(mockWarning).toHaveBeenCalledWith(
const mockedCore = jest.requireMock("@actions/core") as {
warning: jest.Mock;
};
expect(mockedCore.warning).toHaveBeenCalledWith(
"venv-path is only used when activate-environment is true", "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 { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { getUvVersionFromToolVersions } from "../../src/version/tool-versions-file";
const mockReadFileSync = jest.fn(); const mockedFs = fs as jest.Mocked<typeof fs>;
const mockWarning = jest.fn(); const mockedCore = core as jest.Mocked<typeof core>;
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);
}
describe("getUvVersionFromToolVersions", () => { describe("getUvVersionFromToolVersions", () => {
beforeEach(() => { beforeEach(() => {
jest.resetModules();
jest.clearAllMocks(); jest.clearAllMocks();
}); });
it("should return undefined for non-.tool-versions files", async () => { it("should return undefined for non-.tool-versions files", () => {
const result = await getVersionFromToolVersions("package.json"); const result = getUvVersionFromToolVersions("package.json");
expect(result).toBeUndefined(); 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"; 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(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"; 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"); 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 "; 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"); 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"; 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"); 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"; 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"); 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"; 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(); expect(result).toBeUndefined();
}); });
it("should return undefined for empty file", async () => { it("should return undefined for empty file", () => {
mockReadFileSync.mockReturnValue(""); mockedFs.readFileSync.mockReturnValue("");
const result = await getVersionFromToolVersions(".tool-versions"); const result = getUvVersionFromToolVersions(".tool-versions");
expect(result).toBeUndefined(); 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"; 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(result).toBeUndefined();
expect(mockWarning).toHaveBeenCalledWith( expect(mockedCore.warning).toHaveBeenCalledWith(
"The ref syntax of .tool-versions is not supported. Please use a released version instead.", "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"; 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(result).toBe("0.1.0");
expect(mockReadFileSync).toHaveBeenCalledWith( expect(mockedFs.readFileSync).toHaveBeenCalledWith(
"path/to/.tool-versions", "path/to/.tool-versions",
"utf8", "utf8",
); );

View file

@ -75,7 +75,7 @@ inputs:
description: "Custom path to set UV_TOOL_BIN_DIR to." description: "Custom path to set UV_TOOL_BIN_DIR to."
required: false required: false
manifest-file: manifest-file:
description: "URL to a custom manifest file in the astral-sh/versions format." description: "URL to a custom manifest file. Supports the astral-sh/versions NDJSON format and the legacy JSON array format (deprecated)."
required: false required: false
add-problem-matchers: add-problem-matchers:
description: "Add problem matchers." description: "Add problem matchers."
@ -102,8 +102,8 @@ outputs:
description: "A boolean value to indicate the Python cache entry was found" description: "A boolean value to indicate the Python cache entry was found"
runs: runs:
using: "node24" using: "node24"
main: "dist/setup/index.cjs" main: "dist/setup/index.js"
post: "dist/save-cache/index.cjs" post: "dist/save-cache/index.js"
post-if: success() post-if: success()
branding: branding:
icon: "package" 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": { "assist": {
"actions": { "actions": {
"source": { "source": {

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

97179
dist/setup/index.cjs generated vendored

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

33985
dist/update-known-checksums/index.js generated vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -6,7 +6,7 @@ This document covers advanced options for configuring which version of uv to ins
```yaml ```yaml
- name: Install the latest version of uv - name: Install the latest version of uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
version: "latest" version: "latest"
``` ```
@ -15,7 +15,7 @@ This document covers advanced options for configuring which version of uv to ins
```yaml ```yaml
- name: Install a specific version of uv - name: Install a specific version of uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
version: "0.4.4" version: "0.4.4"
``` ```
@ -28,21 +28,21 @@ to install the latest version that satisfies the range.
```yaml ```yaml
- name: Install a semver range of uv - name: Install a semver range of uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
version: ">=0.4.0" version: ">=0.4.0"
``` ```
```yaml ```yaml
- name: Pinning a minor version of uv - name: Pinning a minor version of uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
version: "0.4.x" version: "0.4.x"
``` ```
```yaml ```yaml
- name: Install a pep440-specifier-satisfying version of uv - name: Install a pep440-specifier-satisfying version of uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
version: ">=0.4.25,<0.5" version: ">=0.4.25,<0.5"
``` ```
@ -54,7 +54,7 @@ You can change this behavior using the `resolution-strategy` input:
```yaml ```yaml
- name: Install the lowest compatible version of uv - name: Install the lowest compatible version of uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
version: ">=0.4.0" version: ">=0.4.0"
resolution-strategy: "lowest" resolution-strategy: "lowest"
@ -76,7 +76,7 @@ uv defined as a dependency in `pyproject.toml` or `requirements.txt`.
```yaml ```yaml
- name: Install uv based on the version defined in pyproject.toml - name: Install uv based on the version defined in pyproject.toml
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
version-file: "pyproject.toml" version-file: "pyproject.toml"
``` ```

View file

@ -23,7 +23,7 @@ The computed cache key is available as the `cache-key` output:
```yaml ```yaml
- name: Setup uv - name: Setup uv
id: setup-uv id: setup-uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
enable-cache: true enable-cache: true
- name: Print cache key - name: Print cache key
@ -50,7 +50,7 @@ You can optionally define a custom cache key suffix.
```yaml ```yaml
- name: Enable caching and define a custom cache key suffix - name: Enable caching and define a custom cache key suffix
id: setup-uv id: setup-uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
enable-cache: true enable-cache: true
cache-suffix: "optional-suffix" cache-suffix: "optional-suffix"
@ -89,7 +89,7 @@ changes. If you use relative paths, they are relative to the working directory.
```yaml ```yaml
- name: Define a cache dependency glob - name: Define a cache dependency glob
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
enable-cache: true enable-cache: true
cache-dependency-glob: "**/pyproject.toml" cache-dependency-glob: "**/pyproject.toml"
@ -97,7 +97,7 @@ changes. If you use relative paths, they are relative to the working directory.
```yaml ```yaml
- name: Define a list of cache dependency globs - name: Define a list of cache dependency globs
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
enable-cache: true enable-cache: true
cache-dependency-glob: | cache-dependency-glob: |
@ -107,7 +107,7 @@ changes. If you use relative paths, they are relative to the working directory.
```yaml ```yaml
- name: Define an absolute cache dependency glob - name: Define an absolute cache dependency glob
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
enable-cache: true enable-cache: true
cache-dependency-glob: "/tmp/my-folder/requirements*.txt" cache-dependency-glob: "/tmp/my-folder/requirements*.txt"
@ -115,7 +115,7 @@ changes. If you use relative paths, they are relative to the working directory.
```yaml ```yaml
- name: Never invalidate the cache - name: Never invalidate the cache
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
enable-cache: true enable-cache: true
cache-dependency-glob: "" cache-dependency-glob: ""
@ -128,7 +128,7 @@ By default, the cache will be restored.
```yaml ```yaml
- name: Don't restore an existing cache - name: Don't restore an existing cache
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
enable-cache: true enable-cache: true
restore-cache: false restore-cache: false
@ -142,7 +142,7 @@ By default, the cache will be saved.
```yaml ```yaml
- name: Don't save the cache after the run - name: Don't save the cache after the run
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
enable-cache: true enable-cache: true
save-cache: false save-cache: false
@ -168,7 +168,7 @@ It defaults to `setup-uv-cache` in the `TMP` dir, `D:\a\_temp\setup-uv-cache` on
```yaml ```yaml
- name: Define a custom uv cache path - name: Define a custom uv cache path
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
cache-local-path: "/path/to/cache" cache-local-path: "/path/to/cache"
``` ```
@ -187,7 +187,7 @@ input.
```yaml ```yaml
- name: Don't prune the cache before saving it - name: Don't prune the cache before saving it
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
enable-cache: true enable-cache: true
prune-cache: false prune-cache: false
@ -205,7 +205,7 @@ To force managed Python installs, set `UV_PYTHON_PREFERENCE=only-managed`.
```yaml ```yaml
- name: Cache Python installs - name: Cache Python installs
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
enable-cache: true enable-cache: true
cache-python: true cache-python: true
@ -213,17 +213,12 @@ To force managed Python installs, set `UV_PYTHON_PREFERENCE=only-managed`.
## Ignore nothing to cache ## Ignore nothing to cache
By default, the action will fail if caching is enabled but there is nothing to upload (the uv cache directory does not exist) with an error like By default, the action will fail if caching is enabled but there is nothing to upload (the uv cache directory does not exist).
```console
Error: Cache path /home/runner/.cache/uv does not exist on disk. This likely indicates that there are no dependencies to cache. Consider disabling the cache input if it is not needed.
```
If you want to ignore this, set the `ignore-nothing-to-cache` input to `true`. If you want to ignore this, set the `ignore-nothing-to-cache` input to `true`.
```yaml ```yaml
- name: Ignore nothing to cache - name: Ignore nothing to cache
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
enable-cache: true enable-cache: true
ignore-nothing-to-cache: true ignore-nothing-to-cache: true

View file

@ -10,7 +10,7 @@ are automatically verified by this action. The sha256 hashes can be found on the
```yaml ```yaml
- name: Install a specific version and validate the checksum - name: Install a specific version and validate the checksum
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
version: "0.3.1" version: "0.3.1"
checksum: "e11b01402ab645392c7ad6044db63d37e4fd1e745e015306993b07695ea5f9f8" checksum: "e11b01402ab645392c7ad6044db63d37e4fd1e745e015306993b07695ea5f9f8"
@ -19,14 +19,14 @@ are automatically verified by this action. The sha256 hashes can be found on the
## Manifest file ## Manifest file
By default, setup-uv reads version metadata from By default, setup-uv reads version metadata from
[`astral-sh/versions`](https://github.com/astral-sh/versions). [`astral-sh/versions`](https://github.com/astral-sh/versions) (NDJSON format).
The `manifest-file` input lets you override that source with your own URL, for example to test The `manifest-file` input lets you override that source with your own URL, for example to test
custom uv builds or alternate download locations. custom uv builds or alternate download locations.
### Format ### 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 must be in NDJSON format, where each line is a JSON object representing a version and its artifacts. For example:
```json ```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.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":"..."}]}
@ -37,9 +37,26 @@ setup-uv currently only supports `default` as the `variant`.
The `archive_format` field is currently ignored. The `archive_format` field is currently ignored.
### Legacy format: JSON array (deprecated)
The previous JSON array format is still supported for compatibility, but deprecated and will be
removed in a future major release.
```json
[
{
"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"
}
]
```
```yaml ```yaml
- name: Use a custom manifest file - name: Use a custom manifest file
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
manifest-file: "https://example.com/my-custom-manifest.ndjson" manifest-file: "https://example.com/my-custom-manifest.ndjson"
``` ```
@ -58,7 +75,7 @@ You can disable this by setting the `add-problem-matchers` input to `false`.
```yaml ```yaml
- name: Install the latest version of uv without problem matchers - name: Install the latest version of uv without problem matchers
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
add-problem-matchers: false add-problem-matchers: false
``` ```

View file

@ -9,7 +9,7 @@ This allows directly using it in later steps:
```yaml ```yaml
- name: Install the latest version of uv and activate the environment - name: Install the latest version of uv and activate the environment
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
activate-environment: true activate-environment: true
- run: uv pip install pip - run: uv pip install pip
@ -20,7 +20,7 @@ By default, the venv is created at `.venv` inside the `working-directory`.
You can customize the venv location with `venv-path`, for example to place it in the runner temp directory: You can customize the venv location with `venv-path`, for example to place it in the runner temp directory:
```yaml ```yaml
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - uses: astral-sh/setup-uv@v7
with: with:
activate-environment: true activate-environment: true
venv-path: ${{ runner.temp }}/custom-venv venv-path: ${{ runner.temp }}/custom-venv
@ -51,7 +51,7 @@ are not sufficient, you can provide a custom GitHub token with the necessary per
```yaml ```yaml
- name: Install the latest version of uv with a custom GitHub token - name: Install the latest version of uv with a custom GitHub token
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
github-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} github-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
``` ```
@ -69,7 +69,7 @@ input:
```yaml ```yaml
- name: Install the latest version of uv with a custom tool dir - name: Install the latest version of uv with a custom tool dir
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
tool-dir: "/path/to/tool/dir" tool-dir: "/path/to/tool/dir"
``` ```
@ -88,7 +88,7 @@ If you want to change this behaviour (especially on self-hosted runners) you can
```yaml ```yaml
- name: Install the latest version of uv with a custom tool bin dir - name: Install the latest version of uv with a custom tool bin dir
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
tool-bin-dir: "/path/to/tool-bin/dir" tool-bin-dir: "/path/to/tool-bin/dir"
``` ```
@ -105,7 +105,7 @@ This action supports expanding the `~` character to the user's home directory fo
```yaml ```yaml
- name: Expand the tilde character - name: Expand the tilde character
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
cache-local-path: "~/path/to/cache" cache-local-path: "~/path/to/cache"
tool-dir: "~/path/to/tool/dir" tool-dir: "~/path/to/tool/dir"
@ -122,7 +122,7 @@ If you want to ignore this, set the `ignore-empty-workdir` input to `true`.
```yaml ```yaml
- name: Ignore empty workdir - name: Ignore empty workdir
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
ignore-empty-workdir: true ignore-empty-workdir: true
``` ```
@ -145,7 +145,7 @@ This action sets several environment variables that influence uv's behavior and
```yaml ```yaml
- name: Example using environment variables - name: Example using environment variables
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@v7
with: with:
python-version: "3.12" python-version: "3.12"
tool-dir: "/custom/tool/dir" tool-dir: "/custom/tool/dir"

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,
};

4134
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,18 +2,16 @@
"name": "setup-uv", "name": "setup-uv",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"type": "module",
"description": "Set up your GitHub Actions workflow with a specific version of uv", "description": "Set up your GitHub Actions workflow with a specific version of uv",
"main": "dist/setup/index.cjs", "main": "dist/index.js",
"scripts": { "scripts": {
"build": "tsc --noEmit", "build": "tsc",
"check": "biome check --write", "check": "biome check --write",
"package": "node scripts/build-dist.mjs", "package": "ncc build -o dist/setup src/setup-uv.ts && ncc build -o dist/save-cache src/save-cache.ts && ncc build -o dist/update-known-checksums src/update-known-checksums.ts",
"test:unit": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", "test": "jest",
"test": "npm run build && npm run test:unit",
"act": "act pull_request -W .github/workflows/test.yml --container-architecture linux/amd64 -s GITHUB_TOKEN=\"$(gh auth token)\"", "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", "update-known-checksums": "RUNNER_TEMP=known_versions node dist/update-known-checksums/index.js src/download/checksum/known-checksums.ts",
"all": "npm run build && npm run check && npm run package && npm run test:unit" "all": "npm run build && npm run check && npm run package && npm test"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -28,26 +26,25 @@
"author": "@eifinger", "author": "@eifinger",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/cache": "^6.0.0", "@actions/cache": "^4.1.0",
"@actions/core": "^3.0.0", "@actions/core": "^1.11.1",
"@actions/exec": "^3.0.0", "@actions/exec": "^1.1.1",
"@actions/glob": "^0.6.1", "@actions/glob": "^0.5.0",
"@actions/io": "^3.0.2", "@actions/io": "^1.1.3",
"@actions/tool-cache": "^4.0.0", "@actions/tool-cache": "^2.0.2",
"@renovatebot/pep440": "^4.2.2", "@renovatebot/pep440": "^4.2.1",
"smol-toml": "^1.6.0", "smol-toml": "^1.6.0",
"undici": "^7.24.2" "undici": "5.28.5"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.7", "@biomejs/biome": "2.3.8",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/node": "^25.5.0", "@types/node": "^24.10.1",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@vercel/ncc": "^0.38.4", "@vercel/ncc": "^0.38.4",
"esbuild": "^0.27.4", "jest": "^30.2.0",
"jest": "^30.3.0", "js-yaml": "^4.1.0",
"js-yaml": "^4.1.1", "ts-jest": "^29.4.5",
"ts-jest": "^29.4.6",
"typescript": "^5.9.3" "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 cache from "@actions/cache";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { hashFiles } from "../hash/hash-files"; 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"; import { getArch, getOSNameVersion, getPlatform } from "../utils/platforms";
export const STATE_CACHE_KEY = "cache-key"; 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"; const CACHE_VERSION = "2";
export async function restoreCache( export async function restoreCache(pythonVersion?: string): Promise<void> {
inputs: SetupInputs, const cacheKey = await computeKeys(pythonVersion);
pythonVersion?: string,
): Promise<void> {
const cacheKey = await computeKeys(inputs, pythonVersion);
core.saveState(STATE_CACHE_KEY, cacheKey); core.saveState(STATE_CACHE_KEY, cacheKey);
core.setOutput("cache-key", cacheKey); core.setOutput("cache-key", cacheKey);
if (!inputs.restoreCache) { if (!shouldRestoreCache) {
core.info("restore-cache is false. Skipping restore cache step."); core.info("restore-cache is false. Skipping restore cache step.");
core.setOutput("python-cache-hit", false); core.setOutput("python-cache-hit", false);
return; return;
} }
if (inputs.cacheLocalPath === undefined) { if (cacheLocalPath === undefined) {
throw new Error( throw new Error(
"cache-local-path is not set. Cannot restore cache without a valid cache path.", "cache-local-path is not set. Cannot restore cache without a valid cache path.",
); );
@ -32,15 +37,15 @@ export async function restoreCache(
await restoreCacheFromKey( await restoreCacheFromKey(
cacheKey, cacheKey,
inputs.cacheLocalPath.path, cacheLocalPath.path,
STATE_CACHE_MATCHED_KEY, STATE_CACHE_MATCHED_KEY,
"cache-hit", "cache-hit",
); );
if (inputs.cachePython) { if (cachePython) {
await restoreCacheFromKey( await restoreCacheFromKey(
`${cacheKey}-python`, `${cacheKey}-python`,
inputs.pythonDir, pythonDir,
STATE_PYTHON_CACHE_MATCHED_KEY, STATE_PYTHON_CACHE_MATCHED_KEY,
"python-cache-hit", "python-cache-hit",
); );
@ -71,34 +76,28 @@ async function restoreCacheFromKey(
handleMatchResult(matchedKey, cacheKey, stateKey, outputKey); handleMatchResult(matchedKey, cacheKey, stateKey, outputKey);
} }
async function computeKeys( async function computeKeys(pythonVersion?: string): Promise<string> {
inputs: SetupInputs,
pythonVersion?: string,
): Promise<string> {
let cacheDependencyPathHash = "-"; let cacheDependencyPathHash = "-";
if (inputs.cacheDependencyGlob !== "") { if (cacheDependencyGlob !== "") {
core.info( core.info(
`Searching files using cache dependency glob: ${inputs.cacheDependencyGlob.split("\n").join(",")}`, `Searching files using cache dependency glob: ${cacheDependencyGlob.split("\n").join(",")}`,
);
cacheDependencyPathHash += await hashFiles(
inputs.cacheDependencyGlob,
true,
); );
cacheDependencyPathHash += await hashFiles(cacheDependencyGlob, true);
if (cacheDependencyPathHash === "-") { if (cacheDependencyPathHash === "-") {
core.warning( 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 === "-") { if (cacheDependencyPathHash === "-") {
cacheDependencyPathHash = "-no-dependency-glob"; cacheDependencyPathHash = "-no-dependency-glob";
} }
const suffix = inputs.cacheSuffix ? `-${inputs.cacheSuffix}` : ""; const suffix = cacheSuffix ? `-${cacheSuffix}` : "";
const version = pythonVersion ?? "unknown"; const version = pythonVersion ?? "unknown";
const platform = await getPlatform(); const platform = await getPlatform();
const osNameVersion = getOSNameVersion(); const osNameVersion = getOSNameVersion();
const pruned = inputs.pruneCache ? "-pruned" : ""; const pruned = pruneCache ? "-pruned" : "";
const python = inputs.cachePython ? "-py" : ""; const python = cachePython ? "-py" : "";
return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform}-${osNameVersion}-${version}${pruned}${python}${cacheDependencyPathHash}${suffix}`; return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform}-${osNameVersion}-${version}${pruned}${python}${cacheDependencyPathHash}${suffix}`;
} }

View file

@ -1,249 +1,5 @@
// AUTOGENERATED_DO_NOT_EDIT // AUTOGENERATED_DO_NOT_EDIT
export const KNOWN_CHECKSUMS: { [key: string]: string } = { export const KNOWN_CHECKSUMS: { [key: string]: string } = {
"aarch64-apple-darwin-0.11.3":
"2bc3d0c7bf2bd08325b1e170abac6f7e5b3346e1d4eab3370d17cefec934996f",
"aarch64-pc-windows-msvc-0.11.3":
"e99c56f9ab5e1e1ddcaea3e2389990c94baf38e0d7cb2148de08baf2d3261d49",
"aarch64-unknown-linux-gnu-0.11.3":
"711382e3158433f06b11d99afb440f4416359fc3c84558886d8ed8826a921bff",
"aarch64-unknown-linux-musl-0.11.3":
"8ecec82cb9a744d5fabff6d16d7777218a7730f699d2aa0d2f751c17858e2efa",
"arm-unknown-linux-musleabihf-0.11.3":
"3d021046a94ad11f12b9d83f36442a1a28e92e7149c3f79ba2951c96653dafac",
"armv7-unknown-linux-gnueabihf-0.11.3":
"13c9a0f5f624275ccd36db2896607f4fee3585f420734b16f6c66d70e32aa458",
"armv7-unknown-linux-musleabihf-0.11.3":
"260a88e2f00daab0363a745fde036a7881002d7a81094388f31925acb284110b",
"i686-pc-windows-msvc-0.11.3":
"036fa39fa5ea3cb86c127324924b913b5858e8d91c4cb413edacfc3123001696",
"i686-unknown-linux-gnu-0.11.3":
"b9410c8dae2fa0d4939af5b0ee7272d5591bd55890e8274dcf7f1aea84bfe043",
"i686-unknown-linux-musl-0.11.3":
"afe533fd409105e753d844490c65a4375e75bfb3812e49122684f996bed9e90a",
"powerpc64le-unknown-linux-gnu-0.11.3":
"5cdcadf4d50a5354312bc8ef37c2a6cfab4e2f13ccdf8380d3012b927b4ded95",
"riscv64gc-unknown-linux-gnu-0.11.3":
"8271e07ed9695870f4b0ae5ec722e3ae08fff280068f08bc6a8ca76c67d7fefa",
"riscv64gc-unknown-linux-musl-0.11.3":
"b750fc8393ced9939448849b05e94de6bf1e998bb7030c4ebe744b47b372bce9",
"s390x-unknown-linux-gnu-0.11.3":
"6dc4f555a5f6515f7fddb281422d2a8a3943853dae5de837bbb5d996d7576c71",
"x86_64-apple-darwin-0.11.3":
"b0e05e0b43a000fdc2132ee3f3400ba5dee427bc2337d3ec4eb8cf4f3d5722af",
"x86_64-pc-windows-msvc-0.11.3":
"ae681c0aaec7cc96af184648cb88d73f8393ed60fa5880abdd6bdb910f9b227c",
"x86_64-unknown-linux-gnu-0.11.3":
"c0f3236f146e55472663cfbcc9be3042a9f1092275bbe3fe2a56a6cbfd3da5ce",
"x86_64-unknown-linux-musl-0.11.3":
"8b40cf16b849634b81a530a3d0a0bcae5f24996ef9ae782976fd69b6266d3b8e",
"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": "aarch64-apple-darwin-0.10.9":
"a92f61e9ac9b0f29668c15f56152e4a60143fca148ff5bfadb86718472c3f376", "a92f61e9ac9b0f29668c15f56152e4a60143fca148ff5bfadb86718472c3f376",
"aarch64-pc-windows-msvc-0.10.9": "aarch64-pc-windows-msvc-0.10.9":

View file

@ -4,15 +4,19 @@ import * as core from "@actions/core";
import * as tc from "@actions/tool-cache"; import * as tc from "@actions/tool-cache";
import * as pep440 from "@renovatebot/pep440"; import * as pep440 from "@renovatebot/pep440";
import * as semver from "semver"; import * as semver from "semver";
import { import { TOOL_CACHE_NAME, VERSIONS_NDJSON_URL } from "../utils/constants";
ASTRAL_MIRROR_PREFIX,
GITHUB_RELEASES_PREFIX,
TOOL_CACHE_NAME,
VERSIONS_MANIFEST_URL,
} from "../utils/constants";
import type { Architecture, Platform } from "../utils/platforms"; import type { Architecture, Platform } from "../utils/platforms";
import { validateChecksum } from "./checksum/checksum"; import { validateChecksum } from "./checksum/checksum";
import { getAllVersions, getArtifact, getLatestVersion } from "./manifest"; import {
getAllVersions as getAllManifestVersions,
getLatestKnownVersion as getLatestVersionInManifest,
getManifestArtifact,
} from "./version-manifest";
import {
getAllVersions as getAllVersionsFromNdjson,
getArtifact as getArtifactFromNdjson,
getLatestVersion as getLatestVersionFromNdjson,
} from "./versions-client";
export function tryGetFromToolCache( export function tryGetFromToolCache(
arch: Architecture, arch: Architecture,
@ -29,85 +33,73 @@ export function tryGetFromToolCache(
return { installedPath, version: resolvedVersion }; return { installedPath, version: resolvedVersion };
} }
export async function downloadVersion( export async function downloadVersionFromNdjson(
platform: Platform, platform: Platform,
arch: Architecture, arch: Architecture,
version: string, version: string,
checksum: string | undefined, checkSum: string | undefined,
githubToken: string, githubToken: string,
manifestUrl?: string,
): Promise<{ version: string; cachedToolDir: string }> { ): Promise<{ version: string; cachedToolDir: string }> {
const artifact = await getArtifact(version, arch, platform, manifestUrl); const artifact = await getArtifactFromNdjson(version, arch, platform);
if (!artifact) { if (!artifact) {
throw new Error( throw new Error(
getMissingArtifactMessage(version, arch, platform, manifestUrl), `Could not find artifact for version ${version}, arch ${arch}, platform ${platform} in ${VERSIONS_NDJSON_URL} .`,
); );
} }
// For the default astral-sh/versions source, checksum validation relies on // For the default astral-sh/versions source, checksum validation relies on
// user input or the built-in KNOWN_CHECKSUMS table, not manifest sha256 values. // user input or the built-in KNOWN_CHECKSUMS table, not NDJSON sha256 values.
const resolvedChecksum = return await downloadVersion(
manifestUrl === undefined artifact.url,
? 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}`, `uv-${arch}-${platform}`,
platform, platform,
arch, arch,
version, version,
resolvedChecksum, checkSum,
downloadToken, githubToken,
);
}
export async function downloadVersionFromManifest(
manifestUrl: string,
platform: Platform,
arch: Architecture,
version: string,
checkSum: string | undefined,
githubToken: string,
): Promise<{ version: string; cachedToolDir: string }> {
const artifact = await getManifestArtifact(
manifestUrl,
version,
arch,
platform,
);
if (!artifact) {
throw new Error(
`manifest-file does not contain version ${version}, arch ${arch}, platform ${platform}.`,
); );
} catch (err) {
if (mirrorUrl === undefined) {
throw err;
} }
core.warning( return await downloadVersion(
`Failed to download from mirror, falling back to GitHub Releases: ${(err as Error).message}`,
);
return await downloadArtifact(
artifact.downloadUrl, artifact.downloadUrl,
`uv-${arch}-${platform}`, `uv-${arch}-${platform}`,
platform, platform,
arch, arch,
version, version,
resolvedChecksum, resolveChecksum(checkSum, artifact.checksum),
githubToken, githubToken,
); );
}
} }
/** async function downloadVersion(
* 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(
downloadUrl: string, downloadUrl: string,
artifactName: string, artifactName: string,
platform: Platform, platform: Platform,
arch: Architecture, arch: Architecture,
version: string, version: string,
checksum: string | undefined, checksum: string | undefined,
githubToken: string | undefined, githubToken: string,
): Promise<{ version: string; cachedToolDir: string }> { ): Promise<{ version: string; cachedToolDir: string }> {
core.info(`Downloading uv from "${downloadUrl}" ...`); core.info(`Downloading uv from "${downloadUrl}" ...`);
const downloadPath = await tc.downloadTool( const downloadPath = await tc.downloadTool(
@ -144,28 +136,15 @@ async function downloadArtifact(
version, version,
arch, arch,
); );
return { cachedToolDir, version }; return { cachedToolDir, version: 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( function resolveChecksum(
checksum: string | undefined, checkSum: string | undefined,
manifestChecksum: string, manifestChecksum?: string,
): string { ): string | undefined {
return checksum !== undefined && checksum !== "" return checkSum !== undefined && checkSum !== ""
? checksum ? checkSum
: manifestChecksum; : manifestChecksum;
} }
@ -179,28 +158,32 @@ export async function resolveVersion(
resolutionStrategy: "highest" | "lowest" = "highest", resolutionStrategy: "highest" | "lowest" = "highest",
): Promise<string> { ): Promise<string> {
core.debug(`Resolving version: ${versionInput}`); core.debug(`Resolving version: ${versionInput}`);
let version: string;
const isSimpleMinimumVersionSpecifier = const isSimpleMinimumVersionSpecifier =
versionInput.includes(">") && !versionInput.includes(","); versionInput.includes(">") && !versionInput.includes(",");
const resolveVersionSpecifierToLatest = const resolveVersionSpecifierToLatest =
isSimpleMinimumVersionSpecifier && resolutionStrategy === "highest"; isSimpleMinimumVersionSpecifier && resolutionStrategy === "highest";
if (resolveVersionSpecifierToLatest) { if (resolveVersionSpecifierToLatest) {
core.info("Found minimum version specifier, using latest version"); core.info("Found minimum version specifier, using latest version");
} }
if (manifestUrl !== undefined) {
const version = version =
versionInput === "latest" || resolveVersionSpecifierToLatest versionInput === "latest" || resolveVersionSpecifierToLatest
? await getLatestVersion(manifestUrl) ? await getLatestVersionInManifest(manifestUrl)
: versionInput; : versionInput;
} else {
version =
versionInput === "latest" || resolveVersionSpecifierToLatest
? await getLatestVersionFromNdjson()
: versionInput;
}
if (tc.isExplicitVersion(version)) { if (tc.isExplicitVersion(version)) {
core.debug(`Version ${version} is an explicit version.`); core.debug(`Version ${version} is an explicit version.`);
if ( if (resolveVersionSpecifierToLatest) {
resolveVersionSpecifierToLatest && if (!pep440.satisfies(version, versionInput)) {
!pep440.satisfies(version, versionInput)
) {
throw new Error(`No version found for ${versionInput}`); throw new Error(`No version found for ${versionInput}`);
} }
}
return version; return version;
} }
@ -225,11 +208,11 @@ async function getAvailableVersions(
core.info( core.info(
`Getting available versions from manifest-file ${manifestUrl} ...`, `Getting available versions from manifest-file ${manifestUrl} ...`,
); );
} else { return await getAllManifestVersions(manifestUrl);
core.info(`Getting available versions from ${VERSIONS_MANIFEST_URL} ...`);
} }
return await getAllVersions(manifestUrl); core.info(`Getting available versions from ${VERSIONS_NDJSON_URL} ...`);
return await getAllVersionsFromNdjson();
} }
function maxSatisfying( function maxSatisfying(

View file

@ -0,0 +1,80 @@
import * as core from "@actions/core";
export interface ManifestEntry {
arch: string;
platform: string;
version: string;
downloadUrl: string;
checksum?: string;
variant?: string;
archiveFormat?: string;
}
interface LegacyManifestEntry {
arch: string;
platform: string;
version: string;
downloadUrl: string;
checksum?: string;
}
const warnedLegacyManifestUrls = new Set<string>();
export function parseLegacyManifestEntries(
parsedEntries: unknown[],
manifestUrl: string,
): ManifestEntry[] {
warnAboutLegacyManifestFormat(manifestUrl);
return parsedEntries.map((entry, index) => {
if (!isLegacyManifestEntry(entry)) {
throw new Error(
`Invalid legacy manifest-file entry at index ${index} in ${manifestUrl}.`,
);
}
return {
arch: entry.arch,
checksum: entry.checksum,
downloadUrl: entry.downloadUrl,
platform: entry.platform,
version: entry.version,
};
});
}
export function clearLegacyManifestWarnings(): void {
warnedLegacyManifestUrls.clear();
}
function warnAboutLegacyManifestFormat(manifestUrl: string): void {
if (warnedLegacyManifestUrls.has(manifestUrl)) {
return;
}
warnedLegacyManifestUrls.add(manifestUrl);
core.warning(
`manifest-file ${manifestUrl} uses the legacy JSON array format, which is deprecated. Please migrate to the astral-sh/versions NDJSON format before the next major release.`,
);
}
function isLegacyManifestEntry(value: unknown): value is LegacyManifestEntry {
if (!isRecord(value)) {
return false;
}
const checksumIsValid =
typeof value.checksum === "string" || value.checksum === undefined;
return (
typeof value.arch === "string" &&
checksumIsValid &&
typeof value.downloadUrl === "string" &&
typeof value.platform === "string" &&
typeof value.version === "string"
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

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

@ -0,0 +1,169 @@
import * as core from "@actions/core";
import * as semver from "semver";
import { fetch } from "../utils/fetch";
import {
clearLegacyManifestWarnings,
type ManifestEntry,
parseLegacyManifestEntries,
} from "./legacy-version-manifest";
import { selectDefaultVariant } from "./variant-selection";
import { type NdjsonVersion, parseVersionData } from "./versions-client";
export interface ManifestArtifact {
downloadUrl: string;
checksum?: string;
archiveFormat?: string;
}
const cachedManifestEntries = new Map<string, ManifestEntry[]>();
export async function getLatestKnownVersion(
manifestUrl: string,
): Promise<string> {
const versions = await getAllVersions(manifestUrl);
const latestVersion = versions.reduce((latest, current) =>
semver.gt(current, latest) ? current : latest,
);
return latestVersion;
}
export async function getAllVersions(manifestUrl: string): Promise<string[]> {
const manifestEntries = await getManifestEntries(manifestUrl);
return [...new Set(manifestEntries.map((entry) => entry.version))];
}
export async function getManifestArtifact(
manifestUrl: string,
version: string,
arch: string,
platform: string,
): Promise<ManifestArtifact | undefined> {
const manifestEntries = await getManifestEntries(manifestUrl);
const entry = selectManifestEntry(
manifestEntries,
manifestUrl,
version,
arch,
platform,
);
if (!entry) {
return undefined;
}
return {
archiveFormat: entry.archiveFormat,
checksum: entry.checksum,
downloadUrl: entry.downloadUrl,
};
}
export function clearManifestCache(): void {
cachedManifestEntries.clear();
clearLegacyManifestWarnings();
}
async function getManifestEntries(
manifestUrl: string,
): Promise<ManifestEntry[]> {
const cachedEntries = cachedManifestEntries.get(manifestUrl);
if (cachedEntries !== undefined) {
core.debug(`Using cached manifest-file from: ${manifestUrl}`);
return cachedEntries;
}
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}`,
);
}
const data = await response.text();
const parsedEntries = parseManifestEntries(data, manifestUrl);
cachedManifestEntries.set(manifestUrl, parsedEntries);
return parsedEntries;
}
function parseManifestEntries(
data: string,
manifestUrl: string,
): ManifestEntry[] {
const trimmed = data.trim();
if (trimmed === "") {
throw new Error(`manifest-file at ${manifestUrl} is empty.`);
}
const parsedAsJson = tryParseJson(trimmed);
if (Array.isArray(parsedAsJson)) {
return parseLegacyManifestEntries(parsedAsJson, manifestUrl);
}
const versions = parseVersionData(trimmed, manifestUrl);
return mapNdjsonVersionsToManifestEntries(versions, manifestUrl);
}
function mapNdjsonVersionsToManifestEntries(
versions: NdjsonVersion[],
manifestUrl: string,
): ManifestEntry[] {
const manifestEntries: ManifestEntry[] = [];
for (const versionData of versions) {
for (const artifact of versionData.artifacts) {
const [arch, ...platformParts] = artifact.platform.split("-");
if (arch === undefined || platformParts.length === 0) {
throw new Error(
`Invalid artifact platform '${artifact.platform}' in manifest-file ${manifestUrl}.`,
);
}
manifestEntries.push({
arch,
archiveFormat: artifact.archive_format,
checksum: artifact.sha256,
downloadUrl: artifact.url,
platform: platformParts.join("-"),
variant: artifact.variant,
version: versionData.version,
});
}
}
return manifestEntries;
}
function selectManifestEntry(
manifestEntries: ManifestEntry[],
manifestUrl: string,
version: string,
arch: string,
platform: string,
): ManifestEntry | undefined {
const matches = manifestEntries.filter(
(candidate) =>
candidate.version === version &&
candidate.arch === arch &&
candidate.platform === platform,
);
if (matches.length === 0) {
return undefined;
}
return selectDefaultVariant(
matches,
`manifest-file ${manifestUrl} contains multiple artifacts for version ${version}, arch ${arch}, platform ${platform}`,
);
}
function tryParseJson(value: string): unknown {
try {
return JSON.parse(value);
} catch {
return undefined;
}
}

View file

@ -0,0 +1,191 @@
import * as core from "@actions/core";
import { VERSIONS_NDJSON_URL } from "../utils/constants";
import { fetch } from "../utils/fetch";
import { selectDefaultVariant } from "./variant-selection";
export interface NdjsonArtifact {
platform: string;
variant?: string;
url: string;
archive_format: string;
sha256: string;
}
export interface NdjsonVersion {
version: string;
artifacts: NdjsonArtifact[];
}
export interface ArtifactResult {
url: string;
sha256: string;
archiveFormat: string;
}
const cachedVersionData = new Map<string, NdjsonVersion[]>();
export async function fetchVersionData(
url: string = VERSIONS_NDJSON_URL,
): Promise<NdjsonVersion[]> {
const cachedVersions = cachedVersionData.get(url);
if (cachedVersions !== undefined) {
core.debug(`Using cached NDJSON version data from ${url}`);
return cachedVersions;
}
core.info(`Fetching version data from ${url} ...`);
const response = await fetch(url, {});
if (!response.ok) {
throw new Error(
`Failed to fetch version data: ${response.status} ${response.statusText}`,
);
}
const body = await response.text();
const versions = parseVersionData(body, url);
cachedVersionData.set(url, versions);
return versions;
}
export function parseVersionData(
data: string,
sourceDescription: string,
): NdjsonVersion[] {
const versions: NdjsonVersion[] = [];
for (const [index, line] of data.split("\n").entries()) {
const trimmed = line.trim();
if (trimmed === "") {
continue;
}
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch (error) {
throw new Error(
`Failed to parse version data from ${sourceDescription} at line ${index + 1}: ${(error as Error).message}`,
);
}
if (!isNdjsonVersion(parsed)) {
throw new Error(
`Invalid NDJSON record in ${sourceDescription} at line ${index + 1}.`,
);
}
versions.push(parsed);
}
if (versions.length === 0) {
throw new Error(`No version data found in ${sourceDescription}.`);
}
return versions;
}
export async function getLatestVersion(): Promise<string> {
const versions = await fetchVersionData();
const latestVersion = versions[0]?.version;
if (!latestVersion) {
throw new Error("No versions found in NDJSON data");
}
core.debug(`Latest version from NDJSON: ${latestVersion}`);
return latestVersion;
}
export async function getAllVersions(): Promise<string[]> {
const versions = await fetchVersionData();
return versions.map((versionData) => versionData.version);
}
export async function getArtifact(
version: string,
arch: string,
platform: string,
): Promise<ArtifactResult | undefined> {
const versions = await fetchVersionData();
const versionData = versions.find(
(candidate) => candidate.version === version,
);
if (!versionData) {
core.debug(`Version ${version} not found in NDJSON data`);
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 = selectArtifact(matchingArtifacts, version, targetPlatform);
return {
archiveFormat: artifact.archive_format,
sha256: artifact.sha256,
url: artifact.url,
};
}
export function clearCache(url?: string): void {
if (url === undefined) {
cachedVersionData.clear();
return;
}
cachedVersionData.delete(url);
}
function selectArtifact(
artifacts: NdjsonArtifact[],
version: string,
targetPlatform: string,
): NdjsonArtifact {
return selectDefaultVariant(
artifacts,
`Multiple artifacts found for ${targetPlatform} in version ${version}`,
);
}
function isNdjsonVersion(value: unknown): value is NdjsonVersion {
if (!isRecord(value)) {
return false;
}
if (typeof value.version !== "string" || !Array.isArray(value.artifacts)) {
return false;
}
return value.artifacts.every(isNdjsonArtifact);
}
function isNdjsonArtifact(value: unknown): value is NdjsonArtifact {
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

@ -9,14 +9,21 @@ import {
STATE_PYTHON_CACHE_MATCHED_KEY, STATE_PYTHON_CACHE_MATCHED_KEY,
} from "./cache/restore-cache"; } from "./cache/restore-cache";
import { STATE_UV_PATH, STATE_UV_VERSION } from "./utils/constants"; 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> { export async function run(): Promise<void> {
try { try {
const inputs = loadInputs(); if (enableCache) {
if (inputs.enableCache) { if (shouldSaveCache) {
if (inputs.saveCache) { await saveCache();
await saveCache(inputs);
} else { } else {
core.info("save-cache is false. Skipping save cache step."); 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 cacheKey = core.getState(STATE_CACHE_KEY);
const matchedKey = core.getState(STATE_CACHE_MATCHED_KEY); const matchedKey = core.getState(STATE_CACHE_MATCHED_KEY);
@ -47,13 +54,13 @@ async function saveCache(inputs: SetupInputs): Promise<void> {
if (matchedKey === cacheKey) { if (matchedKey === cacheKey) {
core.info(`Cache hit occurred on key ${cacheKey}, not saving cache.`); core.info(`Cache hit occurred on key ${cacheKey}, not saving cache.`);
} else { } else {
if (inputs.pruneCache) { if (shouldPruneCache) {
await pruneCache(); await pruneCache();
} }
const actualCachePath = getUvCachePath(inputs); const actualCachePath = getUvCachePath();
if (!fs.existsSync(actualCachePath)) { if (!fs.existsSync(actualCachePath)) {
if (inputs.ignoreNothingToCache) { if (ignoreNothingToCache) {
core.info( core.info(
"No cacheable uv cache paths were found. Ignoring because ignore-nothing-to-cache is enabled.", "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 (cachePython) {
if (!fs.existsSync(inputs.pythonDir)) { if (!fs.existsSync(pythonDir)) {
core.warning( 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; return;
} }
@ -83,7 +90,7 @@ async function saveCache(inputs: SetupInputs): Promise<void> {
const pythonCacheKey = `${cacheKey}-python`; const pythonCacheKey = `${cacheKey}-python`;
await saveCacheToKey( await saveCacheToKey(
pythonCacheKey, pythonCacheKey,
inputs.pythonDir, pythonDir,
STATE_PYTHON_CACHE_MATCHED_KEY, STATE_PYTHON_CACHE_MATCHED_KEY,
"Python cache", "Python cache",
); );
@ -106,22 +113,22 @@ async function pruneCache(): Promise<void> {
await exec.exec(uvPath, execArgs, options); await exec.exec(uvPath, execArgs, options);
} }
function getUvCachePath(inputs: SetupInputs): string { function getUvCachePath(): string {
if (inputs.cacheLocalPath === undefined) { if (cacheLocalPath === undefined) {
throw new Error( throw new Error(
"cache-local-path is not set. Cannot save cache without a valid cache path.", "cache-local-path is not set. Cannot save cache without a valid cache path.",
); );
} }
if ( if (
process.env.UV_CACHE_DIR && process.env.UV_CACHE_DIR &&
process.env.UV_CACHE_DIR !== inputs.cacheLocalPath.path process.env.UV_CACHE_DIR !== cacheLocalPath.path
) { ) {
core.warning( 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 process.env.UV_CACHE_DIR;
} }
return inputs.cacheLocalPath.path; return cacheLocalPath.path;
} }
async function saveCacheToKey( async function saveCacheToKey(

View file

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

View file

@ -6,10 +6,10 @@ import {
updateChecksums, updateChecksums,
} from "./download/checksum/update-known-checksums"; } from "./download/checksum/update-known-checksums";
import { import {
fetchManifest, fetchVersionData,
getLatestVersion, getLatestVersion,
type ManifestVersion, type NdjsonVersion,
} from "./download/manifest"; } from "./download/versions-client";
const VERSION_IN_CHECKSUM_KEY_PATTERN = const VERSION_IN_CHECKSUM_KEY_PATTERN =
/-(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/; /-(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/;
@ -18,7 +18,7 @@ async function run(): Promise<void> {
const checksumFilePath = process.argv.slice(2)[0]; const checksumFilePath = process.argv.slice(2)[0];
if (!checksumFilePath) { if (!checksumFilePath) {
throw new Error( throw new Error(
"Missing checksum file path. Usage: node dist/update-known-checksums/index.cjs <checksum-file-path>", "Missing checksum file path. Usage: node dist/update-known-checksums/index.js <checksum-file-path>",
); );
} }
@ -32,8 +32,8 @@ async function run(): Promise<void> {
return; return;
} }
const versions = await fetchManifest(); const versions = await fetchVersionData();
const checksumEntries = extractChecksumsFromManifest(versions); const checksumEntries = extractChecksumsFromNdjson(versions);
await updateChecksums(checksumFilePath, checksumEntries); await updateChecksums(checksumFilePath, checksumEntries);
core.setOutput("latest-version", latestVersion); core.setOutput("latest-version", latestVersion);
@ -61,8 +61,8 @@ function extractVersionFromChecksumKey(key: string): string | undefined {
return key.match(VERSION_IN_CHECKSUM_KEY_PATTERN)?.[1]; return key.match(VERSION_IN_CHECKSUM_KEY_PATTERN)?.[1];
} }
function extractChecksumsFromManifest( function extractChecksumsFromNdjson(
versions: ManifestVersion[], versions: NdjsonVersion[],
): ChecksumEntry[] { ): ChecksumEntry[] {
const checksums: ChecksumEntry[] = []; const checksums: ChecksumEntry[] = [];

View file

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

View file

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