Link copied

In the previous post I had EAS Cloud → TestFlight working, but EAS build minutes still matter sometimes.
So I tried Xcode Cloud. Apple Developer Program includes 25 free hours per month — handy for solo projects, and this is the flow I personally use day to day.

Expo apps don’t come ready for Xcode Cloud’s build environment, so you need to prep everything in ci_post_clone.sh.

Steps below follow template-expo-build-cicd.

Sample repository

Same template as the previous post.
It already has eas.json and app.json — swap Bundle ID and related values.

git clone https://github.com/testkun08080/template-expo-build-cicd.git
cd template-expo-build-cicd
npm ci

The template expects Node.js 20.x (eas.json base.node is 20.18.0).
The Xcode Cloud ci_post_clone.sh also installs Node 20 via Homebrew when it’s missing.

What to replace after clone

FileReplace
app.jsonios.bundleIdentifier / extra.eas.projectId
ios/templateexpobuild.xcodeproj/project.pbxprojDEVELOPMENT_TEAM / PRODUCT_BUNDLE_IDENTIFIER

extra.eas.projectId isn’t used by Xcode Cloud directly, but local expo prebuild is smoother if you’ve already run eas init.
Also check “What to replace after clone” in the previous post.

EAS Cloud vs Xcode Cloud

Reframing the previous EAS Cloud post from the Xcode Cloud side:

EAS CloudXcode Cloud
Mac requiredNoNo (Apple-managed)
SigningEAS remote credentialsApple automatic (no match)
Expo prebuildBuilt into EASRun in ci_post_clone.sh
Main costEAS build quota25h/month free
Setupeas.json + eas loginPartial ios/ commit + CI scripts

I tend to burn EAS Cloud free builds for quick experiments, and use Xcode Cloud when I’m only shipping iOS.

What you should already have done

Before Xcode Cloud, these help a lot.
I covered them across EAS Local and EAS Cloud — highlights only:

  • App registered in App Store Connect (Bundle ID set)
  • app.json ios.bundleIdentifier fixed (e.g. com.testkun08080.template-expo-build)
  • You’ve submitted to TestFlight at least once (so the flow is familiar)

Xcode Cloud needs neither EAS nor EXPO_TOKEN.
Instead you need a minimal ios/ commit plus ci_post_clone.sh.

What is Xcode Cloud?

Xcode Cloud is Apple’s CI/CD service, configured from App Store Connect or Xcode.
Archive → TestFlight stays entirely on Apple’s side, so it’s a distribution path that doesn’t use EAS.

Before creating a workflow, get one Archive working on your Mac.
The next sections walk through that with screenshots.

Partial-commit strategy for ios/

In a Managed Workflow Expo app, ios/ is usually gitignored.
On Xcode Cloud, committing only the Xcode project and CI scripts (a partial commit) is the practical approach.

After expo prebuild, the template roughly looks like this:

ios/
├── ci_scripts/
│   └── ci_post_clone.sh           # commit
├── templateexpobuild.xcodeproj/   # pbxproj + scheme only
├── templateexpobuild.xcworkspace/ # commit
└── (Podfile, Pods, etc. gitignored → regenerated by prebuild)

Example ios/.gitignore:

# Ignore generated files; keep Xcode project + ci_scripts
*
!.gitignore
!templateexpobuild.xcodeproj/
!templateexpobuild.xcworkspace/
!ci_scripts/

