April 28, 2019 Félix Mézière 17 min read

How is this tutorial different?

It's a modus operandi.

This tutorial will make your React Native deployment pipeline completely operational in 1 hour for iOS and Android.

Other tutorials do a general presentation that lets you figure out the details or a zoom-in on a particular part of the pipeline or a deep-dive on every component which would take too much time to read and require you to join the bits together to make the deployment pipeline.

You can find such articles with this Google search.

In this tutorial, you won't have to open Xcode nor Android Studio, not even once.

It is based on a generator

In this tutorial, you will make heavy use of the awesome generator-rn-toolbox . Thanks @Theodo and @BAM ;)

This will maximise setup speed, reduce bug surface and ensure up-to-date standards when you create your deployment pipeline.

It is designed to be run on existing project (whether it's a 3 years old project or a fresh react-native init awesomeProject doesn't matter)

The resulting deployment pipeline is super customisable

There isn't too much magic. After this, you will have a skeleton with the time-consuming essentials setup but which is very easy to:

tweak to match your workflow: all the tools are in place, you can now re-order/tweak them (for example: CodePush to all versions higher than a constant, instead of CodePushing to currently deployed version only).

extend with extra features (like adding crash reporting, automating further what gets uploaded to the stores, end-to-end testing, hooks etc.).

What if you don't understand the moving parts of this deployment pipeline (e.g. the infamous Apple code signing) or its day-to-day workflow?

Get more insights with our in-house detailed docs on how this works here. This is quite opinionated to fit our needs though.

If you still don't understand something, Google that part specifically until you feel confident, then come back here :-)

This React Native deployment pipeline is meant for you to customise: it is a way for you to learn how all the tools can work together. I expect you to get your hands dirty after setting it up to tweak it to your exact needs.

The stack

Why a CI server instead of VS AppCenter for builds?

Among other problems, Appcenter Build is slow, not very customisable and doesn't handle state-of-the-art shared code signing with match .

One of its biggest issues is also that it's hard to debug: the build tools and scripts used are different than the ones used to build locally and it's not possible to ssh into the distant build machine.

We love Appcenter Releases and CodePush though, that's what we'll use here for deployment hosting!

At Theodo, we chose CircleCI because it's performant, widely used accross web and mobile (which makes sense since React Native devs often come from the ranks of Web devs) and is very customisable. Lots of CI tools are compatible with Fastlane so feel free to swap this one.

Workflow that you will get once completing this

Everytime you push a branch, the node job that runs jest will be triggered. Use this as a hook for branches in Github.

job that runs jest will be triggered. Use this as a hook for branches in Github. If the branch is staging or production , once node deploys with CodePush and passes, two ios and android jobs will be triggered in parallel to do hard deployments (Hard deployment: do a full build of the app, native code included, and deploy it to Appcenter/the Stores. Soft deployment: use CodePush to update the Javascript code of the app live.)

or , once deploys with CodePush and passes, two and jobs will be triggered in parallel to do hard deployments (Hard deployment: do a full build of the app, native code included, and deploy it to Appcenter/the Stores. Soft deployment: use CodePush to update the Javascript code of the app live.) When you want to change the version of the hard-deployed app, change the values in fastlane/.env

At the moment, CodePush is setup to target the app version defined in fastlane/.env

The best workflow with CodePush is obtained by doing pushes to all versions greater than a specified version, fixed in .env . This workflow is not implemented in the generator but feel free to implement it yourself as an exercise to test your understanding of the workflow :-)

Prerequisites

Your React Native project running locally

Admin rights on your project's git repo

An empty git repository for match or an existing match repository linked to your Apple Dev Portal account ( match is used to make Apple code signing a breeze).

or an existing repository linked to your Apple Dev Portal account ( is used to make Apple code signing a breeze). Admin rights on your Organization/account on Appcenter

Admin rights on your Apple Developer Portal

(iOS prod only) Admin rights on your Appstore Connect account

(Android prod only) Admin rights on your Google Play Console account

A CircleCI macOS plan

Steps

⚠️ FRIENDLY ADVICE ️

Remember to commit after each step

1. Create the staging apps in Appcenter

Get your project name (as in react-native init <projectname> )

) In Appcenter, making sure you use your "Organization" if necessary (instead of your own account):

Create staging apps <projectname>-ios-staging and <projectname>-android-staging (configured for React Native and iOS/Android).

and (configured for React Native and iOS/Android). Create production apps <projectname>-ios-production and <projectname>-android-production .

