From 68559e99cc807c1eb7e9238fd43a15bba460eec2 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Wed, 28 Oct 2026 15:09:55 +0100 Subject: [PATCH] Use Astral mirror for version manifests with GitHub fallback (#1119) Amp-Thread-ID: https://ampcode.com/threads/T-01a1822a-68da-7125-9e7d-16e038ab0840 Closes: #1118 Co-authored-by: Amp --- __tests__/download/download-version.test.ts | 2 +- __tests__/download/manifest.test.ts | 74 ++++++++++++++++++++- dist/setup/index.cjs | 16 ++++- dist/update-known-checksums/index.cjs | 16 ++++- docs/customization.md | 6 +- docs/environment-and-tools.md | 11 +-- src/download/manifest.ts | 19 +++++- src/utils/constants.ts | 2 + 8 files changed, 134 insertions(+), 12 deletions(-) diff --git a/__tests__/download/download-version.test.ts b/__tests__/download/download-version.test.ts index 25c02d3..2173bc8 100644 --- a/__tests__/download/download-version.test.ts +++ b/__tests__/download/download-version.test.ts @@ -242,7 +242,7 @@ describe("download-version", () => { "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 .", + "Could not find artifact for version 0.9.26, arch x86_64, platform unknown-linux-gnu in https://releases.astral.sh/github/versions/main/v1/uv.ndjson .", ); expect(mockDownloadTool).not.toHaveBeenCalled(); diff --git a/__tests__/download/manifest.test.ts b/__tests__/download/manifest.test.ts index 61d4a48..4d1db82 100644 --- a/__tests__/download/manifest.test.ts +++ b/__tests__/download/manifest.test.ts @@ -96,6 +96,78 @@ describe("manifest", () => { expect(versions).toHaveLength(2); expect(versions[0]?.version).toBe("0.9.26"); expect(versions[1]?.version).toBe("0.9.25"); + expect(mockFetch.mock.calls.map(([url]) => url)).toEqual([ + "https://releases.astral.sh/github/versions/main/v1/uv.ndjson", + ]); + }); + + it("falls back to GitHub after mirror network retries are exhausted", async () => { + jest.useFakeTimers(); + mockFetch + .mockRejectedValueOnce(new Error("mirror unavailable")) + .mockRejectedValueOnce(new Error("mirror unavailable")) + .mockRejectedValueOnce(new Error("mirror unavailable")) + .mockResolvedValueOnce( + createMockResponse(true, 200, "OK", sampleManifestResponse), + ); + + const result = fetchManifest(); + await jest.runAllTimersAsync(); + + await expect(result).resolves.toEqual([ + expect.objectContaining({ version: "0.9.26" }), + expect.objectContaining({ version: "0.9.25" }), + ]); + expect(mockFetch.mock.calls.map(([url]) => url)).toEqual([ + "https://releases.astral.sh/github/versions/main/v1/uv.ndjson", + "https://releases.astral.sh/github/versions/main/v1/uv.ndjson", + "https://releases.astral.sh/github/versions/main/v1/uv.ndjson", + "https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson", + ]); + + await expect(getLatestVersion()).resolves.toBe("0.9.26"); + expect(mockFetch).toHaveBeenCalledTimes(4); + }); + + it.each([404, 429, 503])( + "falls back to a streaming GitHub manifest on mirror HTTP %s", + async (status) => { + mockFetch + .mockResolvedValueOnce(createMockResponse(false, status, "Error", "")) + .mockResolvedValueOnce( + createStreamingMockResponse([`${sampleManifestResponse}\n`]), + ); + + await expect(getLatestVersion()).resolves.toBe("0.9.26"); + await expect( + getArtifact("0.9.26", "aarch64", "apple-darwin"), + ).resolves.toEqual({ + archiveFormat: "tar.gz", + checksum: + "fcf0a9ea6599c6ae28a4c854ac6da76f2c889354d7c36ce136ef071f7ab9721f", + downloadUrl: + "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-aarch64-apple-darwin.tar.gz", + }); + expect(mockFetch.mock.calls.map(([url]) => url)).toEqual([ + "https://releases.astral.sh/github/versions/main/v1/uv.ndjson", + "https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson", + ]); + }, + ); + + it("does not fall back from a custom manifest", async () => { + mockFetch.mockResolvedValue( + createMockResponse(false, 503, "Service Unavailable", ""), + ); + + await expect( + fetchManifest("https://example.com/custom.ndjson"), + ).rejects.toThrow( + "Failed to fetch manifest data: 503 Service Unavailable", + ); + expect(mockFetch.mock.calls.map(([url]) => url)).toEqual([ + "https://example.com/custom.ndjson", + ]); }); it("retries network failures", async () => { @@ -123,7 +195,7 @@ describe("manifest", () => { await jest.runAllTimersAsync(); await result; - expect(mockFetch).toHaveBeenCalledTimes(MANIFEST_FETCH_ATTEMPTS); + expect(mockFetch).toHaveBeenCalledTimes(MANIFEST_FETCH_ATTEMPTS * 2); }); it("throws on a failed fetch", async () => { diff --git a/dist/setup/index.cjs b/dist/setup/index.cjs index 48512c7..0a02bcd 100644 --- a/dist/setup/index.cjs +++ b/dist/setup/index.cjs @@ -94499,7 +94499,8 @@ function _getGlobal(key, defaultValue) { var TOOL_CACHE_NAME = "uv"; var STATE_UV_PATH = "uv-path"; var STATE_UV_VERSION = "uv-version"; -var VERSIONS_MANIFEST_URL = "https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson"; +var VERSIONS_MANIFEST_URL = "https://releases.astral.sh/github/versions/main/v1/uv.ndjson"; +var GITHUB_VERSIONS_MANIFEST_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/"; @@ -100305,6 +100306,19 @@ async function getArtifact(version3, arch3, platform2, manifestUrl = VERSIONS_MA }; } async function fetchManifestResponse(manifestUrl) { + try { + return await fetchManifestResponseFromUrl(manifestUrl); + } catch (error2) { + if (manifestUrl !== VERSIONS_MANIFEST_URL) { + throw error2; + } + info2( + `Failed to fetch manifest from Astral mirror, falling back to GitHub: ${error2.message}` + ); + return await fetchManifestResponseFromUrl(GITHUB_VERSIONS_MANIFEST_URL); + } +} +async function fetchManifestResponseFromUrl(manifestUrl) { let response; for (let attempt = 1; attempt <= MANIFEST_FETCH_ATTEMPTS; attempt++) { info2(`Fetching manifest data from ${manifestUrl} ...`); diff --git a/dist/update-known-checksums/index.cjs b/dist/update-known-checksums/index.cjs index bf6d19a..12b071f 100644 --- a/dist/update-known-checksums/index.cjs +++ b/dist/update-known-checksums/index.cjs @@ -52854,7 +52854,8 @@ var import_node_readline = require("node:readline"); var import_node_stream = require("node:stream"); // src/utils/constants.ts -var VERSIONS_MANIFEST_URL = "https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson"; +var VERSIONS_MANIFEST_URL = "https://releases.astral.sh/github/versions/main/v1/uv.ndjson"; +var GITHUB_VERSIONS_MANIFEST_URL = "https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson"; // src/utils/fetch.ts var import_undici2 = __toESM(require_undici2(), 1); @@ -52926,6 +52927,19 @@ async function getLatestVersion(manifestUrl = VERSIONS_MANIFEST_URL) { return latestVersion; } async function fetchManifestResponse(manifestUrl) { + try { + return await fetchManifestResponseFromUrl(manifestUrl); + } catch (error2) { + if (manifestUrl !== VERSIONS_MANIFEST_URL) { + throw error2; + } + info2( + `Failed to fetch manifest from Astral mirror, falling back to GitHub: ${error2.message}` + ); + return await fetchManifestResponseFromUrl(GITHUB_VERSIONS_MANIFEST_URL); + } +} +async function fetchManifestResponseFromUrl(manifestUrl) { let response; for (let attempt = 1; attempt <= MANIFEST_FETCH_ATTEMPTS; attempt++) { info2(`Fetching manifest data from ${manifestUrl} ...`); diff --git a/docs/customization.md b/docs/customization.md index 30dbb6c..c379e22 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -20,10 +20,12 @@ You can specify a checksum to override those values. The sha256 hashes can also ## Manifest file By default, setup-uv reads version metadata from -[`astral-sh/versions`](https://github.com/astral-sh/versions). +the [Astral mirror](https://releases.astral.sh/github/versions/main/v1/uv.ndjson). +If the mirror request fails, it falls back to [`astral-sh/versions`](https://github.com/astral-sh/versions). The `manifest-file` input lets you override that source with your own URL, for example to test -custom uv builds or alternate download locations. +custom uv builds or alternate download locations. Custom manifest URLs do not fall back to the +default sources. ### Format diff --git a/docs/environment-and-tools.md b/docs/environment-and-tools.md index f33f94a..e02d3bf 100644 --- a/docs/environment-and-tools.md +++ b/docs/environment-and-tools.md @@ -50,12 +50,13 @@ You can customize the venv location with `venv-path`, for example to place it in ## GitHub authentication token -By default, this action resolves available uv versions from -[`astral-sh/versions`](https://github.com/astral-sh/versions), then downloads uv artifacts from -GitHub Releases. +By default, this action fetches version metadata and uv archives from the Astral mirror. +If a manifest request fails, it falls back to `raw.githubusercontent.com`. If an archive download +fails, it falls back to 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. +You can provide a token via `github-token` to authenticate GitHub Releases downloads. The token +is not sent to the Astral mirror or used for manifest requests. By default, the `GITHUB_TOKEN` +secret is used, which is automatically provided by GitHub Actions. 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) diff --git a/src/download/manifest.ts b/src/download/manifest.ts index 91763e7..de94f09 100644 --- a/src/download/manifest.ts +++ b/src/download/manifest.ts @@ -1,7 +1,10 @@ import { createInterface } from "node:readline"; import { Readable } from "node:stream"; import * as core from "@actions/core"; -import { VERSIONS_MANIFEST_URL } from "../utils/constants"; +import { + GITHUB_VERSIONS_MANIFEST_URL, + VERSIONS_MANIFEST_URL, +} from "../utils/constants"; import { fetch } from "../utils/fetch"; import * as log from "../utils/logging"; import { selectDefaultVariant } from "./variant-selection"; @@ -167,6 +170,20 @@ export function clearManifestCache(manifestUrl?: string): void { } async function fetchManifestResponse(manifestUrl: string) { + try { + return await fetchManifestResponseFromUrl(manifestUrl); + } catch (error) { + if (manifestUrl !== VERSIONS_MANIFEST_URL) { + throw error; + } + log.info( + `Failed to fetch manifest from Astral mirror, falling back to GitHub: ${(error as Error).message}`, + ); + return await fetchManifestResponseFromUrl(GITHUB_VERSIONS_MANIFEST_URL); + } +} + +async function fetchManifestResponseFromUrl(manifestUrl: string) { let response: Awaited> | undefined; for (let attempt = 1; attempt <= MANIFEST_FETCH_ATTEMPTS; attempt++) { log.info(`Fetching manifest data from ${manifestUrl} ...`); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index ba2d07e..c84a04b 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -2,6 +2,8 @@ export const TOOL_CACHE_NAME = "uv"; export const STATE_UV_PATH = "uv-path"; export const STATE_UV_VERSION = "uv-version"; export const VERSIONS_MANIFEST_URL = + "https://releases.astral.sh/github/versions/main/v1/uv.ndjson"; +export const GITHUB_VERSIONS_MANIFEST_URL = "https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson"; /** GitHub Releases URL prefix for uv artifacts. */