Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/release-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ categories:
labels:
- "maintenance"
- "ci"
- "update-known-versions"
- "update-known-checksums"
- title: "📚 Documentation"
labels:
- "documentation"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "Update known versions"
name: "Update known checksums"
on:
workflow_dispatch:
schedule:
Expand All @@ -21,13 +21,11 @@ jobs:
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: "20"
- name: Update known versions
id: update-known-versions
- name: Update known checksums
id: update-known-checksums
run:
node dist/update-known-versions/index.js
node dist/update-known-checksums/index.js
src/download/checksum/known-checksums.ts
version-manifest.json
${{ secrets.GITHUB_TOKEN }}
- name: Check for changes
id: changes-exist
run: |
Expand All @@ -48,10 +46,10 @@ jobs:
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
git add .
git commit -m "chore: update known versions for $LATEST_VERSION"
git commit -m "chore: update known checksums for $LATEST_VERSION"
git push origin HEAD:refs/heads/main
env:
LATEST_VERSION: ${{ steps.update-known-versions.outputs.latest-version }}
LATEST_VERSION: ${{ steps.update-known-checksums.outputs.latest-version }}

- name: Create Pull Request
if: ${{ steps.changes-exist.outputs.changes-exist == 'true' && steps.commit-and-push.outcome != 'success' }}
Expand All @@ -60,11 +58,11 @@ jobs:
commit-message: "chore: update known checksums"
title:
"chore: update known checksums for ${{
steps.update-known-versions.outputs.latest-version }}"
steps.update-known-checksums.outputs.latest-version }}"
body:
"chore: update known checksums for ${{
steps.update-known-versions.outputs.latest-version }}"
steps.update-known-checksums.outputs.latest-version }}"
base: main
labels: "automated-pr,update-known-versions"
branch: update-known-versions-pr
labels: "automated-pr,update-known-checksums"
branch: update-known-checksums-pr
delete-branch: true
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed
# The checksum of the uv version to install
checksum: ""

# Used to increase the rate limit when retrieving versions and downloading uv
# Used when downloading uv from GitHub releases
github-token: ${{ github.token }}

# Enable uploading of the uv cache: true, false, or auto (enabled on GitHub-hosted runners, disabled on self-hosted runners)
Expand Down Expand Up @@ -114,7 +114,7 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed
# Custom path to set UV_TOOL_BIN_DIR to
tool-bin-dir: ""

# URL to the manifest file containing available versions and download URLs
# URL to a custom manifest file (NDJSON preferred, legacy JSON array is deprecated)
manifest-file: ""

# Add problem matchers
Expand Down Expand Up @@ -190,10 +190,12 @@ For more advanced configuration options, see our detailed documentation:

## How it works

This action downloads uv from the uv repo's official
[GitHub Releases](https://github.com/astral-sh/uv) and uses the
[GitHub Actions Toolkit](https://github.com/actions/toolkit) to cache it as a tool to speed up
consecutive runs on self-hosted runners.
By default, this action resolves uv versions from
[`astral-sh/versions`](https://github.com/astral-sh/versions) (NDJSON) and downloads uv from the
official [GitHub Releases](https://github.com/astral-sh/uv).

It then uses the [GitHub Actions Toolkit](https://github.com/actions/toolkit) to cache uv as a
tool to speed up consecutive runs on self-hosted runners.

The installed version of uv is then added to the runner PATH, enabling later steps to invoke it
by name (`uv`).
Expand Down
17 changes: 14 additions & 3 deletions __tests__/download/checksum/checksum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {
validateChecksum,
} from "../../../src/download/checksum/checksum";

const validChecksum =
"f3da96ec7e995debee7f5d52ecd034dfb7074309a1da42f76429ecb814d813a3";
const filePath = "__tests__/fixtures/checksumfile";

test("checksum should match", async () => {
const validChecksum =
"f3da96ec7e995debee7f5d52ecd034dfb7074309a1da42f76429ecb814d813a3";
const filePath = "__tests__/fixtures/checksumfile";
// string params don't matter only test the checksum mechanism, not known checksums
await validateChecksum(
validChecksum,
Expand All @@ -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 };

it.each<KnownVersionFixture>([
Expand Down
256 changes: 256 additions & 0 deletions __tests__/download/download-version.test.ts
Original file line number Diff line number Diff line change
@@ -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",
);
});
});
});
Loading
Loading