From 18d451d679e1650bc59f25f17116168d04f30748 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 11 Aug 2026 11:15:59 +0200 Subject: [PATCH] Add latest-known version selector (#993) ## Summary - add `latest-known` as an explicit version selector - resolve it locally to the newest version in the bundled checksum table - preserve existing default and `latest` behavior - document custom-manifest checksum semantics and update published bundles ## Testing - `npm ci --ignore-scripts` - `npm run all` (99 tests passed) Closes #919 Refs: pi-session 019fed0e-6019-7504-911b-bd9955cbbd49 --- README.md | 2 +- .../download/checksum/known-version.test.ts | 20 ++++++++ __tests__/download/download-version.test.ts | 17 +++++++ action.yml | 2 +- dist/setup/index.cjs | 47 ++++++++++++++----- dist/update-known-checksums/index.cjs | 42 +++++++++-------- docs/advanced-version-configuration.md | 13 +++++ src/download/checksum/known-version.ts | 23 +++++++++ src/update-known-checksums.ts | 29 +----------- src/version/resolve.ts | 5 ++ 10 files changed, 138 insertions(+), 62 deletions(-) create mode 100644 __tests__/download/checksum/known-version.test.ts create mode 100644 src/download/checksum/known-version.ts diff --git a/README.md b/README.md index 70ba4e2..bde68f8 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed - name: Install uv with all available options uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: - # The version of uv to install (default: searches for version in config files, then latest) + # The version of uv to install, e.g., "0.5.0", "latest", or "latest-known" (default: searches for version in config files, then latest) version: "" # Path to a file containing the version of uv to install, e.g., uv.toml, pyproject.toml, .tool-versions, requirements.txt or uv.lock (default: searches uv.toml then pyproject.toml) diff --git a/__tests__/download/checksum/known-version.test.ts b/__tests__/download/checksum/known-version.test.ts new file mode 100644 index 0000000..ff57ef3 --- /dev/null +++ b/__tests__/download/checksum/known-version.test.ts @@ -0,0 +1,20 @@ +import { expect, it, jest } from "@jest/globals"; + +jest.unstable_mockModule( + "../../../src/download/checksum/known-checksums", + () => ({ + KNOWN_CHECKSUMS: { + "aarch64-apple-darwin-1.9.0": "checksum", + "x86_64-unknown-linux-gnu-1.8.0": "checksum", + "x86_64-unknown-linux-gnu-1.10.0": "checksum", + }, + }), +); + +const { getLatestKnownVersion } = await import( + "../../../src/download/checksum/known-version" +); + +it("returns the highest version with a built-in checksum", () => { + expect(getLatestKnownVersion()).toBe("1.10.0"); +}); diff --git a/__tests__/download/download-version.test.ts b/__tests__/download/download-version.test.ts index 49e3406..e45d7e3 100644 --- a/__tests__/download/download-version.test.ts +++ b/__tests__/download/download-version.test.ts @@ -55,6 +55,12 @@ jest.unstable_mockModule("../../src/download/checksum/checksum", () => ({ validateChecksum: mockValidateChecksum, })); +const mockGetLatestKnownVersion = jest.fn(() => "0.9.25"); + +jest.unstable_mockModule("../../src/download/checksum/known-version", () => ({ + getLatestKnownVersion: mockGetLatestKnownVersion, +})); + const { downloadVersion, resolveVersion, rewriteToMirror } = await import( "../../src/download/download-version" ); @@ -72,6 +78,7 @@ describe("download-version", () => { mockGetFirstMatchingVersion.mockReset(); mockGetArtifact.mockReset(); mockValidateChecksum.mockReset(); + mockGetLatestKnownVersion.mockClear(); mockDownloadTool.mockResolvedValue("/tmp/downloaded"); mockExtractTar.mockResolvedValue("/tmp/extracted"); @@ -90,6 +97,16 @@ describe("download-version", () => { expect(mockGetLatestVersion).toHaveBeenCalledWith(undefined); }); + it("resolves latest-known without reading the manifest", async () => { + const version = await resolveVersion("latest-known", undefined); + + expect(version).toBe("0.9.25"); + expect(mockGetLatestKnownVersion).toHaveBeenCalledTimes(1); + expect(mockGetLatestVersion).not.toHaveBeenCalled(); + expect(mockGetAllVersions).not.toHaveBeenCalled(); + expect(mockGetFirstMatchingVersion).not.toHaveBeenCalled(); + }); + it("stops at the first matching version in the default manifest", async () => { mockGetFirstMatchingVersion.mockImplementation( (predicate: (version: string) => boolean) => diff --git a/action.yml b/action.yml index cdcd51d..c589b8f 100644 --- a/action.yml +++ b/action.yml @@ -4,7 +4,7 @@ description: author: "astral-sh" inputs: version: - description: "The version of uv to install e.g., `0.5.0` Defaults to the version in pyproject.toml or 'latest'." + description: "The version of uv to install, e.g., `0.5.0`, `latest`, or `latest-known`. Defaults to the version in pyproject.toml or `latest`." default: "" version-file: description: "Path to a file containing the version of uv to install, e.g., uv.toml, pyproject.toml, .tool-versions, requirements.txt or uv.lock. Defaults to searching for uv.toml and if not found pyproject.toml." diff --git a/dist/setup/index.cjs b/dist/setup/index.cjs index 4258575..f1201e6 100644 --- a/dist/setup/index.cjs +++ b/dist/setup/index.cjs @@ -20008,8 +20008,8 @@ var require_rcompare = __commonJS({ "node_modules/@actions/cache/node_modules/semver/functions/rcompare.js"(exports2, module2) { "use strict"; var compare2 = require_compare(); - var rcompare2 = (a, b, loose) => compare2(b, a, loose); - module2.exports = rcompare2; + var rcompare3 = (a, b, loose) => compare2(b, a, loose); + module2.exports = rcompare3; } }); @@ -21234,7 +21234,7 @@ var require_semver2 = __commonJS({ var patch2 = require_patch(); var prerelease = require_prerelease(); var compare2 = require_compare(); - var rcompare2 = require_rcompare(); + var rcompare3 = require_rcompare(); var compareLoose = require_compare_loose(); var compareBuild = require_compare_build(); var sort = require_sort(); @@ -21272,7 +21272,7 @@ var require_semver2 = __commonJS({ patch: patch2, prerelease, compare: compare2, - rcompare: rcompare2, + rcompare: rcompare3, compareLoose, compareBuild, sort, @@ -27618,8 +27618,8 @@ var require_rcompare2 = __commonJS({ "node_modules/@actions/tool-cache/node_modules/semver/functions/rcompare.js"(exports2, module2) { "use strict"; var compare2 = require_compare2(); - var rcompare2 = (a, b, loose) => compare2(b, a, loose); - module2.exports = rcompare2; + var rcompare3 = (a, b, loose) => compare2(b, a, loose); + module2.exports = rcompare3; } }); @@ -28844,7 +28844,7 @@ var require_semver4 = __commonJS({ var patch2 = require_patch2(); var prerelease = require_prerelease2(); var compare2 = require_compare2(); - var rcompare2 = require_rcompare2(); + var rcompare3 = require_rcompare2(); var compareLoose = require_compare_loose2(); var compareBuild = require_compare_build2(); var sort = require_sort2(); @@ -28882,7 +28882,7 @@ var require_semver4 = __commonJS({ patch: patch2, prerelease, compare: compare2, - rcompare: rcompare2, + rcompare: rcompare3, compareLoose, compareBuild, sort, @@ -54868,8 +54868,8 @@ var require_semver5 = __commonJS({ var versionB = new SemVer(b, loose); return versionA.compare(versionB) || versionA.compareBuild(versionB); } - exports2.rcompare = rcompare2; - function rcompare2(a, b, loose) { + exports2.rcompare = rcompare3; + function rcompare3(a, b, loose) { return compare2(b, a, loose); } exports2.sort = sort; @@ -96923,7 +96923,25 @@ function contains(specifier, input) { } // src/version/resolve.ts +var semver5 = __toESM(require_semver5(), 1); + +// src/download/checksum/known-version.ts var semver4 = __toESM(require_semver5(), 1); +var VERSION_IN_CHECKSUM_KEY_PATTERN = /-(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/; +function getLatestKnownVersion() { + const versions = /* @__PURE__ */ new Set(); + for (const key of Object.keys(KNOWN_CHECKSUMS)) { + const version3 = key.match(VERSION_IN_CHECKSUM_KEY_PATTERN)?.[1]; + if (version3 !== void 0) { + versions.add(version3); + } + } + const latestVersion = [...versions].sort(semver4.rcompare)[0]; + if (!latestVersion) { + throw new Error("Could not determine latest known version from checksums."); + } + return latestVersion; +} // src/version/specifier.ts function normalizeVersionSpecifier(specifier) { @@ -97993,6 +98011,9 @@ async function resolveUvVersion(options) { } async function resolveVersion(versionInput, manifestUrl, resolutionStrategy = "highest") { debug(`Resolving version: ${versionInput}`); + if (versionInput.trim() === "latest-known") { + return getLatestKnownVersion(); + } const context3 = { manifestUrl, parsedSpecifier: parseVersionSpecifier(versionInput), @@ -98007,10 +98028,10 @@ async function resolveVersion(versionInput, manifestUrl, resolutionStrategy = "h throw new Error(`No version found for ${versionInput}`); } async function findHighestSatisfyingVersion(versionSpecifier) { - const semverRange = semver4.validRange(versionSpecifier); + const semverRange = semver5.validRange(versionSpecifier); if (semverRange !== null) { const semverMatch = await getFirstMatchingVersion( - (version3) => semver4.satisfies(version3, semverRange) + (version3) => semver5.satisfies(version3, semverRange) ); if (semverMatch !== void 0) { debug( @@ -98045,7 +98066,7 @@ function maxSatisfying2(versions, version3) { return void 0; } function minSatisfying3(versions, version3) { - const minSemver = semver4.minSatisfying(versions, version3); + const minSemver = semver5.minSatisfying(versions, version3); if (minSemver !== null) { debug(`Found a version that satisfies the semver range: ${minSemver}`); return minSemver; diff --git a/dist/update-known-checksums/index.cjs b/dist/update-known-checksums/index.cjs index 833372b..72b9bab 100644 --- a/dist/update-known-checksums/index.cjs +++ b/dist/update-known-checksums/index.cjs @@ -45701,6 +45701,9 @@ function info(message) { } // src/update-known-checksums.ts +var semver2 = __toESM(require_semver(), 1); + +// src/download/checksum/known-version.ts var semver = __toESM(require_semver(), 1); // src/download/checksum/known-checksums.ts @@ -50833,6 +50836,23 @@ var KNOWN_CHECKSUMS = { "x86_64-unknown-linux-musl-0.0.5": "705bbe04a93a9d4d9db5224c2f980a88bba272538a33a78ea2e966f46b4d5eb7" }; +// src/download/checksum/known-version.ts +var VERSION_IN_CHECKSUM_KEY_PATTERN = /-(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/; +function getLatestKnownVersion() { + const versions = /* @__PURE__ */ new Set(); + for (const key of Object.keys(KNOWN_CHECKSUMS)) { + const version = key.match(VERSION_IN_CHECKSUM_KEY_PATTERN)?.[1]; + if (version !== void 0) { + 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; +} + // src/download/checksum/update-known-checksums.ts var import_node_fs = require("node:fs"); async function updateChecksums(filePath, checksumEntries) { @@ -51047,7 +51067,6 @@ function isRecord(value) { } // src/update-known-checksums.ts -var VERSION_IN_CHECKSUM_KEY_PATTERN = /-(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/; async function run() { const checksumFilePath = process.argv.slice(2)[0]; if (!checksumFilePath) { @@ -51056,8 +51075,8 @@ async function run() { ); } const latestVersion = await getLatestVersion(); - const latestKnownVersion = getLatestKnownVersionFromChecksums(); - if (semver.lte(latestVersion, latestKnownVersion)) { + const latestKnownVersion = getLatestKnownVersion(); + if (semver2.lte(latestVersion, latestKnownVersion)) { info2( `Latest release (${latestVersion}) is not newer than the latest known version (${latestKnownVersion}). Skipping update.` ); @@ -51068,23 +51087,6 @@ async function run() { await updateChecksums(checksumFilePath, checksumEntries); setOutput("latest-version", latestVersion); } -function getLatestKnownVersionFromChecksums() { - const versions = /* @__PURE__ */ new Set(); - for (const key of Object.keys(KNOWN_CHECKSUMS)) { - const version = extractVersionFromChecksumKey(key); - if (version !== void 0) { - 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) { - return key.match(VERSION_IN_CHECKSUM_KEY_PATTERN)?.[1]; -} function extractChecksumsFromManifest(versions) { const checksums = []; for (const version of versions) { diff --git a/docs/advanced-version-configuration.md b/docs/advanced-version-configuration.md index 9925af4..4b0053a 100644 --- a/docs/advanced-version-configuration.md +++ b/docs/advanced-version-configuration.md @@ -11,6 +11,19 @@ This document covers advanced options for configuring which version of uv to ins version: "latest" ``` +## Install the latest version with a checksum verified by this action + +Use `latest-known` to install the newest uv version whose checksums were bundled with the version of setup-uv used by your workflow. Version resolution is performed locally without fetching the latest release, so updating setup-uv also updates the version selected by `latest-known`. + +When `manifest-file` is set, `latest-known` still selects a version from setup-uv's bundled checksum table, but the artifact and checksum come from the custom manifest. + +```yaml +- name: Install the latest version of uv known to setup-uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "latest-known" +``` + ## Install a specific version ```yaml diff --git a/src/download/checksum/known-version.ts b/src/download/checksum/known-version.ts new file mode 100644 index 0000000..39b7e70 --- /dev/null +++ b/src/download/checksum/known-version.ts @@ -0,0 +1,23 @@ +import * as semver from "semver"; +import { KNOWN_CHECKSUMS } from "./known-checksums"; + +const VERSION_IN_CHECKSUM_KEY_PATTERN = + /-(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/; + +export function getLatestKnownVersion(): string { + const versions = new Set(); + + for (const key of Object.keys(KNOWN_CHECKSUMS)) { + const version = key.match(VERSION_IN_CHECKSUM_KEY_PATTERN)?.[1]; + 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; +} diff --git a/src/update-known-checksums.ts b/src/update-known-checksums.ts index 13f50a1..21479d5 100644 --- a/src/update-known-checksums.ts +++ b/src/update-known-checksums.ts @@ -1,6 +1,6 @@ import * as core from "@actions/core"; import * as semver from "semver"; -import { KNOWN_CHECKSUMS } from "./download/checksum/known-checksums"; +import { getLatestKnownVersion } from "./download/checksum/known-version"; import { type ChecksumEntry, updateChecksums, @@ -12,9 +12,6 @@ import { } from "./download/manifest"; import * as log from "./utils/logging"; -const VERSION_IN_CHECKSUM_KEY_PATTERN = - /-(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/; - async function run(): Promise { const checksumFilePath = process.argv.slice(2)[0]; if (!checksumFilePath) { @@ -24,7 +21,7 @@ async function run(): Promise { } const latestVersion = await getLatestVersion(); - const latestKnownVersion = getLatestKnownVersionFromChecksums(); + const latestKnownVersion = getLatestKnownVersion(); if (semver.lte(latestVersion, latestKnownVersion)) { log.info( @@ -40,28 +37,6 @@ async function run(): Promise { core.setOutput("latest-version", latestVersion); } -function getLatestKnownVersionFromChecksums(): string { - const versions = new Set(); - - 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 extractChecksumsFromManifest( versions: ManifestVersion[], ): ChecksumEntry[] { diff --git a/src/version/resolve.ts b/src/version/resolve.ts index 610af42..ea54af7 100644 --- a/src/version/resolve.ts +++ b/src/version/resolve.ts @@ -2,6 +2,7 @@ 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 { getLatestKnownVersion } from "../download/checksum/known-version"; import { getAllVersions, getFirstMatchingVersion, @@ -143,6 +144,10 @@ export async function resolveVersion( ): Promise { core.debug(`Resolving version: ${versionInput}`); + if (versionInput.trim() === "latest-known") { + return getLatestKnownVersion(); + } + const context: ConcreteVersionResolutionContext = { manifestUrl, parsedSpecifier: parseVersionSpecifier(versionInput),