🚀 React Native iOS CI/CD with GitHub Actions — Build and Deploy to TestFlight Automatically
Tired of manually archiving and uploading builds to TestFlight? In this detailed guide, learn how to automate your React Native iOS builds with GitHub Actions. We’ll cover code signing, provisioning profiles, Xcode setup, and automatic TestFlight deployment — end to end.
In this post, I’ll walk you through how I built an automated iOS TestFlight pipeline using GitHub Actions for my React Native apps — the same setup I use for production builds.
You’ll see the final version of the YAML, all the errors I faced, and how I finally got a working pipeline that builds, signs, and uploads to TestFlight — with manual signing for Release.
If you’ve read my Android CI/CD article, this is the iOS companion piece.
🧩 What You’ll Need Before You Start
- ✅ Apple Developer account with:
- Apple Distribution Certificate (
.p12) - App-store provisioning profiles (
.mobileprovision)
- Apple Distribution Certificate (
- ✅ Access to App Store Connect (API key for uploads)
- ✅ Your project already builds locally in Xcode
- ✅ Two files ready:
/.github/workflows/ios-build-testflight.yml/ios/exportOptions.plist
🔐 Required GitHub Secrets
APPLE_CERT_P12— Base64 of your.p12certificateAPPLE_CERT_PASSWORD— Password used when exportingAPPLE_MATCH_PROVISIONING_PROFILE— Base64 of main.mobileprovisionAPPLE_EXTENSION_PROVISION_PROFILE— Base64 of widget/extension profile(if you have one)APPLE_APP_STORE_CONNECT_API_KEY— Base64 of.p8keyAPPLE_APP_STORE_CONNECT_API_KEY_ID— Key IDAPPLE_APP_STORE_CONNECT_API_ISSUER_ID— Issuer UUID
Encode via:
base64 -i certificate.p12 | pbcopy
⚙️ Step 1 – Why iOS CI/CD is “special”
Unlike Android, iOS requires code signing, provisioning profiles, and Xcode version compatibility.
Local Xcode magic (automatic signing) often breaks on CI.
Tools like xcodeproj don’t always support new project versions right away (objectVersion issues).
Getting an .ipa reliably uploaded to TestFlight is a multi-step dance.
I started with the same mindset as Android: build → test → deploy. But iOS quickly forced me to slow down and debug each layer.
🪜 Step 2 – What I started with (Android style optimism)
On Android, I could:
gradlew assembleRelease
So I thought iOS would be “just swap commands.” Here’s my naïve first try:
jobs:
ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 18 }
- run: npm install
- run: cd ios && pod install
- run: |
cd ios
xcodebuild -workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-archivePath $PWD/build/MyApp.xcarchive \
archive
Result
✅ Pods installed successfully.
❌ Build failed with:
Code signing is required for product type Application because I didn’t handle signing or provisioning yet.
🧱 Step 3 – Add Certificates & Temporary Keychain
We import the Apple Distribution certificate (.p12) and unlock a temporary keychain:
- name: Create temporary keychain
run: |
security create-keychain -p "${{ secrets.APPLE_CERT_PASSWORD }}" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "${{ secrets.APPLE_CERT_PASSWORD }}" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
- name: Import certificate
run: |
echo "${{ secrets.APPLE_CERT_P12 }}" | base64 --decode > certificate.p12
security import certificate.p12 -k build.keychain -P "${{ secrets.APPLE_CERT_PASSWORD }}" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${{ secrets.APPLE_CERT_PASSWORD }}" build.keychain
rm certificate.p12
💡 If you skip the set-key-partition-list line, codesign will fail later with
No signing certificate "Apple Distribution" found.
🪪 Step 4 – Install Provisioning Profiles
I realized CI needs to see .mobileprovision files in ~/Library/MobileDevice/Provisioning Profiles/. So I added:
- name: Install provisioning profiles
run: |
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
echo "$PROVISION_APP_BASE64" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/app_dist.mobileprovision
echo "$PROVISION_EXT_BASE64" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/ext_dist.mobileprovision
After this, I got the infamous:
ReactAppDependencyProvider does not support provisioning profiles
Because I had earlier set manual provisioning globally (in project.pbxproj) — that broke pods and dependencies.
🧩 Step 5 – Switch release build to manual, keep dependencies automatic
On my local machine I changed in Xcode:
For the Release configuration under the app target → Manual signing (selecting my distribution profile)
For all pods, React targets, extensions → Automatic signing
With that change, CI no longer tried to manually sign pods.
In the pipeline step I also added explicit flags:
xcodebuild …
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM=MY_TEAM_ID \
CODE_SIGN_IDENTITY="Apple Distribution"
This got me past the “does not support provisioning profiles” error.
🔧 Step 6 – Patch for Xcode 16 (objectVersion = 70)
After opening the project in a newer Xcode, my project.pbxproj had:
objectVersion = 56;
But newer Xcode (16+) bumped it to 70. Then CI or xcodeproj gem would complain:
xcodeproj: unsupported objectVersion 70
When Xcode 16 arrived, the xcodeproj gem didn’t yet know version 70.
Solution: patch the xcodeproj gem at runtime before pod install, e.g.:
- name: Patch xcodeproj for Xcode 16
run: |
ruby -i -pe "gsub(/LAST_KNOWN_OBJECT_VERSION = '[0-9]*'/, \"LAST_KNOWN_OBJECT_VERSION = '70'\")" \\
$(gem which xcodeproj/project/object/native_target.rb) 2>/dev/null || true
That lets pod install proceed with objectVersion 70 recognized.
If your CI runner uses older Xcode, you might instead downgrade the objectVersion in the project file to what the runner supports.
🪄 Step 7 – Auto-update Version & Build Number
Before building, I needed to bump marketing version and build number:
- name: Update version and build number
run: |
cd ios
agvtool new-marketing-version ${{ github.event.inputs.version }}
agvtool new-version -all ${{ github.event.inputs.buildNumber }}
cd ..
This ensures every build has consistent versioning.
🏗️ Step 8 – Archive → Export → Upload
Here’s the complete working workflow.
🟡 Remember
Replace every MyApp → your actual app name
Replace every MY_TEAM_ID → your Team ID
📄 Step 9 – exportOptions.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>MY_TEAM_ID</string>
<key>signingStyle</key>
<string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>com.MyApp.app</key>
<string>ios_distribution_provision</string>
<!--- if you have an extension, add it here, else remove it -->
<key>com.MyApp.app.ImageNotificationExtension</key>
<string>comMyAppappImageNotificationExtension</string>
</dict>
<key>uploadSymbols</key>
<true/>
<key>compileBitcode</key>
<false/>
<key>uploadBitcode</key>
<false/>
</dict>
</plist>
🎯 Result
Each run now:
-
Installs Pods
-
Sets version & build
-
Archives with manual signing
-
Exports .ipa via exportOptions.plist
-
Uploads automatically to TestFlight
-
Saves IPA artifact
No manual Xcode work required. 🎉
🧠 Key Lessons
-
Automatic ≠ CI-friendly — use Manual for Release only.
-
Match your Xcode version to the runner.
-
Patch xcodeproj when Apple bumps objectVersion.
-
Keep signing files as Base64 secrets, never in Git.
-
Always use a temporary keychain in Actions.
-
Confirm provisioning profiles path is correct.
🏁 Closing Thoughts
-
This workflow went through countless errors:
-
No signing certificate found
-
Provisioning profile mismatch
-
ReactAppDependencyProvider doesn’t support profiles
-
objectVersion unsupported
…but once it works, it’s fully automated and repeatable.
Now I can push a tag, trigger a build, and minutes later see it appear in TestFlight.
Written by @iamhusnain
— if you found this helpful, check out the Android version:
React Native Android CI/CD with GitHub Actions → Google Play .
Let's bring your app idea to life
I specialize in mobile and backend development.
Share this article
Related Articles
🚀 CI/CD for React Native Android: Build, Sign, Version, and Upload to Google Play with GitHub Actions
A complete guide to React Native CI/CD with GitHub Actions: build signed APK/AAB, auto-increment versionCode, and release to Google Play automatically — no more manual Gradle commands or Play Console uploads.
Custom Fonts in React Native WebView: The Complete Fix
Struggling with custom fonts not rendering inside a React Native WebView? Learn why it happens and how to properly inject fonts using platform-specific asset paths and base64 embedding.
Building Offline-First React Native Apps: Complete Implementation Guide
Master offline-first architecture in React Native. Learn AsyncStorage, Realm, SQLite, sync strategies, and conflict resolution for seamless offline UX.