
React Native iOS CI wants an Apple silicon Mac so Metro, Hermes, and the iOS simulator all run arm64 natively, and so the archive step uses a current Xcode. Request a Figment M-series Mac from $95/month (exact chip, RAM, and disk are confirmed in the quote) at /mac-hosting/start and register it as a GitHub self-hosted runner. If you want the CI setup, backups, monitoring, and 30 minutes of monthly support handled for you, step up to Managed Build Mac at $249/month (one M-series Mac, CI setup, backups, monitoring, 30 minutes monthly support).
Figment also offers a $29 virtual Intel macOS VM (2 vCPU, 8 GiB RAM, 80 GiB disk, browser-console access, no GPU acceleration). It is real macOS and can host a compatible Intel toolchain — per Apple's porting guide, "if you need to run Xcode on an Intel-based processor, use Xcode 26 or earlier" (developer.apple.com). For React Native iOS release work in 2026 that is usually too constrained: Xcode 27 and arm64-native builds require Apple silicon. Use the $29 VM for scripting, dependency caches, or Intel toolchain validation; use the M-series Mac for the signed TestFlight pipeline below.
A React Native iOS build wires together three ecosystems in a fixed order: Node (Metro, Hermes), Ruby + CocoaPods (native pods declared by npm packages), then Xcode (archive and sign). Skip or reorder any step and the error surfaces in the next layer — a missing Node yields a Metro error, a skipped pod install yields an Xcode "module not found," and a missing scheme yields an xcodebuild exit code. Apple's xcodebuild technote TN2339 documents the -workspace, -scheme, and -archivePath arguments the workflow below uses; React Native's Running on device guide covers the iOS workspace and signing prerequisites.
Toolchain drift is the biggest source of flaky iOS CI. Pin both runtimes explicitly:
actions/setup-node with a specific major (Node 20 LTS is a safe default; defer to package.json engines when it exists). cache: 'npm' caches the npm download index keyed on package-lock.json; npm ci still populates node_modules.ruby/setup-ruby with bundler-cache: true. This caches vendor/bundle keyed on Gemfile.lock, which should pin both CocoaPods and Fastlane. Avoid sudo gem install — the bundler cache is faster and reproducible.Cache node_modules and ios/Pods with separate keys so an npm change does not invalidate your Pods cache. Key the Pods cache on ios/Podfile.lock, which is the precise record of resolved Pod versions.
CocoaPods produces ios/YourApp.xcworkspace; always build the workspace, never the bare .xcodeproj. Find the real scheme on your own machine first — do not guess it in CI:
xcodebuild -list -workspace ios/YourApp.xcworkspace
Copy the exact scheme into Fastlane's build_app call. Use Fastlane Match with --readonly in CI so a PR build never tries to create new certificates (which would fail without interactive auth and can disturb your signing pool). Match stores encrypted certificates and provisioning profiles in a private git repo; the CI runner pulls them under a temporary keychain Match manages for the job. Keep the .p8 App Store Connect key, the Match encryption password, and the Match git token in your CI secret manager — never check them into the repo and never dump the shell environment to a file.
Replace YourApp with your actual scheme. Register your Figment M-series Mac as a self-hosted runner and label it mac-arm64; adjust runs-on to match.
# .github/workflows/ios-ci.yml
name: React Native iOS CI
on:
push:
branches: [main, 'release/**']
pull_request:
branches: [main]
jobs:
build-ios:
runs-on: [self-hosted, mac-arm64]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci --prefer-offline
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- uses: actions/cache@v4
with:
path: ios/Pods
key: pods-${{ runner.os }}-${{ hashFiles('ios/Podfile.lock') }}
restore-keys: pods-${{ runner.os }}-
- name: Install CocoaPods
working-directory: ios
run: bundle exec pod install
- name: Sync signing material (read-only)
run: bundle exec fastlane match appstore --readonly
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
- name: Archive and upload to TestFlight
run: bundle exec fastlane beta
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.ASC_PRIVATE_KEY }}
The matching fastlane/Fastfile (see the Fastlane iOS beta deployment guide for the primary reference):
default_platform(:ios)
platform :ios do
lane :beta do
app_store_connect_api_key(
key_id: ENV.fetch("APP_STORE_CONNECT_API_KEY_ID"),
issuer_id: ENV.fetch("APP_STORE_CONNECT_API_ISSUER_ID"),
key_content: ENV.fetch("APP_STORE_CONNECT_API_KEY_CONTENT"),
is_key_content_base64: true
)
build_app(
workspace: "ios/YourApp.xcworkspace",
scheme: "YourApp",
configuration: "Release",
export_method: "app-store"
)
upload_to_testflight(skip_waiting_for_build_processing: true)
end
end
Steps return non-zero on failure and GitHub Actions stops the job automatically — a failed archive is a failed run, never a "fast" one. Run the workflow once on a feature branch so you read the first log without release-week pressure. First runs populate the npm and CocoaPods caches; subsequent runs reuse them.
Provision these five repository secrets before the first run:
| Secret | Contents |
|---|---|
MATCH_PASSWORD | Encryption password for the Match git repo |
MATCH_GIT_BASIC_AUTHORIZATION | Base64 user:token for Match git access |
ASC_KEY_ID | App Store Connect API key ID |
ASC_ISSUER_ID | App Store Connect issuer UUID |
ASC_PRIVATE_KEY | Base64-encoded .p8 key content |
Three Figment options are relevant to this workflow:
| Plan | Price | What you get |
|---|---|---|
| Virtual Intel macOS | $29/mo | 2 vCPU, 8 GiB, 80 GiB, browser console. Good for Intel-toolchain scripts and caches; not for arm64 or Xcode 27. |
| M-series Mac | from $95/mo | Physical Apple silicon Mac; exact chip, RAM, and disk confirmed in the quote. Request-based while supply is confirmed. |
| Managed Build Mac | $249/mo | One M-series Mac, CI setup, backups, monitoring, 30 minutes monthly support. |
Honest price comparison against the obvious alternative for a single arm64 React Native runner. CloudMac lists an M4 16 GB / 256 GB at $99.99/month on cloudmac.org/pricing and a $999/year option on cloudmac.org. That is an effective $83.25/month on the annual term — $141 less per year than 12 × $95 = $1,140 if the Figment quote lands at $95. CloudMac's $99.99 monthly rate is also a published M4/16/256 configuration; Figment's $95 is an unspecified M-series entry point, so only compare entry price, not hardware. Figment's differentiator for this page is the Managed Build Mac plan: CloudMac is a hardware lease, Managed Build Mac adds the CI setup, backups, monitoring, and support minutes against your pipeline. If you only need bare hardware and the lowest annual outlay, CloudMac's annual term wins on price; if you want the CI work delivered, request a Figment quote.
Verify arm64 once the runner is provisioned:
uname -m # expect arm64
node -e "console.log(process.arch)" # expect 'arm64'
xcodebuild -version
Request a configuration quote at /mac-hosting and start onboarding at /mac-hosting/start.
Related guides: Build Flutter iOS in CI on a Mac runner · Send Slack alerts for iOS CI build status · Remote Xcode setup on Windows