Creating a Pull Request workflow in Azure DevOps
By: Andrew Craven
Tags: Pull Request, Azure Devops, Trunk Based Development, Docker, Continuous Integration

Photo by rawpixel.com from Pexels

Background

I’ve recently moved from a client with a very mature microservice architecture to one whom is just embarking on their microservice journey; mature vs green, AWS vs Azure. I had some ideas about how we should work, discussed them with the team and we set off to quickly make them reality. Or so we thought!

To provide some context, I have used the Trunk Based Development (TBD)¹ model successfully before, using GitHub Pull Requests and Thoughtworks GoCD. There are some great plugins for GoCD that integrate with GitHub that allow teams to validate their Pull Requests with the same rigour as that applied to the trunk.

In summary, we would like the master branch (the trunk) to be protected, and only changed via a Pull Request (PR). Such PRs should only be accepted if the same test suite that is applied to master is successful. Assuming PRs are up-to-date, we will have confidence that the trunk remains intact when the PRs are accepted.

What else?

Personally, I like to see the PR acceptance being subject to an approval too. If a pair have worked on the changes, then either should be able to approve the PR. If only a single person has been involved, then someone else should have to approve the PR. This ensures that at least 2 pairs of eyes have seen every change. Obviously more people can be involved in the approval process if you wish.

I also recommend squashing multiple commit PRs into a single commit on the trunk (the PR branch can be kept alive for a period of time for reference if necessary). For me this has a few benefits; a) it removes any changes that were reverted across commits, any tiny iterative commits and also people’s brain farts, b) it streamlines the trunk history into a single serialisable list of commits, removing potential hideous Git trainlines and allowing developers to easily see what’s changed, c) assuming only the net changes are reviewed, these will be the contents of the squashed commit.

Cracking on

One of my reasons for writing this post was due to the lack of resources in this area. I’m new to Azure and Azure DevOps, but familiar with the concepts of this workflow, so find it incredibly frustrating when I can’t easily set-up processes such as this by experimentation or reading documentation. Azure DevOps has a lot of potential, but like my client’s journey into microservices, it just seems a bit…green. And don’t get me started on the hidden buttons that appear on hover — who thought that was good UX?

Having used AppVeyor for a number of years, I was pleased to see configuration-as-code supported, but as you’ll discover below, we are left wanting. Simply speaking, add an azure-pipelines.yml file to your repo and away ya’ go…well, almost. One has to explicitly create a pipeline in Azure Devops for this file to have an effect. If the respective repo is hosted in Devops (which it is in this case), then I believe this explicit step shouldn’t be required.

I won’t delve into too much detail of the initial solution, but it wasn’t apparent to me (either through fault of my own or the fault of the DevOps UX and associated documentation), that multiple pipelines could exist targeting a single repo, but using different .yml files. Therefore what I initially set-up (below), was somewhat of an abomination, but in equal measures, a triumph of hacking; I had managed to trick the build numbering scheme to not consume [what would be trunk] version numbers when building PRs. Whilst this may appear to be vanity, we would like our build artifacts’ versions to include a -pr suffix for easy identification.

Eureka!

Having stumbled upon 2 pipelines existing targeting the same repo and the same azure-pipelines.yml file completely by accident, I dug around further and discovered I could change the .yml file relating to a pipeline. From there, I realised I could change the pipeline that was used to validate the Pull Request. This was my Eureka! moment.

I quickly created a new repo with a basic Dockerfile and the pipeline files above and began to wire these in to DevOps to prove the concept.

Add a new pipeline targeting azure-piplines.ci.yml . You’ll will probably want to rename this after you’ve been forced to run it for the first time. A Save option would be nice here.

. You’ll will probably want to rename this after you’ve been forced to run it for the first time. A Save option would be nice here. Add a new pipeline targeting azure-piplines.pr.yml and rename accordingly.

and rename accordingly. Modify the Branch policies of the master branch, adding a build policy to build any PR branches using the new PR pipeline. At this point, you can force the Squash merge policy too if you wish.

branch, adding a build policy to build any PR branches using the new PR pipeline. At this point, you can force the Squash merge policy too if you wish. To tag the master branch upon successful build, edit the CI pipeline and select On success for Tag sources. Surely there’s an easier way, but clicking Edit on the CI pipeline, followed by …, Triggers, YAML and Get sources should get you there. Phew!

Now when you submit a PR, the PR pipeline will run with a build number of 0.{#pr}.*-pr where #pr is the PR number and * is the next revision. Assuming the build is successful and the PR is accepted, then the CI pipeline will run with a build number of 1.0.* . Voilà!

Summary

We have learnt how we can create a Trunk Based PR workflow in Azure DevOps, protecting the master branch and building with separate numbering and naming schemes for PR and CI builds. Hopefully as Azure DevOps matures, more of the manual config we had to do in DevOps will become available via .yml configuration files.

If you have any comments or suggestions I would be grateful to hear them.