zc2 0.0.29

P2P compute broker with credit-based billing, WAL, and broker mesh support
name: Release

on:
  push:
    tags: ["v*"]
  # Recovery/backfill path: build and attach assets to a tag that already
  # exists. Needed because auto-release.yml pushes the release tag using
  # secrets.GITHUB_TOKEN, and GitHub deliberately does not start workflows
  # from GITHUB_TOKEN-created events -- so the `push: tags` trigger above
  # never fires for an automated release and the tag ends up with no binaries.
  workflow_dispatch:
    inputs:
      tag:
        description: "Existing tag to build and attach assets to (e.g. v0.0.22)"
        required: true

env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

permissions:
  contents: write

jobs:
  release:
    # Push-only: on the dispatch path the release already exists, and
    # generate_release_notes would overwrite auto-release's git-cliff body.
    if: github.event_name == 'push'
    runs-on: cpu
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          fetch-depth: 0
      - name: Create GitHub Release
        uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b  # v3
        with:
          generate_release_notes: true

  # Build the Linux binaries and attach them so `zc update` (prebuilt path)
  # works without a source build. Mirrors build.yml's proven ARC setup
  # (private-dep deploy key + hooks feature).
  #
  # arm64 is CROSS-COMPILED from the same x86_64 runner rather than built on an
  # arm64 runner, because the org has none. The asset names must match
  # `asset_name()` in src/update.rs exactly -- it already expects
  # `zc-linux-arm64`, so until this job existed an arm64 host asking for an
  # update got a 404.
  build-linux:
    needs: release
    # `skipped` is the normal state on the workflow_dispatch path.
    if: ${{ always() && (needs.release.result == 'success' || needs.release.result == 'skipped') }}
    runs-on: cpu
    timeout-minutes: 30
    env:
      HOME: /home/runner
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            asset: zc-linux-x86_64
            cross: false
          - target: aarch64-unknown-linux-gnu
            asset: zc-linux-arm64
            cross: true
    steps:
      # ARC runners reuse $HOME between jobs, and a job that sets the
      # `url.ssh://git@github.com/zakuro-ai/.insteadOf` rewrite for the private
      # drive dep leaves it in the GLOBAL gitconfig. The next job's checkout
      # then clones zc itself over ssh with a deploy key scoped to
      # zakuro-drive, and fails with a misleading "ERROR: Repository not
      # found." Strip any stale rewrite BEFORE checkout.
      - name: Purge stale git URL rewrites (ARC runner hygiene)
        shell: bash
        run: |
          git config --global --remove-section 'url.ssh://git@github.com/zakuro-ai/' 2>/dev/null || true
          git config --global --remove-section 'url.ssh://git@github.com/zakuro-ai/zakuro-drive' 2>/dev/null || true
          git config --global --unset-all core.sshCommand 2>/dev/null || true

      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          ref: ${{ inputs.tag || github.ref_name }}

      - name: Git auth for private zakuro-drive dependency (read-only deploy key)
        shell: bash
        run: |
          mkdir -p ~/.ssh && chmod 700 ~/.ssh
          printf '%s\n' "${{ secrets.DRIVE_DEPLOY_KEY }}" > ~/.ssh/zakuro_drive_deploy
          chmod 600 ~/.ssh/zakuro_drive_deploy
          ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
          git config --global core.sshCommand "ssh -i ~/.ssh/zakuro_drive_deploy -o IdentitiesOnly=yes"
          git config --global url."ssh://git@github.com/zakuro-ai/".insteadOf "https://github.com/zakuro-ai/"

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

      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32  # v2
        with:
          key: ${{ matrix.target }}

      # The cross linker, and the C compiler ring/aws-lc need for the QUIC
      # stack. Without CC_* those crates build for the host and the link fails
      # with an architecture mismatch rather than anything that names the cause.
      # gcc-aarch64-linux-gnu alone is NOT enough: without the target libc
      # headers the cross compiler falls back to the host /usr/include and
      # dies building libz-sys/ring with
      #   /usr/include/limits.h:26:10: fatal error: bits/libc-header-start.h
      # libc6-dev-arm64-cross supplies the aarch64 sysroot headers.
      - name: Install cross toolchain (arm64 only)
        if: matrix.cross
        run: |
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends \
            gcc-aarch64-linux-gnu libc6-dev-arm64-cross

      # --workspace also builds the zc-hooks sidecar. `zc hooks` execs it, so
      # shipping it beside zc keeps that command working in release binaries
      # exactly as it did when hooks was linked into zc via --features hooks.
      - name: Build release binaries
        env:
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
          CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
        run: cargo build --release --workspace --target ${{ matrix.target }}

      - name: Stage assets
        run: |
          set -euo pipefail
          cp target/${{ matrix.target }}/release/zc ${{ matrix.asset }}
          cp target/${{ matrix.target }}/release/zc-hooks ${{ matrix.asset }}-hooks

      # Confirm the artifact is for the architecture it claims. A silent
      # fallback to a host build would publish an x86_64 binary under the arm64
      # name, which fails for the user at exec time with "cannot execute".
      # Read the ELF header directly instead of shelling out to `file`, which
      # the minimal ARC image does not ship (`file: command not found`, exit
      # 127 -- it failed here after the binary had already built fine).
      # e_machine is a 2-byte little-endian field at offset 0x12:
      #   0x3E -> x86-64 ("3e00"), 0xB7 -> aarch64 ("b700").
      - name: Verify architecture
        run: |
          set -euo pipefail
          ASSET="${{ matrix.asset }}"
          MACHINE=$(od -An -tx1 -j18 -N2 "$ASSET" | tr -d ' \n')
          echo "$ASSET e_machine=0x$MACHINE"
          case "${{ matrix.target }}" in
            aarch64-*) EXPECT="b700" ;;
            x86_64-*)  EXPECT="3e00" ;;
            *) echo "::error::no e_machine mapping for ${{ matrix.target }}"; exit 1 ;;
          esac
          if [ "$MACHINE" != "$EXPECT" ]; then
            echo "::error::$ASSET is e_machine=0x$MACHINE, expected 0x$EXPECT for ${{ matrix.target }}"
            exit 1
          fi

      - name: Attach to release
        uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b  # v3
        with:
          tag_name: ${{ inputs.tag || github.ref_name }}
          files: |
            ${{ matrix.asset }}
            ${{ matrix.asset }}-hooks

      - name: Cleanup git SSH config (runner hygiene)
        if: always()
        run: |
          git config --global --remove-section 'url.ssh://git@github.com/zakuro-ai/' 2>/dev/null || true
          git config --global --unset-all 'core.sshCommand' 2>/dev/null || true

  # macOS binary (`zc-darwin-arm64`). Dormant until a macOS runner is enrolled
  # and the repo variable MACOS_RELEASE is set to 'true' — the org has no macOS
  # runner today and GitHub-hosted macOS runners are billing-blocked, so an
  # un-gated job would hang the release waiting for a runner that never comes.
  build-macos:
    needs: release
    if: ${{ always() && (needs.release.result == 'success' || needs.release.result == 'skipped') && vars.MACOS_RELEASE == 'true' }}
    runs-on: [self-hosted, macOS, ARM64]
    timeout-minutes: 30
    steps:
      # ARC runners reuse $HOME between jobs, and a job that sets the
      # `url.ssh://git@github.com/zakuro-ai/.insteadOf` rewrite for the private
      # drive dep leaves it in the GLOBAL gitconfig. The next job's checkout
      # then clones zc itself over ssh with a deploy key scoped to
      # zakuro-drive, and fails with a misleading "ERROR: Repository not
      # found." Strip any stale rewrite BEFORE checkout.
      - name: Purge stale git URL rewrites (ARC runner hygiene)
        shell: bash
        run: |
          git config --global --remove-section 'url.ssh://git@github.com/zakuro-ai/' 2>/dev/null || true
          git config --global --remove-section 'url.ssh://git@github.com/zakuro-ai/zakuro-drive' 2>/dev/null || true
          git config --global --unset-all core.sshCommand 2>/dev/null || true

      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          ref: ${{ inputs.tag || github.ref_name }}

      - name: Git auth for private zakuro-drive dependency (read-only deploy key)
        shell: bash
        run: |
          mkdir -p ~/.ssh && chmod 700 ~/.ssh
          printf '%s\n' "${{ secrets.DRIVE_DEPLOY_KEY }}" > ~/.ssh/zakuro_drive_deploy
          chmod 600 ~/.ssh/zakuro_drive_deploy
          ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
          git config --global core.sshCommand "ssh -i ~/.ssh/zakuro_drive_deploy -o IdentitiesOnly=yes"
          git config --global url."ssh://git@github.com/zakuro-ai/".insteadOf "https://github.com/zakuro-ai/"

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8  # stable
        with:
          toolchain: "stable"
          targets: aarch64-apple-darwin

      - name: Build release binaries
        run: cargo build --release --workspace --target aarch64-apple-darwin

      - name: Stage assets
        run: |
          set -euo pipefail
          cp target/aarch64-apple-darwin/release/zc zc-darwin-arm64
          cp target/aarch64-apple-darwin/release/zc-hooks zc-darwin-arm64-hooks

      - name: Attach to release
        uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b  # v3
        with:
          tag_name: ${{ inputs.tag || github.ref_name }}
          files: |
            zc-darwin-arm64
            zc-darwin-arm64-hooks

      - name: Cleanup git SSH config (runner hygiene)
        if: always()
        run: |
          git config --global --remove-section 'url.ssh://git@github.com/zakuro-ai/' 2>/dev/null || true
          git config --global --unset-all 'core.sshCommand' 2>/dev/null || true

  deploy-prod:
    needs: release
    # Push-only on purpose: rebuilding assets for an existing tag must never
    # trigger a production deploy.
    if: github.event_name == 'push'
    runs-on: cpu
    timeout-minutes: 10
    steps:
      - name: Deploy to production
        run: |
          curl -X POST \
            -H "Authorization: token ${{ secrets.DEPLOY_PAT }}" \
            -H "Accept: application/vnd.github+json" \
            https://api.github.com/repos/zakuro-ai/zakuro-infra/dispatches \
            -d '{"event_type":"deploy-prod","client_payload":{"service":"zc","version":"${{ github.ref_name }}"}}'