Add resolution-strategy input with highest/lowest options

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-10-11 17:30:01 +00:00
parent 27debe0b58
commit 84643229bb
9 changed files with 194 additions and 10 deletions

View file

@ -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 the latest version](#install-the-latest-version)
- [Install a specific version](#install-a-specific-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) - [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) - [Install a version defined in a requirements or config file](#install-a-version-defined-in-a-requirements-or-config-file)
- [Python version](#python-version) - [Python version](#python-version)
- [Activate environment](#activate-environment) - [Activate environment](#activate-environment)
@ -97,6 +98,25 @@ to install the latest version that satisfies the range.
version: ">=0.4.25,<0.5" 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 ### 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. You can use the `version-file` input to specify a file that contains the version of uv to install.

View file

@ -0,0 +1,24 @@
import { describe, expect, it } from "@jest/globals";
describe("resolution strategy logic", () => {
it("should have correct string values for resolution strategy", () => {
// Test the string literal types are correct
const strategies: Array<"highest" | "lowest"> = ["highest", "lowest"];
expect(strategies).toHaveLength(2);
expect(strategies).toContain("highest");
expect(strategies).toContain("lowest");
});
it("should validate resolution strategy values", () => {
const validStrategies = ["highest", "lowest"];
const invalidStrategies = ["invalid", "HIGHEST", "LOWEST", "middle"];
for (const strategy of validStrategies) {
expect(["highest", "lowest"]).toContain(strategy);
}
for (const strategy of invalidStrategies) {
expect(["highest", "lowest"]).not.toContain(strategy);
}
});
});

View file

@ -0,0 +1,45 @@
jest.mock("@actions/core", () => {
return {
debug: jest.fn(),
getBooleanInput: jest.fn(
(name: string) => (mockInputs[name] ?? "") === "true",
),
getInput: jest.fn((name: string) => mockInputs[name] ?? ""),
};
});
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
// Will be mutated per test before (re-)importing the module under test
let mockInputs: Record<string, string> = {};
describe("resolutionStrategy", () => {
beforeEach(() => {
jest.resetModules();
mockInputs = {};
});
it("returns 'highest' when input not provided", async () => {
const { resolutionStrategy } = await import("../../src/utils/inputs");
expect(resolutionStrategy).toBe("highest");
});
it("returns 'highest' when input is 'highest'", async () => {
mockInputs["resolution-strategy"] = "highest";
const { resolutionStrategy } = await import("../../src/utils/inputs");
expect(resolutionStrategy).toBe("highest");
});
it("returns 'lowest' when input is 'lowest'", async () => {
mockInputs["resolution-strategy"] = "lowest";
const { resolutionStrategy } = await import("../../src/utils/inputs");
expect(resolutionStrategy).toBe("lowest");
});
it("throws error for invalid input", async () => {
mockInputs["resolution-strategy"] = "invalid";
await expect(import("../../src/utils/inputs")).rejects.toThrow(
"Invalid resolution-strategy: invalid. Must be 'highest' or 'lowest'.",
);
});
});

View file

@ -77,6 +77,9 @@ inputs:
add-problem-matchers: add-problem-matchers:
description: "Add problem matchers." description: "Add problem matchers."
default: "true" 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: outputs:
uv-version: uv-version:
description: "The installed uv version. Useful when using latest." description: "The installed uv version. Useful when using latest."

13
dist/save-cache/index.js generated vendored
View file

@ -91010,7 +91010,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); 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; exports.getUvPythonDir = getUvPythonDir;
const node_path_1 = __importDefault(__nccwpck_require__(6760)); const node_path_1 = __importDefault(__nccwpck_require__(6760));
const core = __importStar(__nccwpck_require__(7484)); const core = __importStar(__nccwpck_require__(7484));
@ -91037,6 +91037,7 @@ exports.pythonDir = getUvPythonDir();
exports.githubToken = core.getInput("github-token"); exports.githubToken = core.getInput("github-token");
exports.manifestFile = getManifestFile(); exports.manifestFile = getManifestFile();
exports.addProblemMatchers = core.getInput("add-problem-matchers") === "true"; exports.addProblemMatchers = core.getInput("add-problem-matchers") === "true";
exports.resolutionStrategy = getResolutionStrategy();
function getVersionFile() { function getVersionFile() {
const versionFileInput = core.getInput("version-file"); const versionFileInput = core.getInput("version-file");
if (versionFileInput !== "") { if (versionFileInput !== "") {
@ -91173,6 +91174,16 @@ function getManifestFile() {
} }
return undefined; 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'.`);
}
/***/ }), /***/ }),

41
dist/setup/index.js generated vendored
View file

@ -129114,6 +129114,7 @@ const path = __importStar(__nccwpck_require__(76760));
const core = __importStar(__nccwpck_require__(37484)); const core = __importStar(__nccwpck_require__(37484));
const tc = __importStar(__nccwpck_require__(33472)); const tc = __importStar(__nccwpck_require__(33472));
const pep440 = __importStar(__nccwpck_require__(63297)); const pep440 = __importStar(__nccwpck_require__(63297));
const semver = __importStar(__nccwpck_require__(39318));
const constants_1 = __nccwpck_require__(56156); const constants_1 = __nccwpck_require__(56156);
const octokit_1 = __nccwpck_require__(73352); const octokit_1 = __nccwpck_require__(73352);
const checksum_1 = __nccwpck_require__(17772); const checksum_1 = __nccwpck_require__(17772);
@ -129165,7 +129166,7 @@ async function downloadVersion(downloadUrl, artifactName, platform, arch, versio
function getExtension(platform) { function getExtension(platform) {
return platform === "pc-windows-msvc" ? ".zip" : ".tar.gz"; 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}`); core.debug(`Resolving version: ${versionInput}`);
let version; let version;
const isSimpleMinimumVersionSpecifier = versionInput.includes(">") && !versionInput.includes(","); const isSimpleMinimumVersionSpecifier = versionInput.includes(">") && !versionInput.includes(",");
@ -129195,7 +129196,9 @@ async function resolveVersion(versionInput, manifestFile, githubToken) {
} }
const availableVersions = await getAvailableVersions(githubToken); const availableVersions = await getAvailableVersions(githubToken);
core.debug(`Available versions: ${availableVersions}`); core.debug(`Available versions: ${availableVersions}`);
const resolvedVersion = maxSatisfying(availableVersions, version); const resolvedVersion = resolutionStrategy === "lowest"
? minSatisfying(availableVersions, version)
: maxSatisfying(availableVersions, version);
if (resolvedVersion === undefined) { if (resolvedVersion === undefined) {
throw new Error(`No version found for ${version}`); throw new Error(`No version found for ${version}`);
} }
@ -129275,6 +129278,21 @@ function maxSatisfying(versions, version) {
} }
return undefined; 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) { async function determineVersion(manifestFile) {
if (inputs_1.version !== "") { 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 !== "") { if (inputs_1.versionFile !== "") {
const versionFromFile = (0, resolve_1.getUvVersionFromFile)(inputs_1.versionFile); const versionFromFile = (0, resolve_1.getUvVersionFromFile)(inputs_1.versionFile);
if (versionFromFile === undefined) { if (versionFromFile === undefined) {
throw new Error(`Could not determine uv version from file: ${inputs_1.versionFile}`); 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 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`); const versionFromPyproject = (0, resolve_1.getUvVersionFromFile)(`${inputs_1.workingDirectory}${path.sep}pyproject.toml`);
if (versionFromUvToml === undefined && versionFromPyproject === undefined) { if (versionFromUvToml === undefined && versionFromPyproject === undefined) {
core.info("Could not determine uv version from uv.toml or pyproject.toml. Falling back to latest."); 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) { function addUvToPathAndOutput(cachedPath) {
core.setOutput("uv-path", `${cachedPath}${path.sep}uv`); 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 }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); 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; exports.getUvPythonDir = getUvPythonDir;
const node_path_1 = __importDefault(__nccwpck_require__(76760)); const node_path_1 = __importDefault(__nccwpck_require__(76760));
const core = __importStar(__nccwpck_require__(37484)); const core = __importStar(__nccwpck_require__(37484));
@ -129880,6 +129898,7 @@ exports.pythonDir = getUvPythonDir();
exports.githubToken = core.getInput("github-token"); exports.githubToken = core.getInput("github-token");
exports.manifestFile = getManifestFile(); exports.manifestFile = getManifestFile();
exports.addProblemMatchers = core.getInput("add-problem-matchers") === "true"; exports.addProblemMatchers = core.getInput("add-problem-matchers") === "true";
exports.resolutionStrategy = getResolutionStrategy();
function getVersionFile() { function getVersionFile() {
const versionFileInput = core.getInput("version-file"); const versionFileInput = core.getInput("version-file");
if (versionFileInput !== "") { if (versionFileInput !== "") {
@ -130016,6 +130035,16 @@ function getManifestFile() {
} }
return undefined; 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'.`);
}
/***/ }), /***/ }),

View file

@ -4,6 +4,7 @@ import * as core from "@actions/core";
import * as tc from "@actions/tool-cache"; import * as tc from "@actions/tool-cache";
import type { Endpoints } from "@octokit/types"; import type { Endpoints } from "@octokit/types";
import * as pep440 from "@renovatebot/pep440"; import * as pep440 from "@renovatebot/pep440";
import * as semver from "semver";
import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants"; import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants";
import { Octokit } from "../utils/octokit"; import { Octokit } from "../utils/octokit";
import type { Architecture, Platform } from "../utils/platforms"; import type { Architecture, Platform } from "../utils/platforms";
@ -134,6 +135,7 @@ export async function resolveVersion(
versionInput: string, versionInput: string,
manifestFile: string | undefined, manifestFile: string | undefined,
githubToken: string, githubToken: string,
resolutionStrategy: "highest" | "lowest" = "highest",
): Promise<string> { ): Promise<string> {
core.debug(`Resolving version: ${versionInput}`); core.debug(`Resolving version: ${versionInput}`);
let version: string; let version: string;
@ -164,7 +166,10 @@ export async function resolveVersion(
} }
const availableVersions = await getAvailableVersions(githubToken); const availableVersions = await getAvailableVersions(githubToken);
core.debug(`Available versions: ${availableVersions}`); core.debug(`Available versions: ${availableVersions}`);
const resolvedVersion = maxSatisfying(availableVersions, version); const resolvedVersion =
resolutionStrategy === "lowest"
? minSatisfying(availableVersions, version)
: maxSatisfying(availableVersions, version);
if (resolvedVersion === undefined) { if (resolvedVersion === undefined) {
throw new Error(`No version found for ${version}`); throw new Error(`No version found for ${version}`);
} }
@ -264,3 +269,24 @@ function maxSatisfying(
} }
return undefined; 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;
}

