Automated versioning of React Native apps via GitLab pipeline

Andrej Kuročenko
Creative Talks
Published in
9 min readMar 12, 2021

You are fixing that annoying bug in your fancy mobile application, looking forward to having it released asap (of course). Finally everything seems to work, all tests pass, the app is built and being uploaded … phew … when suddenly, the build is rejected by the store with a message stating that build with the same version/build number already exists. You are like “mmgh, not again 🙄”, or even maybe “@!#$, NOT AGAIN 😤”

Familiar?

Although tolerable in small apps, this is a mandatory and repeated step that is very annoying and a great candidate for automation.

The project that I currently work on is a banking mobile application written in React Native and distributed for iOS and Android. We do several to dozen releases daily to dev and sometimes even several releases a weekly to prod. Managing versioning manually would be a full time job for one person (including coffee breaks, of course), and two if we would count with a bus factor effect .

GIT structure and release management

Before we get to the versioning itself, I’d like to introduce how the code flows in our project from feature branch to production. This is an important part as versioning fits well to this particular architecture.

We are utilizing so called trunk based development — we have only feature/fix branches that are merged directly to master (and features hidden behind feature toggles if needed). This forces us to do small commits to master very often.

On merge to master a new build is automatically triggered, build number is incremented and the app is released to dev distribution channel

Once such an app version is tested and announced as stable, we update the app version and release it to production by triggering the manual stage in our GitLab pipeline from the same commit (merge) as the dev app was built (configuration differs).

Let’s take a look at our proposed solution.

Where to store version and build number

For apipeline (or whatever/whoever builds your app) to be able to access previous app version number and build number this information has to be stored somewhere, ideally in GIT itself.

Of course if we had only iOS or only Android app, it would make sense to store this information in respective project configurations. In React Native, we want to have a version and build number accessible for iOS/Android projects and JavaScript code, ideally from one place during build.

Working with app version number

For version we are utilizing package.json and its version attribute which holds app version in semver format . Your favorite package manager, for example yarn, is then able to increment the version with yarn version command. For example:

yarn version --path
yarn version --minor
yarn version --major

This can be added to package.json scripts and called manually or from CI:

"bump-version:minor": "yarn version --minor --no-git-tag-version",
"bump-version:major": "yarn version --major --no-git-tag-version",
"bump-version:patch": "yarn version --patch --no-git-tag-version",

Note the --no-git-tag-version flag. Normally the yarn version command also creates a GIT tag, which we do not want at the moment as we will use GIT tags for different purposes.

Let’s test it. Say we have version 2.0.1 in our package.json and we want to release a fix version 2.0.2.

We just run yarn bump-version:patch and the version is changed in package.json file, ready to be committed to GIT

Working with a build number

We wanted to be able, by single look at GIT history, to understand which commit relates to particular application build. This helps to understand what features are in what app version or what fixes are missing in users installation when solving production incidents.

Naturally we chose to store build numbers into GIT tags. Each merge to master represents build number increment and thus has its own numeric GIT tag:

On paper this sounds cool, but making it work in GitLab pipelines is not as straightforward as one would expect. This is a stage in our .gitlab-ci.yml that handles the build number update:

bump_build_number:
image: node:latest
stage: bump_build_number
script:
- source ./scripts/gitlab/setup-git-rw.sh
- source ./scripts/gitlab/export-CI_LATEST_BUILD_NUMBER.sh

# bump build number
- export CI_LATEST_BUILD_NUMBER=$(($CI_LATEST_BUILD_NUMBER+1))

# push updated tag into git repo
- git tag $CI_LATEST_BUILD_NUMBER
- git push origin $CI_LATEST_BUILD_NUMBER
only:
- master

This stage is executed before the release stage is started as that one already uses the newly created GIT tag when baking the app version into the code.

Let’s examine step by step what is happening here:

  1. We change GIT repository from read only to read/write
  2. We get last build version from GIT tags
  3. We increment it and store as a new tag and push the changes

Making repository read/write — when GitLab runs pipeline, it checkouts the repository in read-only mode which is sufficient for most cases but we want to be able to increment GIT tag. And this operation already requires write access to the repository so we want to tweak the setup a bit by running the source ./scripts/gitlab/setup-git-rw.sh script.

be aware — in pipeline config this script is called with source prefix command. This will run the content of the script in same bash instance as GitLab runner is using, otherwise the changes or ENV variables might not be visible/available to the runner context

Honestly, following bash scripts is not something I would hang on a wall in our office, but it does the job. Content of ./scripts/gitlab/setup-git-rw.sh:

#!/bin/bash
#
# Expected to run in GitLab job
#
# Configures job environment so it is possible to write into git repo.
# Variables are passed from GitLab config or job `script`/`variables`
#

# setup ssh for pushing into git repo
mkdir -p ~/.ssh && chmod 700 ~/.ssh
ssh-keyscan gitlab.com >> ~/.ssh/known_hosts && chmod 644 ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add <(echo "$SSH_PRIVATE_KEY")

# prepare git config for pushing remote repo
# replace http to ssh git url to use ssh key auth
GIT_REPOSITORY_URL=$(echo $CI_REPOSITORY_URL | sed -e 's/.*\@/git\@/' -e 's/\//:/')
git remote set-url --push origin $GIT_REPOSITORY_URL

# refresh git tags (gitlab uses fetch instead of clone which does not refresh deleted tags)
git tag -l | xargs git tag -d
git fetch --tags

