Compare commits

..
Author SHA1 Message Date
Kevin Stillhammer eef6392152 Test exit-zero 2025-01-26 17:49:55 +01:00
47 changed files with 9282 additions and 17761 deletions
+4 -4
View File
@@ -4,12 +4,12 @@
"owner": "ruff-check",
"pattern": [
{
"code": 4,
"column": 3,
"regexp": "^Error: (.*):(\\d+):(\\d+): (\\w+) (.*)$",
"file": 1,
"line": 2,
"message": 5,
"regexp": "^Error: (.*):(\\d+):(\\d+): (\\w+) (.*)$"
"column": 3,
"code": 4,
"message": 5
}
]
}
+3 -9
View File
@@ -4,15 +4,9 @@
"owner": "ruff-format",
"pattern": [
{
"code": 2,
"column": 5,
"endColumn": 7,
"endLine": 6,
"file": 3,
"line": 4,
"message": 8,
"regexp": "^(?:(error|warning|notice))\\s+title=([^,]+),file=([^,]+),line=(\\d+),col=(\\d+)(?:,endLine=(\\d+))?(?:,endColumn=(\\d+))?::(.+)$",
"severity": 1
"regexp": "^(Would reformat):\\s*(.+)$",
"message": 1,
"file": 2
}
]
}
@@ -1,35 +0,0 @@
import * as fs from "node:fs";
import * as yaml from "js-yaml";
interface WorkflowJob {
needs?: string[];
[key: string]: unknown;
}
interface Workflow {
jobs: Record<string, WorkflowJob>;
[key: string]: unknown;
}
const workflow = yaml.load(
fs.readFileSync("../workflows/test.yml", "utf8"),
) as Workflow;
const jobs = Object.keys(workflow.jobs);
const allTestsPassed = workflow.jobs["all-tests-passed"];
const needs: string[] = allTestsPassed.needs || [];
const expectedNeeds = jobs.filter((j) => j !== "all-tests-passed");
const missing = expectedNeeds.filter((j) => !needs.includes(j));
if (missing.length > 0) {
console.error(
`Missing jobs in all-tests-passed needs: ${missing.join(", ")}`,
);
console.info(
"Please add the missing jobs to the needs section of all-tests-passed in test.yml.",
);
process.exit(1);
}
console.log(
"All jobs in test.yml are in the needs section of all-tests-passed.",
);
+49
View File
@@ -0,0 +1,49 @@
# `dist/index.js` is a special file in Actions.
# When you reference an action with `uses:` in a workflow,
# `index.js` is the code that will run.
# For our project, we generate this file through a build process from other source files.
# We need to make sure the checked-in `index.js` actually matches what we expect it to be.
name: Check dist/
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
check-dist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Rebuild the dist/ directory
run: |
npm run build
npm run package
- name: Compare the expected and actual dist/ directories
run: |
if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then
echo "Detected uncommitted changes after build. See status below:"
git diff --text -v
exit 1
fi
id: diff
# If index.js was different than expected, upload the expected version as an artifact
- uses: actions/upload-artifact@v4
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
with:
name: dist
path: dist/
+4 -7
View File
@@ -12,7 +12,6 @@
name: "CodeQL"
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
@@ -39,13 +38,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
source-root: src
@@ -57,7 +54,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/autobuild@v3
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -71,4 +68,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
uses: github/codeql-action/analyze@v3
+2 -5
View File
@@ -3,20 +3,17 @@ name: Release Drafter
# yamllint disable-line rule:truthy
on:
workflow_dispatch:
push:
branches:
- main
workflow_dispatch:
jobs:
update_release_draft:
name: ✏️ Draft release
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- name: 🚀 Run Release Drafter
uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0
uses: release-drafter/release-drafter@v6.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+36 -280
View File
@@ -1,7 +1,7 @@
name: "test"
on:
workflow_dispatch:
pull_request:
workflow_dispatch:
push:
branches:
- main
@@ -10,106 +10,51 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/checkout@v4
- name: Actionlint
uses: eifinger/actionlint-action@447fbfe7533062b7a9ea55f790f2396fba6d052a # v1.10.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: eifinger/actionlint-action@8ebeb40569f3c15ca66fe36d1b75f9b70c6c4f6e # v1.9.0
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: |
npm ci --ignore-scripts
npm install
- run: |
npm run all
- name: Check all jobs are in all-tests-passed.needs
run: |
tsc check-all-tests-passed-needs.ts
node check-all-tests-passed-needs.js
working-directory: .github/scripts
- name: Make sure no changes from linters are detected
run: |
git diff --exit-code || (echo "::error::Please run 'npm run all' to fix the issues" && exit 1)
git diff --exit-code
test-latest-version:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, macos-14, windows-latest]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/checkout@v4
- name: Use latest version
uses: ./
with:
src: __tests__/fixtures/python-project
version: latest
test-specific-version:
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
strategy:
matrix:
ruff-version: ["0.1.7", "0.1.8", "0.4.7", "0.4.10", "0.7", "0.7.x", ">=0.7.0"]
os: [ ubuntu-latest, ubuntu-24.04-arm, macos-latest, macos-14, windows-latest ]
ruff-version: ["0.7.4", "0.7", "0.7.x", ">=0.7.0"]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/checkout@v4
- name: Use version ${{ matrix.ruff-version }}
uses: ./
with:
version: ${{ matrix.ruff-version }}
src: __tests__/fixtures/python-project
test-unsupported-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Try install old version
id: install
continue-on-error: true
uses: ./
with:
version: 0.0.246
src: __tests__/fixtures/python-project
- name: Check if the action failed
run: |
if [ "${INSTALL_OUTCOME}" == "success" ]; then
exit 1
fi
env:
INSTALL_OUTCOME: ${{ steps.install.outcome }}
test-version-from-version-file-pyproject:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Use version from pyproject.toml
id: ruff-action
uses: ./
with:
src: __tests__/fixtures/python-project
version-file: __tests__/fixtures/pyproject.toml
- name: Correct version gets installed
run: |
if [ "$RUFF_VERSION" != "0.9.3" ]; then
exit 1
fi
env:
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
test-default-version-from-pyproject:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/checkout@v4
- name: Use default version from pyproject.toml
id: ruff-action
uses: ./
@@ -117,7 +62,7 @@ jobs:
src: __tests__/fixtures/python-project
- name: Correct version gets installed
run: |
if [ "$RUFF_VERSION" != "0.14.11" ]; then
if [ "$RUFF_VERSION" != "0.6.2" ]; then
exit 1
fi
env:
@@ -125,9 +70,7 @@ jobs:
test-default-version-from-pyproject-dev-group:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/checkout@v4
- name: Use default version from pyproject.toml dev group
id: ruff-action
uses: ./
@@ -141,145 +84,10 @@ jobs:
fi
env:
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
test-default-version-from-pyproject-dependency-groups:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Use default version from pyproject.toml dependency groups
id: ruff-action
uses: ./
with:
src: __tests__/fixtures/pyproject-dependency-groups-project
version-file: __tests__/fixtures/pyproject-dependency-groups-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-default-version-from-pyproject-dependency-groups-with-env-marker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Use default version from pyproject.toml with environment marker
id: ruff-action
uses: ./
with:
src: __tests__/fixtures/python-project
version-file: __tests__/fixtures/pyproject-dependency-groups-with-env-marker/pyproject.toml
- name: Correct version gets installed
run: |
if [ "$RUFF_VERSION" != "0.14.11" ]; then
exit 1
fi
env:
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
test-default-version-from-pyproject-poetry-groups:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Use default version from pyproject.toml poetry group dependencies
id: ruff-action
uses: ./
with:
src: __tests__/fixtures/pyproject-dependency-poetry-grouped-project
version-file: __tests__/fixtures/pyproject-dependency-poetry-grouped-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-default-version-from-pyproject-poetry:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Use default version from pyproject.toml poetry dependencies
id: ruff-action
uses: ./
with:
src: __tests__/fixtures/pyproject-dependency-poetry-project
version-file: __tests__/fixtures/pyproject-dependency-poetry-project/pyproject.toml
- name: Correct version gets installed
run: |
if [ "$RUFF_VERSION" != "0.8.6" ]; then
exit 1
fi
env:
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
test-default-version-from-pyproject-optional-dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Use default version from pyproject.toml optional dependencies
id: ruff-action
uses: ./
with:
src: __tests__/fixtures/pyproject-optional-dependencies-project
version-file: __tests__/fixtures/pyproject-optional-dependencies-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-default-version-from-requirements:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Use default version from requirements.txt
id: ruff-action
uses: ./
with:
src: __tests__/fixtures/python-project
version-file: __tests__/fixtures/requirements.txt
- name: Correct version gets installed
run: |
if [ "$RUFF_VERSION" != "0.9.0" ]; then
exit 1
fi
env:
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
test-default-version-from-requirements-with-hash:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Use default version from requirements-with-hash.txt
id: ruff-action
uses: ./
with:
src: __tests__/fixtures/python-project
version-file: __tests__/fixtures/requirements-with-hash.txt
- name: Correct version gets installed
run: |
if [ "$RUFF_VERSION" != "0.9.0" ]; then
exit 1
fi
env:
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
test-semver-range:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/checkout@v4
- name: Use version 0.7
id: ruff-action
uses: ./
@@ -293,50 +101,31 @@ jobs:
fi
env:
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
test-pep440-version-specifier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Install version 0.9.10
id: ruff-action
uses: ./
with:
version: ">=0.9.9,<0.10.0"
src: __tests__/fixtures/python-project
- name: Correct version gets installed
run: |
if [ "$RUFF_VERSION" != "0.9.10" ]; then
exit 1
fi
env:
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
test-checksum:
runs-on: ${{ matrix.inputs.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
inputs:
- os: ubuntu-latest
os: [ubuntu-latest, macos-latest]
checksum:
["0de731c669b9ece77e799ac3f4a160c30849752714d9775c94cc4cfaf326860c"]
exclude:
- os: macos-latest
checksum: "0de731c669b9ece77e799ac3f4a160c30849752714d9775c94cc4cfaf326860c"
include:
- os: macos-latest
checksum: "af9583bff12afbca5d5670334e0187dd60c4d91bc71317d1b2dde70cb1200ba9"
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/checkout@v4
- name: Checksum matches expected
uses: ./
with:
version: "0.7.4"
checksum: ${{ matrix.inputs.checksum }}
checksum: ${{ matrix.checksum }}
src: __tests__/fixtures/python-project
test-with-explicit-token:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/checkout@v4
- name: Use default version
uses: ./
with:
@@ -348,9 +137,7 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/checkout@v4
- name: Use args
uses: ./
with:
@@ -359,9 +146,7 @@ jobs:
test-failure:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/checkout@v4
- name: Format should fail
id: format-should-fail
continue-on-error: true
@@ -371,11 +156,9 @@ jobs:
src: __tests__/fixtures/malformed-python-project
- name: Check if the action failed
run: |
if [ "${FORMAT_SHOULD_FAIL_OUTCOME}" == "success" ]; then
if [ ${{ steps.format-should-fail.outcome }} == "success" ]; then
exit 1
fi
env:
FORMAT_SHOULD_FAIL_OUTCOME: ${{ steps.format-should-fail.outcome }}
- name: Check should fail
id: check-should-fail
continue-on-error: true
@@ -384,20 +167,16 @@ jobs:
src: __tests__/fixtures/malformed-python-project
- name: Check if the action failed
run: |
if [ "${CHECK_SHOULD_FAIL_OUTCOME}" == "success" ]; then
if [ ${{ steps.check-should-fail.outcome }} == "success" ]; then
exit 1
fi
env:
CHECK_SHOULD_FAIL_OUTCOME: ${{ steps.check-should-fail.outcome }}
test-multiple-src:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/checkout@v4
- name: Use args
uses: ./
with:
@@ -405,35 +184,12 @@ jobs:
src: >-
__tests__/fixtures/python-project/src/python_project/__init__.py
__tests__/fixtures/python-project/src/python_project/hello_world.py
all-tests-passed:
test-exit-zero:
runs-on: ubuntu-latest
needs:
- lint
- test-latest-version
- test-specific-version
- test-unsupported-version
- test-version-from-version-file-pyproject
- test-default-version-from-pyproject
- test-default-version-from-pyproject-dev-group
- test-default-version-from-pyproject-dependency-groups
- test-default-version-from-pyproject-dependency-groups-with-env-marker
- test-default-version-from-pyproject-poetry-groups
- test-default-version-from-pyproject-poetry
- test-default-version-from-pyproject-optional-dependencies
- test-default-version-from-requirements
- test-default-version-from-requirements-with-hash
- test-semver-range
- test-pep440-version-specifier
- test-checksum
- test-with-explicit-token
- test-args
- test-failure
- test-multiple-src
if: always()
steps:
- name: All tests passed
run: |
echo "All jobs passed: ${{ !(contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}"
# shellcheck disable=SC2242
exit ${{ (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) && 1 || 0 }}
- uses: actions/checkout@v4
- name: Debug this
uses: ./
with:
args: "--exit-zero"
src: __tests__/fixtures/python-project
+4 -11
View File
@@ -1,20 +1,13 @@
name: "Update known checksums"
on:
workflow_dispatch:
schedule:
- cron: "0 4 * * *" # Run every day at 4am UTC
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Update known checksums
@@ -22,9 +15,9 @@ jobs:
run:
node dist/update-known-checksums/index.js
src/download/checksum/known-checksums.ts ${{ secrets.GITHUB_TOKEN }}
- run: npm ci --ignore-scripts && npm run all
- run: npm install && npm run all
- name: Create Pull Request
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
with:
commit-message: "chore: update known checksums"
title:
+3 -34
View File
@@ -13,38 +13,7 @@ jobs:
update_major_minor_tags:
name: Make sure major and minor tags are up to date on a patch release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Update Major Minor Tags
run: |
set -x
cd "${GITHUB_WORKSPACE}" || exit
# Set up variables.
TAG="${GITHUB_REF#refs/tags/}" # v1.2.3
MINOR="${TAG%.*}" # v1.2
MAJOR="${MINOR%.*}" # v1
if [ "${GITHUB_REF}" = "${TAG}" ]; then
echo "This workflow is not triggered by tag push: GITHUB_REF=${GITHUB_REF}"
exit 1
fi
MESSAGE="Release ${TAG}"
# Set up Git.
git config user.name "${GITHUB_ACTOR}"
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
# Update MAJOR/MINOR tag
git tag -fa "${MAJOR}" -m "${MESSAGE}"
git tag -fa "${MINOR}" -m "${MESSAGE}"
# Push
git push --force origin "${MINOR}"
git push --force origin "${MAJOR}"
- uses: actions/checkout@v4
- name: Run Update semver
uses: haya14busa/action-update-semver@v1.2.1
-3
View File
@@ -1,3 +0,0 @@
{
"recommendations": ["biomejs.biome"]
}
-16
View File
@@ -1,16 +0,0 @@
{
"editor.codeActionsOnSave": {
"source.action.useSortedAttributes.biome": "explicit",
"source.action.useSortedKeys.biome": "explicit",
"source.fixAll.biome": "explicit"
},
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"explorer.excludeGitIgnore": false,
"search.defaultViewMode": "list",
"search.exclude": {
"**/node_modules": true
},
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "node_modules/typescript/lib"
}
+9 -32
View File
@@ -4,8 +4,8 @@ A GitHub Action to run [ruff](https://github.com/astral-sh/ruff).
This action is commonly used as a pass/fail test to ensure your repository stays
clean, abiding the [rules](https://docs.astral.sh/ruff/rules/) specified in your
configuration. Though it runs `ruff check` by default, the action can do
anything `ruff` can (ex, fix).
configuration. Though it runs `ruff`, the action can do anything `ruff` can (ex,
fix).
## Contents
@@ -16,9 +16,9 @@ anything `ruff` can (ex, fix).
- [Use to install ruff](#use-to-install-ruff)
- [Use `ruff format`](#use-ruff-format)
- [Install specific versions](#install-specific-versions)
- [Install the latest version](#install-the-latest-version)
- [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 or pep440 specifier](#install-a-version-by-supplying-a-semver-range-or-pep440-specifier)
- [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)
@@ -69,34 +69,19 @@ This action adds ruff to the PATH, so you can use it in subsequent steps.
- run: ruff format
```
By default, this action runs `ruff check` after installation.
If you do not want to run any `ruff` command but only install it,
you can use the `args` input to overwrite the default value (`check`):
```yaml
- name: Install ruff without running check or format
uses: astral-sh/ruff-action@v3
with:
args: "--version"
```
### Use `ruff format`
```yaml
- uses: astral-sh/ruff-action@v3
with:
args: "format --check --diff"
args: "format --check"
```
### Install specific versions
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
`project.dependencies`, `project.optional-dependencies`, or `dependency-groups`,
the latest version is installed.
> [!NOTE]
> This action does only support ruff versions v0.0.247 and above.
either `dependencies` or `dependency-groups.dev` the latest version is installed.
#### Install the latest version
@@ -116,10 +101,9 @@ the latest version is installed.
version: "0.4.4"
```
#### Install a version by supplying a semver range or pep440 specifier
#### 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)
or [pep440 specifier](https://peps.python.org/pep-0440/#version-specifiers)
to install the latest version that satisfies the range.
```yaml
@@ -136,23 +120,16 @@ to install the latest version that satisfies the range.
version: "0.4.x"
```
```yaml
- name: Install a pep440-specifier-satisfying version of ruff
uses: astral-sh/ruff-action@v3
with:
version: ">=0.11.10,<0.12.0"
```
#### 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.
Currently `pyproject.toml` is supported.
```yaml
- name: Install a version from a specified version file
uses: astral-sh/ruff-action@v3
with:
version-file: "my-path/to/pyproject.toml-or-requirements.txt"
version-file: "my-path/to/pyproject.toml"
```
### Validate checksum
@@ -1,22 +0,0 @@
[project]
name = "pyproject-dependency-groups-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
[dependency-groups]
dev = [
{ include-group = "docs" },
{ include-group = "lint" },
]
docs = [
"sphinx",
]
lint = [
"ruff==0.8.3",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
@@ -1,2 +0,0 @@
def hello() -> str:
return "Hello from python-project!"
@@ -1,15 +0,0 @@
[project]
name = "pyproject-dependency-groups-with-env-marker"
version = "0.1.0"
description = "Test fixture for issue #256 - environment markers"
readme = "README.md"
requires-python = ">=3.12"
[dependency-groups]
dev = [
"ruff>=0.14 ; python_version >= '3.11'",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
@@ -1,14 +0,0 @@
[project]
name = "pyproject-dependency-poetry-grouped-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
[tool.poetry.group.dev.dependencies]
sphinx = "*"
ruff = "0.8.3"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
@@ -1,2 +0,0 @@
def hello() -> str:
return "Hello from python-project!"
@@ -1,14 +0,0 @@
[project]
name = "pyproject-dependency-poetry-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
[tool.poetry.dependencies]
sphinx = "*"
ruff = "~0.8.2"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
@@ -1,2 +0,0 @@
def hello() -> str:
return "Hello from python-project!"
@@ -1,15 +0,0 @@
[project]
name = "pyproject-optional-dependencies-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
[project.optional-dependencies]
lint = [
"ruff==0.8.3",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
@@ -1,2 +0,0 @@
def hello() -> str:
return "Hello from python-project!"
-13
View File
@@ -1,13 +0,0 @@
[project]
name = "pyython-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"ruff==0.9.3",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
@@ -1,2 +0,0 @@
ruff==0.9.0 \
--hash=sha256:badc0de
-1
View File
@@ -1 +0,0 @@
ruff==0.9.0
+13 -27
View File
@@ -1,35 +1,20 @@
{
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
"assist": {
"actions": {
"source": {
"organizeImports": "on",
"useSortedAttributes": "on",
"useSortedKeys": "on"
}
}
"$schema": "https://biomejs.dev/schemas/1.9.2/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"includes": [
"**",
"!**/coverage",
"!**/dist",
"!**/lib",
"!**/node_modules",
"!**/package*.json",
"!**/known-checksums.*"
]
"ignore": ["dist", "lib", "node_modules"]
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"trailingCommas": "all"
}
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
@@ -37,9 +22,10 @@
"recommended": true
}
},
"vcs": {
"clientKind": "git",
"enabled": true,
"useIgnoreFile": false
"javascript": {
"formatter": {
"quoteStyle": "double",
"trailingCommas": "all"
}
}
}
Generated Vendored
+4451 -5914
View File
File diff suppressed because it is too large Load Diff
+4424 -4977
View File
File diff suppressed because it is too large Load Diff
-12
View File
@@ -1,12 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"],
moduleFileExtensions: ["ts", "js"],
preset: "ts-jest",
roots: ["<rootDir>/src"],
testEnvironment: "node",
testMatch: ["**/*.test.ts"],
transform: {
"^.+\\.ts$": "ts-jest",
},
};
+189 -3972
View File
File diff suppressed because it is too large Load Diff
+12 -23
View File
@@ -6,45 +6,34 @@
"main": "dist/ruff-action/index.js",
"scripts": {
"build": "tsc",
"check": "biome check --write",
"format": "biome format --fix",
"format-check": "biome format",
"lint": "biome lint --fix",
"package": "ncc build -o dist/ruff-action src/ruff-action.ts && ncc build -o dist/update-known-checksums src/update-known-checksums.ts",
"act": "act pull_request -W .github/workflows/test.yml --container-architecture linux/amd64 -s GITHUB_TOKEN=\"$(gh auth token)\"",
"update-known-checksums": "RUNNER_TEMP=known_checksums node dist/update-known-checksums/index.js src/download/checksum/known-checksums.ts \"$(gh auth token)\"",
"test": "jest",
"all": "npm ci --ignore-scripts && npm run build && npm run check && npm run test && npm run package"
"all": "npm run build && npm run format && npm run lint && npm run package"
},
"repository": {
"type": "git",
"url": "git+https://github.com/astral-sh/ruff-action.git"
},
"keywords": [
"actions",
"python",
"ruff",
"action"
],
"keywords": ["actions", "python", "ruff", "action"],
"author": "@eifinger",
"license": "Apache-2.0",
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/tool-cache": "^2.0.2",
"@octokit/core": "^7.0.3",
"@octokit/plugin-paginate-rest": "^13.1.1",
"@octokit/plugin-rest-endpoint-methods": "^16.0.0",
"@renovatebot/pep440": "^4.2.1",
"smol-toml": "^1.4.1"
"@actions/github": "^6.0.0",
"@actions/tool-cache": "^2.0.1",
"smol-toml": "^1.3.1"
},
"devDependencies": {
"@biomejs/biome": "2.1.4",
"@types/jest": "^29.5.14",
"@types/js-yaml": "^4.0.9",
"@types/node": "^24.2.1",
"@types/semver": "^7.7.0",
"@biomejs/biome": "1.9.4",
"@types/node": "^22.10.5",
"@types/semver": "^7.5.8",
"@vercel/ncc": "^0.38.3",
"jest": "^29.7.0",
"js-yaml": "^4.1.0",
"ts-jest": "^29.2.5",
"typescript": "^5.9.2"
"typescript": "^5.6.3"
}
}
+3 -3
View File
@@ -1,9 +1,9 @@
import * as crypto from "node:crypto";
import * as fs from "node:fs";
import * as crypto from "node:crypto";
import * as core from "@actions/core";
import type { Architecture, Platform } from "../../utils/platforms";
import { KNOWN_CHECKSUMS } from "./known-checksums";
import type { Architecture, Platform } from "../../utils/platforms";
export async function validateChecksum(
checkSum: string | undefined,
@@ -12,7 +12,7 @@ export async function validateChecksum(
platform: Platform,
version: string,
): Promise<void> {
let isValid: boolean | undefined;
let isValid: boolean | undefined = undefined;
if (checkSum !== undefined && checkSum !== "") {
isValid = await validateFileCheckSum(downloadPath, checkSum);
} else {
File diff suppressed because it is too large Load Diff
+23 -141
View File
@@ -1,17 +1,11 @@
import { promises as fs } from "node:fs";
import * as path from "node:path";
import * as core from "@actions/core";
import * as tc from "@actions/tool-cache";
import { Octokit } from "@octokit/core";
import { paginateRest } from "@octokit/plugin-paginate-rest";
import { restEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
import * as pep440 from "@renovatebot/pep440";
import * as semver from "semver";
import * as path from "node:path";
import { promises as fs } from "node:fs";
import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants";
import type { Architecture, Platform } from "../utils/platforms";
import { validateChecksum } from "./checksum/checksum";
const PaginatingOctokit = Octokit.plugin(paginateRest, restEndpointMethods);
import * as github from "@actions/github";
export function tryGetFromToolCache(
arch: Architecture,
@@ -25,7 +19,7 @@ export function tryGetFromToolCache(
resolvedVersion = version;
}
const installedPath = tc.find(TOOL_CACHE_NAME, resolvedVersion, arch);
return { installedPath, version: resolvedVersion };
return { version: resolvedVersion, installedPath };
}
export async function downloadVersion(
@@ -40,59 +34,16 @@ export async function downloadVersion(
if (platform === "pc-windows-msvc") {
extension = ".zip";
}
const downloadUrl = constructDownloadUrl(version, platform, arch);
core.debug(`Downloading ruff from "${downloadUrl}" ...`);
const downloadUrl = `https://github.com/${OWNER}/${REPO}/releases/download/${version}/${artifact}${extension}`;
core.info(`Downloading ruff from "${downloadUrl}" ...`);
const downloadPath = await tc.downloadTool(
downloadUrl,
undefined,
githubToken,
);
core.debug(`Downloaded ruff to "${downloadPath}"`);
await validateChecksum(checkSum, downloadPath, arch, platform, version);
const extractedDir = await extractDownloadedArtifact(
version,
downloadPath,
extension,
platform,
artifact,
);
const cachedToolDir = await tc.cacheDir(
extractedDir,
TOOL_CACHE_NAME,
version,
arch,
);
return { cachedToolDir, version: version };
}
function constructDownloadUrl(
version: string,
platform: Platform,
arch: Architecture,
): string {
const artifactVersionSuffix =
semver.lte(version, "v0.4.10") && semver.gte(version, "v0.1.8")
? `-${version}`
: "";
const artifact = `ruff${artifactVersionSuffix}-${arch}-${platform}`;
let extension = ".tar.gz";
if (platform === "pc-windows-msvc") {
extension = ".zip";
}
const versionPrefix = semver.lte(version, "v0.4.10") ? "v" : "";
return `https://github.com/${OWNER}/${REPO}/releases/download/${versionPrefix}${version}/${artifact}${extension}`;
}
async function extractDownloadedArtifact(
version: string,
downloadPath: string,
extension: string,
platform: Platform,
artifact: string,
): Promise<string> {
let ruffDir: string;
if (platform === "pc-windows-msvc") {
const fullPathWithExtension = `${downloadPath}${extension}`;
@@ -100,22 +51,22 @@ async function extractDownloadedArtifact(
ruffDir = await tc.extractZip(fullPathWithExtension);
// On windows extracting the zip does not create an intermediate directory
} else {
ruffDir = await tc.extractTar(downloadPath);
if (semver.gte(version, "v0.5.0")) {
// Since v0.5.0 an intermediate directory is created
ruffDir = path.join(ruffDir, artifact);
}
const extractedDir = await tc.extractTar(downloadPath);
ruffDir = path.join(extractedDir, artifact);
}
const files = await fs.readdir(ruffDir);
core.debug(`Contents of ${ruffDir}: ${files.join(", ")}`);
return ruffDir;
const cachedToolDir = await tc.cacheDir(
ruffDir,
TOOL_CACHE_NAME,
version,
arch,
);
return { version: version, cachedToolDir };
}
export async function resolveVersion(
versionInput: string,
githubToken: string,
): Promise<string> {
core.debug(`Resolving ${versionInput}...`);
const version =
versionInput === "latest"
? await getLatestVersion(githubToken)
@@ -125,102 +76,33 @@ export async function resolveVersion(
return version;
}
const availableVersions = await getAvailableVersions(githubToken);
const resolvedVersion = maxSatisfying(availableVersions, version);
if (resolvedVersion === undefined) {
const resolvedVersion = tc.evaluateVersions(availableVersions, version);
if (resolvedVersion === "") {
throw new Error(`No version found for ${version}`);
}
core.debug(`Resolved version: ${resolvedVersion}`);
return resolvedVersion;
}
async function getAvailableVersions(githubToken: string): Promise<string[]> {
try {
const octokit = new PaginatingOctokit({
auth: githubToken,
});
return await getReleaseTagNames(octokit);
} catch (err) {
if ((err as Error).message.includes("Bad credentials")) {
core.info(
"No (valid) GitHub token provided. Falling back to anonymous. Requests might be rate limited.",
);
const octokit = new PaginatingOctokit();
return await getReleaseTagNames(octokit);
}
throw err;
}
}
const octokit = github.getOctokit(githubToken);
async function getReleaseTagNames(
octokit: InstanceType<typeof PaginatingOctokit>,
): Promise<string[]> {
const response = await octokit.paginate(octokit.rest.repos.listReleases, {
owner: OWNER,
repo: REPO,
});
const releaseTagNames = response.map((release) => release.tag_name);
if (releaseTagNames.length === 0) {
throw Error(
"Github API request failed while getting releases. Check the GitHub status page for outages. Try again later.",
);
}
return response.map((release) => release.tag_name);
}
async function getLatestVersion(githubToken: string) {
const octokit = new PaginatingOctokit({
auth: githubToken,
});
const octokit = github.getOctokit(githubToken);
let latestRelease: { tag_name: string } | undefined;
try {
latestRelease = await getLatestRelease(octokit);
} catch (err) {
if ((err as Error).message.includes("Bad credentials")) {
core.info(
"No (valid) GitHub token provided. Falling back to anonymous. Requests might be rate limited.",
);
const octokit = new PaginatingOctokit();
latestRelease = await getLatestRelease(octokit);
} else {
core.error(
"Github API request failed while getting latest release. Check the GitHub status page for outages. Try again later.",
);
throw err;
}
}
const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({
owner: OWNER,
repo: REPO,
});
if (!latestRelease) {
throw new Error("Could not determine latest release.");
}
return latestRelease.tag_name;
}
async function getLatestRelease(
octokit: InstanceType<typeof PaginatingOctokit>,
) {
const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({
owner: OWNER,
repo: REPO,
});
return latestRelease;
}
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;
}
+14 -27
View File
@@ -1,13 +1,18 @@
import * as fs from "node:fs";
import * as path from "node:path";
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import * as semver from "semver";
import * as path from "node:path";
import {
downloadVersion,
resolveVersion,
tryGetFromToolCache,
} from "./download/download-version";
import {
type Architecture,
getArch,
getPlatform,
type Platform,
} from "./utils/platforms";
import {
args,
checkSum,
@@ -16,13 +21,8 @@ import {
version,
versionFile as versionFileInput,
} from "./utils/inputs";
import {
type Architecture,
getArch,
getPlatform,
type Platform,
} from "./utils/platforms";
import { getRuffVersionFromRequirementsFile } from "./utils/pyproject";
import { getRuffVersionFromPyproject } from "./utils/pyproject";
import * as fs from "node:fs";
async function run(): Promise<void> {
const platform = getPlatform();
@@ -62,11 +62,6 @@ async function setupRuff(
githubToken: string,
): Promise<{ ruffDir: string; version: string }> {
const resolvedVersion = await determineVersion();
if (semver.lt(resolvedVersion, "v0.0.247")) {
throw Error(
"This action does not support ruff versions older than 0.0.247",
);
}
const toolCacheResult = tryGetFromToolCache(arch, resolvedVersion);
if (toolCacheResult.installedPath) {
core.info(`Found ruffDir in tool-cache for ${toolCacheResult.version}`);
@@ -98,27 +93,19 @@ async function determineVersion(): Promise<string> {
return await resolveVersion(version, githubToken);
}
if (versionFileInput !== "") {
const versionFromPyproject =
getRuffVersionFromRequirementsFile(versionFileInput);
const versionFromPyproject = getRuffVersionFromPyproject(versionFileInput);
if (versionFromPyproject === undefined) {
core.warning(
`Could not parse version from ${versionFileInput}. Using latest version.`,
"Could not parse version from supplied pyproject.toml. Using latest version.",
);
return await resolveVersion("latest", githubToken);
}
return await resolveVersion(versionFromPyproject || "latest", githubToken);
}
const pyProjectPath = path.join(src, "pyproject.toml");
if (!fs.existsSync(pyProjectPath)) {
core.info(`Could not find ${pyProjectPath}. Using latest version.`);
return await resolveVersion("latest", githubToken);
}
const versionFromPyproject =
getRuffVersionFromRequirementsFile(pyProjectPath);
if (versionFromPyproject === undefined) {
core.info(
`Could not parse version from ${pyProjectPath}. Using latest version.`,
);
}
const versionFromPyproject = getRuffVersionFromPyproject(pyProjectPath);
return await resolveVersion(versionFromPyproject || "latest", githubToken);
}
+6 -8
View File
@@ -1,18 +1,16 @@
import * as github from "@actions/github";
import * as core from "@actions/core";
import { Octokit } from "@octokit/core";
import { paginateRest } from "@octokit/plugin-paginate-rest";
import { restEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
import * as semver from "semver";
import { updateChecksums } from "./download/checksum/update-known-checksums";
import { OWNER, REPO } from "./utils/constants";
const PaginatingOctokit = Octokit.plugin(paginateRest, restEndpointMethods);
import { OWNER, REPO } from "./utils/constants";
import * as semver from "semver";
import { updateChecksums } from "./download/checksum/update-known-checksums";
async function run(): Promise<void> {
const checksumFilePath = process.argv.slice(2)[0];
const github_token = process.argv.slice(2)[1];
const octokit = new PaginatingOctokit({ auth: github_token });
const octokit = github.getOctokit(github_token);
const response = await octokit.paginate(octokit.rest.repos.listReleases, {
owner: OWNER,
+2 -2
View File
@@ -9,9 +9,9 @@ export type Architecture = "i686" | "x86_64" | "aarch64";
export function getArch(): Architecture | undefined {
const arch = process.arch;
const archMapping: { [key: string]: Architecture } = {
arm64: "aarch64",
ia32: "i686",
x64: "x86_64",
arm64: "aarch64",
};
if (arch in archMapping) {
@@ -22,8 +22,8 @@ export function getArch(): Architecture | undefined {
export function getPlatform(): Platform | undefined {
const platform = process.platform;
const platformMapping: { [key: string]: Platform } = {
darwin: "apple-darwin",
linux: "unknown-linux-gnu",
darwin: "apple-darwin",
win32: "pc-windows-msvc",
};
-188
View File
@@ -1,188 +0,0 @@
import * as core from "@actions/core";
import { findRuffVersionInSpec } from "./pyproject";
jest.mock("@actions/core", () => ({
info: jest.fn(),
warning: jest.fn(),
}));
describe("findRuffVersionInSpec", () => {
beforeEach(() => {
jest.clearAllMocks();
});
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(core.info).toHaveBeenCalledWith(
"Found ruff version in requirements file: 0.9.3",
);
expect(core.warning).not.toHaveBeenCalled();
});
it("should extract version from 'ruff>=0.14'", () => {
const result = findRuffVersionInSpec("ruff>=0.14");
expect(result).toBe(">=0.14");
expect(core.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(core.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(core.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(core.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(core.info).not.toHaveBeenCalled();
expect(core.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(core.info).not.toHaveBeenCalled();
expect(core.warning).not.toHaveBeenCalled();
});
it("should strip trailing backslash", () => {
const result = findRuffVersionInSpec("ruff==0.9.3 \\");
expect(result).toBe("0.9.3");
expect(core.info).toHaveBeenCalledWith(
"Found ruff version in requirements file: 0.9.3",
);
expect(core.warning).not.toHaveBeenCalled();
});
it("should strip trailing backslash with whitespace", () => {
const result = findRuffVersionInSpec(" ruff==0.9.3 \\ ");
expect(result).toBe("0.9.3");
expect(core.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(core.info).toHaveBeenCalledWith(
"Found ruff version in requirements file: >=0.14",
);
expect(core.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(core.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(core.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(core.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(core.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(core.warning).toHaveBeenCalled();
});
it("should return undefined for empty string", () => {
const result = findRuffVersionInSpec("");
expect(result).toBeUndefined();
expect(core.info).not.toHaveBeenCalled();
expect(core.warning).not.toHaveBeenCalled();
});
it("should return undefined for whitespace only", () => {
const result = findRuffVersionInSpec(" ");
expect(result).toBeUndefined();
expect(core.info).not.toHaveBeenCalled();
expect(core.warning).not.toHaveBeenCalled();
});
it("should return undefined for just semicolon", () => {
const result = findRuffVersionInSpec(";");
expect(result).toBeUndefined();
expect(core.info).not.toHaveBeenCalled();
expect(core.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(core.info).toHaveBeenCalledWith(
"Found ruff version in requirements file: >=0.14",
);
expect(core.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(core.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(core.warning).toHaveBeenCalled();
});
});
});
+28 -112
View File
@@ -2,114 +2,7 @@ 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(
export function getRuffVersionFromPyproject(
filePath: string,
): string | undefined {
if (!fs.existsSync(filePath)) {
@@ -117,14 +10,37 @@ export function getRuffVersionFromRequirementsFile(
return undefined;
}
const pyprojectContent = fs.readFileSync(filePath, "utf-8");
if (filePath.endsWith(".txt")) {
return getRuffVersionFromAllDependencies(pyprojectContent.split("\n"));
}
let pyproject:
| {
project?: { dependencies?: string[] };
"dependency-groups"?: { dev?: string[] };
}
| undefined;
try {
return parsePyproject(pyprojectContent);
pyproject = toml.parse(pyprojectContent);
} catch (err) {
const message = (err as Error).message;
core.warning(`Error while parsing ${filePath}: ${message}`);
return undefined;
}
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;
}
+3 -3
View File
@@ -1,12 +1,12 @@
{
"compilerOptions": {
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"target": "ES2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"outDir": "./lib" /* Redirect output structure to the directory. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
"strict": true /* Enable all strict type-checking options. */,
"target": "ES2022" /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
},
"exclude": ["node_modules", "**/*.test.ts"]
}