From 37802adc94f370d6bfd71619e3f0bf239e1f3b78 Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Mon, 16 Mar 2026 12:38:17 +0000 Subject: [PATCH] Fetch uv from Astral's mirror by default (#809) This PR tries fetching the uv artifact from `releases.astral.sh` by default, only in cases where the artifact would otherwise have come from `https://github.com/astral-sh/uv/releases/download/`. The checksums are supposed to be the same for the mirror, and can still come from `raw.githubusercontent.com/astral-sh/versions`. If the download fails, we fall back to the original URL. This avoids hitting GitHub's Releases API which is prone to rate limiting. As far as I can tell, together with https://github.com/astral-sh/setup-uv/pull/802 this PR makes a github token entirely unnecessary for this action. Towards https://github.com/astral-sh/uv/issues/18503. --- __tests__/download/download-version.test.ts | 130 ++++++++++++++++++++ dist/setup/index.cjs | 47 +++++-- src/download/download-version.ts | 63 ++++++++-- src/utils/constants.ts | 8 ++ 4 files changed, 228 insertions(+), 20 deletions(-) diff --git a/__tests__/download/download-version.test.ts b/__tests__/download/download-version.test.ts index afc57ab..d796aea 100644 --- a/__tests__/download/download-version.test.ts +++ b/__tests__/download/download-version.test.ts @@ -68,6 +68,7 @@ const { downloadVersionFromManifest, downloadVersionFromNdjson, resolveVersion, + rewriteToMirror, } = await import("../../src/download/download-version"); describe("download-version", () => { @@ -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", () => { diff --git a/dist/setup/index.cjs b/dist/setup/index.cjs index da0b05a..4fc9565 100644 --- a/dist/setup/index.cjs +++ b/dist/setup/index.cjs @@ -91887,6 +91887,8 @@ var TOOL_CACHE_NAME = "uv"; var STATE_UV_PATH = "uv-path"; var STATE_UV_VERSION = "uv-version"; var VERSIONS_NDJSON_URL = "https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson"; +var GITHUB_RELEASES_PREFIX = "https://github.com/astral-sh/uv/releases/download/"; +var ASTRAL_MIRROR_PREFIX = "https://releases.astral.sh/github/uv/releases/download/"; // src/download/checksum/checksum.ts var crypto6 = __toESM(require("node:crypto"), 1); @@ -96658,15 +96660,42 @@ async function downloadVersionFromNdjson(platform2, arch3, version4, checkSum2, `Could not find artifact for version ${version4}, arch ${arch3}, platform ${platform2} in ${VERSIONS_NDJSON_URL} .` ); } - return await downloadVersion( - artifact.url, - `uv-${arch3}-${platform2}`, - platform2, - arch3, - version4, - checkSum2, - githubToken2 - ); + const mirrorUrl = rewriteToMirror(artifact.url); + const downloadUrl = mirrorUrl ?? artifact.url; + const downloadToken = mirrorUrl !== void 0 ? void 0 : githubToken2; + try { + return await downloadVersion( + downloadUrl, + `uv-${arch3}-${platform2}`, + platform2, + arch3, + version4, + checkSum2, + downloadToken + ); + } catch (err) { + if (mirrorUrl === void 0) { + throw err; + } + warning( + `Failed to download from mirror, falling back to GitHub Releases: ${err.message}` + ); + return await downloadVersion( + artifact.url, + `uv-${arch3}-${platform2}`, + platform2, + arch3, + version4, + checkSum2, + githubToken2 + ); + } +} +function rewriteToMirror(url2) { + if (!url2.startsWith(GITHUB_RELEASES_PREFIX)) { + return void 0; + } + return ASTRAL_MIRROR_PREFIX + url2.slice(GITHUB_RELEASES_PREFIX.length); } async function downloadVersionFromManifest(manifestUrl, platform2, arch3, version4, checkSum2, githubToken2) { const artifact = await getManifestArtifact( diff --git a/src/download/download-version.ts b/src/download/download-version.ts index ed9207c..5c92068 100644 --- a/src/download/download-version.ts +++ b/src/download/download-version.ts @@ -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 { @@ -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( @@ -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( diff --git a/src/utils/constants.ts b/src/utils/constants.ts index c21d6d5..bc7d03a 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -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/";