name: CI permissions: {} on: push: branches: [main] pull_request: workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always RUSTUP_MAX_RETRIES: 10 PACKAGE_NAME: ruff PYTHON_VERSION: "3.12" jobs: determine_changes: name: "Determine changes" runs-on: ubuntu-latest outputs: # Flag that is raised when any code that affects parser is changed parser: ${{ steps.changed.outputs.parser_any_changed }} # Flag that is raised when any code that affects linter is changed linter: ${{ steps.changed.outputs.linter_any_changed }} # Flag that is raised when any code that affects formatter is changed formatter: ${{ steps.changed.outputs.formatter_any_changed }} # Flag that is raised when any code is changed # This is superset of the linter and formatter code: ${{ steps.changed.outputs.code_any_changed }} # Flag that is raised when any code that affects the fuzzer is changed fuzz: ${{ steps.changed.outputs.fuzz_any_changed }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false - uses: tj-actions/changed-files@v45 id: changed with: files_yaml: | parser: - Cargo.toml - Cargo.lock - crates/ruff_python_trivia/** - crates/ruff_source_file/** - crates/ruff_text_size/** - crates/ruff_python_ast/** - crates/ruff_python_parser/** - python/py-fuzzer/** - .github/workflows/ci.yaml linter: - Cargo.toml - Cargo.lock - crates/** - "!crates/red_knot*/**" - "!crates/ruff_python_formatter/**" - "!crates/ruff_formatter/**" - "!crates/ruff_dev/**" - scripts/* - python/** - .github/workflows/ci.yaml formatter: - Cargo.toml - Cargo.lock - crates/ruff_python_formatter/** - crates/ruff_formatter/** - crates/ruff_python_trivia/** - crates/ruff_python_ast/** - crates/ruff_source_file/** - crates/ruff_python_index/** - crates/ruff_text_size/** - crates/ruff_python_parser/** - crates/ruff_dev/** - scripts/* - python/** - .github/workflows/ci.yaml fuzz: - fuzz/Cargo.toml - fuzz/Cargo.lock - fuzz/fuzz_targets/** code: - "**/*" - "!**/*.md" - "crates/red_knot_python_semantic/resources/mdtest/**/*.md" - "!docs/**" - "!assets/**" cargo-fmt: name: "cargo fmt" runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 with: persist-credentials: false - name: "Install Rust toolchain" run: rustup component add rustfmt - run: cargo fmt --all --check cargo-clippy: name: "cargo clippy" runs-on: ubuntu-latest needs: determine_changes if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }} timeout-minutes: 20 steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: "Install Rust toolchain" run: | rustup component add clippy rustup target add wasm32-unknown-unknown - name: "Clippy" run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - name: "Clippy (wasm)" run: cargo clippy -p ruff_wasm -p red_knot_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings cargo-test-linux: name: "cargo test (linux)" runs-on: depot-ubuntu-22.04-16 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 20 steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: "Install Rust toolchain" run: rustup show - name: "Install mold" uses: rui314/setup-mold@v1 - name: "Install cargo nextest" uses: taiki-e/install-action@v2 with: tool: cargo-nextest - name: "Install cargo insta" uses: taiki-e/install-action@v2 with: tool: cargo-insta - name: "Run tests" shell: bash env: NEXTEST_PROFILE: "ci" run: cargo insta test --all-features --unreferenced reject --test-runner nextest # Check for broken links in the documentation. - run: cargo doc --all --no-deps env: RUSTDOCFLAGS: "-D warnings" # Use --document-private-items so that all our doc comments are kept in # sync, not just public items. Eventually we should do this for all # crates; for now add crates here as they are warning-clean to prevent # regression. - run: cargo doc --no-deps -p red_knot_python_semantic -p red_knot -p red_knot_test -p ruff_db --document-private-items env: # Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025). RUSTDOCFLAGS: "-D warnings" - uses: actions/upload-artifact@v4 with: name: ruff path: target/debug/ruff cargo-test-linux-release: name: "cargo test (linux, release)" runs-on: depot-ubuntu-22.04-16 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 20 steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: "Install Rust toolchain" run: rustup show - name: "Install mold" uses: rui314/setup-mold@v1 - name: "Install cargo nextest" uses: taiki-e/install-action@v2 with: tool: cargo-nextest - name: "Install cargo insta" uses: taiki-e/install-action@v2 with: tool: cargo-insta - name: "Run tests" shell: bash env: NEXTEST_PROFILE: "ci" run: cargo insta test --release --all-features --unreferenced reject --test-runner nextest cargo-test-windows: name: "cargo test (windows)" runs-on: github-windows-2025-x86_64-16 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 20 steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: "Install Rust toolchain" run: rustup show - name: "Install cargo nextest" uses: taiki-e/install-action@v2 with: tool: cargo-nextest - name: "Run tests" shell: bash env: NEXTEST_PROFILE: "ci" # Workaround for . RUSTUP_WINDOWS_PATH_ADD_BIN: 1 run: | cargo nextest run --all-features --profile ci cargo test --all-features --doc cargo-test-wasm: name: "cargo test (wasm)" runs-on: ubuntu-latest needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 10 steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: "Install Rust toolchain" run: rustup target add wasm32-unknown-unknown - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" cache-dependency-path: playground/package-lock.json - uses: jetli/wasm-pack-action@v0.4.0 with: version: v0.13.1 - name: "Test ruff_wasm" run: | cd crates/ruff_wasm wasm-pack test --node - name: "Test red_knot_wasm" run: | cd crates/red_knot_wasm wasm-pack test --node cargo-build-release: name: "cargo build (release)" runs-on: macos-latest if: ${{ github.ref == 'refs/heads/main' }} timeout-minutes: 20 steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: "Install Rust toolchain" run: rustup show - name: "Install mold" uses: rui314/setup-mold@v1 - name: "Build" run: cargo build --release --locked cargo-build-msrv: name: "cargo build (msrv)" runs-on: depot-ubuntu-latest-8 needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 20 steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: SebRollen/toml-action@v1.2.0 id: msrv with: file: "Cargo.toml" field: "workspace.package.rust-version" - uses: Swatinem/rust-cache@v2 - name: "Install Rust toolchain" env: MSRV: ${{ steps.msrv.outputs.value }} run: rustup default "${MSRV}" - name: "Install mold" uses: rui314/setup-mold@v1 - name: "Install cargo nextest" uses: taiki-e/install-action@v2 with: tool: cargo-nextest - name: "Install cargo insta" uses: taiki-e/install-action@v2 with: tool: cargo-insta - name: "Run tests" shell: bash env: NEXTEST_PROFILE: "ci" MSRV: ${{ steps.msrv.outputs.value }} run: cargo "+${MSRV}" insta test --all-features --unreferenced reject --test-runner nextest cargo-fuzz-build: name: "cargo fuzz build" runs-on: ubuntu-latest needs: determine_changes if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' || needs.determine_changes.outputs.code == 'true' }} timeout-minutes: 10 steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 with: workspaces: "fuzz -> target" - name: "Install Rust toolchain" run: rustup show - name: "Install cargo-binstall" uses: cargo-bins/cargo-binstall@main with: tool: cargo-fuzz@0.11.2 - name: "Install cargo-fuzz" # Download the latest version from quick install and not the github releases because github releases only has MUSL targets. run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm - run: cargo fuzz build -s none fuzz-parser: name: "fuzz parser" runs-on: ubuntu-latest needs: - cargo-test-linux - determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && needs.determine_changes.outputs.parser == 'true' }} timeout-minutes: 20 env: FORCE_COLOR: 1 steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: astral-sh/setup-uv@v5 - uses: actions/download-artifact@v4 name: Download Ruff binary to test id: download-cached-binary with: name: ruff path: ruff-to-test - name: Fuzz env: DOWNLOAD_PATH: ${{ steps.download-cached-binary.outputs.download-path }} run: | # Make executable, since artifact download doesn't preserve this chmod +x "${DOWNLOAD_PATH}/ruff" ( uvx \ --python="${PYTHON_VERSION}" \ --from=./python/py-fuzzer \ fuzz \ --test-executable="${DOWNLOAD_PATH}/ruff" \ --bin=ruff \ 0-500 ) scripts: name: "test scripts" runs-on: ubuntu-latest needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 5 steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: "Install Rust toolchain" run: rustup component add rustfmt # Run all code generation scripts, and verify that the current output is # already checked into git. - run: python crates/ruff_python_ast/generate.py - run: python crates/ruff_python_formatter/generate.py - run: test -z "$(git status --porcelain)" # Verify that adding a plugin or rule produces clean code. - run: ./scripts/add_rule.py --name DoTheThing --prefix F --code 999 --linter pyflakes - run: cargo check - run: cargo fmt --all --check - run: | ./scripts/add_plugin.py test --url https://pypi.org/project/-test/0.1.0/ --prefix TST ./scripts/add_rule.py --name FirstRule --prefix TST --code 001 --linter test - run: cargo check - run: cargo fmt --all --check ecosystem: name: "ecosystem" runs-on: depot-ubuntu-latest-8 needs: - cargo-test-linux - determine_changes # Only runs on pull requests, since that is the only we way we can find the base version for comparison. # Ecosystem check needs linter and/or formatter changes. if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.code == 'true' }} timeout-minutes: 20 steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - uses: actions/download-artifact@v4 name: Download comparison Ruff binary id: ruff-target with: name: ruff path: target/debug - uses: dawidd6/action-download-artifact@v8 name: Download baseline Ruff binary with: name: ruff branch: ${{ github.event.pull_request.base.ref }} workflow: "ci.yaml" check_artifacts: true - name: Install ruff-ecosystem run: | pip install ./python/ruff-ecosystem - name: Run `ruff check` stable ecosystem check if: ${{ needs.determine_changes.outputs.linter == 'true' }} env: DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }} run: | # Make executable, since artifact download doesn't preserve this chmod +x ./ruff "${DOWNLOAD_PATH}/ruff" # Set pipefail to avoid hiding errors with tee set -eo pipefail ruff-ecosystem check ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown | tee ecosystem-result-check-stable cat ecosystem-result-check-stable > "$GITHUB_STEP_SUMMARY" echo "### Linter (stable)" > ecosystem-result cat ecosystem-result-check-stable >> ecosystem-result echo "" >> ecosystem-result - name: Run `ruff check` preview ecosystem check if: ${{ needs.determine_changes.outputs.linter == 'true' }} env: DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }} run: | # Make executable, since artifact download doesn't preserve this chmod +x ./ruff "${DOWNLOAD_PATH}/ruff" # Set pipefail to avoid hiding errors with tee set -eo pipefail ruff-ecosystem check ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-check-preview cat ecosystem-result-check-preview > "$GITHUB_STEP_SUMMARY" echo "### Linter (preview)" >> ecosystem-result cat ecosystem-result-check-preview >> ecosystem-result echo "" >> ecosystem-result - name: Run `ruff format` stable ecosystem check if: ${{ needs.determine_changes.outputs.formatter == 'true' }} env: DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }} run: | # Make executable, since artifact download doesn't preserve this chmod +x ./ruff "${DOWNLOAD_PATH}/ruff" # Set pipefail to avoid hiding errors with tee set -eo pipefail ruff-ecosystem format ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown | tee ecosystem-result-format-stable cat ecosystem-result-format-stable > "$GITHUB_STEP_SUMMARY" echo "### Formatter (stable)" >> ecosystem-result cat ecosystem-result-format-stable >> ecosystem-result echo "" >> ecosystem-result - name: Run `ruff format` preview ecosystem check if: ${{ needs.determine_changes.outputs.formatter == 'true' }} env: DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }} run: | # Make executable, since artifact download doesn't preserve this chmod +x ./ruff "${DOWNLOAD_PATH}/ruff" # Set pipefail to avoid hiding errors with tee set -eo pipefail ruff-ecosystem format ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-format-preview cat ecosystem-result-format-preview > "$GITHUB_STEP_SUMMARY" echo "### Formatter (preview)" >> ecosystem-result cat ecosystem-result-format-preview >> ecosystem-result echo "" >> ecosystem-result - name: Export pull request number run: | echo ${{ github.event.number }} > pr-number - uses: actions/upload-artifact@v4 name: Upload PR Number with: name: pr-number path: pr-number - uses: actions/upload-artifact@v4 name: Upload Results with: name: ecosystem-result path: ecosystem-result cargo-shear: name: "cargo shear" runs-on: ubuntu-latest needs: determine_changes if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }} steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: cargo-bins/cargo-binstall@main - run: cargo binstall --no-confirm cargo-shear - run: cargo shear python-package: name: "python package" runs-on: ubuntu-latest timeout-minutes: 20 if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') }} steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} architecture: x64 - uses: Swatinem/rust-cache@v2 - name: "Prep README.md" run: python scripts/transform_readme.py --target pypi - name: "Build wheels" uses: PyO3/maturin-action@v1 with: args: --out dist - name: "Test wheel" run: | pip install --force-reinstall --find-links dist "${PACKAGE_NAME}" ruff --help python -m ruff --help - name: "Remove wheels from cache" run: rm -rf target/wheels pre-commit: name: "pre-commit" runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - uses: Swatinem/rust-cache@v2 - name: "Install Rust toolchain" run: rustup show - name: "Install pre-commit" run: pip install pre-commit - name: "Cache pre-commit" uses: actions/cache@v4 with: path: ~/.cache/pre-commit key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: "Run pre-commit" run: | echo '```console' > "$GITHUB_STEP_SUMMARY" # Enable color output for pre-commit and remove it for the summary # Use --hook-stage=manual to enable slower pre-commit hooks that are skipped by default SKIP=cargo-fmt,clippy,dev-generate-all pre-commit run --all-files --show-diff-on-failure --color=always --hook-stage=manual | \ tee >(sed -E 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[mGK]//g' >> "$GITHUB_STEP_SUMMARY") >&1 exit_code="${PIPESTATUS[0]}" echo '```' >> "$GITHUB_STEP_SUMMARY" exit "$exit_code" docs: name: "mkdocs" runs-on: ubuntu-latest timeout-minutes: 10 env: MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }} steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: actions/setup-python@v5 with: python-version: "3.13" - uses: Swatinem/rust-cache@v2 - name: "Add SSH key" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} uses: webfactory/ssh-agent@v0.9.0 with: ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }} - name: "Install Rust toolchain" run: rustup show - name: Install uv uses: astral-sh/setup-uv@v5 - name: "Install Insiders dependencies" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} run: uv pip install -r docs/requirements-insiders.txt --system - name: "Install dependencies" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} run: uv pip install -r docs/requirements.txt --system - name: "Update README File" run: python scripts/transform_readme.py --target mkdocs - name: "Generate docs" run: python scripts/generate_mkdocs.py - name: "Check docs formatting" run: python scripts/check_docs_formatted.py - name: "Build Insiders docs" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }} run: mkdocs build --strict -f mkdocs.insiders.yml - name: "Build docs" if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }} run: mkdocs build --strict -f mkdocs.public.yml check-formatter-instability-and-black-similarity: name: "formatter instabilities and black similarity" runs-on: ubuntu-latest needs: determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 10 steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: "Install Rust toolchain" run: rustup show - name: "Run checks" run: scripts/formatter_ecosystem_checks.sh - name: "Github step summary" run: cat target/formatter-ecosystem/stats.txt > "$GITHUB_STEP_SUMMARY" - name: "Remove checkouts from cache" run: rm -r target/formatter-ecosystem check-ruff-lsp: name: "test ruff-lsp" runs-on: ubuntu-latest timeout-minutes: 5 needs: - cargo-test-linux - determine_changes if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} steps: - uses: extractions/setup-just@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: actions/checkout@v4 name: "Download ruff-lsp source" with: persist-credentials: false repository: "astral-sh/ruff-lsp" - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - uses: actions/download-artifact@v4 name: Download development ruff binary id: ruff-target with: name: ruff path: target/debug - name: Install ruff-lsp dependencies run: | just install - name: Run ruff-lsp tests env: DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }} run: | # Setup development binary pip uninstall --yes ruff chmod +x "${DOWNLOAD_PATH}/ruff" export PATH="${DOWNLOAD_PATH}:${PATH}" ruff version just test benchmarks: runs-on: ubuntu-22.04 needs: determine_changes if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }} timeout-minutes: 20 steps: - name: "Checkout Branch" uses: actions/checkout@v4 with: persist-credentials: false - uses: Swatinem/rust-cache@v2 - name: "Install Rust toolchain" run: rustup show - name: "Install codspeed" uses: taiki-e/install-action@v2 with: tool: cargo-codspeed - name: "Build benchmarks" run: cargo codspeed build --features codspeed -p ruff_benchmark - name: "Run benchmarks" uses: CodSpeedHQ/action@v3 with: run: cargo codspeed run token: ${{ secrets.CODSPEED_TOKEN }}