CodePush version picker

Tom Oakley
Creating TotallyMoney
7 min readMar 29, 2023

We use CodePush at TotallyMoney to allow us to iterate quickly on our React Native app; while it has its caveats (slow to rollout, need to update the binary on the App Store/Google Play if there is a change in the native files) — we find it really useful to allow us to get work out quickly and asynchronously in our squads.

Below is a gif of the picker in action:

Quick screen recording of moving between CodePush versions with the picker

And here is a link to a gist to the complete code for the picker, which we will go through line by line below.

The Problem

However — we had a problem; we found it tedious to deploy changes. While the API allows us to build a stage version for CodePush (which we can then test internally with our stage app), and then “promote” that release to production, it was an annoying process. This meant we were less likely to do releases, and more possibilities of errors. Here is what our process looked like:

  • Once stage release for CodePush has been built automatically via our CI pipeline, we’d go on the stage app for iOS and/or Android and test it
  • If all looked good and we wanted to release it to production, we’d need to go to Microsoft AppCenter, then to the iOS and Android apps and find the CodePush build on each app to promote
  • From there we could either do it via the AppCenter UI, or we had a small shell script that took the two labels (for the iOS and Android builds), and ran the appcenter-cli commands to promote them

This was slow to do, and while using the script there was possibilities of errors — for example, getting the iOS and Android labels the wrong way round.

This all resulted in much slower iteration for our native app and under-use of CodePush. In addition, we had no proper reporting once a version had been released — making it difficult to be transparent to the rest of the business.

The Solution

To fix this, I wrote a version picker that can show you the latest stage builds for CodePush for the current version of the app (via package.json version property), where you can select the build. The script will then find the equivalent version for Android, and promote both builds to production . Furthermore, I added an automated Slack message which would get the git log of commits to the Native App repository and post them in a Slack channel. Let’s take a look at the code.

To run the picker, we run a shell script called picker.sh:

REQUIREMENTS="jq pip3 python3"
for i in $REQUIREMENTS; do
hash "$i" 2>/dev/null || { \
echo "$0": "I require "$i" but it\'s not installed."; exit 1; \
} \
done
pip3 install pick==1.6.0 - quiet
VERSION=$(jq -r ".version" package.json
| sed -r "s/([0–9]\.[0–9][0–9]\.)([0–9])/\1x/")

First of all, this checks for some bash requirements: jq, for parsing JSON, pip3 (Python package manager) and Python3. It then installs pick, which is a python package for showing a picker CLI. Lastly, it gets the version of the app from the version property in package.json (using jq).

