The Four Constraints
Making native applications for Apple's App Store and Google's Play Store presents a set of constraints which are different from other platforms (notably, web applications). At AWeber, our mobile team set out to find a way to structure a version control methodology that works well in this unique environment.
In brainstorming our strategy, we identified four specific constraints that we needed to account for:
- Releasing mobile application software is not immediate.
- There may be long running feature branches for code written against beta APIs.
- End users may not be running the latest version of our software.
- A hot fix may need to be released at anytime.
Let's elaborate on those constraints before discussing our solution.
Releasing mobile application software is not immediate
In order to make your native iOS or Android application available to the general public, you’re required to release through the Apple App Store or Google Play Store. Each of these entities have their own requirements for the applications that will be served in their stores. This introduces a period in which the code is feature complete, but isn't yet available to the public. Apple’s process of manually reviewing all applications submitted could make that period several days long (Google's release process is faster, but still not immediate). Because of this delay in our software release process, we needed our version management strategy to account for a code base that wasn't quite released, but also wasn't quite under development. While in review, that specific code base is no longer getting new features, and it isn't actually in the end user’s hands either.
Long running feature branches
Earlier this summer, Apple announced iOS 8 in conjunction with releasing a beta version of the OS. Not only did this new OS have many great new features for developers to enhance their apps, there were also some “we strongly suggest you use this” API changes that came from Apple. Unfortunately, while available to developers in beta pre-release versions of their SDK and OS, use of these new APIs couldn't actually be shipped to end users until iOS 8 went live (which was an unknown date until a week before the release). This left developers writing code that couldn't actually be shipped until sometime far in the future. The AWeber mobile team wanted to start writing code that took advantage of several iOS 8 specific features, but we also knew there would be at least one intermediate release of our application that would need to go live ahead of iOS 8 being released. As a result, we needed our version management strategy to be able to support a long running feature branch where we could develop these iOS 8 enhancements in isolation from code that could be released any number of times ahead of the iOS 8 branch going live.
End users may not be running the latest version of our software
Mobile native applications are actually packages of software that get installed on end users' devices. Users find our applications in the stores, download from there, and also use the stores as the central repository for downloading updates to the applications. Also, users may not update their applications. This happens for many reasons - users may forget to update their applications, users may not have bandwidth to update their apps, users’ devices may not support the latest update an application requires, etc. As developers, we are at the mercy of our end users to keep their local copies of our applications up to date.
While we are excited for our users to try out our latest features, we are also excited to retire old pieces of our infrastructure that may be in place to support old versions of our software. Supporting old versions of our applications is a fact of life for mobile developers. We need to ensure server-side APIs are backwards compatible with old versions of our applications. If a customer calls AWeber with a support question for an old version of our application, we need to be able have access to a functional version of that code base to help debug a problem. Thus spawned another requirement of our version strategy: access to old versions of our code base with an easy identification of earlier releases.
Hot fixes may be needed at anytime
In order to best support our customers, we needed to be able to react to an issue in the live version of software product at any time. We needed a solution in our version management strategy that would enable us to make an isolated change to a given version of our released application while also accepting that change into whatever other ongoing development was happening at the time. For example, if we identified a legal need to make a change in our live version of our application, we would need to make that change and release it as quickly as possible, while also minimizing risk of the release by isolating the amount of other change in the release. We call this our “hot fix” requirement.
The solution - a modified “git flow"
In setting out to determine our process, there were a couple of prerequisite tools we knew we wanted to use: Gerrit, Git, and GitHub. Prior to launching our first version of our native mobile applications, we established a development workflow using these tools (I’ll save the details of that for another post). Our versioning strategy must be compatible with those tools.
Having bounced ideas around our engineering department with a lot of brainstorming sessions around the concept of "plussing," we settled on a process that looks a lot like “git flow.” Git stresses the use of branches, and "git flow" provides some structure on how to decide where branches are created, and where branches get merged. There’s even a tool that one may use on top of git to help with “git flow.” We haven’t found a need to use that tool yet.
The point is, it would be silly not to leverage the "fast and easy" branching that git provides for, but that can also make it easy to make a mess. Below are the "Big Ten" conventions that we've come up with that are currently followed in our development process to account for our "four constraints" while also making things consistent and clean across the platforms, and across the whole lifecycle of a release.
The "Big Ten":
- There are two long living branches: Master and Develop.
- The HEAD of the Master branch represents the latest code that Apple or Google has, which may be released or in review by them.
- All new work is committed and pushed against the Develop branch, unless: it’s part of a hot fix, or needs to be part of a feature branch.
- It is the responsibility of the contributors to a feature branch to regularly merge the Develop branch into their branch.
- The Develop branch is merged into the Master branch at the time of a submission to Apple or Google
- Feature branches are branched off of the Develop branch.
- A feature branch is required if it is unknown whether the code going there will be part of the next release or not.
- Feature branches only get merged into the Develop branch, and that happens when the feature is complete, and known to be ready for inclusion with the next release of software
- Hot fix branches are branched off of the Master branch (usually the most recent tagged release), and when complete, merge everywhere: Develop, Master, and all feature branches.
- Once an application is approved and released by Apple or Google, the associated commit in the Master branch is tagged with the version number of the corresponding released application. Thus the most recent tagged release represents what is in customers hands.
This model supports our requirements
Releasing software is not immediate
We can support the intermediate state of software deployment during which our applications are staged with Apple or Google by using our Master branch. At this point in the process, the code is at the HEAD of the Master branch, but not yet tagged.
Long running feature branches
Well, our feature branches are intended for just this. When we started our work on our iOS 8-requiring features, we created a separate feature branch and all the work goes there. Once it’s ready to go live, we will merge that into the Develop branch. And once it’s sent to Apple, we will merge it into the Master branch.
Supporting old versions of our application
By tagging each release that ends up in our customers hands, we can easily jump to older code bases to debug any issues that may be reported from customers.
Branching off of the most recently tagged release enables us to insert an isolated code change to fix a specific issue, while minimizing unnecessary changes in the release. Merging the hot fix branch into all other branches ensures that the code is also included with all future releases.
On the AWeber mobile team, we're always learning and trying to improve our process. Chances are, by the time you’ve read this, we’ve further enhanced it! We’d love to hear from you. How do you solve these problems? Is there a case to consider that we’ve missed?
If you’re curious about “git flow,” there’s a lot of information available through Google. One picture we really like can be found at Vincent Driessen's site NVie.com.
Andy Obusek is a Senior iOS Engineer at AWeber.