Summary: This is a resource for engineers who create mobile apps and want a faster way to automate distributing updates. CodePush makes it possible to bypass the delays and extra steps of store and manual distribution. By following the approach outlined in this article, you’ll be able to streamline the overall release process.

React Native Mobile App CI/CD

Here at Diamond, we’re always on the lookout for ways to improve our software release processes, especially in the mobile arena. We are big fans of Microsoft’s Visual Studio App Center for the continuous distribution of our React Native app builds to the Google Play Store, the Apple App Store, and internally to developers and testers. 

There are drawbacks to the traditional build distribution process that can prevent fast distribution. Apple is the most restrictive. It only allows distributions of their ipa application archive files through their official App Store. Additionally, as part of the distribution process, it requires review. 

This review can take as little as a few hours. Often, it takes a day or two. On the Google side, things are a little easier. In addition to the Play Store,  apk application archive files can be distributed by email or via a website download. However, these methods still require users to take proactive steps to locate and install apps or app updates. This means that users who have opted out of automatic upgrades may never receive updates with crucial bug fixes and key feature enhancements.

What Is CodePush And Why Use It?

Luckily, React Native apps can use an alternate distribution system that bypasses the delays and extra steps of store or manual distribution with CodePush. Originally a standalone cloud service, CodePush is integrated with Microsoft’s Visual Studio App Center to allow for automated delivery of app updates directly to end-user devices. 

There’s just one catch: it can only be used for updates contained wholly within the JS app bundle. Native layer updates still need to go through the usual store-based distribution process. Even so, there are plenty of use cases for which CodePush is useful. One is the JS layer hotfixes where you simply want to repair broken functionality as quickly and seamlessly as possible.

CodePush Integration Approach

On our mobile projects, we generally host source code in GitHub. And we use the Github App Center plugin to automatically generate builds for each PR opened by our front-end engineering team. We also take advantage of GitHub Actions for automating code quality checks/gates into our CI. 

When we settled on using CodePush to automate our JS layer hotfix distribution, we had to find a way to integrate CodePush into our existing CI/CD flow. AppCenter offers two out-of-the-box CI/CD integration choices: AzureDevOps and Travis CI. Since our stack is not Azure-based, the first one wasn’t an option. While the Travis CI option seemed promising, we were hoping for a solution that leveraged our existing stack without adding yet another service to the release process just for this capability. For that reason, we settled on an approach that uses GitHub Actions as the CI/CD tool for automating CodePush updates.

Getting Set Up To Use CodePush

Before we could configure CI/CD, we needed to get our app into a CodePush-ready state by configuring our apps to use the CodePush Client SDK.

CodePush Client Configuration

We followed the official documentation on configuring both our AppCenter Console and our React Native Client SDK. Because we already had pre-existing App Center iOS and Android apps, we found it easier to simply use the AppCenter Console to configure our apps for CodePush distribution:

  1. In the AppCenter console go to your app, and from the side menu select Distribute > CodePush

    Screenshot of an AppCenter console Overview menu with "Distribute" and "CodePush" in bold.

  2. Select Create standard deployments to create CodePush deployment keys for each SDLC environment. On this example project, we have a Staging environment for regression testing and a Production environment for end user distribution. You can switch between environments using the dropdown menu at top right.

    Screenshot of CodePush that shows no deployments and a button "Create standard deployments."

    Screenshot of CodePush that shows no updates released for production. An arrow points at "Production< and another arrow points at an arrow icon.

  3. We then followed the React Native Client SDK configuration steps for React Native version 0.60 and above, which requires native changes to both the iOS and Android codebase.

Integrating The Client Into The React Native Layer

We integrated the CodePush client SDK into our React Native codebase at the top level – usually the index.js or index.ts file in your project – by wrapping the root component with the codePush() higher-order component. Here’s a sample configuration:

import codePush from 'react-native-code-push';

const App = () => {
  return (
    // your root component tree
  );
};

AppRegistry.registerComponent('AppName', () => codePush(App));

