reviewloop 0.2.1

Reproducible, guardrailed automation for academic review workflows on paperreview.ai
Documentation
name: Release

on:
  push:
    tags:
      - 'v*.*.*'
  workflow_dispatch:
    inputs:
      version:
        description: Version to release (for example 0.1.1)
        required: true
        type: string
      skip_crates_publish:
        description: Skip cargo publish and only run tap/GitHub release steps
        required: false
        default: true
        type: boolean

concurrency:
  group: release-${{ github.ref }}
  cancel-in-progress: false

permissions:
  contents: write

env:
  CARGO_TERM_COLOR: always

jobs:
  verify:
    name: Verify Release Inputs
    runs-on: ubuntu-latest
    environment: release
    env:
      REVIEWLOOP_GMAIL_CLIENT_ID: ${{ secrets.REVIEWLOOP_GMAIL_CLIENT_ID }}
      REVIEWLOOP_GMAIL_CLIENT_SECRET: ${{ secrets.REVIEWLOOP_GMAIL_CLIENT_SECRET }}
    outputs:
      crate_name: ${{ steps.meta.outputs.crate_name }}
      version: ${{ steps.meta.outputs.version }}
      tag_name: ${{ steps.meta.outputs.tag_name }}

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy

      - name: Cache Rust artifacts
        uses: Swatinem/rust-cache@v2

      - name: Validate tag and collect package metadata
        id: meta
        shell: bash
        env:
          INPUT_VERSION: ${{ inputs.version }}
        run: |
          set -euo pipefail

          crate_name=$(sed -nE 's/^name\s*=\s*"([^"]+)".*/\1/p' Cargo.toml | head -n1)
          version=$(sed -nE 's/^version\s*=\s*"([^"]+)".*/\1/p' Cargo.toml | head -n1)

          if [[ -z "$crate_name" || -z "$version" ]]; then
            echo "Failed to parse crate metadata from Cargo.toml" >&2
            exit 1
          fi

          if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
            if [[ -z "${INPUT_VERSION:-}" ]]; then
              echo "workflow_dispatch requires inputs.version" >&2
              exit 1
            fi
            if [[ "${INPUT_VERSION}" != "${version}" ]]; then
              echo "Manual release version ${INPUT_VERSION} does not match Cargo.toml version ${version}" >&2
              exit 1
            fi
            tag_name="v${version}"
          else
            tag_version="${GITHUB_REF_NAME#v}"
            if [[ "$tag_version" != "$version" ]]; then
              echo "Tag ${GITHUB_REF_NAME} does not match Cargo.toml version ${version}" >&2
              exit 1
            fi
            tag_name="${GITHUB_REF_NAME}"
          fi

          echo "crate_name=$crate_name" >> "$GITHUB_OUTPUT"
          echo "version=$version" >> "$GITHUB_OUTPUT"
          echo "tag_name=$tag_name" >> "$GITHUB_OUTPUT"

      - name: Release quality gates
        run: ./scripts/quality-gates.sh

  publish-crates-io:
    name: Publish To crates.io
    runs-on: ubuntu-latest
    needs: verify
    environment: release
    env:
      REVIEWLOOP_GMAIL_CLIENT_ID: ${{ secrets.REVIEWLOOP_GMAIL_CLIENT_ID }}
      REVIEWLOOP_GMAIL_CLIENT_SECRET: ${{ secrets.REVIEWLOOP_GMAIL_CLIENT_SECRET }}
    permissions:
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Cache Rust artifacts
        uses: Swatinem/rust-cache@v2

      - name: Publish crate (skip if version already exists)
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
          CRATE_NAME: ${{ needs.verify.outputs.crate_name }}
          VERSION: ${{ needs.verify.outputs.version }}
          SKIP_CRATES_PUBLISH: ${{ github.event_name == 'workflow_dispatch' && inputs.skip_crates_publish && 'true' || 'false' }}
        shell: bash
        run: |
          set -euo pipefail

          if [[ -z "${CARGO_REGISTRY_TOKEN:-}" ]]; then
            echo "Missing required secret: CARGO_REGISTRY_TOKEN" >&2
            exit 1
          fi

          if [[ "${SKIP_CRATES_PUBLISH}" == "true" ]]; then
            echo "Skipping cargo publish by manual request."
            exit 0
          fi

          if curl -fsSL \
            -A "reviewloop-release-workflow" \
            -H "Accept: application/json" \
            "https://crates.io/api/v1/crates/${CRATE_NAME}/${VERSION}" >/dev/null; then
            echo "${CRATE_NAME} ${VERSION} already published; skipping cargo publish."
            exit 0
          fi

          publish_log="$(mktemp)"
          set +e
          cargo publish --locked 2>&1 | tee "${publish_log}"
          status=${PIPESTATUS[0]}
          set -e

          if [[ ${status} -eq 0 ]]; then
            exit 0
          fi

          if grep -q "already exists on crates.io index" "${publish_log}"; then
            echo "${CRATE_NAME} ${VERSION} already published during this workflow run; continuing."
            exit 0
          fi

          exit "${status}"

  update-homebrew-tap:
    name: Update Homebrew Tap
    runs-on: ubuntu-latest
    needs:
      - verify
      - publish-crates-io
    environment: release
    permissions:
      contents: read

    steps:
      - name: Validate tap publishing configuration
        env:
          HOMEBREW_TAP_REPO: ${{ vars.HOMEBREW_TAP_REPO != '' && vars.HOMEBREW_TAP_REPO || 'Acture/homebrew-ac' }}
          HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
        shell: bash
        run: |
          set -euo pipefail

          if [[ -z "${HOMEBREW_TAP_GITHUB_TOKEN:-}" ]]; then
            echo "Missing required secret: HOMEBREW_TAP_GITHUB_TOKEN" >&2
            exit 1
          fi

          echo "Using tap repository: ${HOMEBREW_TAP_REPO}"

      - name: Resolve source archive URL and checksum
        id: source
        env:
          TAG_NAME: ${{ needs.verify.outputs.tag_name }}
        shell: bash
        run: |
          set -euo pipefail

          source_url="https://github.com/${GITHUB_REPOSITORY}/archive/refs/tags/${TAG_NAME}.tar.gz"
          archive_path="$(mktemp)"

          curl -fsSL \
            -A "reviewloop-release-workflow" \
            "${source_url}" \
            -o "${archive_path}"

          checksum="$(sha256sum "${archive_path}" | awk '{print $1}')"

          echo "source_url=${source_url}" >> "$GITHUB_OUTPUT"
          echo "checksum=${checksum}" >> "$GITHUB_OUTPUT"

      - name: Checkout source repository
        uses: actions/checkout@v6

      - name: Checkout tap repository
        uses: actions/checkout@v6
        with:
          repository: ${{ vars.HOMEBREW_TAP_REPO != '' && vars.HOMEBREW_TAP_REPO || 'Acture/homebrew-ac' }}
          token: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
          path: tap

      - name: Render formula
        env:
          CRATE_NAME: ${{ needs.verify.outputs.crate_name }}
          VERSION: ${{ needs.verify.outputs.version }}
          TAG_NAME: ${{ needs.verify.outputs.tag_name }}
          SOURCE_SHA256: ${{ steps.source.outputs.checksum }}
          HOMEBREW_FORMULA_PATH: ${{ vars.HOMEBREW_FORMULA_PATH }}
        shell: bash
        run: |
          set -euo pipefail

          formula_rel_path="${HOMEBREW_FORMULA_PATH:-Formula/${CRATE_NAME}.rb}"
          formula_file="tap/${formula_rel_path}"

          bash tools/render_homebrew_formula.sh "$formula_file"

          grep -Fq "url \"https://github.com/${GITHUB_REPOSITORY}/archive/refs/tags/${TAG_NAME}.tar.gz\"" "$formula_file"
          grep -Fq "sha256 \"${SOURCE_SHA256}\"" "$formula_file"
          grep -Fq 'system "cargo", "install", *std_cargo_args(path: ".")' "$formula_file"

      - name: Commit and push tap update
        env:
          CRATE_NAME: ${{ needs.verify.outputs.crate_name }}
          VERSION: ${{ needs.verify.outputs.version }}
          HOMEBREW_FORMULA_PATH: ${{ vars.HOMEBREW_FORMULA_PATH }}
        shell: bash
        run: |
          set -euo pipefail

          formula_rel_path="${HOMEBREW_FORMULA_PATH:-Formula/${CRATE_NAME}.rb}"

          cd tap
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

          git add "$formula_rel_path"

          if git diff --cached --quiet -- "$formula_rel_path"; then
            echo "No formula changes detected."
            exit 0
          fi

          git commit -m "reviewloop ${VERSION}"

          current_branch=$(git branch --show-current)
          git push origin "HEAD:${current_branch}"

  github-release:
    name: Create GitHub Release
    runs-on: ubuntu-latest
    needs:
      - verify
      - publish-crates-io
      - update-homebrew-tap
    environment: release

    steps:
      - name: Publish GitHub release notes
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ needs.verify.outputs.tag_name }}
          generate_release_notes: true