Tips for Continuous Deployments with React Native and Circle CI

Eli Zibin
g2i_co
Published in
7 min readJun 20, 2018

--

First, why? Most simply, an effective CI/CD pipeline will stabilize and simplify your release cycle, improve your development process, and help guard against code regressions and bugs creeping into production. If that sounds like a good idea, read on.

Since there are many solutions to CI/CD for React Native, this piece is not to be taken as definitive or the best solution. Everyone’s development and deployment process, tool chains, and requirements will be different, so your mileage may vary here, but I’m hoping some of the gotchas I’ll mention will be applicable to anyone looking to automate their deploy process. Other alternatives for CI/CD in the React Native space are Microsoft’s AppCenter, Bitrise offers a solution, and Buddybuild (my personal favourite) still exists for those who signed up before it’s restructuring, and the list goes on.

This article will focus on CircleCI. It’s a good solution for existing projects that you may already be using for a web/backend project, since centralizing your CI/CD is easier than configuring multiple services. CircleCI obviously supports React Native (through macOS and Linux containers), although, I’m sorry to say it is not free to use. In order to build and release iOS apps you’ll need to use their macOS container plan which is available only to paid users.

Overview:

Once you’ve set up an account, you’ll want to follow the instructions for adding an app from your git provider and add your `.circleci` folder to your project. It’ll give you a big thing to copy/paste to get started. Once that’s done, push to git and see if anything happens!

Now that it’s set up, you can create your own customized workflow to run tests/deploys however you want them configured. I find it useful to run tests on every branch on each git push to ensure that everything is working properly, or to learn what still needs to be fixed before it’s merged back into master. I then deploy to the Testflight/Alpha testing when something is merged to a staging or production branch. An example workflow would look something like this:

# config.yml... jobs etc.workflows:
version: 2
test-and-deploy:
jobs:
— run_tests
— deploy_android:
requires:
— run_tests
filters:
branches:
only:
— production
— staging
— deploy_ios:
requires:
— run_tests
— deploy_android
filters:
branches:
only:
— production
— staging

This means that the run_tests job will get kicked off on every push, but the deploy_android and deploy_ios will only occur on the production/staging branches, after all the tests have passed

Testing Etc.:

Pretty simple here. Run your commands as usual. If you’re using Jest, it takes a — ci option and a maxWorkers option to prevent out of memory errors. You can also run your linter here as well, or any other tasks you might want to run alongside your tests. If you integrate with other testing mechanisms, you could also do that here.

- run: yarn test — maxWorkers=2 --ci

Deploying:

This is obviously the more complicated bit. But I’ll break it down, and hopefully illuminate some gotchas.

Fastlane:

Essentially, Fastlane is a great utility belt for building/releasing mobile apps. There’s a million posts about Fastlane and React Native, here are two from Infinite Red:

-Releasing on iOS with Fastlane
-Releasing on Android with Fastlane

Code Signing:

Fastlane provides Match (as mentioned in that first Fastlane article). It handles all code-signing for you, provided you trust hosting all your keys in a private Github repository. I find it’s best to create a machine user on your iTunes Connect/Developer team, something like “deployer@company.xyz” so that all your teammates can share and download the certs of this user when they run the match command locally. The best part is, if something goes wrong, no worries, nuke all your old certs and regenerate new ones in seconds without consequence. It removes the need to really think about code-signing (for the most part).

Your Fastlane script will also have to run match, so that the CI is able to deploy and release with this account. Make sure you add the setup_circle_ci command at the top of your deployment lane. You’ll also need to add the environment variables of FASTLANE_PASSWORD and MATCH_PASSWORD to your CircleCI environment variables so that your container will be able to run match/deploy. Refer to the docs here for more info on that if you want

Android App Signing:

This is something that isn’t covered by the Circle CI docs anywhere, and their React Native example repo also doesn’t suggest how to handle this. After reading a few other blog posts online, it seems like a common solution is to host your key-store file somewhere in a private location online. Some recommend DropBox, but I use the same Github repo that is shared with Fastlane’s Match mentioned above.

