portail 2.1.0

Unified proxy/gateway: AI Gateway + MCP Gateway + CDN cache
Documentation
name: Release

# Security: Release workflow only runs on version tags.
# Self-hosted runners are used for Linux builds.
# macOS builds use GitHub-hosted runners.
#
# IMPORTANT: Only maintainers can push tags.
# Configure branch protection rules to enforce this.

on:
  push:
    tags: ["v*"]
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: always
  RUSTFLAGS: -D warnings
  CARGO_INCREMENTAL: 0

jobs:
  # ── Security Gate ─────────────────────────────────────────────
  verify-tag:
    runs-on: blacksmith
    outputs:
      version: ${{ steps.version.outputs.version }}
    steps:
      - name: Verify tag format
        id: version
        run: |
          TAG="${{ github.ref_name }}"
          if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "::error::Invalid tag format. Must be vX.Y.Z"
            exit 1
          fi
          echo "version=${TAG#v}" >> $GITHUB_OUTPUT

  # ── Linux Build (self-hosted) ─────────────────────────────────
  release-linux:
    needs: verify-tag
    runs-on: self-hosted
    strategy:
      matrix:
        target:
          - x86_64-unknown-linux-gnu
    steps:
      - uses: actions/checkout@v7

      - name: Install Rust toolchain
        uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          target: ${{ matrix.target }}

      - name: Build release binary
        run: cargo build --release --target ${{ matrix.target }} --bin portail

      - name: Install UPX
        uses: crazy-max/ghaction-upx@v4
        with:
          install-only: true

      - name: Compress with UPX
        run: |
          binary="target/${{ matrix.target }}/release/portail"
          upx --best --lzma "$binary"
          mv "$binary" "portail-${{ matrix.target }}"

      - name: Generate checksums
        run: |
          sha256sum portail-${{ matrix.target }} > portail-${{ matrix.target }}.sha256
          cat portail-${{ matrix.target }}.sha256

      - name: Install cosign
        uses: sigstore/cosign-installer@v4

      - name: Sign binary with cosign (keyless)
        run: |
          cosign sign-blob --yes \
            --output-signature portail-${{ matrix.target }}.sig \
            --output-certificate portail-${{ matrix.target }}.pem \
            portail-${{ matrix.target }}

      - uses: actions/upload-artifact@v7
        with:
          name: portail-${{ matrix.target }}
          path: |
            portail-${{ matrix.target }}
            portail-${{ matrix.target }}.sha256
            portail-${{ matrix.target }}.sig
            portail-${{ matrix.target }}.pem

  # ── macOS Build (GitHub-hosted) ───────────────────────────────
  release-macos:
    needs: verify-tag
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v7

      - name: Install Rust toolchain
        uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          target: aarch64-apple-darwin

      - name: Build release binary
        run: cargo build --release --target aarch64-apple-darwin --bin portail

      - name: Compress with UPX
        run: |
          # UPX doesn't support macOS arm64, just rename
          mv target/aarch64-apple-darwin/release/portail portail-aarch64-apple-darwin

      - name: Generate checksums
        run: |
          sha256sum portail-aarch64-apple-darwin > portail-aarch64-apple-darwin.sha256

      - name: Install cosign
        uses: sigstore/cosign-installer@v4

      - name: Sign binary with cosign (keyless)
        run: |
          cosign sign-blob --yes \
            --output-signature portail-aarch64-apple-darwin.sig \
            --output-certificate portail-aarch64-apple-darwin.pem \
            portail-aarch64-apple-darwin

      - uses: actions/upload-artifact@v7
        with:
          name: portail-aarch64-apple-darwin
          path: |
            portail-aarch64-apple-darwin
            portail-aarch64-apple-darwin.sha256
            portail-aarch64-apple-darwin.sig
            portail-aarch64-apple-darwin.pem

  # ── Release Audit ─────────────────────────────────────────────
  release-audit:
    needs: [release-linux, release-macos]
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v7

      - uses: actions/download-artifact@v8
        with:
          pattern: portail-*
          merge-multiple: true
          path: dist/

      - name: Install Rust toolchain
        uses: actions-rust-lang/setup-rust-toolchain@v1

      - name: Build release-audit binary (fast, no LTO)
        run: cargo build --release --bin portail
        env:
          CARGO_PROFILE_RELEASE_LTO: false
          CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 256
          CARGO_PROFILE_RELEASE_STRIP: none
          CARGO_PROFILE_RELEASE_DEBUG: false

      - name: Run release audit
        id: audit
        run: |
          VERSION="${{ needs.verify-tag.outputs.version }}"
          ./target/release/portail release-audit \
            --dir dist/ \
            --version "$VERSION" \
            --out audit/

      - name: Upload audit artifacts
        uses: actions/upload-artifact@v7
        with:
          name: release-audit
          path: audit/

  # ── GitHub Release ────────────────────────────────────────────
  github-release:
    needs: [release-linux, release-macos, release-audit]
    runs-on: self-hosted
    permissions:
      contents: write
      id-token: write
    steps:
      - uses: actions/download-artifact@v8
        with:
          pattern: portail-*
          merge-multiple: true

      - uses: actions/download-artifact@v8
        with:
          name: release-audit
          path: audit/

      - name: Verify cosign signatures
        uses: sigstore/cosign-installer@v4
      - run: |
          for f in portail-*.sig; do
            stem="${f%.sig}"
            cosign verify-blob --cert "$stem.pem" --signature "$f" "$stem"
          done

      - name: Generate combined SHA256SUMS
        run: cat portail-*.sha256 | tee SHA256SUMS

      - name: Sign combined checksums
        run: |
          cosign sign-blob --yes \
            --output-signature SHA256SUMS.sig \
            --output-certificate SHA256SUMS.pem \
            SHA256SUMS

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v3
        with:
          name: Portail ${{ github.ref_name }}
          body_path: CHANGELOG.md
          files: |
            portail-*
            SHA256SUMS
            SHA256SUMS.sig
            SHA256SUMS.pem
            audit/*
          generate_release_notes: true

  # ── Publish to crates.io ──────────────────────────────────────
  # cargo publish needs no self-hosted runner — GitHub-hosted is sufficient
  # and more reliable (no dependency on the org's self-hosted fleet).
  crates-io:
    needs: [github-release]
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v7
      - uses: actions-rust-lang/setup-rust-toolchain@v1
      - name: Publish to crates.io
        run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }}