name: Manage Release PRs and Publish Crates

permissions:
  pull-requests: write
  contents: write

on:
  push:
    branches:
      - main

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

jobs:
  release-pr:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
        with:
          fetch-depth: 0
          token: ${{ secrets.RELEASE_PLZ_TOKEN }}
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
      - name: Create or update release PR
        uses: release-plz/action@064f4d1e36c843611ddf013be726beaa4ad804db # v0.5.129
        with:
          command: release-pr
        env:
          GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }}

  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
        with:
          fetch-depth: 0
          token: ${{ secrets.RELEASE_PLZ_TOKEN }}
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
      - name: Verify lockstep workspace versions
        run: |
          set -euo pipefail
          WORKSPACE_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r \
            '.packages[] | select(.name == "fgumi") | .version')
          MISMATCHES=$(cargo metadata --no-deps --format-version 1 | jq -r \
            --arg ver "$WORKSPACE_VERSION" \
            '.packages[] | select(.name | startswith("fgumi-")) | select(.version != $ver) | "\(.name) \(.version)"')
          if [ -n "$MISMATCHES" ]; then
            echo "ERROR: workspace version drift detected (expected $WORKSPACE_VERSION):"
            echo "$MISMATCHES"
            exit 1
          fi
          echo "All workspace crates at v$WORKSPACE_VERSION"
      - name: Publish workspace crates in dependency order
        id: publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          set -euo pipefail

          # All workspace crates share a single version (lockstep).
          # Check the root crate to decide if publishing is needed.
          VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r \
            '.packages[] | select(.name == "fgumi") | .version')

          PUBLISHED_VERSION=$(curl -sSf \
            -H "User-Agent: fgumi-ci (https://github.com/fulcrumgenomics/fgumi)" \
            "https://crates.io/api/v1/crates/fgumi" | jq -r \
            '.crate.max_version // "0.0.0"')

          echo "version=$VERSION" >> "$GITHUB_OUTPUT"

          if [ "$VERSION" = "$PUBLISHED_VERSION" ]; then
            echo "All crates already at v$VERSION -- nothing to publish"
            echo "published=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          echo "Publishing all workspace crates (v$VERSION)..."

          # Workspace crates in topological (dependency) order.
          # Leaf crates first, root crate last.  Update this list when
          # adding new workspace crates or changing inter-crate dependencies.
          CRATES=(
            fgumi-dna
            fgumi-bgzf
            fgumi-simd-fastq
            fgumi-tag
            fgumi-raw-bam
            fgumi-bam-io
            fgumi-umi
            fgumi-sam
            fgumi-metrics
            fgumi-sort
            fgumi-consensus
            fgumi
          )

          # Verify every publishable workspace crate is in the publish list.
          # Crates marked `publish = false` (serialized as `publish: []` by
          # cargo metadata) are excluded -- e.g. internal xtask tooling.
          WORKSPACE_CRATES=$(cargo metadata --no-deps --format-version 1 \
            | jq -r '.packages[] | select(.publish != []) | .name' | sort)
          LISTED_CRATES=$(printf '%s\n' "${CRATES[@]}" | sort)
          MISSING=$(comm -23 <(echo "$WORKSPACE_CRATES") <(echo "$LISTED_CRATES"))
          if [ -n "$MISSING" ]; then
            echo "ERROR: workspace crates missing from CRATES publish list:"
            echo "$MISSING"
            echo "Add them to the CRATES array in the correct dependency order."
            exit 1
          fi

          EXTRA=$(comm -13 <(echo "$WORKSPACE_CRATES") <(echo "$LISTED_CRATES"))
          if [ -n "$EXTRA" ]; then
            echo "ERROR: CRATES publish list contains entries not in workspace:"
            echo "$EXTRA"
            echo "Remove or rename stale entries before publishing."
            exit 1
          fi

          # Verify the CRATES list is in valid topological order: every
          # workspace dependency of a crate must appear EARLIER in the list.
          # `cargo publish` resolves each crate's dependencies against the
          # crates.io index, so a dependency published later does not yet
          # exist when its dependent is published.  Catching this here -- as a
          # pure list check, before any upload -- prevents a mid-publish
          # failure that would leave a partially released version on crates.io
          # (published versions cannot be deleted, only yanked).
          META=$(cargo metadata --no-deps --format-version 1)
          WS_JSON=$(echo "$META" | jq '[.packages[] | select(.publish != []) | .name]')
          index_of() {
            local target="$1" i
            for i in "${!CRATES[@]}"; do
              if [ "${CRATES[$i]}" = "$target" ]; then echo "$i"; return; fi
            done
            echo "-1"
          }
          ORDER_ERRORS=""
          for i in "${!CRATES[@]}"; do
            crate="${CRATES[$i]}"
            DEPS=$(echo "$META" | jq -r --arg c "$crate" --argjson ws "$WS_JSON" '
              .packages[] | select(.name == $c) | .dependencies[].name
              | select(. as $d | $ws | index($d))' | sort -u)
            for dep in $DEPS; do
              j=$(index_of "$dep")
              if [ "$j" -ge "$i" ]; then
                ORDER_ERRORS="${ORDER_ERRORS}  ${crate} depends on ${dep}, which must be listed earlier\n"
              fi
            done
          done
          if [ -n "$ORDER_ERRORS" ]; then
            echo "ERROR: CRATES publish list is not in valid dependency order:"
            printf "%b" "$ORDER_ERRORS"
            echo "Reorder the CRATES array so every dependency precedes its dependents."
            exit 1
          fi

          for crate in "${CRATES[@]}"; do
            CRATE_MAX_VERSION=$(curl -sS \
              -H "User-Agent: fgumi-ci (https://github.com/fulcrumgenomics/fgumi)" \
              "https://crates.io/api/v1/crates/${crate}" | jq -r \
              '.crate.max_version // "0.0.0"')

            if [ "$CRATE_MAX_VERSION" = "$VERSION" ]; then
              echo "$crate v$VERSION already exists on crates.io -- skipping"
              continue
            fi

            echo "Publishing $crate v$VERSION..."
            cargo publish -p "$crate"
            echo "✓ Published $crate v$VERSION"
          done

          echo "published=true" >> "$GITHUB_OUTPUT"

      - name: Create GitHub release
        if: steps.publish.outputs.version != ''
        env:
          GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }}
        run: |
          set -euo pipefail
          VERSION="${{ steps.publish.outputs.version }}"
          TAG="v${VERSION}"

          # Idempotent: skip if tag/release already exist (e.g. from a
          # previous partial run that published crates but failed here).
          if ! git rev-parse "$TAG" >/dev/null 2>&1; then
            git tag "$TAG"
          fi
          if ! git ls-remote --tags origin "$TAG" | grep -q "$TAG"; then
            git push origin "$TAG"
          fi
          if ! gh release view "$TAG" >/dev/null 2>&1; then
            gh release create "$TAG" \
              --title "v${VERSION}" \
              --generate-notes \
              --latest
          else
            echo "Release $TAG already exists -- skipping"
          fi
