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:
-
MAJOR Version: Incremented when there are breaking changes that are incompatible with previous versions.
- Example:
1.0.0
→2.0.0
- Example:
-
MINOR Version: Incremented when new features are added in a backward-compatible manner.
- Example:
1.0.0
→1.1.0
- Example:
-
PATCH Version: Incremented when backward-compatible bug fixes or small improvements are made.
- Example:
1.0.0
→1.0.1
- Example:
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.
- Main branch as release-ready: The
- 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:
Branch | Increment | When | Prevent Increment If Commit Tagged |
---|---|---|---|
Main | Major (X.0.0) | Manually using tags for significant breaking changes. | ✅ Yes |
Main | Minor (0.X.0) | Automatically on new commits for backward-compatible features. | ✅ Yes |
Pull Request | Patch (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
Configuration | Details |
---|---|
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
- Pipeline Name: Combines the build definition name with the full semantic version:
$(Build.DefinitionName)_$(GitVersion.FullSemVer)
. - Trigger: Builds are triggered on changes to the
main
branch. - Pull Request Trigger: Builds are triggered on pull requests targeting the
main
branch. - Variables: Defines
Source_Branch_Ref
to extract the source branch reference for the build or pull request. - Resources: Dynamically sets the branch reference for the Git repository (
Source_Branch
) to$(Source_Branch_Ref)
. - Job: Increment_Version: Executes if the build succeeds and is triggered by a pull request or the
main
branch but not part of aManual Trigger
. - Checkout: Checks out the
Source_Branch
repository with full Git history and tags for versioning. - GitVersion Setup: Installs GitVersion (
6.0.x
) to calculate semantic versions. - GitVersion Execute: Runs GitVersion using the
GitVersion.yml
configuration file to generate the semantic version. - 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
TheSource_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 theSource_Branch_Ref
variable and dynamically referencing the source branch in theresources
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:
- Generate a semantic version to use. This can be achieved by leveraging the GitVersion task.
- 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
-
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.
- The pipeline checks out two branches:
-
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
).- The version is exposed as pipeline variables, such as
$(GitVersion.AssemblySemFileVer)
.
- The version is exposed as pipeline variables, such as
- GitVersion Setup: The
-
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.
- The
-
Version Stamp the ARM Template
- The
VersionJSONFile
task updates thecontentVersion
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 thecontentVersion
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.
- Inputs:
- The
-
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.
- The artifact is stored under the name
- The
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!