mirror of
https://github.com/astral-sh/setup-uv.git
synced 2026-06-19 11:02:23 +00:00
3faa3174e6
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
144 lines
4.2 KiB
TypeScript
144 lines
4.2 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "@jest/globals";
|
|
import { resolveVersionRequest } from "../../src/version/version-request-resolver";
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
function createTempProject(files: Record<string, string> = {}): string {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "setup-uv-version-test-"));
|
|
tempDirs.push(dir);
|
|
|
|
for (const [relativePath, content] of Object.entries(files)) {
|
|
const filePath = path.join(dir, relativePath);
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
fs.writeFileSync(filePath, content);
|
|
}
|
|
|
|
return dir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
fs.rmSync(dir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
describe("resolveVersionRequest", () => {
|
|
it("prefers explicit input over version-file and workspace config", () => {
|
|
const workingDirectory = createTempProject({
|
|
".tool-versions": "uv 0.4.0\n",
|
|
"pyproject.toml": `[tool.uv]\nrequired-version = "==0.5.14"\n`,
|
|
"uv.toml": `required-version = "==0.5.15"\n`,
|
|
});
|
|
|
|
const request = resolveVersionRequest({
|
|
version: "==0.6.0",
|
|
versionFile: path.join(workingDirectory, ".tool-versions"),
|
|
workingDirectory,
|
|
});
|
|
|
|
expect(request).toEqual({
|
|
source: "input",
|
|
specifier: "0.6.0",
|
|
});
|
|
});
|
|
|
|
it("uses .tool-versions when it is passed via version-file", () => {
|
|
const workingDirectory = createTempProject({
|
|
".tool-versions": "uv 0.5.15\n",
|
|
});
|
|
|
|
const request = resolveVersionRequest({
|
|
versionFile: path.join(workingDirectory, ".tool-versions"),
|
|
workingDirectory,
|
|
});
|
|
|
|
expect(request).toEqual({
|
|
format: ".tool-versions",
|
|
source: "version-file",
|
|
sourcePath: path.join(workingDirectory, ".tool-versions"),
|
|
specifier: "0.5.15",
|
|
});
|
|
});
|
|
|
|
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",
|
|
});
|
|
|
|
const request = resolveVersionRequest({
|
|
versionFile: path.join(workingDirectory, "requirements.txt"),
|
|
workingDirectory,
|
|
});
|
|
|
|
expect(request).toEqual({
|
|
format: "requirements",
|
|
source: "version-file",
|
|
sourcePath: path.join(workingDirectory, "requirements.txt"),
|
|
specifier: "0.6.17",
|
|
});
|
|
});
|
|
|
|
it("prefers uv.toml over pyproject.toml during workspace discovery", () => {
|
|
const workingDirectory = createTempProject({
|
|
"pyproject.toml": `[tool.uv]\nrequired-version = "==0.5.14"\n`,
|
|
"uv.toml": `required-version = "==0.5.15"\n`,
|
|
});
|
|
|
|
const request = resolveVersionRequest({ workingDirectory });
|
|
|
|
expect(request).toEqual({
|
|
format: "uv.toml",
|
|
source: "uv.toml",
|
|
sourcePath: path.join(workingDirectory, "uv.toml"),
|
|
specifier: "0.5.15",
|
|
});
|
|
});
|
|
|
|
it("falls back to latest when no version source is found", () => {
|
|
const workingDirectory = createTempProject({});
|
|
|
|
const request = resolveVersionRequest({ workingDirectory });
|
|
|
|
expect(request).toEqual({
|
|
source: "default",
|
|
specifier: "latest",
|
|
});
|
|
});
|
|
|
|
it("throws when version-file does not resolve a version", () => {
|
|
const workingDirectory = createTempProject({
|
|
"requirements.txt": "uvicorn==0.35.0\n",
|
|
});
|
|
|
|
expect(() =>
|
|
resolveVersionRequest({
|
|
versionFile: path.join(workingDirectory, "requirements.txt"),
|
|
workingDirectory,
|
|
}),
|
|
).toThrow(
|
|
`Could not determine uv version from file: ${path.join(workingDirectory, "requirements.txt")}`,
|
|
);
|
|
});
|
|
});
|