There are more complex configuration options in the CodePush documentation, but this one was simple and streamlined, which is the type of integration we were looking for.

Next, we needed to decide on an update mode. CodePush supports many types of updates: on next restart, when the app resumes from the background, and even finer grained control based on a specific user action or timer interval. You can also choose between Silent Mode, which executes the update without notifying the user, and Active Mode, which prompts the user and requires user interaction before initiating the update.

Based on our use case of prioritizing hotfixes and small, quick updates, we decided to go with Silent Mode Update on Next Restart. This method would be the least complicated and the least disruptive for the end-user. Ultimately, we don’t need to tell users when we’re delivering a bug fix. We simply need to fix the bug and return the user to a seamless app experience without interrupting user interaction, such as audio/video playback. The previous sample code is configured to enable Silent Mode Update on Next Restart.

Deploying and Automating CodePush Updates

Once our app codebase was CodePush-enabled, the next step was to use the AppCenter CodePush CLI to deploy updates. First, we needed to create a CodePush Access Token that allows you to authenticate against the CodePush service with CLI commands in a script (outside of the browser console):

appcenter tokens create -d "Your CodePush Authentication Token"

This command only needed to be run once. We securely stored the value it returned in Github Secrets so that it could be easily accessed through Github Actions. We gave ours the key name APPCENTER_CODEPUSH_TOKEN.

The CodePush documentation on using the CodePush CLI to make updates is straightforward for manual deploys. However, our goal was to automate updates. We accomplished this with a custom Github Actions script tied to our repository:

  1. Add the CodePush CLI commands that execute a CodePush update to a custom YAML script and place it in the <your-repository>/.github/workflows. We called our script codepush.yml.
  2. Configure the script to run as part of a Github Action triggered by publishing a github release by including the following at the top of the script:
    on:
      release:
        types: [published]
  3. On trigger, the script job will parse metadata from the github release that triggered the run and execute the appropriate CodePush CLI commands to build the JS bundle. Then, it will deploy to the desired app releases in either the Staging or Production environment, pulling the necessary configuration parameters and tokens (such as the AppCenter CodePush Token) from Github Secrets.

Our full CodePush release process is as follows:

  1. Push the code changes that we want to release to our github remote, in a release branch named using the following convention: release-<RELEASE-VERSION>+cp-<n> where <RELEASE-VERSION> is the server app release version number and <n> is the CodePush version. Merge the release branch with the long-lived target branch for the corresponding SDLC environment.
  2. Draft a new release in the github repository. Select either the Staging environment branch or the Production environment branch as the tag target,then make sure the release tag and release title are formatted as described below, and select Publish release. (The tag and title formatting is important so that the script can parse them to extract information about which branch to push and which environment to push to.) This will execute the CodePush github actions script that will push the updates contained in the release branch.
     
    • Release Tag format: <RELEASE_VERSION>+codepush.release.<codepush-env>.<codepush-version-id>.<platform-list>
      • Example: 1.11.35+codepush.release.staging.9.ios,android
    • Release Title (also called Release Name) format: <RELEASE_VERSION>+CodePushReleaseTarget=<TARGETED_RELEASE_VERSIONS>
      • Example 1.11.35+CodePushReleaseTarget=~1.11.3 (equivalent to targeting all the releases distributed via the stores in the range >=1.11.3 to <1.12.0)

Screenshot of github showing a tag and title for the CodePush release

 

Here’s a sanitized version of the codepush.yml script we used to deploy Production environment CodePush updates for one of our clients. The last two lines contain the actual CodePush CLI commands to deploy the update to the iOS app (your-ios-app) and the Android app (your-android-app).

 



name: CodePush
on:
  release:
    types: [published]

