Azure DevOps – YAML pipelines and branching strategies

Development teams have various forms of internal agreements about internal in-team cooperation. These agreements usually cover topics like branching strategies, policies, naming conventions, folder structures. In this post, I would like to touch one of them – YAML pipelines in the context of the branching strategy. Or, to be more precise, how to build a shared pipeline that will be used in multiple scenarios: building and releasing the code in various environments and as a build policy validation gate.

Branching strategies

With time Git became a de facto standard for the source control, team collaboration, and code contribution. To set up a consistent way of using such a tool, teams have to define some standards of cooperation, like naming conventions, repository structure, and especially: a suitable branching strategy from a variety of popular choices: Git Flow, GitHub Flow or Release Flow.

Further in the post, I will stick to a Release Flow. However, there is no good or bad choice. Every team has to discuss and choose the one that fits best.

From a high-level point of view the Release Flow branching strategy can be illustrated this way:

  • It has a single collaboration branch – master.
  • The changes performed in feature branches (topics) and delivered to a master branch by raising a pull request which contains quality gates like:
    • Build policies
    • Unit testing
    • Static code analysis
    • Peer reviews
  • Release branches created to deliver the code to deployment targets

In Azure DevOps this flow results into a corresponding branching layout:

The pipeline also has to be a “branching strategy” aware. It should detect the current branch path and trigger only needed stages. An example of the pipeline flow:

  • Feature branch: Builds the code and releases it to the test environment only
  • Release branch: Builds the code, releases it to the acceptance and after approval to production environments
  • Pull request: Continuous Delivery is not needed, therefore the code building happens

Building the pipeline

In this post, I will build an abstract pipeline. It will not have concrete tasks, like MSBuild or Publish Artifacts, however, it will be more about showing concepts of a conditional use of stages depending on the scenario.

The pipeline will have two files:

  • stage-template.yaml: An abstract re-usable stage implemented using parameterized template
  • main.yaml: The entry point of the pipeline. It contains triggers, pool information, and orchestration of the stages
1. stage-template.yaml

Stages will be implemented via a parameterized template, that has two options:

  • Name: To assign and render a name in Azure DevOps UI
  • Enabled: controls if the stage should be triggered or has to be skipped
parameters:
- name: Name
  type: string

- name: Enabled  
  type: boolean
  default: false

stages:
- stage: ${{ parameters.Name }}
  displayName: '${{ parameters.Name }} Stage'
  condition: and(not(failed()), eq('${{ parameters.Enabled }}', true))
  
  jobs:  
  - job: ${{ parameters.Name }}
    displayName: '${{ parameters.Name }} Job'
    steps:
    - powershell: Write-Host "Job '${{ parameters.Name }}' is running. Parameter Enabled - '${{ parameters.Enabled }}' "   
main.yaml

The entry point file contains triggers, that set to start pipeline automatically on changes in feature or release branches. If the pipeline to be started in another branch it should be triggered manually using UI.

trigger:
- features/* 
- releases/* 

pool:
  vmImage: 'windows-latest'

stages:
- template: stage-template.yaml  
  parameters:
    Name: Build
    Enabled: True

- template: stage-template.yaml  
  parameters:
    Name: Test
    Enabled: ${{ startsWith(variables['Build.SourceBranch'], 'refs/heads/features') }}

- template: stage-template.yaml  
  parameters:
    Name: Acceptance
    Enabled: ${{  startsWith(variables['Build.SourceBranch'], 'refs/heads/releases') }}

- template: stage-template.yaml  
  parameters:
    Name: Production
    Enabled: ${{ startsWith(variables['Build.SourceBranch'], 'refs/heads/releases')  }}

Another important thing is the way how the value for parameter Enabled is assigned:

Enabled: ${{ startsWith(variables['Build.SourceBranch'], 'refs/heads/features') }}

It uses inline evaluation of the current branch path. If the parent folder is “features“, the evaluation assigns TRUE and the stage will be executed. And this is a core concept of a discussed approach. Depends on the circumstances, like a current branch or the way how the pipeline was started, some certain stage will be triggered or it will be skipped.

Testing the pipeline

1. Place two yaml files mentioned above into the build folder of your repository.

2. Then register a new YAML pipeline and give it a name. I named it as release-flow-example.

3. Create a feature branch. As an example: features/Alex/Task1234-add-deployment-logging. And then commit some dummy change in it.

As you can see, only two stages completed: Build and release to Test. This is expected behavior because you want to push the build artifacts to Acceptance and Production only from the Release branches.

4. Create a new release branch, for example, releases/release01 from a master branch. This will trigger a new pipeline execution:

This time Test Stage was skipped and only Acceptance and Production stages (deployments) completed.

Build validation of pull request changes

Besides Continuous Delivery, there is another common use of a pipeline. It is a build validation for pull requests. This validation acts like a quality gate: the code is pre-merged and has to be built successfully. Also, a common practice is to include unit testing and static code analyzers as a part of this process.

Configuration of the Build Validation

Build validation can be configured on any branch. But, a common way is to set it on the master. For this, open Branches then in options of the master choose branch policies. Then navigate to the Build Validation section and click on + to add a new policy:

In the wizard fill the listed input fields:

  1. Build pipeline: Use a dropdown box to choose the pipeline that was created by this blog post
  2. Trigger: Set it to automatic
  3. Policy requirement: Required
  4. Display name: give a meaningful name for this build policy
This image has an empty alt attribute; its file name is testing-img3.png

Finally, save it and check if the new build policy created.

Testing the build validation

Create a pull request to merge the recently created feature branch (for instance, features/Alex/Task1234-add-deployment-logging) into master. There is a new status message: Required check. It spins in the background the pipeline with the default parameters and in the context of the master branch.

This image has an empty alt attribute; its file name is testing-img3.png

The pipeline execution progress is available by clicking on “View check”. As you can see, only the build stage was executed time:

This is again a correct behavior because pull request expects a clearance of various gates and they normally are part of the Build Stage.

Final words

This post is about designing a single and reusable YAML pipeline and applying a branching strategy to it. It covers also some topics discussed earlier, like parameterized templates and conditional execution. The next post – Team Conventions and Standards I devoted again to Azure DevOps and focus more on other in-team agreements.

Many thanks for reading.