Use astral-sh/versions as primary version provider (#802)
Some checks failed
test / test-act (push) Failing after 18s
Release Drafter / ✏️ Draft release (push) Waiting to run
test / test-tool-install (macos-14) (push) Waiting to run
test / test-tool-install (macos-latest) (push) Waiting to run
test / test-tool-install (windows-latest) (push) Waiting to run
test / test-cache-key-os-version (macos-14, macos-14) (push) Waiting to run
test / test-default-version (macos-14) (push) Waiting to run
test / test-default-version (macos-latest) (push) Waiting to run
test / test-default-version (windows-latest) (push) Waiting to run
test / test-checksum (map[checksum:a70cbfbf3bb5c08b2f84963b4f12c94e08fbb2468ba418a3bfe1066fbe9e7218 os:macos-latest]) (push) Waiting to run
test / test-python-version (macos-latest) (push) Waiting to run
test / test-activate-environment (windows-latest) (push) Waiting to run
test / test-cache-key-os-version (windows-2022, windows-2022) (push) Waiting to run
test / test-python-version (windows-latest) (push) Waiting to run
test / test-activate-environment (macos-latest) (push) Waiting to run
test / test-cache-key-os-version (macos-15, macos-15) (push) Waiting to run
test / test-activate-environment-custom-path (macos-latest) (push) Waiting to run
test / test-activate-environment-custom-path (windows-latest) (push) Waiting to run
test / test-cache-key-os-version (ubuntu-24.04, ubuntu-24.04) (push) Waiting to run
test / test-cache-key-os-version (windows-2025, windows-2025) (push) Waiting to run
test / test-setup-cache (auto, windows-latest) (push) Waiting to run
test / test-setup-cache (false, windows-latest) (push) Waiting to run
test / test-setup-cache (true, windows-latest) (push) Waiting to run
test / test-restore-cache (auto, ubuntu-latest) (push) Blocked by required conditions
test / test-restore-cache (auto, windows-latest) (push) Blocked by required conditions
test / test-restore-cache (false, ubuntu-latest) (push) Blocked by required conditions
test / test-restore-cache (false, windows-latest) (push) Blocked by required conditions
test / test-cache-local (map[expected-cache-dir:D:\a\_temp\setup-uv-cache os:windows-latest]) (push) Waiting to run
test / test-python-install-dir (map[expected-python-dir:D:\a\_temp\uv-python-dir os:windows-latest]) (push) Waiting to run
test / all-tests-passed (push) Blocked by required conditions
test / test-restore-cache (true, ubuntu-latest) (push) Blocked by required conditions
test / test-restore-cache (true, windows-latest) (push) Blocked by required conditions
CodeQL / Analyze (TypeScript) (push) Failing after 6s
test / lint (push) Failing after 6s
test / test-default-version (ubuntu-latest) (push) Failing after 6s
test / test-uv-no-modify-path (push) Failing after 10s
test / test-specific-version (map[expected-version:0.1.0 resolution-strategy:lowest version-input:>=0.1.0,<0.2]) (push) Failing after 10s
test / test-specific-version (map[expected-version:0.1.45 resolution-strategy:highest version-input:>=0.1,<0.2]) (push) Failing after 10s
test / test-specific-version (map[expected-version:0.3.0 version-input:0.3.0]) (push) Failing after 10s
test / test-specific-version (map[expected-version:0.3.2 version-input:0.3.2]) (push) Failing after 10s
test / test-specific-version (map[expected-version:0.3.5 version-input:0.3.x]) (push) Failing after 12s
test / test-specific-version (map[expected-version:0.3.5 version-input:0.3]) (push) Failing after 12s
test / test-specific-version (map[expected-version:0.4.25 resolution-strategy:lowest version-input:>=0.4.25,<0.5]) (push) Failing after 12s
test / test-specific-version (map[expected-version:0.4.25 resolution-strategy:lowest version-input:>=0.4.25]) (push) Failing after 12s
test / test-specific-version (map[expected-version:0.4.30 version-input:>=0.4.25,<0.5]) (push) Failing after 12s
test / test-latest-version (>=0.8) (push) Failing after 12s
test / test-latest-version (latest) (push) Failing after 13s
test / test-from-working-directory-version (map[expected-version:0.5.14 working-directory:__tests__/fixtures/pyproject-toml-project]) (push) Failing after 18s
test / test-from-working-directory-version (map[expected-version:0.5.15 working-directory:__tests__/fixtures/uv-toml-project]) (push) Failing after 18s
test / test-version-file-version (map[expected-version:0.5.15 version-file:__tests__/fixtures/.tool-versions]) (push) Failing after 18s
test / test-version-file-version (map[expected-version:0.6.17 version-file:__tests__/fixtures/uv-in-requirements-txt-project/requirements.txt]) (push) Failing after 18s
test / test-version-file-version (map[expected-version:0.8.3 version-file:__tests__/fixtures/uv-in-requirements-hash-txt-project/requirements.txt]) (push) Failing after 18s
test / test-malformed-pyproject-file-fallback (push) Failing after 17s
test / test-checksum (map[checksum:4d9279ad5ca596b1e2d703901d508430eb07564dc4d8837de9e2fca9c90f8ecd os:ubuntu-latest]) (push) Failing after 18s
test / test-with-explicit-token (push) Failing after 17s
test / test-uvx (push) Failing after 19s
test / test-tool-install (ubuntu-latest) (push) Failing after 20s
test / test-python-version (ubuntu-latest) (push) Failing after 20s
test / test-activate-environment (ubuntu-latest) (push) Failing after 20s
test / test-activate-environment-custom-path (ubuntu-latest) (push) Failing after 20s
test / test-cache-key-os-version (ubuntu-22.04, ubuntu-22.04) (push) Failing after 18s
test / test-setup-cache (auto, ubuntu-latest) (push) Failing after 18s
test / test-setup-cache (false, ubuntu-latest) (push) Failing after 18s
test / test-setup-cache (true, ubuntu-latest) (push) Failing after 18s
test / test-restore-cache-requirements-txt (push) Has been skipped
test / test-setup-cache-dependency-glob (push) Failing after 18s
test / test-setup-cache-requirements-txt (push) Failing after 18s
test / test-restore-cache-dependency-glob (push) Has been skipped
test / test-setup-cache-save-cache-false (push) Failing after 18s
test / test-restore-cache-save-cache-false (push) Has been skipped
test / test-setup-cache-restore-cache-false (push) Failing after 17s
test / test-restore-cache-restore-cache-false (push) Has been skipped
test / test-cache-local (map[expected-cache-dir:/home/runner/work/_temp/setup-uv-cache os:ubuntu-latest]) (push) Failing after 17s
test / test-cache-local-cache-disabled (push) Failing after 17s
test / test-cache-local-cache-disabled-but-explicit-path (push) Failing after 17s
test / test-no-python-version (push) Failing after 17s
test / test-debian-unstable (push) Failing after 31s
test / test-musl (push) Failing after 30s
test / test-custom-manifest-file (push) Failing after 18s
test / test-absolute-path (push) Failing after 18s
test / test-relative-path (push) Failing after 18s
test / test-cache-prune-force (push) Failing after 18s
test / test-cache-dir-from-file (push) Failing after 18s
test / test-cache-python-missing-managed-install-dir (push) Failing after 19s
test / test-cache-python-installs (push) Failing after 19s
test / test-python-install-dir (map[expected-python-dir:/home/runner/work/_temp/uv-python-dir os:ubuntu-latest]) (push) Failing after 19s
test / test-restore-python-installs (push) Has been skipped
test / validate-typings (push) Failing after 21s

Closes: #777
Closes: #325
This commit is contained in:
Kevin Stillhammer
2026-03-12 22:03:21 +01:00
committed by GitHub
parent 0f6ec07aaf
commit e06108dd0a
28 changed files with 2022 additions and 41294 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,256 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
const mockInfo = jest.fn();
const mockWarning = jest.fn();
jest.mock("@actions/core", () => ({
debug: jest.fn(),
info: mockInfo,
warning: mockWarning,
}));
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockDownloadTool = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockExtractTar = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockExtractZip = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockCacheDir = jest.fn<any>();
jest.mock("@actions/tool-cache", () => {
const actual = jest.requireActual("@actions/tool-cache") as Record<
string,
unknown
>;
return {
...actual,
cacheDir: mockCacheDir,
downloadTool: mockDownloadTool,
extractTar: mockExtractTar,
extractZip: mockExtractZip,
};
});
// 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.
const mockGetAllManifestVersions = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockGetLatestVersionInManifest = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockGetManifestArtifact = jest.fn<any>();
jest.mock("../../src/download/version-manifest", () => ({
getAllVersions: mockGetAllManifestVersions,
getLatestKnownVersion: mockGetLatestVersionInManifest,
getManifestArtifact: mockGetManifestArtifact,
}));
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockValidateChecksum = jest.fn<any>();
jest.mock("../../src/download/checksum/checksum", () => ({
validateChecksum: mockValidateChecksum,
}));
import {
downloadVersionFromManifest,
downloadVersionFromNdjson,
resolveVersion,
} from "../../src/download/download-version";
describe("download-version", () => {
beforeEach(() => {
mockInfo.mockReset();
mockWarning.mockReset();
mockDownloadTool.mockReset();
mockExtractTar.mockReset();
mockExtractZip.mockReset();
mockCacheDir.mockReset();
mockGetLatestVersionFromNdjson.mockReset();
mockGetAllVersionsFromNdjson.mockReset();
mockGetArtifactFromNdjson.mockReset();
mockGetAllManifestVersions.mockReset();
mockGetLatestVersionInManifest.mockReset();
mockGetManifestArtifact.mockReset();
mockValidateChecksum.mockReset();
mockDownloadTool.mockResolvedValue("/tmp/downloaded");
mockExtractTar.mockResolvedValue("/tmp/extracted");
mockExtractZip.mockResolvedValue("/tmp/extracted");
mockCacheDir.mockResolvedValue("/tmp/cached");
});
describe("resolveVersion", () => {
it("uses astral-sh/versions to resolve latest", async () => {
mockGetLatestVersionFromNdjson.mockResolvedValue("0.9.26");
const version = await resolveVersion("latest", undefined);
expect(version).toBe("0.9.26");
expect(mockGetLatestVersionFromNdjson).toHaveBeenCalledTimes(1);
});
it("uses astral-sh/versions to resolve available versions", async () => {
mockGetAllVersionsFromNdjson.mockResolvedValue(["0.9.26", "0.9.25"]);
const version = await resolveVersion("^0.9.0", undefined);
expect(version).toBe("0.9.26");
expect(mockGetAllVersionsFromNdjson).toHaveBeenCalledTimes(1);
});
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 () => {
mockGetAllManifestVersions.mockResolvedValue(["0.9.26", "0.9.25"]);
const version = await resolveVersion(
"^0.9.0",
"https://example.com/custom.ndjson",
);
expect(version).toBe("0.9.26");
expect(mockGetAllManifestVersions).toHaveBeenCalledWith(
"https://example.com/custom.ndjson",
);
});
});
describe("downloadVersionFromNdjson", () => {
it("fails when NDJSON metadata lookup fails", async () => {
mockGetArtifactFromNdjson.mockRejectedValue(
new Error("NDJSON unavailable"),
);
await expect(
downloadVersionFromNdjson(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
undefined,
"token",
),
).rejects.toThrow("NDJSON unavailable");
expect(mockDownloadTool).not.toHaveBeenCalled();
expect(mockValidateChecksum).not.toHaveBeenCalled();
});
it("fails when no matching artifact exists in NDJSON metadata", async () => {
mockGetArtifactFromNdjson.mockResolvedValue(undefined);
await expect(
downloadVersionFromNdjson(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
undefined,
"token",
),
).rejects.toThrow(
"Could not find artifact for version 0.9.26, arch x86_64, platform unknown-linux-gnu in https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson .",
);
expect(mockDownloadTool).not.toHaveBeenCalled();
expect(mockValidateChecksum).not.toHaveBeenCalled();
});
it("uses built-in checksums for default NDJSON downloads", async () => {
mockGetArtifactFromNdjson.mockResolvedValue({
archiveFormat: "tar.gz",
sha256: "ndjson-checksum-that-should-be-ignored",
url: "https://example.com/uv.tar.gz",
});
await downloadVersionFromNdjson(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
undefined,
"token",
);
expect(mockValidateChecksum).toHaveBeenCalledWith(
undefined,
"/tmp/downloaded",
"x86_64",
"unknown-linux-gnu",
"0.9.26",
);
});
});
describe("downloadVersionFromManifest", () => {
it("uses manifest-file checksum metadata when checksum input is unset", async () => {
mockGetManifestArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "manifest-checksum",
downloadUrl: "https://example.com/custom-uv.tar.gz",
});
await downloadVersionFromManifest(
"https://example.com/custom.ndjson",
"unknown-linux-gnu",
"x86_64",
"0.9.26",
"",
"token",
);
expect(mockValidateChecksum).toHaveBeenCalledWith(
"manifest-checksum",
"/tmp/downloaded",
"x86_64",
"unknown-linux-gnu",
"0.9.26",
);
});
it("prefers checksum input over manifest-file checksum metadata", async () => {
mockGetManifestArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "manifest-checksum",
downloadUrl: "https://example.com/custom-uv.tar.gz",
});
await downloadVersionFromManifest(
"https://example.com/custom.ndjson",
"unknown-linux-gnu",
"x86_64",
"0.9.26",
"user-checksum",
"token",
);
expect(mockValidateChecksum).toHaveBeenCalledWith(
"user-checksum",
"/tmp/downloaded",
"x86_64",
"unknown-linux-gnu",
"0.9.26",
);
});
});
});

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

