# Release artifacts — cross-compiled `fak` binaries attached to the GitHub release.
#
# The release CADENCE workflow (release-cadence.yml) cuts the version commit, pushes
# main, tags `vX.Y.Z` after green CI, and creates the GitHub release PAGE from the
# committed note. It ships no binaries. THIS workflow decorates that release: on the
# `v*` tag push it cross-compiles the four supported targets and uploads them (plus
# checksums) as release assets, so an external adopter can `curl | sh` (install.sh) or
# download a prebuilt binary WITHOUT cloning the monorepo (issue #133).
#
# `cmd/fak` is pure Go (no cgo — the Vulkan backend is behind a build tag and absent
# from the default binary), so every target builds static with CGO_ENABLED=0. A binary
# shipped without a VERSION file alongside resolves its version from the BuildVersion
# ldflag stamped here (internal/appversion.Current precedence).
#
# linux/arm64 is a first-class published target: it is the constrained end of the
# deployment spectrum (Raspberry Pi / Jetson / arm64 edge gateway), the same kernel
# binary that runs on a datacenter host. CGO_ENABLED=0 proves there is nothing to port
# — the arm64 NEON Q8 path is the same one Apple-silicon ships — so this is a published
# binary, not a new build (the gap the mobile/edge strategy note flagged highest-leverage).
# See docs/explainers/cross-platform-spine.md.
#
# After the aggregate checksum asset lands, the announce job renders a release status
# card for the scoreboard workspace's #releases channel. Posting is gated on BOTH the
# FAK_SCOREBOARD_TOKEN secret and the FAK_RELEASES_CHANNEL repo variable; without either
# it dry-runs into GITHUB_STEP_SUMMARY so forks and unprovisioned repos stay green.
name: release-artifacts

on:
  push:
    tags: ["v*"]
  workflow_dispatch:
    inputs:
      tag:
        description: "Existing vX.Y.Z tag to (re)build and upload assets for"
        type: string
        required: true

permissions:
  contents: read

concurrency:
  group: release-artifacts-${{ github.ref_name }}
  cancel-in-progress: false

