diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f745a6..844144e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -280,6 +280,25 @@ jobs: fi env: RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }} + test-default-version-from-uv-lock: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Use default version from uv.lock + id: ruff-action + uses: ./ + with: + src: __tests__/fixtures/python-project + version-file: __tests__/fixtures/uv.lock + - name: Correct version gets installed + run: | + if [ "$RUFF_VERSION" != "0.9.5" ]; then + exit 1 + fi + env: + RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }} test-semver-range: runs-on: ubuntu-latest steps: @@ -501,6 +520,7 @@ jobs: - test-default-version-from-pyproject-optional-dependencies - test-default-version-from-requirements - test-default-version-from-requirements-with-hash + - test-default-version-from-uv-lock - test-semver-range - test-pep440-version-specifier - test-checksum diff --git a/README.md b/README.md index 09abd80..4edcd66 100644 --- a/README.md +++ b/README.md @@ -178,14 +178,14 @@ to install the latest version that satisfies the range. #### Install a version from a specified version file You can specify a file to read the version from. -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`. +Currently `pyproject.toml`, `requirements.txt` and `uv.lock` are supported. If the file cannot +be parsed or does not contain a Ruff version, the action warns and falls back to `latest`. ```yaml - name: Install a version from a specified version file uses: astral-sh/ruff-action@v4.0.0 with: - version-file: "my-path/to/pyproject.toml-or-requirements.txt" + version-file: "my-path/to/pyproject.toml-requirements.txt-or-uv.lock" ``` Version resolution precedence is: diff --git a/__tests__/fixtures/uv.lock b/__tests__/fixtures/uv.lock new file mode 100644 index 0000000..a77f346 --- /dev/null +++ b/__tests__/fixtures/uv.lock @@ -0,0 +1,20 @@ +version = 1 +revision = 1 +requires-python = ">=3.11" + +[[package]] +name = "example-project" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [{ name = "ruff", specifier = ">=0.9.0" }] + +[[package]] +name = "ruff" +version = "0.9.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/source/r/ruff/ruff-0.9.5.tar.gz", hash = "sha256:11aecd7a633932875ab3cb05a484c99970b9d52606ce9ea912b690b02653d56c", size = 3634177 } diff --git a/__tests__/version/file-parser.test.ts b/__tests__/version/file-parser.test.ts index c0c2cd9..7c3626b 100644 --- a/__tests__/version/file-parser.test.ts +++ b/__tests__/version/file-parser.test.ts @@ -9,9 +9,11 @@ jest.unstable_mockModule("@actions/core", () => ({ warning, })); -const { findRuffVersionInSpec, getRuffVersionFromFile } = await import( - "../../src/version/file-parser" -); +const { + findRuffVersionInSpec, + getRuffVersionFromFile, + getRuffVersionFromUvLockContent, +} = await import("../../src/version/file-parser"); describe("file-parser", () => { beforeEach(() => { @@ -124,5 +126,26 @@ describe("file-parser", () => { ); expect(result).toBe("~0.8.2"); }); + + it("reads the version from uv.lock", () => { + const result = getRuffVersionFromFile("__tests__/fixtures/uv.lock"); + expect(result).toBe("0.9.5"); + }); + }); + + describe("getRuffVersionFromUvLockContent", () => { + it("extracts the ruff version from uv.lock content", () => { + const result = getRuffVersionFromUvLockContent( + '[[package]]\nname = "other"\nversion = "1.0.0"\n\n[[package]]\nname = "ruff"\nversion = "0.9.5"\n', + ); + expect(result).toBe("0.9.5"); + }); + + it("returns undefined when ruff is not in uv.lock", () => { + const result = getRuffVersionFromUvLockContent( + '[[package]]\nname = "other"\nversion = "1.0.0"\n', + ); + expect(result).toBeUndefined(); + }); }); }); diff --git a/action.yml b/action.yml index e61cfa9..42f29b6 100644 --- a/action.yml +++ b/action.yml @@ -15,7 +15,7 @@ inputs: required: false default: "" version-file: - 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`." + description: "Path to a pyproject.toml, requirements.txt or uv.lock file to read the version from. If parsing fails, the action warns and falls back to `latest`." required: false checksum: description: "The checksum of the ruff version to install" diff --git a/dist/ruff-action/index.cjs b/dist/ruff-action/index.cjs index 36b583d..ebc2641 100644 --- a/dist/ruff-action/index.cjs +++ b/dist/ruff-action/index.cjs @@ -31702,6 +31702,14 @@ var VERSION_FILE_PARSERS = [ }, supports: (filePath) => filePath.endsWith("pyproject.toml") }, + { + format: "uv.lock", + parse: (filePath) => { + const fileContent = import_node_fs2.default.readFileSync(filePath, "utf-8"); + return getRuffVersionFromUvLockContent(fileContent); + }, + supports: (filePath) => filePath.endsWith("uv.lock") + }, { format: "requirements", parse: (filePath) => { @@ -31775,6 +31783,15 @@ function getRuffVersionFromPyprojectContent(pyprojectContent) { function parsePyprojectContent(pyprojectContent) { return parse2(pyprojectContent); } +function getRuffVersionFromUvLockContent(uvLockContent) { + const uvLock = parse(uvLockContent); + const ruffPackage = (uvLock.package || []).find((pkg) => pkg.name === "ruff"); + if (ruffPackage?.version === void 0) { + return void 0; + } + info(`Found ruff version in uv.lock: ${ruffPackage.version}`); + return ruffPackage.version; +} function getVersionFileParser(filePath) { return VERSION_FILE_PARSERS.find((parser) => parser.supports(filePath)); } diff --git a/src/version/file-parser.ts b/src/version/file-parser.ts index 5f9a773..b2ac10e 100644 --- a/src/version/file-parser.ts +++ b/src/version/file-parser.ts @@ -24,6 +24,10 @@ interface Pyproject { }; } +interface UvLock { + package?: Array<{ name?: string; version?: string }>; +} + const VERSION_FILE_PARSERS: VersionFileParser[] = [ { format: "pyproject.toml", @@ -33,6 +37,14 @@ const VERSION_FILE_PARSERS: VersionFileParser[] = [ }, supports: (filePath) => filePath.endsWith("pyproject.toml"), }, + { + format: "uv.lock", + parse: (filePath) => { + const fileContent = fs.readFileSync(filePath, "utf-8"); + return getRuffVersionFromUvLockContent(fileContent); + }, + supports: (filePath) => filePath.endsWith("uv.lock"), + }, { format: "requirements", parse: (filePath) => { @@ -132,6 +144,18 @@ export function parsePyprojectContent(pyprojectContent: string): Pyproject { return toml.parse(pyprojectContent) as Pyproject; } +export function getRuffVersionFromUvLockContent( + uvLockContent: string, +): string | undefined { + const uvLock = toml.parse(uvLockContent) as UvLock; + const ruffPackage = (uvLock.package || []).find((pkg) => pkg.name === "ruff"); + if (ruffPackage?.version === undefined) { + return undefined; + } + core.info(`Found ruff version in uv.lock: ${ruffPackage.version}`); + return ruffPackage.version; +} + function getVersionFileParser(filePath: string): VersionFileParser | undefined { return VERSION_FILE_PARSERS.find((parser) => parser.supports(filePath)); } diff --git a/src/version/types.ts b/src/version/types.ts index 11e7ece..b8d1efb 100644 --- a/src/version/types.ts +++ b/src/version/types.ts @@ -4,7 +4,7 @@ export type VersionSource = | "pyproject.toml" | "default"; -export type VersionFileFormat = "pyproject.toml" | "requirements"; +export type VersionFileFormat = "pyproject.toml" | "requirements" | "uv.lock"; export interface ParsedVersionFile { format: VersionFileFormat;