# iOS build gate for pull requests and main. # # Requires NO Apple/EAS secrets: it validates the Expo config, exports the iOS JS # bundle, generates the native project, installs CocoaPods, and compiles an # UNSIGNED iPhone Simulator target with xcodebuild. Signing/TestFlight lives in # publish-app-store.yml. # # Cheap platform-neutral checks (typecheck + unit tests) run first on Linux and # gate the costly macOS native build. name: iOS CI # NOTE: no paths-ignore here. "Typecheck and unit tests" is a REQUIRED status # check in branch protection; a path-filtered required check never runs on # docs-only PRs, leaving them permanently BLOCKED. Running the cheap Linux # typecheck/test job (and the gated macOS build) on every PR keeps the required # check satisfiable. See #143 (docs PR stuck) for why. on: pull_request: branches: [main] push: branches: [main] # Cancel superseded runs for the same ref (e.g. new push to an open PR). concurrency: group: ios-ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: test: name: Typecheck and unit tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: # Node >= 23.6 runs the TypeScript test files natively (type-stripping), # matching the local toolchain. No build step or extra deps required. node-version: 24 cache: npm - name: Install dependencies (deterministic) run: npm ci --legacy-peer-deps - name: Typecheck run: npm run typecheck - name: Unit tests run: npm test ios-build: name: Unsigned iOS Simulator build needs: test # macos-15 ships Xcode 16.x, which React Native 0.81 / Expo SDK 54 require. runs-on: macos-15 timeout-minutes: 45 env: # No source-map upload from CI (no Sentry auth token here); keep the build hermetic. SENTRY_DISABLE_AUTO_UPLOAD: "true" steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 20 cache: npm - name: Install dependencies (deterministic) run: npm ci --legacy-peer-deps - name: Validate Expo config and plugin resolution run: npx expo config --type introspect --json > expo-config.introspect.json - name: Export iOS JavaScript bundle run: npx expo export --platform ios --output-dir dist - name: Prebuild native iOS project run: npx expo prebuild --platform ios --no-install - name: Install CocoaPods dependencies working-directory: ios run: pod install - name: Resolve Xcode workspace and scheme id: xc run: | set -euo pipefail shopt -s nullglob workspaces=(ios/*.xcworkspace) workspace="${workspaces[0]:-}" if [ -z "$workspace" ]; then echo "::error::No .xcworkspace was generated by expo prebuild." exit 1 fi scheme=$(xcodebuild -workspace "$workspace" -list -json | node -e " const d = JSON.parse(require('fs').readFileSync(0, 'utf8')); const all = (d.workspace && d.workspace.schemes) || []; const app = all.filter((s) => s !== 'Pods' && !s.startsWith('Pods-')); if (app.length === 0) { console.error('No application scheme found in workspace'); process.exit(1); } process.stdout.write(app[0]); ") echo "workspace=$workspace" >> "$GITHUB_OUTPUT" echo "scheme=$scheme" >> "$GITHUB_OUTPUT" echo "Using workspace='$workspace' scheme='$scheme'" - name: Build unsigned iPhone Simulator app run: | set -euo pipefail NSUnbufferedIO=YES xcodebuild \ -workspace "${{ steps.xc.outputs.workspace }}" \ -scheme "${{ steps.xc.outputs.scheme }}" \ -configuration Debug \ -sdk iphonesimulator \ -destination 'generic/platform=iOS Simulator' \ -derivedDataPath ios/build \ CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGN_IDENTITY="" \ build 2>&1 | tee xcodebuild.log - name: Upload xcodebuild log if: always() uses: actions/upload-artifact@v7 with: name: ios-xcodebuild-log path: xcodebuild.log retention-days: 14 if-no-files-found: ignore