I use Expo Prebuild for a lot of my React Native projects, as for most of my projects I don’t need much in the form of custom native code. I wanted to set up cloud CI for building and delivering my apps to TestFlight to simplify my workflow. Setting up a Jenkins server seemed like a bit too much of extra maintenance, and the EAS cloud does cost money.
Since I get 25 compute hours/month as part of my Apple Developer subscription, this should be more than enough for my purposes and the occasional app build.
I started my journey by reading through the Apple documentation, which shows that there’s support for custom scripts within the lifecycle of a cloud workflow. I created an example CI script (at the bottom of this post). There are some caveats to this setup, so see the next section discussing those before you continue.
Setup Caveats
There are a couple key things to keep in mind when using prebuild and XCode Cloud together.
First - the Expo documentation has you add the ios/
and android/
directories to your .gitignore. XCode cloud requires (as far as I’m aware), the ci_scripts
folder to be a sibling of your .xcworkspace
/ .xcodeproj
folders. So, we either need to force-add the ios/ci_scripts/ci_post_clone.sh
below in git, or add it as an exclusion in the .gitignore file so it’s tracked.
Second - running an expo prebuild with --clean
removes ios/
and android/
folders. There are scenarios where if you run a clean prebuild locally, you might need to revert the deletion of the CI scripts in git. Or, add a custom package script which manages this for you. If you accidentally remove the script, you’ll need to re-add it for the XCode cloud build to work.
The CI Script
This is the ci script I use, ci_scripts/ci_post_clone.sh
. This is checked-in inside the expo-prebuild generated ios/
folder, even though the rest of the contents of ios/
are not.
If you need private modules, you can also use this script to generate a .npmrc
file, and inject the npm token you need via the XCode Cloud env variables.
ci_scripts/ci_post_clone.sh
#!/bin.bash
set -e
echo "Running ci_post_clone.sh"
# cd out of ios/ci_scripts into main project directory
cd ../../
# install node and cocoapods
brew install node cocoapods
# install node modules
npm install
# xcode cloud sets `CI` env var to 'TRUE':
# This causes a crash: Error: GetEnv.NoBoolean: TRUE is not a boolean.
# This is a workaround for that issue.
CI="true" npx expo prebuild
TestFlight
Once this is in your repo, all you need to do is configure your XCode cloud to link to your source control (in my case, Github). Then, you can set up a workflow, and add a post-build action to deliver to TestFlight.
Then, a push to Github will both build and push a TestFlight build out to your configured test group.