echo "Getting iOS stage deployments…"
IOS=$(npx appcenter codepush deployment history -a appcenter-org-name/app-name stage - output json
| jq -r - arg VERSION "$VERSION" '.[]
| select(.[2] == $VERSION)
| { label: .[0], notes: .[4] }' \
| jq -s '.[-10:] | reverse | .')

This command gets the iOS stage deployments for CodePush and outputs in JSON format; we have a similar command for Android. It then does some manipulation to the JSON using jq:
- It gets only the builds scoped for the current version
- It then extracts the label and notes properties and formats them into a simple JSON object
- It takes the last 10 versions
- It reverses the list so the latest is at the top

The echo command at the top is just to provide some feedback for the user (an engineer usually) running the script — getting this data usually takes a bit of time and we do it 3 times — iOS stage, Android stage and then also we get the current production deployment data:

LATEST_PROD_RELEASE=$(npx appcenter codepush deployment history -a appcenter-org-name/app-name prod - output json
| jq -r - arg VERSION "$VERSION" '.[]
| select(.[2] == $VERSION)
| { label: .[0], notes: .[4] | split("\n") | .[1] }' \
| jq -s '.[-1:] | reverse | .[0].notes' | awk '{print $1;}')

This does very similar to the command above, but also formats the data a bit more, and only gets the latest release to prod.

After all this data is fetched, we re-format it back into a JSON object:

VERSION_INFO=$(echo "${IOS} ${ANDROID}"
| jq -s '.[0][] as $ios
| (.[1][] | select($ios.notes == .notes)) as $android
| { "ios": $ios.label, android: $android.label, notes: $ios.notes
| split("\n") | .[1] }' \
| jq -s '.')

This command echos both the iOS and Android data we retrieved, and then loops over it to get the CodePush ‘notes’ field — in our setup this is the commit message (PR title usually) to the repo. This helps the engineer to identify which release they want to promote to production. It ensures the iOS and Android labels match, then it puts the iOS build label, Android build label and notes into an object.

Finally for this script, we pass it to the Python script which runs the picker:

python3 "./scripts/picker.py" "$VERSION_INFO" "${LATEST_PROD_RELEASE:1}"
if __name__ == '__main__':
signal(SIGINT, handler)
VERSION_INFO = sys.argv[1]
PROD_COMMIT = sys.argv[2]
versions = json.loads(VERSION_INFO)
selected = pick.pick(
versions,
' Android | iOS | Notes',
indicator='*',
options_map_func=get_label)
deploy(
selected[0]['ios'],
selected[0]['android'],
selected[0]['notes'],
PROD_COMMIT)

Here this script is extracting the two arguments we passed in, then passing versions to the pick package along with a method called get_label:

def get_label(option):
return f"{option.get('android')} | {option.get('ios')} | {option.get('notes')}"

This method simply gets the 3 fields we put in to VERSION_INFO and displays them for the picker. It shows them in the same order as the second argument to pick.pick above which is the column headers.

Finally, we run a method called deploy on the selected version (which also gets the PROD_COMMIT variable which has been passed through without change from the first script:

def deploy(ios, android, notes, current_prod_commit):
new_prod_commit = notes.split(' ')[0]
p = subprocess.Popen(
["./scripts/deploy.sh",
'-i', ios,
'-a', android,
'-c', new_prod_commit,
'-p', current_prod_commit],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
while True:
out = p.stdout.read(1).decode(sys.stdout.encoding)
if out == '' and p.poll() != None:
break
if out != '':
sys.stdout.write(out)
sys.stdout.flush()

This method runs a 3rd script called deploy.sh and passes in the various arguments to it, which then promotes the builds using appcenter CLI:

npx appcenter codepush promote -a appcenter-org-name/app-name - label ${IOS} -s stage -d prod
npx appcenter codepush promote -a appcenter-org-name/app-name - label ${ANDROID} -s stage -d prod

This command takes the label of the build, the deployment environment it’s coming from (stage) and where it’s getting promoted to (prod).

Finally, we wanted some better reporting for other stakeholders within the business to know about a new CodePush release, and what it contained. This is possible via Slack Webhooks:

Below is a bash method called notify_slack, in the same deploy.sh script, which runs after the CodePush releases have been promoted.

notify_slack() {
CHANGELOG=$(git log - pretty="- %s%n" $ACTIVE_COMMIT_HASH..$TARGET_COMMIT_HASH
| awk '/^- feat|^- fix|^- data|^- perf/')

GITHUB_LINK="https://github.com/path/to/nativeapp-repo/compare/$ACTIVE_COMMIT_HASH...$TARGET_COMMIT_HASH"
TEXT_BODY="New CodePush release: iOS: ${IOS}, Android: ${ANDROID} by $(whoami)"

if [[ ! -z "$ACTIVE_COMMIT_HASH" && ! -z "$TARGET_COMMIT_HASH" ]]; then
TEXT_BODY="$TEXT_BODY. Changelog:\n$CHANGELOG\n<$GITHUB_LINK|View changes on GitHub>"
fi
curl -X POST \
--data "{\"text\": \"$TEXT_BODY\", \"channel\": \"#some-channel\"}" - header 'Content-Type: application/json' \
--url <SLACK_WEBHOOK>
}

This method creates a changelog by comparing the current prod release commit hash (this is where the -p current_prod_commit comes in from the picker.py Python deploy method, which was originally generated in the first script — LATEST_PROD_RELEASE variable) and the new commit hash that was selected. At TotallyMoney we use semantic commit labels to label our Pull Requests; for example, feat: adds new feature to the app or fix: fixes this bug. These commits are user facing i.e they make a difference to the app. The command above in CHANGELOG also gets the data and perf semantic commits; we have other types such as refactor or test which aren’t user-facing so PMs and other stakeholders don’t need to know about them in the changelog.

The notify_slack method then creates a link to GitHub allowing for more in-depth comparison of changes. It then writes some text and sends it to Slack via a webhook.

I hope this is helpful. It certainly helped us to release via CodePush more often — we probably do a CodePush at least once a day, sometimes more. Please let us know if you have any questions, we’re happy to answer them!

Useful Links:

--

--

Tom Oakley
Creating TotallyMoney

Senior Engineer @ TotallyMoney, 3D prints and builds ergnonomic keyboards, runs