jobs:
  build:
    name: ${{ matrix.goos }}/${{ matrix.goarch }}
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
      contents: write # gh release upload
      id-token: write # SLSA build-provenance attestation (#1372)
      attestations: write # write the attestation to the repo's attestation store
    strategy:
      fail-fast: false
      matrix:
        include:
          - goos: linux
            goarch: amd64
          - goos: linux
            goarch: arm64
          - goos: darwin
            goarch: amd64
          - goos: darwin
            goarch: arm64
          - goos: windows
            goarch: amd64
    env:
      GOTOOLCHAIN: auto
      GOOS: ${{ matrix.goos }}
      GOARCH: ${{ matrix.goarch }}
      CGO_ENABLED: "0"
    steps:
      - uses: actions/checkout@v4
        with:
          # The tag ref on push; the dispatch input when re-running by hand.
          ref: ${{ github.event.inputs.tag || github.ref }}
          fetch-depth: 0

      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
          cache: false # zero external deps -> no go.sum to key a cache on

      - name: Build, package, checksum
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail
          TAG="${{ github.event.inputs.tag || github.ref_name }}"
          VERSION="${TAG#v}"
          NAME="fak_${VERSION}_${GOOS}_${GOARCH}"
          OUT="fak"; [ "$GOOS" = "windows" ] && OUT="fak.exe"

          mkdir -p dist/pkg
          go build -trimpath \
              -ldflags "-s -w -X github.com/anthony-chaudhary/fak/internal/appversion.BuildVersion=${VERSION}" \
              -o "dist/pkg/${OUT}" ./cmd/fak

          # Bundle the binary with the license and the getting-started guide so a
          # downloaded archive is self-explanatory.
          cp LICENSE dist/pkg/LICENSE
          cp GETTING-STARTED.md dist/pkg/GETTING-STARTED.md

          cd dist
          if [ "$GOOS" = "windows" ]; then
            ARCHIVE="${NAME}.zip"
            ( cd pkg && zip -q -r "../${ARCHIVE}" . )
          else
            ARCHIVE="${NAME}.tar.gz"
            tar -C pkg -czf "${ARCHIVE}" .
          fi
          sha256sum "${ARCHIVE}" > "${ARCHIVE}.sha256"
          echo "built ${ARCHIVE}"
          cat "${ARCHIVE}.sha256"
          # Hand the exact pair this leg produced to the upload step. Globbing for
          # every extension (`dist/*.zip` on a linux leg) fails under `set -u`
          # because the unmatched pattern stays literal and `gh` 404s on it.
          echo "ARCHIVE=dist/${ARCHIVE}" >> "$GITHUB_ENV"
          echo "ARCHIVE_SUM=dist/${ARCHIVE}.sha256" >> "$GITHUB_ENV"

      - name: Upload to the GitHub release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail
          TAG="${{ github.event.inputs.tag || github.ref_name }}"
          # The tag push that triggered us ALSO fires release-cadence's
          # `release_publish.py` step, which creates the release PAGE. We race it:
          # without waiting, `gh release upload` 404s with "release not found"
          # (issue #369). Wait up to ~2 min for the page to appear, then fail loud.
          for i in $(seq 1 24); do
            gh release view "$TAG" >/dev/null 2>&1 && break
            echo "release $TAG not visible yet (attempt $i/24); waiting 5s..."
            sleep 5
          done
          gh release view "$TAG" >/dev/null
          # Upload only the one archive (+ its checksum) this matrix leg built.
          # --clobber makes a re-run (dispatch backfill, retried matrix leg) idempotent.
          gh release upload "$TAG" "$ARCHIVE" "$ARCHIVE_SUM" --clobber

      - name: Attest build provenance (SLSA)
        # Cryptographic, machine-verifiable proof that THIS archive was built by THIS
        # workflow from THIS commit — the supply-chain guarantee a sha256 sidecar
        # can't give (#1372, epic #1354). A consumer verifies with:
        #   gh attestation verify <archive> --repo anthony-chaudhary/fak
        # Best-effort: skip on a fork / unprovisioned repo where id-token isn't
        # available, so the release stays green rather than failing on a perms gap.
        if: ${{ !github.event.repository.fork }}
        continue-on-error: true
        uses: actions/attest-build-provenance@v1
        with:
          subject-path: ${{ env.ARCHIVE }}

  checksums:
    name: aggregate SHA256SUMS
    needs: build
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      contents: write
    steps:
      - name: Fold per-asset sums into one SHA256SUMS
        env:
          GH_TOKEN: ${{ github.token }}
          # No checkout in this job, so `gh` cannot infer the repo from a git
          # remote — it failed with "fatal: not a git repository" (issue #369).
          # Pin the repo explicitly instead of paying for a full checkout.
          GH_REPO: ${{ github.repository }}
        run: |
          set -euo pipefail
          TAG="${{ github.event.inputs.tag || github.ref_name }}"
          # `needs: build` means the page should already exist, but guard anyway so
          # a manual workflow_dispatch re-run can't race a freshly created release.
          for i in $(seq 1 24); do
            gh release view "$TAG" >/dev/null 2>&1 && break
            echo "release $TAG not visible yet (attempt $i/24); waiting 5s..."
            sleep 5
          done
          gh release view "$TAG" >/dev/null
          tmp="$(mktemp -d)"
          # Pull the per-asset .sha256 files back down and concatenate them. Each line
          # is already in `<hash>  <archive>` form, which the installer verifies against.
          gh release download "$TAG" --dir "$tmp" --pattern '*.sha256' --clobber
          cat "$tmp"/*.sha256 | sort -k2 > "$tmp/SHA256SUMS"
          echo "== SHA256SUMS =="
          cat "$tmp/SHA256SUMS"
          gh release upload "$TAG" "$tmp/SHA256SUMS" --clobber

  verify-release:
    # Post-publish verification (#1369, epic #1354). A release that "succeeded" (tag +
    # page + assets present) can still be unusable — a corrupt archive, a binary that
    # prints the wrong version, or an `@latest` that won't resolve. At agentic cadence
    # we cut more often, so an unverified publish ships breakage that costs far more to
    # recover than the speed it bought. This job proves the published release is good
    # against ground truth: re-download every asset, verify its sha256, extract, and
    # run the binary to assert it reports the tag's version. It runs the PUBLISHED
    # artifact (not the source tree) on purpose — that is what a consumer gets.
    name: verify published artifacts
    needs: checksums
    runs-on: ubuntu-latest
    timeout-minutes: 15
    permissions:
      contents: write # quarantine a release that fails post-publish verification
    env:
      GH_TOKEN: ${{ github.token }}
      GH_REPO: ${{ github.repository }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.inputs.tag || github.ref }}

      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
          cache: false

      - name: re-download, checksum, and run the published binaries
        id: verify_published_artifacts
        run: |
          set -euo pipefail
          TAG="${{ github.event.inputs.tag || github.ref_name }}"
          VERSION="${TAG#v}"
          tmp="$(mktemp -d)"
          cd "$tmp"
          gh release download "$TAG" --clobber
          echo "== verifying sha256 sidecars =="
          fail=0
          for sum in *.sha256; do
            # each sidecar is "<hash>  <archive>" — verify the named archive.
            if sha256sum -c "$sum"; then
              echo "ok: $sum"
            else
              echo "::error::checksum mismatch for $sum"; fail=1
            fi
          done
          # Verify the linux/amd64 binary actually runs and reports the right version.
          lin="fak_${VERSION}_linux_amd64.tar.gz"
          if [ -f "$lin" ]; then
            tar -xzf "$lin"
            got="$(./fak version 2>/dev/null | head -1 || true)"
            echo "linux_amd64 fak version -> $got"
            case "$got" in
              *"$VERSION"*) echo "ok: binary reports $VERSION" ;;
              *) echo "::error::published linux_amd64 binary version mismatch: got '$got', want '$VERSION'"; fail=1 ;;
            esac
          else
            echo "::error::expected asset $lin not found on release $TAG"; fail=1
          fi
          {
            echo "## verify published artifacts — ${TAG}"
            echo "Assets present: $(ls *.tar.gz *.zip 2>/dev/null | wc -l) archives; checksum + binary-version verification ran."
          } >> "$GITHUB_STEP_SUMMARY"
          exit "$fail"

      - name: Quarantine failed release
        if: ${{ failure() && steps.verify_published_artifacts.outcome == 'failure' }}
        run: |
          set -euo pipefail
          TAG="${{ github.event.inputs.tag || github.ref_name }}"
          release_id="$(gh release view "$TAG" --json databaseId --jq .databaseId)"
          gh api -X PATCH "repos/${GH_REPO}/releases/${release_id}" \
            -F prerelease=true \
            -f make_latest=false \
            --silent
          {
            echo "## quarantined failed release - ${TAG}"
            echo "Marked the GitHub release as a prerelease and make_latest=false after post-publish verification failed."
          } >> "$GITHUB_STEP_SUMMARY"

      - name: verify go install resolves the tag
        run: |
          set -euo pipefail
          TAG="${{ github.event.inputs.tag || github.ref_name }}"
          # A clean module cache so this proves the public resolution path, not a
          # cached copy. Non-fatal on a proxy lag (the tag can take minutes to appear
          # on proxy.golang.org); the asset checks above are the hard gate.
          export GOFLAGS=-mod=mod GOBIN="$(mktemp -d)"
          if go install "github.com/anthony-chaudhary/fak/cmd/fak@${TAG}"; then
            echo "ok: go install ...@${TAG} resolved and built"
            "$GOBIN/fak" version || true
          else
            echo "::warning::go install ...@${TAG} did not resolve yet (module proxy lag is expected for minutes after a tag push)"
          fi

  announce:
    name: announce release to #releases
    needs: checksums
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      contents: read
    env:
      GOTOOLCHAIN: auto
      FAK_SCOREBOARD_TOKEN: ${{ secrets.FAK_SCOREBOARD_TOKEN }}
      FAK_RELEASES_CHANNEL: ${{ vars.FAK_RELEASES_CHANNEL }}
      FAK_SCOREBOARD_SOURCE: ci
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.inputs.tag || github.ref }}

      - uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
          cache: false

      - name: render release card (dry-run)
        run: |
          set -euo pipefail
          TAG="${{ github.event.inputs.tag || github.ref_name }}"
          NOTES_URL="https://github.com/${{ github.repository }}/releases/tag/${TAG}"
          go run ./cmd/fak scoreboard post \
            --channel "$FAK_RELEASES_CHANNEL" \
            --kpi release \
            --value "$TAG" \
            --grade A \
            --verdict OK \
            --detail "$NOTES_URL" \
            --source ci \
            --dry-run | tee release-card.txt
          {
            echo "## release-artifacts — ${TAG}"
            echo '```'
            cat release-card.txt
            echo '```'
          } >> "$GITHUB_STEP_SUMMARY"

      - name: post to #releases
        if: ${{ env.FAK_SCOREBOARD_TOKEN != '' && env.FAK_RELEASES_CHANNEL != '' }}
        run: |
          set -euo pipefail
          TAG="${{ github.event.inputs.tag || github.ref_name }}"
          NOTES_URL="https://github.com/${{ github.repository }}/releases/tag/${TAG}"
          go run ./cmd/fak scoreboard post \
            --channel "$FAK_RELEASES_CHANNEL" \
            --kpi release \
            --value "$TAG" \
            --grade A \
            --verdict OK \
            --detail "$NOTES_URL" \
            --source ci

      - name: note when posting was skipped
        if: ${{ env.FAK_SCOREBOARD_TOKEN == '' || env.FAK_RELEASES_CHANNEL == '' }}
        run: |
          {
            echo "Release announcement SKIPPED — ran in dry-run, nothing posted."
            if [ -z "${{ env.FAK_SCOREBOARD_TOKEN }}" ]; then
              echo "- FAK_SCOREBOARD_TOKEN secret not set (add it under Settings -> Secrets and variables -> Actions -> Secrets)."
            fi
            if [ -z "${{ env.FAK_RELEASES_CHANNEL }}" ]; then
              echo "- FAK_RELEASES_CHANNEL repo variable not set (#releases has no default channel; create #releases and add its channel id under Variables)."
            fi
          } >> "$GITHUB_STEP_SUMMARY"

      - name: upload card artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: release-card
          path: release-card.txt
          if-no-files-found: ignore
