Delivering code is how developers present their hard work to the world and this makes it, in my opinion, one of the most important parts of our job. However, knowing when code will be ready to deliver can be hard. That’s why my team started moving towards Continuous Integration (CI), a process that allows us to upload our work to the main development branch on the go. Here’s how we started using CI and what we’re currently doing with it.
I joined the iOS engineering team at Over early in 2018. Over is a photo and video editing app that gives you the power to create visuals in an easy and accessible way.
When I joined the team, we were using a process called GitFlow to isolate features and release changes to the App Store. This process allows everyone to do their own work and upload it to the main branch when they are ready. While we are not a particularly big team, even eight people trying to coordinate changes in the codebase using GitFlow was daunting. We found ourselves struggling to get releases out, because we were dealing with plenty of merge conflicts and semantic code conflicts right at the end of feature work when we were supposed to be ready to release. Fixing these conflicts resulted in having to do a lot of extra debugging, bug fixing and testing.
This made it difficult to gauge when we would be able to release and it also decreased our confidence in releases.
Whilst my first priority is to write code, I have a natural tendency to try and improve all manner of processes, and GitFlow raised a red flag for me. In my previous role, my team had also worked with GitFlow before scrapping it in favour of CI. This experience made it easy for me to identify the bottlenecks that GitFlow was creating for us and how CI could help us eliminate them. I explained to my team how CI avoids long lived feature branches (which are a natural product of GitFlow) and instead merges each developer’s work onto the main code branch as soon as possible. We agreed to try it out and are now busy moving towards CI.
This has improved our communication during code reviews and given the team more confidence in telling product managers when a feature will be ready. It has also allowed us to ship more features, more often, leading to better user experiences and ultimately happier users. Here’s how we have been doing it.
Our old process and the headaches it caused us
GitFlow had been the preferred method for delivery for a while before I joined Over. On the surface, GitFlow is inviting because, as mentioned before, it allows developers to do their own work without worrying about it getting tangled up with other developers’ work.
When using GitFlow, a developer’s code is kept separate from the main branch, in a feature branch, until just before it needs to be released. At this point, a release branch is created from the main branch and the feature branch is merged into this release branch. The release branch is then deployed to production. All of this seems fine until more than one developer wants to release and get their work into the shared release branch. This tends to result in last minute panic.
The ‘independent work’ benefits of GitFlow seemed alluring but it was letting us down by hiding file level and semantic conflicts until the different long lived feature branches were merged into the release branch. These issues could be hidden for weeks or even months which led to an almost impossible task of accurately being able to determine when we’d be able to ship to production.
What we did to move away from it
While facing similar release issues at my previous job, I was introduced to CI by a software engineering coach. He pointed me to this blog post by Martin Fowler, which gives a great overview of the technique and how to use it. He also does an excellent job at describing how feature branches fit in with CI and why it is bad getting sucked into long lived feature branches (as used in GitFlow) in his article called FeatureBranch.
Our system was something we needed to work on urgently, and so, having experienced the benefits of CI before, I pitched the idea of trying it out during one of our informal lunch-and-learn sessions. (A lunch-and-learn is merely a chat we have about cool stuff we know while we have lunch 😁) The team was keen and so, we decided to try out CI for our next big release.
CI to the rescue: What it is and how it works
It is important to note that CI is a practice, not a tool.
There are many tools that facilitate CI (in our case it’s buddybuild), but no tool can give you CI since it is a practice that developers in a team need to adopt.
When using CI, a developer’s code is integrated into the main branch as often as daily (sometimes even as often as every commit). The main branch is kept in a releasable or stable state and can therefore be deployed to production at any time. This is a far cry from the normal rush we experienced integrating everyone’s work and getting a release out amidst major conflicts.
In its purest form, CI works like this:
- You check out the latest version of the code from the main development branch onto your local machine.
- Using your local copy, you make all the changes you require, including changes to the production code as well as automated tests.
- After making changes, you run a build – i.e. you run the app and run the automated tests.
- If the build and tests pass, you commit the changes.
- You repeat steps 2 to 4 until all the required work is complete.
- Next, you pull any newer changes from the main branch, since other people have most likely added code.
- You rebuild and fix any compilation issues or failing tests.
- You push your local changes to the shared code repository.
- Finally, you run a build (and tests) on an integration machine, creating a releasable app.
If the build on the integration machine passes, you have a new version of your app ready to be released.
Keeping the changes small and integrating often in this way ensures stable production code with few bugs.
Of course build failures do occur, but since everyone stays up to date with the main branch frequently, such failures never last long.
How we applied CI to our project
Keeping the ‘pure’ CI scenario in mind, my team at Over needed to make some adaptations for it to suit us. Being a fully remote, distributed team, often working asynchronously, we rely on pull requests to get our code into our main branch. This provides a level of visibility that is very important for a team that is not always in the same physical space.
What is a pull request, you might ask? It is a way for developers to request that their code be pulled into a target branch. A pull request shows you all the changes between your branch and the target branch, and that’s why it is often used for code reviews.
In order to accommodate pull requests, we needed to adapt the list above by including a pull request before local changes are pulled into the main branch – i.e. before step 8 above. This is what our process currently looks like:
- After checking out the latest code from the main branch, we create a feature branch. The goal is for these feature branches to be short lived – a day or two.
- We make our changes on these local, feature branches.
- After making changes we run the app and any associated tests (manual or automated).
- We commit the working changes.
- We repeat steps 2 to 4 until all the required work is complete.
- We pull any newer changes from the main branch (since other people have most likely added code).
- We rebuild and fix any compilation issues or failing tests.
- When we’re ready, we open a pull request to merge our changes into the main branch.
- An opened pull request results in an automated build and test cycle handled by buddybuild.
- Once a pull request is merged, buddybuild kicks off another build and test cycle and then deploys our app.
Getting feedback about the build status on a pull request allows us to fix any broken builds immediately. You might wonder what we do with features that aren’t due to ship soon: Those get the same treatment described above. However, this is where feature flags play a crucial role. Feature flags allow us to have all our code in the same codebase, even if some code is not yet available for general consumption. We gave our implementation of feature flags its own blog post, which you can read here.
The results we saw after we started using CI
Less file level conflicts? Yes.
Less semantic code conflicts? Yes.
Less of a mad rush to get releases shipped? Yes.
More confidence in shipping our features? Yes.
More confidence in estimating when a feature will be shipped? Yes.
The above results were promised by CI and it delivered. Pun intended. We did, however, see two more results that hadn’t necessarily been expected:
- More meticulous code reviews, and
- More thorough testing.
Since the new system ensures that all our pull requests are being merged into our main development branch, there are no more chances to try and fix things later. Once a pull request is merged we deem the code ready to be shipped. _This has resulted in a greater sense of accountability and clearer communication within our team. _
Using CI has opened my eyes to what an effective process can do to help teams communicate more effectively, work productively and be accountable at an individual level. If you’re keen to learn more about CI, check out this article I recently read to hear the reason Dave Farley and Jez Humble declared in their book, “we can’t emphasise enough how important this practice is in enabling continuous delivery of valuable, working software”.
Naudé Cruywagen is an iOS engineer at Over – a Silicon Valley backed startup based in Cape Town. He has also been involved in building some of the biggest apps in South Africa. His involvement is mostly technical but he immerses himself in all aspects of delivering great software to end users since he believes that great software is built by inspired, motivated teams of people.