name: Desktop release

# Builds and publishes the Electron desktop app via electron-forge.
# Generates a GitHub Release with installers + update manifests, then uploads
# stable, version-free asset aliases so `ao start`'s constant
# releases/latest/download/<name> URL resolves (spec §8, §11.4).
#
# Each target OS builds on its own runner so the bundled `ao` daemon is compiled
# natively for that platform. build-daemon.mjs keys the binary off the build
# host's platform, so cross-OS packaging (e.g. building the Windows installer on
# macOS) would ship a non-Windows binary named `ao` and the app could not launch
# the daemon (issues #235/#256). The per-OS matrix keeps host == target, and the
# Linux daemon is built on the Linux runner (AgentWrapper/agent-orchestrator#2191).
#
# ⚠️ Until macOS code signing + notarization secrets are configured (see
# frontend/docs/desktop-release.md), published builds are UNSIGNED and will
# NOT auto-update on macOS. The workflow still produces installable artifacts.
#
# ── Cutting a FORK test release (dev loop) ────────────────────────────────────
# Test releases go to harshitsinghbhandari/agent-orchestrator (the fork), never
# to AgentWrapper. To cut one, on the FORK:
#   - push a `desktop-v*` tag (e.g. `git push fork desktop-v0.0.0-test1`), or
#   - run this workflow via `workflow_dispatch` from the fork's Actions tab.
# AO_RELEASE_REPO MUST be set to the fork for a test run. It is derived
# automatically below from `github.repository`, so a fork run publishes to the
# fork and a run on AgentWrapper publishes to AgentWrapper, with no edit. The
# matching test `ao` binary is built with
# `-ldflags -X ...cli.releaseRepo=harshitsinghbhandari/agent-orchestrator` so it
# fetches from the same fork (spec §6.3).

on:
  push:
    tags:
      - "desktop-v*"
  workflow_dispatch:

