React Native, iOS, and Continuous Integration
Exploring building React Native projects with iOS code using Travis CI.

The series Building React Native Projects with Native Code: Part 1 explores the development workflow of React Native with Android code. This article fills the gap by exploring building React Native projects with iOS code.
The final solution to this article is available for download.
note: This article is more of a detailed recipe than a light read on the topic.
Assumptions
This article starts from the assumption that one:
- Is developing on macOS (so that iOS is even an option) using Xcode 10.1; by the time I finished this article, I was auto-upgraded to Xcode 10.1.
- Has just ejected Expo
- Has all the prerequisites documented in Developing With ExpoKit
Ejecting
First, let us review what happens under the hood when you eject from Expo:
- Adds expokit as Node.js dependency (reflected in package.json and yarn.lock)
- A number of keys are added to app.json (below)
- Creates an android folder (5.7MB)
- Creates an ios folder (216K)
note: One critical step in the ejection process is providing an unique bundle identifier for the application; usually in reverse-dns format, e.g., com.larkintuckerllc.reactnativescaffold
app.json
Sidebar into iOS Code Signing
Because we are working through a process of building an iOS application through continuous integration for testing on actual devices, we need to implement iOS code signing. However, unlike the more straightforward Android code signing process, the iOS code signing process is fairly complex.
note: Spent the better part of a day, trying to untangle this process; found the article iOS Code Signing & Provisioning in a Nutshell helpful.
Let us walk through the process:
iOS code signing requires an Apple Developer account and a paid subscription to the Apple Developer Program The Apple Developer Program includes an online tool, Certificates, Identities, & Profiles, that we will use to generate the resources to perform iOS code signing
The first step is to create two iOS certificates (one for development and one for production, aka., distribution); a certificate is a document that Apple issues to you. This certificate states that you are a trusted developer and that you are in fact, who you claim to be.
- We initiate the process by adding an iOS certificate from the Certificates, Identities, & Profiles online tool (first for Development > iOS App Development)
- Per the online tool’s instructions, we initiate the next step in the process by using the Request a Certificate from a Certificate Authority feature of macOS Keychain Access application; saving a certificate signing request (CSR) file. Under the hood, this generates a public / private key pair (also stored in the Keychain Access program) and includes the public key in the CSR file
- We upload the CSR file to the online tool to create and download an iOS certificate; simply double-click the downloaded iOS certificate to install it into the Keychain Access program.
- Repeat the steps to create a Production > App Store and Ad Hoc certificate; skipping step 2 and simply using the previously generated CSR file
note: One can think of an iOS certificate an Apple signed copy of your public key.
The next step is to generate an app id using the Certificates, Identities, & Profiles online tool. We select an explicit app id; using the bundle identifier we provided during the ejection process. We do not need to select any additional application services. As we can see, the generated app id consists of a prefix (team id), the bundle id, and a list of enabled / disabled application services (aka. capabilities).
Distributing iOS applications, outside of the Apple App Store, requires one to target a limited (100 or less) group of devices. As such, we need to define those devices in the Certificates, Identities, & Profiles online tool. The key to this step is to obtain the unique device identifier (UDID) for the devices as described in the article How to find your iOS device’s UDID.
We can manually create the two provisioning profiles (one for each of the two iOS certificates) using the Certificates, Identities, & Profiles online tool. A provisioning profile consisting of an iOS certificate, an app id, and a group of devices. At the same time, we can (and will at first) have Xcode dynamically create them. The two provisioning profiles are used to perform the iOS code signing (along with the entries in the Keychain Access application that we added, specifically, the private / public keys and the two iOS certificates built from them).
Manually Building Beta Application (Xcode GUI)
Before we attempt to automate building the beta application, we ensure that we can first manually built it using the Xcode graphical user interface (GUI).
We begin by running the following command in the ios folder to install all the Swift dependencies.
pod install
Observation:
- The first time one runs this command; expect it to takes some time to download 444MB worth of files
- Luckily, the CocoaPods dependency management system caches the pods; so that one only needs to download them once
- As we likely do not want to store these third-party dependencies in source control, we can add a recommended .gitignore file to the ios folder (uncomment the Pods line).
- While it is generally recommended to store the Podfile.lock file in source control (locks down Pod versions), I ended up also adding this to the .gitignore file. This is because I could never get Travis CI to consistently install pods with it.
Because the iOS application requires the Expo JavaScript bundle to run we need to publish the bundle using the command:
expo publish
In addition to publishing the bundles online, this command creates / modifies the following files (the bundles and configuration files describing where to find the bundles online):
- android/app/src/main/java/host/exp/exponent/generated/AppConstants.java
- android/app/src/main/assets/shell-app-manifest.json
- android/app/src/main/assets/shell-app.bundle
- ios/react-native-scaffold/Supporting/shell-app-manifest.json
- ios/react-native-scaffold/Supporting/shell-app.bundle
We next open the Xcode workspace file, e.g., ios / react-native-scaffold.xcworkspace, in Xcode. You will immediately notice signing errors; to resolve:
- Add an account using your Apple ID using Xcode > Preferences > Accounts
- Open Xcode’s Project Navigator; Xcode > View > Navigators > Show Project Navigator
- From the Project Navigator > General > Signing, select the account as the Team; this will automatically generate the necessary provisioning profiles
note: If one wants to run the application locally using an emulator, one first starts the bundle server (using the yarn start command) and the build and run the application using Xcode.
We finally, build the beta application by:
- Xcode menu, Product > Destination > Generic IOS Device
- Xcode menu, Product > Archive
- The generated archive is displayed; we click the Distribute App button
- We select the Development distribution method
- At this point we can choose all the defaults and end up saving a folder with the application file, e.g., react-native-scaffold.ipa.
At this point we can side-load the application file as documented in the article Non-market App Distribution.
Manually Building Beta Application (xcodebuild)
We next manually build the beta application using the Xcode command-line interface (CLI), aka, xcodebuild.
note: Here we assume that you already performed the pod install and expo publish command.
First, the documentation on using xcodebuild is scarce; did find the article xcodebuild: Deploy iOS app from Command Line helpful.
Following the instructions in the article, the first step is to create an archive with the following command:
xcodebuild \
-workspace react-native-scaffold.xcworkspace \
-scheme react-native-scaffold \
-sdk iphoneos12.1 \
-configuration Release \
-archivePath $PWD/build/react-native-scaffold.xcarchive \
archive
Observations:
- The $PWD environment variable is the absolute path to the current working directory
- The workspace option is the .xcworkspace file in the ios folder
- The scheme and configuration options is selected from the options listed when running xcodebuild -list (we want to use Release as Debug expects a local JavaScript bundle server)
- The sdk option is selected from the options listed when running xcodebuild -showsdks
- The process will ask for a password to unlock signing keys
- If all goes well, one will have a new folder: ios/build/react-native-scaffold.xcarchive
We can then create an export archive, .ipa file, using the command:
xcodebuild \
-exportArchive \
-archivePath $PWD/build/react-native-scaffold.xcarchive \
-exportOptionsPlist $PWD/build/react-native-scaffold.xcarchive/Info.plist \
-exportPath $PWD/build
Observations:
- The process will ask for a password to unlock signing keys
- If all goes well, one will have a new file: ios/build/react-native-scaffold.ipa
Getting Ready for Continuous Integration
We are pretty close, using xcodebuild, to being able to implement our builds through continuous integration; there, however, are a couple of problems:
- It is not clear which provisioning profile (development or production) is being used
- The build process is also dependent on the Apple ID account configured in Xcode; to dynamically create the provisioning profiles
- The build process is dependent on the iOS signing resources locked up in the Keychain Access application
We address the first two concerns by changing the project to manually sign:
- We go back and manually create the two provisioning profiles (one for each of the two iOS certificates — Development and Production) using the Certificates, Identities, & Profiles online tool.
- For now, we will only concern ourselves with the iOS Development provisioning profile; we go ahead and download it
- Open Xcode’s Project Navigator; Xcode > View > Navigators > Show Project Navigator
- From the Project Navigator > General > Signing, we disable the Automatically managing signing feature
- We then import the downloaded iOS Development provisioning profile for both the Signing (Debug) and Signing (Release) sections. Under the hood, this creates a copy of the file in ~/Library/MobileDevice/Provisioning Profiles
- For completeness, we remove the Apple ID account using Xcode > Preferences > Accounts
With this in place, add two variables to the end of the command to build the archive:
xcodebuild \
-workspace react-native-scaffold.xcworkspace \
-scheme react-native-scaffold \
-sdk iphoneos12.1 \
-configuration Release \
-archivePath $PWD/build/react-native-scaffold.xcarchive \
PROVISIONING_PROFILE="XXXX" \
CODE_SIGN_IDENTITY="XXXX" \
archive
Observations:
- The PROVISIONING_PROFILE is the unique id for it; the first part of the name of the file in ~/Library/MobileDevice/Provisioning Profiles (also is readable in the downloaded provisioning profile, search for UUID)
- The CODE_SIGN_IDENTITY is the Common Name of the Developer iOS certificate stored in the Keychain Access application
- The process still will ask for a password to unlock signing keys
One would think that one could use the same approach (adding the variables) to create an export archive, .ipa file. But the answer is no. Instead we need to add a provisioningProfiles entry to the generated file:
ios / build / react-native-scaffold.xcarchive / Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>compileBitcode</key>
<false/>
<key>provisioningProfiles</key>
<dict>
<key>com.larkintuckerllc.reactnativescaffold</key>
<string>XXXX</string>
</dict>
<key>ApplicationProperties</key>
...
</dict>
</plist>
Observations:
- We also add the flag to not compile bitcode, as later, not having this caused it to not export using Travis CI (unknown why)
- The key is the applications bundle identifier
- The string is the provisioning profiles unique identifier (UUID)
With this update, we can run the same command as before to create the export archive, .ipa, file.
xcodebuild \
-exportArchive \
-archivePath $PWD/build/react-native-scaffold.xcarchive \
-exportOptionsPlist $PWD/build/react-native-scaffold.xcarchive/Info.plist \
-exportPath $PWD/build
Observations:
- The process still will ask for a password to unlock signing keys
Now we need to solve for the process asking to unlock the signing keys; basically dealing with the resources locked up in the Keychain Access application, i.e.,
- The two iOS Certificates (Development and Distribution)
- The public / private development key pair
We will address this as we write our script for continuous integration (using Travis CI).
Continuous Integration (with Travis CI)
Having battled through the xcodebuild command-line interface locally, the next big challenge is dealing with the signing keys on Travis CI. Luckily, I found an excellent article that covers this topic, Continuous Deployment for iOS using Travis CI; which forms the basis of our approach here.
Let us walk through the the Travis CI configuration; line by line:
note: Testing Travis CI configurations is challenging as each iteration takes awhile (towards the end of writing the configuration, the executions took up to an hour to complete).
We start with a basic Travis CI configuration for Objective-C or Swift projects. We also enable caching of both the Yarn and Cocoapods.
.travis.yml
language: objective-c
podfile: ios/Podfile
osx_image: xcode10.1
sudo: true
cache:
- yarn
- cocoapods
...
We next need to gather the files with secrets that are needed for code signing and encrypt them before adding them to the repository. The necessary files are
- The provisioning profile (available in ~/Library/MobileDevice/Provisioning Profiles)
- A file with the development certificate including the private key (exported from Keychain Access as a .p12 file)
In this case, we follow the Travis CI instructions on Encrypting Multiple Files; resulting in the two additional lines.
.travis.yml
...
before_install:
- openssl aes-256-cbc -K $encrypted_91d26112053f_key -iv $encrypted_91d26112053f_iv -in secrets.tar.enc -out secrets.tar -d
- tar xvf secrets.tar
...
The next line in the before_install section installs a predictable version of Node.js (e.g., 8.12.0 LTS) using Node Version Manager. Followed by a line installing the Yarn Node.js package manager.
...
before_install:
...
- rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install 8.12.0
- brew install yarn --without-node
...
Observations:
- Installing NVM ended up being challenging as the Travis CI objective-c environment appears to have a partially installed version. Found an article, Change Travis Node Version, on addressing this.
The install section is a straightforward install of the Yarn and CocoaPod dependencies. The script section executes a custom script (we will examine it next).
...
install:
- yarn install
- cd ios && pod install && cd ..
script:
- sh ios.sh
We next create a custom script to execute all the script steps; we write this as a single script as we want the build to stop (and fail) if any single command fails. The first two lines of the script configure the script to echo the commands and stop (and fail) if any single command fails.
The remaining lines (thus-far), are used to publish the Expo bundle.
ios.sh
set -e
set -x# EXPO PUBLISH
npm install -g expo-cli@2.2.0
expo login -u $EXPO_USERNAME -p $EXPO_PASSWORD
expo publish
...
The next block is the details about using the signing keys as described in Continuous Deployment for iOS using Travis CI.
ios.sh
...
# CREATE KEY CHAIN
security create-keychain -p $CUSTOM_KEYCHAIN_PASSWORD ios-build.keychain
security default-keychain -d user -s ios-build.keychain
security unlock-keychain -p $CUSTOM_KEYCHAIN_PASSWORD ios-build.keychain
security set-keychain-settings -t 3600 -l ~/Library/Keychains/ios-build.keychain
# IMPORT ASSETS INTO KEY CHAIN
security import AppleWWDRCA.cer -k ios-build.keychain -A
security import development-cert.p12 -k ios-build.keychain -P $SECURITY_PASSWORD -A
security set-key-partition-list -S apple-tool:,apple: -s -k $CUSTOM_KEYCHAIN_PASSWORD ios-build.keychain > /dev/null
...
We next copy the provisioning profile to the folder that xcodebuild expects it in.
ios.sh
# PROVISIONING PROFILE
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "00e1eafa-154c-413b-ad49-8aaa90befb5e.mobileprovision" ~/Library/MobileDevice/Provisioning\ Profiles
Because the output of an iOS build is extremely verbose (Travis CI will stop the job for too much output), we install and use xcpretty.
We then execute three separate scripts:
- ios-build.sh: iOS archive build (as we did manually from the command line)
- ios-patch-plist.sh: Update the Info.plist file (as we did manually)
- ios-export.sh: iOS archive export (as we did manually from the command line)
ios.sh
...
# BUILD ARCHIVE
sudo gem install xcpretty
sh ios-build.sh
# EXPORT ARCHIVE
sh ios-patch-plist.sh
sh ios-export.sh
...
ios-build.sh
set -e
set -xcd ios && xcodebuild \
-workspace react-native-scaffold.xcworkspace \
-scheme react-native-scaffold \
-sdk iphoneos12.1 \
-configuration Release \
-archivePath $PWD/build/react-native-scaffold.xcarchive \
PROVISIONING_PROFILE="00e1eafa-154c-413b-ad49-8aaa90befb5e" \
CODE_SIGN_IDENTITY="iPhone Developer: John Tucker (WS374528YS)" \
archive | xcpretty && exit ${PIPESTATUS[0]}
ios-patch-plist.sh
sed -i.bak -E '/^\<dict\>/a\
<key>compileBitcode</key>\
<false/>\
<key>provisioningProfiles</key>\
<dict>\
<key>com.larkintuckerllc.reactnativescaffold</key>\
<string>00e1eafa-154c-413b-ad49-8aaa90befb5e</string>\
</dict>\
' ios/build/react-native-scaffold.xcarchive/Info.plist
ios-export.sh
set -e
set -xcd ios && xcodebuild \
-exportArchive \
-archivePath $PWD/build/react-native-scaffold.xcarchive \
-exportOptionsPlist $PWD/build/react-native-scaffold.xcarchive/Info.plist \
-exportPath $PWD/build
Finally, we need to put the .ipa archive somewhere we can get it, e.g., in this case we put it into an AWS bucket.
ios.sh
...
node ios-aws.js
ios-aws.js
const AWS = require('aws-sdk');const SRC_FILE_NAME = 'ios/build/react-native-scaffold.ipa';
const AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY;
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
const AWS_BUCKET = process.env.AWS_BUCKET;
const DST_FILE_NAME = 'react-native-scaffold.ipa';
const s3 = new AWS.S3({
accessKeyId: AWS_ACCESS_KEY,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
});
fs.readFile(SRC_FILE_NAME, (err, data) => {
if (err) { throw err; }
var base64data = new Buffer(data, 'binary');
s3.putObject({
Bucket: AWS_BUCKET,
Key: DST_FILE_NAME,
Body: base64data
}, (resp) => {
console.log(`Successfully uploaded ${DST_FILE_NAME}`);
});
});
Wrap Up
Am guessing that very few of you actually make it down to this point in the article; if you did I am guessing that you found something here useful.
Cheers.