Automating releases with a Mobile Release Pipeline
At Memrise we like our apps to be updated as often as possible, ideally in a weekly cadence. We’ve noticed this gives us numerous advantages versus for example releasing only when big features are ready:
- We can fix non-critical bugs as we go, without the need for hot-fixing.
- We can release features early and get feedback, improving them over the following weeks.
- Because of the smaller time window, releases are relatively lightweight, meaning we introduce fewer issues than what we would introduce if we waited longer.
However, this doesn’t come for free. Making a release means there’s an effort behind it, just to name a few things:
- Someone has to be responsible for all the release admin, such as coordinating the release cut-off, creating the Release Candidate, or uploading the approved release to Google Play Store and the App Store.
- Regression testing by the QA team, to make sure we haven’t introduced new issues. And fixing any issues that we might encounter.
- Monitoring the rollout, making sure crashes and ANRs (Application not responding) are in the expected threshold.
All the above is normally done by the Release Captain, with the help of the QA team. However, this process can be expensive, repetitive, and frankly, quite boring. And in engineering, what should we do when we have something expensive, repetitive, and boring? AUTOMATE IT! 🎉
The goal
We could say the main goal of a mobile release pipeline is to help us reduce the time it takes to generate a release. In order to do this, it will need to coordinate all the moving pieces / third-party services involved, which in our case are GitHub, Slack, CircleCI, Jira, AppCenter, Google Play Store, and the App Store.
Our dream was for our release pipeline (called newton in honor of one of our doggie members at the office) to do only 2 simple actions to update our apps in the store:
- Type
/newton build_rc
in a Slack channel. - Once approved, type
/newton rc_accepted
in the same channel.
This is a bit over-simplified, but in reality, it is pretty much what the mobile team at Memrise has achieved.
Firebase Functions
Our Release Pipeline is fueled by Firebase Functions. What are these? From their documentation:
Cloud Functions for Firebase is a serverless framework that lets you automatically run backend code in response to events triggered by Firebase features and HTTPS requests. Your JavaScript or TypeScript code is stored in Google’s cloud and runs in a managed environment. There’s no need to manage and scale your own servers.
In a nutshell, when you write a Firebase function, you get an URL that will run the function when it is hit. You don’t need to worry about hosting, server configuration, balancing load, or general maintenance since Firebase does everything for you. Deploying functions is as simple as running a single terminal command.
To write Firebase functions, you will need to use node.js and either Javascript or Typescript. Because we’re a team of mobile developers, we chose to use Typescript since we’re more familiar with typed languages.
Just show me the code
To see how this works in practice, let’s follow a release process to see what’s done under the hood.
The entry point to our release pipeline is Slack. The first thing we’ll normally do is type /newton <command>
in a channel called #android-newton
or #iOS-newton
. Thanks to a Slack integration, you can create a hook that will call an URL every time a message is written in these channels. And in our case, we're going to call the URL of a Firebase function named slashCommand
.
A sneak peek of the function that handles slashCommand
will give us an idea about the sort of actions that we can handle:
Some of these commands have just one implementation across Android and iOS, like for example updating translations:
Others, however, will be platform-specific, since the release process of an iOS app can vary from the Android one, despite having common steps.
Let’s now focus on the command build_rc
for Android. This command will generate a release candidate that once tested by the QA team, will be released to Google Play. Let's go step by step to understand all the things the pipeline does for us, so we can appreciate its beauty 😉
build_rc steps
1. Enter /newton build_rc
in the #android-newton
Slack channel
2. Pipeline flow reaches the function createAndroidReleaseCandidate
3. Generate release candidate version name (i.e 2022.02.01.0)
4. Fetch all the tickets from Jira’s API that are part of the release candidate. Once formatted this becomes our release notes, which we’ll use in quite a few places like pull requests or Slack.
5. Update the current Jira release, so any tickets that go into develop
are marked as part of the next release.
6. Using GitHub’s API, create a release-candidate
branch from develop
and a PR to merge release-candidate
into release
(this being our main branch, the one that has the same content as the app in the store), with the version code and name updated. The body of the PR will be the release notes we fetched before.
7. createAndroidReleaseCandidate
execution finishes here. However, we haven't created our debug and release builds yet. Creating a GitHub PR in the release-candidate
branch will trigger ./gradlew assembleRelease bundleGoogleRelease
and ./gradlew assembleDebug bundleGoogleDebug
in the CI, generating our aab
files (as in, our app!), which are then updated to AppCenter.
8. Now that we’ve got the URLs to our aab
files in AppCenter, we call a Firebase function called updateAndroidReleaseCandidate
. This will first update the release candidate PR adding the links to our builds.
9. Next, we create a GitHub Release, where we will upload the aab files along with the proguard mapping files. Whenever the release candidate is approved, we will get the release aab from here.
10. Finally, we post the release notes and the build links in a Slack channel called #android-rc
. It might be worth noting that we split the release notes by user-facing changes and others by using a Jira field that the pipeline looks at when formatting the release notes.
It is now time for our awesome QA team to do a regression on the build, finger crossed no issues are found 🤞
rc_accepted steps
On the RC being accepted, the QA team will run /newton rc_accepted
in the #android-rc
channel, which will:
- Merge the release candidate PR, meaning now
release
is up-to-date with the latest changes. - Post final release notes in a
#releases
Slack channel. - Merge
release
intodevelop
- this way we make sure the version name and code are up-to-date, plus we add any fixes that we might have applied on top of therelease-candidate
branch. - Mark the Jira version as released.
At this stage, the Android team can just grab the aab from GitHub Release and drag it into Google Play. There’s the potential to actually automate this step too, to streamline the process even more.
That was quite a lot of steps, right? Imagine having to do them manually every week 😅 On top of this, some of these steps are very error-prone, so by delegating them to a machine we make our lives easier.
Other pipeline goodies
The great thing about the pipeline is that you can build on top of it all sort of cool automation. Some examples:
androidScheduledUpdateTranslations
: a scheduled function that runs every morning updating the app translations.
updateTicketWithBuildUrl
: every time a ticket has a PR linked where the checks passed, we leave a comment on the Jira ticket with the build, so anyone in the company can pick it up to give it a go if they want.
updateAndroidCoverage
: coverage comparator tool in PRs. Discussed in detail in this article.
Conclusions
Frequent releases are great and very important, but without proper tooling around them, they can take a serious productivity toll in the long term, on top of being error-prone. A release pipeline is a great solution for any medium-big team that wants to release their engineers from this tedious process, so they can focus on building awesome products.
PS: Building the release pipeline was a team effort by the whole mobile team at Memrise, so BIG KUDOS to everyone who took part in it, you’re awesome! ❤️