and . Make sure there is a single Staging CodePush deployment in the 2 staging apps found at Release -> CodePush -> Create standard deployments -> manage. Copy the two deployment keys somewhere.

CodePush deployment in the 2 staging apps found at Release -> CodePush -> Create standard deployments -> manage. Copy the two deployment keys somewhere. Make sure there is a single Production CodePush deployment in the 2 production apps. Copy the 2 deployment keys somewhere.

CodePush deployment in the 2 production apps. Copy the 2 deployment keys somewhere. (Only if you want to use Appcenter analytics and crash reporting) For each of the 4 apps, copy the Appcenter App Secret somewhere.

(We create different apps for staging and prod in appcenter mainly because appcenter analytics + crash reports are per-app).

‌

2. Setup Fastlane

npm install --global yo gem install bundler yarn global add appcenter-cli generator-rn-toolbox appcenter login yo rn-toolbox:fastlane-setup

Answers

Please confirm the project name: <Press Enter>

Commit keystore files?: n

Would you like to use an encrypted archive to store secret files and keys?: Y

Would you like to use a deployment script?: Y

Overwrite : <Press Enter>

‌

3. Create staging environment

yo rn-toolbox:fastlane-env

‌

Answers

Please confirm the project name: <Press Enter>

The name for this new environment (lowercase, no space): staging

The name of your repository Git branch for the environment just set: staging

The name of the company which will be publishing this application: <your company>

Which platform will you use for React Native deployment?: AppCenter

The app name for this environment: <Your App Staging>

The App Id for this environment: <your>.<company>.<projectname>.staging

The type of certificate you will be using: Adhoc

Your git repo for match: git@github.com:<TeamOrg>/<match-repo>.git (<- this is your match repo mentioned earlier)

(<- this is your match repo mentioned earlier) The branch you want to use for match: <Press Enter unless using a different branch than master for match>

The developer.apple.com team id for the certificates: XXXXXXXX (<- find this in the url when on "Account" section of your Apple Dev Portal)

(<- find this in the url when on "Account" section of your Apple Dev Portal) Your apple id (should be admin on the Apple Developer Portal): you@example.com>

Your keystore password: <Press Enter>

Will you deploy with Appcenter CodePush on this environment? (y/n): Y

A valid App Center Username: <the-client's-Organization> (<- find in the url when on the app in the AppCenter interface. It's the username for a person or the name of the Organization for an Organization)

(<- find in the url when on the app in the AppCenter interface. It's the username for a person or the name of the Organization for an Organization) A valid App Center API token: XXXXXXXXXXXXXXXXXXXXXXXXXX (<- make sure the token only has access to your Org's apps. Google how to get it if needed).

(<- make sure the token only has access to your Org's apps. Google how to get it if needed). The iOS project id on AppCenter for this environment, should be different than Android and not contain spaces: <iOS staging Appcenter app name created in step 1.>

The Android project id on AppCenter, should be different than iOS and not contain spaces: <Android staging Appcenter app name created in step 1.>

Your iOS CodePush deployment key: <iOS Staging CodePush deployment key>

Your Android CodePush deployment key: <Android Staging CodePush deployment key>

Your iOS CodePush deployment name: Staging

Your Android CodePush deployment name: Staging

Will you be using Appcenter Analytics and Crash reporting on this environment?: <Up to you :). You are probably better off using Firebase and Sentry for this stuff, but it will require more work.>

(If yes above) Your AppCenter app secret for the iOS App: <staging iOS app secret>

(If yes above) Your AppCenter app secret for the Android App: <staging Android app secret>

(After some npm installation…) Should fastlane modify the Gemfile at path 'xxx' for you? (y/n): Y

‌

4. Create iOS staging deployment certificates and profiles

This creates all the certificates and profiles required for your app in the Apple Dev Portal and installs them in your local environment.

bundle exec fastlane ios setup --env = staging

