workflows:
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
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
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: |
# 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
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..."
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 ""
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"
chmod +x "$uploadScriptPath"
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
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
working_directory: app
scripts:
- name: Set up Firebase
script: |
dart pub global activate flutterfire_cli
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
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
APP_NAME=$(find $(pwd) -name "Omi.app" | head -1)
echo "Found app at: $APP_NAME"
OUTPUT_DIR="$(pwd)/build/macos"
mkdir -p "$OUTPUT_DIR"
PACKAGE_NAME="Omi.pkg"
xcrun productbuild --component "$APP_NAME" /Applications/ "$OUTPUT_DIR/unsigned.pkg"
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"
xcrun productsign --sign "$INSTALLER_CERT_NAME" "$OUTPUT_DIR/unsigned.pkg" "$OUTPUT_DIR/$PACKAGE_NAME"
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
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
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..."
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: 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"
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"
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
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: |
# 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
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
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
working_directory: app
scripts:
- name: Set up Firebase
script: |
dart pub global activate flutterfire_cli
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
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..."
PUBSPEC_VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //' | tr -d ' ')
BUILD_NAME=$(echo "$PUBSPEC_VERSION" | cut -d'+' -f1)
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
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)"
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"
mkdir -p ../desktop/macos/build
gh release download "$LATEST_MACOS_TAG" \
--repo BasedHardware/omi \
--pattern "Omi.zip" \
--dir /tmp
unzip -o /tmp/Omi.zip -d ../desktop/macos/build/
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
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
/usr/libexec/PlistBuddy -c "Delete :com.apple.security.get-task-allow" "$entitlements_file" 2>/dev/null || true
/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
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
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"
echo "$MACOS_DEVELOPER_ID_PROFILE" | base64 --decode > /tmp/developer_id.provisionprofile
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: Configure Xcode project for Developer ID signing
script: |
cd macos
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)
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
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
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
BUNDLED_APP="$APP_PATH/Contents/MacOS/Omi Computer.app"
if [ -d "$BUNDLED_APP" ]; then
echo "Signing bundled Omi Computer.app..."
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
codesign --force --timestamp --options runtime \
--sign "$DEVELOPER_ID_CERT" \
"$BUNDLED_APP"
echo "Bundled Omi Computer.app signed"
fi
SPARKLE_FW="$APP_PATH/Contents/Frameworks/Sparkle.framework"
if [ -d "$SPARKLE_FW" ]; then
find "$SPARKLE_FW" -name "*.xpc" -type d | while read xpc; do
codesign --force --timestamp --options runtime \
--sign "$DEVELOPER_ID_CERT" \
"$xpc"
done
[ -d "$SPARKLE_FW/Versions/B/Updater.app" ] && \
codesign --force --timestamp --options runtime \
--sign "$DEVELOPER_ID_CERT" \
"$SPARKLE_FW/Versions/B/Updater.app"
[ -f "$SPARKLE_FW/Versions/B/Autoupdate" ] && \
codesign --force --timestamp --options runtime \
--sign "$DEVELOPER_ID_CERT" \
"$SPARKLE_FW/Versions/B/Autoupdate"
codesign --force --timestamp --options runtime \
--sign "$DEVELOPER_ID_CERT" \
"$SPARKLE_FW"
fi
codesign --force --timestamp --options runtime \
--sign "$DEVELOPER_ID_CERT" \
--entitlements /tmp/notarization.entitlements \
"$APP_PATH"
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"
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"
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"
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)
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"
cd "$(dirname "$APP_PATH")"
ditto -c -k --sequesterRsrc --keepParent "Omi.app" "$(pwd)/$ZIP_NAME"
mv "$ZIP_NAME" "$APP_DIR/$DIST_DIR/"
cd "$APP_DIR"
[ ! -f "macos/Pods/Sparkle/bin/sign_update" ] && echo "ERROR: Sparkle sign_update binary not found" && exit 1
SIGN_OUTPUT=$(echo "$SPARKLE_PRIVATE_KEY" | macos/Pods/Sparkle/bin/sign_update --ed-key-file - "$DIST_DIR/$ZIP_NAME" 2>&1)
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
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..."
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: 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"
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:
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"
xcode: 26.6
working_directory: desktop/macos
cache:
cache_paths:
- ~/Library/Caches/org.swift.swiftpm
- $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
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
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"
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
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"
command -v cmake >/dev/null 2>&1 || brew install cmake
TEMP_DIR="/tmp/libwebp-build-$$"
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"
mkdir "$SRC/build-arm64" && cd "$SRC/build-arm64"
cmake .. -DCMAKE_OSX_ARCHITECTURES=arm64 $CMAKE_COMMON
make -j$(sysctl -n hw.ncpu)
mkdir "$SRC/build-x86_64" && cd "$SRC/build-x86_64"
cmake .. -DCMAKE_OSX_ARCHITECTURES=x86_64 $CMAKE_COMMON
make -j$(sysctl -n hw.ncpu)
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
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
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
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')"
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"
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"
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_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/"
install_name_tool -add_rpath "@executable_path/../Frameworks" \
"$APP_BUNDLE/Contents/MacOS/$BINARY_NAME"
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"
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
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
[ -f "omi_icon.icns" ] && cp omi_icon.icns "$APP_BUNDLE/Contents/Resources/OmiIcon.icns"
cp Desktop/Sources/GoogleService-Info.plist "$APP_BUNDLE/Contents/Resources/"
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/"
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
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
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
/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
/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"
[ -f "Desktop/embedded.provisionprofile" ] && \
cp Desktop/embedded.provisionprofile "$APP_BUNDLE/Contents/embedded.provisionprofile"
echo "$OMI_DESKTOP_APP_ENV_V3" | base64 --decode > "$APP_BUNDLE/Contents/Resources/.env"
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"
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"
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
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
AGENT_BUNDLE="$APP_BUNDLE/Contents/Resources/agent"
if [ -d "$AGENT_BUNDLE/node_modules" ]; then
rm -rf "$AGENT_BUNDLE/node_modules/@anthropic-ai/claude-code/vendor/claude-code-jetbrains-plugin" \
2>/dev/null || true
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
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
PI_MONO_EXTENSION_BUNDLE="$APP_BUNDLE/Contents/Resources/pi-mono-extension"
if [ -d "$PI_MONO_EXTENSION_BUNDLE/node_modules" ]; then
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
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
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"
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
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
- 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
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
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")
CHANGELOG_PIPE=$(echo "$CHANGELOG_MD" | sed 's/^- //' | tr '\n' '|' | sed 's/|$//')
RELEASE_NOTES="## OMI Desktop v${VERSION}
${CHANGELOG_MD}
- **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 -->"
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
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
omi-desktop-swift-preview:
name: Publish OMI Desktop Preview (Swift)
instance_type: mac_mini_m4
max_build_duration: 120
environment:
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"
DESKTOP_PREVIEW_REGISTRY_URL: "https://api.omi.me"
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:
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