forge-guard 0.3.6

Pre-deployment smart contract auditing framework for Foundry
Documentation
# ═══════════════════════════════════════════════════════════════════════
#  Release — binary build & upload
# ═══════════════════════════════════════════════════════════════════════
#
# Builds pre-compiled binaries for Linux, macOS (x86_64 + ARM), and
# Windows after CI completes, then uploads them to the GitHub Release
# created by the Publish workflow.
#
# Trigger: after CI completes successfully on a tag commit
#
name: Release

on:
  workflow_run:
    workflows: ["CI"]
    types:
      - completed

# Only one Release run per commit — both the main-branch push and the tag
# push complete CI and fire workflow_run; without this, the 4-platform build
# matrix would run twice.
concurrency:
  group: release-${{ github.event.workflow_run.head_sha }}
  cancel-in-progress: false

env:
  CARGO_TERM_COLOR: always

jobs:
  # ── Check tag ─────────────────────────────────────────────────────
  # Verify CI passed and the commit has a version tag
  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 ─────────────────────────────────────────────────────────
  # Native build for each target platform, archive binary, upload to
  # the GitHub Release (created by publish.yml).
  #
  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 ──────────────────────────────────────────────
  # Generates the cargo-dist Homebrew formula from the archives the
  # build matrix just uploaded, then publishes it to the
  # codetibo/homebrew-tap tap so users can `brew install codetibo/tap/forge-guard`.
  #
  # Needs the HOMEBREW_TAP_TOKEN secret: a PAT with write access to the
  # tap repo (GITHUB_TOKEN can't write to a different repository).
  #
  homebrew-formula:
    name: Homebrew formula
    needs: [check, build]
    if: needs.check.outputs.skip != 'true'
    runs-on: ubuntu-latest
    env:
      GH_TOKEN: ${{ github.token }}
      # Set as a job env so it can be used in `if:` conditionals
      # (secrets aren't directly available in `if:` expressions).
      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."