Automating Semantic Versioning in Azure DevOps CI/CD Pipelines with GitVersion

Overview

Versioning is the unsung hero of software development—often overlooked but absolutely essential. Imagine trying to manage a project without a clear way to track changes, communicate updates, or ensure compatibility. Chaos, right? That’s where versioning steps in, providing structure and clarity.

In this post, I’ll share how I streamline versioning in my projects by combining the power of Semantic Versioning (SemVer) with GitVersion, an automation tool that eliminates the manual effort of version management. Whether you’re just beginning your journey or tackling the complexities of feature-rich projects, this post will show you how to automate semantic versioning in Azure DevOps for consistency, traceability, and peace of mind.

Why Version?

Versioning is a fundamental practice in software development that helps us manage change effectively. It provides a structured way to track and communicate updates, ensuring clarity and consistency throughout the development lifecycle.

What is Semantic Versioning?

Semantic Versioning (often abbreviated as SemVer) is a versioning scheme that provides a standardised way to communicate the nature of changes in a software release. It uses a three-part version number format: MAJOR.MINOR.PATCH, where each part conveys specific information about the release.

Breakdown of Semantic Versioning:

  1. MAJOR Version: Incremented when there are breaking changes that are incompatible with previous versions.

    • Example: 1.0.02.0.0
  2. MINOR Version: Incremented when new features are added in a backward-compatible manner.

    • Example: 1.0.01.1.0
  3. PATCH Version: Incremented when backward-compatible bug fixes or small improvements are made.

    • Example: 1.0.01.0.1

Benefits of Semantic Versioning:

  • Clarity: It clearly communicates the scope and impact of changes to users and developers.
  • Predictability: Helps teams and users understand what to expect from a new release.
  • Dependency Management: Makes it easier to specify compatible versions of libraries or APIs.
  • Automation: Tools like GitVersion can automate the generation of semantic versions in CI/CD pipelines, ensuring consistency and reducing manual effort.

Versioning with GitVersion

GitVersion is an open source tool that can be used to automate semantic versioning by analysing our Git repository’s history. It streamlines the versioning process, ensuring consistency and reducing manual errors.

GitVersion has the following key features:

  • Semantic Versioning (SemVer): Automatically calculates version numbers based on Git history, adhering to SemVer principles.
  • Branching Strategy Support: Compatible with Continuous Delivery, GitFlow, GitHub Flow, and Mainline development workflows.
  • Continuous Integration (CI) Friendly: Integrates seamlessly with CI/CD pipelines, generating version numbers for builds and releases.
  • Flexible Configuration: Highly configurable to suit various project needs and versioning schemes.

GitVersion Configuration

GitVersion uses a configuration file (GitVersion.yml) to define how version numbers are calculated based on your Git repository’s history and branching strategy. This file allows us to customise the behaviour of GitVersion to suit our project’s needs.

How I Branch and Configure GitVersion for Projects

My projects tend to use the following setup:

  • Continuous Delivery Branch Model. This model has the following features:
    • Main branch as release-ready: The main branch is always in a deployable state.
    • Frequent Deployments: Changes are deployed to production or staging environments frequently, often automatically.
    • Short-Lived Feature Branches: Feature branches are merged into main after review and testing.
    • Automated Pipelines: CI/CD pipelines handle building, testing, and deploying changes seamlessly.
  • SemVer is incremented using the following strategies:
    • TaggedCommit. This strategy uses Git tags to determine the version. If a commit is tagged with a semantic version (e.g., 1.0.0), GitVersion will use that tag as the base for calculating the next version.
    • Fallback. This strategy is used when no other versioning information (e.g., tags or branch-specific rules) is available. It serves as a default versioning mechanism, especially for newly initiated projects.
  • Two defined branches for GitVersion increments:
BranchIncrementWhenPrevent Increment If Commit Tagged
MainMajor (X.0.0)Manually using tags for significant breaking changes.✅ Yes
MainMinor (0.X.0)Automatically on new commits for backward-compatible features.✅ Yes
Pull RequestPatch (0.0.X)Automatically on pull request creation or updates for backward-compatible bug fixes or small improvements.✅ Yes

GitConfig Setup

The described GitVersion configuration looks like the following:

## YAML
strategies:
- TaggedCommit
- Fallback
branches:
   main:
      increment: Minor
      prevent-increment:
         when-current-commit-tagged: true
   pull-request:
      increment: Patch
      prevent-increment:
         when-current-commit-tagged: true
ConfigurationDetails
Strategies- TaggedCommit: Uses Git tags to determine the version.
- Fallback: Provides a default versioning mechanism when no other information is available.
Branches
main- Increment: Minor (0.X.0)
- Prevent Increment: Enabled when the current commit is tagged.
pull-request- Increment: Patch (0.0.X)
- Prevent Increment: Enabled when the current commit is tagged.

