name: release
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:
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
- 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 }}*
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 }}
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
homebrew:
name: homebrew tap bump
needs: release
runs-on: ubuntu-latest
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