You will be asked twice for a match passphrase for the repo. If you are using an existing match repository, ask for the passhphrase, otherwise define it here and note it somewhere: this is the password to share with teammates so that they can get the iOS code signing identities (you shouldn't need this if you setup automatic React Native deployment with CircleCI, in which case you will enter the match passphrase once in the CI server).

5. (If needed) Open src/environment/index.staging.js and update it with all required js environment variables for the staging environment. Commit.

6. Wrap your app with CodePush

In order for CodePush to work, it needs to wrap your app as explained here​.

import codePush from "react-native-code-push" ; import { AppRegistry } from "react-native" ; import { ENV } from "./src/environment" ; import AppContainer from "./App" ; ​ const codePushOptions = { checkFrequency : codePush . CheckFrequency . ON_APP_RESUME , installMode : codePush . InstallMode . ON_NEXT_SUSPEND , minimumBackgroundDuration : ENV === "production" ? 120 : 0 } ; ​ const CodePushedApp = codePush ( codePushOptions ) ( AppContainer ) ; AppRegistry . registerComponent ( "yourApp" , ( ) => CodePushedApp ) ;

‌

7. Pack the Staging secrets in an encrypted archive

yarn pack-secrets -e staging

⚠️ Make sure that you commit the resulting archive and note down the passphrase somewhere

This archive contains all the staging env secret passwords and files. If you lose it, you won't be able to deploy the app anymore. This archive is useful to share the secrets between developers without having to send through email/usb etc.

⚠️ In the future, whenever adding more secret files (e.g. Firebase keys), make sure that you run the following steps:

run yarn unpack-secrets -e staging -p <your passphrase> to get the latest version of all secrets locally

add the secret file to SECRETS_TO_PACK in pack-secrets.sh

run yarn pack-secrets.sh -e staging and commit the resulting archive: other devs and the CI server will now have access to this secret file.

It would be cool to replace this encrypted archive with the use of transcrypt, PRs welcome! :)

‌

8. Hard-Deploy Staging

yarn deploy -t hard -e staging

=> You can now download the staging app on your phone at https://install.appcenter.ms​

9. Soft-Deploy Staging (CodePush)

yarn deploy -e staging

=> You should receive the update you CodePushed directly on your phone without having to manually update your app from Appcenter.

‌

10. Create production environment

yo rn-toolbox:fastlane-env

‌

Answers

Please confirm the project name: <Press Enter>

The name for this new environment (lowercase, no space): production

The name of your repository Git branch for the environment just set: production

The name of the company which will be publishing this application: <your company>

Which platform will you use for React Native deployment?: AppStore

The App Id for this environment: <your>.<company>.<projectname>.production

The type of certificate you will be using: App Store

Your git repo for match: git@github.com:<TeamOrg>/<match-repo>.git (<- same as for Staging)

(<- same as for Staging) The branch you want to use for match: <Press Enter>

The developer.apple.com team id for the certificates: XXXXXXXX (<- find this in the url when on "Account" section of your Apple Dev Portal)

(<- find this in the url when on "Account" section of your Apple Dev Portal) The appstoreconnect.apple.com team name: <AppStore Connect team name>