View file

@ -21,6 +21,7 @@ import {
manifestFile, manifestFile,
pythonDir, pythonDir,
pythonVersion, pythonVersion,
resolutionStrategy,
toolBinDir, toolBinDir,
toolDir, toolDir,
versionFile as versionFileInput, versionFile as versionFileInput,
@ -120,7 +121,12 @@ async function determineVersion(
manifestFile: string | undefined, manifestFile: string | undefined,
): Promise<string> { ): Promise<string> {
if (versionInput !== "") { if (versionInput !== "") {
return await resolveVersion(versionInput, manifestFile, githubToken); return await resolveVersion(
versionInput,
manifestFile,
githubToken,
resolutionStrategy,
);
} }
if (versionFileInput !== "") { if (versionFileInput !== "") {
const versionFromFile = getUvVersionFromFile(versionFileInput); const versionFromFile = getUvVersionFromFile(versionFileInput);
@ -129,7 +135,12 @@ async function determineVersion(
`Could not determine uv version from file: ${versionFileInput}`, `Could not determine uv version from file: ${versionFileInput}`,
); );
} }
return await resolveVersion(versionFromFile, manifestFile, githubToken); return await resolveVersion(
versionFromFile,
manifestFile,
githubToken,
resolutionStrategy,
);
} }
const versionFromUvToml = getUvVersionFromFile( const versionFromUvToml = getUvVersionFromFile(
`${workingDirectory}${path.sep}uv.toml`, `${workingDirectory}${path.sep}uv.toml`,
@ -146,6 +157,7 @@ async function determineVersion(
versionFromUvToml || versionFromPyproject || "latest", versionFromUvToml || versionFromPyproject || "latest",
manifestFile, manifestFile,
githubToken, githubToken,
resolutionStrategy,
); );
} }

View file

@ -27,6 +27,7 @@ export const githubToken = core.getInput("github-token");
export const manifestFile = getManifestFile(); export const manifestFile = getManifestFile();
export const addProblemMatchers = export const addProblemMatchers =
core.getInput("add-problem-matchers") === "true"; core.getInput("add-problem-matchers") === "true";
export const resolutionStrategy = getResolutionStrategy();
function getVersionFile(): string { function getVersionFile(): string {
const versionFileInput = core.getInput("version-file"); const versionFileInput = core.getInput("version-file");
@ -186,3 +187,16 @@ function getManifestFile(): string | undefined {
} }
return 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'.`,
);
}