forjar 1.6.1

Rust-native Infrastructure as Code — bare-metal first, BLAKE3 state, provenance tracing
Documentation
# Per-repo release workflow — tagged releases only
# Spec: docs/specifications/sovereign-stack-protected-branch-strategy.md
#
# Flow: tag push → clean-room gate → package verify → trusted publish → GitHub Release
#       → cross-compiled binaries (4 Linux + 2 macOS targets) → SHA256SUMS
#       → distribution artifacts (installer, homebrew, nix, deb, rpm)
#
# Tag formats:
#   v1.0.0              — single-crate repos
#   v-<crate>-1.0.0     — workspace repos (e.g. v-apr-cli-0.4.0)
#
# IMPORTANT: Tags must be pushed with a PAT or deploy key (not GITHUB_TOKEN),
# otherwise this workflow will not trigger (GitHub anti-recursion measure).

name: Release

on:
  push:
    tags: ['v*']
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag to release (e.g., v1.2.0)'
        required: true

env:
  RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}

permissions:
  contents: write    # create GitHub Release + upload assets
  # id-token: write — removed; crates.io publish handled out-of-band (not via OIDC)

# One release at a time per repo
concurrency:
  group: release-${{ github.repository }}
  cancel-in-progress: false

jobs:
  # ── Verify: tag-version match + package tarball ─────────
  verify:
    runs-on: [self-hosted, clean-room]
    outputs:
      crate_name: ${{ steps.parse.outputs.crate_name }}
      version: ${{ steps.parse.outputs.version }}
      has_binaries: ${{ steps.bincheck.outputs.has_binaries }}
    steps:
      - name: Checkout
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10  # v6.0.3

      - name: Parse tag and verify version
        id: parse
        run: |
          TAG="${RELEASE_TAG}"

          # Parse tag format: v1.0.0 or v-cratename-1.0.0
          if [[ "$TAG" =~ ^v-([a-z][a-z0-9_-]*)-([0-9]+\..+)$ ]]; then
            CRATE_NAME="${BASH_REMATCH[1]}"
            TAG_VER="${BASH_REMATCH[2]}"
            echo "Workspace release: crate=$CRATE_NAME version=$TAG_VER"
          elif [[ "$TAG" =~ ^v([0-9]+\..+)$ ]]; then
            CRATE_NAME=""
            TAG_VER="${BASH_REMATCH[1]}"
            echo "Single-crate release: version=$TAG_VER"
          else
            echo "::error::Tag '$TAG' does not match expected format (v1.0.0 or v-crate-1.0.0)"
            exit 1
          fi

          # Use cargo metadata for reliable version extraction
          if [ -n "$CRATE_NAME" ]; then
            CARGO_VER=$(cargo metadata --format-version 1 --no-deps \
              | jq -r ".packages[] | select(.name == \"$CRATE_NAME\") | .version")
            if [ -z "$CARGO_VER" ] || [ "$CARGO_VER" = "null" ]; then
              echo "::error::Crate '$CRATE_NAME' not found in workspace"
              exit 1
            fi
          else
            CARGO_VER=$(cargo metadata --format-version 1 --no-deps \
              | jq -r '.packages[0].version')
          fi

          if [ "$TAG_VER" != "$CARGO_VER" ]; then
            echo "::error::Tag version $TAG_VER != Cargo.toml version $CARGO_VER"
            exit 1
          fi

          echo "crate_name=$CRATE_NAME" >> "$GITHUB_OUTPUT"
          echo "version=$TAG_VER" >> "$GITHUB_OUTPUT"
          echo "Version verified: $TAG_VER"

      - name: Detect binary targets
        id: bincheck
        run: |
          HAS_BINS=$(cargo metadata --format-version 1 --no-deps \
            | jq '[.packages[].targets[] | select(.kind[] == "bin")] | length')
          echo "has_binaries=$( [ "$HAS_BINS" -gt 0 ] && echo true || echo false )" >> "$GITHUB_OUTPUT"
          echo "Binary targets found: $HAS_BINS"

      - name: Verify package tarball
        run: |
          CRATE="${{ steps.parse.outputs.crate_name }}"
          if [ -n "$CRATE" ]; then
            cargo package -p "$CRATE"
          else
            cargo package
          fi

  # ── crates.io publish: handled out-of-band ─────────────
  # The OIDC trusted-publishing job was removed. Trusted Publishing is not
  # configured for paiml/forjar on crates.io, and the workflow needs a
  # corresponding registry-side trust policy that we don't currently maintain.
  # Until that's set up (https://crates.io/trusted-publishing), publish to
  # crates.io manually from a host with `~/.cargo/credentials.toml`:
  #
  #     git checkout v<x.y.z> && cargo publish
  #
  # This workflow now only builds binary release artifacts and creates the
  # GitHub Release.

  # ── Create GitHub Release ─────────────────────────────
  create-release:
    needs: verify
    runs-on: [self-hosted, clean-room]
    steps:
      - name: Create or update GitHub Release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # Idempotent: create if missing, otherwise leave existing release
          # (and its hand-written notes) intact so re-runs don't clobber them.
          if gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
            echo "Release $RELEASE_TAG already exists — leaving notes intact, downstream jobs will upload assets."
          else
            gh release create "$RELEASE_TAG" \
              --title "$RELEASE_TAG" \
              --generate-notes \
              --repo "${{ github.repository }}"
          fi

  # ── Build cross-compiled binaries (4 Linux + 2 macOS targets) ────
  build-binaries:
    needs: [verify, create-release]
    if: needs.verify.outputs.has_binaries == 'true'
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            runner: [self-hosted, clean-room]
          - target: x86_64-unknown-linux-musl
            runner: [self-hosted, clean-room]
          - target: aarch64-unknown-linux-gnu
            runner: [self-hosted, clean-room]
          - target: aarch64-unknown-linux-musl
            runner: [self-hosted, clean-room]
          - target: x86_64-apple-darwin
            runner: macos-latest
          - target: aarch64-apple-darwin
            runner: macos-latest
    runs-on: ${{ matrix.runner }}
    steps:
      - name: Checkout
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10  # v6.0.3

      - name: Install Rust toolchain (macOS)
        if: contains(matrix.target, 'darwin')
        run: |
          curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
          echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"

      - name: Install target prerequisites (Linux)
        if: contains(matrix.target, 'linux')
        run: |
          case "${{ matrix.target }}" in
            *-musl)
              if command -v apt-get &>/dev/null; then
                sudo apt-get update -qq
                sudo apt-get install -y -qq musl-tools >/dev/null
              fi
              rustup target add "${{ matrix.target }}" || true
              ;;
          esac
          case "${{ matrix.target }}" in
            aarch64-*)
              if ! command -v cross &>/dev/null; then
                cargo install cross --locked
              fi
              ;;
          esac

      - name: Discover binary names
        id: bins
        run: |
          BINS=$(cargo metadata --format-version 1 --no-deps \
            | jq -r '[.packages[].targets[] | select(.kind[] == "bin") | .name] | join(" ")')
          echo "names=$BINS" >> "$GITHUB_OUTPUT"
          echo "Binaries to build: $BINS"

      - name: Build release binaries
        run: |
          case "${{ matrix.target }}" in
            aarch64-unknown-linux-*)
              cross build --release --features vendored-openssl --target "${{ matrix.target }}"
              ;;
            *)
              cargo build --release --features vendored-openssl --target "${{ matrix.target }}"
              ;;
          esac

      - name: Package binaries
        run: |
          VERSION="${{ needs.verify.outputs.version }}"
          TARGET="${{ matrix.target }}"
          STAGING="/tmp/release-staging"
          mkdir -p "$STAGING"

          for BIN in ${{ steps.bins.outputs.names }}; do
            ARCHIVE="${BIN}-${VERSION}-${TARGET}.tar.gz"
            tar -czf "${STAGING}/${ARCHIVE}" \
              -C "target/${TARGET}/release" "$BIN"
            echo "Packaged: $ARCHIVE"
          done

      - name: Upload artifacts
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
        with:
          name: binaries-${{ matrix.target }}
          path: /tmp/release-staging/*.tar.gz
          retention-days: 5

  # ── Generate checksums and upload to GitHub Release ─────
  checksums:
    needs: [verify, create-release, build-binaries]
    if: needs.verify.outputs.has_binaries == 'true'
    runs-on: [self-hosted, clean-room]
    steps:
      - name: Download all binary artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8.0.1
        with:
          pattern: binaries-*
          merge-multiple: true
          path: /tmp/release-assets

      - name: Generate SHA256SUMS
        working-directory: /tmp/release-assets
        run: |
          sha256sum *.tar.gz > SHA256SUMS
          echo "=== SHA256SUMS ==="
          cat SHA256SUMS

      - name: Upload to GitHub Release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        working-directory: /tmp/release-assets
        run: |
          gh release upload "$RELEASE_TAG" \
            *.tar.gz SHA256SUMS \
            --repo "${{ github.repository }}" \
            --clobber

  # ── Generate distribution artifacts (installer, homebrew, nix, deb, rpm) ──
  dist-artifacts:
    needs: [verify, checksums]
    if: needs.verify.outputs.has_binaries == 'true'
    runs-on: [self-hosted, clean-room]
    steps:
      - name: Checkout
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10  # v6.0.3

      - name: Generate distribution artifacts
        run: |
          cargo run --release -- dist \
            -f dist-forjar.yaml \
            --all \
            --output-dir /tmp/dist-output

      - name: Upload installer to GitHub Release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh release upload "$RELEASE_TAG" \
            /tmp/dist-output/install.sh \
            --repo "${{ github.repository }}" \
            --clobber

      - name: Upload dist artifacts
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
        with:
          name: dist-artifacts
          path: /tmp/dist-output/
          retention-days: 30

  # ── Publish Homebrew formula to tap ──────────────────────
  homebrew:
    needs: [verify, dist-artifacts]
    if: needs.verify.outputs.has_binaries == 'true'
    runs-on: [self-hosted, clean-room]
    steps:
      - name: Checkout
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10  # v6.0.3

      - name: Download dist artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8.0.1
        with:
          name: dist-artifacts
          path: /tmp/dist-output

      - name: Download release checksums
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh release download "$RELEASE_TAG" \
            --pattern "SHA256SUMS" \
            --dir /tmp/checksums \
            --repo "${{ github.repository }}"

      - name: Patch Homebrew formula with real checksums and version
        run: |
          VERSION="${{ needs.verify.outputs.version }}"
          FORMULA="/tmp/dist-output/homebrew.rb"

          # Replace VERSION placeholder
          sed -i "s/version \"VERSION\"/version \"${VERSION}\"/" "$FORMULA"

          # Replace PLACEHOLDER_CHECKSUM for each target
          for TARGET_ASSET in \
            "forjar-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" \
            "forjar-${VERSION}-aarch64-unknown-linux-gnu.tar.gz" \
            "forjar-${VERSION}-x86_64-apple-darwin.tar.gz" \
            "forjar-${VERSION}-aarch64-apple-darwin.tar.gz"; do
            CHECKSUM=$(grep "$TARGET_ASSET" /tmp/checksums/SHA256SUMS | awk '{print $1}' || echo "")
            if [ -n "$CHECKSUM" ]; then
              # Replace first occurrence of PLACEHOLDER_CHECKSUM
              sed -i "0,/PLACEHOLDER_CHECKSUM/s/PLACEHOLDER_CHECKSUM/${CHECKSUM}/" "$FORMULA"
            fi
          done

          echo "=== Patched formula ==="
          cat "$FORMULA"

      - name: Publish to Homebrew tap
        env:
          GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
        run: |
          VERSION="${{ needs.verify.outputs.version }}"
          FORMULA="/tmp/dist-output/homebrew.rb"

          # Clone the tap repo
          git clone "https://x-access-token:${GH_TOKEN}@github.com/paiml/homebrew-tap.git" /tmp/tap
          cp "$FORMULA" /tmp/tap/Formula/forjar.rb

          cd /tmp/tap
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add Formula/forjar.rb
          git commit -m "forjar ${VERSION}" || echo "No changes to commit"
          git push