Xcode Cloud for React Native: ship IPAs to Firebase without the signing trap
A verified pipeline for shipping a React Native IPA from Xcode Cloud to Firebase App Distribution — and the one manual exportArchive mistake that breaks every build.
A React Native project can build a signed .ipa in Xcode Cloud and ship it to Firebase App Distribution with no local Mac, no Fastlane, and no manual exportArchive. This article documents a verified, end-to-end loop: push code → Xcode Cloud archive → auto-exported ad-hoc IPA → Firebase App Distribution → tester notification.
There is really only one trap: do not call xcodebuild -exportArchive yourself inside ci_post_xcodebuild.sh. It always fails, because Xcode Cloud's internal signing proxy is invisible to your script. The fix is to grab the IPA that Cloud already exported for you. Everything else below is the full chain and every pitfall we hit.
Xcode Cloud has only 3 script hooks
Apple officially supports only three (docs):
ci_post_clone.sh— after the repository is clonedci_pre_xcodebuild.sh— before xcodebuildci_post_xcodebuild.sh— after xcodebuild
There is no ci_post_archive.sh. When the Archive action finishes, the hook that fires is ci_post_xcodebuild.sh. Name your script ci_post_archive.sh and it will simply never run — this is the most common "my script didn't execute" cause.
★ The big trap: manual exportArchive signing failure
Do not do this:
# ❌ Always fails — delete these lines
xcodebuild -exportArchive \
-archivePath "$CI_ARCHIVE_PATH/YourApp.xcarchive" \
-exportPath ./out \
-exportOptionsPlist exportOptions.plistIt fails with:
No signing certificate "iOS Distribution" foundWhat's actually happening (confirmed by build-log timing): before ci_post_xcodebuild.sh runs, Xcode Cloud has already auto-exported three IPAs (ad-hoc / app-store / development) using its internal signing proxy, dropped at:
/Volumes/workspace/adhocexport//Volumes/workspace/appstoreexport//Volumes/workspace/developmentexport/
That internal proxy looks like this, and it is invisible to your script:
-DVTPortalRequest.Endpoint=http://localhost:6667
-DVTProvisioningIsManaged=YES
-DVTSkipCertificateValidityCheck=YESYour manual xcodebuild -exportArchive cannot reach localhost:6667, so it always reports "no signing certificate" — regardless of whether exportOptions says ad-hoc or development.
The fix: delete the manual export entirely, find the ad-hoc IPA Cloud already produced, and upload that to Firebase. Manual export is not just redundant, it turns the build red.
> I initially assumed "auto-export happens after post-xcodebuild, so the script can't see the IPA." Wrong. Log order is: auto-export 3 variants → write catalog metadata → then run ci_post_xcodebuild.sh. By the time your script runs, the IPA is already on disk.
ci_scripts must live in ios/ci_scripts/
In a React Native project the .xcodeproj sits inside the ios/ subdirectory, and Xcode Cloud resolves ci_scripts relative to the .xcodeproj. So the scripts must go to:
ios/ci_scripts/ci_post_clone.sh
ios/ci_scripts/ci_post_xcodebuild.shA ci_scripts/ at the repo root will not be found. This is a React-Native-specific gotcha (a native iOS project has its .xcodeproj at the root, so it doesn't hit this).
ci_post_clone.sh: install dependencies
The Xcode Cloud image only pre-installs Homebrew and CocoaPods 1.17 — no node, no npm, no bundler. So step one is installing node:
#!/bin/sh
set -e
# Image ships no node — must install. CocoaPods is already present (skips).
export HOMEBREW_NO_AUTO_UPDATE=1 # saves 15-30s
brew install node
# JS dependencies
npm ci
# iOS dependencies (use the pre-installed pod, not bundler)
cd ios && pod installpod install occasionally fails to clone from GitHub (e.g. GTMSessionFetcher reset) — add a retry:
for i in 1 2 3; do cd ios && pod install && break || sleep 5; doneci_post_xcodebuild.sh: grab the IPA, upload to Firebase
This hook fires after both Build and Archive actions, so guard at the top — if it's not an Archive, exit quietly:
#!/bin/sh
set -e
# Only Archive produces an IPA. For a plain Build, exit quietly.
[ -z "$CI_ARCHIVE_PATH" ] && exit 0
# Take the ad-hoc IPA Cloud already exported (do NOT manual exportArchive!)
IPA=$(find "${CI_AD_HOC_SIGNED_APP_PATH:-/Volumes/workspace/adhocexport}" -name "*.ipa" | head -1)
if [ -z "$IPA" ]; then
echo "⚠️ No IPA found, but build is green — download it from Artifacts"
exit 0
fi
echo "✓ Found IPA: $IPA"
# Upload to Firebase App Distribution
npx firebase-tools@^15 appdistribution:distribute "$IPA" \
--app "$FIREBASE_APP_ID" \
--token "$FIREBASE_TOKEN" \
--groups "testers" \
--release-notes "Build $CI_BUILD_NUMBER"
# Do NOT fail the build on upload error — the IPA is already in ArtifactsKey points:
FIREBASE_APP_ID= theGOOGLE_APP_IDinsideGoogleService-Info.plistFIREBASE_TOKENis an env var (generate locally withnpx firebase-tools login:ci)- Pin
firebase-tools@^15: v15 still acceptsFIREBASE_TOKEN; v16+ removes it. Using@latestwill break the day it bumps. - Wrap the upload so a failure does not turn the build red — the IPA is already safe in Artifacts.
How to configure the workflow
- Action = Archive (not Build — only Archive triggers the auto-export of IPA)
- Signing → Automatic + your Apple Developer Team
- Environment: add
FIREBASE_TOKEN - Trigger with
CI_START_CONDITION=push(push triggers a build); to run manually: Xcode ⌘9 → Cloud → right-click the workflow → Start a Build
Debug checklist
ci_post_clone.shdidn't run → ci_scripts is not underios/ci_post_xcodebuild.shdidn't run → file namedci_post_archive(doesn't exist), or workflow action isn't Archivenpm: command not found/pod: command not found→ didn'tbrew install nodein the clone scriptNo signing certificate "iOS Distribution" found→ you're manual-exportArchiving; delete it- Firebase upload failed but build is green → expected; the upload is best-effort, IPA is in Artifacts
- To grab the IPA file → Xcode Cloud Organizer → the build → Artifacts → "YourApp 1.0 ad-hoc"