# Desktop release builds — macOS (.dmg, arm64 + Intel) and Windows (.msi + NSIS .exe).
#
# CI calls the SAME scripts developers run locally (packaging/build_dmg.sh and
# build_windows.ps1); this file only provisions the toolchain (Node, Rust, a Python venv at
# .venv with PyInstaller) and publishes the results.
#
# Triggers:
#   - tag push `v*`  → builds all targets and attaches them to a DRAFT GitHub Release
#                      (review, then publish by hand).
#   - manual run     → builds and uploads workflow artifacts only (no release).
#
# Each installer is uploaded twice: once with its versioned name (archive) and once with a
# stable name (OpenWorker-macos-arm64.dmg, …) so the website can link to
#   github.com/<repo>/releases/latest/download/<stable-name>
# and never need updating.
#
# macOS signing + notarization: Tauri's bundler handles both during `tauri build` when the
# APPLE_* env vars are present (import cert → sign app + sidecar with hardened runtime →
# notarize via notarytool → staple). Driven by repo secrets:
#   APPLE_CERTIFICATE            base64 .p12 (Developer ID Application cert + key)
#   APPLE_CERTIFICATE_PASSWORD   the .p12 export password
#   APPLE_SIGNING_IDENTITY       e.g. "Developer ID Application: Name (TEAMID)"
#   APPLE_API_KEY_CONTENT        base64 App Store Connect API .p8 (notarytool)
#   APPLE_API_KEY                the API key id
#   APPLE_API_ISSUER             the API issuer id
# When the secrets are absent (forks, scratch runs) the build degrades to unsigned —
# installable via `xattr -cr`. Windows remains unsigned (Authenticode is a later step).

name: Release

on:
  push:
    tags: ["v*", "app-v*"]
  workflow_dispatch:

defaults:
  run:
    shell: bash

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        include:
          # No Intel macOS target: macos-13 (the last Intel runner image) is deprecated and
          # its queue waits run to hours, which blocks the release job. Intel Macs are
          # 2020-and-earlier hardware — revisit only if beta users actually ask.
          - os: macos-latest # Apple Silicon
            slug: macos-arm64
          - os: windows-latest
            slug: windows
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
          cache-dependency-path: surfaces/gui/package-lock.json

      - uses: dtolnay/rust-toolchain@stable

      - uses: Swatinem/rust-cache@v2
        with:
          workspaces: surfaces/gui/src-tauri

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Set up the sidecar venv (.venv)
        # The build scripts expect a venv at .venv with the package + PyInstaller.
        # typer/tzdata are build-time-only (PyInstaller walks mcp.cli, which needs typer;
        # tzdata ships zoneinfo for Windows). aisuite installs like any other dependency
        # (git-pinned in pyproject.toml).
        run: |
          python -m venv .venv
          if [ "$RUNNER_OS" = "Windows" ]; then VPY=.venv/Scripts/python; else VPY=.venv/bin/python; fi
          "$VPY" -m pip install --upgrade pip
          "$VPY" -m pip install -e ".[bedrock]" pyinstaller typer tzdata
          "$VPY" -c "import aisuite, coworker"  # fail fast if either import breaks

      - name: npm ci
        working-directory: surfaces/gui
        run: npm ci

      - name: Build .dmg (macOS)
        if: runner.os == 'macOS'
        env:
          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
          APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }}
          APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
          APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
          # Auto-update artifact signing (minisign, separate from Apple signing). Absent →
          # the build script skips updater artifacts with a warning (fork/scratch runs).
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
        run: |
          # Unset empty APPLE_* vars so runs without secrets stay cleanly unsigned
          # (Tauri treats a present-but-empty var as a config error).
          for v in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_API_KEY APPLE_API_ISSUER; do
            [ -n "$(eval echo "\${$v:-}")" ] || unset "$v"
          done
          if [ -n "${APPLE_API_KEY_CONTENT:-}" ]; then
            echo "$APPLE_API_KEY_CONTENT" | base64 -d > "$RUNNER_TEMP/AuthKey.p8"
            export APPLE_API_KEY_PATH="$RUNNER_TEMP/AuthKey.p8"
          fi
          unset APPLE_API_KEY_CONTENT
          bash packaging/build_dmg.sh

      - name: Build .msi + NSIS .exe (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        env:
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
        run: ./packaging/build_windows.ps1

      - name: Stage artifacts (versioned + stable names)
        run: |
          mkdir -p out
          BUNDLE=surfaces/gui/src-tauri/target/release/bundle
          if [ "$RUNNER_OS" = "Windows" ]; then
            cp "$BUNDLE"/nsis/*.exe out/
            cp "$BUNDLE"/nsis/*.exe out/OpenWorker-windows-setup.exe
            cp "$BUNDLE"/msi/*.msi out/
            cp "$BUNDLE"/msi/*.msi out/OpenWorker-windows.msi
            # Updater signature for the NSIS installer (present only when the updater key
            # secret is configured). The .sig signs CONTENT, so the stable rename is safe.
            SIG=$(ls "$BUNDLE"/nsis/*.exe.sig 2>/dev/null | head -1 || true)
            [ -n "$SIG" ] && cp "$SIG" out/OpenWorker-windows-setup.exe.sig
          else
            cp "$BUNDLE"/dmg/*.dmg out/
            cp "$BUNDLE"/dmg/*.dmg out/OpenWorker-${{ matrix.slug }}.dmg
            # macOS updater artifact: the signed .app tarball the installed app swaps in.
            if [ -f "$BUNDLE"/macos/OpenWorker.app.tar.gz ]; then
              cp "$BUNDLE"/macos/OpenWorker.app.tar.gz out/OpenWorker-${{ matrix.slug }}.app.tar.gz
              cp "$BUNDLE"/macos/OpenWorker.app.tar.gz.sig out/OpenWorker-${{ matrix.slug }}.app.tar.gz.sig
            fi
          fi
          ls -la out

      - uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.slug }}
          path: out/*
          if-no-files-found: error

  release:
    if: startsWith(github.ref, 'refs/tags/v')
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4

      - uses: actions/download-artifact@v4
        with:
          path: dist
          merge-multiple: true

      - name: Compose the auto-update manifest (latest.json)
        # Shipped apps poll releases/latest/download/latest.json (via the branded
        # download.openworker.com redirect) — publishing this release IS pushing the
        # update. The tag must match tauri.conf.json's version or installed apps would
        # see a permanent phantom update; fail loudly on drift. Runs only when signed
        # updater artifacts exist (i.e. the TAURI_SIGNING_PRIVATE_KEY secret is set).
        run: |
          TAG="${GITHUB_REF_NAME}"
          CONF_VERSION=$(python3 -c "import json; print(json.load(open('surfaces/gui/src-tauri/tauri.conf.json'))['version'])")
          if [ "${TAG#v}" != "$CONF_VERSION" ]; then
            echo "::error::tag $TAG != tauri.conf.json version $CONF_VERSION — bump the config before tagging"
            exit 1
          fi
          if ls dist/*.sig >/dev/null 2>&1; then
            python3 packaging/make_update_manifest.py \
              --version "${TAG#v}" --tag "$TAG" --repo "$GITHUB_REPOSITORY" \
              --dist dist --out dist/latest.json \
              --notes "OpenWorker ${TAG#v}"
          else
            echo "::warning::no updater signatures in dist/ — release ships WITHOUT auto-update manifest"
          fi

      - uses: softprops/action-gh-release@v2
        with:
          draft: true
          files: dist/*
          generate_release_notes: true