@@ -26,7 +26,7 @@ inputs:
required: false required: false
github-token: github-token:
description: description:
"Used to increase the rate limit when retrieving versions and downloading uv." "Used when downloading uv from GitHub releases."
required: false required: false
default: ${{ github.token }} default: ${{ github.token }}
enable-cache: enable-cache:
@@ -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 the manifest file containing available versions and download URLs." 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."

5
dist/save-cache/index.js generated vendored
View File

@@ -90979,12 +90979,11 @@ function getConfigValueFromTomlFile(filePath, key) {
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.STATE_UV_VERSION = exports.STATE_UV_PATH = exports.TOOL_CACHE_NAME = exports.OWNER = exports.REPO = void 0; exports.VERSIONS_NDJSON_URL = exports.STATE_UV_VERSION = exports.STATE_UV_PATH = exports.TOOL_CACHE_NAME = void 0;
exports.REPO = "uv";
exports.OWNER = "astral-sh";
exports.TOOL_CACHE_NAME = "uv"; exports.TOOL_CACHE_NAME = "uv";
exports.STATE_UV_PATH = "uv-path"; exports.STATE_UV_PATH = "uv-path";
exports.STATE_UV_VERSION = "uv-version"; exports.STATE_UV_VERSION = "uv-version";
exports.VERSIONS_NDJSON_URL = "https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson";
/***/ }), /***/ }),