Azure DevOps CI/CD Pipeline Setup

The CI/CD pipeline would be configured as shown below:

## YAML
name: $(Build.DefinitionName)_$(GitVersion.FullSemVer)

trigger:
  branches:
    include: [ main ] # branch names which will trigger a build

pr: # will trigger on PR
  branches:
    include: [ main ] # branch names which will trigger a build.

variables:
  - name: Source_Branch_Ref
    value: $[replace(coalesce(variables['System.PullRequest.SourceBranch'], variables['Build.SourceBranch']), 'refs/heads/', '')]

resources:
  repositories:
    - repository: Source_Branch
      type: git
      name: GitVersionSemVerWithTags
      ref: "$(Source_Branch_Ref)"

stages:
  - stage: 'Build_Packages'
    jobs:
      - job: 'Increment_Version'
        condition: and(succeeded(), or(eq(variables['Build.Reason'], 'PullRequest'), and(eq(variables['Build.SourceBranch'], 'refs/heads/main'), ne(variables['Build.Reason'], 'Manual'))))
        pool:
          vmImage: 'windows-latest'
        steps:

        - checkout: Source_Branch
          persistCredentials: true
          fetchTags: true
          fetchDepth: 0 # Ensure we fetch all Git history for Semver

        - task: gitversion/setup@3
          displayName: 'Get current version of GitVersion'
          inputs:
            versionSpec: '6.0.x'

        - task: gitversion/execute@3
          displayName: 'Run GitVersion to generate SEMVER'
          inputs:
            targetPath: '$(Build.SourcesDirectory)\'
            useConfigFile: true
            configFilePath: '$(Build.SourcesDirectory)\GitVersion.yml'

        - task: PowerShell@2
          displayName: 'Increment the Version using Git Tag'
          inputs:
            targetType: 'inline'
            script: |
              cd '$(Build.SourcesDirectory)'
              git config --global user.email "$(Build.RequestedForEmail)"
              git config --global user.name "$(Build.RequestedFor)"
              git tag -a "$(GitVersion.MajorMinorPatch)" -m "Released by $(Build.RequestedFor)"
              git push origin tag "$(GitVersion.MajorMinorPatch)"              

Key Steps in the Pipeline

  1. Pipeline Name: Combines the build definition name with the full semantic version: $(Build.DefinitionName)_$(GitVersion.FullSemVer).
  2. Trigger: Builds are triggered on changes to the main branch.
  3. Pull Request Trigger: Builds are triggered on pull requests targeting the main branch.
  4. Variables: Defines Source_Branch_Ref to extract the source branch reference for the build or pull request.
  5. Resources: Dynamically sets the branch reference for the Git repository (Source_Branch) to $(Source_Branch_Ref).
  6. Job: Increment_Version: Executes if the build succeeds and is triggered by a pull request or the main branch but not part of a Manual Trigger.
  7. Checkout: Checks out the Source_Branch repository with full Git history and tags for versioning.
  8. GitVersion Setup: Installs GitVersion (6.0.x) to calculate semantic versions.
  9. GitVersion Execute: Runs GitVersion using the GitVersion.yml configuration file to generate the semantic version.
  10. PowerShell Script:
    • Configures Git by setting the user email and name to match the build requester.
    • Creates or updates a Git tag with the calculated version ($(GitVersion.MajorMinorPatch)).
    • Pushes the tag to the remote repository, ensuring the version is recorded.

⚠️ Important Note
The Source_Branch_Ref variable and resource in this pipeline are configured as shown above because pull requests (PRs) create their own Git branches which are a merger of the source branch and main. When tags are created during the pipeline execution, they are placed on the PR branch by default, not the source branch where GitVersion calculates automatic increments.
By setting up the Source_Branch_Ref variable and dynamically referencing the source branch in the resources section, the tag is placed on the source branch instead of the PR branch. This ensures that version increments are correctly applied to the source branch, maintaining accurate semantic versioning.
The PR branch should still be used for building and validating solution artifacts.

⚠️ Important Pipeline Permissions
The Azure DevOps [Project] Build Service must have the following Repository Permissions:

  • Contribute: Permission to push changes to the repository.
  • Create Tag: Permission to create and update tags in the repository.

Using GitVersion to Version Artifacts

Versioning artifacts involves two phases:

  1. Generate a semantic version to use. This can be achieved by leveraging the GitVersion task.
  2. Applying the sematic version to artifacts. This can be achieved by using a task such as VersionJSONFile@3.

As an example, when working with ARM templates in Azure, it’s important to version your artifacts for traceability and consistency. Using GitVersion in your Azure DevOps pipeline, you can generate a semantic version and apply it to the contentVersion field of your ARM template. This guarantees that each deployment is uniquely identifiable.

