From 27debe0b588f059b0c11c9dfa2d724e6211b648d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Oct 2025 17:19:50 +0000 Subject: [PATCH 1/6] Initial plan From 84643229bb43313b4e9da7a71ff640d45da72877 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Oct 2025 17:30:01 +0000 Subject: [PATCH 2/6] Add resolution-strategy input with highest/lowest options Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com> --- README.md | 20 +++++++++ .../download/resolution-strategy.test.ts | 24 ++++++++++ __tests__/utils/resolution-strategy.test.ts | 45 +++++++++++++++++++ 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 ++++++ 9 files changed, 194 insertions(+), 10 deletions(-) create mode 100644 __tests__/download/resolution-strategy.test.ts create mode 100644 __tests__/utils/resolution-strategy.test.ts diff --git a/README.md b/README.md index b9f642ae..caa0900e 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/__tests__/download/resolution-strategy.test.ts b/__tests__/download/resolution-strategy.test.ts new file mode 100644 index 00000000..ea11cece --- /dev/null +++ b/__tests__/download/resolution-strategy.test.ts @@ -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); + } + }); +}); diff --git a/__tests__/utils/resolution-strategy.test.ts b/__tests__/utils/resolution-strategy.test.ts new file mode 100644 index 00000000..77155202 --- /dev/null +++ b/__tests__/utils/resolution-strategy.test.ts @@ -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 = {}; + +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'.", + ); + }); +}); diff --git a/action.yml b/action.yml index c29a89dd..1e05946f 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 d5a97997..0da895c2 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 79be815a..e2845520 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 118907f6..fa86ebea 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 fb9c4196..03b2434d 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 c9d22d99..7011c9bb 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'.`, + ); +} From 05d3e5046d8a346d4474a0a387c818c8904c90c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Oct 2025 17:33:30 +0000 Subject: [PATCH 3/6] Add comprehensive tests and fix linting issues for resolution-strategy Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com> --- .../resolution-strategy-logic.test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 __tests__/integration/resolution-strategy-logic.test.ts diff --git a/__tests__/integration/resolution-strategy-logic.test.ts b/__tests__/integration/resolution-strategy-logic.test.ts new file mode 100644 index 00000000..00abe28e --- /dev/null +++ b/__tests__/integration/resolution-strategy-logic.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "@jest/globals"; + +describe("resolution strategy integration test", () => { + it("should choose max satisfying version for highest strategy", () => { + // Test the logic that chooses between max/min satisfying + const versions = ["0.1.0", "0.2.0", "0.3.0"]; + + // Simulate the logic from our implementation + const strategyLogic = (strategy: "highest" | "lowest") => { + // This simulates what semver.minSatisfying and semver.maxSatisfying would return + if (strategy === "lowest") { + return versions.find((v) => v >= "0.1.0"); // First match (lowest) + } else { + return versions.filter((v) => v >= "0.1.0").pop(); // Last match (highest) + } + }; + + expect(strategyLogic("highest")).toBe("0.3.0"); + expect(strategyLogic("lowest")).toBe("0.1.0"); + }); + + it("should validate resolution strategy values correctly", () => { + const getResolutionStrategy = (input: string): "highest" | "lowest" => { + if (input === "lowest") { + return "lowest"; + } + if (input === "highest" || input === "") { + return "highest"; + } + throw new Error( + `Invalid resolution-strategy: ${input}. Must be 'highest' or 'lowest'.`, + ); + }; + + expect(getResolutionStrategy("")).toBe("highest"); + expect(getResolutionStrategy("highest")).toBe("highest"); + expect(getResolutionStrategy("lowest")).toBe("lowest"); + + expect(() => getResolutionStrategy("invalid")).toThrow( + "Invalid resolution-strategy: invalid. Must be 'highest' or 'lowest'.", + ); + }); +}); From e421cf46a9527b5095da8935e9595c769db4fa0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Oct 2025 17:34:31 +0000 Subject: [PATCH 4/6] Add resolution-strategy input to support oldest compatible version selection Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com> --- RESOLUTION_STRATEGY_DEMO.md | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 RESOLUTION_STRATEGY_DEMO.md diff --git a/RESOLUTION_STRATEGY_DEMO.md b/RESOLUTION_STRATEGY_DEMO.md new file mode 100644 index 00000000..71037797 --- /dev/null +++ b/RESOLUTION_STRATEGY_DEMO.md @@ -0,0 +1,43 @@ +# Resolution Strategy Demo + +This file demonstrates the new `resolution-strategy` input. + +## Default behavior (highest strategy) +```yaml +- name: Install highest compatible uv version + uses: astral-sh/setup-uv@v6 + with: + version: ">=0.4.0" + # resolution-strategy: "highest" is the default +``` + +## Lowest strategy for testing compatibility +```yaml +- name: Install lowest compatible uv version + uses: astral-sh/setup-uv@v6 + with: + version: ">=0.4.0" + resolution-strategy: "lowest" +``` + +## Use case: Testing with matrix of strategies +```yaml +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + resolution-strategy: ["highest", "lowest"] + steps: + - uses: actions/checkout@v5 + - name: Install uv with ${{ matrix.resolution-strategy }} strategy + uses: astral-sh/setup-uv@v6 + with: + version: ">=0.4.0" + resolution-strategy: ${{ matrix.resolution-strategy }} + cache-suffix: ${{ matrix.resolution-strategy }} + - name: Test with strategy + run: | + echo "Testing with $(uv --version)" + uv run --frozen pytest +``` \ No newline at end of file From a2d9fd60e7ff565ef0c9a6781f467aceeaeef798 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Oct 2025 18:31:37 +0000 Subject: [PATCH 5/6] Remove unit tests and extend test-specific-version with resolution-strategy matrix Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com> --- .github/workflows/test.yml | 12 ++++- RESOLUTION_STRATEGY_DEMO.md | 43 ------------------ .../download/resolution-strategy.test.ts | 24 ---------- .../resolution-strategy-logic.test.ts | 43 ------------------ __tests__/utils/resolution-strategy.test.ts | 45 ------------------- 5 files changed, 11 insertions(+), 156 deletions(-) delete mode 100644 RESOLUTION_STRATEGY_DEMO.md delete mode 100644 __tests__/download/resolution-strategy.test.ts delete mode 100644 __tests__/integration/resolution-strategy-logic.test.ts delete mode 100644 __tests__/utils/resolution-strategy.test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 93329f4e..2cf3c293 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,<0.2" + expected-version: "0.1.19" + 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/RESOLUTION_STRATEGY_DEMO.md b/RESOLUTION_STRATEGY_DEMO.md deleted file mode 100644 index 71037797..00000000 --- a/RESOLUTION_STRATEGY_DEMO.md +++ /dev/null @@ -1,43 +0,0 @@ -# Resolution Strategy Demo - -This file demonstrates the new `resolution-strategy` input. - -## Default behavior (highest strategy) -```yaml -- name: Install highest compatible uv version - uses: astral-sh/setup-uv@v6 - with: - version: ">=0.4.0" - # resolution-strategy: "highest" is the default -``` - -## Lowest strategy for testing compatibility -```yaml -- name: Install lowest compatible uv version - uses: astral-sh/setup-uv@v6 - with: - version: ">=0.4.0" - resolution-strategy: "lowest" -``` - -## Use case: Testing with matrix of strategies -```yaml -jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - resolution-strategy: ["highest", "lowest"] - steps: - - uses: actions/checkout@v5 - - name: Install uv with ${{ matrix.resolution-strategy }} strategy - uses: astral-sh/setup-uv@v6 - with: - version: ">=0.4.0" - resolution-strategy: ${{ matrix.resolution-strategy }} - cache-suffix: ${{ matrix.resolution-strategy }} - - name: Test with strategy - run: | - echo "Testing with $(uv --version)" - uv run --frozen pytest -``` \ No newline at end of file diff --git a/__tests__/download/resolution-strategy.test.ts b/__tests__/download/resolution-strategy.test.ts deleted file mode 100644 index ea11cece..00000000 --- a/__tests__/download/resolution-strategy.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -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); - } - }); -}); diff --git a/__tests__/integration/resolution-strategy-logic.test.ts b/__tests__/integration/resolution-strategy-logic.test.ts deleted file mode 100644 index 00abe28e..00000000 --- a/__tests__/integration/resolution-strategy-logic.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, it } from "@jest/globals"; - -describe("resolution strategy integration test", () => { - it("should choose max satisfying version for highest strategy", () => { - // Test the logic that chooses between max/min satisfying - const versions = ["0.1.0", "0.2.0", "0.3.0"]; - - // Simulate the logic from our implementation - const strategyLogic = (strategy: "highest" | "lowest") => { - // This simulates what semver.minSatisfying and semver.maxSatisfying would return - if (strategy === "lowest") { - return versions.find((v) => v >= "0.1.0"); // First match (lowest) - } else { - return versions.filter((v) => v >= "0.1.0").pop(); // Last match (highest) - } - }; - - expect(strategyLogic("highest")).toBe("0.3.0"); - expect(strategyLogic("lowest")).toBe("0.1.0"); - }); - - it("should validate resolution strategy values correctly", () => { - const getResolutionStrategy = (input: string): "highest" | "lowest" => { - if (input === "lowest") { - return "lowest"; - } - if (input === "highest" || input === "") { - return "highest"; - } - throw new Error( - `Invalid resolution-strategy: ${input}. Must be 'highest' or 'lowest'.`, - ); - }; - - expect(getResolutionStrategy("")).toBe("highest"); - expect(getResolutionStrategy("highest")).toBe("highest"); - expect(getResolutionStrategy("lowest")).toBe("lowest"); - - expect(() => getResolutionStrategy("invalid")).toThrow( - "Invalid resolution-strategy: invalid. Must be 'highest' or 'lowest'.", - ); - }); -}); diff --git a/__tests__/utils/resolution-strategy.test.ts b/__tests__/utils/resolution-strategy.test.ts deleted file mode 100644 index 77155202..00000000 --- a/__tests__/utils/resolution-strategy.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -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 = {}; - -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'.", - ); - }); -}); From c6dcfa6f9945eb77ba20bc560106df539788c195 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Sat, 11 Oct 2025 20:56:36 +0200 Subject: [PATCH 6/6] Fix expected version --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2cf3c293..f91cc263 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -114,8 +114,8 @@ jobs: - version-input: ">=0.4.25,<0.5" expected-version: "0.4.25" resolution-strategy: "lowest" - - version-input: ">=0.1.0,<0.2" - expected-version: "0.1.19" + - 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"