A while back I introduced Atomic Change Flow, which is a branching model we developed in MediaValet to support continuous integration of a product developed by distributed teams. At the time, the teams were working on multiple features in parallel and frequently blocked each other by merging incomplete features to common release branches. Refactorings and architecture made the use of feature-toggles almost impossible. Atomic Change Flow was introduced to keep changes atomic (i.e. separated from the rest) up until it was deemed ready for production.

The main idea is:

  • Create a distinct feature-xxx branch from master for each features
  • Create a single release branch from master, that is used to trigger the build and deploy pipelines. Use the same artifact to push to all environments.
  • To assemble a release, merge the feature branches into the release branch.
  • If you ever need to remove a feature from a release (because it’s incomplete, buggy, introduces regressions, or if you want to investigate which feature is introducing a regression), simply delete the release branch and recreate/reassemble a new one with only the features you want to keep.
  • Don’t ever commit directly to the release or master branches, only to the feature branches. You need to always be comfortable with deleting the release branch.
  • Once the release is pushed to production, merge back the release branch (or all the individual feature branches) to master. Once this is done, you can delete all the individual feature branches.

We have used this process in production for quite some time, it works well and it’s comfortable and safe.

This post aims at showing how to instrument it using Azure DevOps repositories and pipelines. It assumes you have some basic knowledge of how to use this platform ; here are the 101 documentation for Git repositories and for pipelines.

Starting a new feature

New features need to go into separate branches. These branches are created directly from the master branch.

Create a feature from master

Starting a new release

To start a new release delete the existing “release” branch (you don’t need to do that if the commit SHA of master and release are the same)

Deleting the release branch

Deleting the release branch - confirmation dialog

Then re-create it from master

Create release from master

Note: this in itself should kick the build

Build has kicked

Working on a release

Adding features to the release is done by merging those features to the feature branch. The simplest way is through a pull request.

Create a Pull Request

Then complete the PR right away. Be careful to not delete the feature-branch after merging.

Complete a Pull Request

I like doing a rebase and fast-forward merge, that gives the cleanest commit tree in my opinion (preserve commits from the branch):

Commit tree is clean

If you find this process too cumbersome, you can simply run it on a local machine and push to the repository. Some automation is possible.

The beautiful thing about preserving the individual feature branches is that, if you have issues stabilizing one of the features (e.g. bugs cannot be fixed, or the feature introduces regressions), you can simply squash the release, and start a new one with only the stable new features.

Assembling a release with multiple features is done by repeating the process for each feature.

Fixing bugs in the release is done by fixing the buggy feature-branch, then merging back to the release branch. Do not commit to the release branch directly. If you update the release branch you remove your capacity to delete it later.

Configuring the build

If you want builds to kick only for release, change trigger to the “release” branch:

Trigger on branch == release

Configuring the release

Configure the release to be triggered only for the release branch.

Trigger on branch == release

Then configure the process how you want it to be:

Linear pipeline

Then configure the relevant steps to be approved before deployment. The less manual validation you add, the more continuous your integration is actually going to be – ideally the test environment should be automatically deployed based on smoke tests results in the dev/integration environment.

Approval process has one approver for staging

You might want to change the default parallel deployment settings, so that releases not approved don’t prevent newer releases to be approved – either enable unlimited or Deploy latest and cancel the others.

Approval process set to "deploy latest and cancel the others"

The steps with approval will wait for their owners to approve it:

Deployment is waiting for approval on the staging stage

Optionally you can add a git tag in the last “production” step to mark the last commit as a release

Tagging release automatically when sent to production

Once a release goes to production, you must merge the “release’ branch back to master. Do that either manually or automatically in the pipeline.

You can delete the feature branches that have been merged into the release deployed to production. This can be safely done from the “Branches” tab of devops:

Delete branches that are not ahead of master

Every feature branch that is “0 ahead” of master can be removed.

From there, other live feature branches should be either re-based, or master should be merged into them, so that they start from the latest commit. You can see these by looking at the “behind” column.