## YAML
      - job: 'Test_and_VersionBicepTemplates'
        pool:
          vmImage: 'windows-latest'
        steps:
        - checkout: self
          path: './s/selfBranch/'
          persistCredentials: true
          fetchTags: true
          fetchDepth: 0 # Ensure we fetch all Git history for Semver

        - checkout: Source_Branch
          path: './s/versionBranch/'
          persistCredentials: true
          fetchTags: true
          fetchDepth: 0 # Ensure we fetch all Git history for Semver

        # GitVersion task is needed in each job where the variables are referenced
        - task: gitversion/setup@3
          displayName: 'Get current version of GitVersion'
          inputs:
            versionSpec: '6.0.x'

        - task: gitversion/execute@3
          displayName: 'Run GitVersion to generate SEMVER'
          inputs:
            targetPath: '$(System.DefaultWorkingDirectory)/versionBranch/'
            useConfigFile: true
            configFilePath: '$(System.DefaultWorkingDirectory)/versionBranch/GitVersion.yml'

        - task: BicepInstall@0
          inputs:
            version: 0.35.1

        - task: BicepBuild@0
          inputs:
            process: "single"
            sourceFile: '$(Build.SourcesDirectory)\selfBranch\Deployment\azuredeploy.bicep'
            stdout: false
            outputFile: '$(Build.ArtifactStagingDirectory)\ARMOutput\azuredeploy.json'

        - task: VersionJSONFile@3
          displayName: 'Version stamp ARM templates'
          inputs:
            Path: '$(Build.ArtifactStagingDirectory)\ARMOutput'
            recursion: true
            VersionNumber: '$(GitVersion.AssemblySemFileVer)'
            useBuildNumberDirectly: False
            VersionRegex: '\d+\.\d+\.\d+\.\d+'
            versionForJSONFileFormat: '{1}.{2}.{3}.{4}'
            FilenamePattern: '\w+.json'
            Field: 'contentVersion'
            OutputVersion: 'OutputedVersion'

        - task: PublishPipelineArtifact@1
          displayName: 'Publish Versioned Solution Templates build artefact'
          inputs:
            targetPath: "$(Build.ArtifactStagingDirectory)/ARMOutput"
            publishLocation: "pipeline"
            artifactName: "ARM-Templates"

Key Steps in the Pipeline

  1. Checkout Repositories

    • The pipeline checks out two branches:
      • Self Branch: Contains the Bicep files that will be compiled into ARM templates.
      • Source Branch: Used for versioning and fetching Git history.
    • Full Git history and tags are fetched (fetchTags: true, fetchDepth: 0) to ensure accurate semantic version calculation.
  2. Generate Semantic Version with GitVersion

    • GitVersion Setup: The gitversion/setup@3 task installs GitVersion (6.0.x).
    • GitVersion Execution: The gitversion/execute@3 task calculates the semantic version based on the repository’s history and configuration (GitVersion.yml).
  3. Build ARM Templates

    • The BicepBuild task compiles the Bicep file (azuredeploy.bicep) into an ARM template (azuredeploy.json).
    • The compiled ARM template is stored in the $(Build.ArtifactStagingDirectory)/ARMOutput directory.
  4. Version Stamp the ARM Template

    • The VersionJSONFile task updates the contentVersion field in the ARM template (azuredeploy.json) with the semantic version generated by GitVersion.
      • Inputs:
        • VersionNumber: Uses $(GitVersion.AssemblySemFileVer) (Provides a 4-digit version format (MAJOR.MINOR.PATCH.0), which is ideal for ARM template versioning e.g., 1.2.3.0).
        • Field: Specifies the contentVersion field in the ARM template to be updated.
        • VersionRegex: Ensures only valid version formats (\d+\.\d+\.\d+\.\d+) are replaced.
      • This step ensures that the ARM template is uniquely versioned for traceability.
  5. Publish the Versioned ARM Template

    • The PublishPipelineArtifact task publishes the versioned ARM template as a pipeline artifact.
      • The artifact is stored under the name ARM-Templates and can be used in subsequent deployment stages.

Summary

Automating semantic versioning in Azure DevOps CI/CD pipelines with GitVersion is a game-changer for maintaining consistency, traceability, and efficiency in your development workflow. By leveraging tools like GitVersion, you can eliminate the manual effort of version management, ensure accurate versioning across branches, and streamline the deployment of versioned artifacts like ARM templates.

Whether you’re managing simple projects or complex, feature-rich solutions, adopting semantic versioning practices ensures that your team and stakeholders have a clear understanding of changes, compatibility, and release impact. With the strategies and pipeline configurations shared in this post, you’re now equipped to implement a robust versioning system that aligns with industry best practices.

If you found this post useful, consider sharing it with your team or network to help others streamline their versioning workflows. Happy automating!

For the original version of this post see Andrew Wilson's personal blog at Automating Semantic Versioning in Azure DevOps CI/CD Pipelines with GitVersion