A 12-Factor Approach to Environment-Specific Builds in React Native

Chris Ball
Echobind
Published in
7 min readAug 7, 2018

--

Having separate builds for dev, beta, and production environments is critical for most apps.

These environment specific builds give us a way to:

  • Change the values of variables at build time
  • Change app/bundle ids to allow the installation of any environment variant on the same device at the same time
  • Change the icon for each build variant
  • Change the display name of the app

Let’s look at how to do that in React Native!

There are two main ways to configure environment-specific builds in your app, and neither of them are the following approach:

// development
const API = 'http://localhost:4000';
// staging
// const API = 'https://my-staging-server.com';
// production
// const API = 'https://my-prod-server.com';

The traditional (manual) approach

The first way to set up environment-specific builds involves usingschemes and build configurations in Xcode, and buildTypes or productFlavors for Android. You then have to set up user-defined variables in Xcode and create separate .properties files for Android. But guess what? You now have to manage the values in both places.

It would be nice to have a single place to do this, and ideally a way to do so without touching native code.

The 12-factor approach

At Echobind, we follow the 12-factor approach for all of our web and mobile apps. Using this approach, you create a single version of your app configured differently via environment variables. We see multiple wins by taking this approach in React Native:

  • It’s harder to accidentally build and submit a release of the wrong type and for example, submit a beta build to the App Store.
  • There aren’t multiple configurations to manage differently (one for iOS and one for Android)
  • If you .gitignore your .env files, developers don’t have access to staging or production configurations by default, which increases security.

In React Native, we can gain access to ENV variables by using react-native-config.babel-transform-inline-environment-variables is an alternative option, but it doesn’t currently work in the App Center environment. We use App Center on a lot of client projects (expect an App Center specific post from us soon!), so we stick with react-native-config. It requires a bit more setup, but you also get the added benefit of having access to environment variables in native code, and configuration files if you need them(such asInfo.plist on iOS and strings.xml on Android).

react-native-config expects an .env file which defines all the environment variables used in your app. We strongly believe that .env files should not be checked into source control, so we needed a way to provide one to our CI server.

.env on CI

Most CI’s support build scripts to run custom commands at various points in the build pipeline. We can leverage these scripts to create a .env file for us. Here’s what an exampleappcenter-pre-build.sh script looks like, which creates an .env dynamically:

#!/usr/bin/env bash# Creates an .env from ENV variables for use with react-native-config
ENV_WHITELIST=${ENV_WHITELIST:-"^RN"}
printf "Creating an .env file with the following whitelist:\n"
printf "%s\n\n" $ENV_WHITELIST
set | egrep -e $ENV_WHITELIST | egrep -v "^_" | egrep -v "WHITELIST" > .env
printf "\n.env created with contents:\n"
cat .env

This script takes a configurable whitelist of ENV variables (defaulting to anything with an RN prefix), and uses set and egrep to filter available ENV variables down to only the variables that match that whitelist regex. The output is saved to an .env file which react-native-config reads before building our app.

Changing variables at build time

With react-native-config setup, changing your API server or any other variable is now just a matter of:

  • Using Config.ENV_NAME in place of a hardcoded value in your app code.
  • Updating your local .env file to have a sensible default.
  • Adding an ENV variable to your CI server with the beta or production value.
  • Informing your team members. We like to keep an .env.example in the project root that contains all the variables (without real values) that need to be set.

Change the bundle / app id for each environment

Each app has an id assigned to it (typically com.yourcompany.AppName). This is called a bundle identifier on iOS and an app id on Android. If the beta build of your app and release build of your app share an id, you can’t install both at the same time on the same device. To get around this, we dynamically assign a different id at build time.

We need a way to add a suffix to the id for each release type:

com.yourcompany.AppName
com.yourcompany.AppName.alpha
com.yourcompany.AppName.beta

Tools like PlistBuddy and Gradle scripts can help with this, but we’re trying to do things cross-platform in a unified way. Enter fastlane. To setup fastlane, follow the Getting Started section in the guides.

To start, we’ll create a basic lane that will be used to update the bundle / app id. Create a fastlane folder with the following 2 files in the root of your project:

`fastlane init` will complain about finding an ios project in a subfolder, so just create these manually.

Update Appfile with the app_identifier and package_name that your app uses. This will avoid prompts from fastlane to request these values.

Next, add the magic. fastlane comes bundled with an ios plugin called update_info_plist that we will use to update the app_id. On Android, we’ll leverage a built-in concept called applicationIdSuffix and install a plugin that does the same thing. Add the following a line to android/app/build.gradle, and set it to an empty string by default.

After setting up react-native-config, this is the only “native” change you’ll have to make

Next, install the set_value_in_build plugin:

As of August, 1, 2018, this plugin doesn’t properly update values that contain empty strings (I have an open PR that fixes it). In the meantime, if you want to follow the same process here, you’ll need to point to a fork.

Update thefastlane/Pluginfile that was created when you installed the plugin to point to the fork:

# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!
gem 'fastlane-plugin-android_versioning', github: "cball/fastlane-plugin-android_versioning", ref: "support-blank-id-suffix"

Run bundle install to install the updated gem.

To use these plugins, update your Fastfile to the following:

In this lane, we provide a configurable release type. We typically use alpha, beta, and production, but additional types can be added by changing the environment variable.

We then set a customizable suffix, which defaults to the release type.

Time to test it out! Run the following command from your project root. The use ofbundle exec is important to ensure we execute the command in the context of our project Gemfile.

RN_RELEASE_TYPE=beta bundle exec fastlane prep_release_type

You should see the following updates:

ios changes
android changes

Remember, this is designed to be run on CI and discarded after build, so it’s fine that these values get overwritten.

Change the icon for each environment

Now that we’re able to install the different builds side-by-side we need to make the app icon visually distinctive so we know which one we’re opening.

To do this, we’ll make use of a great plugin for fastlane called badge.

If you don’t already have Imagemagick installed, do that first (brew install imagemagick on a mac).

Next install the badge plugin:
fastlane add_plugin badge

Insert an add_badge command to the existing lane for each platform. If you have a typical icon setup, the following should just work for you. Otherwise, you may have to adjust the glob line for each platform.

Now, we can re-run our previous command to produce some slick looking badges!

RN_RELEASE_TYPE=beta bundle exec fastlane prep_release_type RN_RELEASE_TYPE=alpha bundle exec fastlane prep_release_type

Example of our beta icon
Example of our alpha icon

Note: Like the app id changes, it will modify the icon in place. If you’re testing this out locally, just discard the changes.

Changing the display name

For iOS, we just have a minor update, but to handle Android we need to install another plugin to update our strings.xml file:

fastlane add_plugin update_android_strings

Now we can update our lane to change the display name on both platforms:

Environment-specific builds for the win

Welcome to a scalable, secure way to set up variables, icons, display names, and bundle suffixes for different environments your app needs. You should be able to set up any CI to run your Fastlane lane, overriding the defaults via ENV variables as needed.

Here’s our final Fastfile for reference:

Need help with your React Native app?
Say hello! We’d love to chat.

--

--