mirror of
https://github.com/astral-sh/ruff-action.git
synced 2026-05-12 12:40:14 +02:00
refactor version resolving (#353)
This commit is contained in:
committed by
GitHub
parent
9b8caf6c41
commit
0ce1b0bf8b
@@ -29,7 +29,7 @@ anything `ruff` can (ex, fix).
|
|||||||
|
|
||||||
| Input | Description | Default |
|
| Input | Description | Default |
|
||||||
|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
|
|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
|
||||||
| `version` | The version of Ruff to install. See [Install specific versions](#install-specific-versions) | `latest` |
|
| `version` | The version of Ruff to install. See [Install specific versions](#install-specific-versions) | discovered from `pyproject.toml`, else `latest` |
|
||||||
| `version-file` | The file to read the version from. See [Install a version from a specified version file](#install-a-version-from-a-specified-version-file) | None |
|
| `version-file` | The file to read the version from. See [Install a version from a specified version file](#install-a-version-from-a-specified-version-file) | None |
|
||||||
| `manifest-file` | URL to a custom Ruff manifest in the `astral-sh/versions` format. | None |
|
| `manifest-file` | URL to a custom Ruff manifest in the `astral-sh/versions` format. | None |
|
||||||
| `args` | The arguments to pass to the `ruff` command. See [Configuring Ruff] | `check` |
|
| `args` | The arguments to pass to the `ruff` command. See [Configuring Ruff] | `check` |
|
||||||
@@ -95,10 +95,10 @@ you can use the `args` input to overwrite the default value (`check`):
|
|||||||
|
|
||||||
### Install specific versions
|
### Install specific versions
|
||||||
|
|
||||||
By default this action looks for a pyproject.toml file in the root of the repository to determine
|
By default this action searches upward from `src` until the workspace root to find the nearest
|
||||||
the ruff version to install. If no pyproject.toml file is found, or no ruff version is defined in
|
`pyproject.toml` and determine the Ruff version to install. If no `pyproject.toml` file is found,
|
||||||
`project.dependencies`, `project.optional-dependencies`, or `dependency-groups`,
|
or no Ruff version is defined in `project.dependencies`, `project.optional-dependencies`,
|
||||||
the latest version is installed.
|
`dependency-groups`, or supported Poetry dependency tables, the latest version is installed.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> This action does only support ruff versions v0.0.247 and above.
|
> This action does only support ruff versions v0.0.247 and above.
|
||||||
@@ -151,7 +151,8 @@ to install the latest version that satisfies the range.
|
|||||||
#### Install a version from a specified version file
|
#### Install a version from a specified version file
|
||||||
|
|
||||||
You can specify a file to read the version from.
|
You can specify a file to read the version from.
|
||||||
Currently `pyproject.toml` and `requirements.txt` are supported.
|
Currently `pyproject.toml` and `requirements.txt` are supported. If the file cannot be parsed
|
||||||
|
or does not contain a Ruff version, the action warns and falls back to `latest`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Install a version from a specified version file
|
- name: Install a version from a specified version file
|
||||||
@@ -160,6 +161,13 @@ Currently `pyproject.toml` and `requirements.txt` are supported.
|
|||||||
version-file: "my-path/to/pyproject.toml-or-requirements.txt"
|
version-file: "my-path/to/pyproject.toml-or-requirements.txt"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Version resolution precedence is:
|
||||||
|
|
||||||
|
1. `version`
|
||||||
|
2. `version-file`
|
||||||
|
3. nearest discoverable `pyproject.toml` found by searching upward from `src`
|
||||||
|
4. `latest`
|
||||||
|
|
||||||
#### Install using a custom manifest URL
|
#### Install using a custom manifest URL
|
||||||
|
|
||||||
You can override the default `astral-sh/versions` manifest with `manifest-file`.
|
You can override the default `astral-sh/versions` manifest with `manifest-file`.
|
||||||
|
|||||||
@@ -47,15 +47,17 @@ const mockCopyFile = jest.fn();
|
|||||||
const mockReaddir = jest.fn();
|
const mockReaddir = jest.fn();
|
||||||
|
|
||||||
jest.unstable_mockModule("node:fs", () => ({
|
jest.unstable_mockModule("node:fs", () => ({
|
||||||
|
default: {},
|
||||||
promises: {
|
promises: {
|
||||||
copyFile: mockCopyFile,
|
copyFile: mockCopyFile,
|
||||||
readdir: mockReaddir,
|
readdir: mockReaddir,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { downloadVersion, resolveVersion, rewriteToMirror } = await import(
|
const { downloadVersion, rewriteToMirror } = await import(
|
||||||
"../../src/download/download-version"
|
"../../src/download/download-version"
|
||||||
);
|
);
|
||||||
|
const { resolveVersion } = await import("../../src/version/resolve");
|
||||||
|
|
||||||
describe("download-version", () => {
|
describe("download-version", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
+2
-2
@@ -11,11 +11,11 @@ inputs:
|
|||||||
required: false
|
required: false
|
||||||
default: ${{ github.workspace }}
|
default: ${{ github.workspace }}
|
||||||
version:
|
version:
|
||||||
description: "The version of Ruff to use, e.g., `0.6.0` Defaults to the version in pyproject.toml or 'latest'."
|
description: "The version of Ruff to use, e.g., `0.6.0`. Defaults to the first discoverable version in pyproject.toml searched upward from `src`, or `latest`."
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
version-file:
|
version-file:
|
||||||
description: "Path to a pyproject.toml or requirements.txt file to read the version from."
|
description: "Path to a pyproject.toml or requirements.txt file to read the version from. If parsing fails, the action warns and falls back to `latest`."
|
||||||
required: false
|
required: false
|
||||||
checksum:
|
checksum:
|
||||||
description: "The checksum of the ruff version to install"
|
description: "The checksum of the ruff version to install"
|
||||||
|
|||||||
+334
-128
@@ -24746,7 +24746,6 @@ function _getGlobal(key, defaultValue) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// src/download/download-version.ts
|
// src/download/download-version.ts
|
||||||
var pep440 = __toESM(require_pep440(), 1);
|
|
||||||
var semver3 = __toESM(require_semver(), 1);
|
var semver3 = __toESM(require_semver(), 1);
|
||||||
|
|
||||||
// src/utils/constants.ts
|
// src/utils/constants.ts
|
||||||
@@ -28104,24 +28103,6 @@ async function extractDownloadedArtifact(version2, downloadPath, extension, plat
|
|||||||
debug(`Contents of ${ruffDir}: ${files.join(", ")}`);
|
debug(`Contents of ${ruffDir}: ${files.join(", ")}`);
|
||||||
return ruffDir;
|
return ruffDir;
|
||||||
}
|
}
|
||||||
async function resolveVersion(versionInput, manifestUrl) {
|
|
||||||
debug(`Resolving ${versionInput}...`);
|
|
||||||
const version2 = versionInput === "latest" ? await getLatestVersion(manifestUrl) : versionInput;
|
|
||||||
if (isExplicitVersion(version2)) {
|
|
||||||
debug(`Version ${version2} is an explicit version.`);
|
|
||||||
return version2;
|
|
||||||
}
|
|
||||||
const availableVersions = await getAvailableVersions(manifestUrl);
|
|
||||||
const resolvedVersion = maxSatisfying2(availableVersions, version2);
|
|
||||||
if (resolvedVersion === void 0) {
|
|
||||||
throw new Error(`No version found for ${version2}`);
|
|
||||||
}
|
|
||||||
debug(`Resolved version: ${resolvedVersion}`);
|
|
||||||
return resolvedVersion;
|
|
||||||
}
|
|
||||||
async function getAvailableVersions(manifestUrl) {
|
|
||||||
return await getAllVersions(manifestUrl);
|
|
||||||
}
|
|
||||||
function getMissingArtifactMessage(version2, arch3, platform2, manifestUrl) {
|
function getMissingArtifactMessage(version2, arch3, platform2, manifestUrl) {
|
||||||
if (manifestUrl === void 0) {
|
if (manifestUrl === void 0) {
|
||||||
return `Could not find artifact for version ${version2}, arch ${arch3}, platform ${platform2} in ${VERSIONS_MANIFEST_URL} .`;
|
return `Could not find artifact for version ${version2}, arch ${arch3}, platform ${platform2} in ${VERSIONS_MANIFEST_URL} .`;
|
||||||
@@ -28147,21 +28128,6 @@ function stripVersionPrefix2(version2) {
|
|||||||
function getExtension(platform2) {
|
function getExtension(platform2) {
|
||||||
return platform2 === "pc-windows-msvc" ? ".zip" : ".tar.gz";
|
return platform2 === "pc-windows-msvc" ? ".zip" : ".tar.gz";
|
||||||
}
|
}
|
||||||
function maxSatisfying2(versions, version2) {
|
|
||||||
const maxSemver = evaluateVersions(versions, version2);
|
|
||||||
if (maxSemver !== "") {
|
|
||||||
debug(`Found a version that satisfies the semver range: ${maxSemver}`);
|
|
||||||
return maxSemver;
|
|
||||||
}
|
|
||||||
const maxPep440 = pep440.maxSatisfying(versions, version2);
|
|
||||||
if (maxPep440 !== null) {
|
|
||||||
debug(
|
|
||||||
`Found a version that satisfies the pep440 specifier: ${maxPep440}`
|
|
||||||
);
|
|
||||||
return maxPep440;
|
|
||||||
}
|
|
||||||
return void 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// src/utils/inputs.ts
|
// src/utils/inputs.ts
|
||||||
var version = getInput("version");
|
var version = getInput("version");
|
||||||
@@ -28196,8 +28162,71 @@ function getPlatform() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/utils/pyproject.ts
|
// src/version/resolve.ts
|
||||||
|
var pep440 = __toESM(require_pep440(), 1);
|
||||||
|
|
||||||
|
// src/version/specifier.ts
|
||||||
|
function normalizeVersionSpecifier(specifier) {
|
||||||
|
const trimmedSpecifier = specifier.trim();
|
||||||
|
if (trimmedSpecifier.startsWith("==")) {
|
||||||
|
return trimmedSpecifier.slice(2);
|
||||||
|
}
|
||||||
|
return trimmedSpecifier;
|
||||||
|
}
|
||||||
|
function parseVersionSpecifier(specifier) {
|
||||||
|
const raw = specifier.trim();
|
||||||
|
const normalized = normalizeVersionSpecifier(raw);
|
||||||
|
if (normalized === "latest") {
|
||||||
|
return {
|
||||||
|
kind: "latest",
|
||||||
|
normalized: "latest",
|
||||||
|
raw
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isExplicitVersion(normalized)) {
|
||||||
|
return {
|
||||||
|
kind: "exact",
|
||||||
|
normalized,
|
||||||
|
raw
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind: "range",
|
||||||
|
normalized,
|
||||||
|
raw
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/utils/pyproject-finder.ts
|
||||||
var fs6 = __toESM(require("node:fs"), 1);
|
var fs6 = __toESM(require("node:fs"), 1);
|
||||||
|
var path7 = __toESM(require("node:path"), 1);
|
||||||
|
function findPyprojectToml(startDir, workspaceRoot) {
|
||||||
|
let currentDir = path7.resolve(startDir);
|
||||||
|
const resolvedWorkspaceRoot = path7.resolve(workspaceRoot);
|
||||||
|
while (true) {
|
||||||
|
const pyprojectPath = path7.join(currentDir, "pyproject.toml");
|
||||||
|
debug(`Checking for ${pyprojectPath}`);
|
||||||
|
if (fs6.existsSync(pyprojectPath)) {
|
||||||
|
info(`Found pyproject.toml at ${pyprojectPath}`);
|
||||||
|
return pyprojectPath;
|
||||||
|
}
|
||||||
|
if (currentDir === resolvedWorkspaceRoot) {
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
const parentDir = path7.dirname(currentDir);
|
||||||
|
if (parentDir === currentDir || !isPathWithinWorkspace(parentDir, resolvedWorkspaceRoot)) {
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
currentDir = parentDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function isPathWithinWorkspace(checkPath, workspaceRoot) {
|
||||||
|
const relativePath = path7.relative(workspaceRoot, checkPath);
|
||||||
|
return !relativePath.startsWith("..") && !path7.isAbsolute(relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/version/file-parser.ts
|
||||||
|
var import_node_fs2 = __toESM(require("node:fs"), 1);
|
||||||
|
|
||||||
// node_modules/smol-toml/dist/error.js
|
// node_modules/smol-toml/dist/error.js
|
||||||
function getLineColFromPtr(string, ptr) {
|
function getLineColFromPtr(string, ptr) {
|
||||||
@@ -28889,14 +28918,60 @@ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/utils/pyproject.ts
|
// src/version/file-parser.ts
|
||||||
|
var VERSION_FILE_PARSERS = [
|
||||||
|
{
|
||||||
|
format: "pyproject.toml",
|
||||||
|
parse: (filePath) => {
|
||||||
|
const fileContent = import_node_fs2.default.readFileSync(filePath, "utf-8");
|
||||||
|
return getRuffVersionFromPyprojectContent(fileContent);
|
||||||
|
},
|
||||||
|
supports: (filePath) => filePath.endsWith("pyproject.toml")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "requirements",
|
||||||
|
parse: (filePath) => {
|
||||||
|
const fileContent = import_node_fs2.default.readFileSync(filePath, "utf-8");
|
||||||
|
return getRuffVersionFromRequirementsText(fileContent);
|
||||||
|
},
|
||||||
|
supports: (filePath) => filePath.endsWith(".txt")
|
||||||
|
}
|
||||||
|
];
|
||||||
|
function getParsedVersionFile(filePath) {
|
||||||
|
info(`Trying to find version for ruff in: ${filePath}`);
|
||||||
|
if (!import_node_fs2.default.existsSync(filePath)) {
|
||||||
|
warning(`Could not find file: ${filePath}`);
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
const parser = getVersionFileParser(filePath);
|
||||||
|
if (parser === void 0) {
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const specifier = parser.parse(filePath);
|
||||||
|
if (specifier === void 0) {
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
const normalizedSpecifier = normalizeVersionSpecifier(specifier);
|
||||||
|
info(`Found version for ruff in ${filePath}: ${normalizedSpecifier}`);
|
||||||
|
return {
|
||||||
|
format: parser.format,
|
||||||
|
specifier: normalizedSpecifier
|
||||||
|
};
|
||||||
|
} catch (error2) {
|
||||||
|
warning(
|
||||||
|
`Error while parsing ${filePath}: ${error2.message}`
|
||||||
|
);
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
function findRuffVersionInSpec(spec) {
|
function findRuffVersionInSpec(spec) {
|
||||||
const trimmedSpec = spec.trim();
|
const trimmedSpec = spec.trim();
|
||||||
const fullDepMatch = trimmedSpec.match(/^ruff\s*(.+)$/);
|
if (!trimmedSpec.startsWith("ruff")) {
|
||||||
let versionSpec;
|
return void 0;
|
||||||
if (fullDepMatch) {
|
}
|
||||||
versionSpec = fullDepMatch[1];
|
let versionSpec = trimmedSpec.slice("ruff".length);
|
||||||
} else {
|
if (!versionSpec.match(/^(?:\s+|[=<>~!])/)) {
|
||||||
return void 0;
|
return void 0;
|
||||||
}
|
}
|
||||||
versionSpec = versionSpec.replace(/\\$/, "").trim();
|
versionSpec = versionSpec.replace(/\\$/, "").trim();
|
||||||
@@ -28904,9 +28979,7 @@ function findRuffVersionInSpec(spec) {
|
|||||||
if (match) {
|
if (match) {
|
||||||
let version2 = match[1].trim();
|
let version2 = match[1].trim();
|
||||||
if (version2) {
|
if (version2) {
|
||||||
if (version2.startsWith("==")) {
|
version2 = normalizeVersionSpecifier(version2);
|
||||||
version2 = version2.slice(2);
|
|
||||||
}
|
|
||||||
if (trimmedSpec.includes(";")) {
|
if (trimmedSpec.includes(";")) {
|
||||||
warning(
|
warning(
|
||||||
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version."
|
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version."
|
||||||
@@ -28918,24 +28991,33 @@ function findRuffVersionInSpec(spec) {
|
|||||||
}
|
}
|
||||||
return void 0;
|
return void 0;
|
||||||
}
|
}
|
||||||
function getRuffVersionFromAllDependencies(allDependencies) {
|
function getRuffVersionFromRequirementsText(fileContent) {
|
||||||
return allDependencies.map((dep) => findRuffVersionInSpec(dep)).find((version2) => version2 !== void 0);
|
return getRuffVersionFromAllDependencies(fileContent.split("\n"));
|
||||||
}
|
}
|
||||||
function parsePyproject(pyprojectContent) {
|
function getRuffVersionFromPyprojectContent(pyprojectContent) {
|
||||||
const pyproject = parse(pyprojectContent);
|
const pyproject = parsePyprojectContent(pyprojectContent);
|
||||||
const dependencies = pyproject?.project?.dependencies || [];
|
return getRuffVersionFromParsedPyproject(pyproject);
|
||||||
|
}
|
||||||
|
function parsePyprojectContent(pyprojectContent) {
|
||||||
|
return parse(pyprojectContent);
|
||||||
|
}
|
||||||
|
function getVersionFileParser(filePath) {
|
||||||
|
return VERSION_FILE_PARSERS.find((parser) => parser.supports(filePath));
|
||||||
|
}
|
||||||
|
function getRuffVersionFromParsedPyproject(pyproject) {
|
||||||
|
const dependencies = pyproject.project?.dependencies || [];
|
||||||
const optionalDependencies = Object.values(
|
const optionalDependencies = Object.values(
|
||||||
pyproject?.project?.["optional-dependencies"] || {}
|
pyproject.project?.["optional-dependencies"] || {}
|
||||||
).flat();
|
).flat();
|
||||||
const devDependencies = Object.values(
|
const devDependencies = Object.values(
|
||||||
pyproject?.["dependency-groups"] || {}
|
pyproject["dependency-groups"] || {}
|
||||||
).flat().filter((item) => typeof item === "string");
|
).flat().filter((item) => typeof item === "string");
|
||||||
return getRuffVersionFromAllDependencies(
|
return getRuffVersionFromAllDependencies(
|
||||||
dependencies.concat(optionalDependencies, devDependencies)
|
dependencies.concat(optionalDependencies, devDependencies)
|
||||||
) || getRuffVersionFromPoetryGroups(pyproject);
|
) || getRuffVersionFromPoetryGroups(pyproject);
|
||||||
}
|
}
|
||||||
function getRuffVersionFromPoetryGroups(pyproject) {
|
function getRuffVersionFromPoetryGroups(pyproject) {
|
||||||
const poetry = pyproject?.tool?.poetry || {};
|
const poetry = pyproject.tool?.poetry || {};
|
||||||
const poetryGroups = Object.values(poetry.group || {});
|
const poetryGroups = Object.values(poetry.group || {});
|
||||||
if (poetry.dependencies) {
|
if (poetry.dependencies) {
|
||||||
poetryGroups.unshift({ dependencies: poetry.dependencies });
|
poetryGroups.unshift({ dependencies: poetry.dependencies });
|
||||||
@@ -28947,50 +29029,203 @@ function getRuffVersionFromPoetryGroups(pyproject) {
|
|||||||
return void 0;
|
return void 0;
|
||||||
}).find((version2) => version2 !== void 0);
|
}).find((version2) => version2 !== void 0);
|
||||||
}
|
}
|
||||||
function getRuffVersionFromRequirementsFile(filePath) {
|
function getRuffVersionFromAllDependencies(allDependencies) {
|
||||||
if (!fs6.existsSync(filePath)) {
|
return allDependencies.map((dependency) => findRuffVersionInSpec(dependency)).find((version2) => version2 !== void 0);
|
||||||
warning(`Could not find file: ${filePath}`);
|
|
||||||
return void 0;
|
|
||||||
}
|
|
||||||
const pyprojectContent = fs6.readFileSync(filePath, "utf-8");
|
|
||||||
if (filePath.endsWith(".txt")) {
|
|
||||||
return getRuffVersionFromAllDependencies(pyprojectContent.split("\n"));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return parsePyproject(pyprojectContent);
|
|
||||||
} catch (err) {
|
|
||||||
const message = err.message;
|
|
||||||
warning(`Error while parsing ${filePath}: ${message}`);
|
|
||||||
return void 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/utils/pyproject-finder.ts
|
// src/version/version-request-resolver.ts
|
||||||
var fs7 = __toESM(require("node:fs"), 1);
|
var VersionRequestContext = class {
|
||||||
var path7 = __toESM(require("node:path"), 1);
|
sourceDirectory;
|
||||||
function findPyprojectToml(startDir, workspaceRoot) {
|
version;
|
||||||
let currentDir = path7.resolve(startDir);
|
versionFile;
|
||||||
const resolvedWorkspaceRoot = path7.resolve(workspaceRoot);
|
workspaceRoot;
|
||||||
while (true) {
|
parsedFiles = /* @__PURE__ */ new Map();
|
||||||
const pyprojectPath = path7.join(currentDir, "pyproject.toml");
|
constructor(version2, versionFile2, sourceDirectory, workspaceRoot) {
|
||||||
debug(`Checking for ${pyprojectPath}`);
|
this.version = version2;
|
||||||
if (fs7.existsSync(pyprojectPath)) {
|
this.versionFile = versionFile2;
|
||||||
info(`Found pyproject.toml at ${pyprojectPath}`);
|
this.sourceDirectory = sourceDirectory;
|
||||||
return pyprojectPath;
|
this.workspaceRoot = workspaceRoot;
|
||||||
}
|
|
||||||
if (currentDir === resolvedWorkspaceRoot) {
|
|
||||||
return void 0;
|
|
||||||
}
|
|
||||||
const parentDir = path7.dirname(currentDir);
|
|
||||||
if (parentDir === currentDir || !isPathWithinWorkspace(parentDir, resolvedWorkspaceRoot)) {
|
|
||||||
return void 0;
|
|
||||||
}
|
|
||||||
currentDir = parentDir;
|
|
||||||
}
|
}
|
||||||
|
getVersionFile(filePath) {
|
||||||
|
const cachedResult = this.parsedFiles.get(filePath);
|
||||||
|
if (cachedResult !== void 0 || this.parsedFiles.has(filePath)) {
|
||||||
|
return cachedResult;
|
||||||
|
}
|
||||||
|
const result = getParsedVersionFile(filePath);
|
||||||
|
this.parsedFiles.set(filePath, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
getWorkspacePyprojectPath() {
|
||||||
|
return findPyprojectToml(this.sourceDirectory, this.workspaceRoot);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var ExplicitInputVersionResolver = class {
|
||||||
|
resolve(context) {
|
||||||
|
if (context.version === void 0) {
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
source: "input",
|
||||||
|
specifier: normalizeVersionSpecifier(context.version)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var VersionFileVersionResolver = class {
|
||||||
|
resolve(context) {
|
||||||
|
if (context.versionFile === void 0) {
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
const versionFile2 = context.getVersionFile(context.versionFile);
|
||||||
|
if (versionFile2 === void 0) {
|
||||||
|
warning(
|
||||||
|
`Could not parse version from ${context.versionFile}. Using latest version.`
|
||||||
|
);
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
format: versionFile2.format,
|
||||||
|
source: "version-file",
|
||||||
|
sourcePath: context.versionFile,
|
||||||
|
specifier: versionFile2.specifier
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var WorkspaceVersionResolver = class {
|
||||||
|
resolve(context) {
|
||||||
|
const pyprojectPath = context.getWorkspacePyprojectPath();
|
||||||
|
if (!pyprojectPath) {
|
||||||
|
info("Could not find pyproject.toml. Using latest version.");
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
const versionFile2 = context.getVersionFile(pyprojectPath);
|
||||||
|
if (versionFile2 === void 0) {
|
||||||
|
info(
|
||||||
|
`Could not parse version from ${pyprojectPath}. Using latest version.`
|
||||||
|
);
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
format: versionFile2.format,
|
||||||
|
source: "pyproject.toml",
|
||||||
|
sourcePath: pyprojectPath,
|
||||||
|
specifier: versionFile2.specifier
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var LatestVersionResolver = class {
|
||||||
|
resolve() {
|
||||||
|
return {
|
||||||
|
source: "default",
|
||||||
|
specifier: "latest"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var VERSION_REQUEST_RESOLVERS = [
|
||||||
|
new ExplicitInputVersionResolver(),
|
||||||
|
new VersionFileVersionResolver(),
|
||||||
|
new WorkspaceVersionResolver(),
|
||||||
|
new LatestVersionResolver()
|
||||||
|
];
|
||||||
|
function resolveVersionRequest(options) {
|
||||||
|
const version2 = emptyToUndefined(options.version);
|
||||||
|
const versionFile2 = emptyToUndefined(options.versionFile);
|
||||||
|
if (version2 !== void 0 && versionFile2 !== void 0) {
|
||||||
|
throw new Error(
|
||||||
|
"It is not allowed to specify both version and version-file"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const context = new VersionRequestContext(
|
||||||
|
version2,
|
||||||
|
versionFile2,
|
||||||
|
options.sourceDirectory,
|
||||||
|
options.workspaceRoot
|
||||||
|
);
|
||||||
|
for (const resolver of VERSION_REQUEST_RESOLVERS) {
|
||||||
|
const request = resolver.resolve(context);
|
||||||
|
if (request !== void 0) {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Could not resolve a requested Ruff version.");
|
||||||
}
|
}
|
||||||
function isPathWithinWorkspace(checkPath, workspaceRoot) {
|
function emptyToUndefined(value) {
|
||||||
const relativePath = path7.relative(workspaceRoot, checkPath);
|
return value === void 0 || value === "" ? void 0 : value;
|
||||||
return !relativePath.startsWith("..") && !path7.isAbsolute(relativePath);
|
}
|
||||||
|
|
||||||
|
// src/version/resolve.ts
|
||||||
|
var ExactVersionResolver = class {
|
||||||
|
async resolve(context) {
|
||||||
|
if (context.parsedSpecifier.kind !== "exact") {
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
debug(
|
||||||
|
`Version ${context.parsedSpecifier.normalized} is an explicit version.`
|
||||||
|
);
|
||||||
|
return context.parsedSpecifier.normalized;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var LatestVersionResolver2 = class {
|
||||||
|
async resolve(context) {
|
||||||
|
if (context.parsedSpecifier.kind !== "latest") {
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
return await getLatestVersion(context.manifestUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var RangeVersionResolver = class {
|
||||||
|
async resolve(context) {
|
||||||
|
if (context.parsedSpecifier.kind !== "range") {
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
const availableVersions = await getAllVersions(context.manifestUrl);
|
||||||
|
const resolvedVersion = maxSatisfying2(
|
||||||
|
availableVersions,
|
||||||
|
context.parsedSpecifier.normalized
|
||||||
|
);
|
||||||
|
if (resolvedVersion === void 0) {
|
||||||
|
throw new Error(`No version found for ${context.parsedSpecifier.raw}`);
|
||||||
|
}
|
||||||
|
debug(`Resolved version: ${resolvedVersion}`);
|
||||||
|
return resolvedVersion;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var CONCRETE_VERSION_RESOLVERS = [
|
||||||
|
new ExactVersionResolver(),
|
||||||
|
new LatestVersionResolver2(),
|
||||||
|
new RangeVersionResolver()
|
||||||
|
];
|
||||||
|
async function resolveRuffVersion(options) {
|
||||||
|
const request = resolveVersionRequest(options);
|
||||||
|
return await resolveVersion(request.specifier, options.manifestFile);
|
||||||
|
}
|
||||||
|
async function resolveVersion(versionInput, manifestUrl) {
|
||||||
|
debug(`Resolving ${versionInput}...`);
|
||||||
|
const context = {
|
||||||
|
manifestUrl,
|
||||||
|
parsedSpecifier: parseVersionSpecifier(versionInput)
|
||||||
|
};
|
||||||
|
for (const resolver of CONCRETE_VERSION_RESOLVERS) {
|
||||||
|
const version2 = await resolver.resolve(context);
|
||||||
|
if (version2 !== void 0) {
|
||||||
|
return version2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`No version found for ${versionInput}`);
|
||||||
|
}
|
||||||
|
function maxSatisfying2(versions, version2) {
|
||||||
|
const maxSemver = evaluateVersions(versions, version2);
|
||||||
|
if (maxSemver !== "") {
|
||||||
|
debug(`Found a version that satisfies the semver range: ${maxSemver}`);
|
||||||
|
return maxSemver;
|
||||||
|
}
|
||||||
|
const maxPep440 = pep440.maxSatisfying(versions, version2);
|
||||||
|
if (maxPep440 !== null) {
|
||||||
|
debug(
|
||||||
|
`Found a version that satisfies the pep440 specifier: ${maxPep440}`
|
||||||
|
);
|
||||||
|
return maxPep440;
|
||||||
|
}
|
||||||
|
return void 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/ruff-action.ts
|
// src/ruff-action.ts
|
||||||
@@ -29050,42 +29285,13 @@ async function setupRuff(platform2, arch3, checkSum2, githubToken2) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
async function determineVersion() {
|
async function determineVersion() {
|
||||||
if (versionFile !== "" && version !== "") {
|
return await resolveRuffVersion({
|
||||||
throw Error("It is not allowed to specify both version and version-file");
|
manifestFile: manifestFile || void 0,
|
||||||
}
|
sourceDirectory: src,
|
||||||
if (version !== "") {
|
version,
|
||||||
return await resolveVersion(version, manifestFile || void 0);
|
versionFile,
|
||||||
}
|
workspaceRoot: process.env.GITHUB_WORKSPACE || "."
|
||||||
if (versionFile !== "") {
|
});
|
||||||
const versionFromPyproject2 = getRuffVersionFromRequirementsFile(versionFile);
|
|
||||||
if (versionFromPyproject2 === void 0) {
|
|
||||||
warning(
|
|
||||||
`Could not parse version from ${versionFile}. Using latest version.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return await resolveVersion(
|
|
||||||
versionFromPyproject2 || "latest",
|
|
||||||
manifestFile || void 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const pyProjectPath = findPyprojectToml(
|
|
||||||
src,
|
|
||||||
process.env.GITHUB_WORKSPACE || "."
|
|
||||||
);
|
|
||||||
if (!pyProjectPath) {
|
|
||||||
info(`Could not find pyproject.toml. Using latest version.`);
|
|
||||||
return await resolveVersion("latest", manifestFile || void 0);
|
|
||||||
}
|
|
||||||
const versionFromPyproject = getRuffVersionFromRequirementsFile(pyProjectPath);
|
|
||||||
if (versionFromPyproject === void 0) {
|
|
||||||
info(
|
|
||||||
`Could not parse version from ${pyProjectPath}. Using latest version.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return await resolveVersion(
|
|
||||||
versionFromPyproject || "latest",
|
|
||||||
manifestFile || void 0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
function addRuffToPath(cachedPath) {
|
function addRuffToPath(cachedPath) {
|
||||||
addPath(cachedPath);
|
addPath(cachedPath);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { promises as fs } from "node:fs";
|
|||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import * as tc from "@actions/tool-cache";
|
import * as tc from "@actions/tool-cache";
|
||||||
import * as pep440 from "@renovatebot/pep440";
|
|
||||||
import * as semver from "semver";
|
import * as semver from "semver";
|
||||||
import {
|
import {
|
||||||
ASTRAL_MIRROR_PREFIX,
|
ASTRAL_MIRROR_PREFIX,
|
||||||
@@ -12,7 +11,7 @@ import {
|
|||||||
} from "../utils/constants";
|
} from "../utils/constants";
|
||||||
import type { Architecture, Platform } from "../utils/platforms";
|
import type { Architecture, Platform } from "../utils/platforms";
|
||||||
import { validateChecksum } from "./checksum/checksum";
|
import { validateChecksum } from "./checksum/checksum";
|
||||||
import { getAllVersions, getArtifact, getLatestVersion } from "./manifest";
|
import { getArtifact } from "./manifest";
|
||||||
|
|
||||||
export function tryGetFromToolCache(
|
export function tryGetFromToolCache(
|
||||||
arch: Architecture,
|
arch: Architecture,
|
||||||
@@ -162,35 +161,6 @@ async function extractDownloadedArtifact(
|
|||||||
return ruffDir;
|
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(
|
function getMissingArtifactMessage(
|
||||||
version: string,
|
version: string,
|
||||||
arch: Architecture,
|
arch: Architecture,
|
||||||
@@ -245,22 +215,3 @@ function stripVersionPrefix(version: string): string {
|
|||||||
function getExtension(platform: Platform): string {
|
function getExtension(platform: Platform): string {
|
||||||
return platform === "pc-windows-msvc" ? ".zip" : ".tar.gz";
|
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 * as semver from "semver";
|
||||||
import {
|
import {
|
||||||
downloadVersion,
|
downloadVersion,
|
||||||
resolveVersion,
|
|
||||||
tryGetFromToolCache,
|
tryGetFromToolCache,
|
||||||
} from "./download/download-version";
|
} from "./download/download-version";
|
||||||
import {
|
import {
|
||||||
@@ -22,8 +21,7 @@ import {
|
|||||||
getPlatform,
|
getPlatform,
|
||||||
type Platform,
|
type Platform,
|
||||||
} from "./utils/platforms";
|
} from "./utils/platforms";
|
||||||
import { getRuffVersionFromRequirementsFile } from "./utils/pyproject";
|
import { resolveRuffVersion } from "./version/resolve";
|
||||||
import { findPyprojectToml } from "./utils/pyproject-finder";
|
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
const platform = getPlatform();
|
const platform = getPlatform();
|
||||||
@@ -94,44 +92,13 @@ async function setupRuff(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function determineVersion(): Promise<string> {
|
async function determineVersion(): Promise<string> {
|
||||||
if (versionFileInput !== "" && version !== "") {
|
return await resolveRuffVersion({
|
||||||
throw Error("It is not allowed to specify both version and version-file");
|
manifestFile: manifestFile || undefined,
|
||||||
}
|
sourceDirectory: src,
|
||||||
if (version !== "") {
|
version,
|
||||||
return await resolveVersion(version, manifestFile || undefined);
|
versionFile: versionFileInput,
|
||||||
}
|
workspaceRoot: process.env.GITHUB_WORKSPACE || ".",
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRuffToPath(cachedPath: string): void {
|
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