138 lines
5.3 KiB
YAML
138 lines
5.3 KiB
YAML
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: 'Use the first open iOS patch version after the checked-in version, skipping versions already closed on the App Store.'
|
||
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
|
||
# Archive + upload is ~30–40m when healthy; 90m leaves margin without
|
||
# sitting multi-hour on ASC processing polls (see Fastfile pilot wait).
|
||
timeout-minutes: 90
|
||
|
||
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
|