aperion-shield 1.0.1

Aperion Shield -- a local MCP guardrail for AI coding agents with optional biometric identity gates (ID.me). Standalone, free, open source.
Documentation
name: release

# Build and publish aperion-shield binaries + Docker image + Homebrew tap
# bump on every git tag matching `shield-v*` (e.g. shield-v0.1.0).

on:
  push:
    tags:
      - 'shield-v*'
  workflow_dispatch:
    inputs:
      tag:
        description: "Tag to release as (e.g. shield-v0.1.0). Required for manual runs."
        required: true

jobs:
  # ────────────────────────────────────────────────────────────────────
  # 1. Build per-target binaries
  # ────────────────────────────────────────────────────────────────────
  build:
    name: build (${{ matrix.target }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os:     ubuntu-latest
            archive: tar.gz
          - target: aarch64-unknown-linux-gnu
            os:     ubuntu-latest
            archive: tar.gz
            linker:  aarch64-linux-gnu-gcc
          # Both macOS targets run on macos-14 (Apple Silicon). The x86_64
          # binary is produced via cross-compilation (`rustup target add
          # x86_64-apple-darwin`) -- the macos-13 runner has been
          # deprecated by GitHub and the queue is effectively dead.
          - target: x86_64-apple-darwin
            os:     macos-14
            archive: tar.gz
          - target: aarch64-apple-darwin
            os:     macos-14
            archive: tar.gz
          - target: x86_64-pc-windows-msvc
            os:     windows-latest
            archive: zip
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Install cross-linker (aarch64-linux only)
        if: matrix.linker == 'aarch64-linux-gnu-gcc'
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-aarch64-linux-gnu
          mkdir -p .cargo
          echo "[target.aarch64-unknown-linux-gnu]"     >> .cargo/config.toml
          echo "linker = \"aarch64-linux-gnu-gcc\""    >> .cargo/config.toml

      - name: Build release
        run: cargo build --release --locked --target ${{ matrix.target }}

      - name: Package
        shell: bash
        run: |
          mkdir -p dist
          BIN=aperion-shield
          if [[ "${{ matrix.target }}" == *windows* ]]; then BIN=aperion-shield.exe; fi
          cp target/${{ matrix.target }}/release/${BIN} dist/${BIN}
          cp README.md LICENSE 2>/dev/null || true
          cp shield.example.yaml dist/ 2>/dev/null || true
          cd dist
          ARCHIVE="aperion-shield-${{ github.ref_name }}-${{ matrix.target }}.${{ matrix.archive }}"
          if [[ "${{ matrix.archive }}" == "zip" ]]; then
            7z a "${ARCHIVE}" *
          else
            tar -czf "${ARCHIVE}" *
          fi
          # Portable sha256: shasum on macOS/Linux, sha256sum on most
          # Linux distros, certutil on Windows (no shasum on the
          # windows-latest runner).
          if command -v shasum >/dev/null 2>&1; then
            shasum -a 256 "${ARCHIVE}" > "${ARCHIVE}.sha256"
          elif command -v sha256sum >/dev/null 2>&1; then
            sha256sum "${ARCHIVE}" > "${ARCHIVE}.sha256"
          else
            HASH=$(certutil -hashfile "${ARCHIVE}" SHA256 | sed -n '2p' | tr -d ' \r\n')
            echo "${HASH}  ${ARCHIVE}" > "${ARCHIVE}.sha256"
          fi

      - uses: actions/upload-artifact@v4
        with:
          name: aperion-shield-${{ matrix.target }}
          path: dist/aperion-shield-*.${{ matrix.archive }}*

  # ────────────────────────────────────────────────────────────────────
  # 2. Build & push the Docker image (multi-arch)
  # ────────────────────────────────────────────────────────────────────
  docker:
    name: docker (linux/amd64, linux/arm64)
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build & push
        uses: docker/build-push-action@v5
        with:
          context:   .
          file:      Dockerfile
          platforms: linux/amd64,linux/arm64
          push:      true
          tags: |
            ghcr.io/aperionai/shield:latest
            ghcr.io/aperionai/shield:${{ github.ref_name }}

  # ────────────────────────────────────────────────────────────────────
  # 3. Publish a GitHub release with every per-target archive
  # ────────────────────────────────────────────────────────────────────
  release:
    name: github release
    needs: [build, docker]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          path: dist
          merge-multiple: true

      - name: Create release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ github.ref_name }}
          name:     ${{ github.ref_name }}
          generate_release_notes: true
          fail_on_unmatched_files: true
          files: |
            dist/*.tar.gz
            dist/*.zip
            dist/*.sha256

  # ────────────────────────────────────────────────────────────────────
  # 4. Update the Homebrew tap
  # ────────────────────────────────────────────────────────────────────
  #
  # We do NOT use `brew bump-formula-pr` / dawidd6/action-homebrew-bump-
  # formula here: our formula is a multi-platform layout (on_macos /
  # on_linux with per-arch `url` + `sha256` stanzas, no single top-level
  # `url`), and `bump-formula-pr` fails on it with "Could not find 'url'
  # stanza!". Instead we template the whole formula from the release
  # checksums -- exactly the steps a maintainer ran by hand before this.
  homebrew:
    name: homebrew tap bump
    needs: release
    runs-on: ubuntu-latest
    # Requires a fine-grained PAT (`HOMEBREW_TAP_TOKEN`) with write to
    # the AperionAI/homebrew-tap repository. Job runs every release; the
    # bump step is skipped automatically if the secret is unset (secrets
    # cannot be referenced from job-level `if:` expressions).
    steps:
      - name: Detect Homebrew tap token
        id: check
        env:
          TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
        run: |
          if [ -n "$TAP_TOKEN" ]; then
            echo "has_token=true"  >> "$GITHUB_OUTPUT"
          else
            echo "has_token=false" >> "$GITHUB_OUTPUT"
            echo "::notice::HOMEBREW_TAP_TOKEN secret not set -- skipping tap bump."
          fi

      - name: Check out the Homebrew tap
        if: steps.check.outputs.has_token == 'true'
        uses: actions/checkout@v4
        with:
          repository: AperionAI/homebrew-tap
          token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
          path: tap

      - name: Render & bump aperion-shield formula
        if: steps.check.outputs.has_token == 'true'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG: ${{ github.ref_name }}
        run: |
          set -euo pipefail
          VERSION="${TAG#shield-v}"
          BASE="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}"

          # Pull the per-target sha256 for one release archive. Homebrew
          # only needs the macOS + Linux targets.
          sha_for() {
            local t="$1"
            local f="aperion-shield-${TAG}-${t}.tar.gz.sha256"
            gh release download "${TAG}" -R "${GITHUB_REPOSITORY}" -p "${f}" -O "${f}" >&2
            cut -d' ' -f1 "${f}"
          }
          SHA_MAC_ARM="$(sha_for aarch64-apple-darwin)"
          SHA_MAC_X86="$(sha_for x86_64-apple-darwin)"
          SHA_LIN_ARM="$(sha_for aarch64-unknown-linux-gnu)"
          SHA_LIN_X86="$(sha_for x86_64-unknown-linux-gnu)"

          mkdir -p tap/Formula
          cat > tap/Formula/aperion-shield.rb <<EOF
          class AperionShield < Formula
            desc "Local MCP guardrail for AI coding agents (Cursor, Claude Code, ...)"
            homepage "https://github.com/AperionAI/shield"
            version "${VERSION}"
            license "Apache-2.0"

            on_macos do
              on_arm do
                url "${BASE}/aperion-shield-${TAG}-aarch64-apple-darwin.tar.gz"
                sha256 "${SHA_MAC_ARM}"
              end
              on_intel do
                url "${BASE}/aperion-shield-${TAG}-x86_64-apple-darwin.tar.gz"
                sha256 "${SHA_MAC_X86}"
              end
            end

            on_linux do
              on_arm do
                url "${BASE}/aperion-shield-${TAG}-aarch64-unknown-linux-gnu.tar.gz"
                sha256 "${SHA_LIN_ARM}"
              end
              on_intel do
                url "${BASE}/aperion-shield-${TAG}-x86_64-unknown-linux-gnu.tar.gz"
                sha256 "${SHA_LIN_X86}"
              end
            end

            def install
              bin.install "aperion-shield"
              (etc/"aperion-shield").install "shield.example.yaml" if File.exist?("shield.example.yaml")
              doc.install "README.md" if File.exist?("README.md")
              doc.install "LICENSE" if File.exist?("LICENSE")
            end

            test do
              assert_match "Aperion Shield", shell_output("#{bin}/aperion-shield --help 2>&1")
              assert_match version.to_s, shell_output("#{bin}/aperion-shield --version 2>&1")
              pipe_output("#{bin}/aperion-shield --check", "", 0)
            end
          end
          EOF

          # Fail loudly if the rendered formula is malformed Ruby.
          ruby -c tap/Formula/aperion-shield.rb

      - name: Commit & push the bump
        if: steps.check.outputs.has_token == 'true'
        run: |
          set -euo pipefail
          cd tap
          git config user.name  "aperion-release-bot"
          git config user.email "release-bot@aperion.ai"
          if git diff --quiet -- Formula/aperion-shield.rb; then
            echo "::notice::formula already current for ${GITHUB_REF_NAME} -- nothing to push."
            exit 0
          fi
          git add Formula/aperion-shield.rb
          git commit -m "aperion-shield ${GITHUB_REF_NAME}"
          git push