name: Release-plz

# Conventional-commit release automation, mirroring the proven setup in
# nickderobertis/allowlister. On every push to main:
#   * release     — if a merged version-bump PR left Cargo.toml at a version with
#                   no tag yet, tag vX.Y.Z and cut the GitHub Release.
#   * release-pr  — open/update a "release vX.Y.Z" PR that bumps the version and
#                   writes the CHANGELOG for the commits since the last release.
#   * auto-merge  — enable auto-merge on that PR so it merges once the required
#                   status checks pass; the merge re-triggers this workflow's
#                   `release` step above, which tags and cuts the Release.
#
# The GitHub Release that release-plz cuts (via the PAT) fires the
# `release: published` event that drives release.yml to build and attach the
# cross-platform binaries — so artifacts still publish only after a green gate,
# with no manual merge step and no gating skipped.
#
# Two secrets gate this workflow (the `guard` job requires both, else no-ops):
#   * RELEASE_PLZ_TOKEN — a PAT (contents:write + pull-requests:write), NOT the
#     default GITHUB_TOKEN: a tag or release created by GITHUB_TOKEN does NOT
#     trigger other workflows, so release.yml would never build the binaries.
#   * CARGO_REGISTRY_TOKEN — a crates.io API token; `release` runs `cargo publish`
#     for both crates, so without it a release would hard-fail mid-run (after
#     publishing the engine but before the binary). Requiring it up front keeps
#     releases all-or-nothing instead of leaving a partial publish behind.
# Until both are set, the guard keeps this workflow dormant instead of failing
# on every push to main.
#
# Why the binary is driven directly (not release-plz/action): version numbers are
# computed against the last release *tag* as the baseline, not crates.io.
# `--registry-manifest-path` points release-plz at a checkout of that tag's
# Cargo.toml, which keeps conventional-commit versioning deterministic from git
# history AND works for the very first crates.io publish, when nothing is on the
# registry yet to diff against. The release-plz action does not expose that flag.

on:
  push:
    branches: [main]

permissions:
  contents: write
  pull-requests: write

env:
  # Pin release-plz so the release process is reproducible; bump deliberately.
  RELEASE_PLZ_VERSION: "0.3.158"

jobs:
  # `secrets` is not available in a job-level `if`, so a tiny job maps the two
  # required secrets to an output the release job gates on. Missing either one ->
  # the release job is skipped, and the workflow is a clean no-op instead of a
  # per-push failure (or a partial crates.io publish).
  guard:
    runs-on: ubuntu-latest
    outputs:
      enabled: ${{ steps.check.outputs.enabled }}
    steps:
      - id: check
        env:
          RELEASE_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }}
          CARGO_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          enabled=true
          if [ -z "$RELEASE_TOKEN" ]; then
            echo "RELEASE_PLZ_TOKEN not set." >&2
            echo "Add a PAT (contents:write + pull-requests:write) as the repo" >&2
            echo "secret RELEASE_PLZ_TOKEN to enable conventional-commit releases." >&2
            enabled=false
          fi
          if [ -z "$CARGO_TOKEN" ]; then
            echo "CARGO_REGISTRY_TOKEN not set." >&2
            echo "Add a crates.io API token as the repo secret CARGO_REGISTRY_TOKEN" >&2
            echo "so releases can 'cargo publish' both crates." >&2
            enabled=false
          fi
          if [ "$enabled" = false ]; then
            echo "Skipping release automation until both secrets are set." >&2
          fi
          echo "enabled=$enabled" >> "$GITHUB_OUTPUT"

  release-plz:
    needs: guard
    if: needs.guard.outputs.enabled == 'true'
    runs-on: ubuntu-latest
    # Serialize: two quick pushes must not race to tag or to update the same PR.
    concurrency:
      group: release-plz
      cancel-in-progress: false
    steps:
      - uses: actions/checkout@v5
        with:
          # Full history + tags are required to diff against the last release.
          fetch-depth: 0
          token: ${{ secrets.RELEASE_PLZ_TOKEN }}

      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2

      - name: Install release-plz
        run: |
          curl -fsSL "https://github.com/release-plz/release-plz/releases/download/release-plz-v${RELEASE_PLZ_VERSION}/release-plz-x86_64-unknown-linux-gnu.tar.gz" \
            | tar -xz -C /usr/local/bin release-plz
          release-plz --version

      - name: Use the last release tag as the version baseline
        id: baseline
        run: |
          tag="$(git tag --list 'v*' --sort=-version:refname | head -n1)"
          if [ -n "$tag" ]; then
            # A worktree outside the repo keeps the working tree clean (an
            # untracked dir inside it would trip release-plz's dirty-tree check).
            git worktree add --detach "${RUNNER_TEMP}/release-baseline" "$tag"
            echo "arg=--registry-manifest-path ${RUNNER_TEMP}/release-baseline/Cargo.toml" >> "$GITHUB_OUTPUT"
            echo "Baseline: $tag"
          else
            echo "arg=" >> "$GITHUB_OUTPUT"
            echo "No release tag yet; the first release uses the current version."
          fi

      # `release` publishes any package whose manifest version isn't on crates.io
      # yet, then tags it and cuts its GitHub Release. In practice the version
      # only changes when the release PR (below) merges, so ordinary pushes find
      # nothing unreleased and no-op; but running it every push (release-plz.toml
      # `release_always = true`) makes a failed publish RECOVERABLE — a fix lands
      # as a normal commit and the next push finishes the pending release. It runs
      # `cargo publish` for both crates (CARGO_REGISTRY_TOKEN), then tags and cuts
      # the GitHub Release (RELEASE_PLZ_TOKEN).
      - name: Publish to crates.io, tag, and cut the GitHub Release
        env:
          GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }}
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: release-plz release --git-token "$GITHUB_TOKEN"

      - name: Open or update the release PR
        env:
          GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }}
        run: release-plz release-pr ${{ steps.baseline.outputs.arg }} --git-token "$GITHUB_TOKEN"

      # Enable GitHub auto-merge on the release PR so it merges itself once the
      # required status checks (the deterministic gate) pass. That merge
      # re-triggers the `release` step above, which tags vX.Y.Z and cuts the
      # Release — no manual merge, no gating skipped.
      - name: Auto-merge the release PR when the gate is green
        env:
          GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }}
        run: |
          pr=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --json number,headRefName \
                 --jq 'map(select(.headRefName | startswith("release-plz-"))) | .[0].number // empty')
          if [ -n "$pr" ]; then
            echo "Enabling auto-merge on release PR #$pr"
            gh pr merge "$pr" --repo "$GITHUB_REPOSITORY" --auto --squash --delete-branch \
              || echo "::warning::could not enable auto-merge on #$pr (already enabled, or already merging)"
          else
            echo "No open release PR to auto-merge."
          fi
