refactor version resolving (#353)

This commit is contained in:
Kevin Stillhammer
2026-04-12 13:44:40 +02:00
committed by GitHub
parent 9b8caf6c41
commit 0ce1b0bf8b
15 changed files with 1210 additions and 551 deletions
+3 -1
View File
@@ -47,15 +47,17 @@ const mockCopyFile = jest.fn();
const mockReaddir = jest.fn();
jest.unstable_mockModule("node:fs", () => ({
default: {},
promises: {
copyFile: mockCopyFile,
readdir: mockReaddir,
},
}));
const { downloadVersion, resolveVersion, rewriteToMirror } = await import(
const { downloadVersion, rewriteToMirror } = await import(
"../../src/download/download-version"
);
const { resolveVersion } = await import("../../src/version/resolve");
describe("download-version", () => {
beforeEach(() => {
-193
View File
@@ -1,193 +0,0 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
const info = jest.fn();
const warning = jest.fn();
jest.unstable_mockModule("@actions/core", () => ({
info,
warning,
}));
const { findRuffVersionInSpec } = await import("../../src/utils/pyproject");
describe("findRuffVersionInSpec", () => {
beforeEach(() => {
info.mockReset();
warning.mockReset();
});
describe("ruff dependency strings", () => {
it("should extract version from 'ruff==0.9.3'", () => {
const result = findRuffVersionInSpec("ruff==0.9.3");
expect(result).toBe("0.9.3");
expect(info).toHaveBeenCalledWith(
"Found ruff version in requirements file: 0.9.3",
);
expect(warning).not.toHaveBeenCalled();
});
it("should extract version from 'ruff>=0.14'", () => {
const result = findRuffVersionInSpec("ruff>=0.14");
expect(result).toBe(">=0.14");
expect(warning).not.toHaveBeenCalled();
});
it("should extract version from 'ruff ~=1.0.0'", () => {
const result = findRuffVersionInSpec("ruff ~=1.0.0");
expect(result).toBe("~=1.0.0");
expect(warning).not.toHaveBeenCalled();
});
it("should extract version from 'ruff>=0.14,<1.0'", () => {
const result = findRuffVersionInSpec("ruff>=0.14,<1.0");
expect(result).toBe(">=0.14,<1.0");
expect(warning).not.toHaveBeenCalled();
});
it("should extract version from 'ruff>=0.14,<2.0,!=1.5.0'", () => {
const result = findRuffVersionInSpec("ruff>=0.14,<2.0,!=1.5.0");
expect(result).toBe(">=0.14,<2.0,!=1.5.0");
expect(warning).not.toHaveBeenCalled();
});
it("should return undefined for non-ruff dependency 'another-dep 0.1.6'", () => {
const result = findRuffVersionInSpec("another-dep 0.1.6");
expect(result).toBeUndefined();
expect(info).not.toHaveBeenCalled();
expect(warning).not.toHaveBeenCalled();
});
it("should return undefined for non-ruff dependency 'another-dep==0.1.6'", () => {
const result = findRuffVersionInSpec("another-dep==0.1.6");
expect(result).toBeUndefined();
expect(info).not.toHaveBeenCalled();
expect(warning).not.toHaveBeenCalled();
});
it("should strip trailing backslash", () => {
const result = findRuffVersionInSpec("ruff==0.9.3 \\");
expect(result).toBe("0.9.3");
expect(info).toHaveBeenCalledWith(
"Found ruff version in requirements file: 0.9.3",
);
expect(warning).not.toHaveBeenCalled();
});
it("should strip trailing backslash with whitespace", () => {
const result = findRuffVersionInSpec(" ruff==0.9.3 \\ ");
expect(result).toBe("0.9.3");
expect(warning).not.toHaveBeenCalled();
});
});
describe("environment markers", () => {
it("should strip python_version environment marker", () => {
const result = findRuffVersionInSpec(
'ruff>=0.14 ; python_version >= "3.11"',
);
expect(result).toBe(">=0.14");
expect(info).toHaveBeenCalledWith(
"Found ruff version in requirements file: >=0.14",
);
expect(warning).toHaveBeenCalledWith(
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
);
});
it("should strip sys_platform environment marker", () => {
const result = findRuffVersionInSpec(
"ruff==0.9.3 ; sys_platform == 'linux'",
);
expect(result).toBe("0.9.3");
expect(warning).toHaveBeenCalledWith(
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
);
});
it("should strip multiple environment markers", () => {
const result = findRuffVersionInSpec(
'ruff>=0.14 ; python_version >= "3.11" and sys_platform == "linux"',
);
expect(result).toBe(">=0.14");
expect(warning).toHaveBeenCalledWith(
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
);
});
it("should handle environment markers with multiple constraints", () => {
const result = findRuffVersionInSpec(
'ruff>=0.14,<1.0 ; python_version >= "3.11"',
);
expect(result).toBe(">=0.14,<1.0");
expect(warning).toHaveBeenCalledWith(
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
);
});
});
describe("edge cases", () => {
it("should handle whitespace", () => {
const result = findRuffVersionInSpec(" ruff >=0.14 ");
expect(result).toBe(">=0.14");
expect(warning).not.toHaveBeenCalled();
});
it("should handle whitespace with environment markers", () => {
const result = findRuffVersionInSpec(
" ruff >=0.14 ; python_version >= '3.11' ",
);
expect(result).toBe(">=0.14");
expect(warning).toHaveBeenCalled();
});
it("should return undefined for empty string", () => {
const result = findRuffVersionInSpec("");
expect(result).toBeUndefined();
expect(info).not.toHaveBeenCalled();
expect(warning).not.toHaveBeenCalled();
});
it("should return undefined for whitespace only", () => {
const result = findRuffVersionInSpec(" ");
expect(result).toBeUndefined();
expect(info).not.toHaveBeenCalled();
expect(warning).not.toHaveBeenCalled();
});
it("should return undefined for just semicolon", () => {
const result = findRuffVersionInSpec(";");
expect(result).toBeUndefined();
expect(info).not.toHaveBeenCalled();
expect(warning).not.toHaveBeenCalled();
});
it("should handle exact example from issue #256", () => {
const result = findRuffVersionInSpec(
'ruff>=0.14 ; python_version >= "3.11"',
);
expect(result).toBe(">=0.14");
expect(info).toHaveBeenCalledWith(
"Found ruff version in requirements file: >=0.14",
);
expect(warning).toHaveBeenCalledWith(
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
);
});
it("should handle single-quoted environment markers", () => {
const result = findRuffVersionInSpec(
"ruff>=0.14 ; python_version >= '3.11'",
);
expect(result).toBe(">=0.14");
expect(warning).toHaveBeenCalled();
});
it("should handle double-quoted environment markers", () => {
const result = findRuffVersionInSpec(
'ruff>=0.14 ; python_version >= "3.11"',
);
expect(result).toBe(">=0.14");
expect(warning).toHaveBeenCalled();
});
});
});
+128
View File
@@ -0,0 +1,128 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
const info = jest.fn();
const warning = jest.fn();
jest.unstable_mockModule("@actions/core", () => ({
debug: jest.fn(),
info,
warning,
}));
const { findRuffVersionInSpec, getRuffVersionFromFile } = await import(
"../../src/version/file-parser"
);
describe("file-parser", () => {
beforeEach(() => {
info.mockReset();
warning.mockReset();
});
describe("findRuffVersionInSpec", () => {
it("extracts version from 'ruff==0.9.3'", () => {
const result = findRuffVersionInSpec("ruff==0.9.3");
expect(result).toBe("0.9.3");
expect(info).toHaveBeenCalledWith(
"Found ruff version in requirements file: 0.9.3",
);
expect(warning).not.toHaveBeenCalled();
});
it("extracts version from 'ruff>=0.14'", () => {
const result = findRuffVersionInSpec("ruff>=0.14");
expect(result).toBe(">=0.14");
expect(warning).not.toHaveBeenCalled();
});
it("extracts version from 'ruff ~=1.0.0'", () => {
const result = findRuffVersionInSpec("ruff ~=1.0.0");
expect(result).toBe("~=1.0.0");
expect(warning).not.toHaveBeenCalled();
});
it("extracts version from 'ruff>=0.14,<1.0'", () => {
const result = findRuffVersionInSpec("ruff>=0.14,<1.0");
expect(result).toBe(">=0.14,<1.0");
expect(warning).not.toHaveBeenCalled();
});
it("extracts version from 'ruff>=0.14,<2.0,!=1.5.0'", () => {
const result = findRuffVersionInSpec("ruff>=0.14,<2.0,!=1.5.0");
expect(result).toBe(">=0.14,<2.0,!=1.5.0");
expect(warning).not.toHaveBeenCalled();
});
it("returns undefined for non-ruff dependencies", () => {
const result = findRuffVersionInSpec("another-dep==0.1.6");
expect(result).toBeUndefined();
expect(info).not.toHaveBeenCalled();
expect(warning).not.toHaveBeenCalled();
});
it("strips trailing backslash", () => {
const result = findRuffVersionInSpec("ruff==0.9.3 \\");
expect(result).toBe("0.9.3");
expect(info).toHaveBeenCalledWith(
"Found ruff version in requirements file: 0.9.3",
);
expect(warning).not.toHaveBeenCalled();
});
it("strips environment markers and warns", () => {
const result = findRuffVersionInSpec(
'ruff>=0.14 ; python_version >= "3.11"',
);
expect(result).toBe(">=0.14");
expect(info).toHaveBeenCalledWith(
"Found ruff version in requirements file: >=0.14",
);
expect(warning).toHaveBeenCalledWith(
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
);
});
it("handles whitespace", () => {
const result = findRuffVersionInSpec(" ruff >=0.14 ");
expect(result).toBe(">=0.14");
expect(warning).not.toHaveBeenCalled();
});
it("returns undefined for empty strings", () => {
const result = findRuffVersionInSpec("");
expect(result).toBeUndefined();
expect(info).not.toHaveBeenCalled();
expect(warning).not.toHaveBeenCalled();
});
});
describe("getRuffVersionFromFile", () => {
it("reads the version from requirements.txt", () => {
const result = getRuffVersionFromFile(
"__tests__/fixtures/requirements.txt",
);
expect(result).toBe("0.9.0");
});
it("reads the version from requirements files with hashes", () => {
const result = getRuffVersionFromFile(
"__tests__/fixtures/requirements-with-hash.txt",
);
expect(result).toBe("0.9.0");
});
it("reads the version from pyproject.toml dependencies", () => {
const result = getRuffVersionFromFile(
"__tests__/fixtures/pyproject.toml",
);
expect(result).toBe("0.9.3");
});
it("reads the version from Poetry dependencies", () => {
const result = getRuffVersionFromFile(
"__tests__/fixtures/pyproject-dependency-poetry-project/pyproject.toml",
);
expect(result).toBe("~0.8.2");
});
});
});
@@ -0,0 +1,164 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
afterEach,
beforeEach,
describe,
expect,
it,
jest,
} from "@jest/globals";
const debug = jest.fn();
const info = jest.fn();
const warning = jest.fn();
jest.unstable_mockModule("@actions/core", () => ({
debug,
info,
warning,
}));
const { resolveVersionRequest } = await import(
"../../src/version/version-request-resolver"
);
const tempDirs: string[] = [];
function createTempProject(files: Record<string, string> = {}): string {
const dir = fs.mkdtempSync(
path.join(os.tmpdir(), "ruff-action-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", () => {
beforeEach(() => {
debug.mockReset();
info.mockReset();
warning.mockReset();
});
it("prefers explicit input over workspace discovery", () => {
const workspaceRoot = createTempProject({
"pyproject.toml": `[project]\ndependencies = ["ruff==0.5.14"]\n`,
});
const request = resolveVersionRequest({
sourceDirectory: workspaceRoot,
version: "==0.6.0",
workspaceRoot,
});
expect(request).toEqual({
source: "input",
specifier: "0.6.0",
});
});
it("uses requirements.txt when it is passed via version-file", () => {
const workspaceRoot = createTempProject({
"requirements.txt": "ruff==0.6.17\nruff-api==0.1.0\n",
});
const request = resolveVersionRequest({
sourceDirectory: workspaceRoot,
versionFile: path.join(workspaceRoot, "requirements.txt"),
workspaceRoot,
});
expect(request).toEqual({
format: "requirements",
source: "version-file",
sourcePath: path.join(workspaceRoot, "requirements.txt"),
specifier: "0.6.17",
});
});
it("warns and falls back to latest when version-file does not resolve a version", () => {
const workspaceRoot = createTempProject({
"requirements.txt": "ruff-api==0.1.0\n",
});
const request = resolveVersionRequest({
sourceDirectory: workspaceRoot,
versionFile: path.join(workspaceRoot, "requirements.txt"),
workspaceRoot,
});
expect(request).toEqual({
source: "default",
specifier: "latest",
});
expect(warning).toHaveBeenCalledWith(
`Could not parse version from ${path.join(workspaceRoot, "requirements.txt")}. Using latest version.`,
);
});
it("discovers pyproject.toml by searching upward from src", () => {
const workspaceRoot = createTempProject({
"pyproject.toml": `[project]\ndependencies = ["ruff==0.10.0"]\n`,
"subproject/nested/example.py": 'print("hello")\n',
});
const sourceDirectory = path.join(workspaceRoot, "subproject", "nested");
const request = resolveVersionRequest({
sourceDirectory,
workspaceRoot,
});
expect(request).toEqual({
format: "pyproject.toml",
source: "pyproject.toml",
sourcePath: path.join(workspaceRoot, "pyproject.toml"),
specifier: "0.10.0",
});
});
it("falls back to latest when no workspace version source is found", () => {
const workspaceRoot = createTempProject({
"subproject/example.py": 'print("hello")\n',
});
const request = resolveVersionRequest({
sourceDirectory: path.join(workspaceRoot, "subproject"),
workspaceRoot,
});
expect(request).toEqual({
source: "default",
specifier: "latest",
});
expect(info).toHaveBeenCalledWith(
"Could not find pyproject.toml. Using latest version.",
);
});
it("throws when both version and version-file are specified", () => {
const workspaceRoot = createTempProject();
expect(() =>
resolveVersionRequest({
sourceDirectory: workspaceRoot,
version: "0.6.0",
versionFile: path.join(workspaceRoot, "requirements.txt"),
workspaceRoot,
}),
).toThrow("It is not allowed to specify both version and version-file");
});
});