gatehouse 0.5.1

An in-process authorization engine for Rust with composable policies and request-scoped fact loading.
Documentation
name: Release

on:
  push:
    tags:
      - "v*"

concurrency:
  group: release-${{ github.ref }}
  cancel-in-progress: false

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:
  version-check:
    name: Verify tag and crate version
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - name: Require tag to match Cargo.toml
        env:
          TAG: ${{ github.ref_name }}
        run: |
          set -euo pipefail
          python3 - <<'PY'
          import os
          import sys
          import tomllib

          with open("Cargo.toml", "rb") as file:
              crate_version = tomllib.load(file)["package"]["version"]

          tag = os.environ["TAG"]
          expected_tag = f"v{crate_version}"
          if tag != expected_tag:
              print(
                  f"::error file=Cargo.toml::tag {tag!r} does not match "
                  f"crate version {crate_version!r}; expected {expected_tag!r}"
              )
              sys.exit(1)

          print(f"Verified {tag} matches gatehouse {crate_version}")
          PY

  candidate-ci:
    name: Verify exact-candidate CI
    needs: version-check
    permissions:
      actions: read
      contents: read
    runs-on: ubuntu-latest
    steps:
      - name: Require successful main CI on this commit
        env:
          GH_TOKEN: ${{ github.token }}
          EXPECTED_SHA: ${{ github.sha }}
        run: |
          set -euo pipefail
          count=$(gh api -X GET \
            "repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs" \
            -f branch=main \
            -f head_sha="${EXPECTED_SHA}" \
            -f event=push \
            -f status=success \
            -f per_page=100 \
            --jq '.total_count')

          if [ "$count" -lt 1 ]; then
            echo "::error::No successful main push CI run exists for ${EXPECTED_SHA}."
            echo "Merge the release preparation, wait for main CI, then tag that exact commit."
            exit 1
          fi

          echo "Found ${count} successful main CI run(s) for ${EXPECTED_SHA}."

  verify-package:
    name: Audit and verify package
    needs: candidate-ci
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
        with:
          key: "release-v1"
      - uses: taiki-e/install-action@v2
        with:
          tool: cargo-audit
      - name: Audit dependencies
        run: cargo audit
      - name: Verify publishable crate
        run: cargo publish --dry-run --locked

  publish-crate:
    name: Publish to crates.io
    needs: verify-package
    permissions:
      contents: read
      id-token: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
      - name: Authenticate to crates.io
        id: crates-io-auth
        uses: rust-lang/crates-io-auth-action@v1
      - name: Publish crate
        run: cargo publish --locked
        env:
          CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token }}

  github-release:
    name: Publish GitHub Release
    needs: publish-crate
    permissions:
      contents: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - name: Create release from changelog
        env:
          GH_TOKEN: ${{ github.token }}
          GH_REPO: ${{ github.repository }}
          TAG: ${{ github.ref_name }}
        run: |
          set -euo pipefail

          if gh release view "$TAG" >/dev/null 2>&1; then
            echo "::error::GitHub Release ${TAG} already exists. Release by pushing the tag only."
            exit 1
          fi

          awk -v section="## [${TAG#v}]" '
            index($0, section) == 1 { inside = 1; next }
            inside && /^## \[/ { exit }
            inside { print }
          ' CHANGELOG.md > release-notes.md

          args=(--verify-tag --title "$TAG" --generate-notes)
          if [ -s release-notes.md ]; then
            args+=(--notes-file release-notes.md)
          fi
          if [[ "$TAG" == *-* ]]; then
            args+=(--prerelease)
          fi

          gh release create "$TAG" "${args[@]}"