name: Release

on:
  workflow_dispatch:
    inputs:
      build_target:
        description: Build target environment
        required: true
        type: choice
        default: production
        options:
          - production
          - staging
      release_type:
        description: Version increment type
        required: false
        default: patch
        type: choice
        options:
          - patch
          - minor
          - major

permissions:
  contents: write
  packages: write

concurrency:
  group: release-${{ github.event.inputs.build_target || 'production' }}-main
  cancel-in-progress: false

# ---------------------------------------------------------------------------
# Job dependency graph
#
#   prepare-build
#        │
#        ├─── create-release
#        │         │
#        │    ┌────┴────────────┐
#        │    │                 │
#        │  build-desktop    build-docker
#        │  (matrix: macOS,  (GHCR image)
#        │   linux, windows)
#        │    │                 │
#        │    └────┬────────────┘
#        │         │
#        │    publish-release
#        │         │
#        │    notify-discord
#        │
#        └─── cleanup-failed-release (on failure)
# ---------------------------------------------------------------------------

jobs:
  # =========================================================================
  # Phase 1: Version bump, commit, tag
  # =========================================================================
  prepare-build:
    name: Prepare build context
    runs-on: ubuntu-latest
    environment: Production
    outputs:
      version: ${{ steps.resolve.outputs.version }}
      tag: ${{ steps.resolve.outputs.tag }}
      sha: ${{ steps.resolve.outputs.sha }}
      build_ref: ${{ steps.resolve.outputs.build_ref }}
      release_enabled: ${{ steps.resolve.outputs.release_enabled }}
      base_url: ${{ steps.resolve.outputs.base_url }}
    steps:
      - name: Enforce main branch
        if: github.ref != 'refs/heads/main'
        run: |
          echo "This workflow can only run from main. Current ref: $GITHUB_REF"
          exit 1

      - name: Generate GitHub App token
        if: inputs.build_target == 'production'
        id: app-token
        uses: tibdex/github-app-token@v1
        with:
          app_id: ${{ secrets.XGITHUB_APP_ID }}
          private_key: ${{ secrets.XGITHUB_APP_PRIVATE_KEY }}

      - name: Checkout main for production release
        if: inputs.build_target == 'production'
        uses: actions/checkout@v4
        with:
          ref: main
          fetch-depth: 0
          token: ${{ steps.app-token.outputs.token }}

      - name: Checkout main for staging build
        if: inputs.build_target == 'staging'
        uses: actions/checkout@v4
        with:
          ref: main
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 24.x

      - name: Configure Git
        if: inputs.build_target == 'production'
        env:
          APP_TOKEN: ${{ steps.app-token.outputs.token }}
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git remote set-url origin https://${APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
          git fetch origin --tags
          git checkout main
          git pull origin main --ff-only

      - name: Compute next version and sync release files
        if: inputs.build_target == 'production'
        id: bump
        run: node scripts/release/bump-version.js "${{ inputs.release_type }}"

      - name: Verify release version sync
        if: inputs.build_target == 'production'
        run: node scripts/release/verify-version-sync.js "${{ steps.bump.outputs.version }}"

      - name: Ensure tag does not already exist
        if: inputs.build_target == 'production'
        env:
          TAG: ${{ steps.bump.outputs.tag }}
        run: |
          if git rev-parse "$TAG" >/dev/null 2>&1; then
            echo "Tag already exists locally: $TAG"
            exit 1
          fi

          if git ls-remote --tags origin "refs/tags/$TAG" | grep -q .; then
            echo "Tag already exists on origin: $TAG"
            exit 1
          fi

      - name: Commit, push and tag
        if: inputs.build_target == 'production'
        id: push
        env:
          VERSION: ${{ steps.bump.outputs.version }}
          TAG: ${{ steps.bump.outputs.tag }}
        run: |
          git add app/package.json app/src-tauri/tauri.conf.json app/src-tauri/Cargo.toml Cargo.toml
          git commit -m "chore(release): v${VERSION}"
          git push origin main

          git tag -a "$TAG" -m "Release $TAG"
          git push origin "$TAG"

          echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

      - name: Resolve build outputs
        id: resolve
        shell: bash
        run: |
          if [ "${{ inputs.build_target }}" = "production" ]; then
            VERSION="${{ steps.bump.outputs.version }}"
            TAG="${{ steps.bump.outputs.tag }}"
            SHA="${{ steps.push.outputs.sha }}"
            BUILD_REF="$TAG"
            RELEASE_ENABLED="true"
            BASE_URL="https://api.tinyhumans.ai/"
          else
            VERSION="$(node -p "require('./app/package.json').version")"
            TAG=""
            SHA="$(git rev-parse HEAD)"
            BUILD_REF="$SHA"
            RELEASE_ENABLED="false"
            BASE_URL="https://staging-api.tinyhumans.ai/"
          fi

          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
          echo "tag=$TAG" >> "$GITHUB_OUTPUT"
          echo "sha=$SHA" >> "$GITHUB_OUTPUT"
          echo "build_ref=$BUILD_REF" >> "$GITHUB_OUTPUT"
          echo "release_enabled=$RELEASE_ENABLED" >> "$GITHUB_OUTPUT"
          echo "base_url=$BASE_URL" >> "$GITHUB_OUTPUT"

  # =========================================================================
  # Phase 2: Create draft GitHub release
  # =========================================================================
  create-release:
    name: Create GitHub release
    runs-on: ubuntu-latest
    environment: Production
    if: needs.prepare-build.outputs.release_enabled == 'true'
    needs: prepare-build
    outputs:
      release_id: ${{ steps.create.outputs.release_id }}
      upload_url: ${{ steps.create.outputs.upload_url }}
    steps:
      - name: Create draft release with generated notes
        id: create
        uses: actions/github-script@v7
        with:
          script: |
            const tag = '${{ needs.prepare-build.outputs.tag }}';
            const version = '${{ needs.prepare-build.outputs.version }}';
            const target = '${{ needs.prepare-build.outputs.sha }}';

            const { owner, repo } = context.repo;

            try {
              await github.rest.repos.getReleaseByTag({ owner, repo, tag });
              core.setFailed(`Release already exists for ${tag}`);
              return;
            } catch (error) {
              if (error.status !== 404) {
                throw error;
              }
            }

            const release = await github.rest.repos.createRelease({
              owner,
              repo,
              tag_name: tag,
              target_commitish: target,
              name: `OpenHuman v${version}`,
              draft: true,
              prerelease: false,
              generate_release_notes: true,
            });

            core.setOutput('release_id', String(release.data.id));
            core.setOutput('upload_url', release.data.upload_url);

  # =========================================================================
  # Phase 3a: Build desktop artifacts (parallel matrix)
  # =========================================================================
  build-desktop:
    name: "Desktop: ${{ matrix.settings.artifact_suffix }}"
    needs: [prepare-build, create-release]
    if: >-
      always()
      && needs.prepare-build.result == 'success'
      && (needs.prepare-build.outputs.release_enabled != 'true'
          || needs.create-release.result == 'success')
    runs-on: ${{ matrix.settings.platform }}
    environment: Production
    strategy:
      fail-fast: false
      matrix:
        settings:
          - platform: macos-latest
            args: --target aarch64-apple-darwin
            target: aarch64-apple-darwin
            artifact_suffix: aarch64-apple-darwin
          - platform: macos-latest
            args: --target x86_64-apple-darwin
            target: x86_64-apple-darwin
            artifact_suffix: x86_64-apple-darwin
          - platform: ubuntu-22.04
            args: --target x86_64-unknown-linux-gnu
            target: x86_64-unknown-linux-gnu
            artifact_suffix: ubuntu
          - platform: windows-latest
            args: --target x86_64-pc-windows-msvc
            target: x86_64-pc-windows-msvc
            artifact_suffix: windows
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      # Keep in sync with DEFAULT_TELEGRAM_BOT_USERNAME_* in channels/controllers/ops.rs
      OPENHUMAN_TELEGRAM_BOT_USERNAME: ${{ inputs.build_target == 'staging' && 'alphahumantest_bot' || 'openhumanaibot' }}
      VITE_TELEGRAM_BOT_USERNAME: ${{ inputs.build_target == 'staging' && 'alphahumantest_bot' || 'openhumanaibot' }}
    steps:
      - name: Checkout build ref
        uses: actions/checkout@v4
        with:
          ref: ${{ needs.prepare-build.outputs.build_ref }}
          fetch-depth: 1
          submodules: true

      - name: Configure staging app environment
        if: inputs.build_target == 'staging'
        shell: bash
        run: |
          echo "OPENHUMAN_APP_ENV=staging" >> "$GITHUB_ENV"
          echo "VITE_OPENHUMAN_APP_ENV=staging" >> "$GITHUB_ENV"
          echo "OPENHUMAN_TELEGRAM_BOT_USERNAME=alphahumantest_bot" >> "$GITHUB_ENV"
          echo "VITE_TELEGRAM_BOT_USERNAME=alphahumantest_bot" >> "$GITHUB_ENV"

      - name: Set Xcode version
        if: matrix.settings.platform == 'macos-latest'
        uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: latest-stable

      - name: Setup Node.js 24.x
        uses: actions/setup-node@v4
        with:
          node-version: 24.x
          cache: yarn

      - name: Install Rust (rust-toolchain.toml)
        uses: dtolnay/rust-toolchain@1.93.0
        with:
          targets: ${{ matrix.settings.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}

      - name: Install Tauri dependencies (ubuntu only)
        if: matrix.settings.platform == 'ubuntu-22.04'
        run: |
          sudo apt-get update
          sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf cmake libasound2-dev libxdo-dev libxtst-dev libx11-dev libxi-dev libevdev-dev libssl-dev libclang-dev

      # Skip first 7 lines of Cargo.lock (workspace package version bumps) so the key tracks dependency changes only
      - name: Cargo.lock fingerprint (deps only)
        id: cargo-lock-fingerprint
        shell: bash
        run: |
          echo "hash=$(tail -n +8 Cargo.lock | openssl dgst -sha256 | awk '{print $2}')" >> "$GITHUB_OUTPUT"

      - name: Cache Cargo registry and git sources
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
          key: ${{ runner.os }}-cargo-registry-${{ steps.cargo-lock-fingerprint.outputs.hash }}
          restore-keys: |
            ${{ runner.os }}-cargo-registry-

      - name: Install dependencies
        run: yarn install --frozen-lockfile

      - name: Validate signing prerequisites
        shell: bash
        env:
          MATRIX_PLATFORM: ${{ matrix.settings.platform }}
          UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY || vars.UPDATER_PUBLIC_KEY }}
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }}
          APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
        run: |
          if [ -z "$UPDATER_PUBLIC_KEY" ]; then
            echo "Missing UPDATER_PUBLIC_KEY (set as secret or repo/environment variable)."
            exit 1
          fi

          if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]; then
            echo "Missing TAURI_SIGNING_PRIVATE_KEY (or fallback UPDATER_PRIVATE_KEY)."
            exit 1
          fi

          if [ "$MATRIX_PLATFORM" = "macos-latest" ]; then
            for var in APPLE_CERTIFICATE_BASE64 APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do
              if [ -z "${!var}" ]; then
                echo "Missing required macOS signing secret: $var"
                exit 1
              fi
            done
          fi

      - name: Define Tauri configuration overrides
        id: config-overrides
        uses: actions/github-script@v7
        env:
          BASE_URL: ${{ needs.prepare-build.outputs.base_url }}
          UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY || vars.UPDATER_PUBLIC_KEY }}
          UPDATER_ENDPOINT: ${{ vars.UPDATER_ENDPOINT }}
          UPDATER_REPO: tinyhumansai/openhuman
          WITH_UPDATER: "true"
        with:
          script: |
            const workspacePath = process.env.GITHUB_WORKSPACE.replace(/\\/g, '/');
            const prefix = workspacePath.startsWith('/') ? 'file://' : 'file:///';
            const moduleUrl = `${prefix}${workspacePath}/scripts/prepareTauriConfig.js`;
            const { default: prepareTauriConfig } = await import(moduleUrl);
            const config = prepareTauriConfig();
            core.setOutput('json', JSON.stringify(config));

      - name: Build frontend
        run: yarn build
        env:
          NODE_ENV: production
          VITE_OPENHUMAN_APP_ENV: ${{ inputs.build_target == 'staging' && 'staging' || 'production' }}
          VITE_BACKEND_URL: ${{ needs.prepare-build.outputs.base_url }}
          VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
          VITE_DEBUG: ${{ vars.VITE_DEBUG }}
          # OAuth guardrails (#365): block openhuman://oauth/success on outdated desktop builds.
          VITE_MINIMUM_SUPPORTED_APP_VERSION: ${{ vars.VITE_MINIMUM_SUPPORTED_APP_VERSION }}
          VITE_LATEST_APP_DOWNLOAD_URL: ${{ vars.VITE_LATEST_APP_DOWNLOAD_URL }}

      - name: Resolve core manifest and binary names
        id: core-paths
        shell: bash
        run: |
          if [ -f "openhuman_core/Cargo.toml" ]; then
            CORE_DIR="openhuman_core"
          elif [ -f "rust-core/Cargo.toml" ]; then
            CORE_DIR="rust-core"
          elif [ -f "Cargo.toml" ] && grep -q '^name = "openhuman"' Cargo.toml; then
            CORE_DIR="."
          else
            echo "No core Cargo manifest found (expected root Cargo.toml with openhuman, openhuman_core/Cargo.toml, or rust-core/Cargo.toml)"
            exit 1
          fi

          SIDE_CAR_BASE="$(node -e "const fs=require('fs');const c=JSON.parse(fs.readFileSync('app/src-tauri/tauri.conf.json','utf8'));const b=(c.bundle&&Array.isArray(c.bundle.externalBin)&&c.bundle.externalBin[0])||'binaries/openhuman-core';process.stdout.write(String(b).split('/').pop());")"
          CORE_BIN_NAME="${SIDE_CAR_BASE}"

          echo "core_dir=$CORE_DIR" >> "$GITHUB_OUTPUT"
          echo "core_manifest=$CORE_DIR/Cargo.toml" >> "$GITHUB_OUTPUT"
          # Cargo workspace: release artifacts are under repo root target/, not <member>/target/
          echo "core_target_dir=target/$MATRIX_TARGET/release" >> "$GITHUB_OUTPUT"
          echo "core_bin_name=$CORE_BIN_NAME" >> "$GITHUB_OUTPUT"
          echo "sidecar_base=$SIDE_CAR_BASE" >> "$GITHUB_OUTPUT"
        env:
          MATRIX_TARGET: ${{ matrix.settings.target }}

      - name: Build sidecar core binary
        shell: bash
        run: |
          cargo build \
            --manifest-path "$CORE_MANIFEST" \
            --release \
            --target "$MATRIX_TARGET" \
            --bin "$CORE_BIN_NAME"
        env:
          MATRIX_TARGET: ${{ matrix.settings.target }}
          CORE_MANIFEST: ${{ steps.core-paths.outputs.core_manifest }}
          CORE_BIN_NAME: ${{ steps.core-paths.outputs.core_bin_name }}
          OPENHUMAN_APP_ENV: ${{ inputs.build_target == 'staging' && 'staging' || 'production' }}
          OPENHUMAN_SENTRY_DSN: ${{ vars.OPENHUMAN_SENTRY_DSN }}

      - name: Stage sidecar for Tauri bundler
        shell: bash
        run: |
          bash scripts/release/stage-sidecar.sh \
            "${{ matrix.settings.target }}" \
            "${{ steps.core-paths.outputs.core_target_dir }}" \
            "${{ steps.core-paths.outputs.core_bin_name }}" \
            "${{ steps.core-paths.outputs.sidecar_base }}"

      - name: Resolve standalone core CLI artifact path
        id: cli-paths
        shell: bash
        run: |
          BASE_DIR="$CORE_TARGET_DIR"
          EXE_SUFFIX=""
          if [[ "$MATRIX_TARGET" == *"windows"* ]]; then
            EXE_SUFFIX=".exe"
          fi
          echo "base_dir=$BASE_DIR" >> "$GITHUB_OUTPUT"
          echo "cli_path=$BASE_DIR/${CORE_BIN_NAME}${EXE_SUFFIX}" >> "$GITHUB_OUTPUT"
        env:
          MATRIX_TARGET: ${{ matrix.settings.target }}
          CORE_TARGET_DIR: ${{ steps.core-paths.outputs.core_target_dir }}
          CORE_BIN_NAME: ${{ steps.core-paths.outputs.core_bin_name }}

      - name: Build, package and upload to release
        if: needs.prepare-build.outputs.release_enabled == 'true'
        uses: tauri-apps/tauri-action@v0.6.2
        id: tauri-build
        env:
          # NOTE: APPLE_CERTIFICATE, APPLE_SIGNING_IDENTITY, APPLE_ID, etc. are
          # intentionally omitted so tauri-action builds WITHOUT signing.
          # Signing + notarization are handled in a dedicated step afterwards
          # where we sign everything with hardened runtime + entitlements
          # (required by Apple notarization).
          BASE_URL: ${{ needs.prepare-build.outputs.base_url }}
          OPENHUMAN_APP_ENV: ${{ inputs.build_target == 'staging' && 'staging' || 'production' }}
          VITE_OPENHUMAN_APP_ENV: ${{ inputs.build_target == 'staging' && 'staging' || 'production' }}
          VITE_BACKEND_URL: ${{ needs.prepare-build.outputs.base_url }}
          MACOSX_DEPLOYMENT_TARGET: ${{ matrix.settings.platform == 'macos-latest' && '10.15' || '' }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD || secrets.UPDATER_PRIVATE_KEY_PASSWORD }}
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }}
          WITH_UPDATER: "true"
          # Must match standalone "Build frontend" so tauri-action's beforeBuildCommand embeds the gate.
          VITE_MINIMUM_SUPPORTED_APP_VERSION: ${{ vars.VITE_MINIMUM_SUPPORTED_APP_VERSION }}
          VITE_LATEST_APP_DOWNLOAD_URL: ${{ vars.VITE_LATEST_APP_DOWNLOAD_URL }}
        with:
          projectPath: app
          args: -c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }}
          includeDebug: false
          includeRelease: true
          releaseId: ${{ needs.create-release.outputs.release_id }}
          owner: tinyhumansai
          repo: openhuman

      - name: Build and package staging artifacts
        if: needs.prepare-build.outputs.release_enabled != 'true'
        shell: bash
        env:
          BASE_URL: ${{ needs.prepare-build.outputs.base_url }}
          OPENHUMAN_APP_ENV: staging
          VITE_OPENHUMAN_APP_ENV: staging
          VITE_BACKEND_URL: ${{ needs.prepare-build.outputs.base_url }}
          MACOSX_DEPLOYMENT_TARGET: ${{ matrix.settings.platform == 'macos-latest' && '10.15' || '' }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD || secrets.UPDATER_PRIVATE_KEY_PASSWORD }}
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }}
          WITH_UPDATER: "true"
          VITE_MINIMUM_SUPPORTED_APP_VERSION: ${{ vars.VITE_MINIMUM_SUPPORTED_APP_VERSION }}
          VITE_LATEST_APP_DOWNLOAD_URL: ${{ vars.VITE_LATEST_APP_DOWNLOAD_URL }}
        run: |
          yarn --cwd app tauri build \
            -c '${{ steps.config-overrides.outputs.json }}' \
            ${{ matrix.settings.args }}

      - name: Locate macOS .app bundle
        if: matrix.settings.platform == 'macos-latest'
        id: locate-app
        shell: bash
        run: |
          set -euo pipefail
          # tauri-action with projectPath: app may output to either location
          # depending on workspace config — search both
          APP_PATH=""
          for candidate in \
            "app/src-tauri/target/${{ matrix.settings.target }}/release/bundle/macos/OpenHuman.app" \
            "target/${{ matrix.settings.target }}/release/bundle/macos/OpenHuman.app"; do
            if [ -d "$candidate" ]; then
              APP_PATH="$candidate"
              break
            fi
          done

          # Fallback: search for it
          if [ -z "$APP_PATH" ]; then
            APP_PATH="$(find . -path '*/release/bundle/macos/OpenHuman.app' -type d 2>/dev/null | head -1)"
          fi

          if [ -z "$APP_PATH" ]; then
            echo "ERROR: Could not find OpenHuman.app bundle anywhere"
            find . -name 'OpenHuman.app' -type d 2>/dev/null || true
            exit 1
          fi

          BUNDLE_DIR="$(dirname "$(dirname "$APP_PATH")")"
          echo "app_path=$APP_PATH" >> "$GITHUB_OUTPUT"
          echo "bundle_dir=$BUNDLE_DIR" >> "$GITHUB_OUTPUT"
          echo "Found .app at: $APP_PATH"
          echo "Bundle dir:    $BUNDLE_DIR"

      - name: Re-sign sidecar with hardened runtime and notarize
        if: matrix.settings.platform == 'macos-latest'
        shell: bash
        env:
          APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
        run: |
          bash scripts/release/sign-and-notarize-macos.sh \
            "${{ steps.locate-app.outputs.app_path }}" \
            "app/src-tauri/entitlements.sidecar.plist"

      - name: Re-package DMG after notarization
        if: matrix.settings.platform == 'macos-latest'
        shell: bash
        env:
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
        run: |
          bash scripts/release/repackage-dmg.sh \
            "${{ steps.locate-app.outputs.app_path }}" \
            "${{ steps.locate-app.outputs.bundle_dir }}"

      - name: Re-upload notarized macOS artifacts to release
        if: matrix.settings.platform == 'macos-latest' && needs.prepare-build.outputs.release_enabled == 'true'
        shell: bash
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
        run: |
          bash scripts/release/upload-macos-artifacts.sh \
            "${{ steps.locate-app.outputs.app_path }}" \
            "${{ steps.locate-app.outputs.bundle_dir }}" \
            "${{ needs.prepare-build.outputs.version }}" \
            "${{ matrix.settings.target }}"

      - name: Verify macOS app bundle sidecar layout
        if: matrix.settings.platform == 'macos-latest'
        shell: bash
        run: |
          APP_PATH="${{ steps.locate-app.outputs.app_path }}"
          echo "Inspecting bundle at: $APP_PATH"
          ls -la "$APP_PATH/Contents/MacOS"
          ls -la "$APP_PATH/Contents/Resources" | grep openhuman || true

          # Sidecar may be in MacOS/ or Resources/ depending on Tauri version
          FOUND=false
          if ls "$APP_PATH/Contents/MacOS"/openhuman* 2>/dev/null | grep -qv OpenHuman; then
            echo "Sidecar found in Contents/MacOS/"
            FOUND=true
          fi
          if ls "$APP_PATH/Contents/Resources"/openhuman-* 2>/dev/null; then
            echo "Sidecar found in Contents/Resources/"
            FOUND=true
          fi
          if [ "$FOUND" = "false" ]; then
            echo "WARNING: Sidecar binary not found in expected locations"
          fi

          # Verify notarization staple
          echo "Checking staple..."
          xcrun stapler validate "$APP_PATH" || echo "WARNING: Staple validation failed"

      - name: Package CLI tarball and upload to release (unix)
        if: matrix.settings.platform != 'windows-latest' && needs.prepare-build.outputs.release_enabled == 'true'
        shell: bash
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # For macOS, prefer the notarized binary from inside the .app bundle
          if [[ "${{ matrix.settings.platform }}" == "macos-latest" ]]; then
            APP_PATH="${{ steps.locate-app.outputs.app_path }}"
            BIN=$(find "$APP_PATH/Contents/MacOS" -maxdepth 1 -name "openhuman-core-*" \
              ! -name "*.sig" 2>/dev/null | head -1 || true)
            [[ -z "$BIN" ]] && BIN=$(find "$APP_PATH/Contents/Resources" -maxdepth 1 \
              -name "openhuman-core-*" ! -name "*.sig" 2>/dev/null | head -1 || true)
            if [[ -z "$BIN" ]]; then
              echo "[pkg] Falling back to target dir binary (no notarized sidecar found)"
              BIN="${{ steps.cli-paths.outputs.cli_path }}"
            fi
          else
            BIN="${{ steps.cli-paths.outputs.cli_path }}"
          fi

          bash scripts/release/package-cli-tarball.sh \
            "$BIN" \
            "${{ needs.prepare-build.outputs.version }}" \
            "${{ matrix.settings.target }}"

      - name: Package CLI zip and upload to release (windows)
        if: matrix.settings.platform == 'windows-latest' && needs.prepare-build.outputs.release_enabled == 'true'
        shell: pwsh
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          $Version = "${{ needs.prepare-build.outputs.version }}"
          $Target = "${{ matrix.settings.target }}"
          $BinPath = "${{ steps.cli-paths.outputs.cli_path }}"
          $ZipName = "openhuman-core-${Version}-${Target}.zip"

          $Work = New-Item -ItemType Directory -Path (Join-Path $env:TEMP "cli-pkg-$(Get-Random)")
          Copy-Item $BinPath (Join-Path $Work.FullName "openhuman-core.exe")
          Compress-Archive -Path (Join-Path $Work.FullName "openhuman-core.exe") -DestinationPath $ZipName

          $Hash = (Get-FileHash -Path $ZipName -Algorithm SHA256).Hash.ToLowerInvariant()
          Set-Content -Path "${ZipName}.sha256" -Value $Hash -NoNewline

          Write-Host "[package-cli] Created $ZipName (sha256: $Hash)"

          gh release upload "v${Version}" $ZipName "${ZipName}.sha256" --repo tinyhumansai/openhuman --clobber
          Write-Host "[package-cli] Uploaded $ZipName to v${Version}"

      - name: Upload staging desktop bundles
        if: needs.prepare-build.outputs.release_enabled != 'true'
        uses: actions/upload-artifact@v4
        with:
          name: desktop-bundles-${{ matrix.settings.platform }}-${{ matrix.settings.artifact_suffix }}
          path: |
            app/src-tauri/target/${{ matrix.settings.target }}/release/bundle/**
            target/${{ matrix.settings.target }}/release/bundle/**

      - name: Upload standalone CLI artifacts
        uses: actions/upload-artifact@v4
        with:
          name: standalone-bins-${{ matrix.settings.platform }}-${{ matrix.settings.artifact_suffix }}
          path: |
            ${{ steps.cli-paths.outputs.cli_path }}

  # =========================================================================
  # Phase 3b: Build & push Docker image (runs parallel with build-desktop)
  # =========================================================================
  build-docker:
    name: "Docker: build and push"
    needs: [prepare-build, create-release]
    if: >-
      always()
      && needs.prepare-build.result == 'success'
      && needs.prepare-build.outputs.release_enabled == 'true'
      && needs.create-release.result == 'success'
    runs-on: ubuntu-latest
    environment: Production
    env:
      REGISTRY: ghcr.io
      IMAGE_NAME: tinyhumansai/openhuman-core
    steps:
      - name: Checkout build ref
        uses: actions/checkout@v4
        with:
          ref: ${{ needs.prepare-build.outputs.build_ref }}
          fetch-depth: 1

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # TODO: Uncomment this when we have a staging tag
      # - name: Build and push staging tag
      #   uses: docker/build-push-action@v6
      #   with:
      #     context: .
      #     file: Dockerfile
      #     push: true
      #     tags: |
      #       ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:staging-${{ needs.prepare-release.outputs.tag }}
      #     cache-from: type=gha
      #     cache-to: type=gha,mode=max
      #     labels: |
      #       org.opencontainers.image.title=openhuman-core
      #       org.opencontainers.image.description=OpenHuman core JSON-RPC server
      #       org.opencontainers.image.version=${{ needs.prepare-release.outputs.version }}
      #       org.opencontainers.image.source=https://github.com/tinyhumansai/openhuman
      #       org.opencontainers.image.revision=${{ needs.prepare-release.outputs.sha }}

  # =========================================================================
  # Phase 4: Publish the draft release (waits for ALL build phases)
  # =========================================================================
  publish-release:
    name: Publish draft release
    runs-on: ubuntu-latest
    environment: Production
    needs: [prepare-build, create-release, build-desktop, build-docker]
    if: >-
      needs.prepare-build.outputs.release_enabled == 'true'
      && needs.build-desktop.result == 'success'
      && needs.build-docker.result == 'success'
    steps:
      - name: Validate required installer assets exist
        uses: actions/github-script@v7
        with:
          script: |
            const releaseId = Number('${{ needs.create-release.outputs.release_id }}');
            const { owner, repo } = context.repo;
            const { data: assets } = await github.rest.repos.listReleaseAssets({
              owner,
              repo,
              release_id: releaseId,
              per_page: 100,
            });

            const names = assets.map((a) => a.name);
            const requiredPatterns = [
              /OpenHuman_.*_aarch64\.dmg$/,
              /OpenHuman_.*_x64\.dmg$/,
              /OpenHuman_.*_amd64\.AppImage$/,
              /OpenHuman_.*_amd64\.deb$/,
              /(OpenHuman_.*_x64-setup\.exe$|OpenHuman_.*_x64.*\.msi$)/,
            ];

            const missing = requiredPatterns.filter((pattern) => !names.some((name) => pattern.test(name)));
            if (missing.length > 0) {
              core.setFailed(`Missing required installer assets. Got: ${names.join(', ')}`);
              return;
            }
            core.info('All required installer assets are present.');

      # - name: Promote Docker image from staging to release tags
      #   env:
      #     REGISTRY: ghcr.io
      #     IMAGE_NAME: tinyhumansai/openhuman-core
      #     TAG: ${{ needs.prepare-release.outputs.tag }}
      #   run: |
      #     # Log in to GHCR
      #     echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin

      #     STAGING="${REGISTRY}/${IMAGE_NAME}:staging-${TAG}"
      #     VERSIONED="${REGISTRY}/${IMAGE_NAME}:${TAG}"
      #     LATEST="${REGISTRY}/${IMAGE_NAME}:latest"

      #     echo "Promoting ${STAGING} → ${VERSIONED}, ${LATEST}"
      #     docker buildx imagetools create --tag "${VERSIONED}" --tag "${LATEST}" "${STAGING}"

      # - name: Append Docker pull instructions to release notes
      #   env:
      #     GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      #     TAG: ${{ needs.prepare-release.outputs.tag }}
      #     IMAGE_NAME: tinyhumansai/openhuman-core
      #   run: |
      #     gh release view "${TAG}" --repo tinyhumansai/openhuman --json body -q .body > /tmp/body.md
      #     printf '\n---\n\n### Docker\n\n```\ndocker pull ghcr.io/%s:%s\ndocker run -p 7788:7788 --env-file .env ghcr.io/%s:%s\n```\n' \
      #       "${IMAGE_NAME}" "${TAG}" "${IMAGE_NAME}" "${TAG}" >> /tmp/body.md
      #     gh release edit "${TAG}" --repo tinyhumansai/openhuman --notes-file /tmp/body.md

      - name: Publish release
        uses: actions/github-script@v7
        with:
          script: |
            const releaseId = Number('${{ needs.create-release.outputs.release_id }}');
            await github.rest.repos.updateRelease({
              owner: context.repo.owner,
              repo: context.repo.repo,
              release_id: releaseId,
              draft: false,
            });
            core.info(`Published release ${releaseId}`);

  # =========================================================================
  # Cleanup: remove draft release + tag if ANY build phase failed
  # =========================================================================
  cleanup-failed-release:
    name: Remove release and tag if build failed
    runs-on: ubuntu-latest
    environment: Production
    needs: [prepare-build, create-release, build-desktop, build-docker]
    if: >-
      always()
      && needs.prepare-build.outputs.release_enabled == 'true'
      && needs.create-release.result == 'success'
      && (needs.build-desktop.result == 'failure' || needs.build-desktop.result == 'cancelled'
          || needs.build-docker.result == 'failure' || needs.build-docker.result == 'cancelled')
    steps:
      - name: Delete GitHub release
        uses: actions/github-script@v7
        with:
          script: |
            const owner = context.repo.owner;
            const repo = context.repo.repo;
            const releaseId = Number('${{ needs.create-release.outputs.release_id }}');

            if (!Number.isFinite(releaseId) || releaseId <= 0) {
              core.setFailed('Invalid or missing release_id; cannot delete release.');
              return;
            }

            try {
              await github.rest.repos.deleteRelease({ owner, repo, release_id: releaseId });
              core.info(`Deleted release ${releaseId}`);
            } catch (e) {
              core.warning(`deleteRelease failed: ${e.message}`);
            }

      - name: Delete remote tag
        uses: actions/github-script@v7
        with:
          script: |
            const owner = context.repo.owner;
            const repo = context.repo.repo;
            const tag = '${{ needs.prepare-build.outputs.tag }}';

            try {
              await github.rest.git.deleteRef({ owner, repo, ref: `tags/${tag}` });
              core.info(`Deleted remote tag ${tag}`);
            } catch (e) {
              if (e.status === 404) {
                core.info(`Tag ${tag} already absent on remote`);
              } else {
                throw e;
              }
            }

      - name: Delete staging Docker image
        continue-on-error: true
        env:
          TAG: ${{ needs.prepare-build.outputs.tag }}
        run: |
          PACKAGE="openhuman-core"
          STAGING_TAG="staging-${TAG}"
          echo "Attempting to delete staging Docker tag: ${STAGING_TAG}"
          # List package versions and find the staging tag
          VERSION_ID="$(gh api \
            -H "Accept: application/vnd.github+json" \
            "/orgs/tinyhumansai/packages/container/${PACKAGE}/versions" \
            --paginate --jq ".[] | select(.metadata.container.tags[] == \"${STAGING_TAG}\") | .id" 2>/dev/null | head -1)"
          if [ -n "$VERSION_ID" ]; then
            gh api -X DELETE "/orgs/tinyhumansai/packages/container/${PACKAGE}/versions/${VERSION_ID}" || true
            echo "Deleted staging image version ${VERSION_ID}"
          else
            echo "Staging tag ${STAGING_TAG} not found or already deleted"
          fi
