Running iOS builds — Part 3, React Native DevOps Guide

Tyler Pate
16 min readSep 26, 2018

--

TLDR: Configure a React Native iOS project for three build types: development, staging, and release. Customize build behavior for repeatability in Xcode, then streamline flows using Fastlane. Use Jenkins to automate Fastlane build flows.

Intro | Setup | Pre-Build | iOS | Android | Jenkins | CodePush | Testing

Synopsis

Concepts — Xcode

  • Build types and destinations
  • Keys and profiles
  • Managing Xcode
  • Configurations
  • Schemes
  • Build settings
  • Build phases
  • New build system

Concepts — Fastlane

  • Lanes
  • Options
  • Metadata / App Store
  • Jenkins

Walkthrough

  • Create staging configuration
  • Create dev, staging, and release schemes
  • Edit build options and phases
  • Review signing config
  • Add Fastlane lanes, options
  • Add iOS Fastlane metadata
  • Create Jenkins jobs
  • Test jobs

System overview

Concepts — Xcode

Build types and destinations

Apple sure loves to complicate app development. As touched on in Part 1 of the series, there are a lot more named build types for iOS than Android.

The main driver of complexity is provisioning profiles.

Each profile restricts the number of eligible devices that can run your app. Release builds can only run on device, and they must be installed from beta or prod app store. Development builds target either a device or a simulator, and the mandatory profile also restricts eligible hardware device targets by UUID. Adding a new hardware device to the profile involves an annoying trip to developer.apple.com to update and download a new profile.

If you have three different build configurations (dev, staging, release), then you will end up having five different build configurations to support simulators and physical devices. If you use CodePush and have two destination keys, then your number of build configurations will grow.

  • Dev: simulator, device
  • Staging: simulator, device
  • Release: device

Fastlane can simplify complexity using CLI arguments to specify binary destinations. xcodebuild, a built-in fastlane action, interfaces with the xcodebuild command line tool.

In the code below, we define a simulator variable whose value is set by the environment, a command parameter, or a default value. simulator is used in a ternary expression as a parameter to the xcodebuild action, allowing us flexibility in choosing the app destination.

// Fastfilesimulator = handle_env_and_options(ENV["SIMULATOR"],
options[:simulator], false)
xcodebuild(
...
destination: (simulator ?
"generic/platform=iOS Simulator" :
"generic/platform=iOS"),
),
...
)

Keys and profiles

Match automates creation, storage, and updating of iOS provisioning profiles and signing certificates. All files generated by Match are then stored in an encrypted git repository, with a decryption key shared by the development team.

If you haven’t set this up for your team yet, see the Match tutorial to get started.

Match is used in our iOS lanes to verify validity of profiles and certificates, and to correctly specify the key and profile used for a given build. Credential name strings are used in the xcodebuild action to select a desired credential type.

Managing Xcode

Next we’ll take a look at Xcode internals, and how Xcode flows fit into the overall picture of our RN project. Here are two excellent resources that dive into Xcode internals at a deeper level.

Configurations

Configurations manage a group of variables in Xcode on a per-build basis. In our application, configurations match 1:1 with the build types we create for Fastlane builds.

Out of the box, the RN starter Xcode project comes with Debug and Release configurations. We will add Staging config and modify a few parameters for each.

With Staging added, we will have build configurations that match our three major build types. These cross-reference the env files created in part 2 of the tutorial. We will duplicate this setup for the Android tutorial, part 4.

Schemes

Schemes define what steps xcodebuild takes to produce a final product. Build targets can go through the following steps during build: analyze, test, build, run, profile, and archive.

myCoolApp-staging Scheme

We can decrease our build times, especially for dev and staging builds, by reducing the number of these steps used in each build. If your project is mainly written in Javascript, you may even be able to get away without test / analyze / profile for release builds as well.

In the scheme build tab, uncheck any steps you want to skip. I am only using run and archive steps for most builds. Note, this is a project dependent choice — if you are using a lot of Swift / Obj-C, you will probably want to include analyze / test / profile.

Speed optimized build scheme

Build settings

