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
130 changes: 130 additions & 0 deletions __tests__/download/download-version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const {
downloadVersionFromManifest,
downloadVersionFromNdjson,
resolveVersion,
rewriteToMirror,
} = await import("../../src/download/download-version");

describe("download-version", () => {
Expand Down Expand Up @@ -198,6 +199,135 @@ describe("download-version", () => {
"0.9.26",
);
});

it("rewrites GitHub Releases URLs to the Astral mirror", async () => {
mockGetArtifactFromNdjson.mockResolvedValue({
archiveFormat: "tar.gz",
sha256: "abc123",
url: "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
});

await downloadVersionFromNdjson(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
undefined,
"token",
);

expect(mockDownloadTool).toHaveBeenCalledWith(
"https://releases.astral.sh/github/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
undefined,
undefined,
);
});

it("does not rewrite non-GitHub URLs", async () => {
mockGetArtifactFromNdjson.mockResolvedValue({
archiveFormat: "tar.gz",
sha256: "abc123",
url: "https://example.com/uv.tar.gz",
});

await downloadVersionFromNdjson(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
undefined,
"token",
);

expect(mockDownloadTool).toHaveBeenCalledWith(
"https://example.com/uv.tar.gz",
undefined,
"token",
);
});

it("falls back to GitHub Releases when the mirror fails", async () => {
mockGetArtifactFromNdjson.mockResolvedValue({
archiveFormat: "tar.gz",
sha256: "abc123",
url: "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
});

mockDownloadTool
.mockRejectedValueOnce(new Error("mirror unavailable"))
.mockResolvedValueOnce("/tmp/downloaded");

await downloadVersionFromNdjson(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
undefined,
"token",
);

expect(mockDownloadTool).toHaveBeenCalledTimes(2);
// Mirror request: no token
expect(mockDownloadTool).toHaveBeenNthCalledWith(
1,
"https://releases.astral.sh/github/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
undefined,
undefined,
);
// GitHub fallback: token restored
expect(mockDownloadTool).toHaveBeenNthCalledWith(
2,
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
undefined,
"token",
);
expect(mockWarning).toHaveBeenCalledWith(
"Failed to download from mirror, falling back to GitHub Releases: mirror unavailable",
);
});

it("does not fall back for non-GitHub URLs", async () => {
mockGetArtifactFromNdjson.mockResolvedValue({
archiveFormat: "tar.gz",
sha256: "abc123",
url: "https://example.com/uv.tar.gz",
});

mockDownloadTool.mockRejectedValue(new Error("download failed"));

await expect(
downloadVersionFromNdjson(
"unknown-linux-gnu",
"x86_64",
"0.9.26",
undefined,
"token",
),
).rejects.toThrow("download failed");

expect(mockDownloadTool).toHaveBeenCalledTimes(1);
});
});

describe("rewriteToMirror", () => {
it("rewrites a GitHub Releases URL to the Astral mirror", () => {
expect(
rewriteToMirror(
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
),
).toBe(
"https://releases.astral.sh/github/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
);
});

it("returns undefined for non-GitHub URLs", () => {
expect(rewriteToMirror("https://example.com/uv.tar.gz")).toBeUndefined();
});

it("returns undefined for a different GitHub repo", () => {
expect(
rewriteToMirror(
"https://github.com/other/repo/releases/download/v1.0/file.tar.gz",
),
).toBeUndefined();
});
});

describe("downloadVersionFromManifest", () => {
Expand Down
47 changes: 38 additions & 9 deletions dist/setup/index.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 52 additions & 11 deletions src/download/download-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import * as core from "@actions/core";
import * as tc from "@actions/tool-cache";
import * as pep440 from "@renovatebot/pep440";
import * as semver from "semver";
import { TOOL_CACHE_NAME, VERSIONS_NDJSON_URL } from "../utils/constants";
import {
ASTRAL_MIRROR_PREFIX,
GITHUB_RELEASES_PREFIX,
TOOL_CACHE_NAME,
VERSIONS_NDJSON_URL,
} from "../utils/constants";
import type { Architecture, Platform } from "../utils/platforms";
import { validateChecksum } from "./checksum/checksum";
import {
Expand Down Expand Up @@ -48,17 +53,53 @@ export async function downloadVersionFromNdjson(
);
}

const mirrorUrl = rewriteToMirror(artifact.url);
const downloadUrl = mirrorUrl ?? artifact.url;
// Don't send the GitHub token to the Astral mirror.
const downloadToken = mirrorUrl !== undefined ? undefined : githubToken;

// 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(
artifact.url,
`uv-${arch}-${platform}`,
platform,
arch,
version,
checkSum,
githubToken,
);
try {
return await downloadVersion(
downloadUrl,
`uv-${arch}-${platform}`,
platform,
arch,
version,
checkSum,
downloadToken,
);
} catch (err) {
if (mirrorUrl === undefined) {
throw err;
}

core.warning(
`Failed to download from mirror, falling back to GitHub Releases: ${(err as Error).message}`,
);

return await downloadVersion(
artifact.url,
`uv-${arch}-${platform}`,
platform,
arch,
version,
checkSum,
githubToken,
);
}
}

/**
* Rewrite a GitHub Releases URL to the Astral mirror.
* Returns `undefined` if the URL does not match the expected GitHub prefix.
*/
export function rewriteToMirror(url: string): string | undefined {
if (!url.startsWith(GITHUB_RELEASES_PREFIX)) {
return undefined;
}
return ASTRAL_MIRROR_PREFIX + url.slice(GITHUB_RELEASES_PREFIX.length);
}

export async function downloadVersionFromManifest(
Expand Down Expand Up @@ -99,7 +140,7 @@ async function downloadVersion(
arch: Architecture,
version: string,
checksum: string | undefined,
githubToken: string,
githubToken: string | undefined,
): Promise<{ version: string; cachedToolDir: string }> {
core.info(`Downloading uv from "${downloadUrl}" ...`);
const downloadPath = await tc.downloadTool(
Expand Down
8 changes: 8 additions & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@ export const STATE_UV_PATH = "uv-path";
export const STATE_UV_VERSION = "uv-version";
export const VERSIONS_NDJSON_URL =
"https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson";

/** GitHub Releases URL prefix for uv artifacts. */
export const GITHUB_RELEASES_PREFIX =
"https://github.com/astral-sh/uv/releases/download/";

/** Astral mirror URL prefix that fronts GitHub Releases for uv artifacts. */
export const ASTRAL_MIRROR_PREFIX =
"https://releases.astral.sh/github/uv/releases/download/";
Loading