How to Protect a Branch to Accept Pull Requests From Just a Single Branch in Azure DevOps


Perhaps you’re familiar with this:

You’re working in Azure DevOps and next to your main branch, you’ve configured a development branch to do most of your work in.
You’ve configured branch policies on both of these branches so that they require a Pull Request to accept changes, they can’t be changed by direct commits.

Now you’re all done creating your new feature in your feature branch and you create a Pull Request to merge these changes into development so they get tested before you push it into main.
But on the Pull Request ‘wizard’, you forget to check where you’re merging into and presto chango, your PR goes from your feature branch straight into main. 😢😭

Of course, branch policies can also be configured to enable reviewers to make sure others check your work, but unfortunately this can still be overlooked.
In comes todays solution: create a validation check to make sure only Pull Requests from development can be merged into main and nothing else.

In order to solve our problem, we’ll:

  • Make a build pipeline in Azure DevOps to do a simple check on the source branch if it matches an approved branch
  • Set up a branch policy Build Validation to deny the Pull Request if the build pipeline fails

The build pipeline yaml code is fairly straight forward:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
trigger: none

pool:
  vmImage: ubuntu-latest

steps:
- task: PowerShell@2
  inputs:
    targetType: 'inline'
    script: |
      $acceptedBranch = 'refs/heads/development'
      $sourceBranch = "$(System.PullRequest.SourceBranch)"

      if($sourceBranch -ne $acceptedBranch)
      {
        "PR to 'main' only allowed from $acceptedBranch"
        "PR to 'main' attempted from $sourceBranch"
        exit 1
      }      
    failOnStderr: true

Let’s break it down:

  • trigger - We don’t want it to be triggered on anything but a Pull Request.
    We will arrange that through a Branch Policy setting later.
  • pool: vmimage - Pick whatever image you want here, as long as it supports PowerShell.
  • steps - We will use an inline PowerShell script ( we can also use a scriptfile, but it’s too simple in my eyes to justify that).
  • script - Here’s the actual logic:
    • $acceptedBranch - provide the full name of the branch that IS allowed to open a Pull Request to main.
      This normally includes refs/heads/ as shown above.
    • $sourceBranch - Use the system defined variable to find out which branch is used as source for the Pull Request.
    • if-loop - In case the sourcebranch is NOT development, provide some verbose information on what went wrong and exit the script with exitcode 1 (error)
  • failOnStderr - In case the script exits with any code other than 0, it will report as failed, blocking the Pull Request.

So how to we create the pipeline?

  1. In Azure DevOps, go to the Pipelines tab.
  2. Select the Pipelines option.
  3. Click the New pipeline button.

New Pipeline

  1. Our code resides in Azure Repos Git, so we select this.

Select Azure Repos Git

  1. Select the actual repository in which your code resides (in my case the Bicep repo).

Select your repository

  1. Configure a new Starter pipeline by clicking this option.

New Starter Pipeline

  1. Paste the yaml code as defined above (of course adapting it to your own needs/requirements) into the code field.
  2. Click the filename to rename it to something you might recognize/remember.
    The file will be saved in the root of your chosen repository by default.
  3. Don’t click on the Save and run button, but the dropdown option right next to it.
  4. Now click on the Save button.

Save the pipeline

  1. Select your build pipeline and click the context menu.
  2. Click Rename/move to rename the DisplayName of your Build Pipeline.

Rename your pipeline

The pipeline section is now ready to be used.

To make use of the newly created Build Pipeline, we need to configure it in a Branch Policy.

  1. In Azure DevOps, go to the Repos tab.
  2. Select the Branches option.
  3. Select the context menu for the branch you want to ‘secure’.
  4. Select the Branch policies option.

Branch Policies

  1. Go to the Build Validation section.
  2. Click on the + symbol to add a new build policy.

Build Validation

  1. Select the BranchProtection Build Pipeline we’ve created earlier.
  2. Make sure the Policy requirement is set to Required for obvious reasons.
  3. Give the Build policy a simple and recognizable Display name.
  4. Save your Build policy.

BranchProtection

Ok, we’ve now set up our Build pipeline and Branch policy and everything should work as intended… but does it really?

Give it a run for its money and edit/commit a file in a branch other than main or development and create a Pull Request directly into main to see how this works:

In this example I’ve created a Pull Request directly from test into main.
When looking at the Overview, you can see that it’s Queueing up a Build pipeline called BranchProtection.

Pull request attempted

Low and behold, the pipeline should fail, because we’re not creating a Pull Request from development, here’s how to find out what went wrong

Click on the failed Build Pipeline

Click on the failed pipeline

And now under Jobs select the job that has run that failed for more information

Click the failed job for more information

Under Jobs -> PowerShell, you can see exactly what went wrong:

Pull Request from wrong branch

And there you have it, it’s not perfect, but it’s something… an easy way of preventing mistakes!

Happy coding! 😀