Note: for a premium or self-managed version of GitLab it should be enough to use Gitlab project access tokens to replicate this DIY script.

Before running the script we had to also add a private SSH key into GitLab CI variables. Using this key the GitLab job is authenticating to the GIT repository with write access. Simply generate new SSH key by running ssh-keygen (do not add any password to protect the key) and copy the key into CI variables in GitLab (Project > Settings > CI/CD > Variables):

Also here is a script for getting the latest build number from GIT tags which is also used in pipeline setup. Content of ./scripts/gitlab/export-CI_LATEST_BUILD_NUMBER.sh:

#!/bin/bash
#
# Exports latest build number from the git tag into a variable later used in pipeline job
#
# refresh all tags from remote
git fetch --tags >/dev/null

# get latest tag - we search for numeric tags only sorted by creation date
LATEST_TAG=$(git for-each-ref --sort=authordate --format '%(refname:short)' 'refs\/tags\/[0-9][0-9]*' | tail -1)
# if tag not found, the default build number will be 1
CI_LATEST_BUILD_NUMBER=${LATEST_TAG:-1}

echo "Got version number ${CI_LATEST_BUILD_NUMBER}"
# export for use in CI stage
export CI_LATEST_BUILD_NUMBER

The rest is being solved in the aforementioned GitLab stage configuration. Now each time this stage is being executed it creates a new GIT tag 🥳 (do not forget to run git fetch --tags on local machine to see changes).

So, we have the app version and the build number updated, but how to use it in React Native app? 🤔

Using version and build number in React Native app

Let’s take a look on how to propagate app version and build number into an app. In our stack we are using Fastlane tool to orchestrate builds and releases.

React Native app consists of three parts that need access to the app version and build number. Those are:

  1. JavaScript code — in case you want to show this information in your application
  2. iOS project configuration — for AppStore to understand and use this information during release management
  3. Android project configuration — for Play Store to understand and use this information during release management

Fastlane works well with .env files, which is perfect as React Native can work with ENV variables via react-native-config library. Our .env file has following variables defined with default values that are overridden during build:

// .env
VERSION=1.0.0
BUILD_NUMBER=1

To update these values we use a custom script that reads the app version from package.json and build number from GIT tag and replaces them in the .env file. Let’s say this script is called setAppVersionInDotEnvFile.sh 🤓

#!/bin/bash
#
# Exports latest semantic version from package.json and
# latest build number from git tag and replaces in .env
# for later build process
#

ENV_FILE=${1:-".env"}

export VERSION=$(yarn run -s package-version)

git fetch --tags >/dev/null
LATEST_TAG=$(git for-each-ref --sort=authordate --format '%(refname:short)' 'refs\/tags\/[0-9][0-9]*' | tail -1)
export BUILD_NUMBER=${LATEST_TAG:-1}

# sed works a bit differently on mac than on linux
if [ "$(uname)" == "Darwin" ]; then
sed -i '' "s/VERSION=.*/VERSION=$VERSION/g" $ENV_FILE
sed -i '' "s/BUILD_NUMBER=.*/BUILD_NUMBER=$BUILD_NUMBER/g" $ENV_FILE
else
sed -i "s/VERSION=.*/VERSION=$VERSION/g" $ENV_FILE
sed -i "s/BUILD_NUMBER=.*/BUILD_NUMBER=$BUILD_NUMBER/g" $ENV_FILE
fi

Accessing ENV variables in ReactNative JavaScript

To access ENV variables from JavaScript code use React Native Config library. In our case:

import env from 'react-native-config';const config = {
version: env.VERSION,
buildNumber: env.BUILD_NUMBER,
}

Updating version and build number for iOS

Now we can use it in Fasltane Fastfile to set version and build number in xcode project:

desc "Set version and build number"
lane :sync_version do
sh("setAppVersionInDotEnvFile.sh .env")

version = get_properties_value(key: "VERSION", path: ENV['ENVFILE'])
buildNumber = get_properties_value(key: "BUILD_NUMBER", path: ENV['ENVFILE'])

increment_build_number(
xcodeproj: './ios/myproject.xcodeproj',
build_number: buildNumber
)
increment_version_number(
xcodeproj: './ios/myproject.xcodeproj',
version_number: version
)
end

Updating version and build number for Android

For android config we have to change just few lines:

desc "Set version and build number"
lane :sync_version do
sh("setAppVersionInDotEnvFile.sh .env")

version = get_properties_value(key: "VERSION", path: ENV['ENVFILE'])
buildNumber = get_properties_value(key: "BUILD_NUMBER", path: ENV['ENVFILE'])

increment_version_code(
gradle_file_path: "./android/app/build.gradle",
version_code: Integer(buildNumber)
)

increment_version_name(
gradle_file_path: "./android/app/build.gradle",
version_name: version
)
end

The sh script and both Fastlane lanes (for iOS and Android) will change files in the project so it would make sense to clean the GIT repo after each such build.

After this you are ready to run the build and release your app. If you are interested in how to do so using Fastlane and make the process from git push till having released an app in AppStore or PlayStore fully automated, follow us and read the followup articles on Fastlane setup with GitLab pipelines.

Stay in touch

In case the article was helpful we will be grateful if you will follow us here and support us with some claps 👏. In return we will share even more cool tech articles.

Feel free to comment and ask for details, share your pains and learnings.

This article was written by tech enthusiasts Andrej & Honza currently working at CreativeDock.

--

--