name: CI

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/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
      - name: "Install Rust toolchain"
        run: |
          rustup component add clippy
          rustup target add wasm32-unknown-unknown
      - uses: Swatinem/rust-cache@v2
      - 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: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - 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
      - uses: Swatinem/rust-cache@v2
      - 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: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - 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
      - uses: Swatinem/rust-cache@v2
      - 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: windows-latest-xlarge
    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
      - name: "Install Rust toolchain"
        run: rustup show
      - name: "Install cargo nextest"
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-nextest
      - uses: Swatinem/rust-cache@v2
      - name: "Run tests"
        shell: bash
        env:
          # Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
          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: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - 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
      - uses: Swatinem/rust-cache@v2
      - 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
      - name: "Install Rust toolchain"
        run: rustup show
      - name: "Install mold"
        uses: rui314/setup-mold@v1
      - uses: Swatinem/rust-cache@v2
      - name: "Build"
        run: cargo build --release --locked

  cargo-build-msrv:
    name: "cargo build (msrv)"
    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: SebRollen/toml-action@v1.2.0
        id: msrv
        with:
          file: "Cargo.toml"
          field: "workspace.package.rust-version"
      - 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
      - uses: Swatinem/rust-cache@v2
      - 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
      - name: "Install Rust toolchain"
        run: rustup show
      - uses: Swatinem/rust-cache@v2
        with:
          workspaces: "fuzz -> target"
      - 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: ${{ 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: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - name: "Install Rust toolchain"
        run: rustup component add rustfmt
      - uses: Swatinem/rust-cache@v2
      - 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: ${{ 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@v7
        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
    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 }}
      - name: "Install Rust toolchain"
        run: rustup show
      - uses: Swatinem/rust-cache@v2
      - 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"
      - 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
      - uses: Swatinem/rust-cache@v2
      - 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: needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main'
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - name: "Install Rust toolchain"
        run: rustup show
      - name: "Cache rust"
        uses: Swatinem/rust-cache@v2
      - 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: ${{ 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' && (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

      - name: "Install Rust toolchain"
        run: rustup show

      - name: "Install codspeed"
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-codspeed

      - uses: Swatinem/rust-cache@v2

      - 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 }}
