diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6ef5926..dfa0f22 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,17 +28,18 @@ jobs: - name: Make sure no changes from linters are detected run: | git diff --exit-code - test-default-version: + test-latest-version: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, macos-14, windows-latest] steps: - uses: actions/checkout@v4 - - name: Use default version + - name: Use latest version uses: ./ with: src: __tests__/fixtures/python-project + version: latest test-specific-version: runs-on: ubuntu-latest strategy: @@ -51,6 +52,39 @@ jobs: with: version: ${{ matrix.ruff-version }} src: __tests__/fixtures/python-project + test-default-version-from-pyproject: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use default version from pyproject.toml + id: ruff-action + uses: ./ + with: + src: __tests__/fixtures/python-project + - name: Correct version gets installed + run: | + if [ "$RUFF_VERSION" != "0.6.2" ]; then + exit 1 + fi + env: + RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }} + test-default-version-from-pyproject-dev-group: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use default version from pyproject.toml dev group + id: ruff-action + uses: ./ + with: + src: __tests__/fixtures/pyproject-dependency-dev-project + version-file: __tests__/fixtures/pyproject-dependency-dev-project/pyproject.toml + - name: Correct version gets installed + run: | + if [ "$RUFF_VERSION" != "0.8.3" ]; then + exit 1 + fi + env: + RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }} test-semver-range: runs-on: ubuntu-latest steps: diff --git a/README.md b/README.md index a12d31c..e50cf47 100644 --- a/README.md +++ b/README.md @@ -19,19 +19,21 @@ fix). - [Install the latest version (default)](#install-the-latest-version-default) - [Install a specific version](#install-a-specific-version) - [Install a version by supplying a semver range](#install-a-version-by-supplying-a-semver-range) + - [Install a version from a specified version file](#install-a-version-from-a-specified-version-file) - [Validate checksum](#validate-checksum) - [GitHub authentication token](#github-authentication-token) - [Outputs](#outputs) ## Usage -| Input | Description | Default | -|----------------|---------------------------------------------------------------------------------------------|--------------------| -| `version` | The version of Ruff to install. See [Install specific versions](#install-specific-versions) | `latest` | -| `args` | The arguments to pass to the `ruff` command. See [Configuring Ruff] | `check` | -| `src` | The directory or single files to run `ruff` on. | [github.workspace] | -| `checksum` | The sha256 checksum of the downloaded executable. | None | -| `github-token` | The GitHub token to use for authentication. | `GITHUB_TOKEN` | +| Input | Description | Default | +|----------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------------| +| `version` | The version of Ruff to install. See [Install specific versions](#install-specific-versions) | `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 | +| `args` | The arguments to pass to the `ruff` command. See [Configuring Ruff] | `check` | +| `src` | The directory or single files to run `ruff` on. | [github.workspace] | +| `checksum` | The sha256 checksum of the downloaded executable. | None | +| `github-token` | The GitHub token to use for authentication. | `GITHUB_TOKEN` | ### Basic @@ -77,7 +79,11 @@ This action adds ruff to the PATH, so you can use it in subsequent steps. ### Install specific versions -#### Install the latest version (default) +By default this action looks for a pyproject.toml file in the root of the repository to determine +the ruff version to install. If no pyproject.toml file is found, or no ruff version is defined in +either `dependencies` or `dependency-groups.dev` the latest version is installed. + +#### Install the latest version ```yaml - name: Install the latest version of ruff @@ -86,13 +92,7 @@ This action adds ruff to the PATH, so you can use it in subsequent steps. version: "latest" ``` -> [!TIP] -> -> Using `latest` requires to download the ruff executable on every run, which incurs a cost -> (especially on self-hosted runners). As a best practice, consider pinning the version to a -> specific release. - -### Install a specific version +#### Install a specific version ```yaml - name: Install a specific version of ruff @@ -101,7 +101,7 @@ This action adds ruff to the PATH, so you can use it in subsequent steps. version: "0.4.4" ``` -### Install a version by supplying a semver range +#### Install a version by supplying a semver range You can specify a [semver range](https://github.com/npm/node-semver?tab=readme-ov-file#ranges) to install the latest version that satisfies the range. @@ -120,6 +120,18 @@ to install the latest version that satisfies the range. version: "0.4.x" ``` +#### Install a version from a specified version file + +You can specify a file to read the version from. +Currently `pyproject.toml` is supported. + +```yaml +- name: Install a version from a specified version file + uses: astral-sh/ruff-action@v2 + with: + version-file: "my-path/to/pyproject.toml" +``` + ### Validate checksum You can specify a checksum to validate the downloaded executable. Checksums up to the default version diff --git a/__tests__/fixtures/pyproject-dependency-dev-project/README.md b/__tests__/fixtures/pyproject-dependency-dev-project/README.md new file mode 100644 index 0000000..e69de29 diff --git a/__tests__/fixtures/pyproject-dependency-dev-project/pyproject.toml b/__tests__/fixtures/pyproject-dependency-dev-project/pyproject.toml new file mode 100644 index 0000000..b6ce798 --- /dev/null +++ b/__tests__/fixtures/pyproject-dependency-dev-project/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "pyproject-dependency-dev-project" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" + +[dependency-groups] +dev = [ + "ruff==0.8.3" +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/__tests__/fixtures/pyproject-dependency-dev-project/src/pyproject_dependency_dev_project/__init__.py b/__tests__/fixtures/pyproject-dependency-dev-project/src/pyproject_dependency_dev_project/__init__.py new file mode 100644 index 0000000..feca748 --- /dev/null +++ b/__tests__/fixtures/pyproject-dependency-dev-project/src/pyproject_dependency_dev_project/__init__.py @@ -0,0 +1,2 @@ +def hello() -> str: + return "Hello from python-project!" diff --git a/__tests__/fixtures/pyproject-dependency-dev-project/src/pyproject_dependency_dev_project/hello_world.py b/__tests__/fixtures/pyproject-dependency-dev-project/src/pyproject_dependency_dev_project/hello_world.py new file mode 100644 index 0000000..f1a1813 --- /dev/null +++ b/__tests__/fixtures/pyproject-dependency-dev-project/src/pyproject_dependency_dev_project/hello_world.py @@ -0,0 +1 @@ +print("Hello world!") diff --git a/__tests__/fixtures/python-project/pyproject.toml b/__tests__/fixtures/python-project/pyproject.toml index 66377ba..f8999d3 100644 --- a/__tests__/fixtures/python-project/pyproject.toml +++ b/__tests__/fixtures/python-project/pyproject.toml @@ -5,7 +5,7 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "ruff>=0.6.2", + "ruff==0.6.2", ] [build-system] diff --git a/action.yml b/action.yml index 91b0cbb..72ca55e 100644 --- a/action.yml +++ b/action.yml @@ -11,9 +11,12 @@ inputs: required: false default: ${{ github.workspace }} version: - description: "The version of Ruff to use, e.g., `0.6.0` Defaults to the latest version." + description: "The version of Ruff to use, e.g., `0.6.0` Defaults to the version in pyproject.toml or 'latest'." + required: false + default: "" + version-file: + description: "Path to a pyproject.toml or requirements.txt file to read the version from." required: false - default: "latest" checksum: description: "The checksum of the ruff version to install" required: false diff --git a/dist/ruff-action/index.js b/dist/ruff-action/index.js index 9f31d51..41a9d13 100644 Binary files a/dist/ruff-action/index.js and b/dist/ruff-action/index.js differ diff --git a/package-lock.json b/package-lock.json index 73390f0..8abe58d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@actions/core": "^1.11.1", "@actions/exec": "^1.1.1", "@actions/github": "^6.0.0", - "@actions/tool-cache": "^2.0.1" + "@actions/tool-cache": "^2.0.1", + "toml": "^3.0.0" }, "devDependencies": { "@biomejs/biome": "1.9.4", @@ -453,6 +454,11 @@ "semver": "bin/semver.js" } }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", diff --git a/package.json b/package.json index c485fb9..bee93b8 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "@actions/core": "^1.11.1", "@actions/exec": "^1.1.1", "@actions/github": "^6.0.0", - "@actions/tool-cache": "^2.0.1" + "@actions/tool-cache": "^2.0.1", + "toml": "^3.0.0" }, "devDependencies": { "@biomejs/biome": "1.9.4", diff --git a/src/download/download-version.ts b/src/download/download-version.ts index bb3fd0d..4b9b7c6 100644 --- a/src/download/download-version.ts +++ b/src/download/download-version.ts @@ -29,13 +29,12 @@ export async function downloadVersion( checkSum: string | undefined, githubToken: string, ): Promise<{ version: string; cachedToolDir: string }> { - const resolvedVersion = await resolveVersion(version, githubToken); const artifact = `ruff-${arch}-${platform}`; let extension = ".tar.gz"; if (platform === "pc-windows-msvc") { extension = ".zip"; } - const downloadUrl = `https://github.com/${OWNER}/${REPO}/releases/download/${resolvedVersion}/${artifact}${extension}`; + const downloadUrl = `https://github.com/${OWNER}/${REPO}/releases/download/${version}/${artifact}${extension}`; core.info(`Downloading ruff from "${downloadUrl}" ...`); const downloadPath = await tc.downloadTool( @@ -43,13 +42,7 @@ export async function downloadVersion( undefined, githubToken, ); - await validateChecksum( - checkSum, - downloadPath, - arch, - platform, - resolvedVersion, - ); + await validateChecksum(checkSum, downloadPath, arch, platform, version); let ruffDir: string; if (platform === "pc-windows-msvc") { @@ -64,10 +57,10 @@ export async function downloadVersion( const cachedToolDir = await tc.cacheDir( ruffDir, TOOL_CACHE_NAME, - resolvedVersion, + version, arch, ); - return { version: resolvedVersion, cachedToolDir }; + return { version: version, cachedToolDir }; } export async function resolveVersion( diff --git a/src/ruff-action.ts b/src/ruff-action.ts index 38424f0..fb5e3e8 100644 --- a/src/ruff-action.ts +++ b/src/ruff-action.ts @@ -13,7 +13,16 @@ import { getPlatform, type Platform, } from "./utils/platforms"; -import { args, checkSum, githubToken, src, version } from "./utils/inputs"; +import { + args, + checkSum, + githubToken, + src, + version, + versionFile as versionFileInput, +} from "./utils/inputs"; +import { getRuffVersionFromPyproject } from "./utils/pyproject"; +import * as fs from "node:fs"; async function run(): Promise { const platform = getPlatform(); @@ -26,13 +35,7 @@ async function run(): Promise { if (arch === undefined) { throw new Error(`Unsupported architecture: ${process.arch}`); } - const setupResult = await setupRuff( - platform, - arch, - version, - checkSum, - githubToken, - ); + const setupResult = await setupRuff(platform, arch, checkSum, githubToken); addRuffToPath(setupResult.ruffDir); setOutputFormat(); @@ -55,11 +58,10 @@ async function run(): Promise { async function setupRuff( platform: Platform, arch: Architecture, - versionInput: string, checkSum: string | undefined, githubToken: string, ): Promise<{ ruffDir: string; version: string }> { - const resolvedVersion = await resolveVersion(versionInput, githubToken); + const resolvedVersion = await determineVersion(); const toolCacheResult = tryGetFromToolCache(arch, resolvedVersion); if (toolCacheResult.installedPath) { core.info(`Found ruffDir in tool-cache for ${toolCacheResult.version}`); @@ -83,6 +85,30 @@ async function setupRuff( }; } +async function determineVersion(): Promise { + if (versionFileInput !== "" && version !== "") { + throw Error("It is not allowed to specify both version and version-file"); + } + if (version !== "") { + return await resolveVersion(version, githubToken); + } + if (versionFileInput !== "") { + const versionFromPyproject = getRuffVersionFromPyproject(versionFileInput); + if (versionFromPyproject === undefined) { + core.warning( + "Could not parse version from supplied pyproject.toml. Using latest version.", + ); + return await resolveVersion("latest", githubToken); + } + } + const pyProjectPath = path.join(src, "pyproject.toml"); + if (!fs.existsSync(pyProjectPath)) { + return await resolveVersion("latest", githubToken); + } + const versionFromPyproject = getRuffVersionFromPyproject(pyProjectPath); + return await resolveVersion(versionFromPyproject || "latest", githubToken); +} + function addRuffToPath(cachedPath: string): void { core.addPath(cachedPath); core.info(`Added ${cachedPath} to the path`); diff --git a/src/utils/inputs.ts b/src/utils/inputs.ts index 26b9a29..6b59b93 100644 --- a/src/utils/inputs.ts +++ b/src/utils/inputs.ts @@ -5,3 +5,4 @@ export const checkSum = core.getInput("checksum"); export const githubToken = core.getInput("github-token"); export const args = core.getInput("args"); export const src = core.getInput("src"); +export const versionFile = core.getInput("version-file"); diff --git a/src/utils/pyproject.ts b/src/utils/pyproject.ts new file mode 100644 index 0000000..ad0c778 --- /dev/null +++ b/src/utils/pyproject.ts @@ -0,0 +1,30 @@ +import * as fs from "node:fs"; +import * as core from "@actions/core"; +import * as toml from "toml"; + +export function getRuffVersionFromPyproject( + filePath: string, +): string | undefined { + const pyprojectContent = fs.readFileSync(filePath, "utf-8"); + const pyproject = toml.parse(pyprojectContent); + + const dependencies: string[] = pyproject.project?.dependencies || []; + const devDependencies: string[] = pyproject["dependency-groups"]?.dev || []; + + const ruffVersionDefinition = + dependencies.find((dep: string) => dep.startsWith("ruff")) || + devDependencies.find((dep: string) => dep.startsWith("ruff")); + + if (ruffVersionDefinition) { + const ruffVersion = ruffVersionDefinition + .match(/^ruff([^A-Z0-9._-]+.*)$/)?.[1] + .trim(); + if (ruffVersion?.startsWith("==")) { + return ruffVersion.slice(2); + } + core.info(`Found ruff version in pyproject.toml: ${ruffVersion}`); + return ruffVersion; + } + + return undefined; +}