jobs:
  # codepush production release
  appcenter-prod-job:
    if: contains(github.ref_name, 'codepush') && github.event.release.target_commitish == 'master'
    runs-on: macos-11
    steps:
      - name: Echo github release event
        run: |
          echo target branch is $
          echo release tag is: $
          echo release title is: $
          echo release body is: $
          echo repository is: $
      - name: Get release options
        env:
          RELEASE_TAG_NAME: $
          RELEASE_NAME: $
        run: |
          echo $RELEASE_TAG_NAME $RELEASE_NAME
          echo RELEASE_VERSION=`echo "$RELEASE_TAG_NAME" | cut -f 1 -d +` >> $GITHUB_ENV
          echo CP_ACTION=`echo "$RELEASE_TAG_NAME" |  cut -f 3 -d . | cut -f2 -d+` >> $GITHUB_ENV
          echo CP_TYPE=`echo "$RELEASE_TAG_NAME" |  cut -f 4 -d .` >> $GITHUB_ENV
          echo CP_VERSION=`echo "$RELEASE_TAG_NAME" |  cut -f 6 -d .` >> $GITHUB_ENV
          echo CP_PLATFORM=`echo "$RELEASE_TAG_NAME" |  cut -f 7 -d .` >> $GITHUB_ENV
          echo CP_TARGET=`echo "$RELEASE_NAME" | cut -f 2 -d =` >> $GITHUB_ENV
          echo $RELEASE_VERSION $CP_ACTION $CP_TYPE $CP_VERSION $CP_PLATFORM $CP_TARGET
      - uses: actions/setup-node@v2
        with:
          node-version: '14'
      - uses: actions/checkout@v2
      - run: npm install --global yarn
      - run: yarn install
      - run: npm install -g appcenter-cli
      - name: Appcenter login & pre-build script & codepush command
        shell: bash
        env:
          CODEPUSH_TOKEN: $
        run: |
          appcenter login --token "$CODEPUSH_TOKEN"
          if grep -q "ios" <<< "$CP_PLATFORM"; then appcenter codepush release-react -a your-appcenter-org/your-ios-app -d Production --target-binary-version "$CP_TARGET"; else echo "not pushing to ios"; fi
          if grep -q "android" <<< "$CP_PLATFORM"; then appcenter codepush release-react -a your-appcenter-org/your-android-app -d Production --target-binary-version "$CP_TARGET"; else echo "not pushing to android"; fi


All CodePush runs will show up under the Actions tab in the repository. You can drill down into the job details to see the individual steps executed. (Tip: if there were any errors executing the script, the specific step where the error occurred will be highlighted red.)

 

Screenshot of CodePush run showing up in the repository's Actions tab

Screenshot of CodePush run showing the actions for the job

Lessons Learned

One of the biggest lessons we learned from testing this deployment strategy is that you need to be very careful which existing releases you target with CodePush updates. For example, if you deploy an update to an older app version that has different native dependencies than the version used to create the CodePush JS bundle, you might inadvertently introduce a breaking change to that older version. 

CodePush is a very open-minded service that will allow you to deploy updates to any app version that has ever been released. So, you need to exercise care when devising your deployment strategy to make sure you don’t accidentally break the app for some users. 

For this reason, we adhere to our traditional SDLC model and deploy to Production-only after first deploying to Staging and running a full set of regression tests, just as we do for our store-based releases. We also chose to fold our CodePush implementation into a disciplined project-wide semantic versioning strategy that differentiated between releases that contained native layer changes vs. those that contained only JS layer changes:

  • Minor version update: Code includes changes at the native layer and may include changes at the JS layer. It will be distributed through the stores. (Note: native layer changes may include package updates.) Example version change: 1.13.2 -> 1.14.0
  • Patch version update: Code only includes changes at the JS layer. It will be distributed through the stores. Example version change: 1.13.2 -> 1.13.3
  • Build metadata update: Code only includes changes at the JS layer. It will be distributed through CodePush. Example version change: 1.13.2 -> 1.13.2 CP 1 

CodePush is a useful and versatile service for deploying JavaScript layer app updates directly to end user devices. By following this approach, it can be added to a React Native mobile CI/CD pipeline to streamline the overall release process with minimal additional tooling.


Don't miss an update! 

This blog post is part of a series of tutorials from our product team. Subscribe to our newsletter to stay on top of the latest insights from Diamond's engineers and designers.