Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
13 changes: 12 additions & 1 deletion dist/save-cache/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 35 additions & 6 deletions dist/setup/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 27 additions & 1 deletion src/download/download-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -134,6 +135,7 @@ export async function resolveVersion(
versionInput: string,
manifestFile: string | undefined,
githubToken: string,
resolutionStrategy: "highest" | "lowest" = "highest",
): Promise<string> {
core.debug(`Resolving version: ${versionInput}`);
let version: string;
Expand Down Expand Up @@ -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}`);
}
Expand Down Expand Up @@ -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;
}
16 changes: 14 additions & 2 deletions src/setup-uv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
manifestFile,
pythonDir,
pythonVersion,
resolutionStrategy,
toolBinDir,
toolDir,
versionFile as versionFileInput,
Expand Down Expand Up @@ -120,7 +121,12 @@ async function determineVersion(
manifestFile: string | undefined,
): Promise<string> {
if (versionInput !== "") {
return await resolveVersion(versionInput, manifestFile, githubToken);
return await resolveVersion(
versionInput,
manifestFile,
githubToken,
resolutionStrategy,
);
}
if (versionFileInput !== "") {
const versionFromFile = getUvVersionFromFile(versionFileInput);
Expand All @@ -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`,
Expand All @@ -146,6 +157,7 @@ async function determineVersion(
versionFromUvToml || versionFromPyproject || "latest",
manifestFile,
githubToken,
resolutionStrategy,
);
}

Expand Down
14 changes: 14 additions & 0 deletions src/utils/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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'.`,
);
}
Loading