templateexpobuild.xcodeproj/*
!templateexpobuild.xcodeproj/project.pbxproj
!templateexpobuild.xcodeproj/project.xcworkspace/
!templateexpobuild.xcodeproj/xcshareddata/

Every expo prebuild --clean regenerates Podfile and entitlements, so this minimal layout is easier than committing everything.
Make sure scheme files live under xcshareddata/xcschemes/ and are explicitly tracked in git.

ci_post_clone.sh

Right after clone, install Node and prepare the native project.
Xcode Cloud looks for ios/ci_scripts/ci_post_clone.sh.

Here’s what the template actually uses (ci_post_clone.sh):

#!/bin/sh
set -euo pipefail

cd "$CI_PRIMARY_REPOSITORY_PATH"

# Xcode Cloud images do not include Node.js by default.
if ! command -v node >/dev/null 2>&1; then
  brew install node@20
  brew link node@20 --force --overwrite
fi

npm ci
npx expo prebuild --platform ios

cd ios
pod install

In short:

  1. cd to CI_PRIMARY_REPOSITORY_PATH (repo root Xcode Cloud provides)
  2. Install node@20 via Homebrew if Node is missing
  3. npm ciexpo prebuildpod install

I use the same CI_PRIMARY_REPOSITORY_PATH pattern in my Flutter × Xcode Cloud post.

If CocoaPods isn’t on the image, adding brew install cocoapods right after Node usually stabilizes things.
If prebuild cache mismatches keep failing, switch to npx expo prebuild --platform ios --non-interactive --clean and pod install --repo-update.

When you need NODE_BINARY

If a React Native build phase can’t find Node, append this to ci_post_clone.sh:

NODE_BINARY="$(command -v node)"
echo "export NODE_BINARY=${NODE_BINARY}" > ios/.xcode.env.local

ios/.xcode.env.local is usually gitignored, so generate it every build.

Archive locally first

Before configuring an Xcode Cloud workflow, get Archive working on your Mac.
After expo prebuild and pod install, open ios/templateexpobuild.xcworkspace in Xcode.

Open the workspace and check signing

Select the project in the navigator and open Signing & Capabilities.
Check the Release configuration used for Archive too.

Confirm:

  • Automatically manage signing is on
  • Team is your Apple Developer account
  • Bundle Identifier is com.testkun08080.template-expo-build and matches app.json
Signing & Capabilities (Release)

If you edited project.pbxproj by hand, make sure it still matches what Xcode shows.
With the partial-commit strategy I usually have:

CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = XXXXXXXXXX;
PRODUCT_BUNDLE_IDENTIFIER = com.testkun08080.template-expo-build;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;

Xcode Cloud talks to App Store Connect, so you don’t manually manage certs or run fastlane match.
Reuse the Bundle ID you set up in the EAS Cloud post.

Create an Archive

Pick a device or Any iOS Device, then Product → Archive.

Product → Archive

Organizer after Archive

When it succeeds, Organizer opens with the archive listed.
In the template it shows up as templateexpobuild, e.g. Version 1.0.0 (1).

Organizer after Archive

If Identifier shows com.testkun08080.template-expo-build and Team shows your account, signing is probably fine.
For a manual TestFlight upload you’d use Distribute App; for Xcode Cloud, move on to the workflow.

Once this works, Integrate → Create Workflow… often becomes available too.

Create an Xcode Cloud workflow

With Archive green, create the workflow.
Menu: Integrate → Create Workflow… (same place via Product → Xcode Cloud).

Integrate → Create Workflow...

Select a product

Pick templateexpobuild from the apps in the workspace.
Confirm your account appears under Team.

Select a Product

Grant source access

When asked for GitHub access, check the target repo (e.g. testkun08080/template-expo-build-cicd) and continue.

Grant Access to Your Source Code

The first time you may need to link GitHub and Apple ID.
Also check Settings → Accounts in Xcode for a GitHub account.

You’ll see the App Store Connect app name and Bundle ID.
Confirm com.testkun08080.template-expo-build, then Next.

Confirm App on App Store Connect

Review the workflow

Defaults look roughly like this:

ItemValue
Start conditionBranch Changes (main)
EnvironmentLatest Release (newest Xcode)
ActionArchive (iOS)
Post-actionOften no TestFlight on first create
Review Workflow

Scheme: templateexpobuild. Workspace: templateexpobuild.xcworkspace.
First-time wizards sometimes omit TestFlight under Post-Actions (see screenshot).
I also hit a case where editing the workflow at this point still didn’t show a TestFlight option.

Start the first build

You’ll get a Start Build screen.
Pick main and hit Start Build to run on Xcode Cloud.

Start Build

On my machine the Start Build sheet didn’t close on its own…
After pressing the button, confirm the build is actually running (next section), then close the sheet yourself.

Watch the build

Once started, you can follow progress in App Store Connect under the Xcode Cloud tab.
Status spins at first.

App Store Connect — build in progress

When done, you get a green check and build numbers (1, 2, …).
Warnings may appear, but if Archive succeeded the build can still go to TestFlight.

App Store Connect — build finished

There can be a short lag before the build shows under the TestFlight tab.
Also peek at Xcode Cloud logs to confirm ci_post_clone.sh succeeded.

Enable TestFlight distribution

After one successful build, open the Archive - iOS action and under Distribution Preparation pick TestFlight (Internal Testing Only).

Pick TestFlight in the Archive action

The first workflow wizard sometimes hides TestFlight.
If so, try the “TestFlight missing in the wizard” section below, then set it via Edit Workflow….

Common issues

TestFlight missing when creating the workflow

Sometimes the initial wizard never shows a TestFlight option.
On my setup this order fixed it:

  1. Confirm a build actually runs via Start Build (App Store Connect → Xcode Cloud tab)
  2. Restart Xcode
  3. Open Integrate → Manage Workflows… (or edit the workflow) again
  4. Check whether Archive - iOSDistribution Preparation now lists TestFlight

If that still fails, ship one Archive-only workflow first, then add TestFlight.

Pod errors after prebuild

Put pod install --repo-update in ci_post_clone.sh to reduce cache oddities.
Put entitlements (App Groups, etc.) in app.json and let prebuild apply them.

Scheme not found

Confirm the project name after expo prebuild matches the scheme Xcode Cloud selects.
With partial commits you must explicitly track schemes under templateexpobuild.xcodeproj/xcshareddata/xcschemes/.

Build time

Every run does npm ci and often a clean-ish prebuild, so times can feel like a first build.
Lean on Xcode Cloud’s caching and keep the script minimal.

EAS works, Xcode Cloud fails

If EAS Cloud passes, Bundle ID and entitlements are usually fine.
On Xcode Cloud, walk ci_post_clone.sh logs, then NODE_BINARY, then whether pod install succeeded.

Wrap-up

  • On Xcode Cloud you need Node + expo prebuild + pod install in ci_post_clone.sh
  • Partial-commit ios/ (Xcode project + ci_scripts only) stays manageable
  • Local Archive first → smoother workflow setup
  • If TestFlight is missing: one Xcode Cloud build → restart Xcode
  • Cost-wise, 25 free hours/month fits solo work well

Next I’ll put the same build → TestFlight flow on GitHub Actions
both a fastlane match version and an EAS Cloud version.

Expo + GitHub Actions — TestFlight with fastlane match or EAS