name: Mobile iOS Release # Why a separate workflow from Android: iOS releases go through App Store # review, which can take days. Decoupling the triggers lets an Android release # ship immediately without waiting on iOS, and vice versa. on: push: tags: - 'mobile-ios-v*' workflow_dispatch: inputs: bump_patch_version: description: 'Bump the iOS marketing version patch number before release. Tick this after a version has shipped to the App Store (the release fails fast if the current version''s train is already closed).' required: false default: false type: boolean release_version: description: 'Optional exact iOS marketing version override, e.g. 0.0.15' required: false type: string testflight_changelog: description: 'Optional TestFlight external tester changelog' required: false type: string jobs: ios-build: # GitHub-hosted macOS runner: required for Xcode. Expo SDK 55's # expo-modules-core declares swift_version 6.0 and uses Swift 6 syntax # (@MainActor isolation). Xcode 16.x (macos-15) can't even parse it # ("unknown attribute 'MainActor'"), so we need Xcode 26.x → macos-26. runs-on: macos-26 defaults: run: working-directory: mobile steps: - name: Checkout uses: actions/checkout@v6 - name: Select Xcode uses: maxim-lobanov/setup-xcode@v1 with: # Xcode 26.x ships the Swift 6.x toolchain Expo SDK 55 requires. xcode-version: '26.5' - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 24 - name: Setup pnpm uses: pnpm/action-setup@v6 with: run_install: false - name: Install dependencies run: pnpm install --frozen-lockfile - name: Setup Ruby and fastlane uses: ruby/setup-ruby@v1 with: ruby-version: '3.3' bundler-cache: true working-directory: mobile - name: Resolve release version and build number env: ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} ASC_API_KEY_P8: ${{ secrets.ASC_API_KEY_P8 }} MOBILE_IOS_RELEASE_VERSION: ${{ github.event.inputs.release_version }} MOBILE_IOS_BUMP_PATCH_VERSION: ${{ github.event.inputs.bump_patch_version }} FASTLANE_SKIP_UPDATE_CHECK: '1' FASTLANE_HIDE_CHANGELOG: '1' run: bundle exec fastlane ios prepare_release_version version:"$MOBILE_IOS_RELEASE_VERSION" bump_patch:"$MOBILE_IOS_BUMP_PATCH_VERSION" - name: Expo prebuild run: npx expo prebuild --platform ios --no-install - name: Install CocoaPods run: npx pod-install ios # Why: `-allowProvisioningUpdates` + the App Store Connect API key can # create/refresh provisioning profiles, but it cannot recreate the # distribution certificate's PRIVATE KEY across runs. So we import a # pre-exported distribution .p12 (created once via Apple Developer) into a # throwaway keychain. The keychain is ephemeral to the runner and torn # down with the VM; nothing secret is written to the repo. - name: Import distribution certificate env: IOS_DIST_CERT_P12: ${{ secrets.IOS_DIST_CERT_P12 }} IOS_DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }} run: | set -euo pipefail KEYCHAIN_PATH="$RUNNER_TEMP/orca-signing.keychain-db" # Random per-run keychain password; never persisted. KEYCHAIN_PASSWORD="$(openssl rand -base64 24)" CERT_PATH="$RUNNER_TEMP/orca-dist-cert.p12" echo "$IOS_DIST_CERT_P12" | base64 --decode > "$CERT_PATH" security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security import "$CERT_PATH" -P "$IOS_DIST_CERT_PASSWORD" \ -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" # Allow codesign/xcodebuild to use the key without an interactive prompt. security set-key-partition-list -S apple-tool:,apple: \ -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" >/dev/null # Put our keychain in the search list so xcodebuild can find the identity. security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db rm -f "$CERT_PATH" - name: Build and upload to TestFlight env: ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} ASC_API_KEY_P8: ${{ secrets.ASC_API_KEY_P8 }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} TESTFLIGHT_CHANGELOG: ${{ github.event.inputs.testflight_changelog }} # Keep fastlane non-interactive and quiet about analytics in CI. FASTLANE_SKIP_UPDATE_CHECK: '1' FASTLANE_HIDE_CHANGELOG: '1' run: bundle exec fastlane ios release - name: Upload .ipa artifact if: always() uses: actions/upload-artifact@v7 with: name: orca-mobile-ipa path: mobile/build/*.ipa if-no-files-found: ignore