From 65ddd5df652ff7faef342f22fa9a39931012ad88 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:03:04 +0000 Subject: [PATCH 1/7] feat: support uv.lock as a version-file source (#918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `uv.lock` as a supported `version-file` source. When `uv` is locked as a dependency in `uv.lock`, the action now installs the exact pinned version, closing the gap reported in #682. This is useful for deterministic CI: the same uv version is used until the lockfile is updated, which avoids "CI worked yesterday, fails today" drift and reduces supply-chain exposure from auto-installing the latest release. The implementation mirrors the existing `version-file` parsers — a new `uv.lock` entry in the parser registry reads the `[[package]]` whose `name = "uv"` and returns its locked `version`. Scoped to explicit `version-file: uv.lock`; workspace auto-detection is left as a possible follow-up to avoid precedence ambiguity with `uv.toml` / `pyproject.toml`. Validation (local, Node 23; dist build is esbuild-deterministic): - `npm run all` → build clean, biome clean, package clean, jest 77/77 - New tests: 3 unit (`uv-lock-file.test.ts`) + 1 integration — exact pin resolves through the full pipeline (`uv.lock` → `0.8.17`) - dist rebuilt + committed (single bundle, no spurious churn) related: #682 --- __tests__/version/uv-lock-file.test.ts | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/__tests__/version/uv-lock-file.test.ts b/__tests__/version/uv-lock-file.test.ts index 98e48d4..0261b7b 100644 --- a/__tests__/version/uv-lock-file.test.ts +++ b/__tests__/version/uv-lock-file.test.ts @@ -34,3 +34,39 @@ version = "4.6.0" expect(getUvVersionFromUvLockContent("version = 1\n")).toBeUndefined(); }); }); +import { describe, expect, it } from "@jest/globals"; +import { getUvVersionFromUvLockContent } from "../../src/version/uv-lock-file"; + +const UV_LOCK = `version = 1 +requires-python = ">=3.12" + +[[package]] +name = "anyio" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } + +[[package]] +name = "uv" +version = "0.8.17" +source = { registry = "https://pypi.org/simple" } +`; + +describe("getUvVersionFromUvLockContent", () => { + it("returns the exact uv version locked in uv.lock", () => { + expect(getUvVersionFromUvLockContent(UV_LOCK)).toBe("0.8.17"); + }); + + it("returns undefined when uv is not a locked package", () => { + const content = `version = 1 + +[[package]] +name = "anyio" +version = "4.6.0" +`; + expect(getUvVersionFromUvLockContent(content)).toBeUndefined(); + }); + + it("returns undefined when there are no packages", () => { + expect(getUvVersionFromUvLockContent("version = 1\n")).toBeUndefined(); + }); +}); From 87b892e860dcffc6a8ffa57cea731bbd1e258fff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:03:05 +0000 Subject: [PATCH 2/7] feat: support uv.lock as a version-file source (#918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `uv.lock` as a supported `version-file` source. When `uv` is locked as a dependency in `uv.lock`, the action now installs the exact pinned version, closing the gap reported in #682. This is useful for deterministic CI: the same uv version is used until the lockfile is updated, which avoids "CI worked yesterday, fails today" drift and reduces supply-chain exposure from auto-installing the latest release. The implementation mirrors the existing `version-file` parsers — a new `uv.lock` entry in the parser registry reads the `[[package]]` whose `name = "uv"` and returns its locked `version`. Scoped to explicit `version-file: uv.lock`; workspace auto-detection is left as a possible follow-up to avoid precedence ambiguity with `uv.toml` / `pyproject.toml`. Validation (local, Node 23; dist build is esbuild-deterministic): - `npm run all` → build clean, biome clean, package clean, jest 77/77 - New tests: 3 unit (`uv-lock-file.test.ts`) + 1 integration — exact pin resolves through the full pipeline (`uv.lock` → `0.8.17`) - dist rebuilt + committed (single bundle, no spurious churn) related: #682 --- .../version/version-request-resolver.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/__tests__/version/version-request-resolver.test.ts b/__tests__/version/version-request-resolver.test.ts index 754f29f..c7ecf84 100644 --- a/__tests__/version/version-request-resolver.test.ts +++ b/__tests__/version/version-request-resolver.test.ts @@ -81,6 +81,24 @@ describe("resolveVersionRequest", () => { }); }); + it("uses the exact uv version locked in uv.lock when it is passed via version-file", () => { + const workingDirectory = createTempProject({ + "uv.lock": `version = 1\n\n[[package]]\nname = "uv"\nversion = "0.8.17"\nsource = { registry = "https://pypi.org/simple" }\n`, + }); + + const request = resolveVersionRequest({ + versionFile: path.join(workingDirectory, "uv.lock"), + workingDirectory, + }); + + expect(request).toEqual({ + format: "uv.lock", + source: "version-file", + sourcePath: path.join(workingDirectory, "uv.lock"), + specifier: "0.8.17", + }); + }); + it("uses requirements.txt when it is passed via version-file", () => { const workingDirectory = createTempProject({ "requirements.txt": "uv==0.6.17\nuvicorn==0.35.0\n", From b2eca2e48fc045c55f284a30da14a53377e1e99a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:03:06 +0000 Subject: [PATCH 3/7] feat: support uv.lock as a version-file source (#918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `uv.lock` as a supported `version-file` source. When `uv` is locked as a dependency in `uv.lock`, the action now installs the exact pinned version, closing the gap reported in #682. This is useful for deterministic CI: the same uv version is used until the lockfile is updated, which avoids "CI worked yesterday, fails today" drift and reduces supply-chain exposure from auto-installing the latest release. The implementation mirrors the existing `version-file` parsers — a new `uv.lock` entry in the parser registry reads the `[[package]]` whose `name = "uv"` and returns its locked `version`. Scoped to explicit `version-file: uv.lock`; workspace auto-detection is left as a possible follow-up to avoid precedence ambiguity with `uv.toml` / `pyproject.toml`. Validation (local, Node 23; dist build is esbuild-deterministic): - `npm run all` → build clean, biome clean, package clean, jest 77/77 - New tests: 3 unit (`uv-lock-file.test.ts`) + 1 integration — exact pin resolves through the full pipeline (`uv.lock` → `0.8.17`) - dist rebuilt + committed (single bundle, no spurious churn) related: #682 --- docs/advanced-version-configuration.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/advanced-version-configuration.md b/docs/advanced-version-configuration.md index 12e084c..df419e0 100644 --- a/docs/advanced-version-configuration.md +++ b/docs/advanced-version-configuration.md @@ -91,3 +91,14 @@ silently picking up a newer uv until the lockfile is updated. with: version-file: "uv.lock" ``` + +If uv is locked as a dependency in your `uv.lock`, you can point `version-file` at the +lockfile to install the exact pinned version. This keeps CI runs deterministic and avoids +silently picking up a newer uv until the lockfile is updated. + +```yaml +- name: Install uv based on the version locked in uv.lock + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + version-file: "uv.lock" +``` From d930936f8a132daa08e9840141092639058c6dc8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:03:08 +0000 Subject: [PATCH 4/7] feat: support uv.lock as a version-file source (#918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `uv.lock` as a supported `version-file` source. When `uv` is locked as a dependency in `uv.lock`, the action now installs the exact pinned version, closing the gap reported in #682. This is useful for deterministic CI: the same uv version is used until the lockfile is updated, which avoids "CI worked yesterday, fails today" drift and reduces supply-chain exposure from auto-installing the latest release. The implementation mirrors the existing `version-file` parsers — a new `uv.lock` entry in the parser registry reads the `[[package]]` whose `name = "uv"` and returns its locked `version`. Scoped to explicit `version-file: uv.lock`; workspace auto-detection is left as a possible follow-up to avoid precedence ambiguity with `uv.toml` / `pyproject.toml`. Validation (local, Node 23; dist build is esbuild-deterministic): - `npm run all` → build clean, biome clean, package clean, jest 77/77 - New tests: 3 unit (`uv-lock-file.test.ts`) + 1 integration — exact pin resolves through the full pipeline (`uv.lock` → `0.8.17`) - dist rebuilt + committed (single bundle, no spurious churn) related: #682 --- src/version/uv-lock-file.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/version/uv-lock-file.ts b/src/version/uv-lock-file.ts index 5faceba..cc0e438 100644 --- a/src/version/uv-lock-file.ts +++ b/src/version/uv-lock-file.ts @@ -22,3 +22,27 @@ export function getUvVersionFromUvLockContent( const uvPackage = parsed.package?.find((pkg) => pkg.name === "uv"); return uvPackage?.version; } +import fs from "node:fs"; +import * as toml from "smol-toml"; + +interface UvLockPackage { + name?: string; + version?: string; +} + +interface UvLock { + package?: UvLockPackage[]; +} + +export function getUvVersionFromUvLock(filePath: string): string | undefined { + const fileContent = fs.readFileSync(filePath, "utf-8"); + return getUvVersionFromUvLockContent(fileContent); +} + +export function getUvVersionFromUvLockContent( + fileContent: string, +): string | undefined { + const parsed = toml.parse(fileContent) as UvLock; + const uvPackage = parsed.package?.find((pkg) => pkg.name === "uv"); + return uvPackage?.version; +} From e047776849ce485021c36485855bb880d5c55e3a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:03:12 +0000 Subject: [PATCH 5/7] Add a threat model for setup-uv (#923) This adds a threat model for `setup-uv` so security scanners can use it as a baseline in terms of what's in-, and out of scope. The TM covers credential recipients, executable and cache boundaries, and release authority. It treats checkout-selected interpreters, paths, virtual environments, symlinks, and helpers as delegated project authority unless they override an explicit workflow choice or cross an independent cache, runner, remote, or publication boundary. --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index 77568b2..edf9984 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,3 +1,8 @@ +# Security policy + +Report suspected vulnerabilities according to [Astral's security policy](https://github.com/astral-sh/.github/blob/main/SECURITY.md). + +For this repository's security boundaries and reporting criteria, see the [setup-uv threat model](docs/threat-model.md). # Security Policy ## Reporting a Vulnerability From 5aa507db8f7ef6d1ec38f4f724f9a6245a0d3d0c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:03:17 +0000 Subject: [PATCH 6/7] Fix cache keys for Python version ranges (#937) ## Summary - URL-encode the Python version component before adding it to the cache key - URL-encode the user-provided cache suffix for the same reason - Add cache key tests for Python ranges, comma-containing suffixes, and unchanged simple inputs Fixes #914 Refs: pi-session 019f3164-85e7-7817-bffd-501d89b3a1fd ## Tests - npm run all --- __tests__/cache/restore-cache.test.ts | 74 +++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/__tests__/cache/restore-cache.test.ts b/__tests__/cache/restore-cache.test.ts index ec342d3..8c48045 100644 --- a/__tests__/cache/restore-cache.test.ts +++ b/__tests__/cache/restore-cache.test.ts @@ -72,3 +72,77 @@ describe("restoreCache", () => { ); }); }); +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import { createSetupInputs } from "../helpers/setup-inputs"; + +const mockRestoreCache = jest.fn(); +const mockSaveState = jest.fn(); +const mockSetOutput = jest.fn(); + +jest.unstable_mockModule("@actions/cache", () => ({ + restoreCache: mockRestoreCache, +})); + +jest.unstable_mockModule("@actions/core", () => ({ + saveState: mockSaveState, + setOutput: mockSetOutput, +})); + +jest.unstable_mockModule("../../src/hash/hash-files", () => ({ + hashFiles: jest.fn(async () => "dependencyhash"), +})); + +jest.unstable_mockModule("../../src/utils/logging", () => ({ + info: jest.fn(), + warning: jest.fn(), +})); + +jest.unstable_mockModule("../../src/utils/platforms", () => ({ + getArch: jest.fn(() => "x86_64"), + getOSNameVersion: jest.fn(() => "ubuntu-24.04"), + getPlatform: jest.fn(async () => "unknown-linux-gnu"), +})); + +const { restoreCache } = await import("../../src/cache/restore-cache"); + +function cacheKeyOutput(): string { + const call = mockSetOutput.mock.calls.find(([name]) => name === "cache-key"); + expect(call).toBeDefined(); + return call?.[1] as string; +} + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe("restoreCache", () => { + it("encodes Python version ranges before adding them to the cache key", async () => { + await restoreCache(createSetupInputs(), ">3.10.11,<3.11"); + + const cacheKey = cacheKeyOutput(); + + expect(cacheKey).not.toContain(","); + expect(cacheKey).toContain("-%3E3.10.11%2C%3C3.11-"); + }); + + it("encodes cache suffixes before adding them to the cache key", async () => { + const inputs = createSetupInputs({ cacheSuffix: "tests-3.10,3.11" }); + + await restoreCache(inputs, "3.11"); + + const cacheKey = cacheKeyOutput(); + + expect(cacheKey).not.toContain(","); + expect(cacheKey).toContain("-tests-3.10%2C3.11"); + }); + + it("keeps cache keys unchanged for exact Python versions and simple suffixes", async () => { + const inputs = createSetupInputs({ cacheSuffix: "tests-3.11" }); + + await restoreCache(inputs, "3.11"); + + expect(cacheKeyOutput()).toBe( + "setup-uv-2-x86_64-unknown-linux-gnu-ubuntu-24.04-3.11-pruned-dependencyhash-tests-3.11", + ); + }); +}); From 16b802111556147a35aa97f708c49b4225305ec6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:03:17 +0000 Subject: [PATCH 7/7] Fix cache keys for Python version ranges (#937) ## Summary - URL-encode the Python version component before adding it to the cache key - URL-encode the user-provided cache suffix for the same reason - Add cache key tests for Python ranges, comma-containing suffixes, and unchanged simple inputs Fixes #914 Refs: pi-session 019f3164-85e7-7817-bffd-501d89b3a1fd ## Tests - npm run all --- __tests__/helpers/setup-inputs.ts | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/__tests__/helpers/setup-inputs.ts b/__tests__/helpers/setup-inputs.ts index 1ca0d0d..52d238d 100644 --- a/__tests__/helpers/setup-inputs.ts +++ b/__tests__/helpers/setup-inputs.ts @@ -34,3 +34,39 @@ export function createSetupInputs( ...overrides, }; } +import { CacheLocalSource, type SetupInputs } from "../../src/utils/inputs"; + +export function createSetupInputs( + overrides: Partial = {}, +): SetupInputs { + return { + activateEnvironment: false, + addProblemMatchers: false, + cacheDependencyGlob: "uv.lock", + cacheLocalPath: { + path: "/tmp/setup-uv-cache", + source: CacheLocalSource.Input, + }, + cachePython: false, + cacheSuffix: "", + checksum: "", + downloadFromAstralMirror: false, + enableCache: true, + githubToken: "", + ignoreEmptyWorkdir: false, + ignoreNothingToCache: false, + noProject: false, + pruneCache: true, + pythonDir: "/tmp/uv-python-dir", + pythonVersion: "", + quiet: false, + resolutionStrategy: "highest", + restoreCache: false, + saveCache: true, + venvPath: "/workspace/.venv", + version: "", + versionFile: "", + workingDirectory: "/workspace", + ...overrides, + }; +}