User defined build settings allow us to set variables based on a given configuration. There are a few must-add options for RN projects, and you will probably have additional options beyond these.

While adding the staging configuration + scheme, we will also have to adjust build settings for each variable to make sure we generate the correct product. Here’s a list of important variables you will want to take a look at after adding a new scheme + configuration.

Build locations: built products path, intermediate build files path

Build options: validate build product

Linking: dead code stripping

Signing: code signing identity, development team

Apple LLVM 9.0 — code generation: optimization level, symbols hidden

Apple LLVM 9.90 — preprocessing: preprocessor macros

Search paths: framework, header, library

Packaging: product name

User-Defined: MTL_ENABLE_DEBUG_INFO, SWIFT_OPTIMIZATION_LEVEL, bundle ID suffix

Adding a bundle ID suffix is a must-do for testing and validation. By adding bundle ID suffixes, you can install dev, staging, and release versions of your app side-by-side on the same phone or simulator. Without the suffix, you are limited to one build of the app at a time.

You must also edit Info.plist, adding ${BUNDLE_ID_SUFFIX} to the Bundle Identifier key. See this post by Chris Miles for more information.

User defined build setting — bundle ID suffix. Allows multiple builds on the same phone.

Build phases

The last piece to discuss is build phases. Build phases allow you to define pre- and post-build actions, like bundling JS, managing Cocoapods, uploading debug symbols to a tracking provider, and more.

There are three important changes in the Bundle React Native code and images step.

  • Set a known shell for predictable behavior — use the shell you run CLI operations with.
  • Source the custom node executable path
    source “${SRCROOT}/../env/node_binary
  • Custom react-native-xcode.sh

Xcode calls react-native-xcode.sh as part of the Bundle React Native code and images step. This script expects only two build configurations / schemes, development and release.

To ensure JS is bundled properly in staging, create a custom script react-native-xcode-custom.sh and put this file in a sub-dir of your project root. Edit the file to support staging configuration / scheme (alternatively, clone from the example repo).

A basic app’s bundle phase will look like this:

Updates to bundle step in build phases

New build system

Apple recently debuted a new build system, included in the latest versions of Xcode (since Nov 2017). There are reports of improved build times. I have not attempted this build system with a RN project yet. If you are curious, check out this repo for more information.

Concepts —Fastlane

The main goal of iOS lanes in our fastfile is to manage complexity and create a repeatable build process. We will continue using dev / staging / release as the major build types. With additional build targets brought on by provisioning profiles, we will also create lane options that specify a build for simulator or device.

At a high level:

  • Xcode provides ~infinite configurability
  • Fastlane forces repeated patterns when interacting with Xcode, reducing the number of options, but still provides a high configurability
  • Jenkins limits Fastlane configurability through parametric jobs. Options should be rarely changed.

Fastlane phases

For each build, fastlane runs through the following phases:

  • parse options
  • prebuild: badge, match, cocoapods
  • build: xcodebuild, gym
  • postbuild: install to simulator, deliver, webhook

Lanes

There are 3 major lanes for iOS, potentially more if you use CodePush in your project.

  • dev: debug config, simulator or device, debug = true
  • staging: staging config, simulator or device, debug = false
  • release: release config, device, debug = false, suitable for app store

Options

Options let us adjust lane variables at execution time. This is a critical component of interoperability with Jenkins. Options, passed into fastlane CLI calls via Jenkins build parameters, will let us produce a variety of products from the 3 main iOS lanes listed above.

I use the following options:

  • badge (bool): generate badge overlay with version, revision, and dev or beta flags on the app image
  • simulator (bool): target the product to run on a simulator
  • clean (bool): clean project workspace before xcodebuild actions
  • xcargs (string): any additional build-time cli arguments passed to xcbuild
  • install (bool): install app to running simulator as a post-build action
  • app_store (bool): submit app to iTunes Connect after successful release build
  • hook (bool): send a webhook success/fail message to defined webhook endpoint (in fastfile)
  • channel (string): define the channel name for Slack webhook integration

App Store Metadata

In the Fastlane boilerplate and project repo, you will find pre-generated Metadata files for working with the App Store. Edit Appfile and Deliverfile and update with your project specific information.

