Add resolution-strategy input to support oldest compatible version selection (#631)
Some checks failed
CodeQL / Analyze (TypeScript) (push) Failing after 2s
test / lint (push) Failing after 2s
test / test-default-version (ubuntu-latest) (push) Failing after 2s
test / test-specific-version (map[expected-version:0.1.0 resolution-strategy:lowest version-input:>=0.1.0,<0.2]) (push) Failing after 1s
test / test-uv-no-modify-path (push) Failing after 1s
test / test-specific-version (map[expected-version:0.1.45 resolution-strategy:highest version-input:>=0.1,<0.2]) (push) Failing after 1s
test / test-specific-version (map[expected-version:0.3.0 version-input:0.3.0]) (push) Failing after 1s
test / test-specific-version (map[expected-version:0.3.2 version-input:0.3.2]) (push) Failing after 2s
test / test-specific-version (map[expected-version:0.3.5 version-input:0.3.x]) (push) Failing after 1s
test / test-specific-version (map[expected-version:0.3.5 version-input:0.3]) (push) Failing after 1s
test / test-specific-version (map[expected-version:0.4.25 resolution-strategy:lowest version-input:>=0.4.25,<0.5]) (push) Failing after 1s
test / test-specific-version (map[expected-version:0.4.30 version-input:>=0.4.25,<0.5]) (push) Failing after 1s
test / test-latest-version (>=0.8) (push) Failing after 2s
test / test-latest-version (latest) (push) Failing after 1s
test / test-from-working-directory-version (map[expected-version:0.5.14 working-directory:__tests__/fixtures/pyproject-toml-project]) (push) Failing after 1s
test / test-from-working-directory-version (map[expected-version:0.5.15 working-directory:__tests__/fixtures/uv-toml-project]) (push) Failing after 2s
test / test-version-file-version (map[expected-version:0.5.15 version-file:__tests__/fixtures/.tool-versions]) (push) Failing after 2s
test / test-version-file-version (map[expected-version:0.6.17 version-file:__tests__/fixtures/uv-in-requirements-txt-project/requirements.txt]) (push) Failing after 1s
test / test-version-file-version (map[expected-version:0.8.3 version-file:__tests__/fixtures/uv-in-requirements-hash-txt-project/requirements.txt]) (push) Failing after 1s
test / test-malformed-pyproject-file-fallback (push) Failing after 1s
test / test-checksum (map[checksum:4d9279ad5ca596b1e2d703901d508430eb07564dc4d8837de9e2fca9c90f8ecd os:ubuntu-latest]) (push) Failing after 1s
test / test-with-explicit-token (push) Failing after 1s
test / test-uvx (push) Failing after 1s
test / test-tool-install (ubuntu-latest) (push) Failing after 1s
test / test-python-version (ubuntu-latest) (push) Failing after 1s
test / test-activate-environment (ubuntu-latest) (push) Failing after 1s
test / test-setup-cache (auto, ubuntu-latest) (push) Failing after 1s
test / test-setup-cache (false, ubuntu-latest) (push) Failing after 1s
test / test-setup-cache (true, ubuntu-latest) (push) Failing after 1s
test / test-musl (push) Failing after 5s
test / test-restore-cache-requirements-txt (push) Has been skipped
test / test-setup-cache-requirements-txt (push) Failing after 1s
test / test-setup-cache-dependency-glob (push) Failing after 2s
test / test-restore-cache-dependency-glob (push) Has been skipped
test / test-setup-cache-save-cache-false (push) Failing after 4s
test / test-restore-cache-save-cache-false (push) Has been skipped
test / test-setup-cache-restore-cache-false (push) Failing after 4s
test / test-restore-cache-restore-cache-false (push) Has been skipped
test / test-cache-local (map[expected-cache-dir:/home/runner/work/_temp/setup-uv-cache os:ubuntu-latest]) (push) Failing after 5s
test / test-cache-local-cache-disabled (push) Failing after 5s
test / test-no-python-version (push) Failing after 4s
test / test-custom-manifest-file (push) Failing after 4s
test / test-absolute-path (push) Failing after 3s
test / test-relative-path (push) Failing after 3s
test / test-cache-prune-force (push) Failing after 3s
test / test-cache-dir-from-file (push) Failing after 4s
test / test-cache-python-installs (push) Failing after 4s
test / test-restore-python-installs (push) Has been skipped
test / test-python-install-dir (map[expected-python-dir:/home/runner/work/_temp/uv-python-dir os:ubuntu-latest]) (push) Failing after 5s
Release Drafter / ✏️ Draft release (push) Has been cancelled
test / test-tool-install (macos-latest) (push) Has been cancelled
test / test-tool-install (windows-latest) (push) Has been cancelled
test / test-tilde-expansion-tool-dirs (push) Has been cancelled
test / test-python-version (macos-latest) (push) Has been cancelled
test / test-default-version (macos-14) (push) Has been cancelled
test / test-default-version (macos-latest) (push) Has been cancelled
test / test-default-version (windows-latest) (push) Has been cancelled
test / test-checksum (map[checksum:a70cbfbf3bb5c08b2f84963b4f12c94e08fbb2468ba418a3bfe1066fbe9e7218 os:macos-latest]) (push) Has been cancelled
test / test-tool-install (macos-14) (push) Has been cancelled
test / test-python-version (windows-latest) (push) Has been cancelled
test / test-activate-environment (macos-latest) (push) Has been cancelled
test / test-activate-environment (windows-latest) (push) Has been cancelled
test / test-setup-cache (auto, selfhosted-ubuntu-arm64) (push) Has been cancelled
test / test-setup-cache (auto, windows-latest) (push) Has been cancelled
test / test-setup-cache (false, selfhosted-ubuntu-arm64) (push) Has been cancelled
test / test-setup-cache (false, windows-latest) (push) Has been cancelled
test / test-setup-cache (true, selfhosted-ubuntu-arm64) (push) Has been cancelled
test / test-setup-cache (true, windows-latest) (push) Has been cancelled
test / test-cache-local (map[expected-cache-dir:/home/ubuntu/.cache/uv os:selfhosted-ubuntu-arm64]) (push) Has been cancelled
test / test-cache-local (map[expected-cache-dir:D:\a\_temp\setup-uv-cache os:windows-latest]) (push) Has been cancelled
test / test-setup-cache-local (push) Has been cancelled
test / test-tilde-expansion-cache-local-path (push) Has been cancelled
test / test-tilde-expansion-cache-dependency-glob (push) Has been cancelled
test / test-python-install-dir (map[expected-python-dir:/home/ubuntu/.local/share/uv/python os:selfhosted-ubuntu-arm64]) (push) Has been cancelled
test / test-restore-cache (false, selfhosted-ubuntu-arm64) (push) Has been cancelled
test / test-restore-cache (auto, selfhosted-ubuntu-arm64) (push) Has been cancelled
test / cleanup-tilde-expansion-tests (push) Has been cancelled
test / all-tests-passed (push) Has been cancelled
test / test-python-install-dir (map[expected-python-dir:D:\a\_temp\uv-python-dir os:windows-latest]) (push) Has been cancelled
test / test-restore-cache (false, ubuntu-latest) (push) Has been cancelled
test / test-restore-cache (false, windows-latest) (push) Has been cancelled
test / test-restore-cache (true, selfhosted-ubuntu-arm64) (push) Has been cancelled
test / test-restore-cache (true, ubuntu-latest) (push) Has been cancelled
test / test-restore-cache (true, windows-latest) (push) Has been cancelled
test / test-restore-cache (auto, ubuntu-latest) (push) Has been cancelled
test / test-restore-cache (auto, windows-latest) (push) Has been cancelled
test / test-restore-cache-local (push) Has been cancelled
Update known versions / build (push) Has been cancelled

Adds a new `resolution-strategy` input that allows users to choose
between installing the highest (default) or lowest compatible version
when resolving version ranges.
This commit is contained in:
Copilot
2025-10-11 21:02:04 +02:00
committed by GitHub
parent a5129e99f4
commit 9c6b5e9fb5
8 changed files with 136 additions and 11 deletions

View File

@@ -111,15 +111,25 @@ jobs:
expected-version: "0.3.5" expected-version: "0.3.5"
- version-input: ">=0.4.25,<0.5" - version-input: ">=0.4.25,<0.5"
expected-version: "0.4.30" 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: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
persist-credentials: false 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 id: setup-uv
uses: ./ uses: ./
with: with:
version: ${{ matrix.input.version-input }} version: ${{ matrix.input.version-input }}
resolution-strategy: ${{ matrix.input.resolution-strategy || 'highest' }}
- name: Correct version gets installed - name: Correct version gets installed
run: | run: |
if [ "$(uv --version)" != "uv ${{ matrix.input.expected-version }}" ]; then if [ "$(uv --version)" != "uv ${{ matrix.input.expected-version }}" ]; then

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

@@ -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'.`,
);
}