Automate React Native Builds With GitLab CI: Walkthrough and Tutorial

Automate React Native Builds With GitLab CI:  Walkthrough and Tutorial

Besides powerful version control and project management, the GitLab ecosystem also integrates tools such as GitLab CI, that allows developers to streamline and optimize the testing and deployment stages of your app development lifecycle.
In this article we will tackle the steps needed to implement continuous integration to your React Native apps in GitLab, and help you maintain momentum in your team by offering constant availability of your builds for testing, demo and release purposes.

There are only 2 steps needed to have a working CI system:

  1. Set up a Runner
  2. Add a .gitlab-ci.yml file to your project

Set up a Runner

Essentially, a Runner will be used to run your jobs in any kind of environment you install it onto, and send the results back to GitLab. It can run your jobs encapsulated in a docker image or simply in the shell of a targeted machine. For our React Native needs, the Runner needs to be executed in an environment hosting Android SDKs, Xcode and Node. For this specific article, we will be installing our Runner on a macOS machine.

Once the environment is ready, simply install GitLab Runner, register it with the token provided by GitLab and run the daemon. You can set up a tag to uniquely identify the runner that help you target this specific environment in your YAML file. Let's pretend we use the tag rnbuild.

Configure .gitlab-ci.yml

Jobs within a CI configuration can belong to three different default stages : build, test and deploy. In order to keep this article focused, we will only pay attention to the deploy stage, and work our script around that. The CI script uses the YAML markup language, make sure you get familiar with the syntax before diving head on in the code.

Building on Android or iOS will require different steps, so let's define a job for each. To keep it simple, we can start with Android.

Automating the Android build

First things first, we need to find a way to pass our password for the keystore, in order to get a digitally signed build. You generally don't want to push that to your repository. GitLab explicitely offers variables that you can pass to your runners. Let's define a variable for the password and call it KEYSTORE_PASSWORD.

gitlab-variables

Let's now look into the YAML script. We need to tell the gitlab runner to install our npm packages before running anything else. Let's use the anchor before_script.

deploy:android:
	stage: deploy
	tags:
		- rnbuild
	before_script:
		- npm install

We then apply the usual build commands and use environment variables to pass our keystore password to the build script.

script:
	- cd android && ./gradlew assembleRelease -PMYAPP_RELEASE_STORE_PASSWORD=$KEYSTORE_PASSWORD -PMYAPP_RELEASE_KEY_PASSWORD=$KEYSTORE_PASSWORD

Once the runner will create the build, we need to find it within the android folder and upload it back to GitLab. We use the artifacts anchor for this end purpose. We can use a combination of variables given by the runner such as $CI_PROJECT_NAME and $CI_COMMIT_REF_NAME to name our build file.

    - cp android/app/build/outputs/apk/app-release.apk $CI_PROJECT_NAME-$CI_COMMIT_REF_NAME.apk
    artifacts:
        name: "$CI_PROJECT_NAME-$PLATFORM-$CI_COMMIT_REF_NAME"
        paths:
            - $CI_PROJECT_NAME-$CI_COMMIT_REF_NAME.apk
        expire_in: 7 days

That's all it takes. Specifying when: manual will help you optimize ressources if that's a concern, so your build machine will only work when needed by the team.

deploy:android:
	stage: deploy
	tags:
		- rnbuild
	before_script:
		- npm install
	script:
		- cd android && ./gradlew assembleRelease -PMYAPP_RELEASE_STORE_PASSWORD=$KEYSTORE_PASSWORD -PMYAPP_RELEASE_KEY_PASSWORD=$KEYSTORE_PASSWORD
    	- cp android/app/build/outputs/apk/app-release.apk $CI_PROJECT_NAME-$CI_COMMIT_REF_NAME.apk
    artifacts:
        name: "$CI_PROJECT_NAME-$PLATFORM-$CI_COMMIT_REF_NAME"
        paths:
            - $CI_PROJECT_NAME-$CI_COMMIT_REF_NAME.apk
        expire_in: 7 days
	when: manual

Automating the iOS build

Xcode comes with the xcodebuild CLI, a command line tool that essentially will help you archive and build your app.

For the sake of simplicity, we set up a machine to automatically manage signings by logging in to the relevant accounts on Xcode. But you could use tools like Fastlane to help you manage codesigning and use the right provisioning profiles for your project. The relevant provisioning profile can be specified in Info.plist under PROVISIONING_PROFILE, or added as a variable to the xcodebuild command.