4798
dist/setup/index.js generated vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

260
package-lock.json generated
View File

@@ -15,9 +15,6 @@
"@actions/glob": "^0.5.0", "@actions/glob": "^0.5.0",
"@actions/io": "^1.1.3", "@actions/io": "^1.1.3",
"@actions/tool-cache": "^2.0.2", "@actions/tool-cache": "^2.0.2",
"@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": "^14.0.0",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"@renovatebot/pep440": "^4.2.1", "@renovatebot/pep440": "^4.2.1",
"smol-toml": "^1.6.0", "smol-toml": "^1.6.0",
"undici": "5.28.5" "undici": "5.28.5"
@@ -1589,133 +1586,6 @@
"@tybys/wasm-util": "^0.10.0" "@tybys/wasm-util": "^0.10.0"
} }
}, },
"node_modules/@octokit/auth-token": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
"integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
"license": "MIT",
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/core": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.3",
"@octokit/request": "^10.0.6",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"before-after-hook": "^4.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/endpoint": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz",
"integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/graphql": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
"integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^10.0.6",
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/openapi-types": {
"version": "27.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
"integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
"integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz",
"integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@octokit/request": {
"version": "10.0.7",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.7.tgz",
"integrity": "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^11.0.2",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"fast-content-type-parse": "^3.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/request-error": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
"integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/types": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
"integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^27.0.0"
}
},
"node_modules/@opentelemetry/api": { "node_modules/@opentelemetry/api": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz",
@@ -2452,12 +2322,6 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
}, },
"node_modules/before-after-hook": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
"integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
"license": "Apache-2.0"
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -3067,22 +2931,6 @@
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
} }
}, },
"node_modules/fast-content-type-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/fast-json-stable-stringify": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -5365,12 +5213,6 @@
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/universal-user-agent": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
"integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
"license": "ISC"
},
"node_modules/unrs-resolver": { "node_modules/unrs-resolver": {
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
@@ -6881,93 +6723,6 @@
"@tybys/wasm-util": "^0.10.0" "@tybys/wasm-util": "^0.10.0"
} }
}, },
"@octokit/auth-token": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
"integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="
},
"@octokit/core": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
"requires": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.3",
"@octokit/request": "^10.0.6",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"before-after-hook": "^4.0.0",
"universal-user-agent": "^7.0.0"
}
},
"@octokit/endpoint": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz",
"integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==",
"requires": {
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.2"
}
},
"@octokit/graphql": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
"integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
"requires": {
"@octokit/request": "^10.0.6",
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.0"
}
},
"@octokit/openapi-types": {
"version": "27.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
"integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="
},
"@octokit/plugin-paginate-rest": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
"integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
"requires": {
"@octokit/types": "^16.0.0"
}
},
"@octokit/plugin-rest-endpoint-methods": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz",
"integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==",
"requires": {
"@octokit/types": "^16.0.0"
}
},
"@octokit/request": {
"version": "10.0.7",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.7.tgz",
"integrity": "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==",
"requires": {
"@octokit/endpoint": "^11.0.2",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"fast-content-type-parse": "^3.0.0",
"universal-user-agent": "^7.0.2"
}
},
"@octokit/request-error": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
"integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
"requires": {
"@octokit/types": "^16.0.0"
}
},
"@octokit/types": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
"integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
"requires": {
"@octokit/openapi-types": "^27.0.0"
}
},
"@opentelemetry/api": { "@opentelemetry/api": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz",
@@ -7468,11 +7223,6 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
}, },
"before-after-hook": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
"integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="
},
"brace-expansion": { "brace-expansion": {
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -7873,11 +7623,6 @@
"jest-util": "30.2.0" "jest-util": "30.2.0"
} }
}, },
"fast-content-type-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="
},
"fast-json-stable-stringify": { "fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -9411,11 +9156,6 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="
}, },
"universal-user-agent": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
"integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="
},
"unrs-resolver": { "unrs-resolver": {
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",

View File

@@ -7,10 +7,10 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"check": "biome check --write", "check": "biome check --write",
"package": "ncc build -o dist/setup src/setup-uv.ts && ncc build -o dist/save-cache src/save-cache.ts && ncc build -o dist/update-known-versions src/update-known-versions.ts", "package": "ncc build -o dist/setup src/setup-uv.ts && ncc build -o dist/save-cache src/save-cache.ts && ncc build -o dist/update-known-checksums src/update-known-checksums.ts",
"test": "jest", "test": "jest",
"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-versions": "RUNNER_TEMP=known_versions node dist/update-known-versions/index.js src/download/checksum/known-versions.ts \"$(gh auth token)\"", "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 test" "all": "npm run build && npm run check && npm run package && npm test"
}, },
"repository": { "repository": {
@@ -32,9 +32,6 @@
"@actions/glob": "^0.5.0", "@actions/glob": "^0.5.0",
"@actions/io": "^1.1.3", "@actions/io": "^1.1.3",
"@actions/tool-cache": "^2.0.2", "@actions/tool-cache": "^2.0.2",
"@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": "^14.0.0",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"@renovatebot/pep440": "^4.2.1", "@renovatebot/pep440": "^4.2.1",
"smol-toml": "^1.6.0", "smol-toml": "^1.6.0",
"undici": "5.28.5" "undici": "5.28.5"

View File

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

View File

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

View File

@@ -2,20 +2,21 @@ import { promises as fs } from "node:fs";
import * as path from "node:path"; import * as path from "node:path";
import * as core from "@actions/core"; import * as core from "@actions/core";
import * as tc from "@actions/tool-cache"; import * as tc from "@actions/tool-cache";
import type { Endpoints } from "@octokit/types";
import * as pep440 from "@renovatebot/pep440"; import * as pep440 from "@renovatebot/pep440";
import * as semver from "semver"; import * as semver from "semver";
import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants"; import { TOOL_CACHE_NAME, VERSIONS_NDJSON_URL } from "../utils/constants";
import { Octokit } from "../utils/octokit";
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 { import {
getDownloadUrl, getAllVersions as getAllManifestVersions,
getLatestKnownVersion as getLatestVersionInManifest, getLatestKnownVersion as getLatestVersionInManifest,
getManifestArtifact,
} from "./version-manifest"; } from "./version-manifest";
import {
type Release = getAllVersions as getAllVersionsFromNdjson,
Endpoints["GET /repos/{owner}/{repo}/releases"]["response"]["data"][number]; getArtifact as getArtifactFromNdjson,
getLatestVersion as getLatestVersionFromNdjson,
} from "./versions-client";
export function tryGetFromToolCache( export function tryGetFromToolCache(
arch: Architecture, arch: Architecture,
@@ -32,19 +33,26 @@ export function tryGetFromToolCache(
return { installedPath, version: resolvedVersion }; return { installedPath, version: resolvedVersion };
} }
export async function downloadVersionFromGithub( 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,
): Promise<{ version: string; cachedToolDir: string }> { ): Promise<{ version: string; cachedToolDir: string }> {
const artifact = `uv-${arch}-${platform}`; const artifact = await getArtifactFromNdjson(version, arch, platform);
const extension = getExtension(platform);
const downloadUrl = `https://github.com/${OWNER}/${REPO}/releases/download/${version}/${artifact}${extension}`; if (!artifact) {
throw new Error(
`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
// user input or the built-in KNOWN_CHECKSUMS table, not NDJSON sha256 values.
return await downloadVersion( return await downloadVersion(
downloadUrl, artifact.url,
artifact, `uv-${arch}-${platform}`,
platform, platform,
arch, arch,
version, version,
@@ -54,38 +62,32 @@ export async function downloadVersionFromGithub(
} }
export async function downloadVersionFromManifest( export async function downloadVersionFromManifest(
manifestUrl: string | undefined, manifestUrl: string,
platform: Platform, platform: Platform,
arch: Architecture, arch: Architecture,
version: string, version: string,
checkSum: string | undefined, checkSum: string | undefined,
githubToken: string, githubToken: string,
): Promise<{ version: string; cachedToolDir: string }> { ): Promise<{ version: string; cachedToolDir: string }> {
const downloadUrl = await getDownloadUrl( const artifact = await getManifestArtifact(
manifestUrl, manifestUrl,
version, version,
arch, arch,
platform, platform,
); );
if (!downloadUrl) { if (!artifact) {
core.info( throw new Error(
`manifest-file does not contain version ${version}, arch ${arch}, platform ${platform}. Falling back to GitHub releases.`, `manifest-file does not contain version ${version}, arch ${arch}, platform ${platform}.`,
);
return await downloadVersionFromGithub(
platform,
arch,
version,
checkSum,
githubToken,
); );
} }
return await downloadVersion( return await downloadVersion(
downloadUrl, artifact.downloadUrl,
`uv-${arch}-${platform}`, `uv-${arch}-${platform}`,
platform, platform,
arch, arch,
version, version,
checkSum, resolveChecksum(checkSum, artifact.checksum),
githubToken, githubToken,
); );
} }
@@ -96,7 +98,7 @@ async function downloadVersion(
platform: Platform, platform: Platform,
arch: Architecture, arch: Architecture,
version: string, version: string,
checkSum: string | undefined, checksum: string | undefined,
githubToken: string, githubToken: string,
): Promise<{ version: string; cachedToolDir: string }> { ): Promise<{ version: string; cachedToolDir: string }> {
core.info(`Downloading uv from "${downloadUrl}" ...`); core.info(`Downloading uv from "${downloadUrl}" ...`);
@@ -105,14 +107,14 @@ async function downloadVersion(
undefined, undefined,
githubToken, githubToken,
); );
await validateChecksum(checkSum, downloadPath, arch, platform, version); await validateChecksum(checksum, downloadPath, arch, platform, version);
let uvDir: string; let uvDir: string;
if (platform === "pc-windows-msvc") { if (platform === "pc-windows-msvc") {
// On windows extracting the zip does not create an intermediate directory // On windows extracting the zip does not create an intermediate directory.
try { try {
// Try tar first as it's much faster, but only bsdtar supports zip files, // Try tar first as it's much faster, but only bsdtar supports zip files,
// so this my fail if another tar, like gnu tar, ends up being used. // so this may fail if another tar, like gnu tar, ends up being used.
uvDir = await tc.extractTar(downloadPath, undefined, "x"); uvDir = await tc.extractTar(downloadPath, undefined, "x");
} catch (err) { } catch (err) {
core.info( core.info(
@@ -127,6 +129,7 @@ async function downloadVersion(
const extractedDir = await tc.extractTar(downloadPath); const extractedDir = await tc.extractTar(downloadPath);
uvDir = path.join(extractedDir, artifactName); uvDir = path.join(extractedDir, artifactName);
} }
const cachedToolDir = await tc.cacheDir( const cachedToolDir = await tc.cacheDir(
uvDir, uvDir,
TOOL_CACHE_NAME, TOOL_CACHE_NAME,
@@ -136,14 +139,22 @@ async function downloadVersion(
return { cachedToolDir, version: version }; return { cachedToolDir, version: version };
} }
function resolveChecksum(
checkSum: string | undefined,
manifestChecksum?: string,
): string | undefined {
return checkSum !== undefined && checkSum !== ""
? checkSum
: manifestChecksum;
}
function getExtension(platform: Platform): string { function getExtension(platform: Platform): string {
return platform === "pc-windows-msvc" ? ".zip" : ".tar.gz"; return platform === "pc-windows-msvc" ? ".zip" : ".tar.gz";
} }
export async function resolveVersion( export async function resolveVersion(
versionInput: string, versionInput: string,
manifestFile: string | undefined, manifestUrl: string | undefined,
githubToken: string,
resolutionStrategy: "highest" | "lowest" = "highest", resolutionStrategy: "highest" | "lowest" = "highest",
): Promise<string> { ): Promise<string> {
core.debug(`Resolving version: ${versionInput}`); core.debug(`Resolving version: ${versionInput}`);
@@ -155,15 +166,15 @@ export async function resolveVersion(
if (resolveVersionSpecifierToLatest) { if (resolveVersionSpecifierToLatest) {
core.info("Found minimum version specifier, using latest version"); core.info("Found minimum version specifier, using latest version");
} }
if (manifestFile) { if (manifestUrl !== undefined) {
version = version =
versionInput === "latest" || resolveVersionSpecifierToLatest versionInput === "latest" || resolveVersionSpecifierToLatest
? await getLatestVersionInManifest(manifestFile) ? await getLatestVersionInManifest(manifestUrl)
: versionInput; : versionInput;
} else { } else {
version = version =
versionInput === "latest" || resolveVersionSpecifierToLatest versionInput === "latest" || resolveVersionSpecifierToLatest
? await getLatestVersion(githubToken) ? await getLatestVersionFromNdjson()
: versionInput; : versionInput;
} }
if (tc.isExplicitVersion(version)) { if (tc.isExplicitVersion(version)) {
@@ -175,91 +186,33 @@ export async function resolveVersion(
} }
return version; return version;
} }
const availableVersions = await getAvailableVersions(githubToken);
const availableVersions = await getAvailableVersions(manifestUrl);
core.debug(`Available versions: ${availableVersions}`); core.debug(`Available versions: ${availableVersions}`);
const resolvedVersion = const resolvedVersion =
resolutionStrategy === "lowest" resolutionStrategy === "lowest"
? minSatisfying(availableVersions, version) ? minSatisfying(availableVersions, version)
: maxSatisfying(availableVersions, version); : maxSatisfying(availableVersions, version);
if (resolvedVersion === undefined) { if (resolvedVersion === undefined) {
throw new Error(`No version found for ${version}`); throw new Error(`No version found for ${version}`);
} }
return resolvedVersion; return resolvedVersion;
} }
async function getAvailableVersions(githubToken: string): Promise<string[]> { async function getAvailableVersions(
core.info("Getting available versions from GitHub API..."); manifestUrl: string | undefined,
try { ): Promise<string[]> {
const octokit = new Octokit({ if (manifestUrl !== undefined) {
auth: githubToken, core.info(
}); `Getting available versions from manifest-file ${manifestUrl} ...`,
return await getReleaseTagNames(octokit);
} catch (err) {
if ((err as Error).message.includes("Bad credentials")) {
core.info(
"No (valid) GitHub token provided. Falling back to anonymous. Requests might be rate limited.",
);
const octokit = new Octokit();
return await getReleaseTagNames(octokit);
}
throw err;
}
}
async function getReleaseTagNames(octokit: Octokit): Promise<string[]> {
const response: Release[] = await octokit.paginate(
octokit.rest.repos.listReleases,
{
owner: OWNER,
repo: REPO,
},
);
const releaseTagNames = response.map((release) => release.tag_name);
if (releaseTagNames.length === 0) {
throw Error(
"Github API request failed while getting releases. Check the GitHub status page for outages. Try again later.",
); );
} return await getAllManifestVersions(manifestUrl);
return releaseTagNames;
}
async function getLatestVersion(githubToken: string) {
core.info("Getting latest version from GitHub API...");
const octokit = new Octokit({
auth: githubToken,
});
let latestRelease: { tag_name: string } | undefined;
try {
latestRelease = await getLatestRelease(octokit);
} catch (err) {
if ((err as Error).message.includes("Bad credentials")) {
core.info(
"No (valid) GitHub token provided. Falling back to anonymous. Requests might be rate limited.",
);
const octokit = new Octokit();
latestRelease = await getLatestRelease(octokit);
} else {
core.error(
"Github API request failed while getting latest release. Check the GitHub status page for outages. Try again later.",
);
throw err;
}
} }
if (!latestRelease) { core.info(`Getting available versions from ${VERSIONS_NDJSON_URL} ...`);
throw new Error("Could not determine latest release."); return await getAllVersionsFromNdjson();
}
core.debug(`Latest version: ${latestRelease.tag_name}`);
return latestRelease.tag_name;
}
async function getLatestRelease(octokit: Octokit) {
const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({
owner: OWNER,
repo: REPO,
});
return latestRelease;
} }
function maxSatisfying( 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

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

View File

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

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

@@ -5,6 +5,7 @@ import * as exec from "@actions/exec";
import { restoreCache } from "./cache/restore-cache"; import { restoreCache } from "./cache/restore-cache";
import { import {
downloadVersionFromManifest, downloadVersionFromManifest,
downloadVersionFromNdjson,
resolveVersion, resolveVersion,
tryGetFromToolCache, tryGetFromToolCache,
} from "./download/download-version"; } from "./download/download-version";
@@ -139,14 +140,23 @@ async function setupUv(
}; };
} }
const downloadVersionResult = await downloadVersionFromManifest( const downloadVersionResult =
manifestFile, manifestFile !== undefined
platform, ? await downloadVersionFromManifest(
arch, manifestFile,
resolvedVersion, platform,
checkSum, arch,
githubToken, resolvedVersion,
); checkSum,
githubToken,
)
: await downloadVersionFromNdjson(
platform,
arch,
resolvedVersion,
checkSum,
githubToken,
);
return { return {
uvDir: downloadVersionResult.cachedToolDir, uvDir: downloadVersionResult.cachedToolDir,
@@ -158,12 +168,7 @@ async function determineVersion(
manifestFile: string | undefined, manifestFile: string | undefined,
): Promise<string> { ): Promise<string> {
if (versionInput !== "") { if (versionInput !== "") {
return await resolveVersion( return await resolveVersion(versionInput, manifestFile, resolutionStrategy);
versionInput,
manifestFile,
githubToken,
resolutionStrategy,
);
} }
if (versionFileInput !== "") { if (versionFileInput !== "") {
const versionFromFile = getUvVersionFromFile(versionFileInput); const versionFromFile = getUvVersionFromFile(versionFileInput);
@@ -175,7 +180,6 @@ async function determineVersion(
return await resolveVersion( return await resolveVersion(
versionFromFile, versionFromFile,
manifestFile, manifestFile,
githubToken,
resolutionStrategy, resolutionStrategy,
); );
} }
@@ -193,7 +197,6 @@ async function determineVersion(
return await resolveVersion( return await resolveVersion(
versionFromUvToml || versionFromPyproject || "latest", versionFromUvToml || versionFromPyproject || "latest",
manifestFile, manifestFile,
githubToken,
resolutionStrategy, resolutionStrategy,
); );
} }

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
export const REPO = "uv";
export const OWNER = "astral-sh";
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_NDJSON_URL =
"https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson";

View File

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

File diff suppressed because it is too large Load Diff