Store both the key-store file and the developer.json file that Fastlane requires in order to upload your APK to Google Play in the Match repo. Afterwards, it’s as simple as adding a simple shell script command to be run in your circle config:

You may have to give the file executable permissions first:

# circle.yml
... previous build steps in job
- run: chmod u+x download_release_data.sh
- run: ./download_release_data.sh

These files can look something like this:

#!/bin/bash
# download_release_data.sh
# If you’re using bitbucket, you can just set the env-var in the circle config for your bitbucket repogit archive — remote=ssh://${BITBUCKET_CERTS_REPO} HEAD prod.keystore | tar -x
git archive — remote=ssh://${BITBUCKET_CERTS_REPO} HEAD google-play.json | tar -x
# if you’re using github, you can give your machine user a GH access token to simply grab the file, the names of which can be stored in the circle env-var section as wellcurl -L — header “Authorization: token ${GH_TOKEN}” \
— header ‘Accept: application/vnd.github.v3.raw’ \
— remote-name \
— location “${GITHUB_URL_FOR_KEYSTORE}”
curl -L — header “Authorization: token ${GH_TOKEN}” \
— header ‘Accept: application/vnd.github.v3.raw’ \
— remote-name \
— location “${GITHUB_URL_FOR_DEVELOPER_JSON}”

After the key-store and developer.json files have been downloaded, depending on where they end up, you may need to move them so that the react-native cli is able to find and use them accordingly.

Cacheing:

You obviously don’t want to go over your the build limits and end up paying more, and most likely want your build processing time to be as fast as possible, so cache the steps that take the longest! Cache your node modules, your gems, your pods, whatever else you can!

It’s pretty easy, add a step that checks for the cache, in this case our node_modules:

- restore_cache:
name: Restore node_modules cache
key: yarn-cache-{{ .Branch }}-{{ checksum “yarn.lock” }}

Here, this cache is branch specific and checksum dependent. You can make it more specific, or less specific. Make sure you add the step to save the cache as well after, in case the branch has changed, or if there was no cache in the first place:

- run: yarn
- save_cache:
name: Save node_modules cache
key: yarn-cache-{{ .Branch }}-{{ checksum “yarn.lock” }}

Rinse and repeat for whatever else you need!

By the way, if you are using CocoaPods, CircleCI provides a cache for you to speed up build time!

Gotchas

Out of Memory errors: These are the worst! Android builds take a lot of processing power, and if you’re on the smallest tier, or haven’t enabled some of the other options that CircleCI provides to increase container sizes, it can run out of memory pretty easily and crash. I’ve found that tweaking some limits on the gradle options in the CircleCI script, as well as specifying the amount of workers used in your build.gradle inside your React Native project can solve most of these issues:

# build.gradle# add this line before the ‘apply from … react.gradle’ as mentioned in the comments that RN ships with in the file.project.ext.react = [ extraPackagerArgs: [“ — max-workers=1”] ]
# circle.yml
environment:
GRADLE_OPTS: ‘-Dorg.gradle.jvmargs=”-Xmx1024m
-XX:+HeapDumpOnOutOfMemoryError”’

Xcode version: Set it! Circle defaults to 9.0 at the moment. You may want to use something closer to what you’re developing with, as well as some Fastlane commands don’t seem to share the same compatibility between Xcode versions sometimes:

# circle.yml
macos:
xcode: “9.3.0”

CodePush: No gotcha here, rather CodePush has got your back! You can still use CodePush if you’re using CircleCI and Fastlane. Create a lane to run your release commands, or upload source maps or whatever you need to do!

Conclusion:

I know this isn’t enough to walk away with a working pipeline of copy/paste code, but so often one’s deployment needs are different enough that I thought it best to just give an overview of what’s possible and what bites. Using CircleCI in conjunction with Fastlane is a great way to setup CI/CD solution for React Native. Make sure you take advantage of caching, using CircleCI workflows, tweaking your options to prevent out-of-memory errors, Fastlane and Match to save you time and complexity.

A special thanks to G2i for this post. For those who are unfamiliar, G2i is a hiring platform run by engineers that matches companies with pre-vetted React, React Native, GraphQL, and native iOS/Android focused engineers you can trust.

--

--