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:

Set up a Runner 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.

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.

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.

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: 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 <<: 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 <<: 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 sufire way to provide 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!