f(x) = σ(Wx + b)∇loss.backward()model.predict(x)torch.nn.Transformerawait fetch('/api')git rebase -i HEAD~3docker compose up -dconsole.log('here')∫f(x)dx∑(i=0→n)O(log n)fn main() -> Result<>SELECT * FROM userskubectl get pods{ ...state, loading }npm run build && deploypipe(filter, map, reduce)env.PROD=true
Codse logo
  • Services
  • Work
  • OpenClaw
  • Blog
  • Home
  • Services
  • Work
  • OpenClaw
  • Blog

Get in touch

Let's build something

Tell us what you're working on. We'll scope it within 48 hours and propose a sprint or retainer that fits.

Quick links

ServicesWorkAI ReadinessOpenClawBlog

Also find us on

GithubFacebookInstagram
Codse© 2026 Codse
Software · AI Agents
App Development
Guides
Software Engineering

How we ship Sisu on Xcode Cloud: Expo archives as an EAS alternative

Codse Tech
Codse Tech
August 9, 2026

EAS Build is the default path for most Expo teams, and it works well. You can also ship the same app through Xcode Cloud — Apple's CI next to App Store Connect — when you want Archive and TestFlight in one Apple-native workflow, a second production lane beside EAS, or builds that are not tied to whatever Xcode is installed on a developer Mac.

We set this up for Sisu, Codse Tech's Expo kids learning app (managed workflow, ios/ gitignored). This post is the playbook: regenerate native projects in ci_post_clone, keep build numbers sane, trigger only from release/*, Archive with App Store Connect deployment preparation (so builds are App Store–eligible), and upload to TestFlight from a post-action.

One constraint up front: do not archive for the App Store from a macOS beta host. Even with a "stable" Xcode.app, Apple can reject the IPA with ITMS-90111 (unsupported SDK / Xcode). Cloud builders on release images are fine. Local eas build --local on the same beta Mac hits the same wall.

Workflow diagram showing an Expo repo cloning into Xcode Cloud, running prebuild, then archiving to TestFlight

When Xcode Cloud fits next to EAS

Treat Cloud as an alternative or complement to EAS iOS Archive, not a forever fork.

Apple-native Archive

Archive and TestFlight post-actions live next to App Store Connect, with env vars editable in Xcode.

Second CI lane

Useful when you want EAS for day-to-day Expo work and Cloud for store archives, or the reverse.

Stable builder OS

Runs on Apple's release macOS/Xcode images, independent of beta OS on a laptop.

Still keep EAS

Pin EAS production to a stable image (for example "image": "sdk-57") so cloud EAS is not coupled to a local toolchain.

On a beta Mac: use EAS cloud or Xcode Cloud for store binaries. Skip local Archive and eas build --local until you are back on a release OS.

The Expo problem: no committed ios/ project

Managed Expo usually keeps ios/ out of git. Xcode Cloud needs a workspace after clone.

What works for Sisu:

  1. Track a thin launcher at ios/ci_scripts/ci_post_clone.sh (ignore the rest of ios/).
  2. Keep the real script under xcode-cloud/ci_post_clone.sh.
  3. On Cloud: install Node/CocoaPods if needed → npm ci → expo prebuild --platform ios --clean → restore the launcher → pod install.

prebuild --clean deletes ios/, including the launcher Cloud used to find your script. Copy that launcher back in the same job or the next run has nothing to exec.

Local one-time setup so you can create the workflow:

npm run prebuild:ios   # prebuild + restore ci_scripts launcher
open ios/*.xcworkspace
# Product → Xcode Cloud → Create Workflow
# Archive → App Store Connect (distribute + TestFlight), then post-action upload

Pick the app product in the workflow — for Sisu that is the Sisu target, not an ExpoPods target.

Environment variables (managed in Xcode)

You do not need production secrets in git for Cloud builds. Edit workflow env in:

  • Xcode → Product → Xcode Cloud → Manage Workflows → Environment
  • or App Store Connect → Xcode Cloud → the same workflow editor

Mark values as secret, scope them per workflow, and change them without a code push. Anything ci_post_clone, expo prebuild, or the JS bundle needs at build time should live there (plus Apple's own CI vars like CI_BUILD_NUMBER).

Ideas for workflow env (examples, not a required list)

KindExample namesNotes
Public client configEXPO_PUBLIC_API_URL, EXPO_PUBLIC_ENV=productionInlined into the JS bundle; treat as public
IAP / subscriptionsEXPO_PUBLIC_REVENUECAT_IOS_API_KEY, entitlement idUse the store SDK key on Archive workflows; mark secret
AdsEXPO_PUBLIC_ADMOB_USE_TEST_IDS=0Production workflows should not ship test ad units
Analytics / flagsEXPO_PUBLIC_POSTHOG_KEY, EXPO_PUBLIC_FLAGS_KEYClient / read-only keys only
Native secretsSentry auth, upload tokensSecret in Xcode; never EXPO_PUBLIC_*
Build controlIOS_BUILD_NUMBER, IOS_BUILD_NUMBER_BASEOnly if you stamp before prebuild (Option B below)

Apple also injects CI metadata (CI, CI_BUILD_NUMBER, CI_PRIMARY_REPOSITORY_PATH, …). Export CI=true (lowercase) in ci_post_clone if your tooling only treats the boolean form as truthy — Apple often sets CI=TRUE.

On the same Environment tab, pick a release macOS / Xcode image. Creating the workflow from beta Xcode on your Mac is fine; building the store IPA on a beta image is not.

Build numbers: why the first Cloud build was 1

If app.json has no expo.ios.buildNumber, expo prebuild writes CFBundleVersion as 1. Fine for a brand-new app. Wrong if ASC already has higher builds — duplicates and backward jumps get rejected.

Fix it one of two ways. Pick one. Do not run both.

Option A — Xcode Cloud manages and increments

In the Archive action, enable Managed Version and Build Number. Set the next build number above whatever is already in App Store Connect. Cloud stamps that value and increments on later runs.

This is the simplest path when Cloud is your primary Archive lane: no script math, fewer env vars, Apple owns the counter.

Option B — stamp app.json before prebuild

Useful when you want the number driven from env, or you are coordinating closely with EAS:

buildNumber = IOS_BUILD_NUMBER
  or
buildNumber = IOS_BUILD_NUMBER_BASE + CI_BUILD_NUMBER

Example: base 40 + first Cloud CI_BUILD_NUMBER=1 → 41.

If you use Option B, turn Managed Version and Build Number off so Cloud does not overwrite your stamp.

After a Cloud ship, sync EAS if you still use it:

eas build:version:set -p ios <n>

How we trigger store builds on Sisu

We do not Archive from every main push. The workflow start condition is custom branches matching release/* only. Pushes to main, feature branches, or tags alone do not start this job.

# version = marketing (e.g. 1.0.7), build = intended CFBundleVersion label
git checkout -b release/1.0.7-41
git push -u origin release/1.0.7-41   # this push starts Xcode Cloud

git tag v1.0.7-41                     # optional git label
git push origin v1.0.7-41

Branch pattern: release/{version}-{build}. The branch push is the trigger. The tag is for humans and history.

Also turn Auto-cancel Builds off on this Archive workflow. With it on, a second push while a run is queued cancels the earlier one — fine for PR spam, bad mid-release.

What runs after the trigger

Configure the Archive action carefully. Deployment preparation must be App Store Connect — the option for distributing on the App Store and testing (TestFlight). Do not leave it on an internal-only / TestFlight-internal destination.

If you Archive for internal testing only, the build lands as buildAudienceType: INTERNAL_ONLY. It can show VALID in TestFlight and still refuse App Store version attach (The specified pre-release build could not be added). App Review needs APP_STORE_ELIGIBLE.

Recommended Archive + post-action setup:

  1. Archive → Deployment Preparation: App Store Connect (distribute and test).
  2. Post-action → TestFlight Internal Testing (and/or External) so the same App Store–eligible IPA is uploaded for testers after a successful Archive.

Green Cloud + TestFlight is still not App Review. You attach a Valid, App Store–eligible build to the version and submit (IAP and metadata are separate).

Minimal ci_post_clone shape

npm ci
export CI=true
# Optional: only if you use Option B build numbers
# node ./xcode-cloud/set-ios-build-number.mjs
npx expo prebuild --platform ios --clean
# restore ios/ci_scripts/ci_post_clone.sh launcher
cd ios && pod install --repo-update

Write the durable bits into AGENTS.md so the next engineer or coding agent does not rediscover ITMS-90111 the hard way.

Failure modes we hit

SymptomLikely causeFix
ITMS-90111Archive from macOS beta hostBuild on EAS cloud or Xcode Cloud release image
Build attaches in TF but not to App Store versionArchive destination was internal-only → INTERNAL_ONLY audienceSet Archive deployment preparation to App Store Connect; rebuild
Huge EAS upload / failed archiveFat paths in the upload (.build/, node_modules, screenshot trees)Tighten .easignore if you still use EAS
Cloud build number 1No prior number, or Managed + script fightingOption A or Option B, not both
Pending build disappearsAuto-cancel onDisable for the release Archive workflow
Wrong target / odd compileWorkflow pointed at a podSelect the Sisu app product
Wrong client config in binaryWorkflow env missing in XcodeSet Environment vars; rebuild
Launcher gone after cleanForgot restore after prebuild --cleanCopy launcher back in the same script

CI green does not prove the app boots. Separate Expo gotchas we have seen as blank TestFlight windows: react-i18next suspending with useSuspense: true before initI18n, a nested Stack under Native Tabs hanging in Release only, and Info.plist pointing at a SceneDelegate class that never landed in the binary. Smoke a Release build on device or TestFlight before you trust Archive alone.

FAQ

Is this meant to replace EAS?+

No. Use it as an alternative or second lane. Keep one source of truth for CFBundleVersion — Cloud-managed or stamped in prebuild, not both — and sync the other system with eas build:version:set after a ship if you use both.

Do I need to commit the full ios/ folder?+

No. Track only ios/ci_scripts/ (launcher) plus your xcode-cloud/ scripts. Regenerate native projects on every Cloud run with expo prebuild.

Why did Apple accept a cloud build but reject my local archive?+

Same app, different host OS/SDK. A release-image cloud builder (EAS cloud or Xcode Cloud) is what Apple expects. Local beta macOS archives are a common ITMS-90111 source even when Xcode.app looks stable.

Why is my Valid build INTERNAL_ONLY and unusable for App Review?+

The Xcode Cloud Archive action was set to an internal-only / TestFlight-internal destination. Switch Deployment Preparation to App Store Connect (distribute and test), run a new release branch build, and attach that App Store–eligible binary instead.

How do you keep Archive from running too often?+

Start condition on release/* only, Auto-cancel off for that workflow, and no "every change to main." Intentional ships only.

If you want an Expo store archive that is not bound to a laptop Xcode install — or you are stuck behind the macOS beta wall — release-image Xcode Cloud plus prebuild-in-ci_post_clone is a clean alternative to EAS Archive.

Pair this with the screenshot pipeline when you are refreshing store creatives for the same release.

App Store Optimization

Listing, keywords, and creative work when you want a partner on store conversion.

Explore service

Custom software

Expo / React Native product builds, including the release plumbing in this post.

Explore service
xcode cloud
expo
eas build
app store connect
react native ios
testflight
sisu