name: Release

# Triggered when a semver tag is pushed from main. The workflow:
#   1. Verifies the workspace (fmt, clippy, test, doc) on Linux + macOS.
#   2. Enforces a CHANGELOG entry for the tagged version.
#   3. Runs cargo publish --dry-run / cargo package --list as a packaging gate.
#   4. Publishes all workspace crates to crates.io in topological order
#      (requires the `release` environment and CARGO_REGISTRY_TOKEN secret).
#   5. Creates a GitHub release with autogenerated notes and Cargo.lock attached.
#
# Branch model: feature/* → integration → (squash merge + tag) → main
# Tags must only be pushed on main. RC tags (v0.8.0-rc.1) can be cut from
# integration for pre-release testing — they will trigger this workflow but
# the publish job will push a pre-release version to crates.io.
#
# Publish order (path deps replaced with registry versions by cargo):
#   swink-agent → swink-agent-macros → {adapters, memory, policies, artifacts,
#   auth, eval, local-llm, mcp, patterns, plugin-web} → swink-agent-tui
#
# A 20-second sleep between each publish gives crates.io time to index the
# crate before dependent crates attempt to resolve it.

on:
  push:
    tags:
      - "v*"

permissions:
  contents: write

env:
  CARGO_TERM_COLOR: always
  RUSTFLAGS: "-D warnings"
  CARGO_NET_GIT_FETCH_WITH_CLI: "true"

