Branching strategies for CI/CD

Like tabs versus spaces, branching strategies is one of those emotive topics that trigger heated debates both online and offline. While many hold strong opinions on the best approach, as with many things in software development, the right answer depends on context. With that in mind, let’s look at what branching strategies are and how they tie into CI/CD.

Put simply, a branching strategy is your team’s agreement on how and when to create and merge branches in version control. How you set up your version control system and use branches will impact how you set up your CI/CD pipeline, so it’s important to choose a model that meets your needs.

Branching in DVCS

Branching strategies became a consideration for development teams with the rise in popularity of distributed version control systems, particularly Git, which made branching easier.

With distributed systems, there are multiple copies of the repository and therefore multiple sources of truth (although it’s common for teams to nominate a central or primary copy). You and your team members work on your changes in parallel on your copies of the repo, sharing your work by pushing changes to other copies of the same repo.

Git made creating branches and merging commits from different branches a very lightweight exercise. In Git, a branch is just a commit (or set of commits) labelled with a particular name. Branches are ideal for containing a set of changes, such as work on a new feature or a hacky experiment.

You can then share your changes by pushing the branch to another repo and/or merging it with another branch – such as master – to combine it with the rest of the codebase. You can also discard the changes if you decide against them. When you merge two branches together, Git takes care of aligning the commits on each of them and avoids much of the complexity associated with merging in other version control systems.

Using branches for CI/CD

With continuous integration, the focus is on getting everyone in your development team to commit their changes frequently. This means you can regularly test that everything works as expected, rather than spending weeks or months trying to integrate different workstreams after coding is complete. In turn, it is possible to release software updates more frequently, unlocking the benefits of continuous delivery and deployment.

Deciding where changes should be committed, when automated builds and tests should be run, and where updates should be released from is where the use of branches interacts with CI/CD. The simplest approach – at least conceptually – is to avoid branches altogether. In trunk-based development, everyone commits changes regularly to the master branch on a central repo, kept in a releasable state and frequently deployed to production.

Although trunk-based development can work very effectively, particularly if you have a mature CI/CD setup and are running continuous deployment to a hosted system, it also raises some challenges. Branching strategies provide alternative ways to manage code changes, with different benefits and disadvantages. The following are some of those most commonly used by development teams running CI/CD.

Feature branches

As the name suggests, feature branches are created to keep individual features separate from the rest of the codebase. Keeping work in progress out of the master branch can make it easier to keep master in a deployable state and – if you’re deploying directly from master rather than a release branch (see below) – avoids the potential for unfinished functionality to be deployed to production. You can still share your work with the rest of your team by pushing your branch either to the central repo (if you’ve nominated one) or to any other repo.

The main downside with feature branches – and the criticism often levelled at them from advocates of trunk-based development – is that by delaying the integration of changes until the feature is “complete”, you lose out on the benefits of continuous integration. This risks merge conflicts and introduces more complex bugs that take longer to fix than if changes had been committed iteratively.

You can mitigate these issues by setting up your CI server to run automated builds and tests on feature branches as well as master (or whichever branch you use to prepare releases). This ensures you get the benefit of immediate feedback on what is being built, while also reducing the risk of issues arising when you do merge changes.

One way to minimize merge conflicts is to keep feature branches short-lived – up to a day or two at most. Another option is to rebase the branch on top of or merge changes from master regularly. This brings your feature branch up to date with all other changes committed to master. However, if you have a large number of concurrent feature branches all delaying commits to master, there’s still a risk of conflicts.

Release branches

Whereas feature branches provide a way to manage work in progress, release branches are used to “harden” changes prior to release. Release branches are well suited to a continuous delivery model, where updates are delivered at intervals rather than as soon as they’re ready, and make it easier to support multiple versions in production.

With a release branch workflow, once the changes planned for a particular release are ready, a release branch containing the relevant changes is created (either from master or another development branch), after which no further features are merged in. Meanwhile, the development of features planned for other releases can continue to be merged to master or elsewhere.

Release branch builds are then put through a series of automated tests, and any bug fixes are made on the release branch, followed by further rounds of testing until you’re ready to release. Those bug fixes should also be applied back to the master branch to ensure they are included in future product versions.

Keeping your release branches for as long as you need to support each version of your software makes it much easier to deploy fixes to old versions.

When an update is required, it can be developed in a hotfix branch or on master and tested as normal, and then applied to the release branch (for example, via a cherry-pick).

It will then run through the CI/CD pipeline on that branch to ensure there are no issues on that version before deploying the change. Alternatively, you can directly do the work on the release branch and apply it back to master as applicable.

Hotfix branches

Hotfix branches operate similarly to feature branches, but it’s worth mentioning them for completeness. A hotfix branch aims to get a bug fix out to production as quickly as possible.

You can create a hotfix branch from master or from a release branch, depending on where you deploy. As with feature branches, you may choose to run some of the CI/CD elements on your hotfix branch before merging it to the release or master branch, where it can undergo further automated testing before release.

Wrapping up

We’ve looked at some of the most common ways to use branches in your CI/CD workflow, but there are many more variations out there.

The most important thing is to find a strategy that works for your specific context. Adopting a DevOps mentality of continuous improvement and staying open to different options will allow you to keep adjusting your approach to your needs as your CI/CD practice matures.