In the previous post I tried EAS Local on my Mac.
Local had me fighting a keychain vs EAS certificate mismatch, so I ran a cloud build as a sanity check — and it passed cleanly.
This time I treat EAS Cloud as the main distribution path and push to TestFlight from my terminal.
It’s simpler than Local, so if you don’t need local builds often, start here.
No Xcode window required on your Mac, which makes it a natural fit for CI.
Steps below follow template-expo-build-cicd.
Sample repository
Same template as the previous post.
It already has eas.json and build/submit settings — swap in your project values.
git clone https://github.com/testkun08080/template-expo-build-cicd.git
cd template-expo-build-cicd
npm ciThe template expects Node.js 20.x (eas.json base.node is 20.18.0).
What to replace after clone
The template ships with my values, so change these before you build:
| File | Replace |
|---|---|
app.json | ios.bundleIdentifier / extra.eas.projectId |
eas.json | submit.production.ios: appleId / ascAppId / appleTeamId |
For extra.eas.projectId, running eas init and linking to your Expo project is the reliable path.
If you leave the template projectId as-is, eas build will talk to someone else’s project.
EAS Cloud vs Local (Cloud view)
Same comparison as before, reframed from the Cloud side:
| EAS Cloud | EAS Local | |
|---|---|---|
| Mac required | No | Yes (Xcode 26.x) |
| Where it builds | Expo servers | Your Mac |
| Best for | Convenience / CI | Debugging native bits |
| Build logs | Dashboard | Directly in the terminal |
The template also has a production-local profile; for Cloud, use production.
What you should already have done
Before Cloud, having these in place makes life easier.
I covered them in the previous post — just the highlights here:
- App registered in App Store Connect (Bundle ID set)
app.jsonextra.eas.projectIdpointed at your project (eas init)eas.jsonsubmit.production.iosfilled with:appleId— Apple ID (email)ascAppId— numeric Apple ID under App Store Connect App InformationappleTeamId— Apple Developer Team ID
eas logindoneeas credentialsfor iOS / production shows Distribution cert + profile as Valid
Brand new to EAS? Do App Store Connect & Bundle ID and Signing setup in the previous post first, then come back.
eas.json production profile
Template excerpt aimed at TestFlight:
{
"cli": {
"version": ">= 15.0.0",
"appVersionSource": "remote"
},
"build": {
"base": {
"node": "20.18.0"
},
"production": {
"extends": "base",
"channel": "production",
"autoIncrement": true,
"ios": {
"resourceClass": "m-medium"
}
},
"production-local": {
"extends": "production"
}
},
"submit": {
"production": {
"ios": {
"appleId": "REPLACE_WITH_APPLE_ID",
"ascAppId": "REPLACE_WITH_ASC_APP_ID",
"appleTeamId": "REPLACE_WITH_APPLE_TEAM_ID"
}
}
}
}ascAppId is the numeric Apple ID under App Store Connect App Information — same field as ss6 in the previous post.
appleId and appleTeamId are used for eas submit auth, so fill all three.
Optional: pin Xcode with ios.image
The template does not set ios.image; EAS picks a default that fits the SDK.
I was on Xcode 26.2 for Local, so when I wanted Cloud to match I added this under production.ios:
"ios": {
"resourceClass": "m-medium",
"image": "macos-sequoia-15.6-xcode-26.2"
}Right after a major SDK bump, cross-check the EAS build image list.
Not required — mainly useful when Local and Cloud disagree.
Run the cloud build
Install deps, then start a cloud build.
The only difference from Local is no --local:
npm ci
eas build --platform ios --profile productionWith projectId and eas credentials done, signing mostly carries over.
First runs may still prompt for eas init or interactive confirms.
You can watch progress on the Expo dashboard — open the URL the CLI prints, or check the Builds tab on expo.dev.
# When you suspect cache issues (I used this while debugging Local too)
eas build --platform ios --profile production --clear-cacheWhen it finishes, the IPA lives on EAS (downloadable from the dashboard).
There’s no local build.ipa — submit with eas submit next.
Submit to TestFlight
Right after a Cloud build, submitting the latest success is the easy path:
eas submit --platform ios --profile production --latest--latest targets the most recent successful build.
You can also pin a build ID:
eas submit --platform ios --profile production --id <BUILD_ID>Local used eas submit --path ./build.ipa; Cloud points at an artifact already on EAS.
With eas.json submit.production.ios (appleId / ascAppId / appleTeamId) filled in, the destination resolves automatically.
The first eas submit may walk you through App Store Connect API key setup.
Follow the prompts — you don’t need a fastlane match Git repo.
The template app.json sets ITSAppUsesNonExemptEncryption: false, so the encryption prompt often never appears.
If another project asks, answer Y for a typical HTTPS-only app (see ss13 in the previous post).
About --non-interactive
Add --non-interactive when wiring this into CI or scripts.
Only after projectId, signing, and submit settings have been set up once:
eas build --platform ios --profile production --non-interactive
eas submit --platform ios --profile production --latest --non-interactiveFrom your own terminal, eas login is enough. You need EXPO_TOKEN when automating from GitHub Actions.
I cover Xcode Cloud distribution in the next post.
Common issues
Duplicate build numbers
With appVersionSource: remote and autoIncrement, EAS manages build numbers and collisions are rare.
Hard-coding ios.buildNumber in app.json can conflict — for production I prefer remote management.
Xcode version mismatch
The template leaves ios.image unset.
If Local and Cloud disagree, pin ios.image as in the section above.
Local fails, Cloud succeeds
Same pattern I hit in the previous post.
If Cloud passes, EAS credentials are fine — on Local, suspect a Mac keychain vs Distribution certificate mismatch.
Wrap-up
Honestly, this path is the easy one.
eas build(no--local) produces an IPA on Expo’s serverseas submit --latestsends that Cloud artifact straight to TestFlight- Reuse the signing you set up in the previous post
- No Xcode on your machine — solid CI-oriented path
Next: shipping to TestFlight with Xcode Cloud, skipping EAS.
The free hours that come with the Apple Developer Program are nice for solo projects.
→ Expo on Xcode Cloud — ci_post_clone.sh and TestFlight