workflows:
  # ============================================
  # AUTO-DEPLOY WORKFLOWS (Triggered on push to main)
  # ============================================

  ios-internal-auto:
    name: Auto Deploy iOS to Internal TestFlight
    instance_type: mac_mini_m2
    max_build_duration: 120
    integrations:
      app_store_connect: codemagic_v4
    environment:
      ios_signing:
        provisioning_profiles:
          - codemagic_v7
          - codemagic_watchos_v7
          - codemagic_widget_v7
        certificates:
          - codemagic_v7
      groups:
        - app_env
        - firebase
        - shorebird
      vars:
        APP_ID: 6502156163
      flutter: 3.44.5
      xcode: 26.0.1
      cocoapods: 1.16.2
    cache:
      cache_paths:
        - $HOME/.pub-cache
        - $HOME/.gradle/caches
        - $HOME/Library/Caches/CocoaPods
    when:
      changeset:
        includes:
          - 'app/**'
    working_directory: app
    scripts:
      - name: Verify dispatcher source pin
        script: |
          set -euo pipefail
          if [[ -n "${OMI_RELEASE_SOURCE_SHA:-}" ]]; then
            CHECKED_OUT_SHA="$(git rev-parse HEAD)"
            [[ "$CHECKED_OUT_SHA" == "$OMI_RELEASE_SOURCE_SHA" ]] || {
              echo "dispatcher source pin does not match checked-out HEAD" >&2
              exit 1
            }
          fi

      - name: Installing Opus
        script: |
          brew install opus
          brew install opus-tools

      - name: Set up Firebase
        script: |
          dart pub global activate flutterfire_cli

          echo "$FIREBASE_SERVICE_ACCOUNT_KEY" > ./firebase_key.json
          flutterfire config \
            --platforms="android,ios" \
            --out=lib/firebase_options_prod.dart \
            --ios-bundle-id=com.friend-app-with-wearable.ios12 \
            --android-package-name=com.friend.ios \
            --android-out=android/app/src/prod/  \
            --ios-out=ios/Config/Prod/ \
            --service-account="./firebase_key.json" \
            --project="based-hardware" \
            --ios-target="Runner" \
            --yes

          echo "$FIREBASE_SERVICE_ACCOUNT_DEV_KEY" > ./firebase_dev_key.json
          flutterfire config \
            --platforms="android,ios" \
            --out=lib/firebase_options_dev.dart \
            --ios-bundle-id=com.friend-app-with-wearable.ios12.development \
            --android-package-name=com.friend.ios.dev \
            --android-out=android/app/src/dev/  \
            --ios-out=ios/Config/Dev/ \
            --service-account="./firebase_dev_key.json" \
            --project="based-hardware-dev" \
            --ios-target="Runner" \
            --yes

      - name: Set up App .env
        script: |
          echo OPENAI_API_KEY=$OPENAI_API_KEY >> .env
          echo INSTABUG_API_KEY=$INSTABUG_API_KEY >> .env
          echo ONESIGNAL_APP_ID=$ONESIGNAL_APP_ID >> .env
          echo API_BASE_URL=https://api.omi.me/ >> .env
          echo GROWTHBOOK_API_KEY=$GROWTHBOOK_API_KEY >> .env
          echo GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY >> .env
          echo INTERCOM_APP_ID=$INTERCOM_APP_ID >> .env
          echo INTERCOM_IOS_API_KEY=$INTERCOM_IOS_API_KEY >> .env
          echo POSTHOG_API_KEY=$POSTHOG_API_KEY >> .env
          echo GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID >> .env
          echo GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET >> .env

      - name: Set up Google Service Info
        script: |
          echo "$GOOGLE_INFO_PLIST_KEY" > "$(pwd)/ios/Runner/GoogleService-Info.plist"

      - name: Generate iOS Custom Config (Custom.xcconfig)
        script: |
          sh scripts/generate_ios_custom_config.sh ios/Config/Prod/GoogleService-Info.plist ios/Flutter/

      - name: Get Flutter packages
        script: |
          flutter pub get

      - name: Run build runner
        script: |
          dart run build_runner build --delete-conflicting-outputs

      - name: Installs pod
        script: |
          cd ios && pod install --repo-update

      - name: Resolve next version from TestFlight + App Store
        script: |
          set -euo pipefail

          if ! TF_RAW=$(app-store-connect get-latest-testflight-build-number "$APP_ID" --include-version --json 2>/dev/null); then
            echo "TestFlight version lookup failed; refusing to guess a release version." >&2
            exit 1
          fi
          if ! AS_RAW=$(app-store-connect get-latest-app-store-build-number "$APP_ID" --include-version --json 2>/dev/null); then
            echo "App Store version lookup failed; refusing to guess a release version." >&2
            exit 1
          fi
          STORE_INPUT="$(mktemp)"
          trap 'rm -f "$STORE_INPUT"' EXIT
          export TF_RAW AS_RAW
          python3 - "$STORE_INPUT" <<'PY'
          import json
          import os
          import sys
          from pathlib import Path

          Path(sys.argv[1]).write_text(json.dumps({
              "testflight": {"stdout": os.environ["TF_RAW"], "exit_code": 0},
              "app_store": {"stdout": os.environ["AS_RAW"], "exit_code": 0},
          }), encoding="utf-8")
          PY
          PUBSPEC_VERSION="$(grep '^version:' pubspec.yaml | sed 's/version: //' | tr -d ' ')"
          RELEASE_PLAN="$(python3 scripts/mobile_store_version.py \
            --platform ios \
            --pubspec-version "$PUBSPEC_VERSION" \
            --input "$STORE_INPUT")"
          BUILD_NAME="$(jq -er '.version' <<<"$RELEASE_PLAN")"
          BUILD_NUMBER="$(jq -er '.build_number' <<<"$RELEASE_PLAN")"

          echo "Resolved next version: ${BUILD_NAME}+${BUILD_NUMBER}"
          echo BUILD_NAME="$BUILD_NAME" >> $CM_ENV
          echo BUILD_NUMBER="$BUILD_NUMBER" >> $CM_ENV

      - name: Flutter build ipa
        script: |
          xcode-project use-profiles

          echo "Building iOS with version $BUILD_NAME ($BUILD_NUMBER)"

          if ! PROVENANCE_ARGS="$(scripts/build_provenance_dart_defines.sh)"; then
            echo "Build provenance validation failed; refusing to build." >&2
            exit 1
          fi
          flutter build ipa \
            --release \
            --build-name=$BUILD_NAME \
            --build-number=$BUILD_NUMBER \
            --flavor prod \
            --export-options-plist=$HOME/export_options.plist \
            $PROVENANCE_ARGS

    artifacts:
      - build/ios/ipa/*.ipa
      - /tmp/xcodebuild_logs/*.log
      - flutter_drive.log
      - $HOME/Library/Developer/Xcode/DerivedData/**/Build/**/*.dSYM
    publishing:
      scripts:
        - name: Upload debug symbols to Firebase Crashlytics
          script: |
            echo "Finding all debug symbol files..."
            dsymFiles=$(find $HOME/Library/Developer/Xcode/DerivedData -name "*.dSYM" -type d 2>/dev/null)
            if [[ -z "$dsymFiles" ]]; then
              echo "No debug symbols were found, skip publishing to Firebase Crashlytics"
            else
              echo "Found dSYM files:"
              echo "$dsymFiles"
              uploadScriptPath=$(find ios/Pods/FirebaseCrashlytics -name "upload-symbols" -type f 2>/dev/null | head -1)
              if [[ -n ${uploadScriptPath} ]]; then
                chmod +x "$uploadScriptPath"
                echo "$dsymFiles" | while IFS= read -r dsymPath; do
                  if [[ -n "$dsymPath" ]]; then
                    "$uploadScriptPath" -gsp ios/Runner/GoogleService-Info.plist -p ios "$dsymPath"
                  fi
                done
              fi
            fi
      email:
        recipients:
          - ngocthinhdp@gmail.com
          - joan@basedhardware.com
          - nik@basedhardware.com
          - mohsin.lp710@gmail.com
          - i@m13v.com
        notify:
          success: true
          failure: true
      app_store_connect:
        auth: integration
        submit_to_testflight: true
        submit_to_app_store: false

  android-internal-auto:
    name: Auto Deploy Android to Internal Play Store
    instance_type: mac_mini_m2
    max_build_duration: 120
    integrations:
      app_store_connect: codemagic_v4
    environment:
      android_signing:
        - prod_android_upload_keystore
      groups:
        - app_env
        - firebase
        - google_play
        - shorebird
      vars:
        PACKAGE_NAME: "com.friend.ios"
        APP_ID: 6502156163
        JAVA_TOOL_OPTIONS: "-Xmx8g"
      flutter: 3.44.5
      xcode: 16.4
      cocoapods: 1.16.2
      java: 21
    cache:
      cache_paths:
        - $HOME/opt
        - $HOME/.pub-cache
        - $HOME/.gradle/caches
    when:
      changeset:
        includes:
          - 'app/**'
    working_directory: app
    scripts:
      - name: Verify dispatcher source pin
        script: |
          set -euo pipefail
          if [[ -n "${OMI_RELEASE_SOURCE_SHA:-}" ]]; then
            CHECKED_OUT_SHA="$(git rev-parse HEAD)"
            [[ "$CHECKED_OUT_SHA" == "$OMI_RELEASE_SOURCE_SHA" ]] || {
              echo "dispatcher source pin does not match checked-out HEAD" >&2
              exit 1
            }
          fi

      - name: Installing Android SDK
        script: |
          export ANDROID_HOME="$HOME/opt/android-sdk"
          if [ ! -d "$ANDROID_HOME" ]; then
            echo "$ANDROID_HOME does not exist. Installing..."
            mkdir -p $ANDROID_HOME
            cd "$HOME/opt" && \
            curl --fail --show-error --silent --connect-timeout 10.00 --max-time 120.00 \
              --output commandlinetools-mac-11076708_latest.zip \
              https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip && \
            7z -bd x commandlinetools-mac-11076708_latest.zip && \
            mkdir -p $ANDROID_HOME && \
            yes | cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} "tools" && \
            yes | cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} "cmdline-tools;latest" && \
            yes | cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} "ndk;28.2.13676358" && \
            yes | cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} --licenses  && \
            cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} --list
          fi
          if [ -d "$ANDROID_HOME" ]; then
            export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools
            echo ANDROID_HOME="$ANDROID_HOME" >> $CM_ENV
            echo ANDROID_SDK_ROOT="$ANDROID_HOME" >> $CM_ENV
            echo PATH="$PATH" >> $CM_ENV
          fi

      - name: Installing Opus
        script: |
          brew install opus
          brew install opus-tools

      - name: Set up Firebase
        script: |
          dart pub global activate flutterfire_cli

          echo "$FIREBASE_SERVICE_ACCOUNT_KEY" > ./firebase_key.json
          flutterfire config \
            --platforms="android,ios" \
            --out=lib/firebase_options_prod.dart \
            --ios-bundle-id=com.friend-app-with-wearable.ios12 \
            --android-package-name=com.friend.ios \
            --android-out=android/app/src/prod/  \
            --ios-out=ios/Config/Prod/ \
            --service-account="./firebase_key.json" \
            --project="based-hardware" \
            --ios-target="Runner" \
            --yes

          echo "$FIREBASE_SERVICE_ACCOUNT_DEV_KEY" > ./firebase_dev_key.json
          flutterfire config \
            --platforms="android,ios" \
            --out=lib/firebase_options_dev.dart \
            --ios-bundle-id=com.friend-app-with-wearable.ios12.development \
            --android-package-name=com.friend.ios.dev \
            --android-out=android/app/src/dev/  \
            --ios-out=ios/Config/Dev/ \
            --service-account="./firebase_dev_key.json" \
            --project="based-hardware-dev" \
            --ios-target="Runner" \
            --yes

      - name: Set up local.properties
        script: |
          echo "flutter.sdk=$HOME/programs/flutter" > "$(pwd)/android/local.properties"
          echo "sdk.dir=$ANDROID_HOME" >> "$(pwd)/android/local.properties"

      - name: Set up App .env
        script: |
          echo OPENAI_API_KEY=$OPENAI_API_KEY >> .env
          echo INSTABUG_API_KEY=$INSTABUG_API_KEY >> .env
          echo ONESIGNAL_APP_ID=$ONESIGNAL_APP_ID >> .env
          echo API_BASE_URL=https://api.omi.me/ >> .env
          echo GROWTHBOOK_API_KEY=$GROWTHBOOK_API_KEY >> .env
          echo GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY >> .env
          echo INTERCOM_APP_ID=$INTERCOM_APP_ID >> .env
          echo INTERCOM_ANDROID_API_KEY=$INTERCOM_ANDROID_API_KEY >> .env
          echo POSTHOG_API_KEY=$POSTHOG_API_KEY >> .env
          echo GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID >> .env
          echo GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET >> .env

      - name: Get Flutter packages
        script: |
          flutter packages pub get

      - name: Run build runner
        script: |
          dart run build_runner build --delete-conflicting-outputs

      - name: Resolve next version from TestFlight + App Store + Play Store
        script: |
          set -euo pipefail

          if ! TF_RAW=$(app-store-connect get-latest-testflight-build-number "$APP_ID" --include-version --json 2>/dev/null); then
            echo "TestFlight version lookup failed; refusing to guess a release version." >&2
            exit 1
          fi
          if ! AS_RAW=$(app-store-connect get-latest-app-store-build-number "$APP_ID" --include-version --json 2>/dev/null); then
            echo "App Store version lookup failed; refusing to guess a release version." >&2
            exit 1
          fi
          if ! PLAY_RAW=$(google-play get-latest-build-number --package-name "$PACKAGE_NAME" --tracks internal 2>/dev/null); then
            echo "Play version lookup failed; refusing to guess a release version." >&2
            exit 1
          fi
          STORE_INPUT="$(mktemp)"
          trap 'rm -f "$STORE_INPUT"' EXIT
          export TF_RAW AS_RAW PLAY_RAW
          python3 - "$STORE_INPUT" <<'PY'
          import json
          import os
          import sys
          from pathlib import Path

          Path(sys.argv[1]).write_text(json.dumps({
              "testflight": {"stdout": os.environ["TF_RAW"], "exit_code": 0},
              "app_store": {"stdout": os.environ["AS_RAW"], "exit_code": 0},
              "play": {"stdout": os.environ["PLAY_RAW"], "exit_code": 0},
          }), encoding="utf-8")
          PY
          PUBSPEC_VERSION="$(grep '^version:' pubspec.yaml | sed 's/version: //' | tr -d ' ')"
          RELEASE_PLAN="$(python3 scripts/mobile_store_version.py \
            --platform android \
            --pubspec-version "$PUBSPEC_VERSION" \
            --input "$STORE_INPUT")"
          BUILD_NAME="$(jq -er '.version' <<<"$RELEASE_PLAN")"
          BUILD_NUMBER="$(jq -er '.build_number' <<<"$RELEASE_PLAN")"

          echo "Resolved next version: ${BUILD_NAME}+${BUILD_NUMBER}"
          echo BUILD_NAME="$BUILD_NAME" >> $CM_ENV
          echo BUILD_NUMBER="$BUILD_NUMBER" >> $CM_ENV

      - name: Build AAB with Flutter
        script: |
          echo "Building Android with version $BUILD_NAME ($BUILD_NUMBER)"

          if ! PROVENANCE_ARGS="$(scripts/build_provenance_dart_defines.sh)"; then
            echo "Build provenance validation failed; refusing to build." >&2
            exit 1
          fi
          flutter build appbundle \
            --release \
            --build-name=$BUILD_NAME \
            --build-number=$BUILD_NUMBER \
            --flavor prod \
            $PROVENANCE_ARGS

    artifacts:
      - build/**/outputs/**/*.aab
      - flutter_drive.log
    publishing:
      email:
        recipients:
          - ngocthinhdp@gmail.com
          - joan@basedhardware.com
          - nik@basedhardware.com
          - mohsin.lp710@gmail.com
          - i@m13v.com
        notify:
          success: true
          failure: true
      google_play:
        credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
        track: "internal"
        submit_as_draft: false

  # ============================================
  # PRODUCTION RELEASE WORKFLOWS (Triggered by tags)
  # ============================================

  ios-prod-testflight:
    name: Release iOS to Testflight
    instance_type: mac_mini_m2
    max_build_duration: 120
    integrations:
      app_store_connect: codemagic_v4
    environment:
      ios_signing:
        provisioning_profiles:
          - codemagic_v7
          - codemagic_watchos_v7
          - codemagic_widget_v7
        certificates:
          - codemagic_v7
      groups:
        - app_env
        - firebase
        - shorebird
      vars:
        APP_ID: 6502156163
      flutter: 3.44.5
      xcode: 26.0.1
      cocoapods: 1.16.2
    cache:
      cache_paths:
        - $HOME/.pub-cache
        - $HOME/.gradle/caches
        - $HOME/Library/Caches/CocoaPods
    triggering:
      events:
        - tag
      tag_patterns:
        - pattern: "v*-ios-cm"
          include: true
        - pattern: "v*-mobile-cm"
          include: true
    working_directory: app
    scripts:
      - name: Installing Opus
        script: |
          brew install opus
          brew install opus-tools

      - name: Set up Firebase
        script: |
          dart pub global activate flutterfire_cli

          # https://github.com/invertase/flutterfire_cli/issues/233
          echo "$FIREBASE_SERVICE_ACCOUNT_KEY" > ./firebase_key.json
          flutterfire config \
            --platforms="android,ios" \
            --out=lib/firebase_options_prod.dart \
            --ios-bundle-id=com.friend-app-with-wearable.ios12 \
            --android-package-name=com.friend.ios \
            --android-out=android/app/src/prod/  \
            --ios-out=ios/Config/Prod/ \
            --service-account="./firebase_key.json" \
            --project="based-hardware" \
            --ios-target="Runner" \
            --yes

          # DEV, should remove
          echo "$FIREBASE_SERVICE_ACCOUNT_DEV_KEY" > ./firebase_dev_key.json
          flutterfire config \
            --platforms="android,ios" \
            --out=lib/firebase_options_dev.dart \
            --ios-bundle-id=com.friend-app-with-wearable.ios12.development \
            --android-package-name=com.friend.ios.dev \
            --android-out=android/app/src/dev/  \
            --ios-out=ios/Config/Dev/ \
            --service-account="./firebase_dev_key.json" \
            --project="based-hardware-dev" \
            --ios-target="Runner" \
            --yes

      - name: Set up App .env
        script: |
          echo OPENAI_API_KEY=$OPENAI_API_KEY >> .env
          echo INSTABUG_API_KEY=$INSTABUG_API_KEY >> .env
          echo ONESIGNAL_APP_ID=$ONESIGNAL_APP_ID >> .env
          echo API_BASE_URL=https://api.omi.me/ >> .env
          echo GROWTHBOOK_API_KEY=$GROWTHBOOK_API_KEY >> .env
          echo GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY >> .env
          echo INTERCOM_APP_ID=$INTERCOM_APP_ID >> .env
          echo INTERCOM_IOS_API_KEY=$INTERCOM_IOS_API_KEY >> .env
          echo POSTHOG_API_KEY=$POSTHOG_API_KEY >> .env
          echo GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID >> .env
          echo GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET >> .env

      - name: Set up Google Service Info
        script: |
          # TODO: check why we need this ?
          echo "$GOOGLE_INFO_PLIST_KEY" > "$(pwd)/ios/Runner/GoogleService-Info.plist"

      - name: Generate iOS Custom Config (Custom.xcconfig)
        script: |
          sh scripts/generate_ios_custom_config.sh ios/Config/Prod/GoogleService-Info.plist ios/Flutter/

      - name: Get Flutter packages
        script: |
          flutter pub get

      - name: Run build runner
        script: |
          dart run build_runner build --delete-conflicting-outputs

      - name: Installs pod
        script: |
          cd ios && pod install --repo-update

      - name: Flutter build ipa
        script: |
          set -euo pipefail
          # Set up code signing settings on Xcode project
          # -> $HOME/export_options.plist
          xcode-project use-profiles

          # The tag is the release identity. Validate its platform, source
          # commit, version, and build number before invoking Flutter.
          CHECKED_OUT_SHA="$(git rev-parse HEAD)"
          TAG_SOURCE_SHA="$(git rev-parse "${CM_TAG:?CM_TAG is required}^{commit}")"
          if ! IDENTITY_OUTPUT="$(python3 scripts/mobile_release_identity.py \
            --tag "${CM_TAG}" \
            --platform ios \
            --source-sha "${CHECKED_OUT_SHA}" \
            --tag-source-sha "${TAG_SOURCE_SHA}" \
            --format shell)"; then
            echo "Mobile release identity validation failed; refusing to build." >&2
            exit 1
          fi
          eval "$IDENTITY_OUTPUT"
          export OMI_RELEASE_SOURCE_SHA="$CHECKED_OUT_SHA"
          export OMI_RELEASE_PLATFORM=ios
          echo "OMI_RELEASE_SOURCE_SHA=$OMI_RELEASE_SOURCE_SHA" >> "$CM_ENV"
          echo "OMI_RELEASE_PLATFORM=$OMI_RELEASE_PLATFORM" >> "$CM_ENV"

          ADMISSION_PROOF="$(mktemp)"
          trap 'rm -f "$ADMISSION_PROOF"' EXIT
          python3 ../.github/scripts/collect_mobile_release_admission.py \
            --sha "$CHECKED_OUT_SHA" \
            --repository "${GITHUB_REPOSITORY:-BasedHardware/omi}" \
            --token "${GITHUB_TOKEN:-}" \
            --output "$ADMISSION_PROOF"
          python3 ../.github/scripts/verify_mobile_release_admission.py \
            --sha "$CHECKED_OUT_SHA" \
            --repository "${GITHUB_REPOSITORY:-BasedHardware/omi}" \
            --proof "$ADMISSION_PROOF"

          echo "Building iOS with version $BUILD_NAME ($BUILD_NUMBER)"
          if ! PROVENANCE_ARGS="$(scripts/build_provenance_dart_defines.sh)"; then
            echo "Build provenance validation failed; refusing to build." >&2
            exit 1
          fi
          flutter build ipa \
            --release \
            --build-name=$BUILD_NAME \
            --build-number=$BUILD_NUMBER \
            --flavor prod \
            --export-options-plist=$HOME/export_options.plist \
            $PROVENANCE_ARGS

    artifacts:
      - build/ios/ipa/*.ipa
      - /tmp/xcodebuild_logs/*.log
      - flutter_drive.log
      - $HOME/Library/Developer/Xcode/DerivedData/**/Build/**/*.dSYM
    publishing:
      scripts:
        - name: Upload debug symbols to Firebase Crashlytics
          script: |
            echo "Finding all debug symbol files..."

            # Find all dSYM files in the derived data directory
            dsymFiles=$(find $HOME/Library/Developer/Xcode/DerivedData -name "*.dSYM" -type d 2>/dev/null)

            if [[ -z "$dsymFiles" ]]
            then
              echo "No debug symbols were found, skip publishing to Firebase Crashlytics"
            else
              echo "Found dSYM files:"
              echo "$dsymFiles"
              echo ""

              # Find the upload-symbols script in the Pods directory
              uploadScriptPath=$(find ios/Pods/FirebaseCrashlytics -name "upload-symbols" -type f 2>/dev/null | head -1)

              if [[ -n ${uploadScriptPath} ]]
              then
                echo "Found upload-symbols script at: $uploadScriptPath"
                # Make sure it's executable
                chmod +x "$uploadScriptPath"

                # Upload each dSYM file
                echo "$dsymFiles" | while IFS= read -r dsymPath; do
                  if [[ -n "$dsymPath" ]]; then
                    echo "Uploading dSYM: $dsymPath"
                    "$uploadScriptPath" \
                      -gsp ios/Runner/GoogleService-Info.plist -p ios "$dsymPath"
                    echo "Completed upload for: $dsymPath"
                    echo ""
                  fi
                done

                echo "All dSYM files uploaded successfully"
              else
                echo "upload-symbols script not found. Skipping Firebase Crashlytics upload."
              fi
            fi
      email:
        recipients:
          - ngocthinhdp@gmail.com
          - joan@basedhardware.com
          - nik@basedhardware.com
          - mohsin.lp710@gmail.com
          - i@m13v.com
        notify:
          success: true
          failure: false
      app_store_connect:
        auth: integration
        submit_to_testflight: true
        # Internal-type TestFlight groups auto-receive every processed build;
        # Apple rejects explicit assignment ("Cannot add internal group to a
        # build"). Only external groups (Discord Folks) are named here.
        beta_groups:
          - Discord Folks
        submit_to_app_store: false

  macos-prod-appstore:
    name: Release macOS to App Store
    instance_type: mac_mini_m4
    max_build_duration: 120
    integrations:
      app_store_connect: codemagic_v4
    environment:
      groups:
        - app_env
        - firebase
        - appstore_credentials
      vars:
        APP_ID: 6502156163
        BUNDLE_ID: "com.friend-app-with-wearable.ios12"
      flutter: 3.44.5
      xcode: 26.0.1
      cocoapods: 1.16.2
    cache:
      cache_paths:
        - $HOME/.pub-cache
        - $HOME/Library/Caches/CocoaPods
    #triggering:
    #  events:
    #    - tag
    #  tag_patterns:
    #    - pattern: "v*-desktop-cm"
    #      include: true
    working_directory: app
    scripts:
      - name: Set up Firebase
        script: |
          dart pub global activate flutterfire_cli

          # https://github.com/invertase/flutterfire_cli/issues/233
          echo "$FIREBASE_SERVICE_ACCOUNT_KEY" > ./firebase_key.json
          flutterfire config \
            --platforms="macos" \
            --out=lib/firebase_options_prod.dart \
            --macos-bundle-id=com.friend-app-with-wearable.ios12 \
            --macos-out=macos/Config/Prod \
            --service-account="./firebase_key.json" \
            --project="based-hardware" \
            --macos-target="Runner" \
            --yes

          # DEV, should remove
          echo "$FIREBASE_SERVICE_ACCOUNT_DEV_KEY" > ./firebase_dev_key.json
          flutterfire config \
            --platforms="macos" \
            --out=lib/firebase_options_dev.dart \
            --macos-bundle-id=com.friend-app-with-wearable.ios12.development \
            --macos-out=macos/Config/Dev/ \
            --service-account="./firebase_dev_key.json" \
            --project="based-hardware-dev" \
            --macos-target="Runner" \
            --yes

      - name: Set up Google Service Info
        script: |
          # Copy GoogleService-Info.plist
          cp macos/Config/Prod/GoogleService-Info.plist macos/GoogleService-Info.plist

      - name: Set up App .env
        script: |
          echo OPENAI_API_KEY=$OPENAI_API_KEY >> .env
          echo INSTABUG_API_KEY=$INSTABUG_API_KEY >> .env
          echo ONESIGNAL_APP_ID=$ONESIGNAL_APP_ID >> .env
          echo API_BASE_URL=https://api.omi.me/ >> .env
          echo GROWTHBOOK_API_KEY=$GROWTHBOOK_API_KEY >> .env
          echo GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY >> .env
          echo INTERCOM_APP_ID=$INTERCOM_APP_ID >> .env
          echo INTERCOM_IOS_API_KEY=$INTERCOM_IOS_API_KEY >> .env
          echo POSTHOG_API_KEY=$POSTHOG_API_KEY >> .env
          echo GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID >> .env
          echo GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET >> .env

      - name: Get Flutter packages
        script: |
          flutter pub get

      - name: Run build runner
        script: |
          dart run build_runner build --delete-conflicting-outputs

      - name: Install pods
        script: |
          cd macos && pod install --repo-update

      - name: Set up keychain for code signing
        script: |
          keychain initialize

      - name: Fetch signing files
        script: |
          app-store-connect fetch-signing-files "$BUNDLE_ID" \
            --platform MAC_OS \
            --type MAC_APP_STORE \
            --create

      - name: Fetch Mac Installer Distribution certificates
        script: |
          # Try to find an existing certificate with our private key
          echo "Checking for existing Mac Installer Distribution certificate with our private key..."
          if app-store-connect certificates list --type MAC_INSTALLER_DISTRIBUTION --save 2>&1 | grep -q "Saved Signing Certificate"; then
            echo "Found existing certificate with matching private key"
          else
            echo "No matching certificate found, creating new one for Codemagic..."
            app-store-connect certificates create --type MAC_INSTALLER_DISTRIBUTION --save
          fi

      - name: Add certificates to keychain
        script: |
          keychain add-certificates

      - name: Set up code signing settings
        script: |
          xcode-project use-profiles

      - name: Flutter build macOS
        script: |
          # Build
          BUILD_NAME=$(echo $CM_TAG | sed 's/^v\(.*\)+\(.*\)-desktop-cm$/\1/')
          BUILD_NUMBER=$(echo $CM_TAG | sed 's/^v\(.*\)+\(.*\)-desktop-cm$/\2/')
          flutter build macos \
            --release \
            --build-name=$BUILD_NAME \
            --build-number=$BUILD_NUMBER \
            --flavor prod

      - name: Package application for App Store
        script: |
          set -x

          # Find the app bundle
          APP_NAME=$(find $(pwd) -name "Omi.app" | head -1)
          echo "Found app at: $APP_NAME"

          # Set output directory
          OUTPUT_DIR="$(pwd)/build/macos"
          mkdir -p "$OUTPUT_DIR"

          # Create unsigned package
          PACKAGE_NAME="Omi.pkg"
          xcrun productbuild --component "$APP_NAME" /Applications/ "$OUTPUT_DIR/unsigned.pkg"

          # Find the installer certificate common name in keychain
          INSTALLER_CERT_NAME=$(keychain list-certificates \
            | jq -r '.[] | select(.common_name | contains("3rd Party Mac Developer Installer")) | .common_name' \
            | head -1)

          if [ -z "$INSTALLER_CERT_NAME" ]; then
            echo "ERROR: No Mac Installer Distribution certificate found"
            echo "Available certificates:"
            keychain list-certificates | jq -r '.[].common_name'
            exit 1
          fi

          echo "Using installer certificate: $INSTALLER_CERT_NAME"

          # Sign the package
          xcrun productsign --sign "$INSTALLER_CERT_NAME" "$OUTPUT_DIR/unsigned.pkg" "$OUTPUT_DIR/$PACKAGE_NAME"

          # Clean up
          rm -f "$OUTPUT_DIR/unsigned.pkg"

          echo "Package created at: $OUTPUT_DIR/$PACKAGE_NAME"

    artifacts:
      - build/macos/*.pkg
      - build/macos/**/*.app
    publishing:
      email:
        recipients:
          - ngocthinhdp@gmail.com
          - joan@basedhardware.com
          - nik@basedhardware.com
          - mohsin.lp710@gmail.com
          - i@m13v.com
        notify:
          success: true
          failure: false
      app_store_connect:
        auth: integration
        submit_to_testflight: true
        # Internal-type TestFlight groups auto-receive every processed build;
        # Apple rejects explicit assignment ("Cannot add internal group to a
        # build"). Only external groups (Discord Folks) are named here.
        beta_groups:
          - Discord Folks
        submit_to_app_store: false

  android-prod-internal:
    name: Release Android Internal Production
    instance_type: mac_mini_m2
    max_build_duration: 120
    environment:
      android_signing:
        - prod_android_upload_keystore
      groups:
        - app_env
        - firebase
        - google_play
        - shorebird
      vars:
        PACKAGE_NAME: "com.friend.ios"
        JAVA_TOOL_OPTIONS: "-Xmx8g"
      flutter: 3.44.5
      xcode: 16.4
      cocoapods: 1.16.2
      java: 21
    cache:
      cache_paths:
        - $HOME/opt
        - $HOME/.pub-cache
        - $HOME/.gradle/caches
    triggering:
      events:
        - tag
      tag_patterns:
        - pattern: "v*-android-cm"
          include: true
        - pattern: "v*-mobile-cm"
          include: true
    working_directory: app
    scripts:
      - name: Installing Android SDK
        script: |
          export ANDROID_HOME="$HOME/opt/android-sdk"
          if [ ! -d "$ANDROID_HOME" ]; then
            echo "$ANDROID_HOME does not exist. Installing..."

            mkdir -p $ANDROID_HOME

            # Install cmdline-tools 11076708; NDK: 28.2.13676358
            cd "$HOME/opt" && \
            curl --fail --show-error --silent --connect-timeout 10.00 --max-time 120.00 \
              --output commandlinetools-mac-11076708_latest.zip \
              https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip && \
            7z -bd x commandlinetools-mac-11076708_latest.zip && \
            mkdir -p $ANDROID_HOME && \
            yes | cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} "tools" && \
            yes | cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} "cmdline-tools;latest" && \
            yes | cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} "ndk;28.2.13676358" && \
            yes | cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} --licenses  && \
            cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} --list
          fi

          if [ -d "$ANDROID_HOME" ]; then
            echo "$ANDROID_HOME does exist. Setting up env..."

            # PATH
            export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools

            # Override ENV
            echo ANDROID_HOME="$ANDROID_HOME" >> $CM_ENV
            echo ANDROID_SDK_ROOT="$ANDROID_HOME" >> $CM_ENV
            echo PATH="$PATH" >> $CM_ENV
          fi

      - name: Installing Opus
        script: |
          brew install opus
          brew install opus-tools

      - name: Set up Firebase
        script: |
          dart pub global activate flutterfire_cli

          # https://github.com/invertase/flutterfire_cli/issues/233
          echo "$FIREBASE_SERVICE_ACCOUNT_KEY" > ./firebase_key.json
          flutterfire config \
            --platforms="android,ios" \
            --out=lib/firebase_options_prod.dart \
            --ios-bundle-id=com.friend-app-with-wearable.ios12 \
            --android-package-name=com.friend.ios \
            --android-out=android/app/src/prod/  \
            --ios-out=ios/Config/Prod/ \
            --service-account="./firebase_key.json" \
            --project="based-hardware" \
            --ios-target="Runner" \
            --yes

          # DEV, should remove
          echo "$FIREBASE_SERVICE_ACCOUNT_DEV_KEY" > ./firebase_dev_key.json
          flutterfire config \
            --platforms="android,ios" \
            --out=lib/firebase_options_dev.dart \
            --ios-bundle-id=com.friend-app-with-wearable.ios12.development \
            --android-package-name=com.friend.ios.dev \
            --android-out=android/app/src/dev/  \
            --ios-out=ios/Config/Dev/ \
            --service-account="./firebase_dev_key.json" \
            --project="based-hardware-dev" \
            --ios-target="Runner" \
            --yes

      - name: Set up local.properties
        script: |
          echo "flutter.sdk=$HOME/programs/flutter" > "$(pwd)/android/local.properties"
          echo "sdk.dir=$ANDROID_HOME" >> "$(pwd)/android/local.properties"

      - name: Set up App .env
        script: |
          echo OPENAI_API_KEY=$OPENAI_API_KEY >> .env
          echo INSTABUG_API_KEY=$INSTABUG_API_KEY >> .env
          echo ONESIGNAL_APP_ID=$ONESIGNAL_APP_ID >> .env
          echo API_BASE_URL=https://api.omi.me/ >> .env
          echo GROWTHBOOK_API_KEY=$GROWTHBOOK_API_KEY >> .env
          echo GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY >> .env
          echo INTERCOM_APP_ID=$INTERCOM_APP_ID >> .env
          echo INTERCOM_ANDROID_API_KEY=$INTERCOM_ANDROID_API_KEY >> .env
          echo POSTHOG_API_KEY=$POSTHOG_API_KEY >> .env
          echo GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID >> .env
          echo GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET >> .env

      - name: Get Flutter packages
        script: |
          flutter packages pub get
      - name: Run build runner
        script: |
          dart run build_runner build --delete-conflicting-outputs
      - name: Build AAB with Flutter
        script: |
          set -euo pipefail
          # TODO: Don't tricky
          # Tricky Opus > build.gradle, force use NDK 28 to deal with "ERROR: Unknown host CPU architecture: arm64 and 16kb memory page size support"
          # echo "Z3JvdXAgJ2V1LmVwbncub3B1c19mbHV0dGVyX2FuZHJvaWQnDQp2ZXJzaW9uICcxLjAnDQoNCmJ1aWxkc2NyaXB0IHsNCiAgICByZXBvc2l0b3JpZXMgew0KICAgICAgICBnb29nbGUoKQ0KICAgICAgICBtYXZlbkNlbnRyYWwoKQ0KICAgIH0NCg0KICAgIGRlcGVuZGVuY2llcyB7DQogICAgICAgIGNsYXNzcGF0aCAnY29tLmFuZHJvaWQudG9vbHMuYnVpbGQ6Z3JhZGxlOjcuMy4wJw0KICAgIH0NCn0NCg0Kcm9vdFByb2plY3QuYWxscHJvamVjdHMgew0KICAgIHJlcG9zaXRvcmllcyB7DQogICAgICAgIGdvb2dsZSgpDQogICAgICAgIG1hdmVuQ2VudHJhbCgpDQogICAgfQ0KfQ0KDQphcHBseSBwbHVnaW46ICdjb20uYW5kcm9pZC5saWJyYXJ5Jw0KDQphbmRyb2lkIHsNCiAgICBpZiAocHJvamVjdC5hbmRyb2lkLmhhc1Byb3BlcnR5KCJuYW1lc3BhY2UiKSkgew0KICAgICAgICBuYW1lc3BhY2UgJ2V1LmVwbncub3B1c19mbHV0dGVyX2FuZHJvaWQnDQogICAgfQ0KDQogICAgY29tcGlsZVNka1ZlcnNpb24gMzMNCg0KICAgIGNvbXBpbGVPcHRpb25zIHsNCiAgICAgICAgc291cmNlQ29tcGF0aWJpbGl0eSBKYXZhVmVyc2lvbi5WRVJTSU9OXzFfOA0KICAgICAgICB0YXJnZXRDb21wYXRpYmlsaXR5IEphdmFWZXJzaW9uLlZFUlNJT05fMV84DQogICAgfQ0KDQogICAgZGVmYXVsdENvbmZpZyB7DQogICAgICAgIG1pblNka1ZlcnNpb24gMTkNCiAgICB9DQogICAgZXh0ZXJuYWxOYXRpdmVCdWlsZCB7DQogICAgICAgIG5ka0J1aWxkew0KICAgICAgICAgICAgcGF0aCAiQW5kcm9pZC5tayINCiAgICAgICAgfQ0KICAgIH0NCg0KICAgIGRlcGVuZGVuY2llcyB7DQogICAgICAgIHRlc3RJbXBsZW1lbnRhdGlvbiAnanVuaXQ6anVuaXQ6NC4xMy4yJw0KICAgICAgICB0ZXN0SW1wbGVtZW50YXRpb24gJ29yZy5tb2NraXRvOm1vY2tpdG8tY29yZTo1LjAuMCcNCiAgICB9DQoNCiAgICB0ZXN0T3B0aW9ucyB7DQogICAgICAgIHVuaXRUZXN0cy5hbGwgew0KICAgICAgICAgICAgdGVzdExvZ2dpbmcgew0KICAgICAgICAgICAgICAgZXZlbnRzICJwYXNzZWQiLCAic2tpcHBlZCIsICJmYWlsZWQiLCAic3RhbmRhcmRPdXQiLCAic3RhbmRhcmRFcnJvciINCiAgICAgICAgICAgICAgIG91dHB1dHMudXBUb0RhdGVXaGVuIHtmYWxzZX0NCiAgICAgICAgICAgICAgIHNob3dTdGFuZGFyZFN0cmVhbXMgPSB0cnVlDQogICAgICAgICAgICB9DQogICAgICAgIH0NCiAgICB9DQogICAgbmRrVmVyc2lvbiAnMjcuMC4xMjA3Nzk3MycNCn0NCg==" | base64 -d > "$HOME/.pub-cache/hosted/pub.dev/opus_flutter_android-3.0.1/android/build.gradle"

          # The tag is the release identity. Validate its platform, source
          # commit, version, and build number before invoking Flutter.
          CHECKED_OUT_SHA="$(git rev-parse HEAD)"
          TAG_SOURCE_SHA="$(git rev-parse "${CM_TAG:?CM_TAG is required}^{commit}")"
          if ! IDENTITY_OUTPUT="$(python3 scripts/mobile_release_identity.py \
            --tag "${CM_TAG}" \
            --platform android \
            --source-sha "${CHECKED_OUT_SHA}" \
            --tag-source-sha "${TAG_SOURCE_SHA}" \
            --format shell)"; then
            echo "Mobile release identity validation failed; refusing to build." >&2
            exit 1
          fi
          eval "$IDENTITY_OUTPUT"
          export OMI_RELEASE_SOURCE_SHA="$CHECKED_OUT_SHA"
          export OMI_RELEASE_PLATFORM=android
          echo "OMI_RELEASE_SOURCE_SHA=$OMI_RELEASE_SOURCE_SHA" >> "$CM_ENV"
          echo "OMI_RELEASE_PLATFORM=$OMI_RELEASE_PLATFORM" >> "$CM_ENV"

          # Google Play version codes are global and immutable, and the
          # internal-auto lane keeps allocating them after a release tag
          # pins its build number. Keep the tag number when it is above the
          # live floor; otherwise allocate floor + 1 so publishing cannot
          # collide with an already-used code.
          PLAY_LOOKUP="$(mktemp)"
          if ! google-play get-latest-build-number \
              --package-name "$PACKAGE_NAME" --tracks internal \
              > "$PLAY_LOOKUP" 2>/dev/null; then
            echo "Google Play version lookup failed; refusing to guess a build number." >&2
            exit 1
          fi
          PLAY_PLAN="$(python3 scripts/mobile_play_build_number.py \
            --tag-number "$BUILD_NUMBER" \
            --play-output "$PLAY_LOOKUP")"
          rm -f "$PLAY_LOOKUP"
          TAG_BUILD_NUMBER="$BUILD_NUMBER"
          BUILD_NUMBER="$(jq -er '.build_number' <<<"$PLAY_PLAN")"
          echo "Android build number $BUILD_NUMBER (tag $TAG_BUILD_NUMBER, source: $(jq -r '.source' <<<"$PLAY_PLAN"))"

          ADMISSION_PROOF="$(mktemp)"
          trap 'rm -f "$ADMISSION_PROOF"' EXIT
          python3 ../.github/scripts/collect_mobile_release_admission.py \
            --sha "$CHECKED_OUT_SHA" \
            --repository "${GITHUB_REPOSITORY:-BasedHardware/omi}" \
            --token "${GITHUB_TOKEN:-}" \
            --output "$ADMISSION_PROOF"
          python3 ../.github/scripts/verify_mobile_release_admission.py \
            --sha "$CHECKED_OUT_SHA" \
            --repository "${GITHUB_REPOSITORY:-BasedHardware/omi}" \
            --proof "$ADMISSION_PROOF"

          echo "Building Android with version $BUILD_NAME ($BUILD_NUMBER)"
          if ! PROVENANCE_ARGS="$(scripts/build_provenance_dart_defines.sh)"; then
            echo "Build provenance validation failed; refusing to build." >&2
            exit 1
          fi
          flutter build appbundle \
            --release \
            --build-name=$BUILD_NAME \
            --build-number=$BUILD_NUMBER \
            --flavor prod \
            $PROVENANCE_ARGS


    artifacts:
      - build/**/outputs/**/*.aab
      - flutter_drive.log
    publishing:
      email:
        recipients:
          - ngocthinhdp@gmail.com
          - joan@basedhardware.com
          - nik@basedhardware.com
          - mohsin.lp710@gmail.com
          - i@m13v.com
        notify:
          success: true
          failure: false
      google_play:
        credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
        track: "internal"
        submit_as_draft: false
        release_promotion:
          track: "alpha"

  ios-prod-patch:
    name: Patch iOS to Production
    instance_type: mac_mini_m2
    max_build_duration: 120
    integrations:
      app_store_connect: codemagic_v4
    environment:
      ios_signing:
        provisioning_profiles:
          - codemagic_v7
          - codemagic_watchos_v7
          - codemagic_widget_v7
        certificates:
          - codemagic_v7
      groups:
        - app_env
        - firebase
        - shorebird
      vars:
        APP_ID: 6502156163
      flutter: 3.44.5
      xcode: 26.0.1
      cocoapods: 1.16.2
    cache:
      cache_paths:
        - $HOME/.pub-cache
        - $HOME/.gradle/caches
        - $HOME/Library/Caches/CocoaPods
    triggering:
      events:
        - tag
      tag_patterns:
        - pattern: "v*-ios-patch-cm"
          include: true
        - pattern: "v*-mobile-patch-cm"
          include: true
    working_directory: app
    scripts:
      - name: Installing Shorebird
        script: |
          # Install the Shorebird CLI
          curl --proto '=https' --tlsv1.2 https://raw.githubusercontent.com/shorebirdtech/install/main/install.sh -sSf | bash
          # Set Shorebird PATH
          echo PATH="/Users/builder/.shorebird/bin:$PATH" >> $CM_ENV

      - name: Installing Opus
        script: |
          brew install opus
          brew install opus-tools

      - name: Set up Firebase
        script: |
          dart pub global activate flutterfire_cli

          # https://github.com/invertase/flutterfire_cli/issues/233
          echo "$FIREBASE_SERVICE_ACCOUNT_KEY" > ./firebase_key.json
          flutterfire config \
            --platforms="android,ios" \
            --out=lib/firebase_options_prod.dart \
            --ios-bundle-id=com.friend-app-with-wearable.ios12 \
            --android-package-name=com.friend.ios \
            --android-out=android/app/src/prod/  \
            --ios-out=ios/Config/Prod/ \
            --service-account="./firebase_key.json" \
            --project="based-hardware" \
            --ios-target="Runner" \
            --yes

          # DEV, should remove
          echo "$FIREBASE_SERVICE_ACCOUNT_DEV_KEY" > ./firebase_dev_key.json
          flutterfire config \
            --platforms="android,ios" \
            --out=lib/firebase_options_dev.dart \
            --ios-bundle-id=com.friend-app-with-wearable.ios12.development \
            --android-package-name=com.friend.ios.dev \
            --android-out=android/app/src/dev/  \
            --ios-out=ios/Config/Dev/ \
            --service-account="./firebase_dev_key.json" \
            --project="based-hardware-dev" \
            --ios-target="Runner" \
            --yes

      - name: Set up App .env
        script: |
          echo OPENAI_API_KEY=$OPENAI_API_KEY >> .env
          echo INSTABUG_API_KEY=$INSTABUG_API_KEY >> .env
          echo ONESIGNAL_APP_ID=$ONESIGNAL_APP_ID >> .env
          echo API_BASE_URL=https://api.omi.me/ >> .env
          echo GROWTHBOOK_API_KEY=$GROWTHBOOK_API_KEY >> .env
          echo GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY >> .env
          echo INTERCOM_APP_ID=$INTERCOM_APP_ID >> .env
          echo INTERCOM_IOS_API_KEY=$INTERCOM_IOS_API_KEY >> .env
          echo POSTHOG_API_KEY=$POSTHOG_API_KEY >> .env
          echo GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID >> .env
          echo GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET >> .env

      - name: Set up Google Service Info
        script: |
          # TODO: check why we need this ?
          echo "$GOOGLE_INFO_PLIST_KEY" > "$(pwd)/ios/Runner/GoogleService-Info.plist"

      - name: Get Flutter packages
        script: |
          flutter pub get

      - name: Run build runner
        script: |
          dart run build_runner build --delete-conflicting-outputs

      - name: Installs pod
        script: |
          cd ios && pod install --repo-update

      - name: Patch iOS app with Shorebird
        script: |
          # Set up code signing settings on Xcode project
          # -> $HOME/export_options.plist
          xcode-project use-profiles

          # Build
          if ! PROVENANCE_ARGS="$(scripts/build_provenance_dart_defines.sh)"; then
            echo "Build provenance validation failed; refusing to patch." >&2
            exit 1
          fi
          shorebird patch ios \
            --flavor prod -- \
            --export-options-plist=$HOME/export_options.plist \
            $PROVENANCE_ARGS
    artifacts:
      - build/ios/ipa/*.ipa
      - /tmp/xcodebuild_logs/*.log
      - flutter_drive.log
    publishing:
      email:
        recipients:
          - ngocthinhdp@gmail.com
          - joan@basedhardware.com
          - nik@basedhardware.com
          - mohsin.lp710@gmail.com
          - i@m13v.com
        notify:
          success: true
          failure: false

  # Retained only for local artifact diagnostics; it has no trigger or publisher.
  macos-prod-legacy-no-publish:
    instance_type: mac_mini_m4
    max_build_duration: 120
    integrations:
      app_store_connect: codemagic_v4
    environment:
      groups:
        - app_env
        - firebase
        - appstore_credentials
      vars:
        PLATFORM: "macos"
        BUNDLE_ID: "com.friend-app-with-wearable.ios12"
        DMGBUILD_VERSION: "1.6.7"
      flutter: 3.44.5
      xcode: 26.0.1
      cocoapods: 1.16.2
    cache:
      cache_paths:
        - $HOME/.pub-cache
        - $HOME/Library/Caches/CocoaPods
    # DISABLED: Triggering removed — replaced by omi-desktop-swift-release workflow
    working_directory: app
    scripts:
      - name: Set up Firebase
        script: |
          dart pub global activate flutterfire_cli

          # https://github.com/invertase/flutterfire_cli/issues/233
          echo "$FIREBASE_SERVICE_ACCOUNT_KEY" > ./firebase_key.json
          flutterfire config \
            --platforms="macos" \
            --out=lib/firebase_options_prod.dart \
            --macos-bundle-id=com.friend-app-with-wearable.ios12 \
            --macos-out=macos/Config/Prod \
            --service-account="./firebase_key.json" \
            --project="based-hardware" \
            --macos-target="Runner" \
            --yes

          # DEV, should remove
          echo "$FIREBASE_SERVICE_ACCOUNT_DEV_KEY" > ./firebase_dev_key.json
          flutterfire config \
            --platforms="macos" \
            --out=lib/firebase_options_dev.dart \
            --macos-bundle-id=com.friend-app-with-wearable.ios12.development \
            --macos-out=macos/Config/Dev/ \
            --service-account="./firebase_dev_key.json" \
            --project="based-hardware-dev" \
            --macos-target="Runner" \
            --yes

      - name: Set up Google Service Info
        script: |
          # Copy GoogleService-Info.plist
          cp macos/Config/Prod/GoogleService-Info.plist macos/GoogleService-Info.plist

      - name: Set up App .env
        script: |
          echo OPENAI_API_KEY=$OPENAI_API_KEY >> .env
          echo INSTABUG_API_KEY=$INSTABUG_API_KEY >> .env
          echo ONESIGNAL_APP_ID=$ONESIGNAL_APP_ID >> .env
          echo GROWTHBOOK_API_KEY=$GROWTHBOOK_API_KEY >> .env
          echo GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY >> .env
          echo INTERCOM_APP_ID=$INTERCOM_APP_ID >> .env
          echo INTERCOM_IOS_API_KEY=$INTERCOM_IOS_API_KEY >> .env
          echo POSTHOG_API_KEY=$POSTHOG_API_KEY >> .env
          echo GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID >> .env
          echo GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET >> .env
          echo GEMINI_API_KEY=$GEMINI_API_KEY >> .env

      - name: Extract version (from tag or auto-increment)
        script: |
          # Check if triggered by tag or push
          if [ -n "$CM_TAG" ]; then
            # Tag-triggered: extract version from tag (supports both -desktop-cm and -macos-cm)
            echo "Triggered by tag: $CM_TAG"
            BUILD_NAME=$(echo $CM_TAG | sed -E 's/^v([0-9.]+)\+([0-9]+)-(desktop|macos)-cm$/\1/')
            BUILD_NUMBER=$(echo $CM_TAG | sed -E 's/^v([0-9.]+)\+([0-9]+)-(desktop|macos)-cm$/\2/')
            TAG_NAME="$CM_TAG"
          else
            # Push-triggered: extract version from pubspec and auto-increment build number
            echo "Triggered by push to main, auto-incrementing build number..."

            # Extract version name from pubspec.yaml (source of truth)
            PUBSPEC_VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //' | tr -d ' ')
            BUILD_NAME=$(echo "$PUBSPEC_VERSION" | cut -d'+' -f1)  # e.g., "1.0.78"

            # Get latest build number from GitHub releases for desktop tags and increment
            LATEST_BUILD=$(gh release list --repo BasedHardware/omi --limit 100 | grep -E 'desktop-cm|macos-cm|desktop-auto' | head -1 | grep -oE '\+[0-9]+' | tr -d '+' || echo "0")
            if [ -z "$LATEST_BUILD" ] || [ "$LATEST_BUILD" = "0" ]; then
              # Fallback: try to get build number from release title format "Omi Desktop vX.X.X (BUILD_NUM)"
              LATEST_BUILD=$(gh release list --repo BasedHardware/omi --limit 100 | grep -i 'desktop\|macos' | head -1 | grep -oE '\([0-9]+\)' | tr -d '()' || echo "0")
            fi
            if [ -z "$LATEST_BUILD" ]; then
              LATEST_BUILD=0
            fi
            BUILD_NUMBER=$((LATEST_BUILD + 1))
            TAG_NAME="v${BUILD_NAME}+${BUILD_NUMBER}-desktop-auto"
          fi

          echo "Building macOS with version $BUILD_NAME ($BUILD_NUMBER)"

          # Export to environment for all subsequent steps
          echo BUILD_NAME="$BUILD_NAME" >> $CM_ENV
          echo BUILD_NUMBER="$BUILD_NUMBER" >> $CM_ENV
          echo VERSION="${BUILD_NAME}+${BUILD_NUMBER}" >> $CM_ENV
          echo TAG_NAME="$TAG_NAME" >> $CM_ENV

      - name: Get Flutter packages
        script: |
          flutter pub get

      - name: Run build runner
        script: |
          dart run build_runner build --delete-conflicting-outputs

      - name: Install pods
        script: |
          cd macos && pod install --repo-update

      - name: Download Omi Computer app for bundling
        script: |
          # Get latest Omi Computer release (Swift app) to bundle into Flutter app
          echo "Looking for latest Omi Computer release..."
          LATEST_MACOS_TAG=$(gh release list --repo BasedHardware/omi --limit 50 | grep -E '\-macos\s' | head -1 | awk -F'\t' '{print $3}')

          if [ -n "$LATEST_MACOS_TAG" ]; then
            echo "Downloading Omi Computer from release: $LATEST_MACOS_TAG"

            # Create directory where bundle_helper.sh expects to find the app
            # bundle_helper.sh looks at: $PROJECT_DIR/../../desktop/macos/build/
            # PROJECT_DIR = /path/clone/app/macos
            # From working_directory (app/), that's ../desktop
            mkdir -p ../desktop/macos/build

            # Download the signed app ZIP from GitHub releases
            gh release download "$LATEST_MACOS_TAG" \
              --repo BasedHardware/omi \
              --pattern "Omi.zip" \
              --dir /tmp

            # Extract - the ZIP contains "Omi Computer.app"
            unzip -o /tmp/Omi.zip -d ../desktop/macos/build/

            # Verify extraction
            if [ -d "../desktop/macos/build/Omi Computer.app" ]; then
              echo "SUCCESS: Omi Computer app ready for bundling"
              ls -la "../desktop/macos/build/"
            else
              echo "WARNING: Extraction may have failed, checking contents..."
              ls -la ../desktop/macos/build/
            fi
          else
            echo "WARNING: No Omi Computer release found with -macos tag"
            echo "The Flutter app will be built without bundled Omi Computer"
          fi

      - name: Verify Sparkle signing key
        script: |
          if [ -z "$SPARKLE_PRIVATE_KEY" ]; then
            echo "ERROR: SPARKLE_PRIVATE_KEY environment variable not set"
            exit 1
          fi

      - name: Clean up entitlements (this step can be removed once we move away from AppStore)
        script: |
          # Process all Release entitlements files (including -prod variants)
          find macos -name "*Release*.entitlements" -type f | while read entitlements_file; do
            # Remove Sign in with Apple (if present)
            grep -q "applesignin" "$entitlements_file" 2>/dev/null && \
              /usr/libexec/PlistBuddy -c "Delete :com.apple.developer.applesignin" "$entitlements_file" 2>/dev/null || true

            # Remove App Sandbox (if present)
            grep -q "com.apple.security.app-sandbox" "$entitlements_file" 2>/dev/null && \
              /usr/libexec/PlistBuddy -c "Delete :com.apple.security.app-sandbox" "$entitlements_file" 2>/dev/null || true

            # Remove get-task-allow (debug-only, blocks notarization)
            /usr/libexec/PlistBuddy -c "Delete :com.apple.security.get-task-allow" "$entitlements_file" 2>/dev/null || true

            # Add hardened runtime entitlements
            /usr/libexec/PlistBuddy -c "Set :com.apple.security.cs.allow-jit bool true" "$entitlements_file" 2>/dev/null || \
              /usr/libexec/PlistBuddy -c "Add :com.apple.security.cs.allow-jit bool true" "$entitlements_file" 2>/dev/null || true
            /usr/libexec/PlistBuddy -c "Set :com.apple.security.cs.allow-unsigned-executable-memory bool true" "$entitlements_file" 2>/dev/null || \
              /usr/libexec/PlistBuddy -c "Add :com.apple.security.cs.allow-unsigned-executable-memory bool true" "$entitlements_file" 2>/dev/null || true
            /usr/libexec/PlistBuddy -c "Set :com.apple.security.cs.disable-library-validation bool true" "$entitlements_file" 2>/dev/null || \
              /usr/libexec/PlistBuddy -c "Add :com.apple.security.cs.disable-library-validation bool true" "$entitlements_file" 2>/dev/null || true
          done

      - name: Set up keychain and import Developer ID certificate
        script: |
          # Initialize keychain
          keychain initialize

          # Import Developer ID Application certificate
          echo "$MACOS_DEVELOPER_ID_P12" | base64 --decode > /tmp/developer_id.p12

          KEYCHAIN_PATH=$(keychain get-default)

          security import /tmp/developer_id.p12 \
            -k "$KEYCHAIN_PATH" \
            -P "${MACOS_DEVELOPER_ID_P12_PASSWORD:-}" \
            -T /usr/bin/codesign \
            -T /usr/bin/security

          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" "$KEYCHAIN_PATH" 2>/dev/null || true

          rm /tmp/developer_id.p12

          # Find and export the Developer ID cert name
          DEVELOPER_ID_CERT=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | awk -F'"' '{print $2}')

          if [ -z "$DEVELOPER_ID_CERT" ]; then
            echo "ERROR: No Developer ID Application certificate found!"
            exit 1
          fi

          echo "DEVELOPER_ID_CERT=$DEVELOPER_ID_CERT" >> $CM_ENV

      - name: Install Developer ID provisioning profile
        script: |
          mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"

          # Decode and save the provisioning profile
          echo "$MACOS_DEVELOPER_ID_PROFILE" | base64 --decode > /tmp/developer_id.provisionprofile

          # Extract the plist from the CMS-signed provisioning profile
          security cms -D -i /tmp/developer_id.provisionprofile > /tmp/profile.plist

          # Get the profile UUID
          PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print :UUID" /tmp/profile.plist)

          # Now install it with the UUID as the filename (this is what Xcode expects)
          cp /tmp/developer_id.provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/$PROFILE_UUID.provisionprofile"

          # Export UUID for later use
          echo "PROFILE_UUID=$PROFILE_UUID" >> $CM_ENV

      - name: Configure Xcode project for Developer ID signing
        script: |
          cd macos

          # Use Codemagic's xcode-project tool to configure manual signing
          xcode-project use-profiles \
            --project Runner.xcodeproj \
            --code-signing-setup-verbose-logging

          cd ..

      - name: Flutter build macOS
        script: |
          # Build with release configuration using Developer ID certificate and provisioning profile
          export CODE_SIGN_STYLE=Manual
          export CODE_SIGN_IDENTITY="$DEVELOPER_ID_CERT"
          export DEVELOPMENT_TEAM=9536L8KLMP
          export PROVISIONING_PROFILE_SPECIFIER="$PROFILE_UUID"

          flutter build macos \
            --release \
            --build-name=$BUILD_NAME \
            --build-number=$BUILD_NUMBER \
            --flavor prod

      - name: Bundle .env into app
        script: |
          # Copy .env file into app bundle for native Swift code to access
          APP_PATH=$(find $(pwd)/build/macos -name "Omi.app" -type d | head -1)
          cp .env "$APP_PATH/Contents/Resources/.env"
          echo "Bundled .env into $APP_PATH/Contents/Resources/.env"

      - name: Sign app for notarization
        script: |
          # Find the built app
          APP_PATH=$(find $(pwd)/build/macos -name "Omi.app" -type d | head -1)

          # Create hardened runtime entitlements for notarization
          cat > /tmp/notarization.entitlements << 'ENTITLEMENTS_EOF'
          <?xml version="1.0" encoding="UTF-8"?>
          <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
          <plist version="1.0">
          <dict>
            <key>com.apple.security.cs.allow-jit</key>
            <true/>
            <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
            <true/>
            <key>com.apple.security.cs.disable-library-validation</key>
            <true/>
            <key>com.apple.security.device.audio-input</key>
            <true/>
            <key>com.apple.security.device.bluetooth</key>
            <true/>
            <key>com.apple.security.network.client</key>
            <true/>
            <key>com.apple.security.network.server</key>
            <true/>
            <key>com.apple.security.personal-information.calendars</key>
            <true/>
            <key>com.apple.security.files.user-selected.read-only</key>
            <true/>
            <key>com.apple.developer.aps-environment</key>
            <string>production</string>
            <key>keychain-access-groups</key>
            <array>
              <string>9536L8KLMP.com.google.GIDSignIn</string>
              <string>9536L8KLMP.com.friend-app-with-wearable.ios12</string>
            </array>
          </dict>
          </plist>
          ENTITLEMENTS_EOF

          # Sign ALL embedded frameworks FIRST (REQUIRED when re-signing app with --force)
          # Sign embedded frameworks
          find "$APP_PATH/Contents/Frameworks" -name "*.framework" -type d -print0 | while IFS= read -r -d '' framework; do
            codesign --force --timestamp --options runtime \
              --sign "$DEVELOPER_ID_CERT" \
              "$framework"
          done

          # Sign any dylibs
          find "$APP_PATH" -name "*.dylib" -type f -print0 | while IFS= read -r -d '' dylib; do
            codesign --force --timestamp --options runtime \
              --sign "$DEVELOPER_ID_CERT" \
              "$dylib"
          done

          # Sign bundled Omi Computer.app (if present)
          # The bundled app was signed with a different cert, must re-sign with our Developer ID
          BUNDLED_APP="$APP_PATH/Contents/MacOS/Omi Computer.app"
          if [ -d "$BUNDLED_APP" ]; then
            echo "Signing bundled Omi Computer.app..."
            # Sign all nested code first
            find "$BUNDLED_APP" -name "*.framework" -type d | while read fw; do
              codesign --force --timestamp --options runtime \
                --sign "$DEVELOPER_ID_CERT" "$fw"
            done
            find "$BUNDLED_APP" -name "*.dylib" -type f | while read lib; do
              codesign --force --timestamp --options runtime \
                --sign "$DEVELOPER_ID_CERT" "$lib"
            done
            # Sign the bundled app itself
            codesign --force --timestamp --options runtime \
              --sign "$DEVELOPER_ID_CERT" \
              "$BUNDLED_APP"
            echo "Bundled Omi Computer.app signed"
          fi

          # Sign Sparkle framework and its components first
          SPARKLE_FW="$APP_PATH/Contents/Frameworks/Sparkle.framework"

          if [ -d "$SPARKLE_FW" ]; then
            # Sign XPC services
            find "$SPARKLE_FW" -name "*.xpc" -type d | while read xpc; do
              codesign --force --timestamp --options runtime \
                --sign "$DEVELOPER_ID_CERT" \
                "$xpc"
            done

            # Sign Updater.app
            [ -d "$SPARKLE_FW/Versions/B/Updater.app" ] && \
              codesign --force --timestamp --options runtime \
                --sign "$DEVELOPER_ID_CERT" \
                "$SPARKLE_FW/Versions/B/Updater.app"

            # Sign Autoupdate binary
            [ -f "$SPARKLE_FW/Versions/B/Autoupdate" ] && \
              codesign --force --timestamp --options runtime \
                --sign "$DEVELOPER_ID_CERT" \
                "$SPARKLE_FW/Versions/B/Autoupdate"

            # Sign the framework itself
            codesign --force --timestamp --options runtime \
              --sign "$DEVELOPER_ID_CERT" \
              "$SPARKLE_FW"
          fi

          # Sign the app bundle with --force to update entitlements
          codesign --force --timestamp --options runtime \
            --sign "$DEVELOPER_ID_CERT" \
            --entitlements /tmp/notarization.entitlements \
            "$APP_PATH"

          # Verify no get-task-allow entitlement (blocks notarization)
          if codesign -d --entitlements :- "$APP_PATH" 2>&1 | grep -q "get-task-allow"; then
            echo "ERROR: get-task-allow entitlement found"
            exit 1
          fi

      - name: Notarize app
        script: |
          set -e

          APP_PATH=$(find $(pwd)/build/macos -name "Omi.app" -type d | head -1)
          APP_ZIP="build/macos/Omi.zip"
          ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "$APP_ZIP"

          # Check if private key is a file or content
          if [[ "$APP_STORE_CONNECT_PRIVATE_KEY" == /* ]] || [[ "$APP_STORE_CONNECT_PRIVATE_KEY" == @file:* ]]; then
            PRIVATE_KEY_PATH="${APP_STORE_CONNECT_PRIVATE_KEY#@file:}"
          else
            mkdir -p ~/private_keys
            PRIVATE_KEY_PATH=~/private_keys/AuthKey_${APP_STORE_CONNECT_KEY_IDENTIFIER}.p8
            echo -e "$APP_STORE_CONNECT_PRIVATE_KEY" > "$PRIVATE_KEY_PATH"
          fi

          RESULT=$(xcrun notarytool submit "$APP_ZIP" \
            --key "$PRIVATE_KEY_PATH" \
            --key-id "$APP_STORE_CONNECT_KEY_IDENTIFIER" \
            --issuer "$APP_STORE_CONNECT_ISSUER_ID" \
            --wait \
            --output-format json)

          SUBMISSION_ID=$(echo "$RESULT" | jq -r '.id')
          STATUS=$(echo "$RESULT" | jq -r '.status')

          if [ "$STATUS" != "Accepted" ]; then
            echo "Notarization failed"
            xcrun notarytool log "$SUBMISSION_ID" \
              --key "$PRIVATE_KEY_PATH" \
              --key-id "$APP_STORE_CONNECT_KEY_IDENTIFIER" \
              --issuer "$APP_STORE_CONNECT_ISSUER_ID"
            exit 1
          fi

          xcrun stapler staple "$APP_PATH"

      - name: Create DMG installer
        script: |
          set -e
          pip3 install --break-system-packages "dmgbuild==$DMGBUILD_VERSION"
          APP_PATH=$(find $(pwd)/build/macos -name "Omi.app" -type d | head -1)

          mkdir -p build/macos/dmg
          DMG_PATH="build/macos/dmg/omi.dmg"

          # Use dmgbuild instead of create-dmg — writes .DS_Store directly
          # without Finder/AppleScript (which hangs in CI with --skip-jenkins)
          dmgbuild -s ../desktop/macos/dmg-assets/dmgbuild_settings.py \
            -D app_path="$APP_PATH" \
            -D app_name=Omi \
            -D assets_dir="$(pwd)/../desktop/macos/dmg-assets" \
            "Omi ${VERSION}" \
            "$DMG_PATH"

          # Sign DMG
          codesign --force --sign "$DEVELOPER_ID_CERT" "$DMG_PATH"
          codesign --verify --verbose "$DMG_PATH"

      - name: Notarize DMG
        script: |
          set -e

          DMG_PATH=$(find build/macos/dmg -name "omi.dmg" | head -1)

          # Check if private key is a file or content
          if [[ "$APP_STORE_CONNECT_PRIVATE_KEY" == /* ]] || [[ "$APP_STORE_CONNECT_PRIVATE_KEY" == @file:* ]]; then
            PRIVATE_KEY_PATH="${APP_STORE_CONNECT_PRIVATE_KEY#@file:}"
          else
            mkdir -p ~/private_keys
            PRIVATE_KEY_PATH=~/private_keys/AuthKey_${APP_STORE_CONNECT_KEY_IDENTIFIER}.p8
            echo -e "$APP_STORE_CONNECT_PRIVATE_KEY" > "$PRIVATE_KEY_PATH"
          fi

          RESULT=$(xcrun notarytool submit "$DMG_PATH" \
            --key "$PRIVATE_KEY_PATH" \
            --key-id "$APP_STORE_CONNECT_KEY_IDENTIFIER" \
            --issuer "$APP_STORE_CONNECT_ISSUER_ID" \
            --wait \
            --output-format json)

          SUBMISSION_ID=$(echo "$RESULT" | jq -r '.id')
          STATUS=$(echo "$RESULT" | jq -r '.status')

          if [ "$STATUS" != "Accepted" ]; then
            echo "DMG notarization failed"
            xcrun notarytool log "$SUBMISSION_ID" \
              --key "$PRIVATE_KEY_PATH" \
              --key-id "$APP_STORE_CONNECT_KEY_IDENTIFIER" \
              --issuer "$APP_STORE_CONNECT_ISSUER_ID"
            exit 1
          fi

          xcrun stapler staple "$DMG_PATH"

      - name: Create and sign Sparkle-compatible ZIP
        script: |
          APP_PATH="$(pwd)/build/macos/Build/Products/Release-prod/Omi.app"

          ZIP_NAME="Omi.zip"
          DIST_DIR="dist/${VERSION}-macos"
          APP_DIR=$(pwd)

          mkdir -p "$DIST_DIR"

          # Create ZIP that Sparkle can use
          cd "$(dirname "$APP_PATH")"
          ditto -c -k --sequesterRsrc --keepParent "Omi.app" "$(pwd)/$ZIP_NAME"
          mv "$ZIP_NAME" "$APP_DIR/$DIST_DIR/"

          # Sign the ZIP file for Sparkle
          cd "$APP_DIR"

          [ ! -f "macos/Pods/Sparkle/bin/sign_update" ] && echo "ERROR: Sparkle sign_update binary not found" && exit 1

          # Sign using Sparkle's sign_update with private key from environment variable
          SIGN_OUTPUT=$(echo "$SPARKLE_PRIVATE_KEY" | macos/Pods/Sparkle/bin/sign_update --ed-key-file - "$DIST_DIR/$ZIP_NAME" 2>&1)

          # Extract the signature value
          SIGNATURE=$(echo "$SIGN_OUTPUT" | grep -oE 'sparkle:edSignature="[^"]+"' | sed 's/sparkle:edSignature="//;s/"$//')

          if [ -z "$SIGNATURE" ]; then
            echo "ERROR: Failed to extract signature"
            echo "Output: $SIGN_OUTPUT"
            exit 1
          fi

          echo "$SIGNATURE" > "$DIST_DIR/signature.txt"

    artifacts:
      - dist/**/*
      - build/macos/**/*.app
      - build/macos/dmg/*.dmg
    publishing:
      email:
        recipients:
          - ngocthinhdp@gmail.com
          - joan@basedhardware.com
          - nik@basedhardware.com
          - mohsin.lp710@gmail.com
          - i@m13v.com
        notify:
          success: true
          failure: false

  android-prod-patch:
    name: Patch Android to Production
    instance_type: mac_mini_m2
    max_build_duration: 120
    environment:
      android_signing:
        - prod_android_upload_keystore
      groups:
        - app_env
        - firebase
        - shorebird
      vars:
        PACKAGE_NAME: "com.friend.ios"
      flutter: 3.44.5
      xcode: 16.4
      cocoapods: 1.16.2
      java: 21
    cache:
      cache_paths:
        - $HOME/opt
        - $HOME/.pub-cache
        - $HOME/.gradle/caches
    triggering:
      events:
        - tag
      tag_patterns:
        - pattern: "v*-android-patch-cm"
          include: true
        - pattern: "v*-mobile-patch-cm"
          include: true
    working_directory: app
    scripts:
      - name: Installing Shorebird
        script: |
          # Install the Shorebird CLI
          curl --proto '=https' --tlsv1.2 https://raw.githubusercontent.com/shorebirdtech/install/main/install.sh -sSf | bash
          # Set Shorebird PATH
          echo PATH="/Users/builder/.shorebird/bin:$PATH" >> $CM_ENV

      - name: Installing Android SDK
        script: |
          export ANDROID_HOME="$HOME/opt/android-sdk"
          if [ ! -d "$ANDROID_HOME" ]; then
            echo "$ANDROID_HOME does not exist. Installing..."

            mkdir -p $ANDROID_HOME

            # Install cmdline-tools 11076708; NDK: 28.2.13676358
            cd "$HOME/opt" && \
            curl --fail --show-error --silent --connect-timeout 10.00 --max-time 120.00 \
              --output commandlinetools-mac-11076708_latest.zip \
              https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip && \
            7z -bd x commandlinetools-mac-11076708_latest.zip && \
            mkdir -p $ANDROID_HOME && \
            yes | cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} "tools" && \
            yes | cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} "cmdline-tools;latest" && \
            yes | cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} "ndk;28.2.13676358" && \
            yes | cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} --licenses  && \
            cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_HOME} --list
          fi

          if [ -d "$ANDROID_HOME" ]; then
            echo "$ANDROID_HOME does exist. Setting up env..."

            # PATH
            export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools

            # Override ENV
            echo ANDROID_HOME="$ANDROID_HOME" >> $CM_ENV
            echo ANDROID_SDK_ROOT="$ANDROID_HOME" >> $CM_ENV
            echo PATH="$PATH" >> $CM_ENV
          fi

      - name: Installing Opus
        script: |
          brew install opus
          brew install opus-tools

      - name: Set up Firebase
        script: |
          dart pub global activate flutterfire_cli

          # https://github.com/invertase/flutterfire_cli/issues/233
          echo "$FIREBASE_SERVICE_ACCOUNT_KEY" > ./firebase_key.json
          flutterfire config \
            --platforms="android,ios" \
            --out=lib/firebase_options_prod.dart \
            --ios-bundle-id=com.friend-app-with-wearable.ios12 \
            --android-package-name=com.friend.ios \
            --android-out=android/app/src/prod/  \
            --ios-out=ios/Config/Prod/ \
            --service-account="./firebase_key.json" \
            --project="based-hardware" \
            --ios-target="Runner" \
            --yes

          # DEV, should remove
          echo "$FIREBASE_SERVICE_ACCOUNT_DEV_KEY" > ./firebase_dev_key.json
          flutterfire config \
            --platforms="android,ios" \
            --out=lib/firebase_options_dev.dart \
            --ios-bundle-id=com.friend-app-with-wearable.ios12.development \
            --android-package-name=com.friend.ios.dev \
            --android-out=android/app/src/dev/  \
            --ios-out=ios/Config/Dev/ \
            --service-account="./firebase_dev_key.json" \
            --project="based-hardware-dev" \
            --ios-target="Runner" \
            --yes

      - name: Set up local.properties
        script: |
          echo "flutter.sdk=$HOME/programs/flutter" > "$(pwd)/android/local.properties"
          echo "sdk.dir=$ANDROID_HOME" >> "$(pwd)/android/local.properties"

      - name: Set up App .env
        script: |
          echo OPENAI_API_KEY=$OPENAI_API_KEY >> .env
          echo INSTABUG_API_KEY=$INSTABUG_API_KEY >> .env
          echo ONESIGNAL_APP_ID=$ONESIGNAL_APP_ID >> .env
          echo API_BASE_URL=https://api.omi.me/ >> .env
          echo GROWTHBOOK_API_KEY=$GROWTHBOOK_API_KEY >> .env
          echo GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY >> .env
          echo INTERCOM_APP_ID=$INTERCOM_APP_ID >> .env
          echo INTERCOM_ANDROID_API_KEY=$INTERCOM_ANDROID_API_KEY >> .env
          echo POSTHOG_API_KEY=$POSTHOG_API_KEY >> .env
          echo GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID >> .env
          echo GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET >> .env

      - name: Get Flutter packages
        script: |
          flutter packages pub get
      - name: Run build runner
        script: |
          dart run build_runner build --delete-conflicting-outputs
      - name: Patch Android app with Shorebird
        script: |
          # TODO: Don't tricky
          # Tricky Opus > build.gradle, force use NDK 27 to deal with "ERROR: Unknown host CPU architecture: arm64"
          # echo "Z3JvdXAgJ2V1LmVwbncub3B1c19mbHV0dGVyX2FuZHJvaWQnDQp2ZXJzaW9uICcxLjAnDQoNCmJ1aWxkc2NyaXB0IHsNCiAgICByZXBvc2l0b3JpZXMgew0KICAgICAgICBnb29nbGUoKQ0KICAgICAgICBtYXZlbkNlbnRyYWwoKQ0KICAgIH0NCg0KICAgIGRlcGVuZGVuY2llcyB7DQogICAgICAgIGNsYXNzcGF0aCAnY29tLmFuZHJvaWQudG9vbHMuYnVpbGQ6Z3JhZGxlOjcuMy4wJw0KICAgIH0NCn0NCg0Kcm9vdFByb2plY3QuYWxscHJvamVjdHMgew0KICAgIHJlcG9zaXRvcmllcyB7DQogICAgICAgIGdvb2dsZSgpDQogICAgICAgIG1hdmVuQ2VudHJhbCgpDQogICAgfQ0KfQ0KDQphcHBseSBwbHVnaW46ICdjb20uYW5kcm9pZC5saWJyYXJ5Jw0KDQphbmRyb2lkIHsNCiAgICBpZiAocHJvamVjdC5hbmRyb2lkLmhhc1Byb3BlcnR5KCJuYW1lc3BhY2UiKSkgew0KICAgICAgICBuYW1lc3BhY2UgJ2V1LmVwbncub3B1c19mbHV0dGVyX2FuZHJvaWQnDQogICAgfQ0KDQogICAgY29tcGlsZVNka1ZlcnNpb24gMzMNCg0KICAgIGNvbXBpbGVPcHRpb25zIHsNCiAgICAgICAgc291cmNlQ29tcGF0aWJpbGl0eSBKYXZhVmVyc2lvbi5WRVJTSU9OXzFfOA0KICAgICAgICB0YXJnZXRDb21wYXRpYmlsaXR5IEphdmFWZXJzaW9uLlZFUlNJT05fMV84DQogICAgfQ0KDQogICAgZGVmYXVsdENvbmZpZyB7DQogICAgICAgIG1pblNka1ZlcnNpb24gMTkNCiAgICB9DQogICAgZXh0ZXJuYWxOYXRpdmVCdWlsZCB7DQogICAgICAgIG5ka0J1aWxkew0KICAgICAgICAgICAgcGF0aCAiQW5kcm9pZC5tayINCiAgICAgICAgfQ0KICAgIH0NCg0KICAgIGRlcGVuZGVuY2llcyB7DQogICAgICAgIHRlc3RJbXBsZW1lbnRhdGlvbiAnanVuaXQ6anVuaXQ6NC4xMy4yJw0KICAgICAgICB0ZXN0SW1wbGVtZW50YXRpb24gJ29yZy5tb2NraXRvOm1vY2tpdG8tY29yZTo1LjAuMCcNCiAgICB9DQoNCiAgICB0ZXN0T3B0aW9ucyB7DQogICAgICAgIHVuaXRUZXN0cy5hbGwgew0KICAgICAgICAgICAgdGVzdExvZ2dpbmcgew0KICAgICAgICAgICAgICAgZXZlbnRzICJwYXNzZWQiLCAic2tpcHBlZCIsICJmYWlsZWQiLCAic3RhbmRhcmRPdXQiLCAic3RhbmRhcmRFcnJvciINCiAgICAgICAgICAgICAgIG91dHB1dHMudXBUb0RhdGVXaGVuIHtmYWxzZX0NCiAgICAgICAgICAgICAgIHNob3dTdGFuZGFyZFN0cmVhbXMgPSB0cnVlDQogICAgICAgICAgICB9DQogICAgICAgIH0NCiAgICB9DQogICAgbmRrVmVyc2lvbiAnMjcuMC4xMjA3Nzk3MycNCn0NCg==" | base64 -d > "$HOME/.pub-cache/hosted/pub.dev/opus_flutter_android-3.0.1/android/build.gradle"

          # Should use bump version automatically by CI
          if ! PROVENANCE_ARGS="$(scripts/build_provenance_dart_defines.sh)"; then
            echo "Build provenance validation failed; refusing to patch." >&2
            exit 1
          fi
          shorebird patch android \
            --flavor prod -- \
            $PROVENANCE_ARGS
    artifacts:
      - build/**/outputs/**/*.aab
      - build/**/outputs/**/mapping.txt
      - flutter_drive.log
    publishing:
      email:
        recipients:
          - ngocthinhdp@gmail.com
          - joan@basedhardware.com
          - nik@basedhardware.com
          - mohsin.lp710@gmail.com
          - i@m13v.com
        notify:
          success: true
          failure: false

  # ============================================
  # OMI DESKTOP SWIFT RELEASE
  # Builds, signs, notarizes, and publishes the Swift macOS app from desktop/macos/.
  # Source code lives directly in desktop/macos/ (monorepo).
  #
  # Trigger: push a tag like v0.0.10+10-macos to BasedHardware/omi
  #   git tag v0.0.10+10-macos && git push origin v0.0.10+10-macos
  #
  # Required CodeMagic secret group "desktop_secrets":
  #   SPARKLE_PRIVATE_KEY             — EdDSA private key for Sparkle auto-update signing
  #   RELEASE_SECRET                  — shared secret for Firestore release registration API
  #   OMI_DESKTOP_APP_ENV_V3          — base64-encoded .env bundled into .app at build time
  #                                     Contains only secrets (not URLs):
  #                                       FIREBASE_API_KEY=<prod firebase web api key>
  #                                       MIXPANEL_PROJECT_TOKEN=<token>
  #                                     URLs are set via workflow vars (not in this secret):
  #                                       OMI_DESKTOP_API_URL, OMI_PYTHON_API_URL
  #                                     REMOVED from V1 (now served by backend /v1/config/api-keys):
  #                                       DEEPGRAM_API_KEY, GEMINI_API_KEY, ANTHROPIC_API_KEY,
  #                                       GOOGLE_CALENDAR_API_KEY
  #   FIREBASE_PROJECT_ID, FIREBASE_API_KEY
  #   APPLE_CLIENT_ID, APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_PRIVATE_KEY
  #   RESEND_API_KEY
  #   SENTRY_AUTH_TOKEN, SENTRY_ADMIN_UID, SENTRY_WEBHOOK_SECRET
  #   REDIS_DB_HOST, REDIS_DB_PORT, REDIS_DB_PASSWORD
  #   BETA_PROMOTION_TOKEN             — narrow backend capability for candidate reservation and signed Beta promotion
  #   GITHUB_TOKEN                     — repo-scoped token used for immutable candidate-release publishing
  #   NOTE: MACOS_DEVELOPER_ID_P12/P12_PASSWORD must NOT be in this group — they live in appstore_credentials
  #
  # Cloud Run secrets (GCP Secret Manager, DESKTOP_ prefix to avoid conflicts with Python backend):
  #   DESKTOP_DEEPGRAM_API_KEY, DESKTOP_ANTHROPIC_API_KEY, DESKTOP_GOOGLE_CALENDAR_API_KEY
  #   (mapped to DEEPGRAM_API_KEY, ANTHROPIC_API_KEY, GOOGLE_CALENDAR_API_KEY inside the container)
  #
  # Re-uses from existing groups:
  #   app_env:            GEMINI_API_KEY, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
  #   appstore_credentials: APP_STORE_CONNECT_PRIVATE_KEY, APP_STORE_CONNECT_KEY_IDENTIFIER,
  #                         APP_STORE_CONNECT_ISSUER_ID (notarization),
  #                         MACOS_DEVELOPER_ID_P12 — base64 .p12 (Developer ID Application: Based Hardware INC 9536L8KLMP)
  #                         MACOS_DEVELOPER_ID_P12_PASSWORD
  # ============================================
  omi-desktop-swift-release:
    name: Release OMI Desktop (Swift)
    instance_type: mac_mini_m4
    max_build_duration: 120
    environment:
      groups:
        - app_env
        - firebase
        - appstore_credentials
        - desktop_secrets
      vars:
        BINARY_NAME: "Omi Computer"
        APP_NAME: "Omi"
        BUNDLE_ID: "com.omi.computer-macos"
        BUILD_DIR: "build"
        GITHUB_REPO: "BasedHardware/omi"
        OMI_DESKTOP_API_URL: "https://desktop-backend-hhibjajaja-uc.a.run.app/"
        OMI_PYTHON_API_URL: "https://api.omi.me"
        GCP_PROJECT: "based-hardware"
        GCP_REGION: "us-central1"
        CLOUD_RUN_SERVICE: "desktop-backend"
        BACKEND_IMAGE: "gcr.io/based-hardware/desktop-backend"
        GCS_DESKTOP_UPDATES_BUCKET: "gs://omi_macos_updates"
        DMGBUILD_VERSION: "1.6.7"
      # Desktop Swift ship/CI toolchain pin. Keep in sync with
      # desktop/macos/ci/xcode-pin.json (enforced by the
      # desktop-swift-ci-contract check); never `latest` or `edge`.
      xcode: 26.6
    # API-dispatched by desktop_auto_release.yml after its exact tag is pushed.
    # Keeping this workflow manual prevents a webhook/API race from creating
    # duplicate builds for one immutable candidate.
    working_directory: desktop/macos
    cache:
      cache_paths:
        - ~/Library/Caches/org.swift.swiftpm
        # SwiftPM build products for both release triples. Without this every
        # candidate cold-compiles the full dependency graph twice (~the largest
        # single cost in the release wall-clock); SwiftPM's own fingerprinting
        # invalidates stale objects, so reuse is safe.
        - $CM_BUILD_DIR/desktop/macos/Desktop/.build
    scripts: &desktop_signed_artifact_steps
      - name: Resolve trusted source and build identity
        script: |
          set -euo pipefail
          if [[ "${PREVIEW_MODE:-false}" == "true" ]]; then
            [[ "$PREVIEW_SLUG" =~ ^[a-z][a-z0-9-]{0,47}$ ]] || {
              echo "ERROR: PREVIEW_SLUG must be a canonical preview slug" >&2
              exit 1
            }
            [[ "$PREVIEW_SOURCE_REF" == "preview/$PREVIEW_SLUG" ]] || {
              echo "ERROR: PREVIEW_SOURCE_REF must match PREVIEW_SLUG" >&2
              exit 1
            }
            [[ "$PREVIEW_SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]] || {
              echo "ERROR: PREVIEW_SOURCE_SHA must be a full lowercase SHA" >&2
              exit 1
            }
            expected_preview_id="p$(printf '%s' "$PREVIEW_SLUG" | shasum -a 256 | cut -c1-10)"
            [[ "$PREVIEW_ID" == "$expected_preview_id" ]] || {
              echo "ERROR: PREVIEW_ID does not match the canonical slug-derived identity" >&2
              exit 1
            }
            case "$PREVIEW_BACKEND_MODE" in
              production_compatible)
                [[ "$OMI_PYTHON_API_URL" == "https://api.omi.me" ]] || {
                  echo "ERROR: production-compatible previews must use https://api.omi.me" >&2
                  exit 1
                }
                [[ "$OMI_DESKTOP_API_URL" == "https://desktop-backend-hhibjajaja-uc.a.run.app/" ]] || {
                  echo "ERROR: production-compatible previews must use the canonical desktop API" >&2
                  exit 1
                }
                preview_backend="production"
                ;;
              preview_backend)
                [[ "$OMI_PYTHON_API_URL" =~ ^https:// ]] && [[ "$OMI_DESKTOP_API_URL" =~ ^https:// ]] || {
                  echo "ERROR: preview_backend requires HTTPS API URLs" >&2
                  exit 1
                }
                preview_backend="development"
                ;;
              *)
                echo "ERROR: unsupported PREVIEW_BACKEND_MODE: $PREVIEW_BACKEND_MODE" >&2
                exit 1
                ;;
            esac

            # Codemagic always starts this workflow from main, where its configuration
            # is trusted. The approved SHA is data supplied by the protected GitHub
            # dispatcher; do not re-resolve the mutable source branch here.
            git fetch --no-tags origin "$PREVIEW_SOURCE_SHA"
            git checkout --detach "$PREVIEW_SOURCE_SHA"
            [[ "$(git rev-parse HEAD)" == "$PREVIEW_SOURCE_SHA" ]] || {
              echo "ERROR: checked out source does not match approved SHA" >&2
              exit 1
            }

            VERSION="0.0.$(printf '%d' "0x${PREVIEW_SOURCE_SHA:0:6}")"
            BUILD_NUMBER="$(git show -s --format=%ct "$PREVIEW_SOURCE_SHA")"
            APP_NAME="Omi Preview - $PREVIEW_SLUG"
            BUNDLE_ID="com.omi.preview.$PREVIEW_ID"
            URL_SCHEME="omi-preview-$PREVIEW_ID"
            DMG_PATH="$BUILD_DIR/Omi-Preview.dmg"
            SPARKLE_ZIP_PATH=""
            echo "PREVIEW_BACKEND=$preview_backend" >> "$CM_ENV"
          else
            # Tag format: v{version}+{build}-macos  e.g. v0.0.10+10-macos
            VERSION=$(echo "$CM_TAG" | sed -E 's/^v([0-9.]+)\+([0-9]+)-macos$/\1/')
            BUILD_NUMBER=$(echo "$CM_TAG" | sed -E 's/^v([0-9.]+)\+([0-9]+)-macos$/\2/')
            if [ -z "$VERSION" ] || [ "$VERSION" = "$CM_TAG" ]; then
              echo "ERROR: Could not parse version from tag: $CM_TAG"
              echo "Expected format: v{version}+{build}-macos (e.g. v0.0.10+10-macos)"
              exit 1
            fi
            URL_SCHEME="omi-computer"
            DMG_PATH="$BUILD_DIR/omi.dmg"
            SPARKLE_ZIP_PATH="$BUILD_DIR/Omi.zip"
            # Separately-installable beta identity, packaged from the same build
            # so stable and Omi Beta can run side-by-side.
            BETA_APP_NAME="Omi Beta"
            BETA_BUNDLE_ID="com.omi.computer-macos.beta"
            BETA_SPARKLE_ZIP_PATH="$BUILD_DIR/Omi.Beta.zip"
            BETA_DMG_PATH="$BUILD_DIR/omi-beta.dmg"
          fi
          echo "VERSION=$VERSION" >> $CM_ENV
          echo "BUILD_NUMBER=$BUILD_NUMBER" >> $CM_ENV
          echo "APP_NAME=$APP_NAME" >> $CM_ENV
          echo "BUNDLE_ID=$BUNDLE_ID" >> $CM_ENV
          echo "URL_SCHEME=$URL_SCHEME" >> $CM_ENV
          echo "APP_BUNDLE=$BUILD_DIR/$APP_NAME.app" >> $CM_ENV
          echo "DMG_PATH=$DMG_PATH" >> $CM_ENV
          echo "SPARKLE_ZIP_PATH=$SPARKLE_ZIP_PATH" >> $CM_ENV
          echo "BETA_APP_NAME=${BETA_APP_NAME:-}" >> $CM_ENV
          echo "BETA_BUNDLE_ID=${BETA_BUNDLE_ID:-}" >> $CM_ENV
          echo "BETA_SPARKLE_ZIP_PATH=${BETA_SPARKLE_ZIP_PATH:-}" >> $CM_ENV
          echo "BETA_DMG_PATH=${BETA_DMG_PATH:-}" >> $CM_ENV
          echo "Building OMI Desktop v$VERSION (build $BUILD_NUMBER)"

      - name: Set up keychain and import Developer ID certificate
        script: |
          keychain initialize
          echo "$MACOS_DEVELOPER_ID_P12" | base64 --decode > /tmp/developer_id.p12
          KEYCHAIN_PATH=$(keychain get-default)
          security import /tmp/developer_id.p12 \
            -k "$KEYCHAIN_PATH" \
            -P "${MACOS_DEVELOPER_ID_P12_PASSWORD:-}" \
            -T /usr/bin/codesign \
            -T /usr/bin/security
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" "$KEYCHAIN_PATH" 2>/dev/null || true
          rm /tmp/developer_id.p12
          SIGN_IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | awk -F'"' '{print $2}')
          if [ -z "$SIGN_IDENTITY" ]; then
            echo "ERROR: Developer ID Application certificate not found in keychain"
            exit 1
          fi
          echo "SIGN_IDENTITY=$SIGN_IDENTITY" >> $CM_ENV
          echo "Found signing identity: $SIGN_IDENTITY"

      - name: Install provisioning profile
        script: |
          mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
          echo "$MACOS_DEVELOPER_ID_PROFILE" | base64 --decode > /tmp/developer_id.provisionprofile
          # Copy into Desktop/ so the bundle creation step can embed it
          cp /tmp/developer_id.provisionprofile Desktop/embedded.provisionprofile
          # Install system-wide for toolchain
          security cms -D -i /tmp/developer_id.provisionprofile > /tmp/profile.plist
          PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print :UUID" /tmp/profile.plist)
          cp /tmp/developer_id.provisionprofile \
            "$HOME/Library/MobileDevice/Provisioning Profiles/$PROFILE_UUID.provisionprofile"
          echo "PROFILE_UUID=$PROFILE_UUID" >> $CM_ENV

      - name: Prepare agent runtime
        script: |
          scripts/prepare-agent-runtime.sh --universal-node

      - name: Prepare universal ffmpeg (arm64 + x86_64)
        script: |
          FFMPEG_RESOURCE="Desktop/Sources/Resources/ffmpeg"
          if file "$FFMPEG_RESOURCE" 2>/dev/null | grep -q "universal binary"; then
            echo "ffmpeg already universal, skipping download"
          else
            echo "Creating universal ffmpeg..."
            TEMP_DIR="/tmp/ffmpeg-universal-$$"
            mkdir -p "$TEMP_DIR"
            curl -L -o "$TEMP_DIR/arm64.zip" \
              "https://ffmpeg.martin-riedl.de/redirect/latest/macos/arm64/release/ffmpeg.zip"
            unzip -q -o "$TEMP_DIR/arm64.zip" -d "$TEMP_DIR/arm64/"
            curl -L -o "$TEMP_DIR/x86_64.zip" \
              "https://ffmpeg.martin-riedl.de/redirect/latest/macos/amd64/release/ffmpeg.zip"
            unzip -q -o "$TEMP_DIR/x86_64.zip" -d "$TEMP_DIR/x86_64/"
            ARM64=$(find "$TEMP_DIR/arm64" -name "ffmpeg" -type f | head -1)
            X86=$(find "$TEMP_DIR/x86_64" -name "ffmpeg" -type f | head -1)
            lipo -create "$ARM64" "$X86" -output "$FFMPEG_RESOURCE"
            chmod +x "$FFMPEG_RESOURCE"
            xattr -cr "$FFMPEG_RESOURCE"
            codesign -f -s - "$FFMPEG_RESOURCE"
            rm -rf "$TEMP_DIR"
          fi
          echo "ffmpeg: $(file "$FFMPEG_RESOURCE" | sed 's/.*: //')"

      - name: Prepare universal libwebp (arm64 + x86_64)
        script: |
          # Homebrew's libwebp is arm64-only on M2 runners, but we build a universal app.
          # We need universal libwebp.7.dylib + libsharpyuv.0.dylib at
          # /tmp/libwebp-universal/ for the later "Build Swift app" patch step.
          WEBP_VERSION="1.5.0"
          mkdir -p /tmp/libwebp-universal

          # Fast path: use the prebuilt universal dylibs vendored in the repo
          # (desktop/macos/vendor/libwebp/, built from libwebp $WEBP_VERSION source with
          # the same flags as the fallback below — see desktop/macos/vendor/libwebp/README.md).
          # This skips compiling libwebp from source twice every release run.
          VENDOR_DIR="vendor/libwebp"
          USE_VENDORED=true
          for dylib in libwebp.7.dylib libsharpyuv.0.dylib; do
            src="$VENDOR_DIR/$dylib"
            if [ ! -f "$src" ] || ! file "$src" | grep -q "universal binary"; then
              USE_VENDORED=false
              break
            fi
          done
          if [ "$USE_VENDORED" = true ]; then
            echo "Using vendored universal libwebp dylibs from $VENDOR_DIR (skipping source build)"
            cp "$VENDOR_DIR/libwebp.7.dylib" /tmp/libwebp-universal/libwebp.7.dylib
            cp "$VENDOR_DIR/libsharpyuv.0.dylib" /tmp/libwebp-universal/libsharpyuv.0.dylib
            for lib in /tmp/libwebp-universal/*.dylib; do
              echo "$(basename "$lib"): $(file "$lib" | sed 's/.*: //')"
              file "$lib" | grep -q "universal binary" || {
                echo "ERROR: vendored $(basename "$lib") is not a universal binary"
                exit 1
              }
            done
            exit 0
          fi

          echo "Vendored libwebp dylibs missing/invalid — building from source"
          # Build each arch separately (multi-arch cmake fails with _Float16 on x86_64),
          # then lipo them into universal dylibs.
          command -v cmake >/dev/null 2>&1 || brew install cmake
          TEMP_DIR="/tmp/libwebp-build-$$"
          # -mmacosx-version-min pins LC_BUILD_VERSION minos to 13.0 even on
          # toolchains where CMAKE_OSX_DEPLOYMENT_TARGET alone doesn't stick
          # (newer SDKs otherwise stamp the SDK version, which would refuse to
          # load on older macOS). Keep in sync with desktop/macos/vendor/libwebp/README.md.
          export MACOSX_DEPLOYMENT_TARGET=13.0
          CMAKE_COMMON="-DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release \
            -DCMAKE_OSX_DEPLOYMENT_TARGET=13.0 \
            -DCMAKE_C_FLAGS=-mmacosx-version-min=13.0 \
            -DCMAKE_SHARED_LINKER_FLAGS=-mmacosx-version-min=13.0 \
            -DWEBP_BUILD_EXTRAS=OFF -DWEBP_BUILD_ANIM_UTILS=OFF \
            -DWEBP_BUILD_CWEBP=OFF -DWEBP_BUILD_DWEBP=OFF \
            -DWEBP_BUILD_GIF2WEBP=OFF -DWEBP_BUILD_IMG2WEBP=OFF \
            -DWEBP_BUILD_VWEBP=OFF -DWEBP_BUILD_WEBPINFO=OFF \
            -DWEBP_BUILD_WEBPMUX=OFF"
          mkdir -p "$TEMP_DIR"
          curl -sL "https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-$WEBP_VERSION.tar.gz" \
            | tar xz -C "$TEMP_DIR"
          SRC="$TEMP_DIR/libwebp-$WEBP_VERSION"

          # Build arm64
          mkdir "$SRC/build-arm64" && cd "$SRC/build-arm64"
          cmake .. -DCMAKE_OSX_ARCHITECTURES=arm64 $CMAKE_COMMON
          make -j$(sysctl -n hw.ncpu)

          # Build x86_64
          mkdir "$SRC/build-x86_64" && cd "$SRC/build-x86_64"
          cmake .. -DCMAKE_OSX_ARCHITECTURES=x86_64 $CMAKE_COMMON
          make -j$(sysctl -n hw.ncpu)

          # Lipo into universal dylibs
          mkdir -p /tmp/libwebp-universal
          ARM64_WEBP=$(find "$SRC/build-arm64" -name "libwebp.7.*.dylib" -not -type l | head -1)
          X86_WEBP=$(find "$SRC/build-x86_64" -name "libwebp.7.*.dylib" -not -type l | head -1)
          lipo -create "$ARM64_WEBP" "$X86_WEBP" -output /tmp/libwebp-universal/libwebp.7.dylib

          ARM64_SY=$(find "$SRC/build-arm64" -name "libsharpyuv.0.*.dylib" -not -type l | head -1)
          X86_SY=$(find "$SRC/build-x86_64" -name "libsharpyuv.0.*.dylib" -not -type l | head -1)
          lipo -create "$ARM64_SY" "$X86_SY" -output /tmp/libwebp-universal/libsharpyuv.0.dylib

          # Verify both architectures are present
          for lib in /tmp/libwebp-universal/*.dylib; do
            echo "$(basename "$lib"): $(file "$lib" | sed 's/.*: //')"
            file "$lib" | grep -q "universal binary" || {
              echo "ERROR: $(basename "$lib") is not a universal binary"
              exit 1
            }
          done

          rm -rf "$TEMP_DIR"

      - name: Resolve SPM packages
        script: |
          # Unset TOOLCHAINS to use Xcode's default toolchain
          unset TOOLCHAINS
          # brew install is only for pkg-config/headers. Keep it off the
          # compile step; overlap it with SPM resolve. The earlier
          # "Prepare universal libwebp" step already built universal dylibs.
          echo "Installing webp headers/pkg-config in parallel with SPM resolve..."
          brew install webp &
          brew_pid=$!
          echo "Resolving SPM packages (downloads ~673MB of binary artifacts on first run)..."
          resolve_ok=0
          for attempt in 1 2 3; do
            if xcrun swift package resolve --package-path Desktop; then
              echo "  Packages resolved on attempt $attempt"
              echo "  Artifacts: $(ls Desktop/.build/artifacts/ 2>/dev/null | wc -l | tr -d ' ') cached items"
              resolve_ok=1
              break
            fi
            echo "  Resolution failed on attempt $attempt"
            if [ $attempt -lt 3 ]; then
              echo "  Retrying in 15 seconds..."
              sleep 15
            fi
          done
          if [[ "$resolve_ok" -ne 1 ]]; then
            echo "ERROR: Package resolution failed after 3 attempts"
            if ! wait "$brew_pid"; then
              echo "ERROR: brew install webp also failed"
            fi
            exit 1
          fi
          if ! wait "$brew_pid"; then
            echo "ERROR: brew install webp failed"
            exit 1
          fi

          # Homebrew's libwebp on Codemagic's Mac mini is arm64-only, which
          # breaks Swift's x86_64 cross-compile link step. Overwrite those
          # copies with the universal dylibs prepared earlier so pkg-config
          # (used by Package.swift's CWebP system-lib target) is linker-safe.
          WEBP_LIB_DIR="$(brew --prefix webp)/lib"
          for dylib in libwebp.7.dylib libsharpyuv.0.dylib; do
            src="/tmp/libwebp-universal/$dylib"
            dst="$WEBP_LIB_DIR/$dylib"
            if [ ! -f "$src" ]; then
              echo "ERROR: universal $dylib missing at $src"
              exit 1
            fi
            if [ ! -e "$dst" ]; then
              echo "ERROR: Homebrew $dylib not at $dst"
              exit 1
            fi
            rm -f "$dst"
            cp "$src" "$dst"
            file "$dst" | grep -q "universal binary" \
              && echo "Patched $dst to universal" \
              || { echo "ERROR: $dst is not universal after patch"; exit 1; }
          done

      - name: Build Swift app (arm64 + x86_64)
        script: |
          mkdir -p "$BUILD_DIR"
          # Unset TOOLCHAINS to use Xcode's default toolchain (avoids Swift version conflicts)
          unset TOOLCHAINS

          # Build arm64. Use --triple (not --arch) so binary always lands in
          # arm64-apple-macosx/release/ regardless of the machine's native arch.
          echo "Building arm64..."
          xcrun swift build -c release --package-path Desktop --triple arm64-apple-macosx

          ARM64_PATH="Desktop/.build/arm64-apple-macosx/release/$BINARY_NAME"
          if [ ! -f "$ARM64_PATH" ]; then
            echo "ERROR: arm64 binary not found at $ARM64_PATH"
            ls "Desktop/.build/" 2>/dev/null
            ls "Desktop/.build/arm64-apple-macosx/release/" 2>/dev/null | head -20 || true
            exit 1
          fi
          cp "$ARM64_PATH" "/tmp/OmiComputer-arm64"
          echo "arm64 binary: $(file "/tmp/OmiComputer-arm64" | grep -oE 'arm64|x86_64')"

          # Build x86_64 (cross-compile). Use --triple for explicit arch directory.
          echo "Building x86_64..."
          xcrun swift build -c release --package-path Desktop --triple x86_64-apple-macosx

          X86_64_PATH="Desktop/.build/x86_64-apple-macosx/release/$BINARY_NAME"
          # Symmetric existence check — without this, a silent x86_64 build failure
          # (exit 0 but no output) bubbles up later as "missing binary" in the
          # universal-bundle step with no traceback. Burned a build in May 2026.
          if [ ! -f "$X86_64_PATH" ]; then
            echo "ERROR: x86_64 binary not found at $X86_64_PATH"
            echo "Listing $(dirname "$X86_64_PATH"):"
            ls -la "$(dirname "$X86_64_PATH")" 2>&1 | head -30 || true
            echo "Listing Desktop/.build/:"
            ls -la "Desktop/.build/" 2>&1 || true
            exit 1
          fi
          echo "x86_64 binary: $(file "$X86_64_PATH" | grep -oE 'arm64|x86_64' | head -1)"

      - name: Create universal app bundle
        script: |
          # arm64 binary was saved to /tmp in the Build Swift step
          ARM64_BINARY="/tmp/OmiComputer-arm64"
          # x86_64 binary is in the explicit arch-specific dir
          X86_64_BINARY="Desktop/.build/x86_64-apple-macosx/release/$BINARY_NAME"

          if [ ! -f "$ARM64_BINARY" ] || [ ! -f "$X86_64_BINARY" ]; then
            echo "ERROR: Missing built binaries"
            echo "  arm64 ($ARM64_BINARY): $([ -f "$ARM64_BINARY" ] && echo EXISTS || echo MISSING)"
            echo "  x86_64 ($X86_64_BINARY): $([ -f "$X86_64_BINARY" ] && echo EXISTS || echo MISSING)"
            echo ".build/ contents:" && ls "Desktop/.build/" 2>/dev/null
            ls "Desktop/.build/arm64-apple-macosx/release/" 2>/dev/null | grep -E "^Omi" | head -5 || true
            ls "Desktop/.build/x86_64-apple-macosx/release/" 2>/dev/null | grep -E "^Omi" | head -5 || true
            exit 1
          fi
          echo "arm64 arch: $(file "$ARM64_BINARY" | grep -oE 'arm64|x86_64')"
          echo "x86_64 arch: $(file "$X86_64_BINARY" | grep -oE 'arm64|x86_64' | head -1)"

          mkdir -p "$APP_BUNDLE/Contents/MacOS"
          mkdir -p "$APP_BUNDLE/Contents/Resources"
          mkdir -p "$APP_BUNDLE/Contents/Frameworks"

          # Merge architectures into universal binary
          lipo -create "$ARM64_BINARY" "$X86_64_BINARY" -output "$APP_BUNDLE/Contents/MacOS/$BINARY_NAME"
          file "$APP_BUNDLE/Contents/MacOS/$BINARY_NAME" | grep -q "universal binary" || {
            echo "ERROR: lipo failed to create universal binary"
            exit 1
          }

          cp Desktop/Info.plist "$APP_BUNDLE/Contents/Info.plist"

          # Sparkle: use x86_64 arch path; fallback to arm64
          SPARKLE_FW="Desktop/.build/x86_64-apple-macosx/release/Sparkle.framework"
          if [ ! -d "$SPARKLE_FW" ]; then
            SPARKLE_FW="Desktop/.build/arm64-apple-macosx/release/Sparkle.framework"
          fi
          if [ ! -d "$SPARKLE_FW" ]; then
            echo "ERROR: Sparkle.framework not found in either arch dir"
            exit 1
          fi
          cp -R "$SPARKLE_FW" "$APP_BUNDLE/Contents/Frameworks/"

          # Add rpath so Sparkle can be found at runtime
          install_name_tool -add_rpath "@executable_path/../Frameworks" \
            "$APP_BUNDLE/Contents/MacOS/$BINARY_NAME"

          # Bundle universal libwebp dylibs (built from source in earlier step)
          BREW_WEBP_LIB="$(pkg-config --variable=libdir libwebp 2>/dev/null)/libwebp.7.dylib"
          if [ -f "/tmp/libwebp-universal/libwebp.7.dylib" ]; then
            cp /tmp/libwebp-universal/libwebp.7.dylib "$APP_BUNDLE/Contents/Frameworks/libwebp.7.dylib"
            [ -f /tmp/libwebp-universal/libsharpyuv.0.dylib ] && \
              cp /tmp/libwebp-universal/libsharpyuv.0.dylib "$APP_BUNDLE/Contents/Frameworks/libsharpyuv.0.dylib"
            install_name_tool -id "@rpath/libwebp.7.dylib" "$APP_BUNDLE/Contents/Frameworks/libwebp.7.dylib"
            [ -f "$APP_BUNDLE/Contents/Frameworks/libsharpyuv.0.dylib" ] && \
              install_name_tool -id "@rpath/libsharpyuv.0.dylib" "$APP_BUNDLE/Contents/Frameworks/libsharpyuv.0.dylib"
            # Fix libwebp's reference to libsharpyuv (source build uses local path)
            SHARPYUV_REF=$(otool -L "$APP_BUNDLE/Contents/Frameworks/libwebp.7.dylib" | grep libsharpyuv | awk '{print $1}')
            if [ -n "$SHARPYUV_REF" ] && [ "$SHARPYUV_REF" != "@rpath/libsharpyuv.0.dylib" ]; then
              install_name_tool -change "$SHARPYUV_REF" "@rpath/libsharpyuv.0.dylib" \
                "$APP_BUNDLE/Contents/Frameworks/libwebp.7.dylib"
            fi
            # Fix main binary's reference to libwebp (linked against Homebrew path)
            install_name_tool -change "$BREW_WEBP_LIB" "@rpath/libwebp.7.dylib" "$APP_BUNDLE/Contents/MacOS/$BINARY_NAME"
            echo "Bundled universal libwebp: $(file "$APP_BUNDLE/Contents/Frameworks/libwebp.7.dylib" | grep -oE 'arm64|x86_64' | tr '\n' '+' | sed 's/+$//')"
          else
            echo "ERROR: universal libwebp not found at /tmp/libwebp-universal/"
            echo "Ensure 'Prepare universal libwebp' step ran successfully"
            exit 1
          fi

          # Copy app icon
          [ -f "omi_icon.icns" ] && cp omi_icon.icns "$APP_BUNDLE/Contents/Resources/OmiIcon.icns"

          # Copy Firebase config
          cp Desktop/Sources/GoogleService-Info.plist "$APP_BUNDLE/Contents/Resources/"

          # Copy SPM resource bundle (app assets: permissions.gif, herologo.png, etc.)
          # Use x86_64 arch path; fallback to arm64
          SWIFT_BUILD_DIR="Desktop/.build/x86_64-apple-macosx/release"
          if [ ! -d "$SWIFT_BUILD_DIR" ]; then
            SWIFT_BUILD_DIR="Desktop/.build/arm64-apple-macosx/release"
          fi
          RESOURCE_BUNDLE="$SWIFT_BUILD_DIR/Omi Computer_Omi Computer.bundle"
          if [ -d "$RESOURCE_BUNDLE" ]; then
            cp -R "$RESOURCE_BUNDLE" "$APP_BUNDLE/Contents/Resources/"
            # SwiftPM emits Resources/node at the resource-bundle root. Keep
            # Codemagic aligned with run.sh and the signed-artifact contract by
            # moving it to the app-style nested resource path before signing.
            source scripts/launcher-bootstrap.sh
            omi_normalize_packaged_resource_bundle \
              "$APP_BUNDLE/Contents/Resources/$(basename "$RESOURCE_BUNDLE")"
          else
            echo "WARNING: SPM resource bundle not found (non-fatal)"
          fi

          # Copy agent (Claude Code integration)
          if [ -d "agent/dist" ]; then
            mkdir -p "$APP_BUNDLE/Contents/Resources/agent"
            cp -Rf agent/dist        "$APP_BUNDLE/Contents/Resources/agent/"
            cp -f  agent/package.json "$APP_BUNDLE/Contents/Resources/agent/"
            if [ ! -d ".harness/agent-runtime/agent-node_modules" ]; then
              echo "ERROR: packaged agent dependencies missing"
              exit 1
            fi
            cp -Rf .harness/agent-runtime/agent-node_modules "$APP_BUNDLE/Contents/Resources/agent/node_modules"
          else
            echo "ERROR: agent/dist not found"
            exit 1
          fi

          # Copy pi-mono-extension (default harness — registers the Omi provider
          # with pi and routes requests to the Rust desktop-backend /v2/chat/completions
          # proxy). Without this, piMono mode cannot start and the app is broken.
          if [ -d "pi-mono-extension" ]; then
            mkdir -p "$APP_BUNDLE/Contents/Resources/pi-mono-extension"
            cp -f pi-mono-extension/index.ts    "$APP_BUNDLE/Contents/Resources/pi-mono-extension/"
            cp -f pi-mono-extension/package.json "$APP_BUNDLE/Contents/Resources/pi-mono-extension/"
            cp -f pi-mono-extension/package-lock.json "$APP_BUNDLE/Contents/Resources/pi-mono-extension/"
            if [ ! -d ".harness/agent-runtime/pi-mono-extension-node_modules" ]; then
              echo "ERROR: packaged pi-mono-extension dependencies missing"
              exit 1
            fi
            cp -Rf .harness/agent-runtime/pi-mono-extension-node_modules "$APP_BUNDLE/Contents/Resources/pi-mono-extension/node_modules"
          else
            echo "ERROR: pi-mono-extension directory not found"
            exit 1
          fi

          # Update Info.plist metadata
          /usr/libexec/PlistBuddy -c "Set :CFBundleExecutable   $BINARY_NAME"  "$APP_BUNDLE/Contents/Info.plist"
          /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier   $BUNDLE_ID"    "$APP_BUNDLE/Contents/Info.plist"
          /usr/libexec/PlistBuddy -c "Set :CFBundleName         $APP_NAME"     "$APP_BUNDLE/Contents/Info.plist"
          /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName  $APP_NAME"     "$APP_BUNDLE/Contents/Info.plist"
          /usr/libexec/PlistBuddy -c "Set :CFBundleURLTypes:0:CFBundleURLSchemes:0 $URL_SCHEME" "$APP_BUNDLE/Contents/Info.plist"
          /usr/libexec/PlistBuddy -c "Set :LSMinimumSystemVersion 14.0"        "$APP_BUNDLE/Contents/Info.plist"
          /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" \
            "$APP_BUNDLE/Contents/Info.plist" 2>/dev/null || \
          /usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string $VERSION" \
            "$APP_BUNDLE/Contents/Info.plist"
          /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" \
            "$APP_BUNDLE/Contents/Info.plist" 2>/dev/null || \
          /usr/libexec/PlistBuddy -c "Add :CFBundleVersion string $BUILD_NUMBER" \
            "$APP_BUNDLE/Contents/Info.plist"

          if [[ "${PREVIEW_MODE:-false}" == "true" ]]; then
            # The runtime also treats the reserved bundle-ID prefix as the safety
            # boundary. These explicit fields make a correctly packaged preview
            # auditable and keep it isolated from the shared Sparkle feed.
            /usr/libexec/PlistBuddy -c "Set :OMIExternalPreview true" "$APP_BUNDLE/Contents/Info.plist"
            /usr/libexec/PlistBuddy -c "Set :OMIExternalPreviewBackend $PREVIEW_BACKEND" "$APP_BUNDLE/Contents/Info.plist"
            /usr/libexec/PlistBuddy -c "Delete :SUFeedURL" "$APP_BUNDLE/Contents/Info.plist" 2>/dev/null || true
            /usr/libexec/PlistBuddy -c "Set :SUEnableAutomaticChecks false" "$APP_BUNDLE/Contents/Info.plist"
            /usr/libexec/PlistBuddy -c "Set :SUAutomaticallyUpdate false" "$APP_BUNDLE/Contents/Info.plist"
          fi

          printf "APPL????" > "$APP_BUNDLE/Contents/PkgInfo"

          # Embed provisioning profile
          [ -f "Desktop/embedded.provisionprofile" ] && \
            cp Desktop/embedded.provisionprofile "$APP_BUNDLE/Contents/embedded.provisionprofile"

          # Write runtime .env from OMI_DESKTOP_APP_ENV_V3 secret (base64-encoded, secrets only)
          # V3 contains only: FIREBASE_API_KEY, MIXPANEL_PROJECT_TOKEN
          # URLs are appended from workflow vars below
          # API keys (Deepgram, Gemini, Anthropic, Google Calendar) are fetched at
          # runtime from backend /v1/config/api-keys via APIKeyService
          echo "$OMI_DESKTOP_APP_ENV_V3" | base64 --decode > "$APP_BUNDLE/Contents/Resources/.env"
          # Append backend URLs from workflow vars
          echo "OMI_DESKTOP_API_URL=$OMI_DESKTOP_API_URL" >> "$APP_BUNDLE/Contents/Resources/.env"
          echo "OMI_PYTHON_API_URL=$OMI_PYTHON_API_URL" >> "$APP_BUNDLE/Contents/Resources/.env"
          echo "App bundle created: $APP_BUNDLE"

          # Generate symbols from the final universal executable after lipo and
          # install_name_tool mutations. The helper fails closed unless the dSYM
          # UUID set exactly matches both executable slices.
          DSYM_PATH="$BUILD_DIR/Omi Computer.app.dSYM"
          DSYM_ARCHIVE="$BUILD_DIR/Omi.dSYM.zip"
          scripts/publish-desktop-debug-symbols.sh generate \
            --binary "$APP_BUNDLE/Contents/MacOS/$BINARY_NAME" \
            --dsym "$DSYM_PATH" \
            --archive "$DSYM_ARCHIVE"
          echo "DSYM_PATH=$DSYM_PATH" >> "$CM_ENV"
          echo "DSYM_ARCHIVE=$DSYM_ARCHIVE" >> "$CM_ENV"

      - name: Sign app
        script: |
          scripts/prepare-desktop-bundle-native-deps.sh "$APP_BUNDLE"

          xattr -cr "$APP_BUNDLE"

          # Sign ffmpeg (inside SPM resource bundle)
          FFMPEG_PATH="$APP_BUNDLE/Contents/Resources/Omi Computer_Omi Computer.bundle/ffmpeg"
          if [ -f "$FFMPEG_PATH" ]; then
            codesign --force --options runtime --timestamp \
              --sign "$SIGN_IDENTITY" "$FFMPEG_PATH"
          fi

          # Sign node with JIT entitlements (V8 requires MAP_JIT under Hardened Runtime)
          NODE_BUNDLE_PATH="$APP_BUNDLE/Contents/Resources/Omi Computer_Omi Computer.bundle/Contents/Resources/node"
          if [ -f "$NODE_BUNDLE_PATH" ]; then
            codesign --force --options runtime --timestamp \
              --sign "$SIGN_IDENTITY" \
              --entitlements Desktop/Node.entitlements \
              "$NODE_BUNDLE_PATH"
          fi

          # Sign agent native binaries
          AGENT_BUNDLE="$APP_BUNDLE/Contents/Resources/agent"
          if [ -d "$AGENT_BUNDLE/node_modules" ]; then
            # Remove JetBrains plugin — its JARs contain .jnilib files Apple rejects during notarization
            rm -rf "$AGENT_BUNDLE/node_modules/@anthropic-ai/claude-code/vendor/claude-code-jetbrains-plugin" \
              2>/dev/null || true

            # Primary pass: sign known native binary extensions
            find "$AGENT_BUNDLE/node_modules" -type f \
              \( -name "*.node" -o -name "*.dylib" -o -name "*.jnilib" -o -name "*.so" -o -name "rg" \) \
              2>/dev/null | while read native_bin; do
              file "$native_bin" 2>/dev/null | grep -q "Mach-O" && \
                codesign --force --options runtime --timestamp \
                  --sign "$SIGN_IDENTITY" "$native_bin"
            done

            # Catch-all pass: any remaining unsigned Mach-O binaries
            find "$AGENT_BUNDLE/node_modules" -type f \
              ! -name "*.js"   ! -name "*.json" ! -name "*.ts"   ! -name "*.map" \
              ! -name "*.md"   ! -name "*.txt"  ! -name "*.yml"  ! -name "*.yaml" \
              ! -name "*.css"  ! -name "*.html" ! -name "*.jar"  ! -name "*.d.ts" \
              ! -name "*.node" ! -name "*.dylib" ! -name "*.jnilib" ! -name "*.so" ! -name "rg" \
              2>/dev/null | while read candidate; do
              file "$candidate" 2>/dev/null | grep -q "Mach-O" && \
                codesign --force --options runtime --timestamp \
                  --sign "$SIGN_IDENTITY" "$candidate"
            done
          fi

          # Sign pi-mono-extension native binaries, if any dependency adds Mach-O payloads.
          PI_MONO_EXTENSION_BUNDLE="$APP_BUNDLE/Contents/Resources/pi-mono-extension"
          if [ -d "$PI_MONO_EXTENSION_BUNDLE/node_modules" ]; then
            # Primary pass: sign known native binary extensions
            find "$PI_MONO_EXTENSION_BUNDLE/node_modules" -type f \
              \( -name "*.node" -o -name "*.dylib" -o -name "*.jnilib" -o -name "*.so" -o -name "rg" \) \
              2>/dev/null | while read native_bin; do
              file "$native_bin" 2>/dev/null | grep -q "Mach-O" && \
                codesign --force --options runtime --timestamp \
                  --sign "$SIGN_IDENTITY" "$native_bin"
            done

            # Catch-all pass: any remaining unsigned Mach-O binaries
            find "$PI_MONO_EXTENSION_BUNDLE/node_modules" -type f \
              ! -name "*.js"   ! -name "*.json" ! -name "*.ts"   ! -name "*.map" \
              ! -name "*.md"   ! -name "*.txt"  ! -name "*.yml"  ! -name "*.yaml" \
              ! -name "*.css"  ! -name "*.html" ! -name "*.jar"  ! -name "*.d.ts" \
              ! -name "*.node" ! -name "*.dylib" ! -name "*.jnilib" ! -name "*.so" ! -name "rg" \
              2>/dev/null | while read candidate; do
              file "$candidate" 2>/dev/null | grep -q "Mach-O" && \
                codesign --force --options runtime --timestamp \
                  --sign "$SIGN_IDENTITY" "$candidate"
            done
          fi

          # Sign Sparkle components — innermost first (required order)
          SPARKLE_FW="$APP_BUNDLE/Contents/Frameworks/Sparkle.framework"
          codesign --force --options runtime --timestamp --sign "$SIGN_IDENTITY" \
            "$SPARKLE_FW/Versions/B/XPCServices/Downloader.xpc"
          codesign --force --options runtime --timestamp --sign "$SIGN_IDENTITY" \
            "$SPARKLE_FW/Versions/B/XPCServices/Installer.xpc"
          codesign --force --options runtime --timestamp --sign "$SIGN_IDENTITY" \
            "$SPARKLE_FW/Versions/B/Autoupdate"
          codesign --force --options runtime --timestamp --sign "$SIGN_IDENTITY" \
            "$SPARKLE_FW/Versions/B/Updater.app"
          codesign --force --options runtime --timestamp --sign "$SIGN_IDENTITY" \
            "$SPARKLE_FW"

          # Sign libwebp dylibs (must be before main app bundle)
          if [ -f "$APP_BUNDLE/Contents/Frameworks/libsharpyuv.0.dylib" ]; then
            codesign --force --options runtime --timestamp --sign "$SIGN_IDENTITY" "$APP_BUNDLE/Contents/Frameworks/libsharpyuv.0.dylib"
          fi
          if [ -f "$APP_BUNDLE/Contents/Frameworks/libwebp.7.dylib" ]; then
            codesign --force --options runtime --timestamp --sign "$SIGN_IDENTITY" "$APP_BUNDLE/Contents/Frameworks/libwebp.7.dylib"
          fi

          # Sign the main app bundle with release entitlements
          codesign --force --options runtime --timestamp \
            --sign "$SIGN_IDENTITY" \
            --entitlements Desktop/Omi-Release.entitlements \
            "$APP_BUNDLE"

          codesign --verify --verbose=2 "$APP_BUNDLE" 2>&1 | head -3
          echo "App signed successfully"

      - name: Audit app bundle dependencies
        script: |
          scripts/audit-desktop-bundle-deps.sh "$APP_BUNDLE"

      - name: Prepare Omi Beta identity
        script: |
          if [[ "${PREVIEW_MODE:-false}" == "true" ]]; then
            echo "External previews do not ship an Omi Beta variant."
            exit 0
          fi
          set -euo pipefail
          scripts/create-omi-beta-variant.sh \
            --phase prepare \
            --source-app "$APP_BUNDLE" \
            --build-dir "$BUILD_DIR" \
            --beta-app-name "$BETA_APP_NAME" \
            --beta-bundle-id "$BETA_BUNDLE_ID"

      - name: Notarize stable and Beta apps concurrently
        script: |
          set -euo pipefail
          NOTARIZE_ARGS=(
            --kind app
            --work-dir "$BUILD_DIR/notarization"
            --artifact stable "$APP_BUNDLE"
          )
          if [[ "${PREVIEW_MODE:-false}" != "true" ]]; then
            NOTARIZE_ARGS+=(--artifact beta "$BUILD_DIR/$BETA_APP_NAME.app")
          fi
          scripts/notarize-desktop-artifacts.sh "${NOTARIZE_ARGS[@]}"

      - name: Create stable and Beta DMGs concurrently
        script: |
          set -euo pipefail
          pip3 install --break-system-packages "dmgbuild==$DMGBUILD_VERSION"
          DMG_ARGS=(
            --work-dir "$BUILD_DIR/dmg-packaging"
            --dmg stable "$APP_BUNDLE" "$APP_NAME" "$DMG_PATH"
          )
          if [[ "${PREVIEW_MODE:-false}" != "true" ]]; then
            DMG_ARGS+=(--dmg beta "$BUILD_DIR/$BETA_APP_NAME.app" "$BETA_APP_NAME" "$BETA_DMG_PATH")
          fi
          scripts/create-desktop-dmgs.sh "${DMG_ARGS[@]}"

      - name: Notarize stable and Beta DMGs concurrently
        script: |
          set -euo pipefail
          NOTARIZE_ARGS=(
            --kind dmg
            --work-dir "$BUILD_DIR/notarization"
            --artifact stable "$DMG_PATH"
          )
          if [[ "${PREVIEW_MODE:-false}" != "true" ]]; then
            NOTARIZE_ARGS+=(--artifact beta "$BETA_DMG_PATH")
          fi
          scripts/notarize-desktop-artifacts.sh "${NOTARIZE_ARGS[@]}"

      - name: Create stable and Beta Sparkle archives
        script: |
          if [[ "${PREVIEW_MODE:-false}" == "true" ]]; then
            echo "External previews do not participate in the shared Sparkle feed."
            exit 0
          fi
          set -euo pipefail
          # Sparkle expects a ZIP of .app — not a DMG.
          ditto -c -k --keepParent "$APP_BUNDLE" "$SPARKLE_ZIP_PATH"

          SPARKLE_BIN="Desktop/.build/artifacts/sparkle/Sparkle/bin"
          ED_SIGNATURE=""
          if [[ -f "$SPARKLE_BIN/sign_update" && -n "$SPARKLE_PRIVATE_KEY" ]]; then
            ED_SIGNATURE=$(echo "$SPARKLE_PRIVATE_KEY" | \
              "$SPARKLE_BIN/sign_update" "$SPARKLE_ZIP_PATH" --ed-key-file - 2>/dev/null | \
              grep "sparkle:edSignature" | \
              sed 's/.*edSignature="\([^"]*\)".*/\1/')
          fi
          if [ -z "$ED_SIGNATURE" ]; then
            echo "ERROR: Could not generate EdDSA signature — Sparkle auto-update will not work" >&2
            exit 1
          fi
          echo "ED_SIGNATURE=$ED_SIGNATURE" >> "$CM_ENV"

          scripts/create-omi-beta-variant.sh \
            --phase sparkle \
            --build-dir "$BUILD_DIR" \
            --beta-app-name "$BETA_APP_NAME" \
            --sparkle-zip-out "$BETA_SPARKLE_ZIP_PATH" \
            --cm-env "$CM_ENV"

      - name: Smoke signed desktop artifact
        script: |
          if [[ "${PREVIEW_MODE:-false}" == "true" ]]; then
            scripts/smoke-signed-desktop-artifact.sh \
              --app "$APP_BUNDLE" \
              --dmg "$DMG_PATH" \
              --expected-channel preview \
              --expected-bundle-id "$BUNDLE_ID" \
              --expected-url-scheme "$URL_SCHEME" \
              --expected-python-api-url "$OMI_PYTHON_API_URL" \
              --expected-desktop-api-url "$OMI_DESKTOP_API_URL" \
              --preview \
              --result-json "$BUILD_DIR/desktop-smoke-result.json"
          else
            scripts/smoke-signed-desktop-artifact.sh \
              --app "$APP_BUNDLE" \
              --zip "$SPARKLE_ZIP_PATH" \
              --dmg "$DMG_PATH" \
              --tag "$CM_TAG" \
              --source-sha "$(git rev-parse HEAD)" \
              --expected-channel beta \
              --result-json "$BUILD_DIR/desktop-smoke-result.json"
          fi

      # Keep the installable Beta identity in a distinct provider step. A failed
      # build must reveal which signed identity failed even when the operator can
      # see only the mirrored GitHub check summary, not private Codemagic logs.
      - name: Smoke signed desktop beta artifact
        script: |
          if [[ "${PREVIEW_MODE:-false}" == "true" ]]; then
            echo "External previews do not produce the Omi Beta identity."
            exit 0
          fi
          set -euo pipefail
          OMI_SIGNED_ARTIFACT_SMOKE_ALLOW_PRODUCTION_LAUNCH=1 \
          scripts/smoke-signed-desktop-artifact.sh \
            --app "$BUILD_DIR/$BETA_APP_NAME.app" \
            --zip "$BETA_SPARKLE_ZIP_PATH" \
            --dmg "$BETA_DMG_PATH" \
            --tag "$CM_TAG" \
            --source-sha "$(git rev-parse HEAD)" \
            --expected-channel beta \
            --expected-bundle-id "$BETA_BUNDLE_ID" \
            --expected-feed-url "https://api.omi.me/v2/desktop/appcast.xml?identity=beta" \
            --expected-python-api-url "https://api.omiapi.com/" \
            --expected-desktop-api-url "https://desktop-backend-dt5lrfkkoa-uc.a.run.app/" \
            --launch \
            --auth-storage-canary \
            --notification-callback-canary \
            --timeout 90 \
            --result-json "$BUILD_DIR/desktop-smoke-result-beta.json"

      - name: Publish immutable external preview
        script: |
          if [[ "${PREVIEW_MODE:-false}" != "true" ]]; then
            echo "Normal releases publish through the beta/stable signed-smoke lane."
            exit 0
          fi
          set -euo pipefail
          test -n "${GCP_DESKTOP_PREVIEW_SERVICE_ACCOUNT:-}" || {
            echo "ERROR: missing preview-only GCS service account" >&2
            exit 1
          }
          test -n "${DESKTOP_PREVIEW_PUBLISH_KEY:-}" || {
            echo "ERROR: missing preview-only registry credential" >&2
            exit 1
          }
          test "${DESKTOP_PREVIEW_REGISTRY_URL:-}" = "https://api.omi.me" || {
            echo "ERROR: preview registry must use the canonical production control plane" >&2
            exit 1
          }
          command -v gcloud >/dev/null || { echo "ERROR: gcloud is required" >&2; exit 1; }
          command -v jq >/dev/null || { echo "ERROR: jq is required" >&2; exit 1; }

          key_path="$(mktemp /tmp/omi-preview-gcs.XXXXXX.json)"
          trap 'rm -f "$key_path"' EXIT
          printf '%s' "$GCP_DESKTOP_PREVIEW_SERVICE_ACCOUNT" | base64 --decode > "$key_path"
          gcloud auth activate-service-account --key-file="$key_path" >/dev/null

          artifact_path="${GCS_DESKTOP_UPDATES_BUCKET}/previews/${PREVIEW_SLUG}/${PREVIEW_SOURCE_SHA}/Omi-Preview.dmg"
          dmg_sha256="$(shasum -a 256 "$DMG_PATH" | awk '{print $1}')"
          existing_path="$(mktemp /tmp/omi-preview-existing.XXXXXX.dmg)"
          trap 'rm -f "$key_path" "$existing_path"' EXIT
          # Read one exact object (no bucket listing) so the GCS identity can
          # remain limited to previews/** object get/create operations.
          if gcloud storage cp "$artifact_path" "$existing_path" >/dev/null 2>&1; then
            existing_sha256="$(shasum -a 256 "$existing_path" | awk '{print $1}')"
            [[ "$existing_sha256" == "$dmg_sha256" ]] || {
              echo "ERROR: immutable preview object already exists with a different digest" >&2
              exit 1
            }
            echo "Immutable preview artifact already exists with the same digest."
          else
            gcloud storage cp \
              --if-generation-match=0 \
              --cache-control='public,max-age=31536000,immutable' \
              "$DMG_PATH" "$artifact_path"
          fi

          notes=""
          if [[ -n "${PREVIEW_NOTES_BASE64:-}" ]]; then
            notes="$(printf '%s' "$PREVIEW_NOTES_BASE64" | base64 --decode)"
          fi
          payload="$(jq -n \
            --arg slug "$PREVIEW_SLUG" \
            --arg source_sha "$PREVIEW_SOURCE_SHA" \
            --arg dmg_url "https://storage.googleapis.com/omi_macos_updates/previews/${PREVIEW_SLUG}/${PREVIEW_SOURCE_SHA}/Omi-Preview.dmg" \
            --arg dmg_sha256 "$dmg_sha256" \
            --arg app_name "$APP_NAME" \
            --arg bundle_id "$BUNDLE_ID" \
            --arg url_scheme "$URL_SCHEME" \
            --arg built_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
            --arg signer "$SIGN_IDENTITY" \
            --arg notes "$notes" \
            --arg backend_url "$OMI_PYTHON_API_URL" \
            '{slug: $slug, source_sha: $source_sha, dmg_url: $dmg_url, dmg_sha256: $dmg_sha256, app_name: $app_name, bundle_id: $bundle_id, url_scheme: $url_scheme, built_at: $built_at, signer: $signer, notarization: "stapled", notes: $notes, backend_url: $backend_url}')"
          curl --fail --show-error --silent \
            --retry 3 \
            -H 'Content-Type: application/json' \
            -H "secret-key: $DESKTOP_PREVIEW_PUBLISH_KEY" \
            --data "$payload" \
            "${DESKTOP_PREVIEW_REGISTRY_URL%/}/v2/desktop/previews/publish"

      - name: Create GitHub release
        script: |
          if [[ "${PREVIEW_MODE:-false}" == "true" ]]; then
            echo "External previews do not create GitHub releases."
            exit 0
          fi
          set -euo pipefail

          # Keep symbol publication repairable: a Sentry credential outage must not
          # strand an otherwise signed and smoke-tested candidate. The exact,
          # UUID-verified dSYM is retained as a GitHub release artifact below.
          scripts/publish-desktop-debug-symbols.sh upload-best-effort \
            --binary "$APP_BUNDLE/Contents/MacOS/$BINARY_NAME" \
            --dsym "$DSYM_PATH"

          CHANGELOG_MD=$(python3 ../../.github/scripts/desktop-changelog.py latest-release --format markdown || echo "- Bug fixes and improvements")

          # Build changelog as pipe-separated string for KEY_VALUE_START
          CHANGELOG_PIPE=$(echo "$CHANGELOG_MD" | sed 's/^- //' | tr '\n' '|' | sed 's/|$//')

          RELEASE_NOTES="## OMI Desktop v${VERSION}

          ### What's New
          ${CHANGELOG_MD}

          ### Downloads
          - **DMG Installer**: For fresh installs, download the DMG below
          - **Auto-Update**: Existing users will receive this update automatically via Sparkle

          <!-- KEY_VALUE_START
          isLive: false
          channel: candidate
          edSignature: ${ED_SIGNATURE}
          betaEdSignature: ${BETA_ED_SIGNATURE}
          changelog: ${CHANGELOG_PIPE}
          KEY_VALUE_END -->"

          # A candidate is the immutable evidence container. Once published,
          # preserve its signed artifacts; retries resume Beta promotion below.
          if gh release view "$CM_TAG" --repo "$GITHUB_REPO" >/dev/null 2>&1; then
            echo "GitHub release candidate already exists; preserving immutable evidence for $CM_TAG"
          else
            # The backend reservation is the authoritative Beta fence. It runs
            # only after package, signature, notarization, and signed smoke pass,
            # immediately before this workflow creates the immutable candidate.
            set -euo pipefail
            test -n "${BETA_PROMOTION_TOKEN:-}" || {
              echo "ERROR: BETA_PROMOTION_TOKEN is required to reserve a canonical candidate" >&2
              exit 1
            }
            curl --fail-with-body --silent --show-error \
              --request POST "${OMI_PYTHON_API_URL%/}/v2/desktop/beta/candidates/reserve" \
              --header "Authorization: Bearer ${BETA_PROMOTION_TOKEN}" \
              --header 'Content-Type: application/json' \
              --data "{\"tag\":\"${CM_TAG}\"}"
            gh release create "$CM_TAG" \
              --repo "$GITHUB_REPO" \
              --title "Omi Desktop v${VERSION} (candidate)" \
              --notes "$RELEASE_NOTES" \
              "$SPARKLE_ZIP_PATH" \
              "$DMG_PATH" \
              "$BETA_SPARKLE_ZIP_PATH" \
              "$BETA_DMG_PATH" \
              "$DSYM_ARCHIVE" \
              "$BUILD_DIR/desktop-smoke-result.json" \
              "$BUILD_DIR/desktop-smoke-result-beta.json"
            echo "GitHub release candidate created: $CM_TAG"
          fi

      - name: Promote signed candidate to Omi Beta
        script: |
          if [[ "${PREVIEW_MODE:-false}" == "true" ]]; then
            echo "External previews do not enter the shared Beta channel."
            exit 0
          fi
          set -euo pipefail
          test -n "${BETA_PROMOTION_TOKEN:-}" || {
            echo "ERROR: BETA_PROMOTION_TOKEN is required to promote the signed candidate" >&2
            exit 1
          }
          # The endpoint independently reads the immutable release, tag, GitHub
          # digests, and exact Beta signed-smoke asset. Retries are idempotent.
          for attempt in 1 2 3; do
            if curl --fail-with-body --silent --show-error \
              --connect-timeout 10 --max-time 30 \
              --request POST "${OMI_PYTHON_API_URL%/}/v2/desktop/beta/promote-candidate" \
              --header "Authorization: Bearer ${BETA_PROMOTION_TOKEN}" \
              --header 'Content-Type: application/json' \
              --data "{\"tag\":\"${CM_TAG}\"}"; then
              echo "Omi Beta now points to $CM_TAG."
              exit 0
            fi
            if [[ "$attempt" -lt 3 ]]; then
              sleep "$((attempt * 2))"
            fi
          done
          echo "ERROR: Beta promotion was not confirmed after bounded retry; candidate remains non-live." >&2
          exit 1

    artifacts:
      - build/*.app
      - build/*.dmg
      - build/*.zip
      - build/*.dSYM
      - build/desktop-smoke-result.json
      - build/desktop-smoke-result-beta.json
    publishing:
      email:
        recipients:
          - i@m13v.com
        notify:
          success: true
          failure: true

  # This workflow has no automatic trigger. It is started only by
  # .github/workflows/desktop_publish_preview.yml after the protected
  # desktop-preview-publish environment approves an immutable source SHA.
  # Keep this control definition on main: the source SHA is checked out only
  # after this trusted configuration has loaded.
  omi-desktop-swift-preview:
    name: Publish OMI Desktop Preview (Swift)
    instance_type: mac_mini_m4
    max_build_duration: 120
    environment:
      # TEMPORARY — tracked by #10221. The dedicated Preview group was not
      # provisioned with the signing/notarization inputs required by the shared
      # signed-artifact steps. Reuse the existing groups only to unblock an
      # immutable Preview publication; restore a signing-only group and remove
      # these broad imports in the follow-up.
      groups:
        - desktop_preview_secrets
        - appstore_credentials
        - desktop_secrets
      vars:
        PREVIEW_MODE: "true"
        BINARY_NAME: "Omi Computer"
        BUILD_DIR: "build"
        DMGBUILD_VERSION: "1.6.7"
        GCS_DESKTOP_UPDATES_BUCKET: "gs://omi_macos_updates"
        # This registry remains on the production control plane even when the
        # preview artifact itself is configured to use a preview backend.
        DESKTOP_PREVIEW_REGISTRY_URL: "https://api.omi.me"
      # Desktop Swift ship/CI toolchain pin. Keep in sync with
      # desktop/macos/ci/xcode-pin.json (enforced by the
      # desktop-swift-ci-contract check); never `latest` or `edge`.
      xcode: 26.6
    working_directory: desktop/macos
    cache:
      cache_paths:
        - ~/Library/Caches/org.swift.swiftpm
    scripts: *desktop_signed_artifact_steps
    artifacts:
      - build/*.app
      - build/*.dmg
      - build/desktop-smoke-result.json

  # ============================================
  # CONTEXT FOR CLAUDE RELEASE
  # A separately-installable micro-app release. The package/release helpers own
  # product identity and artifact naming; only Apple signing/notarization inputs
  # are shared with the Omi Desktop workflow.
  #
  # Trigger: context-for-claude-v1.1.0
  # Required group: context_for_claude_release
  #   CONTEXT_SPARKLE_PUBLIC_KEY   — Context-only Sparkle Ed25519 public key
  #   CONTEXT_SPARKLE_PRIVATE_KEY  — Context-only private ZIP-signing key
  #   CONTEXT_GITHUB_TOKEN         — repo Contents-write token for release upload
  # Reused appstore_credentials inputs:
  #   MACOS_DEVELOPER_ID_P12, MACOS_DEVELOPER_ID_P12_PASSWORD,
  #   APP_STORE_CONNECT_PRIVATE_KEY, APP_STORE_CONNECT_KEY_IDENTIFIER,
  #   APP_STORE_CONNECT_ISSUER_ID
  # ============================================
  context-for-claude-release:
    name: Release Context for Claude
    instance_type: mac_mini_m4
    max_build_duration: 120
    environment:
      groups:
        - appstore_credentials
        - context_for_claude_release
      vars:
        CONTEXT_RELEASE_REPO: "BasedHardware/omi"
      xcode: 16.4
    triggering:
      events:
        - tag
      tag_patterns:
        - pattern: "context-for-claude-v*"
          include: true
    working_directory: desktop/context-for-claude
    cache:
      cache_paths:
        - ~/Library/Caches/org.swift.swiftpm
    scripts:
      - name: Validate Context release tag
        script: |
          set -euo pipefail
          ./scripts/release-context.sh --tag "${CM_TAG:?CM_TAG is required}" --dry-run

      - name: Import Developer ID certificate
        script: |
          set -euo pipefail
          keychain initialize
          printf '%s' "$MACOS_DEVELOPER_ID_P12" | base64 --decode > /tmp/context-developer-id.p12
          KEYCHAIN_PATH="$(keychain get-default)"
          security import /tmp/context-developer-id.p12 \
            -k "$KEYCHAIN_PATH" \
            -P "${MACOS_DEVELOPER_ID_P12_PASSWORD:-}" \
            -T /usr/bin/codesign \
            -T /usr/bin/security
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" "$KEYCHAIN_PATH" 2>/dev/null || true
          rm -f /tmp/context-developer-id.p12
          SIGN_IDENTITY="$(security find-identity -v -p codesigning | grep 'Developer ID Application' | head -1 | awk -F'"' '{print $2}')"
          [[ -n "$SIGN_IDENTITY" ]] || {
            echo "ERROR: Developer ID Application certificate not found" >&2
            exit 1
          }
          printf 'CFC_SIGN_IDENTITY=%s\n' "$SIGN_IDENTITY" >> "$CM_ENV"

      - name: Resolve Context Swift packages
        script: |
          set -euo pipefail
          # --disable-keychain: the Developer ID import above initializes a build keychain, after
          # which SwiftPM's credential search fails with errSecInteractionNotAllowed (-25308) and
          # aborts the resolve while fetching Sparkle's binary artifact. That artifact is a public
          # release asset and none of this package's dependencies are private, so there is no
          # credential to find.
          xcrun swift package resolve --disable-keychain

      - name: Build, notarize, sign, and publish Context
        script: |
          set -euo pipefail
          # These are existing Apple notarization inputs from appstore_credentials.
          # They are mapped to Context-specific helper variables and never printed.
          export CFC_ASC_PRIVATE_KEY="$APP_STORE_CONNECT_PRIVATE_KEY"
          export CFC_ASC_KEY_ID="$APP_STORE_CONNECT_KEY_IDENTIFIER"
          export CFC_ASC_ISSUER_ID="$APP_STORE_CONNECT_ISSUER_ID"
          ./scripts/release-context.sh --tag "${CM_TAG:?CM_TAG is required}" --publish

    artifacts:
      - dist/*.dmg
      - dist/*.zip