name: publish

# Manual-trigger workflow that publishes a tagged version to crates.io
# via OIDC trusted publishing (no long-lived token).
#
# Pre-1.0 we deliberately don't auto-publish on every tag — accidental
# tags shouldn't accidentally publish to crates.io. Trigger explicitly:
#
#   gh workflow run publish.yml --field tag=v0.2.2
#
# Authentication uses crates.io's OIDC trusted-publishing flow. The
# Trusted Publisher config on the registry side
# (https://crates.io/crates/mneme-mcp/settings → Trusted Publishers)
# binds this exact identity:
#
#   owner    = tr0mb1r
#   repo     = mneme
#   workflow = publish.yml
#   env      = crates-io
#
# The `rust-lang/crates-io-auth-action` step requests a JWT from
# GitHub's OIDC provider and exchanges it for a short-lived
# crates.io access token. The `Publish to crates.io` step uses that
# token via $CARGO_REGISTRY_TOKEN. There is no long-lived secret —
# `CARGO_REGISTRY_TOKEN` was rotated out of repo secrets when this
# workflow landed.
#
# Security notes:
# - All third-party action refs are pinned to commit SHAs (with the
#   tag in a trailing comment for readability). Pinning to tags would
#   leave the workflow vulnerable to a tag-repointing supply-chain
#   attack on the secret-touching publish step.
# - `if: github.repository_owner == 'tr0mb1r'` blocks fork runs.
# - `concurrency: publish-crates-io` prevents double-publish races.
# - `--dry-run` runs first; failed packaging aborts before the token
#   exchange happens.
# - The `Verify Cargo.toml version matches tag` step blocks publishing
#   from a tag whose code carries a mismatched Cargo.toml version.

on:
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag to publish to crates.io (e.g., v0.2.2)'
        required: true

permissions:
  # OIDC: GitHub's id-token endpoint is what the auth action exchanges
  # for a crates.io publish token. Without `id-token: write` the
  # auth-action step fails with a 401.
  contents: read
  id-token: write

concurrency:
  group: publish-crates-io
  cancel-in-progress: false

jobs:
  publish:
    name: cargo publish
    if: ${{ github.repository_owner == 'tr0mb1r' }}
    runs-on: ubuntu-latest
    environment:
      name: crates-io
      url: https://crates.io/crates/mneme-mcp

    steps:
      - name: Checkout tag
        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4
        with:
          ref: ${{ github.event.inputs.tag }}
          fetch-depth: 1

      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8  # stable (March 2026)

      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32  # v2
        with:
          key: publish

      - name: Verify Cargo.toml version matches tag
        shell: bash
        run: |
          set -euo pipefail
          tag="${{ github.event.inputs.tag }}"
          if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
            echo "::error::Tag '$tag' does not match v*.*.* pattern."
            exit 1
          fi
          version="${tag#v}"
          cargo_ver="$(awk -F'"' '/^version[[:space:]]*=/{print $2; exit}' Cargo.toml)"
          if [ "$cargo_ver" != "$version" ]; then
            echo "::error::Cargo.toml version '$cargo_ver' does not match tag '$tag' (expected '$version')."
            exit 1
          fi
          echo "::notice::Publishing mneme-mcp $version from tag $tag"

      - name: Dry-run publish
        run: cargo publish --dry-run --locked

      # OIDC token exchange — produces a short-lived crates.io token
      # bound to this workflow run. The action's `token` output is
      # plumbed straight into `cargo publish` via the env block below;
      # nothing else in the run can read it (GitHub auto-masks
      # `outputs.token` in logs).
      - name: Get short-lived crates.io token via OIDC
        uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe  # v1.0.4
        id: cargo-auth

      - name: Publish to crates.io
        env:
          CARGO_REGISTRY_TOKEN: ${{ steps.cargo-auth.outputs.token }}
        run: cargo publish --locked
