name: Build iOS on: workflow_dispatch: inputs: upload_to_release: description: "Upload IPA to latest GitHub release" required: false type: boolean default: false concurrency: group: build-ios-${{ github.ref }} cancel-in-progress: true env: APP_DIR: packages/app NODE_VERSION: "24.15.0" # Default to least privilege. Override per-job where needed. permissions: contents: read jobs: build-ios: runs-on: macos-15 timeout-minutes: 45 permissions: contents: write steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 with: bun-version: canary - name: Select latest Xcode run: | XCODE_PATH=$(ls -d /Applications/Xcode*.app 2>/dev/null | sort -V | tail -1) echo "Using Xcode at: $XCODE_PATH" sudo xcode-select -s "$XCODE_PATH" xcodebuild -version - name: Cache CocoaPods uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 with: path: ${{ env.APP_DIR }}/ios/App/Pods key: pods-${{ runner.os }}-${{ hashFiles('packages/app-core/platforms/ios/App/Podfile', 'packages/app/capacitor.config.ts', 'packages/app/package.json') }} restore-keys: pods-${{ runner.os }}- - name: Install dependencies run: bun install - name: Build web assets working-directory: ${{ env.APP_DIR }} run: bun run build - name: Build Capacitor plugins working-directory: ${{ env.APP_DIR }} run: bun run plugin:build - name: Sync Capacitor iOS working-directory: ${{ env.APP_DIR }} run: bun run cap:sync:ios - name: Overlay iOS native files run: node packages/app-core/scripts/run-mobile-build.mjs ios-overlay - name: Install CocoaPods working-directory: ${{ env.APP_DIR }}/ios/App run: pod install - name: Run iOS developer-install preflight working-directory: ${{ env.APP_DIR }} run: bun run preflight:ios:sideload - name: Determine version id: version run: | if [[ "${{ github.event_name }}" == "release" ]]; then VERSION="${{ github.event.release.tag_name }}" VERSION="${VERSION#v}" else VERSION="0.0.0-dev" fi echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Set version in Info.plist working-directory: ${{ env.APP_DIR }}/ios/App/App run: | /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${{ steps.version.outputs.version }}" Info.plist /usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${{ github.run_number }}" Info.plist # --- Code signing (only when secrets are configured) --- - name: Install Apple certificate and provisioning profile if: env.IOS_CERTIFICATE_BASE64 != '' env: IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }} IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} IOS_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_PROVISION_PROFILE_BASE64 }} KEYCHAIN_PASSWORD: ${{ github.run_id }} run: | # Create temporary keychain KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" # Import certificate CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 echo "$IOS_CERTIFICATE_BASE64" | base64 --decode -o "$CERTIFICATE_PATH" security import "$CERTIFICATE_PATH" -P "$IOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security list-keychain -d user -s "$KEYCHAIN_PATH" # Install provisioning profile PROFILE_PATH=$RUNNER_TEMP/build_profile.mobileprovision echo "$IOS_PROVISION_PROFILE_BASE64" | base64 --decode -o "$PROFILE_PATH" mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles cp "$PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/ - name: Build unsigned archive (no signing secrets) if: env.IOS_CERTIFICATE_BASE64 == '' env: IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }} working-directory: ${{ env.APP_DIR }}/ios/App run: | xcodebuild archive \ -workspace App.xcworkspace \ -scheme App \ -configuration Release \ -archivePath $RUNNER_TEMP/Eliza.xcarchive \ CODE_SIGN_IDENTITY="" \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGNING_ALLOWED=NO \ -allowProvisioningUpdates \ | xcbeautify || true - name: Build signed archive if: env.IOS_CERTIFICATE_BASE64 != '' env: IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }} working-directory: ${{ env.APP_DIR }}/ios/App run: | xcodebuild archive \ -workspace App.xcworkspace \ -scheme App \ -configuration Release \ -archivePath $RUNNER_TEMP/Eliza.xcarchive \ -allowProvisioningUpdates \ | xcbeautify || true - name: Export IPA if: env.IOS_CERTIFICATE_BASE64 != '' env: IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }} IOS_EXPORT_METHOD: ${{ secrets.IOS_EXPORT_METHOD || 'ad-hoc' }} IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }} IOS_PROVISION_PROFILE_NAME: ${{ secrets.IOS_PROVISION_PROFILE_NAME }} run: | # Create export options plist cat > $RUNNER_TEMP/ExportOptions.plist << PLIST method ${IOS_EXPORT_METHOD} teamID ${IOS_TEAM_ID} provisioningProfiles ai.eliza.app ${IOS_PROVISION_PROFILE_NAME} PLIST xcodebuild -exportArchive \ -archivePath $RUNNER_TEMP/Eliza.xcarchive \ -exportOptionsPlist $RUNNER_TEMP/ExportOptions.plist \ -exportPath $RUNNER_TEMP/export # Rename IPA with version mv $RUNNER_TEMP/export/App.ipa $RUNNER_TEMP/export/Eliza-${{ steps.version.outputs.version }}.ipa - name: Upload archive artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: eliza-ios-${{ env.IOS_CERTIFICATE_BASE64 != '' && 'release' || 'unsigned' }} path: | $RUNNER_TEMP/Eliza.xcarchive $RUNNER_TEMP/export/*.ipa if-no-files-found: warn retention-days: ${{ env.IOS_CERTIFICATE_BASE64 != '' && 30 || 7 }} env: IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }} - name: Confirm iOS distribution policy if: (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.upload_to_release)) && env.IOS_CERTIFICATE_BASE64 != '' env: IOS_CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }} run: | { echo "## iOS artifact policy" echo "" echo "Signed IPA files are kept as private workflow artifacts for TestFlight/App Store traceability." echo "They are not uploaded to public GitHub Releases. Public iOS distribution must use TestFlight or the App Store." } >> "$GITHUB_STEP_SUMMARY" - name: Cleanup keychain if: always() run: | security delete-keychain $RUNNER_TEMP/app-signing.keychain-db 2>/dev/null || true