Once that is set up, you can start writing the YAML script. Most of it is very similar to the Android script, except 2 key-steps. What we will do instead of the gradlew command is essentially build an archive with xcodebuild archive, and then create the .ipa with xcodebuild -exportArchive.

    - xcodebuild -scheme $PACKAGE_NAME archive -archivePath $PACKAGE_NAME.xcarchive -allowProvisioningUpdates
    - xcodebuild -exportArchive -archivePath ./$PACKAGE_NAME.xcarchive -exportPath . -exportOptionsPlist $PACKAGE_NAME/Info.plist

This is what the final iOS script looks like:

deploy:ios:
	stage: deploy
	tags:
		- rnbuild
	before_script:
		- npm install
	script:
    - export PACKAGE_NAME=$(node -p -e "require('./package.json').name")
    - cd ios
    - xcodebuild -scheme $PACKAGE_NAME archive -archivePath $PACKAGE_NAME.xcarchive -allowProvisioningUpdates
    - xcodebuild -exportArchive -archivePath ./$PACKAGE_NAME.xcarchive -exportPath . -exportOptionsPlist $PACKAGE_NAME/Info.plist
    - mv $PACKAGE_NAME.ipa ../$PACKAGE_NAME.ipa

Note how we get the package_name to access the relevant scheme and workspace name.

Consolidation

To avoid redundancy and keep things easily scalable, we can also consolidate our scripts into a template, this is what our final code will look like.

.job_template: &job_deploy
  stage: deploy
  before_script:
    - npm install
  tags:
    - rnbuild
  after_script:
    - cp $OUTPUT_PATH.$FILE_TYPE $CI_PROJECT_NAME-$CI_COMMIT_REF_NAME.$FILE_TYPE
  artifacts:
    name: "$CI_PROJECT_NAME-$PLATFORM-$CI_COMMIT_REF_NAME"
    paths:
    - $CI_PROJECT_NAME-$CI_COMMIT_REF_NAME.$FILE_TYPE
    expire_in: 7 days
  when: manual

deploy:android:prod:
  variables:
    PLATFORM: android
    FILE_TYPE: apk
    OUTPUT_PATH: android/app/build/outputs/apk/app-release
  <<: *job_deploy
  script:
    - cd android && ./gradlew assembleRelease -PMYAPP_RELEASE_STORE_PASSWORD=$KEYSTORE_PASSWORD -PMYAPP_RELEASE_KEY_PASSWORD=$KEYSTORE_PASSWORD
  
deploy:ios:prod:
  variables:
    PLATFORM: ios
    FILE_TYPE: ipa
    OUTPUT_PATH: ./$CI_PROJECT_NAME
  <<: *job_deploy
  script:
    - export PACKAGE_NAME=$(node -p -e "require('./package.json').name")
    - cd ios
    - xcodebuild -scheme $PACKAGE_NAME archive -archivePath $PACKAGE_NAME.xcarchive -allowProvisioningUpdates
    - xcodebuild -exportArchive -archivePath ./$PACKAGE_NAME.xcarchive -exportPath . -exportOptionsPlist $PACKAGE_NAME/Info.plist
    - mv $PACKAGE_NAME.ipa ../$PACKAGE_NAME.ipa

From then on, it's easy to add features that will be applied to both platforms. Want to hook up your Slack bot? Piece of cake!

- "curl -X POST -H 'Content-type: application/json' --data ' ''{\"text\":\"🚀 '${CI_PROJECT_NAME}' '${PLATFORM}' *'${CI_COMMIT_REF_NAME}'* is now available for download: <https://gitlab.com/hybridheroes/physiofit/-/jobs/'${CI_JOB_ID}'/artifacts/download>\" }'' ' ${SLACK_HOOK}"

Simply trigger the build process from the GitLab interface to have your files delivered directly to the GitLab server.

Wrapping Up

Manually building your branches on devices and simulators with React Native can sometimes take a tremendous amount of time, especially when different developers work on the same project, or when a non-technical QA has to go through the process every time.

Automating builds is the best and surefire way to provide a constant delivery of your software builds and will help you optimize turnaround times and focus more on what matters in your own React Native development lifecycle. Happy continuous integration!

If you want to read more about development with React Native, check out this blog post about Firebase Analytics with React Native or this blog post about FormatJS in React Native.