macp-runtime 0.6.0

MACP reference runtime: a coordination kernel and gRPC server enforcing session boundaries, message validation, append-only history, modes, and governance policy.
Documentation
name: Publish Crates

# Publishes the workspace crates to crates.io in dependency order when a
# version tag (e.g. v0.4.0) is pushed. Can also be run manually for a dry run.
on:
  push:
    tags: ["v*"]
  workflow_dispatch:
    inputs:
      dry_run:
        description: "Package and verify without uploading"
        type: boolean
        default: true
      skip_semver_checks:
        description: "Skip cargo-semver-checks (escape hatch for false positives)"
        type: boolean
        default: false

env:
  CARGO_TERM_COLOR: always
  # Workspace publishing (`cargo publish --workspace`, stabilized in 1.90)
  # runs on the repo toolchain pin. Keep in sync with rust-toolchain.toml.
  RUST_TOOLCHAIN: "1.96.1"

jobs:
  publish:
    name: Publish to crates.io
    runs-on: ubuntu-latest
    permissions:
      # Needed to create the GitHub Release after a successful publish.
      contents: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
        with:
          toolchain: ${{ env.RUST_TOOLCHAIN }}

      - name: Install protoc
        uses: arduino/setup-protoc@f4d5893b897028ff5739576ea0409746887fa536 # v3.0.0
        with:
          version: "31.x"
          repo-token: ${{ github.token }}

      # Guard against tagging a release whose version doesn't match the
      # workspace. The tag drives the release; the manifest must agree.
      - name: Verify tag matches workspace version
        if: github.ref_type == 'tag'
        run: |
          tag="${GITHUB_REF_NAME#v}"
          ws=$(cargo metadata --format-version 1 --no-deps \
            | jq -r '.packages[] | select(.name=="macp-runtime") | .version')
          echo "tag=$tag workspace=$ws"
          if [ "$tag" != "$ws" ]; then
            echo "::error::tag $GITHUB_REF_NAME does not match workspace version $ws"
            exit 1
          fi

      # A release tag without release notes is a process bug: the CHANGELOG
      # must have a section for the version being published.
      - name: Verify CHANGELOG documents this version
        if: github.ref_type == 'tag'
        run: |
          tag="${GITHUB_REF_NAME#v}"
          if ! grep -Eq "^## \[$tag\]" CHANGELOG.md; then
            echo "::error::CHANGELOG.md has no '## [$tag]' section for this release"
            exit 1
          fi

      - name: Install cargo-semver-checks
        if: ${{ !inputs.skip_semver_checks }}
        uses: taiki-e/install-action@899b013517f9e7774591216672bf75a46bb9a481 # v2.9.4
        with:
          tool: cargo-semver-checks

      # Blocking: the workspace version bump must be adequate for the API
      # changes since the last published release (0.x minor bumps permit
      # breaking changes per cargo's semver interpretation). Escape hatch:
      # the skip_semver_checks dispatch input.
      - name: Semver check against latest published release
        if: ${{ !inputs.skip_semver_checks }}
        run: cargo semver-checks --workspace

      # `cargo publish --workspace` computes the dependency order itself, builds
      # each crate's verify step against its sibling path deps (so a dry-run
      # validates the whole graph without anything on the index yet), and waits
      # for each upload to appear on the index before publishing its dependents.
      #
      # Re-run safety: a crate whose current version is already live is passed
      # via --exclude, so re-running after a mid-release failure skips the
      # already-published members instead of erroring on "already exists".
      - name: Publish workspace to crates.io
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
          DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
        run: |
          set -euo pipefail

          is_published() {
            local name="$1" ver="$2" code
            code=$(curl -s -o /dev/null -w '%{http_code}' \
              -A "macp-runtime-release (https://github.com/multiagentcoordinationprotocol/macp-runtime)" \
              "https://crates.io/api/v1/crates/$name/$ver")
            [ "$code" = "200" ]
          }

          total=0
          exclude_args=()
          while read -r name ver; do
            total=$((total + 1))
            if is_published "$name" "$ver"; then
              echo "==> skip $name@$ver (already on crates.io)"
              exclude_args+=(--exclude "$name")
            fi
          done < <(cargo metadata --format-version 1 --no-deps \
            | jq -r '.packages[] | "\(.name) \(.version)"')

          if [ "${#exclude_args[@]}" -eq $((total * 2)) ]; then
            echo "All workspace crates already published; nothing to do."
            exit 0
          fi

          # ${arr[@]+...} guards against an empty array tripping `set -u`.
          if [ "${DRY_RUN:-false}" = "true" ]; then
            echo "==> dry-run publish workspace"
            cargo publish --workspace --dry-run --locked ${exclude_args[@]+"${exclude_args[@]}"}
          else
            echo "==> publish workspace"
            cargo publish --workspace --locked ${exclude_args[@]+"${exclude_args[@]}"}
          fi

      # Tag-driven releases also get a GitHub Release whose notes are the
      # CHANGELOG section for the version (verified to exist above).
      - name: Create GitHub Release
        if: github.ref_type == 'tag'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail
          tag="${GITHUB_REF_NAME#v}"
          # Extract the "## [<tag>]" section body (up to the next "## [").
          awk -v ver="$tag" '
            $0 ~ "^## \\[" ver "\\]" { hit = 1; next }
            hit && /^## \[/ { exit }
            hit { print }
          ' CHANGELOG.md > release-notes.md
          if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
            echo "release $GITHUB_REF_NAME already exists; skipping (re-run safe)"
          else
            gh release create "$GITHUB_REF_NAME" \
              --repo "$GITHUB_REPOSITORY" \
              --title "macp-runtime $GITHUB_REF_NAME" \
              --notes-file release-notes.md
          fi