jobs:
  release:
    strategy:
      fail-fast: false
      matrix:
        os: [macos-latest, windows-latest, ubuntu-latest]
    runs-on: ${{ matrix.os }}
    # Gates the signed release behind the `release` environment's required
    # reviewers: any write-access user can push a desktop-v* tag, but only
    # designated approvers can let the build that consumes the signing secrets
    # proceed. Configure reviewers in repo Settings > Environments > release.
    environment: release
    permissions:
      contents: write
    defaults:
      run:
        working-directory: frontend
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
          cache-dependency-path: frontend/package-lock.json
      # The daemon is compiled by build-daemon.mjs during prepackage/premake, so
      # the Go toolchain must be present and pinned on every runner.
      - uses: actions/setup-go@v5
        with:
          go-version-file: backend/go.mod
          cache-dependency-path: backend/go.sum
      - run: npm ci
      - name: macOS signing setup
        if: startsWith(matrix.os, 'macos')
        uses: ./.github/actions/macos-signing-setup
        with:
          csc-link: ${{ secrets.CSC_LINK }}
          csc-key-password: ${{ secrets.CSC_KEY_PASSWORD }}
          apple-api-key-base64: ${{ secrets.APPLE_API_KEY_BASE64 }}
          apple-api-key-id: ${{ secrets.APPLE_API_KEY_ID }}
          apple-api-issuer: ${{ secrets.APPLE_API_ISSUER }}
          apple-signing-identity: ${{ secrets.APPLE_SIGNING_IDENTITY }}
      - name: Publish
        shell: bash
        run: |
          # Retry transient macOS sign/notary flakes (Apple notary -1009,
          # osx-sign keychain races) so the build self-heals instead of failing.
          for i in 1 2 3; do
            if npm run publish; then exit 0; fi
            echo "::warning::publish attempt $i/3 failed (likely a transient macOS sign/notary flake)"
            [ "$i" -lt 3 ] && sleep 30
          done
          echo "::error::publish failed after 3 attempts" >&2
          exit 1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          AO_RELEASE_REPO: ${{ github.repository }}
          # macOS signing + notarization env (APPLE_SIGNING_IDENTITY, APPLE_API_KEY,
          # APPLE_API_KEY_ID, APPLE_API_ISSUER) is exported by the "macOS signing
          # setup" step above via $GITHUB_ENV; the Developer ID cert is in the
          # keychain. No-op on non-macOS runners.

      # Upload stable, version-free asset aliases for this runner's platform so
      # `ao start`'s constant releases/latest/download/<name> URL resolves. The
      # forge makers emit versioned, productName-derived filenames; we copy them
      # to the space-free names `ao start` fetches and upload to the v<version>
      # release the publish step just created, with --clobber (so re-runs replace).
      #
      # Arch-coverage note: each runner only builds its host arch. macos-latest
      # (Apple silicon) produces darwin-arm64; macos-15-intel (Intel x64) produces
      # darwin-x64. Both macOS runners run this step (see the `if` below), so the
      # arm64 and x64 stable aliases are now both built (spec §8).
      # The Linux stable name is undecided (spec §11.3), so we build the deb/rpm
      # but intentionally upload no Linux alias yet.
      - name: Upload stable asset aliases (macOS)
        if: startsWith(matrix.os, 'macos')
        shell: bash
        run: |
          # electron-forge publisher-github creates the release as v<package.json
          # version> (PublisherGithub.js: `${tagPrefix ?? 'v'}${version}`), NOT the
          # triggering git tag. Target that release, not GITHUB_REF_NAME, or the
          # upload hits a non-existent release.
          tag="v$(node -p "require('./package.json').version")"
          arch="$(uname -m)"
          case "$arch" in
            arm64) alias_name="agent-orchestrator-darwin-arm64.zip" ;;
            x86_64) alias_name="agent-orchestrator-darwin-x64.zip" ;;
            *) echo "unsupported macOS arch: $arch" >&2; exit 1 ;;
          esac
          # maker-zip output: out/make/zip/darwin/<arch>/<dir-basename>-<version>.zip
          src="$(ls out/make/zip/darwin/*/*.zip | head -n1)"
          if [ -z "$src" ]; then echo "no macOS zip found under out/make/zip/darwin" >&2; exit 1; fi
          cp "$src" "$alias_name"
          gh release upload "$tag" "$alias_name" --clobber
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Upload stable asset aliases (Windows)
        if: matrix.os == 'windows-latest'
        shell: bash
        run: |
          # Forge publishes to v<version> (see the macOS step), not the git tag.
          tag="v$(node -p "require('./package.json').version")"
          alias_name="agent-orchestrator-win32-x64.exe"
          # NSIS (electron-builder) output: out/make/<productName> Setup <version>.exe
          src="$(ls "out/make/"*Setup*.exe | head -n1)"
          if [ -z "$src" ]; then echo "no NSIS installer found under out/make" >&2; exit 1; fi
          cp "$src" "$alias_name"
          gh release upload "$tag" "$alias_name" --clobber
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      # Linux: deb/rpm AND an AppImage build on this runner (issue #2191). The
      # AppImage is the fetch-and-run artifact for `ao start` (spec §11.3); we
      # copy it to the stable, space-free name the bootstrapper fetches and
      # upload to the v<version> release. The versioned deb/rpm are still
      # published by the forge publisher above for users who want a system package.
      - name: Upload stable asset aliases (Linux)
        if: matrix.os == 'ubuntu-latest'
        shell: bash
        run: |
          # Forge publishes to v<version> (see the macOS step), not the git tag.
          tag="v$(node -p "require('./package.json').version")"
          alias_name="agent-orchestrator-linux-x64.AppImage"
          # AppImage (electron-builder) output: out/make/<productName>-<version>.AppImage
          src="$(ls out/make/*.AppImage | head -n1)"
          if [ -z "$src" ]; then echo "no AppImage found under out/make" >&2; exit 1; fi
          cp "$src" "$alias_name"
          gh release upload "$tag" "$alias_name" --clobber
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  # Dedicated Intel (darwin-x64) build leg. Kept a separate job (not in the
  # release matrix) because only a macOS host can build the x64 installer, but
  # publish-feed now waits on it (needs: [release, release-intel]) so the x64
  # entry is always present in latest-mac.yml and Intel users get auto-updates.
  # Runner is macos-15-intel: macos-13 is deprecated/has no capacity (multi-hour
  # queues), so this leg uses the supported Intel x64 image.
  release-intel:
    runs-on: macos-15-intel
    environment: release # same approval gate as the matrix release job
    permissions:
      contents: write
    defaults:
      run:
        working-directory: frontend
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
          cache-dependency-path: frontend/package-lock.json
      - uses: actions/setup-go@v5
        with:
          go-version-file: backend/go.mod
          cache-dependency-path: backend/go.sum
      - run: npm ci
      - name: macOS signing setup
        uses: ./.github/actions/macos-signing-setup
        with:
          csc-link: ${{ secrets.CSC_LINK }}
          csc-key-password: ${{ secrets.CSC_KEY_PASSWORD }}
          apple-api-key-base64: ${{ secrets.APPLE_API_KEY_BASE64 }}
          apple-api-key-id: ${{ secrets.APPLE_API_KEY_ID }}
          apple-api-issuer: ${{ secrets.APPLE_API_ISSUER }}
          apple-signing-identity: ${{ secrets.APPLE_SIGNING_IDENTITY }}
      - name: Publish
        shell: bash
        run: |
          # Retry transient macOS sign/notary flakes (Apple notary -1009,
          # osx-sign keychain races) so the build self-heals instead of failing.
          for i in 1 2 3; do
            if npm run publish; then exit 0; fi
            echo "::warning::publish attempt $i/3 failed (likely a transient macOS sign/notary flake)"
            [ "$i" -lt 3 ] && sleep 30
          done
          echo "::error::publish failed after 3 attempts" >&2
          exit 1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          AO_RELEASE_REPO: ${{ github.repository }}
      - name: Upload stable asset aliases (macOS)
        shell: bash
        run: |
          tag="v$(node -p "require('./package.json').version")"
          arch="$(uname -m)"
          case "$arch" in
            arm64) alias_name="agent-orchestrator-darwin-arm64.zip" ;;
            x86_64) alias_name="agent-orchestrator-darwin-x64.zip" ;;
            *) echo "unsupported macOS arch: $arch" >&2; exit 1 ;;
          esac
          src="$(ls out/make/zip/darwin/*/*.zip | head -n1)"
          if [ -z "$src" ]; then echo "no macOS zip found under out/make/zip/darwin" >&2; exit 1; fi
          cp "$src" "$alias_name"
          gh release upload "$tag" "$alias_name" --clobber
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  # After every platform has built and uploaded its installers, generate the
  # electron-updater feed (latest*.yml + .blockmap sidecars) from the versioned
  # assets and upload them to the same release. Runs once, on Linux: it only
  # hashes already-built artifacts, so it needs no per-OS runner.
  publish-feed:
    needs: [release, release-intel]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    defaults:
      run:
        working-directory: frontend
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
          cache-dependency-path: frontend/package-lock.json
      # feed.mjs imports app-builder-lib's blockmap generator, so deps are needed.
      - run: npm ci
      - name: Generate and upload the latest feed
        shell: bash
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # Forge publishes to v<package.json version> (not the git tag).
          tag="v$(node -p "require('./package.json').version")"
          mkdir -p dist
          gh release download "$tag" --dir dist --clobber
          node scripts/feed.mjs dist "${tag#v}" latest
          shopt -s nullglob
          assets=(dist/latest*.yml dist/*.blockmap)
          if [ ${#assets[@]} -eq 0 ]; then echo "no feed assets generated" >&2; exit 1; fi
          gh release upload "$tag" "${assets[@]}" --clobber
