Skip to content

Commit 9c6b5e9

Browse files
authored
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.
1 parent a5129e9 commit 9c6b5e9

File tree

8 files changed

+136
-11
lines changed

8 files changed

+136
-11
lines changed

.github/workflows/test.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,25 @@ jobs:
111111
expected-version: "0.3.5"
112112
- version-input: ">=0.4.25,<0.5"
113113
expected-version: "0.4.30"
114+
- version-input: ">=0.4.25,<0.5"
115+
expected-version: "0.4.25"
116+
resolution-strategy: "lowest"
117+
- version-input: ">=0.1,<0.2"
118+
expected-version: "0.1.45"
119+
resolution-strategy: "highest"
120+
- version-input: ">=0.1.0,<0.2"
121+
expected-version: "0.1.0"
122+
resolution-strategy: "lowest"
114123
steps:
115124
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
116125
with:
117126
persist-credentials: false
118-
- name: Install version ${{ matrix.input.version-input }}
127+
- name: Install version ${{ matrix.input.version-input }} with strategy ${{ matrix.input.resolution-strategy || 'highest' }}
119128
id: setup-uv
120129
uses: ./
121130
with:
122131
version: ${{ matrix.input.version-input }}
132+
resolution-strategy: ${{ matrix.input.resolution-strategy || 'highest' }}
123133
- name: Correct version gets installed
124134
run: |
125135
if [ "$(uv --version)" != "uv ${{ matrix.input.expected-version }}" ]; then

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Set up your GitHub Actions workflow with a specific version of [uv](https://docs
1515
- [Install the latest version](#install-the-latest-version)
1616
- [Install a specific version](#install-a-specific-version)
1717
- [Install a version by supplying a semver range or pep440 specifier](#install-a-version-by-supplying-a-semver-range-or-pep440-specifier)
18+
- [Resolution strategy](#resolution-strategy)
1819
- [Install a version defined in a requirements or config file](#install-a-version-defined-in-a-requirements-or-config-file)
1920
- [Python version](#python-version)
2021
- [Activate environment](#activate-environment)
@@ -97,6 +98,25 @@ to install the latest version that satisfies the range.
9798
version: ">=0.4.25,<0.5"
9899
```
99100

101+
### Resolution strategy
102+
103+
By default, when resolving version ranges, setup-uv will install the highest compatible version.
104+
You can change this behavior using the `resolution-strategy` input:
105+
106+
```yaml
107+
- name: Install the lowest compatible version of uv
108+
uses: astral-sh/setup-uv@v6
109+
with:
110+
version: ">=0.4.0"
111+
resolution-strategy: "lowest"
112+
```
113+
114+
The supported resolution strategies are:
115+
- `highest` (default): Install the latest version that satisfies the constraints
116+
- `lowest`: Install the oldest version that satisfies the constraints
117+
118+
This can be useful for testing compatibility with older versions of uv, similar to uv's own `--resolution-strategy` option.
119+
100120
### Install a version defined in a requirements or config file
101121

102122
You can use the `version-file` input to specify a file that contains the version of uv to install.

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ inputs:
7777
add-problem-matchers:
7878
description: "Add problem matchers."
7979
default: "true"
80+
resolution-strategy:
81+
description: "Resolution strategy to use when resolving version ranges. 'highest' uses the latest compatible version, 'lowest' uses the oldest compatible version."
82+
default: "highest"
8083
outputs:
8184
uv-version:
8285
description: "The installed uv version. Useful when using latest."

dist/save-cache/index.js

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/setup/index.js

Lines changed: 35 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/download/download-version.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as core from "@actions/core";
44
import * as tc from "@actions/tool-cache";
55
import type { Endpoints } from "@octokit/types";
66
import * as pep440 from "@renovatebot/pep440";
7+
import * as semver from "semver";
78
import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants";
89
import { Octokit } from "../utils/octokit";
910
import type { Architecture, Platform } from "../utils/platforms";
@@ -134,6 +135,7 @@ export async function resolveVersion(
134135
versionInput: string,
135136
manifestFile: string | undefined,
136137
githubToken: string,
138+
resolutionStrategy: "highest" | "lowest" = "highest",
137139
): Promise<string> {
138140
core.debug(`Resolving version: ${versionInput}`);
139141
let version: string;
@@ -164,7 +166,10 @@ export async function resolveVersion(
164166
}
165167
const availableVersions = await getAvailableVersions(githubToken);
166168
core.debug(`Available versions: ${availableVersions}`);
167-
const resolvedVersion = maxSatisfying(availableVersions, version);
169+
const resolvedVersion =
170+
resolutionStrategy === "lowest"
171+
? minSatisfying(availableVersions, version)
172+
: maxSatisfying(availableVersions, version);
168173
if (resolvedVersion === undefined) {
169174
throw new Error(`No version found for ${version}`);
170175
}
@@ -264,3 +269,24 @@ function maxSatisfying(
264269
}
265270
return undefined;
266271
}
272+
273+
function minSatisfying(
274+
versions: string[],
275+
version: string,
276+
): string | undefined {
277+
// For semver, we need to use a different approach since tc.evaluateVersions only returns max
278+
// Let's use semver directly for min satisfying
279+
const minSemver = semver.minSatisfying(versions, version);
280+
if (minSemver !== null) {
281+
core.debug(`Found a version that satisfies the semver range: ${minSemver}`);
282+
return minSemver;
283+
}
284+
const minPep440 = pep440.minSatisfying(versions, version);
285+
if (minPep440 !== null) {
286+
core.debug(
287+
`Found a version that satisfies the pep440 specifier: ${minPep440}`,
288+
);
289+
return minPep440;
290+
}
291+
return undefined;
292+
}

src/setup-uv.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
manifestFile,
2222
pythonDir,
2323
pythonVersion,
24+
resolutionStrategy,
2425
toolBinDir,
2526
toolDir,
2627
versionFile as versionFileInput,
@@ -120,7 +121,12 @@ async function determineVersion(
120121
manifestFile: string | undefined,
121122
): Promise<string> {
122123
if (versionInput !== "") {
123-
return await resolveVersion(versionInput, manifestFile, githubToken);
124+
return await resolveVersion(
125+
versionInput,
126+
manifestFile,
127+
githubToken,
128+
resolutionStrategy,
129+
);
124130
}
125131
if (versionFileInput !== "") {
126132
const versionFromFile = getUvVersionFromFile(versionFileInput);
@@ -129,7 +135,12 @@ async function determineVersion(
129135
`Could not determine uv version from file: ${versionFileInput}`,
130136
);
131137
}
132-
return await resolveVersion(versionFromFile, manifestFile, githubToken);
138+
return await resolveVersion(
139+
versionFromFile,
140+
manifestFile,
141+
githubToken,
142+
resolutionStrategy,
143+
);
133144
}
134145
const versionFromUvToml = getUvVersionFromFile(
135146
`${workingDirectory}${path.sep}uv.toml`,
@@ -146,6 +157,7 @@ async function determineVersion(
146157
versionFromUvToml || versionFromPyproject || "latest",
147158
manifestFile,
148159
githubToken,
160+
resolutionStrategy,
149161
);
150162
}
151163

src/utils/inputs.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const githubToken = core.getInput("github-token");
2727
export const manifestFile = getManifestFile();
2828
export const addProblemMatchers =
2929
core.getInput("add-problem-matchers") === "true";
30+
export const resolutionStrategy = getResolutionStrategy();
3031

3132
function getVersionFile(): string {
3233
const versionFileInput = core.getInput("version-file");
@@ -186,3 +187,16 @@ function getManifestFile(): string | undefined {
186187
}
187188
return undefined;
188189
}
190+
191+
function getResolutionStrategy(): "highest" | "lowest" {
192+
const resolutionStrategyInput = core.getInput("resolution-strategy");
193+
if (resolutionStrategyInput === "lowest") {
194+
return "lowest";
195+
}
196+
if (resolutionStrategyInput === "highest" || resolutionStrategyInput === "") {
197+
return "highest";
198+
}
199+
throw new Error(
200+
`Invalid resolution-strategy: ${resolutionStrategyInput}. Must be 'highest' or 'lowest'.`,
201+
);
202+
}

0 commit comments

Comments
 (0)