Deliverfile stores all metadata and associated information for uploading to the Play Store. You’ll want to update all of this information before you start using Fastlane to deliver complete builds to the App Store, as it can save you time with manual data entry on iTunes Connect.

Fastlane + Jenkins

Now that we have lanes and options to further control the behavior of each lane, we will take advantage of this setup to execute Jenkins jobs. In addition, we can aggregate multiple fastlane operations into a single Jenkins job, streamlining build flows.

By moving from a basic job to a parametric job, we can now pass in job parameters through environment variables when calling fastlane.

fastlane ios staging xcargs:$XCARGS simulator:$SIMULATOR

When triggering a job, click Build now with parameters , and adjust the build to your liking.

Using parameterized build to modify fastlane options at execution time

It does not make sense to include every single option for every job you create. Most of the time, builds don’t need to be parameterized, especially after you have established a consistent flow.

Let’s take mobile-staging-simulator as an example job. Here, we will trigger an iOS and Android build, targeted for the simulator, with a staging configuration. There’s not a real need for parameterization in this job. The build step will look like this:

#!/usr/local/bin/zsh
source $HOME/.zshrc
fastlane ios staging simulator:true
fastlane android staging

Here’s a few example fastlane aggregations to build with this technique. We will dive into getting the most out of your Jenkins agent in part 5.

  • mobile-scheduled-compile: dev build, android + ios, verify binary product exists
  • mobile-nightly: staging build, android + ios, device target, archive build products, install to phones on dedicated nightly desk for employees to test
  • mobile-staging-simulator: staging build, android + ios, simulator target
  • mobile-staging-device: staging build, android + ios, device target
  • mobile-prod-binary: release build, android + ios, upload to respective stores

Walkthrough

I recommend cloning the companion repository for comparison and reference while you work through this tutorial. The sample project used here is called myCoolApp.

  1. Create staging configuration

Open your project in Xcode. Click on the project name in the Navigator pane, navigate to the Info tab, and highlight your project name from the sub-navigator pane. Press the + sign, and click on Duplicate "Release" Configuration. Rename the configuration to Staging.

Duplicate release configuration for staging builds

2. Create dev, staging, and release schemes

Drop-down the Product menu from the status bar, select Scheme , and click on Manage Schemes located at the very bottom of the scheme list.

You will see the default project scheme, which is the same name as your project. Highlight the scheme, and click duplicate. Rename the scheme to myCoolApp-staging. Repeat for myCoolApp-release.

Using duplicate scheme to create myCoolApp-staging and myCoolApp-release

Now, we will modify all three of the schemes. Start with the debug / development scheme, myCoolApp (not renamed, you can if you like). Highlight the scheme and click edit.

Scheme editor dialog

For each of the phases — Run, Test, Profile, Analyze, and Archive , ensure the Build Configuration dropdown selection matches the configuration.

We won’t use all of the phases for each scheme. If you need to add one of these phases later on, it’s good practice to set them all to the same configuration now.

Now, click on the Build menu in the left navigator pane. For the debug / dev scheme, I prioritize speed of build over completeness. Therefore, I will only use Run during this scheme.

Also, note that I removed myCoolAppTests from participating in the build steps. See the concepts section above for a more in-depth discussion on the tradeoffs using this method.

In short, if you have a project based primarily in JS land, I find the analyze, test, and profile steps add time without a ton of benefit. If you have a Swift / Obj-C heavy app, then you may get more value out of including these steps.

Speed optimized build step

When you are done, click close , and return to the scheme editor. Repeat the same flows for myCoolApp-staging and myCoolApp-release . Click on myCoolApp above the left navigator pane as a shortcut to access the other schemes.

3. Edit build settings

Return to the project editor. In the left sub-navigator pane, click on myCoolApp under Targets. Then, select the Build Settings tab from the top menu. Click on All and Combined in the tab menu. Now, we will make changes to build variables based on scheme selection.

Editing Build Settings

All of the variables can be modified per build configuration. If all three of the values are the same, there will be no visible drop-down arrow to the left of the variable name. If you mouse-over the left side of the variable name, a drop-down arrow will appear so you can make configuration-specific changes.

