forjar 1.6.1

Rust-native Infrastructure as Code — bare-metal first, BLAKE3 state, provenance tracing
Documentation
name: Binary Release

# Attaches cross-compiled `forjar` binaries to a GitHub Release as tar.gz +
# sha256 on github-hosted runners. This runs in parallel with the existing
# release.yml (which uses [self-hosted, clean-room] runners) and is the
# durable path for binary attachment — github-hosted runners are not
# subject to self-hosted infra availability/queueing.
#
# Background: v1.3.0 only had 1/6 expected binaries attached because the
# self-hosted matrix in release.yml didn't complete reliably. This workflow
# reliably attaches 4 Linux binaries (musl + gnu × x86_64 + aarch64).
# macOS targets are intentionally deferred to a follow-up.
#
# Triggered by:
#   - `push: tags v*` — the durable path: creates the GitHub Release itself
#     (idempotently) on a hosted runner, so a tag always produces a release
#     with binaries even when release.yml's self-hosted verify job fails
#     or the clean-room queue is down.
#   - `release: published` — fires when a release is published by hand
#   - `workflow_dispatch` — manual re-run / backfill, takes a tag input
#
# Targets: 4 Linux (x86_64/aarch64 × musl/gnu). musl variants are static
# and ideal for Docker / scratch / Alpine; gnu variants are smaller and
# match typical Linux distributions. aarch64 builds use `cross`.

on:
  push:
    tags: ['v*']
  release:
    types: [published]
  workflow_dispatch:
    inputs:
      tag:
        description: 'Release tag to attach binaries to (e.g. v1.3.0)'
        required: true
        type: string
      unlocked:
        description: 'Drop --locked (backfill of pre-v1.4.3 tags whose Cargo.lock is stale)'
        required: false
        type: boolean
        default: false

permissions:
  contents: write  # create the release + upload assets

concurrency:
  group: binary-release-${{ github.event.release.tag_name || inputs.tag || github.ref_name }}
  cancel-in-progress: false

env:
  CROSS_VERSION: v0.2.5

