From 9c6b5e9fb575cac8e82bb437dd7fc25a094bd85d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Oct 2025 21:02:04 +0200 Subject: [PATCH] Add resolution-strategy input to support oldest compatible version selection (#631) Adds a new `resolution-strategy` input that allows users to choose between installing the highest (default) or lowest compatible version when resolving version ranges. --- .github/workflows/test.yml | 12 +++++++++- README.md | 20 ++++++++++++++++ action.yml | 3 +++ dist/save-cache/index.js | 13 +++++++++- dist/setup/index.js | 41 +++++++++++++++++++++++++++----- src/download/download-version.ts | 28 +++++++++++++++++++++- src/setup-uv.ts | 16 +++++++++++-- src/utils/inputs.ts | 14 +++++++++++ 8 files changed, 136 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 93329f4..f91cc26 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -111,15 +111,25 @@ jobs: expected-version: "0.3.5" - version-input: ">=0.4.25,<0.5" expected-version: "0.4.30" + - version-input: ">=0.4.25,<0.5" + expected-version: "0.4.25" + resolution-strategy: "lowest" + - version-input: ">=0.1,<0.2" + expected-version: "0.1.45" + resolution-strategy: "highest" + - version-input: ">=0.1.0,<0.2" + expected-version: "0.1.0" + resolution-strategy: "lowest" steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - - name: Install version ${{ matrix.input.version-input }} + - name: Install version ${{ matrix.input.version-input }} with strategy ${{ matrix.input.resolution-strategy || 'highest' }} id: setup-uv uses: ./ with: version: ${{ matrix.input.version-input }} + resolution-strategy: ${{ matrix.input.resolution-strategy || 'highest' }} - name: Correct version gets installed run: | if [ "$(uv --version)" != "uv ${{ matrix.input.expected-version }}" ]; then diff --git a/README.md b/README.md index b9f642a..caa0900 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Set up your GitHub Actions workflow with a specific version of [uv](https://docs - [Install the latest version](#install-the-latest-version) - [Install a specific version](#install-a-specific-version) - [Install a version by supplying a semver range or pep440 specifier](#install-a-version-by-supplying-a-semver-range-or-pep440-specifier) + - [Resolution strategy](#resolution-strategy) - [Install a version defined in a requirements or config file](#install-a-version-defined-in-a-requirements-or-config-file) - [Python version](#python-version) - [Activate environment](#activate-environment) @@ -97,6 +98,25 @@ to install the latest version that satisfies the range. version: ">=0.4.25,<0.5" ``` +### Resolution strategy + +By default, when resolving version ranges, setup-uv will install the highest compatible version. +You can change this behavior using the `resolution-strategy` input: + +```yaml +- name: Install the lowest compatible version of uv + uses: astral-sh/setup-uv@v6 + with: + version: ">=0.4.0" + resolution-strategy: "lowest" +``` + +The supported resolution strategies are: +- `highest` (default): Install the latest version that satisfies the constraints +- `lowest`: Install the oldest version that satisfies the constraints + +This can be useful for testing compatibility with older versions of uv, similar to uv's own `--resolution-strategy` option. + ### Install a version defined in a requirements or config file You can use the `version-file` input to specify a file that contains the version of uv to install. diff --git a/action.yml b/action.yml index c29a89d..1e05946 100644 --- a/action.yml +++ b/action.yml @@ -77,6 +77,9 @@ inputs: add-problem-matchers: description: "Add problem matchers." default: "true" + resolution-strategy: + description: "Resolution strategy to use when resolving version ranges. 'highest' uses the latest compatible version, 'lowest' uses the oldest compatible version." + default: "highest" outputs: uv-version: description: "The installed uv version. Useful when using latest." diff --git a/dist/save-cache/index.js b/dist/save-cache/index.js index d5a9799..0da895c 100644 --- a/dist/save-cache/index.js +++ b/dist/save-cache/index.js @@ -91010,7 +91010,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.addProblemMatchers = exports.manifestFile = exports.githubToken = exports.pythonDir = exports.toolDir = exports.toolBinDir = exports.ignoreEmptyWorkdir = exports.ignoreNothingToCache = exports.cachePython = exports.pruneCache = exports.cacheDependencyGlob = exports.cacheLocalPath = exports.cacheSuffix = exports.saveCache = exports.restoreCache = exports.enableCache = exports.checkSum = exports.activateEnvironment = exports.pythonVersion = exports.versionFile = exports.version = exports.workingDirectory = void 0; +exports.resolutionStrategy = exports.addProblemMatchers = exports.manifestFile = exports.githubToken = exports.pythonDir = exports.toolDir = exports.toolBinDir = exports.ignoreEmptyWorkdir = exports.ignoreNothingToCache = exports.cachePython = exports.pruneCache = exports.cacheDependencyGlob = exports.cacheLocalPath = exports.cacheSuffix = exports.saveCache = exports.restoreCache = exports.enableCache = exports.checkSum = exports.activateEnvironment = exports.pythonVersion = exports.versionFile = exports.version = exports.workingDirectory = void 0; exports.getUvPythonDir = getUvPythonDir; const node_path_1 = __importDefault(__nccwpck_require__(6760)); const core = __importStar(__nccwpck_require__(7484)); @@ -91037,6 +91037,7 @@ exports.pythonDir = getUvPythonDir(); exports.githubToken = core.getInput("github-token"); exports.manifestFile = getManifestFile(); exports.addProblemMatchers = core.getInput("add-problem-matchers") === "true"; +exports.resolutionStrategy = getResolutionStrategy(); function getVersionFile() { const versionFileInput = core.getInput("version-file"); if (versionFileInput !== "") { @@ -91173,6 +91174,16 @@ function getManifestFile() { } return undefined; } +function getResolutionStrategy() { + const resolutionStrategyInput = core.getInput("resolution-strategy"); + if (resolutionStrategyInput === "lowest") { + return "lowest"; + } + if (resolutionStrategyInput === "highest" || resolutionStrategyInput === "") { + return "highest"; + } + throw new Error(`Invalid resolution-strategy: ${resolutionStrategyInput}. Must be 'highest' or 'lowest'.`); +} /***/ }), diff --git a/dist/setup/index.js b/dist/setup/index.js index 79be815..e284552 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -129114,6 +129114,7 @@ const path = __importStar(__nccwpck_require__(76760)); const core = __importStar(__nccwpck_require__(37484)); const tc = __importStar(__nccwpck_require__(33472)); const pep440 = __importStar(__nccwpck_require__(63297)); +const semver = __importStar(__nccwpck_require__(39318)); const constants_1 = __nccwpck_require__(56156); const octokit_1 = __nccwpck_require__(73352); const checksum_1 = __nccwpck_require__(17772); @@ -129165,7 +129166,7 @@ async function downloadVersion(downloadUrl, artifactName, platform, arch, versio function getExtension(platform) { return platform === "pc-windows-msvc" ? ".zip" : ".tar.gz"; } -async function resolveVersion(versionInput, manifestFile, githubToken) { +async function resolveVersion(versionInput, manifestFile, githubToken, resolutionStrategy = "highest") { core.debug(`Resolving version: ${versionInput}`); let version; const isSimpleMinimumVersionSpecifier = versionInput.includes(">") && !versionInput.includes(","); @@ -129195,7 +129196,9 @@ async function resolveVersion(versionInput, manifestFile, githubToken) { } const availableVersions = await getAvailableVersions(githubToken); core.debug(`Available versions: ${availableVersions}`); - const resolvedVersion = maxSatisfying(availableVersions, version); + const resolvedVersion = resolutionStrategy === "lowest" + ? minSatisfying(availableVersions, version) + : maxSatisfying(availableVersions, version); if (resolvedVersion === undefined) { throw new Error(`No version found for ${version}`); } @@ -129275,6 +129278,21 @@ function maxSatisfying(versions, version) { } return undefined; } +function minSatisfying(versions, version) { + // For semver, we need to use a different approach since tc.evaluateVersions only returns max + // Let's use semver directly for min satisfying + const minSemver = semver.minSatisfying(versions, version); + if (minSemver !== null) { + core.debug(`Found a version that satisfies the semver range: ${minSemver}`); + return minSemver; + } + const minPep440 = pep440.minSatisfying(versions, version); + if (minPep440 !== null) { + core.debug(`Found a version that satisfies the pep440 specifier: ${minPep440}`); + return minPep440; + } + return undefined; +} /***/ }), @@ -129583,21 +129601,21 @@ async function setupUv(platform, arch, checkSum, githubToken) { } async function determineVersion(manifestFile) { if (inputs_1.version !== "") { - return await (0, download_version_1.resolveVersion)(inputs_1.version, manifestFile, inputs_1.githubToken); + return await (0, download_version_1.resolveVersion)(inputs_1.version, manifestFile, inputs_1.githubToken, inputs_1.resolutionStrategy); } if (inputs_1.versionFile !== "") { const versionFromFile = (0, resolve_1.getUvVersionFromFile)(inputs_1.versionFile); if (versionFromFile === undefined) { throw new Error(`Could not determine uv version from file: ${inputs_1.versionFile}`); } - return await (0, download_version_1.resolveVersion)(versionFromFile, manifestFile, inputs_1.githubToken); + return await (0, download_version_1.resolveVersion)(versionFromFile, manifestFile, inputs_1.githubToken, inputs_1.resolutionStrategy); } const versionFromUvToml = (0, resolve_1.getUvVersionFromFile)(`${inputs_1.workingDirectory}${path.sep}uv.toml`); const versionFromPyproject = (0, resolve_1.getUvVersionFromFile)(`${inputs_1.workingDirectory}${path.sep}pyproject.toml`); if (versionFromUvToml === undefined && versionFromPyproject === undefined) { core.info("Could not determine uv version from uv.toml or pyproject.toml. Falling back to latest."); } - return await (0, download_version_1.resolveVersion)(versionFromUvToml || versionFromPyproject || "latest", manifestFile, inputs_1.githubToken); + return await (0, download_version_1.resolveVersion)(versionFromUvToml || versionFromPyproject || "latest", manifestFile, inputs_1.githubToken, inputs_1.resolutionStrategy); } function addUvToPathAndOutput(cachedPath) { core.setOutput("uv-path", `${cachedPath}${path.sep}uv`); @@ -129853,7 +129871,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.addProblemMatchers = exports.manifestFile = exports.githubToken = exports.pythonDir = exports.toolDir = exports.toolBinDir = exports.ignoreEmptyWorkdir = exports.ignoreNothingToCache = exports.cachePython = exports.pruneCache = exports.cacheDependencyGlob = exports.cacheLocalPath = exports.cacheSuffix = exports.saveCache = exports.restoreCache = exports.enableCache = exports.checkSum = exports.activateEnvironment = exports.pythonVersion = exports.versionFile = exports.version = exports.workingDirectory = void 0; +exports.resolutionStrategy = exports.addProblemMatchers = exports.manifestFile = exports.githubToken = exports.pythonDir = exports.toolDir = exports.toolBinDir = exports.ignoreEmptyWorkdir = exports.ignoreNothingToCache = exports.cachePython = exports.pruneCache = exports.cacheDependencyGlob = exports.cacheLocalPath = exports.cacheSuffix = exports.saveCache = exports.restoreCache = exports.enableCache = exports.checkSum = exports.activateEnvironment = exports.pythonVersion = exports.versionFile = exports.version = exports.workingDirectory = void 0; exports.getUvPythonDir = getUvPythonDir; const node_path_1 = __importDefault(__nccwpck_require__(76760)); const core = __importStar(__nccwpck_require__(37484)); @@ -129880,6 +129898,7 @@ exports.pythonDir = getUvPythonDir(); exports.githubToken = core.getInput("github-token"); exports.manifestFile = getManifestFile(); exports.addProblemMatchers = core.getInput("add-problem-matchers") === "true"; +exports.resolutionStrategy = getResolutionStrategy(); function getVersionFile() { const versionFileInput = core.getInput("version-file"); if (versionFileInput !== "") { @@ -130016,6 +130035,16 @@ function getManifestFile() { } return undefined; } +function getResolutionStrategy() { + const resolutionStrategyInput = core.getInput("resolution-strategy"); + if (resolutionStrategyInput === "lowest") { + return "lowest"; + } + if (resolutionStrategyInput === "highest" || resolutionStrategyInput === "") { + return "highest"; + } + throw new Error(`Invalid resolution-strategy: ${resolutionStrategyInput}. Must be 'highest' or 'lowest'.`); +} /***/ }), diff --git a/src/download/download-version.ts b/src/download/download-version.ts index 118907f..fa86ebe 100644 --- a/src/download/download-version.ts +++ b/src/download/download-version.ts @@ -4,6 +4,7 @@ import * as core from "@actions/core"; import * as tc from "@actions/tool-cache"; import type { Endpoints } from "@octokit/types"; import * as pep440 from "@renovatebot/pep440"; +import * as semver from "semver"; import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants"; import { Octokit } from "../utils/octokit"; import type { Architecture, Platform } from "../utils/platforms"; @@ -134,6 +135,7 @@ export async function resolveVersion( versionInput: string, manifestFile: string | undefined, githubToken: string, + resolutionStrategy: "highest" | "lowest" = "highest", ): Promise { core.debug(`Resolving version: ${versionInput}`); let version: string; @@ -164,7 +166,10 @@ export async function resolveVersion( } const availableVersions = await getAvailableVersions(githubToken); core.debug(`Available versions: ${availableVersions}`); - const resolvedVersion = maxSatisfying(availableVersions, version); + const resolvedVersion = + resolutionStrategy === "lowest" + ? minSatisfying(availableVersions, version) + : maxSatisfying(availableVersions, version); if (resolvedVersion === undefined) { throw new Error(`No version found for ${version}`); } @@ -264,3 +269,24 @@ function maxSatisfying( } return undefined; } + +function minSatisfying( + versions: string[], + version: string, +): string | undefined { + // For semver, we need to use a different approach since tc.evaluateVersions only returns max + // Let's use semver directly for min satisfying + const minSemver = semver.minSatisfying(versions, version); + if (minSemver !== null) { + core.debug(`Found a version that satisfies the semver range: ${minSemver}`); + return minSemver; + } + const minPep440 = pep440.minSatisfying(versions, version); + if (minPep440 !== null) { + core.debug( + `Found a version that satisfies the pep440 specifier: ${minPep440}`, + ); + return minPep440; + } + return undefined; +} diff --git a/src/setup-uv.ts b/src/setup-uv.ts index fb9c419..03b2434 100644 --- a/src/setup-uv.ts +++ b/src/setup-uv.ts @@ -21,6 +21,7 @@ import { manifestFile, pythonDir, pythonVersion, + resolutionStrategy, toolBinDir, toolDir, versionFile as versionFileInput, @@ -120,7 +121,12 @@ async function determineVersion( manifestFile: string | undefined, ): Promise { if (versionInput !== "") { - return await resolveVersion(versionInput, manifestFile, githubToken); + return await resolveVersion( + versionInput, + manifestFile, + githubToken, + resolutionStrategy, + ); } if (versionFileInput !== "") { const versionFromFile = getUvVersionFromFile(versionFileInput); @@ -129,7 +135,12 @@ async function determineVersion( `Could not determine uv version from file: ${versionFileInput}`, ); } - return await resolveVersion(versionFromFile, manifestFile, githubToken); + return await resolveVersion( + versionFromFile, + manifestFile, + githubToken, + resolutionStrategy, + ); } const versionFromUvToml = getUvVersionFromFile( `${workingDirectory}${path.sep}uv.toml`, @@ -146,6 +157,7 @@ async function determineVersion( versionFromUvToml || versionFromPyproject || "latest", manifestFile, githubToken, + resolutionStrategy, ); } diff --git a/src/utils/inputs.ts b/src/utils/inputs.ts index c9d22d9..7011c9b 100644 --- a/src/utils/inputs.ts +++ b/src/utils/inputs.ts @@ -27,6 +27,7 @@ export const githubToken = core.getInput("github-token"); export const manifestFile = getManifestFile(); export const addProblemMatchers = core.getInput("add-problem-matchers") === "true"; +export const resolutionStrategy = getResolutionStrategy(); function getVersionFile(): string { const versionFileInput = core.getInput("version-file"); @@ -186,3 +187,16 @@ function getManifestFile(): string | undefined { } return undefined; } + +function getResolutionStrategy(): "highest" | "lowest" { + const resolutionStrategyInput = core.getInput("resolution-strategy"); + if (resolutionStrategyInput === "lowest") { + return "lowest"; + } + if (resolutionStrategyInput === "highest" || resolutionStrategyInput === "") { + return "highest"; + } + throw new Error( + `Invalid resolution-strategy: ${resolutionStrategyInput}. Must be 'highest' or 'lowest'.`, + ); +}