name: "[ruff] Release"

on:
  workflow_dispatch:
    inputs:
      tag:
        description: "The version to tag, without the leading 'v'. If omitted, will initiate a dry run (no uploads)."
        type: string
      sha:
        description: "The full sha of the commit to be released. If omitted, the latest commit on the default branch will be used."
        default: ""
        type: string
  pull_request:
    paths:
      # When we change pyproject.toml, we want to ensure that the maturin builds still work
      - pyproject.toml
      # And when we change this workflow itself...
      - .github/workflows/release.yaml

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  PACKAGE_NAME: ruff
  PYTHON_VERSION: "3.11"
  CARGO_INCREMENTAL: 0
  CARGO_NET_RETRY: 10
  CARGO_TERM_COLOR: always
  RUSTUP_MAX_RETRIES: 10

jobs:
  sdist:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.sha }}
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
      - name: "Prep README.md"
        run: python scripts/transform_readme.py --target pypi
      - name: "Build sdist"
        uses: PyO3/maturin-action@v1
        with:
          command: sdist
          args: --out dist
      - name: "Test sdist"
        run: |
          pip install dist/${{ env.PACKAGE_NAME }}-*.tar.gz --force-reinstall
          ruff --help
          python -m ruff --help
      - name: "Upload sdist"
        uses: actions/upload-artifact@v4
        with:
          name: wheels-sdist
          path: dist

  macos-x86_64:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.sha }}
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          architecture: x64
      - name: "Prep README.md"
        run: python scripts/transform_readme.py --target pypi
      - name: "Build wheels - x86_64"
        uses: PyO3/maturin-action@v1
        with:
          target: x86_64
          args: --release --locked --out dist
      - name: "Test wheel - x86_64"
        run: |
          pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
          ruff --help
          python -m ruff --help
      - name: "Upload wheels"
        uses: actions/upload-artifact@v4
        with:
          name: wheels-macos-x86_64
          path: dist
      - name: "Archive binary"
        run: |
          ARCHIVE_FILE=ruff-${{ inputs.tag }}-x86_64-apple-darwin.tar.gz
          tar czvf $ARCHIVE_FILE -C target/x86_64-apple-darwin/release ruff
          shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: binaries-macos-x86_64
          path: |
            *.tar.gz
            *.sha256

  macos-universal:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.sha }}
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          architecture: x64
      - name: "Prep README.md"
        run: python scripts/transform_readme.py --target pypi
      - name: "Build wheels - universal2"
        uses: PyO3/maturin-action@v1
        with:
          args: --release --locked --target universal2-apple-darwin --out dist
      - name: "Test wheel - universal2"
        run: |
          pip install dist/${{ env.PACKAGE_NAME }}-*universal2.whl --force-reinstall
          ruff --help
          python -m ruff --help
      - name: "Upload wheels"
        uses: actions/upload-artifact@v4
        with:
          name: wheels-aarch64-apple-darwin
          path: dist
      - name: "Archive binary"
        run: |
          ARCHIVE_FILE=ruff-${{ inputs.tag }}-aarch64-apple-darwin.tar.gz
          tar czvf $ARCHIVE_FILE -C target/aarch64-apple-darwin/release ruff
          shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: binaries-aarch64-apple-darwin
          path: |
            *.tar.gz
            *.sha256

  windows:
    runs-on: windows-latest
    strategy:
      matrix:
        platform:
          - target: x86_64-pc-windows-msvc
            arch: x64
          - target: i686-pc-windows-msvc
            arch: x86
          - target: aarch64-pc-windows-msvc
            arch: x64
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.sha }}
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          architecture: ${{ matrix.platform.arch }}
      - name: "Prep README.md"
        run: python scripts/transform_readme.py --target pypi
      - name: "Build wheels"
        uses: PyO3/maturin-action@v1
        with:
          target: ${{ matrix.platform.target }}
          args: --release --locked --out dist
      - name: "Test wheel"
        if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
        shell: bash
        run: |
          python -m pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
          ruff --help
          python -m ruff --help
      - name: "Upload wheels"
        uses: actions/upload-artifact@v4
        with:
          name: wheels-${{ matrix.platform.target }}
          path: dist
      - name: "Archive binary"
        shell: bash
        run: |
          ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.zip
          7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
          sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: binaries-${{ matrix.platform.target }}
          path: |
            *.zip
            *.sha256

  linux:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target:
          - x86_64-unknown-linux-gnu
          - i686-unknown-linux-gnu
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.sha }}
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          architecture: x64
      - name: "Prep README.md"
        run: python scripts/transform_readme.py --target pypi
      - name: "Build wheels"
        uses: PyO3/maturin-action@v1
        with:
          target: ${{ matrix.target }}
          manylinux: auto
          args: --release --locked --out dist
      - name: "Test wheel"
        if: ${{ startsWith(matrix.target, 'x86_64') }}
        run: |
          pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
          ruff --help
          python -m ruff --help
      - name: "Upload wheels"
        uses: actions/upload-artifact@v4
        with:
          name: wheels-${{ matrix.target }}
          path: dist
      - name: "Archive binary"
        run: |
          ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.target }}.tar.gz
          tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
          shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: binaries-${{ matrix.target }}
          path: |
            *.tar.gz
            *.sha256

  linux-cross:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        platform:
          - target: aarch64-unknown-linux-gnu
            arch: aarch64
            # see https://github.com/astral-sh/ruff/issues/3791
            # and https://github.com/gnzlbg/jemallocator/issues/170#issuecomment-1503228963
            maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
          - target: armv7-unknown-linux-gnueabihf
            arch: armv7
          - target: s390x-unknown-linux-gnu
            arch: s390x
          - target: powerpc64le-unknown-linux-gnu
            arch: ppc64le
            # see https://github.com/astral-sh/ruff/issues/10073
            maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
          - target: powerpc64-unknown-linux-gnu
            arch: ppc64
            # see https://github.com/astral-sh/ruff/issues/10073
            maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16

    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.sha }}
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
      - name: "Prep README.md"
        run: python scripts/transform_readme.py --target pypi
      - name: "Build wheels"
        uses: PyO3/maturin-action@v1
        with:
          target: ${{ matrix.platform.target }}
          manylinux: auto
          docker-options: ${{ matrix.platform.maturin_docker_options }}
          args: --release --locked --out dist
      - uses: uraimo/run-on-arch-action@v2
        if: matrix.platform.arch != 'ppc64'
        name: Test wheel
        with:
          arch: ${{ matrix.platform.arch }}
          distro: ubuntu20.04
          githubToken: ${{ github.token }}
          install: |
            apt-get update
            apt-get install -y --no-install-recommends python3 python3-pip
            pip3 install -U pip
          run: |
            pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
            ruff --help
      - name: "Upload wheels"
        uses: actions/upload-artifact@v4
        with:
          name: wheels-${{ matrix.platform.target }}
          path: dist
      - name: "Archive binary"
        run: |
          ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.tar.gz
          tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
          shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: binaries-${{ matrix.platform.target }}
          path: |
            *.tar.gz
            *.sha256

  musllinux:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target:
          - x86_64-unknown-linux-musl
          - i686-unknown-linux-musl
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.sha }}
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          architecture: x64
      - name: "Prep README.md"
        run: python scripts/transform_readme.py --target pypi
      - name: "Build wheels"
        uses: PyO3/maturin-action@v1
        with:
          target: ${{ matrix.target }}
          manylinux: musllinux_1_2
          args: --release --locked --out dist
      - name: "Test wheel"
        if: matrix.target == 'x86_64-unknown-linux-musl'
        uses: addnab/docker-run-action@v3
        with:
          image: alpine:latest
          options: -v ${{ github.workspace }}:/io -w /io
          run: |
            apk add python3
            python -m venv .venv
            .venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
            .venv/bin/ruff check --help
      - name: "Upload wheels"
        uses: actions/upload-artifact@v4
        with:
          name: wheels-${{ matrix.target }}
          path: dist
      - name: "Archive binary"
        run: |
          ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.target }}.tar.gz
          tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
          shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: binaries-${{ matrix.target }}
          path: |
            *.tar.gz
            *.sha256

  musllinux-cross:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        platform:
          - target: aarch64-unknown-linux-musl
            arch: aarch64
            maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
          - target: armv7-unknown-linux-musleabihf
            arch: armv7

    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.sha }}
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
      - name: "Prep README.md"
        run: python scripts/transform_readme.py --target pypi
      - name: "Build wheels"
        uses: PyO3/maturin-action@v1
        with:
          target: ${{ matrix.platform.target }}
          manylinux: musllinux_1_2
          args: --release --locked --out dist
          docker-options: ${{ matrix.platform.maturin_docker_options }}
      - uses: uraimo/run-on-arch-action@v2
        name: Test wheel
        with:
          arch: ${{ matrix.platform.arch }}
          distro: alpine_latest
          githubToken: ${{ github.token }}
          install: |
            apk add python3
          run: |
            python -m venv .venv
            .venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
            .venv/bin/ruff check --help
      - name: "Upload wheels"
        uses: actions/upload-artifact@v4
        with:
          name: wheels-${{ matrix.platform.target }}
          path: dist
      - name: "Archive binary"
        run: |
          ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.tar.gz
          tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
          shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: binaries-${{ matrix.platform.target }}
          path: |
            *.tar.gz
            *.sha256

  validate-tag:
    name: Validate tag
    runs-on: ubuntu-latest
    # If you don't set an input tag, it's a dry run (no uploads).
    if: ${{ inputs.tag }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: main # We checkout the main branch to check for the commit
      - name: Check main branch
        if: ${{ inputs.sha }}
        run: |
          # Fetch the main branch since a shallow checkout is used by default
          git fetch origin main --unshallow
          if ! git branch --contains ${{ inputs.sha }} | grep -E '(^|\s)main$'; then
            echo "The specified sha is not on the main branch" >&2
            exit 1
          fi
      - name: Check tag consistency
        run: |
          # Switch to the commit we want to release
          git checkout ${{ inputs.sha }}
          version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
          if [ "${{ inputs.tag }}" != "${version}" ]; then
            echo "The input tag does not match the version from pyproject.toml:" >&2
            echo "${{ inputs.tag }}" >&2
            echo "${version}" >&2
            exit 1
          else
            echo "Releasing ${version}"
          fi

  upload-release:
    name: Upload to PyPI
    runs-on: ubuntu-latest
    needs:
      - macos-universal
      - macos-x86_64
      - windows
      - linux
      - linux-cross
      - musllinux
      - musllinux-cross
      - validate-tag
    # If you don't set an input tag, it's a dry run (no uploads).
    if: ${{ inputs.tag }}
    environment:
      name: release
    permissions:
      # For pypi trusted publishing
      id-token: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          pattern: wheels-*
          path: wheels
          merge-multiple: true
      - name: Publish to PyPi
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          skip-existing: true
          packages-dir: wheels
          verbose: true

  tag-release:
    name: Tag release
    runs-on: ubuntu-latest
    needs: upload-release
    # If you don't set an input tag, it's a dry run (no uploads).
    if: ${{ inputs.tag }}
    permissions:
      # For git tag
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.sha }}
      - name: git tag
        run: |
          git config user.email "hey@astral.sh"
          git config user.name "Ruff Release CI"
          git tag -m "v${{ inputs.tag }}" "v${{ inputs.tag }}"
          # If there is duplicate tag, this will fail. The publish to pypi action will have been a noop (due to skip
          # existing), so we make a non-destructive exit here
          git push --tags

  publish-release:
    name: Publish to GitHub
    runs-on: ubuntu-latest
    needs: tag-release
    # If you don't set an input tag, it's a dry run (no uploads).
    if: ${{ inputs.tag }}
    permissions:
      # For GitHub release publishing
      contents: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          pattern: binaries-*
          path: binaries
          merge-multiple: true
      - name: "Publish to GitHub"
        uses: softprops/action-gh-release@v2
        with:
          draft: true
          files: binaries/*
          tag_name: v${{ inputs.tag }}

  docker-publish:
    # This action doesn't need to wait on any other task, it's easy to re-tag if something failed and we're validating
    # the tag here also
    name: Push Docker image ghcr.io/astral-sh/ruff
    runs-on: ubuntu-latest
    environment:
      name: release
    permissions:
      # For the docker push
      packages: write
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.sha }}

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/astral-sh/ruff

      - name: Check tag consistency
        # Unlike validate-tag we don't check if the commit is on the main branch, but it seems good enough since we can
        # change docker tags
        if: ${{ inputs.tag }}
        run: |
          version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
          if [ "${{ inputs.tag }}" != "${version}" ]; then
            echo "The input tag does not match the version from pyproject.toml:" >&2
            echo "${{ inputs.tag }}" >&2
            echo "${version}" >&2
            exit 1
          else
            echo "Releasing ${version}"
          fi

      - name: "Build and push Docker image"
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          # Reuse the builder
          cache-from: type=gha
          cache-to: type=gha,mode=max
          push: ${{ inputs.tag != '' }}
          tags: ghcr.io/astral-sh/ruff:latest,ghcr.io/astral-sh/ruff:${{ inputs.tag || 'dry-run' }}
          labels: ${{ steps.meta.outputs.labels }}

  # After the release has been published, we update downstream repositories
  # This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers
  update-dependents:
    name: Update dependents
    runs-on: ubuntu-latest
    needs: publish-release
    steps:
      - name: "Update pre-commit mirror"
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
          script: |
            github.rest.actions.createWorkflowDispatch({
              owner: 'astral-sh',
              repo: 'ruff-pre-commit',
              workflow_id: 'main.yml',
              ref: 'main',
            })