An AppstoreConnect Apple Id (good practice: ID should have "developer" access - only allowed to upload builds): <someBotDevAccount@you.com> (<- possibly not the same apple id as used for the apple dev portal, since this one should only have dev access to Appstoreconnect and will be used by CI: you don't want it to have admin access to the apple dev portal)

(<- possibly not the same apple id as used for the apple dev portal, since this one should only have dev access to Appstoreconnect and will be used by CI: you don't want it to have admin access to the apple dev portal) Your apple id (should be admin on the Apple Developer Portal): you@example.com

Your keystore password: <Press Enter>

A Google Play JSON Key relative path: <relative/path/to/dsadsadasdad.json> (<- follow tutorial here to get this with a service account. /!\ Make sure this file is in the repo and is gitignored )

(<- follow tutorial here to get this with a service account. /!\ Make sure this file is in the repo ) Will you deploy with Appcenter CodePush on this environment? (y/n): Y

A valid App Center Username: <your-Organization> (<- find in the url when on the app in the AppCenter interface. It's the username for a person or the name of the Organization for an Organization)

(<- find in the url when on the app in the AppCenter interface. It's the username for a person or the name of the Organization for an Organization) A valid App Center API token: XXXXXXXXXXXXXXXXXXXXXXXXXX (<- make sure the token only has access to your Org's apps. Google how to get it if needed).

(<- make sure the token only has access to your Org's apps. Google how to get it if needed). The iOS project id on AppCenter for this environment, should be different than Android and not contain spaces: <iOS production Appcenter app name created in step 1.>

The Android project id on AppCenter, should be different than iOS and not contain spaces: <Android production Appcenter app name created in step 1.>

Your iOS CodePush deployment key: <iOS Production CodePush deployment key>

Your Android CodePush deployment key: <Android Production CodePush deployment key>

Your iOS CodePush deployment name: Production

Your Android CodePush deployment name: Production

Will you be using Appcenter Analytics and Crash reporting on this environment?: <Up to you :). You are probably better of using Firebase and Sentry for this stuff, but it will require more work.>

(If yes above) Your AppCenter app secret for the iOS App: <production iOS app secret>

(If yes above) Your AppCenter app secret for the Android App: <production Android app secret>

(After some npm installation…) Should fastlane modify the Gemfile at path 'xxx' for you? (y/n): Y

‌

11. Setup iOS production deployment certificates

bundle exec fastlane ios setup --env = production

12. Pack the Production secrets in a encrypted archive

Make sure the Google API JSON key file added in step 9. is in .gitignore

Add this file to the production secrets by appending its relative path to the SECRETS_TO_PACK string in pack-secrets.sh and running:

yarn pack-secrets -e production

⚠️ Make sure that you commit the resulting archive and note down the passphrase somewhere. Passphrase should be different than for staging to allow different access rights.

‌

This archive contains all the production env secret passwords and files. If you lose it, you won't be able to deploy the app anymore.

⚠️ In the future, whenever adding more secret files (e.g. Firebase keys), make sure that you run the same steps as for staging above (replace staging with production)

⚠️ If you lose the keystore file present in this archive, you will not be able to deploy the Android app ever again and will have to create a new app in the Google Play Store.

‌

13. (If needed) Open src/environment/index.production.js and update it with all required js environment variables for the production environment. Commit.

‌

14. Create the production apps in the portals

‌

In Appstore Connect, create your app with the correct bundle id <your>.<client>.<projectname>.production

In Google Play Console:

create your app

locally, do an Android hard deploy and see it fail:

yarn deploy -t hard -e production -o android

get the apk located at android/app/build/outputs/apk/release/app-release.apk and upload it to the Internal track. Accept Google Code Signing prompt if asked. Click Save and don't specify anything else: your app is now linked to the correct bundle id and keystores.

‌

15. Make sure you have an app icon

‌

Otherwise AppStore Connect will not accept your app. You need a 192 x 192 png.

Thanks generator-rn-toolbox again! :)

yo rn-toolbox:assets --icon relative/path/to/your/icon.png

‌

16. Hard-Deploy Production

yarn deploy -t hard -e production

‌

=> You should now be able to see your app's binaries in both Google Play Store Internal track ("add from library") and AppStore Connect's TestFlight.

17. Soft-Deploy Production (CodePush)

yarn deploy -e production

=> You should receive the update you CodePushed directly on your phone without having to update their app.

‌

18. Now setup automated React Native deployment with CircleCI so that no-one has to use these commands anymore :)

Enter CircleCI and select your team/account on the top-left corner

Click Add Projects -> iOS -> Setup project-> Start building (on your project)

Locally, run yo rn-toolbox:circleci :

: Please confirm the react-native project name (as in react-native-init ): <Press enter>

Path to the React Native project relative to the root of the repository (no trailing slash): <Press enter unless weird config/monorepo in which case put the path to React Native folder>

Go to CircleCI's Environment Variables settings. Add the following variables as specified by the generator:

FL_APPCENTER_API_TOKEN : same appcenter API Token as in fastlane/.env.***.secret files

: same appcenter API Token as in files MATCH_PASSWORD : the password used for the match repo when doing the first deployment

: the password used for the match repo when doing the first deployment FASTLANE_PASSWORD : the password for the AppStoreConnect Apple ID

: the password for the AppStoreConnect Apple ID PRODUCTION_SECRETS_PASSPHRASE : the production secrets archive password

: the production secrets archive password STAGING_SECRETS_PASSPHRASE : the staging secrets archive password

: the staging secrets archive password …. any other secrets passphrases if you added other environments

Go to Checkout SSH keys section and add a github account that has access to the match repo (ideally read access and only to this repo)

Push your code. staging and production branches should run additional deployment steps!

and branches should run additional deployment steps! Setup CircleCI hooks on Github to only allow branch merge on Circle builds succeeding.

Appendix: Some nice tweaks to do, good exercises for you to understand the moving parts of the stack

Change the parameters of pilot in Fastfile and info.plist so that the upload to test flight is directly distributed to external testers.

in and so that the upload to test flight is directly distributed to external testers. By changing Fastfile , Make the staging app be deployed to a new Testers distribution group in Appcenter instead of Collaborators

, Make the staging app be deployed to a new distribution group in Appcenter instead of Create a new variable in .env (called e.g. MINIMUM_CODEPUSH_VERSION ) and change Fastfile accordingly so that CodePush targets all versions greater than or equal to the value of this variable instead of targetting only ANDROID_VERSION_NAME and IOS_VERSION .

If you want to know more about the why of all this, checkout this video of my talk at the React Native London Meetup!