mirror of
https://github.com/astral-sh/ruff-action.git
synced 2026-08-08 18:27:03 +00:00
refactor version resolving (#353)
This commit is contained in:
@@ -2,7 +2,6 @@ import { promises as fs } from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
import * as pep440 from "@renovatebot/pep440";
|
||||
import * as semver from "semver";
|
||||
import {
|
||||
ASTRAL_MIRROR_PREFIX,
|
||||
@@ -12,7 +11,7 @@ import {
|
||||
} from "../utils/constants";
|
||||
import type { Architecture, Platform } from "../utils/platforms";
|
||||
import { validateChecksum } from "./checksum/checksum";
|
||||
import { getAllVersions, getArtifact, getLatestVersion } from "./manifest";
|
||||
import { getArtifact } from "./manifest";
|
||||
|
||||
export function tryGetFromToolCache(
|
||||
arch: Architecture,
|
||||
@@ -162,35 +161,6 @@ async function extractDownloadedArtifact(
|
||||
return ruffDir;
|
||||
}
|
||||
|
||||
export async function resolveVersion(
|
||||
versionInput: string,
|
||||
manifestUrl?: string,
|
||||
): Promise<string> {
|
||||
core.debug(`Resolving ${versionInput}...`);
|
||||
|
||||
const version =
|
||||
versionInput === "latest"
|
||||
? await getLatestVersion(manifestUrl)
|
||||
: versionInput;
|
||||
|
||||
if (tc.isExplicitVersion(version)) {
|
||||
core.debug(`Version ${version} is an explicit version.`);
|
||||
return version;
|
||||
}
|
||||
|
||||
const availableVersions = await getAvailableVersions(manifestUrl);
|
||||
const resolvedVersion = maxSatisfying(availableVersions, version);
|
||||
if (resolvedVersion === undefined) {
|
||||
throw new Error(`No version found for ${version}`);
|
||||
}
|
||||
core.debug(`Resolved version: ${resolvedVersion}`);
|
||||
return resolvedVersion;
|
||||
}
|
||||
|
||||
async function getAvailableVersions(manifestUrl?: string): Promise<string[]> {
|
||||
return await getAllVersions(manifestUrl);
|
||||
}
|
||||
|
||||
function getMissingArtifactMessage(
|
||||
version: string,
|
||||
arch: Architecture,
|
||||
@@ -245,22 +215,3 @@ function stripVersionPrefix(version: string): string {
|
||||
function getExtension(platform: Platform): string {
|
||||
return platform === "pc-windows-msvc" ? ".zip" : ".tar.gz";
|
||||
}
|
||||
|
||||
function maxSatisfying(
|
||||
versions: string[],
|
||||
version: string,
|
||||
): string | undefined {
|
||||
const maxSemver = tc.evaluateVersions(versions, version);
|
||||
if (maxSemver !== "") {
|
||||
core.debug(`Found a version that satisfies the semver range: ${maxSemver}`);
|
||||
return maxSemver;
|
||||
}
|
||||
const maxPep440 = pep440.maxSatisfying(versions, version);
|
||||
if (maxPep440 !== null) {
|
||||
core.debug(
|
||||
`Found a version that satisfies the pep440 specifier: ${maxPep440}`,
|
||||
);
|
||||
return maxPep440;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
+8
-41
@@ -4,7 +4,6 @@ import * as exec from "@actions/exec";
|
||||
import * as semver from "semver";
|
||||
import {
|
||||
downloadVersion,
|
||||
resolveVersion,
|
||||
tryGetFromToolCache,
|
||||
} from "./download/download-version";
|
||||
import {
|
||||
@@ -22,8 +21,7 @@ import {
|
||||
getPlatform,
|
||||
type Platform,
|
||||
} from "./utils/platforms";
|
||||
import { getRuffVersionFromRequirementsFile } from "./utils/pyproject";
|
||||
import { findPyprojectToml } from "./utils/pyproject-finder";
|
||||
import { resolveRuffVersion } from "./version/resolve";
|
||||
|
||||
async function run(): Promise<void> {
|
||||
const platform = getPlatform();
|
||||
@@ -94,44 +92,13 @@ async function setupRuff(
|
||||
}
|
||||
|
||||
async function determineVersion(): Promise<string> {
|
||||
if (versionFileInput !== "" && version !== "") {
|
||||
throw Error("It is not allowed to specify both version and version-file");
|
||||
}
|
||||
if (version !== "") {
|
||||
return await resolveVersion(version, manifestFile || undefined);
|
||||
}
|
||||
if (versionFileInput !== "") {
|
||||
const versionFromPyproject =
|
||||
getRuffVersionFromRequirementsFile(versionFileInput);
|
||||
if (versionFromPyproject === undefined) {
|
||||
core.warning(
|
||||
`Could not parse version from ${versionFileInput}. Using latest version.`,
|
||||
);
|
||||
}
|
||||
return await resolveVersion(
|
||||
versionFromPyproject || "latest",
|
||||
manifestFile || undefined,
|
||||
);
|
||||
}
|
||||
const pyProjectPath = findPyprojectToml(
|
||||
src,
|
||||
process.env.GITHUB_WORKSPACE || ".",
|
||||
);
|
||||
if (!pyProjectPath) {
|
||||
core.info(`Could not find pyproject.toml. Using latest version.`);
|
||||
return await resolveVersion("latest", manifestFile || undefined);
|
||||
}
|
||||
const versionFromPyproject =
|
||||
getRuffVersionFromRequirementsFile(pyProjectPath);
|
||||
if (versionFromPyproject === undefined) {
|
||||
core.info(
|
||||
`Could not parse version from ${pyProjectPath}. Using latest version.`,
|
||||
);
|
||||
}
|
||||
return await resolveVersion(
|
||||
versionFromPyproject || "latest",
|
||||
manifestFile || undefined,
|
||||
);
|
||||
return await resolveRuffVersion({
|
||||
manifestFile: manifestFile || undefined,
|
||||
sourceDirectory: src,
|
||||
version,
|
||||
versionFile: versionFileInput,
|
||||
workspaceRoot: process.env.GITHUB_WORKSPACE || ".",
|
||||
});
|
||||
}
|
||||
|
||||
function addRuffToPath(cachedPath: string): void {
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as core from "@actions/core";
|
||||
import * as toml from "smol-toml";
|
||||
|
||||
/**
|
||||
* Find ruff version in a dependency specification.
|
||||
* Only handles strings that start with "ruff" (e.g., "ruff==0.9.3").
|
||||
* Returns undefined for non-ruff dependencies.
|
||||
* Strips environment markers (everything after ';').
|
||||
* Strips leading '==' from exact version specifiers (PEP 440) for downstream compatibility.
|
||||
*
|
||||
* @internal This is exported for testing purposes only.
|
||||
*/
|
||||
export function findRuffVersionInSpec(spec: string): string | undefined {
|
||||
const trimmedSpec = spec.trim();
|
||||
|
||||
const fullDepMatch = trimmedSpec.match(/^ruff\s*(.+)$/);
|
||||
let versionSpec: string;
|
||||
if (fullDepMatch) {
|
||||
versionSpec = fullDepMatch[1];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Strip trailing backslash (line continuation)
|
||||
versionSpec = versionSpec.replace(/\\$/, "").trim();
|
||||
|
||||
// Strip environment markers (everything after ';')
|
||||
const match = versionSpec.match(/^([^;]+)(?:;.*)?$/);
|
||||
|
||||
if (match) {
|
||||
let version = match[1].trim();
|
||||
if (version) {
|
||||
// Strip leading '==' from exact version specifiers for compatibility with semver
|
||||
if (version.startsWith("==")) {
|
||||
version = version.slice(2);
|
||||
}
|
||||
if (trimmedSpec.includes(";")) {
|
||||
core.warning(
|
||||
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
|
||||
);
|
||||
}
|
||||
core.info(`Found ruff version in requirements file: ${version}`);
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getRuffVersionFromAllDependencies(
|
||||
allDependencies: string[],
|
||||
): string | undefined {
|
||||
return allDependencies
|
||||
.map((dep) => findRuffVersionInSpec(dep))
|
||||
.find((version) => version !== undefined);
|
||||
}
|
||||
|
||||
interface Pyproject {
|
||||
project?: {
|
||||
dependencies?: string[];
|
||||
"optional-dependencies"?: Record<string, string[]>;
|
||||
};
|
||||
"dependency-groups"?: Record<string, Array<string | object>>;
|
||||
tool?: {
|
||||
poetry?: {
|
||||
dependencies?: Record<string, string | object>;
|
||||
group?: Record<string, { dependencies: Record<string, string | object> }>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function parsePyproject(pyprojectContent: string): string | undefined {
|
||||
const pyproject: Pyproject = toml.parse(pyprojectContent);
|
||||
const dependencies: string[] = pyproject?.project?.dependencies || [];
|
||||
const optionalDependencies: string[] = Object.values(
|
||||
pyproject?.project?.["optional-dependencies"] || {},
|
||||
).flat();
|
||||
const devDependencies: string[] = Object.values(
|
||||
pyproject?.["dependency-groups"] || {},
|
||||
)
|
||||
.flat()
|
||||
.filter((item: string | object) => typeof item === "string");
|
||||
return (
|
||||
getRuffVersionFromAllDependencies(
|
||||
dependencies.concat(optionalDependencies, devDependencies),
|
||||
) || getRuffVersionFromPoetryGroups(pyproject)
|
||||
);
|
||||
}
|
||||
|
||||
function getRuffVersionFromPoetryGroups(
|
||||
pyproject: Pyproject,
|
||||
): string | undefined {
|
||||
// Special handling for Poetry until it supports PEP 735
|
||||
// See: <https://github.com/python-poetry/poetry/issues/9751>
|
||||
const poetry = pyproject?.tool?.poetry || {};
|
||||
const poetryGroups = Object.values(poetry.group || {});
|
||||
if (poetry.dependencies) {
|
||||
poetryGroups.unshift({ dependencies: poetry.dependencies });
|
||||
}
|
||||
return poetryGroups
|
||||
.flatMap((group) => Object.entries(group.dependencies))
|
||||
.map(([name, spec]) => {
|
||||
if (typeof spec === "string") {
|
||||
return findRuffVersionInSpec(`${name} ${spec}`);
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.find((version) => version !== undefined);
|
||||
}
|
||||
|
||||
export function getRuffVersionFromRequirementsFile(
|
||||
filePath: string,
|
||||
): string | undefined {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
core.warning(`Could not find file: ${filePath}`);
|
||||
return undefined;
|
||||
}
|
||||
const pyprojectContent = fs.readFileSync(filePath, "utf-8");
|
||||
if (filePath.endsWith(".txt")) {
|
||||
return getRuffVersionFromAllDependencies(pyprojectContent.split("\n"));
|
||||
}
|
||||
try {
|
||||
return parsePyproject(pyprojectContent);
|
||||
} catch (err) {
|
||||
const message = (err as Error).message;
|
||||
core.warning(`Error while parsing ${filePath}: ${message}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import fs from "node:fs";
|
||||
import * as core from "@actions/core";
|
||||
import * as toml from "smol-toml";
|
||||
import { normalizeVersionSpecifier } from "./specifier";
|
||||
import type { ParsedVersionFile, VersionFileFormat } from "./types";
|
||||
|
||||
interface VersionFileParser {
|
||||
format: VersionFileFormat;
|
||||
parse(filePath: string): string | undefined;
|
||||
supports(filePath: string): boolean;
|
||||
}
|
||||
|
||||
interface Pyproject {
|
||||
project?: {
|
||||
dependencies?: string[];
|
||||
"optional-dependencies"?: Record<string, string[]>;
|
||||
};
|
||||
"dependency-groups"?: Record<string, Array<string | object>>;
|
||||
tool?: {
|
||||
poetry?: {
|
||||
dependencies?: Record<string, string | object>;
|
||||
group?: Record<string, { dependencies: Record<string, string | object> }>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const VERSION_FILE_PARSERS: VersionFileParser[] = [
|
||||
{
|
||||
format: "pyproject.toml",
|
||||
parse: (filePath) => {
|
||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||
return getRuffVersionFromPyprojectContent(fileContent);
|
||||
},
|
||||
supports: (filePath) => filePath.endsWith("pyproject.toml"),
|
||||
},
|
||||
{
|
||||
format: "requirements",
|
||||
parse: (filePath) => {
|
||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||
return getRuffVersionFromRequirementsText(fileContent);
|
||||
},
|
||||
supports: (filePath) => filePath.endsWith(".txt"),
|
||||
},
|
||||
];
|
||||
|
||||
export function getParsedVersionFile(
|
||||
filePath: string,
|
||||
): ParsedVersionFile | undefined {
|
||||
core.info(`Trying to find version for ruff in: ${filePath}`);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
core.warning(`Could not find file: ${filePath}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parser = getVersionFileParser(filePath);
|
||||
if (parser === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const specifier = parser.parse(filePath);
|
||||
if (specifier === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalizedSpecifier = normalizeVersionSpecifier(specifier);
|
||||
core.info(`Found version for ruff in ${filePath}: ${normalizedSpecifier}`);
|
||||
return {
|
||||
format: parser.format,
|
||||
specifier: normalizedSpecifier,
|
||||
};
|
||||
} catch (error) {
|
||||
core.warning(
|
||||
`Error while parsing ${filePath}: ${(error as Error).message}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getRuffVersionFromFile(filePath: string): string | undefined {
|
||||
return getParsedVersionFile(filePath)?.specifier;
|
||||
}
|
||||
|
||||
export function findRuffVersionInSpec(spec: string): string | undefined {
|
||||
const trimmedSpec = spec.trim();
|
||||
|
||||
if (!trimmedSpec.startsWith("ruff")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let versionSpec = trimmedSpec.slice("ruff".length);
|
||||
if (!versionSpec.match(/^(?:\s+|[=<>~!])/)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
versionSpec = versionSpec.replace(/\\$/, "").trim();
|
||||
|
||||
const match = versionSpec.match(/^([^;]+)(?:;.*)?$/);
|
||||
|
||||
if (match) {
|
||||
let version = match[1].trim();
|
||||
if (version) {
|
||||
version = normalizeVersionSpecifier(version);
|
||||
if (trimmedSpec.includes(";")) {
|
||||
core.warning(
|
||||
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
|
||||
);
|
||||
}
|
||||
core.info(`Found ruff version in requirements file: ${version}`);
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getRuffVersionFromRequirementsText(
|
||||
fileContent: string,
|
||||
): string | undefined {
|
||||
return getRuffVersionFromAllDependencies(fileContent.split("\n"));
|
||||
}
|
||||
|
||||
export function getRuffVersionFromPyprojectContent(
|
||||
pyprojectContent: string,
|
||||
): string | undefined {
|
||||
const pyproject = parsePyprojectContent(pyprojectContent);
|
||||
return getRuffVersionFromParsedPyproject(pyproject);
|
||||
}
|
||||
|
||||
export function parsePyprojectContent(pyprojectContent: string): Pyproject {
|
||||
return toml.parse(pyprojectContent) as Pyproject;
|
||||
}
|
||||
|
||||
function getVersionFileParser(filePath: string): VersionFileParser | undefined {
|
||||
return VERSION_FILE_PARSERS.find((parser) => parser.supports(filePath));
|
||||
}
|
||||
|
||||
function getRuffVersionFromParsedPyproject(
|
||||
pyproject: Pyproject,
|
||||
): string | undefined {
|
||||
const dependencies: string[] = pyproject.project?.dependencies || [];
|
||||
const optionalDependencies: string[] = Object.values(
|
||||
pyproject.project?.["optional-dependencies"] || {},
|
||||
).flat();
|
||||
const devDependencies: string[] = Object.values(
|
||||
pyproject["dependency-groups"] || {},
|
||||
)
|
||||
.flat()
|
||||
.filter((item: string | object) => typeof item === "string");
|
||||
|
||||
return (
|
||||
getRuffVersionFromAllDependencies(
|
||||
dependencies.concat(optionalDependencies, devDependencies),
|
||||
) || getRuffVersionFromPoetryGroups(pyproject)
|
||||
);
|
||||
}
|
||||
|
||||
function getRuffVersionFromPoetryGroups(
|
||||
pyproject: Pyproject,
|
||||
): string | undefined {
|
||||
const poetry = pyproject.tool?.poetry || {};
|
||||
const poetryGroups = Object.values(poetry.group || {});
|
||||
if (poetry.dependencies) {
|
||||
poetryGroups.unshift({ dependencies: poetry.dependencies });
|
||||
}
|
||||
|
||||
return poetryGroups
|
||||
.flatMap((group) => Object.entries(group.dependencies))
|
||||
.map(([name, spec]) => {
|
||||
if (typeof spec === "string") {
|
||||
return findRuffVersionInSpec(`${name} ${spec}`);
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.find((version) => version !== undefined);
|
||||
}
|
||||
|
||||
function getRuffVersionFromAllDependencies(
|
||||
allDependencies: string[],
|
||||
): string | undefined {
|
||||
return allDependencies
|
||||
.map((dependency) => findRuffVersionInSpec(dependency))
|
||||
.find((version) => version !== undefined);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
import * as pep440 from "@renovatebot/pep440";
|
||||
import { getAllVersions, getLatestVersion } from "../download/manifest";
|
||||
import {
|
||||
type ParsedVersionSpecifier,
|
||||
parseVersionSpecifier,
|
||||
} from "./specifier";
|
||||
import type { ResolveRuffVersionOptions } from "./types";
|
||||
import { resolveVersionRequest } from "./version-request-resolver";
|
||||
|
||||
interface ConcreteVersionResolutionContext {
|
||||
manifestUrl?: string;
|
||||
parsedSpecifier: ParsedVersionSpecifier;
|
||||
}
|
||||
|
||||
interface ConcreteVersionResolver {
|
||||
resolve(
|
||||
context: ConcreteVersionResolutionContext,
|
||||
): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
class ExactVersionResolver implements ConcreteVersionResolver {
|
||||
async resolve(
|
||||
context: ConcreteVersionResolutionContext,
|
||||
): Promise<string | undefined> {
|
||||
if (context.parsedSpecifier.kind !== "exact") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
core.debug(
|
||||
`Version ${context.parsedSpecifier.normalized} is an explicit version.`,
|
||||
);
|
||||
return context.parsedSpecifier.normalized;
|
||||
}
|
||||
}
|
||||
|
||||
class LatestVersionResolver implements ConcreteVersionResolver {
|
||||
async resolve(
|
||||
context: ConcreteVersionResolutionContext,
|
||||
): Promise<string | undefined> {
|
||||
if (context.parsedSpecifier.kind !== "latest") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await getLatestVersion(context.manifestUrl);
|
||||
}
|
||||
}
|
||||
|
||||
class RangeVersionResolver implements ConcreteVersionResolver {
|
||||
async resolve(
|
||||
context: ConcreteVersionResolutionContext,
|
||||
): Promise<string | undefined> {
|
||||
if (context.parsedSpecifier.kind !== "range") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const availableVersions = await getAllVersions(context.manifestUrl);
|
||||
const resolvedVersion = maxSatisfying(
|
||||
availableVersions,
|
||||
context.parsedSpecifier.normalized,
|
||||
);
|
||||
if (resolvedVersion === undefined) {
|
||||
throw new Error(`No version found for ${context.parsedSpecifier.raw}`);
|
||||
}
|
||||
|
||||
core.debug(`Resolved version: ${resolvedVersion}`);
|
||||
return resolvedVersion;
|
||||
}
|
||||
}
|
||||
|
||||
const CONCRETE_VERSION_RESOLVERS: ConcreteVersionResolver[] = [
|
||||
new ExactVersionResolver(),
|
||||
new LatestVersionResolver(),
|
||||
new RangeVersionResolver(),
|
||||
];
|
||||
|
||||
export async function resolveRuffVersion(
|
||||
options: ResolveRuffVersionOptions,
|
||||
): Promise<string> {
|
||||
const request = resolveVersionRequest(options);
|
||||
return await resolveVersion(request.specifier, options.manifestFile);
|
||||
}
|
||||
|
||||
export async function resolveVersion(
|
||||
versionInput: string,
|
||||
manifestUrl?: string,
|
||||
): Promise<string> {
|
||||
core.debug(`Resolving ${versionInput}...`);
|
||||
|
||||
const context: ConcreteVersionResolutionContext = {
|
||||
manifestUrl,
|
||||
parsedSpecifier: parseVersionSpecifier(versionInput),
|
||||
};
|
||||
|
||||
for (const resolver of CONCRETE_VERSION_RESOLVERS) {
|
||||
const version = await resolver.resolve(context);
|
||||
if (version !== undefined) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`No version found for ${versionInput}`);
|
||||
}
|
||||
|
||||
function maxSatisfying(
|
||||
versions: string[],
|
||||
version: string,
|
||||
): string | undefined {
|
||||
const maxSemver = tc.evaluateVersions(versions, version);
|
||||
if (maxSemver !== "") {
|
||||
core.debug(`Found a version that satisfies the semver range: ${maxSemver}`);
|
||||
return maxSemver;
|
||||
}
|
||||
|
||||
const maxPep440 = pep440.maxSatisfying(versions, version);
|
||||
if (maxPep440 !== null) {
|
||||
core.debug(
|
||||
`Found a version that satisfies the pep440 specifier: ${maxPep440}`,
|
||||
);
|
||||
return maxPep440;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import * as tc from "@actions/tool-cache";
|
||||
|
||||
export type ParsedVersionSpecifier =
|
||||
| {
|
||||
kind: "exact";
|
||||
normalized: string;
|
||||
raw: string;
|
||||
}
|
||||
| {
|
||||
kind: "latest";
|
||||
normalized: "latest";
|
||||
raw: string;
|
||||
}
|
||||
| {
|
||||
kind: "range";
|
||||
normalized: string;
|
||||
raw: string;
|
||||
};
|
||||
|
||||
export function normalizeVersionSpecifier(specifier: string): string {
|
||||
const trimmedSpecifier = specifier.trim();
|
||||
|
||||
if (trimmedSpecifier.startsWith("==")) {
|
||||
return trimmedSpecifier.slice(2);
|
||||
}
|
||||
|
||||
return trimmedSpecifier;
|
||||
}
|
||||
|
||||
export function parseVersionSpecifier(
|
||||
specifier: string,
|
||||
): ParsedVersionSpecifier {
|
||||
const raw = specifier.trim();
|
||||
const normalized = normalizeVersionSpecifier(raw);
|
||||
|
||||
if (normalized === "latest") {
|
||||
return {
|
||||
kind: "latest",
|
||||
normalized: "latest",
|
||||
raw,
|
||||
};
|
||||
}
|
||||
|
||||
if (tc.isExplicitVersion(normalized)) {
|
||||
return {
|
||||
kind: "exact",
|
||||
normalized,
|
||||
raw,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "range",
|
||||
normalized,
|
||||
raw,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
export type VersionSource =
|
||||
| "input"
|
||||
| "version-file"
|
||||
| "pyproject.toml"
|
||||
| "default";
|
||||
|
||||
export type VersionFileFormat = "pyproject.toml" | "requirements";
|
||||
|
||||
export interface ParsedVersionFile {
|
||||
format: VersionFileFormat;
|
||||
specifier: string;
|
||||
}
|
||||
|
||||
export interface ResolveRuffVersionOptions {
|
||||
manifestFile?: string;
|
||||
sourceDirectory: string;
|
||||
version?: string;
|
||||
versionFile?: string;
|
||||
workspaceRoot: string;
|
||||
}
|
||||
|
||||
export interface VersionRequest {
|
||||
format?: VersionFileFormat;
|
||||
source: VersionSource;
|
||||
sourcePath?: string;
|
||||
specifier: string;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import * as core from "@actions/core";
|
||||
import { findPyprojectToml } from "../utils/pyproject-finder";
|
||||
import { getParsedVersionFile } from "./file-parser";
|
||||
import { normalizeVersionSpecifier } from "./specifier";
|
||||
import type {
|
||||
ParsedVersionFile,
|
||||
ResolveRuffVersionOptions,
|
||||
VersionRequest,
|
||||
} from "./types";
|
||||
|
||||
export interface VersionRequestResolver {
|
||||
resolve(context: VersionRequestContext): VersionRequest | undefined;
|
||||
}
|
||||
|
||||
export class VersionRequestContext {
|
||||
readonly sourceDirectory: string;
|
||||
readonly version: string | undefined;
|
||||
readonly versionFile: string | undefined;
|
||||
readonly workspaceRoot: string;
|
||||
|
||||
private readonly parsedFiles = new Map<
|
||||
string,
|
||||
ParsedVersionFile | undefined
|
||||
>();
|
||||
|
||||
constructor(
|
||||
version: string | undefined,
|
||||
versionFile: string | undefined,
|
||||
sourceDirectory: string,
|
||||
workspaceRoot: string,
|
||||
) {
|
||||
this.version = version;
|
||||
this.versionFile = versionFile;
|
||||
this.sourceDirectory = sourceDirectory;
|
||||
this.workspaceRoot = workspaceRoot;
|
||||
}
|
||||
|
||||
getVersionFile(filePath: string): ParsedVersionFile | undefined {
|
||||
const cachedResult = this.parsedFiles.get(filePath);
|
||||
if (cachedResult !== undefined || this.parsedFiles.has(filePath)) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const result = getParsedVersionFile(filePath);
|
||||
this.parsedFiles.set(filePath, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
getWorkspacePyprojectPath(): string | undefined {
|
||||
return findPyprojectToml(this.sourceDirectory, this.workspaceRoot);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExplicitInputVersionResolver implements VersionRequestResolver {
|
||||
resolve(context: VersionRequestContext): VersionRequest | undefined {
|
||||
if (context.version === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
source: "input",
|
||||
specifier: normalizeVersionSpecifier(context.version),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class VersionFileVersionResolver implements VersionRequestResolver {
|
||||
resolve(context: VersionRequestContext): VersionRequest | undefined {
|
||||
if (context.versionFile === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const versionFile = context.getVersionFile(context.versionFile);
|
||||
if (versionFile === undefined) {
|
||||
core.warning(
|
||||
`Could not parse version from ${context.versionFile}. Using latest version.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
format: versionFile.format,
|
||||
source: "version-file",
|
||||
sourcePath: context.versionFile,
|
||||
specifier: versionFile.specifier,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkspaceVersionResolver implements VersionRequestResolver {
|
||||
resolve(context: VersionRequestContext): VersionRequest | undefined {
|
||||
const pyprojectPath = context.getWorkspacePyprojectPath();
|
||||
if (!pyprojectPath) {
|
||||
core.info("Could not find pyproject.toml. Using latest version.");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const versionFile = context.getVersionFile(pyprojectPath);
|
||||
if (versionFile === undefined) {
|
||||
core.info(
|
||||
`Could not parse version from ${pyprojectPath}. Using latest version.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
format: versionFile.format,
|
||||
source: "pyproject.toml",
|
||||
sourcePath: pyprojectPath,
|
||||
specifier: versionFile.specifier,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class LatestVersionResolver implements VersionRequestResolver {
|
||||
resolve(): VersionRequest {
|
||||
return {
|
||||
source: "default",
|
||||
specifier: "latest",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const VERSION_REQUEST_RESOLVERS: VersionRequestResolver[] = [
|
||||
new ExplicitInputVersionResolver(),
|
||||
new VersionFileVersionResolver(),
|
||||
new WorkspaceVersionResolver(),
|
||||
new LatestVersionResolver(),
|
||||
];
|
||||
|
||||
export function resolveVersionRequest(
|
||||
options: ResolveRuffVersionOptions,
|
||||
): VersionRequest {
|
||||
const version = emptyToUndefined(options.version);
|
||||
const versionFile = emptyToUndefined(options.versionFile);
|
||||
|
||||
if (version !== undefined && versionFile !== undefined) {
|
||||
throw new Error(
|
||||
"It is not allowed to specify both version and version-file",
|
||||
);
|
||||
}
|
||||
|
||||
const context = new VersionRequestContext(
|
||||
version,
|
||||
versionFile,
|
||||
options.sourceDirectory,
|
||||
options.workspaceRoot,
|
||||
);
|
||||
|
||||
for (const resolver of VERSION_REQUEST_RESOLVERS) {
|
||||
const request = resolver.resolve(context);
|
||||
if (request !== undefined) {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Could not resolve a requested Ruff version.");
|
||||
}
|
||||
|
||||
function emptyToUndefined(value: string | undefined): string | undefined {
|
||||
return value === undefined || value === "" ? undefined : value;
|
||||
}
|
||||
Reference in New Issue
Block a user