jobs:
  # Resolve the tag once and make sure the GitHub Release exists before any
  # build leg runs — hosted runners only, no self-hosted dependency.
  ensure-release:
    name: Ensure release exists
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.tag.outputs.tag }}
    steps:
      - name: Resolve release tag
        id: tag
        run: |
          TAG="${{ github.event.release.tag_name || inputs.tag || github.ref_name }}"
          if [ -z "$TAG" ]; then
            echo "::error::No tag resolved (release event missing tag_name and no dispatch input)"
            exit 1
          fi
          case "$TAG" in
            v*) ;;
            *) echo "::error::Resolved ref '$TAG' is not a v* tag"; exit 1 ;;
          esac
          echo "tag=$TAG" >> "$GITHUB_OUTPUT"
          echo "Releasing forjar $TAG"

      - name: Create GitHub Release if missing (idempotent)
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG="${{ steps.tag.outputs.tag }}"
          # Mirrors release.yml create-release: never clobber an existing
          # release or its hand-written notes.
          if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
            echo "Release $TAG already exists — leaving notes intact, will upload assets."
          else
            gh release create "$TAG" \
              --title "$TAG" \
              --generate-notes \
              --verify-tag \
              --repo "${{ github.repository }}"
          fi

  build:
    name: ${{ matrix.target }}
    needs: ensure-release
    # 22.04 pins the gnu builds to glibc 2.35 — binaries built on
    # ubuntu-latest (24.04, glibc 2.39) refuse to run on older hosts.
    # musl legs are static and aarch64 legs build inside cross containers,
    # so only the x86_64-gnu leg actually needs this, but one runner keeps
    # the matrix uniform.
    runs-on: ubuntu-22.04
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-musl
            cross: false
          - target: x86_64-unknown-linux-gnu
            cross: false
          - target: aarch64-unknown-linux-musl
            cross: true
          - target: aarch64-unknown-linux-gnu
            cross: true
    steps:
      - name: Resolve release tag
        id: tag
        run: |
          TAG="${{ needs.ensure-release.outputs.tag }}"
          echo "tag=$TAG" >> "$GITHUB_OUTPUT"
          echo "Building forjar for $TAG → ${{ matrix.target }}"

      - name: Checkout at tag
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10  # v6.0.3
        with:
          ref: ${{ steps.tag.outputs.tag }}

      # Contract assertions (src/generated_contracts.rs) are git-tracked and
      # aprender-contracts resolves from crates.io — no sibling checkout or
      # pv codegen needed (the old provable-contracts path-dep dance caused
      # issues #104/#112/#113).

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

      # The workspace pins a specific channel in `rust-toolchain.toml`
      # which overrides the dtolnay action for any cargo invocation in
      # the tree. Add the target explicitly to that pinned toolchain so
      # native cross-compiles (x86_64-musl) don't fail with `can't find
      # crate for core` mid-build.
      - name: Install target on workspace-pinned toolchain
        if: ${{ !matrix.cross }}
        run: rustup target add ${{ matrix.target }}

      - name: Install musl tools
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y --no-install-recommends musl-tools

      # Cross is used for aarch64 targets — pinned to v0.2.5 (matches the
      # legacy release.yml convention).
      - name: Install cross
        if: matrix.cross
        run: |
          dir="$RUNNER_TEMP/cross"
          mkdir -p "$dir"
          curl -sL "https://github.com/cross-rs/cross/releases/download/${CROSS_VERSION}/cross-x86_64-unknown-linux-musl.tar.gz" \
            | tar xz -C "$dir"
          echo "$dir" >> "$GITHUB_PATH"

      - name: Cache cargo
        uses: Swatinem/rust-cache@v2
        with:
          # The runner image MUST be part of the key: objects cached on
          # 24.04 (glibc 2.38, __isoc23_* symbols) fail to link on 22.04
          # (v1.4.4 x86_64-gnu leg failed exactly this way).
          shared-key: binary-${{ matrix.target }}-u2204

      - name: Build forjar
        run: |
          LOCKED="--locked"
          if [ "${{ inputs.unlocked }}" = "true" ]; then
            LOCKED=""
            echo "::warning::building without --locked (stale-lock backfill mode)"
          fi
          if [ "${{ matrix.cross }}" = "true" ]; then
            cross build --release --features vendored-openssl --bin forjar --target ${{ matrix.target }} $LOCKED
          else
            cargo build --release --features vendored-openssl --bin forjar --target ${{ matrix.target }} $LOCKED
          fi

      - name: Strip binary
        run: |
          TARGET="${{ matrix.target }}"
          BIN_PATH="target/${TARGET}/release/forjar"
          # Each cross image ships only the matching ${triple}-strip;
          # the gnu image lacks musl-strip and vice-versa.
          case "$TARGET" in
            aarch64-unknown-linux-musl) STRIP=aarch64-linux-musl-strip ;;
            aarch64-unknown-linux-gnu)  STRIP=aarch64-linux-gnu-strip ;;
            *)                          STRIP="" ;;
          esac
          if [ -n "$STRIP" ]; then
            docker run --rm -v "$PWD/target:/target:Z" \
              "ghcr.io/cross-rs/${TARGET}:main" \
              "$STRIP" "/$BIN_PATH" || true
          else
            strip "$BIN_PATH" || true
          fi
          ls -la "$BIN_PATH"

      - name: Package archive
        id: package
        run: |
          TAG="${{ steps.tag.outputs.tag }}"
          # Strip the leading 'v': assets are forjar-<x.y.z>-<target>.tar.gz,
          # matching release.yml, the homebrew checksum patching, and the
          # name install.sh resolves (VERSION_NUM="${TAG#v}").
          VERSION="${TAG#v}"
          TARGET="${{ matrix.target }}"
          ARCHIVE="forjar-${VERSION}-${TARGET}"
          mkdir -p "$ARCHIVE"
          cp "target/${TARGET}/release/forjar" "$ARCHIVE/"
          for f in README.md LICENSE LICENSE-MIT LICENSE-APACHE; do
            [ -f "$f" ] && cp "$f" "$ARCHIVE/"
          done
          tar czf "${ARCHIVE}.tar.gz" "$ARCHIVE"
          shasum -a 256 "${ARCHIVE}.tar.gz" > "${ARCHIVE}.tar.gz.sha256"
          echo "archive=${ARCHIVE}.tar.gz" >> "$GITHUB_OUTPUT"
          echo "sha=${ARCHIVE}.tar.gz.sha256" >> "$GITHUB_OUTPUT"
          du -h "${ARCHIVE}.tar.gz"

      - name: Upload assets to release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh release upload "${{ steps.tag.outputs.tag }}" \
            "${{ steps.package.outputs.archive }}" \
            "${{ steps.package.outputs.sha }}" \
            --clobber

  # Combined SHA256SUMS regenerated from every tarball on the release —
  # install.sh's verify_checksum prefers this file (with per-asset .sha256
  # as fallback). Runs after all build legs so it covers the full set;
  # macOS assets added later by release.yml get covered on its own
  # checksums pass or the next re-run.
  checksums:
    name: Aggregate SHA256SUMS
    needs: [ensure-release, build]
    runs-on: ubuntu-latest
    steps:
      - name: Regenerate and upload SHA256SUMS
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG="${{ needs.ensure-release.outputs.tag }}"
          mkdir assets && cd assets
          gh release download "$TAG" --repo "${{ github.repository }}" --pattern '*.tar.gz'
          sha256sum -- *.tar.gz > SHA256SUMS
          cat SHA256SUMS
          gh release upload "$TAG" SHA256SUMS --clobber --repo "${{ github.repository }}"

  summary:
    name: Summary
    needs: [ensure-release, build]
    runs-on: ubuntu-latest
    if: always()
    steps:
      - name: Write summary
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG="${{ needs.ensure-release.outputs.tag }}"
          STATUS="${{ needs.build.result }}"
          {
            echo "### Binary Release: $TAG"
            echo ""
            if [ "$STATUS" = "success" ]; then
              echo "- Build matrix: **4/4 targets succeeded**"
            else
              echo "- Build matrix: **partial** (status=$STATUS)"
            fi
            echo "- Targets:"
            echo "  - x86_64-unknown-linux-musl"
            echo "  - x86_64-unknown-linux-gnu"
            echo "  - aarch64-unknown-linux-musl"
            echo "  - aarch64-unknown-linux-gnu"
          } >> "$GITHUB_STEP_SUMMARY"