name: Publish crates

on:
  release:
    types: [published]

permissions:
  contents: read

jobs:
  publish:
    name: Publish crates.io packages
    runs-on: ubuntu-latest
    # Every published GitHub Release publishes to crates.io, release-candidates
    # included (Policy B): the manifest carries the prerelease version (e.g.
    # 0.5.0-rc.4), so `cargo publish` uploads it, and Cargo serves a prerelease
    # only on an explicit `--version`/prerelease requirement — a normal `cargo
    # add`/`cargo install kache` never picks it. The version-consistency gate
    # (tag == manifest) and require-ci-green below are the fail-closed
    # protections; the GitHub `prerelease` flag does not gate THIS publish.
    # That flag is NOT cosmetic for the binary path, though: the release builder
    # (zondax/_workflows/_release-rust.yml@v10) marks rc/alpha/beta/pre/dev tags
    # `--prerelease --latest=false`, so an RC never becomes the GitHub "Latest"
    # release that `@latest` installers / the README badge resolve to — Policy B
    # depends on that boundary behavior. (Drafts never reach here:
    # `release: published` excludes them.)
    # If the crates.io Trusted Publisher config includes a GitHub
    # Environment, add the same `environment: <name>` here. The
    # workflow file name and environment must stay in sync with
    # crates.io's trusted-publisher claims.
    permissions:
      contents: read
      id-token: write
      actions: read # read CI job conclusions for the release commit (gate below)
    env:
      CARGO_TERM_COLOR: always
      RELEASE_TAG: ${{ github.event.release.tag_name }}
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ github.event.release.tag_name }}

      - uses: dtolnay/rust-toolchain@stable

      - uses: DeterminateSystems/nix-installer-action@v22

      - name: Check release tag matches Cargo manifests (and dep-pin)
        # The version gate: tag version (prerelease suffix included) == kache ==
        # kache-core == kache-core dep-pin (run directly here rather than polling
        # for the CI job, so the manual-UI-release path is gated too without the
        # tag-gated-job / same-SHA run ambiguity that `require-ci-green` would
        # hit). Hermetic (pure manifest reads), so no nix/installer surface
        # before the publish. This is what makes publishing an RC safe: crates.io
        # gets exactly the manifest version the tag was checked against.
        run: ./scripts/check-version-consistency.sh "$RELEASE_TAG"

      - name: Check release tag matches Nix package
        # Cheap consistency gate (<5s): the full `nix build .#kache` is the
        # CI `nix-package` job's responsibility and already runs on the tagged
        # commit. Rebuilding it here only duplicated ~8min of work and pulled
        # in the Determinate/FlakeHub installer as an extra failure surface
        # before the irreversible crates.io publish. The version-match check
        # is what actually guards against tag/manifest drift, so it stays.
        run: |
          version="${RELEASE_TAG#v}"
          nix_version="$(nix eval --raw .#kache.version)"
          if [ "$nix_version" != "$version" ]; then
            echo "Nix package version $nix_version does not match release tag $RELEASE_TAG" >&2
            exit 1
          fi

      - name: Require CI green for this release commit
        # Don't publish to crates.io unless the full CI suite passed for this
        # exact commit. `release: published` can fire from a hand-drafted UI
        # release before/independent of CI; the tag-push path is already gated
        # by the release job's `needs`, but this closes the manual-release hole.
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          ./scripts/require-ci-green.sh "$(git rev-parse HEAD)" \
            "Check (Linux)" "Nix package" \
            "E2E smoke (Linux)" "E2E smoke (macOS)" "E2E smoke (Windows)" \
            "Test (macOS)" "Test (Windows)"

      - name: Package kache-core
        run: cargo package -p kache-core --locked

      - name: Authenticate to crates.io
        id: auth
        # Pinned to v1.0.4: the floating @v1 tag still runs on the deprecated
        # Node 20 runtime; v1.0.4 is the first node24 release.
        uses: rust-lang/crates-io-auth-action@v1.0.4

      - name: Publish kache-core
        env:
          CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
        run: |
          version="${RELEASE_TAG#v}"
          status="$(curl -sS -A kache-publish-workflow -o /dev/null -w "%{http_code}" "https://crates.io/api/v1/crates/kache-core/$version")"
          if [ "$status" = "200" ]; then
            echo "kache-core $version is already published"
            exit 0
          fi
          if [ "$status" != "404" ]; then
            echo "unexpected crates.io status while checking kache-core $version: $status" >&2
            exit 1
          fi
          cargo publish -p kache-core --locked

      - name: Wait for kache-core in crates.io index
        # Poll the sparse index (index.crates.io) — the source `cargo publish`
        # resolves dependencies against, updated within seconds of a publish.
        # The old `cargo search` check hit the crates.io *search* index, which
        # is eventually-consistent and lagged past this loop on the 0.4.0 cut.
        run: |
          version="${RELEASE_TAG#v}"
          for attempt in {1..30}; do
            if curl -sf -A kache-publish-workflow \
                 "https://index.crates.io/ka/ch/kache-core" | grep -q "\"vers\":\"$version\""; then
              echo "kache-core $version visible in crates.io sparse index"
              exit 0
            fi
            echo "kache-core $version not visible yet; retry $attempt/30"
            sleep 10
          done
          echo "kache-core $version did not become visible in crates.io index" >&2
          exit 1

      - name: Package kache
        run: cargo package -p kache --locked

      - name: Publish kache
        env:
          CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
        run: |
          version="${RELEASE_TAG#v}"
          status="$(curl -sS -A kache-publish-workflow -o /dev/null -w "%{http_code}" "https://crates.io/api/v1/crates/kache/$version")"
          if [ "$status" = "200" ]; then
            echo "kache $version is already published"
            exit 0
          fi
          if [ "$status" != "404" ]; then
            echo "unexpected crates.io status while checking kache $version: $status" >&2
            exit 1
          fi
          cargo publish -p kache --locked