I’ll include the search term in bold, and recommended changes following.

  • Build Locations: ensure everything looks as expected for staging and release. Per-config Build Products Path for Release should look something like Build/Release-iphoneos.
  • Validate Built Product: Set to NO for Debug and Staging. This is a speed optimization.
  • Dead code stripping: Set to YES for Staging.
  • Signing: Set the code signing identity for Staging to match Debug. If you haven’t yet set up your signing configuration, you can come back to this step. We will cover it later.
  • Code generation: Set Optimization Level to match Release for Staging configuration. I also set Symbols Hidden by Default to YES for Staging.
  • Preprocessor macros: Edit Staging to match release. This will remove the debug flag. To change, double-click on the value and delete $(inherited).
  • Search paths / Framework: Edit all three configurations at once by double-clicking on the value next to Framework Search Paths. Add $(PROJECT_DIR) to the list with the plus button at the bottom left as a non-recursive search.
Add $(PROJECT_DIR) as a recursive search for framework search paths

Important change for Staging configuration
We need to do a little hack to get the Framework search paths working properly for the staging configuration. Double click on the value next to Staging under Framework search paths. Use the plus sign to add $(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME) as a recursive search.

This change is required for interoperability with third party modules built for React Native. As many do not have support for a Staging configuration, the staging build will put important frameworks in a directory under Release. Therefore, by changing the framework search path to look in the release directory, we can link everything properly even while using third party modules.

Now, the value next to Framework search paths should read <Multiple values>.

  • Search paths / Header: Add $(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME)/include as a non-recursive search to Staging search paths.
  • Search paths / Library: Add $(BUILD_DIR)/Release$(EFFECTIVE_PLATFORM_NAME) as a non-recursive search to Staging search paths.
  • Product name: Modify the product name to match the build configuration.
    Important note: if you do use the built-in tests with Xcode, you will also need to modify the test product name targets as well. When building Debug without a modified test target product name, the build will fail as Xcode cannot find myCoolApp. The product name is now myCoolApp-debug, but the test target is still looking for myCoolApp.
Renaming the product according to scheme / configuration
  • MTL_ENABLE_DEBUG_INFO: Set Debug = YES, Staging and Release = NO
  • Optimization level: Modify the staging key to match the release key, Fastest, Smallest [-Os]
  • BUNDLE_ID_SUFFIX: Clear any existing search terms, and scroll to the very bottom of the Build Settings tab. Press the + sign in the filter row, to the left of the search box at the top of the page. Add BUNDLE_ID_SUFFIX as a new variable, and add values for Debug and Staging.
User defined build setting — bundle ID suffix. Allows multiple builds on the same phone.

4. Review signing config

Navigate to the General tab, with myCoolApp as the target in the left sub-navigator pane. By this point, you will need to have Match set up to manage your certificates and profiles, as well as having a shared encrypted git repository.

Uncheck Automatically manage signing, and confirm your action. Now, modify Provisioning Profile and Signing certificate for each of the three build types. Only Release should use the release profile and certificate.

5. Edit build phases

Navigate to the Build Phases tab, with myCoolApp as the target in the left sub-navigator pane. Click the drop-down arrow next to Bundle React Native code and images.

First, change the shell to a familiar shell. I use zsh across all of my systems, so I am also using it here. Next, source in the custom node executable you defined in part 2 of the series. This will load in the NODE_BINARY environment variable, so we are using node set by NVM.

Finally, we will use the custom bundling script, react-native-xcode-custom.sh, which adds support for Staging build scheme. You can edit and save this file on your own, or copy it from the companion repository.

Updated bundle script call

6. Edit Info.plist

In the far left navigator pane, open the myCoolApp folder and double click on Info.plist.

  • Edit Bundle display name and change it to $(PRODUCT_NAME).
  • Edit Bundle identifier and change it to $(PRODUCT_BUNDLE_IDENTIFIER)$(BUNDLE_ID_SUFFIX)

Save and close the file.

7. Run test builds