jobs:
  verify:
    name: Verify (${{ matrix.os }})
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: ./.github/actions/rust-setup
        with:
          toolchain: stable
          cache-key: release
      - name: cargo fmt --check
        run: cargo fmt --all -- --check
      - name: cargo clippy
        run: cargo clippy --workspace --all-features --exclude swink-agent-local-llm -- -D warnings
      - name: cargo test
        run: cargo test --workspace --all-features --exclude swink-agent-local-llm
      - name: cargo doc
        env:
          RUSTDOCFLAGS: "-D warnings"
        run: cargo doc --workspace --no-deps

  changelog-gate:
    name: Changelog gate
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - name: Ensure CHANGELOG has an entry for this tag
        shell: bash
        run: |
          set -euo pipefail
          tag="${GITHUB_REF_NAME}"
          version="${tag#v}"
          if [ ! -f CHANGELOG.md ]; then
            echo "CHANGELOG.md missing at repo root" >&2
            exit 1
          fi
          if ! grep -E "^## \[v?${version}\]" CHANGELOG.md >/dev/null; then
            echo "CHANGELOG.md has no '## [${version}]' (or '## [v${version}]') section" >&2
            exit 1
          fi
          echo "Found CHANGELOG entry for ${version}"

  publish-dry-run:
    name: cargo publish --dry-run
    needs: [verify, changelog-gate]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: ./.github/actions/rust-setup
        with:
          toolchain: stable
          cache-key: release-publish
      # Topological order matches the publish job below.
      #
      # tier 0: swink-agent, swink-agent-macros (no internal deps)
      # tier 1: auth, memory, policies, artifacts, eval, local-llm, mcp,
      #         patterns, plugin-web (depend only on swink-agent)
      # tier 2: adapters (depends on swink-agent + swink-agent-auth)
      # tier 3: tui (depends on swink-agent + adapters + memory + local-llm)
      #
      # `cargo publish --dry-run` is used for tier 0 (no internal deps to
      # resolve via registry). For tier 1+, we use `cargo package --list`
      # because dry-run requires the upstream version to already exist on
      # crates.io — which it doesn't until the publish job actually runs.
      # `cargo package --list` exercises license, readme, categories,
      # rust-version, etc., but NOT the manifest's "all deps need a
      # version" check. That check fires only on real `cargo publish`, so
      # workspaces with internal path deps must keep `version = "X.Y.Z"`
      # on every regular [dependencies] entry (dev-deps must NOT have it).
      - name: Dry-run publish swink-agent
        run: cargo publish --dry-run -p swink-agent --allow-dirty
      - name: Dry-run publish swink-agent-macros
        run: cargo publish --dry-run -p swink-agent-macros --allow-dirty
      - name: Package swink-agent-auth
        run: cargo package --list --allow-dirty -p swink-agent-auth >/dev/null
      - name: Package swink-agent-memory
        run: cargo package --list --allow-dirty -p swink-agent-memory >/dev/null
      - name: Package swink-agent-policies
        run: cargo package --list --allow-dirty -p swink-agent-policies >/dev/null
      - name: Package swink-agent-artifacts
        run: cargo package --list --allow-dirty -p swink-agent-artifacts >/dev/null
      - name: Package swink-agent-eval
        run: cargo package --list --allow-dirty -p swink-agent-eval >/dev/null
      - name: Package swink-agent-local-llm
        run: cargo package --list --allow-dirty -p swink-agent-local-llm >/dev/null
      - name: Package swink-agent-mcp
        run: cargo package --list --allow-dirty -p swink-agent-mcp >/dev/null
      - name: Package swink-agent-patterns
        run: cargo package --list --allow-dirty -p swink-agent-patterns >/dev/null
      - name: Package swink-agent-plugin-web
        run: cargo package --list --allow-dirty -p swink-agent-plugin-web >/dev/null
      - name: Package swink-agent-adapters
        run: cargo package --list --allow-dirty -p swink-agent-adapters >/dev/null
      - name: Package swink-agent-tui
        run: cargo package --list --allow-dirty -p swink-agent-tui >/dev/null

  publish:
    name: Publish to crates.io
    needs: publish-dry-run
    runs-on: ubuntu-latest
    environment: release
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: ./.github/actions/rust-setup
        with:
          toolchain: stable
          cache-key: release-publish
      # Topological publish order. Each entry is "<sleep_after_secs> <crate>".
      #
      # tier 0: swink-agent, swink-agent-macros (no internal deps)
      # tier 1: auth → memory, policies, artifacts, eval, local-llm, mcp,
      #         patterns, plugin-web (depend only on swink-agent)
      # tier 2: adapters (depends on swink-agent + swink-agent-auth)
      # tier 3: tui (depends on swink-agent + adapters + memory + local-llm)
      #
      # Skip-if-published makes the job idempotent: if a run fails midway
      # (crates.io rate limit, network, etc.), re-running the workflow
      # skips already-published versions and retries only the missing ones.
      #
      # Sleep is 40s for version-update releases where all 13 crates already
      # exist on crates.io. FIRST-PUBLISH releases (where a crate has never
      # been published) hit crates.io's "5 new crates / 10 min" rate limit
      # after the 5th new crate. If the rate limit fires, wait 10 minutes
      # and re-run — skip-if-published picks up where it left off. crates.io
      # will grant higher limits on request (help@crates.io).
      - name: Publish to crates.io
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          set -euo pipefail
          version="${GITHUB_REF_NAME#v}"

          publish_order=(
            "40 swink-agent"
            "40 swink-agent-macros"
            "40 swink-agent-auth"
            "40 swink-agent-memory"
            "40 swink-agent-policies"
            "40 swink-agent-artifacts"
            "40 swink-agent-eval"
            "40 swink-agent-local-llm"
            "40 swink-agent-mcp"
            "40 swink-agent-patterns"
            "40 swink-agent-plugin-web"
            "40 swink-agent-adapters"
            "0 swink-agent-tui"
          )

          for entry in "${publish_order[@]}"; do
            read -r sleep_secs crate <<< "$entry"
            echo "::group::$crate v$version"
            if curl -sSf -o /dev/null "https://crates.io/api/v1/crates/$crate/$version"; then
              echo "$crate v$version already on crates.io — skipping"
            else
              cargo publish -p "$crate"
            fi
            echo "::endgroup::"
            if [ "$sleep_secs" != "0" ]; then
              sleep "$sleep_secs"
            fi
          done

  github-release:
    name: GitHub release
    needs: publish
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - name: Create release and upload Cargo.lock
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          gh release create "${GITHUB_REF_NAME}" \
            --generate-notes \
            --title "${GITHUB_REF_NAME}" \
            Cargo.lock
