name: Release
on:
workflow_run:
workflows: ["CI"]
types:
- completed
concurrency:
group: release-${{ github.event.workflow_run.head_sha }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
jobs:
check:
name: Check tag
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
skip: ${{ steps.tag.outputs.skip }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-tags: true
- name: Check that commit has a version tag
id: tag
shell: bash
run: |
COMMIT_SHA="${{ github.event.workflow_run.head_sha }}"
# || true: under `set -o pipefail`, grep exits 1 when the commit has
# no version tag; without it the empty-result case would abort the
# step before the skip branch below.
TAG_NAME=$(git tag --points-at "$COMMIT_SHA" | grep -E '^v[0-9]' | head -1 || true)
if [ -z "$TAG_NAME" ]; then
echo "ℹ️ Commit $COMMIT_SHA has no version tag — skipping binary build"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Found tag: $TAG_NAME"
echo "tag=$TAG_NAME" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
build:
name: ${{ matrix.target }}
needs: [check]
if: needs.check.outputs.skip != 'true'
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
ext: ""
archive: tar.gz
- target: x86_64-apple-darwin
os: macos-15
ext: ""
archive: tar.gz
- target: aarch64-apple-darwin
os: macos-latest
ext: ""
archive: tar.gz
- target: x86_64-pc-windows-msvc
os: windows-latest
ext: .exe
archive: zip
runs-on: ${{ matrix.os }}
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
fetch-tags: true
- name: Lookup version tag
id: lookup
shell: bash
run: |
COMMIT_SHA="${{ github.event.workflow_run.head_sha }}"
# || true: under `set -o pipefail`, grep exits 1 when the commit has
# no version tag; without it the empty-result case would abort the
# step before the skip branch below.
TAG_NAME=$(git tag --points-at "$COMMIT_SHA" | grep -E '^v[0-9]' | head -1 || true)
echo "tag=$TAG_NAME" >> "$GITHUB_OUTPUT"
echo "Building binaries for $TAG_NAME"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Build release binary
run: cargo build --release --target ${{ matrix.target }}
- name: Prepare archive
shell: bash
run: |
BINARY="forge-guard${{ matrix.ext }}"
SRC="target/${{ matrix.target }}/release/$BINARY"
STAGING="forge-guard-${{ matrix.target }}"
mkdir -p "$STAGING"
cp "$SRC" "$STAGING/"
cp README.md LICENSE "$STAGING/" 2>/dev/null || true
if [ "${{ matrix.archive }}" = "zip" ]; then
if command -v 7z &>/dev/null; then
7z a -tzip "${STAGING}.zip" "$STAGING/"
else
zip -r "${STAGING}.zip" "$STAGING/"
fi
echo "artifact=${STAGING}.zip" >> "$GITHUB_ENV"
else
tar czf "${STAGING}.tar.gz" "$STAGING/"
echo "artifact=${STAGING}.tar.gz" >> "$GITHUB_ENV"
fi
- name: Upload binary to GitHub Release
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
ARCHIVE="${{ env.artifact }}"
TAG="${{ steps.lookup.outputs.tag }}"
# The release is created by publish.yml, but a fast build can
# finish uploading before that workflow's create step lands.
# Create it here if missing so uploads never race the release.
# `|| true`: publish.yml may win the race and create it first;
# the create is idempotent on both sides (publish.yml edits
# existing releases instead of failing).
if ! gh release view "$TAG" --json tagName --jq '.tagName' &>/dev/null; then
echo "ℹ️ Release $TAG not found yet — creating it now"
gh release create "$TAG" \
--title "Forge Guard $TAG" \
--notes "Forge Guard $TAG" \
--latest \
2>/dev/null || true
fi
echo "Uploading $ARCHIVE to release $TAG ..."
gh release upload "$TAG" "$ARCHIVE" --clobber
echo "✅ Uploaded $ARCHIVE"
- name: Upload raw binary artifact (for CI reuse)
uses: actions/upload-artifact@v7
with:
name: forge-guard-${{ matrix.target }}
path: target/${{ matrix.target }}/release/forge-guard${{ matrix.ext }}
retention-days: 7
homebrew-formula:
name: Homebrew formula
needs: [check, build]
if: needs.check.outputs.skip != 'true'
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
fetch-tags: true
- name: Lookup version tag
id: lookup
shell: bash
run: |
COMMIT_SHA="${{ github.event.workflow_run.head_sha }}"
# || true: under `set -o pipefail`, grep exits 1 when the commit has
# no version tag; without it the empty-result case would abort the
# step before the skip branch below.
TAG_NAME=$(git tag --points-at "$COMMIT_SHA" | grep -E '^v[0-9]' | head -1 || true)
echo "tag=$TAG_NAME" >> "$GITHUB_OUTPUT"
echo "Building Homebrew formula for $TAG_NAME"
- name: Install cargo-dist
run: |
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/axodotdev/cargo-dist/releases/download/v0.28.0/cargo-dist-installer.sh | sh
- name: Download archives and compute checksums
shell: bash
run: |
set -euo pipefail
TAG="${{ steps.lookup.outputs.tag }}"
mkdir -p target/distrib
cd target/distrib
# Homebrew supports macOS and Linux only (no Windows formula)
for ART in \
forge-guard-aarch64-apple-darwin.tar.gz \
forge-guard-x86_64-apple-darwin.tar.gz \
forge-guard-x86_64-unknown-linux-gnu.tar.gz; do
gh release download "$TAG" -p "$ART" --clobber
done
# Synthesize the per-target dist manifests cargo-dist merges for
# checksums, so the generated formula pins the exact sha256 of the
# archives uploaded to the release.
TAG="$TAG" python3 - <<'EOF'
import hashlib, json, os
entries = {
"forge-guard-aarch64-apple-darwin.tar.gz": "aarch64-apple-darwin",
"forge-guard-x86_64-apple-darwin.tar.gz": "x86_64-apple-darwin",
"forge-guard-x86_64-unknown-linux-gnu.tar.gz": "x86_64-unknown-linux-gnu",
}
artifacts = {}
for name, triple in entries.items():
digest = hashlib.sha256(open(name, "rb").read()).hexdigest()
artifacts[name] = {
"name": name,
"kind": "executable-zip",
"target_triples": [triple],
"checksums": {"sha256": digest},
}
manifest = {"announcement_tag": os.environ["TAG"], "artifacts": artifacts}
with open("x86_64-unknown-linux-gnu-dist-manifest.json", "w") as f:
json.dump(manifest, f)
EOF
- name: Generate formula with cargo-dist
shell: bash
run: |
dist build --artifacts=global --tag="${{ steps.lookup.outputs.tag }}" \
--output-format=json > /dev/null
test -f target/distrib/forge-guard.rb
echo "✅ Generated target/distrib/forge-guard.rb"
- name: Publish formula to Homebrew tap
if: ${{ env.TAP_TOKEN != '' }}
shell: bash
run: |
set -euo pipefail
TAG="${{ steps.lookup.outputs.tag }}"
VERSION="${TAG#v}"
git clone "https://x-access-token:${TAP_TOKEN}@github.com/codetibo/homebrew-tap.git" tap
mkdir -p tap/Formula
cp target/distrib/forge-guard.rb tap/Formula/forge-guard.rb
cd tap
git config user.name "Forge Guard Bot"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Formula/forge-guard.rb
if git diff --cached --quiet; then
echo "ℹ️ Formula unchanged — nothing to commit"
exit 0
fi
git commit -m "forge-guard ${VERSION}"
git push
echo "✅ Published forge-guard ${VERSION} formula to codetibo/homebrew-tap"
- name: Warn about missing tap token
if: ${{ env.TAP_TOKEN == '' }}
shell: bash
run: |
echo "⚠️ HOMEBREW_TAP_TOKEN secret is not set — formula was generated"
echo " but not published. Add a PAT with write access to"
echo " codetibo/homebrew-tap as the HOMEBREW_TAP_TOKEN repo secret."