Use the scheme navigator at the upper left hand side of the top status bar to select the desired scheme. Run builds of all three schemes, verifying that the built product is functional and all search paths are matched up properly.

If you are having build issues, re-visit step 3 and ensure all of your modifications are correct.

Scheme navigator drop-down

8. Integrate with Fastlane

At this point, you should have been able to produce 3 different builds through Xcode. Now, we will take this flow and move it to the CLI with Fastlane.

Clone the react-native-fastlane-boilerplate and place the repo in your project root directory. Remove the .git subfolder if you want to avoid managing a submodule.

Edit fastlane/Fastfile, changing constant values to those that match your project. Also, edit Appfile, Deliverfile, and Gymfile to match your project values and paths.

Alternatively, look at the example repo react-native-dev-ops-guide and update your own Fastlane implementation to match the flows we are building here.

Assuming you have followed the tutorial so far, you should be able to run a few Fastlane tests based on the boilerplate with minimal changes.

Before moving on to the Jenkins steps, make sure you are successful in running the following commands from the CLI.

fastlane ios dev clean:true simulator:true

fastlane ios staging clean:true simulator:true

Extra features

  • Install the built product directly to your simulator.
    Open a iOS Simulator and leave it running, then run the command below. This will uninstall any bundles with a matching bundle id, then will install and launch your newly built binary.
    fastlane ios dev clean:true simulator:true install:true
  • Badge the app icons before building
    Add the badge:true flag to your build command. Your icons will be badged with either dev or beta, depending on if you run a dev or staging build.
    fastlane ios dev clean:true simulator:true install:true badge:true

Troubleshooting notes

  • If you do not have a paid developer account, you will not be able to generate a provisioning profile to run your app on a device. Make sure to use simulator:true flag when building to avoid errors.
  • Ensure you have correctly updated constant values in fastlane/Fastfile to match your project.
  • Make sure all paths are correct, including case sensitivity. You may have a custom built products path, set at the system level in Xcode preferences, and also at the project level in workspace settings.

9. Create Jenkins jobs

Now that we have Fastlane running our Xcode builds through the command line, we can port the same functionality to our Jenkins agent!

We will create three jobs:

  • mobile-compile: verifies latest code can be built successfully
  • mobile-staging: produces a staging build (will extend functionality to auto-installs on phones later)
  • mobile-production: produces a release build and uploads to app store

For each:

  • create a new job
  • set up github access
  • set as a parametric job, so we can pass in fastlane parameters
  • ensure the workspace is deleted before the build starts (start with fresh node modules every time)
  • add an Execute shell step to the build phase

Now, edit the shell script for each job to match the following:

#### mobile-compile #####!/usr/local/bin/zsh
source $HOME/.zshrc
fastlane ios dev clean:$CLEAN simulator:$SIMULATOR#### mobile-staging #####!/usr/local/bin/zsh
source $HOME/.zshrc
fastlane ios staging clean:$CLEAN simulator:$SIMULATOR#### mobile-production #####!/usr/local/bin/zsh
source $HOME/.zshrc
fastlane ios release app_store:$APP_STORE

Make sure you have each of the parametric variables set for each job. For the release build, you should the $APP_STORE variable default to false, just for safety. This way you won’t accidentally upload a release build before you are ready.

10. Test Jenkins jobs

Now, test each of the jobs. Click on Build with Parameters , and run through a full build of each job type.

If you have issues with a build type, debug the issue until you get consistency in your builds.

Note, fastlane loves to regurgitate the last X lines of a build log after encountering an error. You may need to do some digging in the console logs to find the exact build error you encountered.

If you are still having problems, feel free to leave a comment and we can take a look together.

Our first successful Jenkins build!

Once your jobs run successfully, take a deep breath and give yourself a pat on the back! Moving build operations to Jenkins is time consuming, but will pay off in the long run, especially as your team grows and your app gets more sophisticated.

Next up, we’ll do the same treatment for Android in part 4.

--

--

Tyler Pate

Former Lead Mobile Developer @ Earn.com, acquired by Coinbase. Stuff I love: React Native, DevOps, JS, 3D Printing, Skiing, Motorcycles, Music