Custom version numbers in Azure DevOps yaml pipelines
I’m a fan of semantic versioning, especially for software meant to be consumed by other developers, such as NuGet packages. If you want to use semantic versioning however, it means you need to have some control over the version number that gets assigned to your software by your build system. Whether you are looking to use semantic versioning, or want to use some other version number format, in this post we will look at how to accomplish that when using yaml files for you Azure Pipeline.
Using the classic editor
Before we look at the yaml way, if you’ve been using Azure DevOps for a while you may have already solved this problem in the classic build pipeline editor.
One way to do this was to use the $(Rev:r)
syntax in your Build number format
; for example, using 1.0.0.$(Rev:r)
.
The $(Rev:r)
syntax acts as a variable with an auto-incrementing value, so the first build would be 1.0.0.0
, the next would be 1.0.0.1
, then 1.0.0.2
, and so on.
Once any part of the string to the left of $(Rev:r)
changes, the counter resets to zero.
So if you changed the Build Number Format to 1.1.0.$(Rev:r)
, the next build would have a value of 1.1.0.0
.
To access the Build Number Format value in your tasks so that you could actually use it, you would use the built-in $(Build.BuildNumber)
variable.
For example, if you wanted to apply the version to all of your .Net projects before building the assemblies, you could do this:
I am a huge fan of Richard Fennell’s Manifest Versioning Build Tasks Azure DevOps extension, which is what is being used in the above screenshot to version all of the .Net assemblies with our version number.
NOTE: You’ll need to have the extension installed in order to use the
richardfennellBM.BM-VSTS-Versioning-Task.Version-Assemblies-Task.VersionAssemblies@2
task shown in yaml snippets below. You may be able to simply useVersionAssemblies@2
, but it conflicts with other extensions I have installed so I use the fully qualified name here to avoid the ambiguity error.
Simple yaml solution
Microsoft is moving away from the classic editor and investing in yaml pipelines. To accomplish the same thing as described in the above classic editor scenario is very easy to do in yaml, and the code would look like this:
name: '1.0.0.$(Rev:r)'
steps:
- task: richardfennellBM.BM-VSTS-Versioning-Task.Version-Assemblies-Task.VersionAssemblies@2
displayName: Version the assemblies
inputs:
Path: '$(Build.SourcesDirectory)'
VersionNumber: '$(Build.BuildNumber)'
InjectVersion: true
FilenamePattern: 'AssemblyInfo.*'
OutputVersion: 'OutputedVersion'
In the yaml definition, the name
element corresponds to the Build number format
of the classic editor, but in both yaml and the classic editor the $(Build.BuildNumber)
variable is used to access the value.
A bit more advanced yaml
Having seen the simple yaml solution, there’s a few things we should mention:
- The
$(Rev:r)
auto-incrementing syntax is only valid for thename
element; you cannot use it in any other variables or fields. - The
name
(i.e.Build number format
) is what shows up on your Azure Pipeline’s build summary page. If you want to show more information in the build’s title, such as the git branch the build was made from or the date it was created, then this solution won’t work; something like1.0.0.1_master_2020-01-15
is not a valid version number that can be assigned to assemblies.
To overcome this problem we can make use of yaml variables and the counter
function.
This function provides the same functionality as the $(Rev:r)
syntax, where you give it a prefix and if that prefix changes, the auto-incrementing integer will reset.
In addition, this function let’s us set the seed value of the auto-incrementing integer, so we can have it start from something other than zero if we want.
So now we can generate our version number and version our assemblies using the yaml below. Note that I’ve switched from a 4 part version number to a 3 part one to show off how you might do semantic versioning.
name: '$(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)_$(Rev:.r)'
variables:
version.MajorMinor: '1.2' # Manually adjust the version number as needed for semantic versioning. Patch is auto-incremented.
version.Patch: $[counter(variables['version.MajorMinor'], 0)]
versionNumber: '$(version.MajorMinor).$(version.Patch)'
steps:
- task: richardfennellBM.BM-VSTS-Versioning-Task.Version-Assemblies-Task.VersionAssemblies@2
displayName: Version the assemblies
inputs:
Path: '$(Build.SourcesDirectory)'
VersionNumber: '$(versionNumber)'
InjectVersion: true
FilenamePattern: 'AssemblyInfo.*'
OutputVersion: 'OutputedVersion'
You can see now that we’ve introduced some variables:
version.MajorMinor
is the one that you would manually adjust.version.Patch
will auto-increment with each build, and reset back to zero whenversion.MajorMinor
is changed.versionNumber
is the full 3-part semantic version, and is used in the assembly versioning task.
You may have noticed that the name
was changed quite a bit as well.
It now shows the name of the build definition, the git branch that the build used, the date the build was made, and a patch number.
The patch number is still appended to ensure that multiple builds made from the same branch on the same day have different names.
There are some other tokens that name
supports as well.
Showing the version number in the build name
One major issue with the yaml solution above, in my opinion, is that the name of the build no longer includes the version number in it.
Unfortunately, getting the version number into the name
isn’t as simple as just doing:
name: '$(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)_$(versionNumber)'
This is because our custom yaml variables are processed at runtime, and the name
is evaluated before then.
To work around this issue, we can use the UpdateBuildNumber command, as in the following yaml:
name: 'Set dynamically below in a task'
variables:
version.MajorMinor: '1.2' # Manually adjust the version number as needed for semantic versioning. Patch is auto-incremented.
version.Patch: $[counter(variables['version.MajorMinor'], 0)]
versionNumber: '$(version.MajorMinor).$(version.Patch)'
steps:
- task: PowerShell@2
displayName: Set the name of the build (i.e. the Build.BuildNumber)
inputs:
targetType: 'inline'
script: |
[string] $buildName = "$(versionNumber)_$(Build.SourceBranchName)"
Write-Host "Setting the name of the build to '$buildName'."
Write-Host "##vso[build.updatebuildnumber]$buildName"
- task: richardfennellBM.BM-VSTS-Versioning-Task.Version-Assemblies-Task.VersionAssemblies@2
displayName: Version the assemblies
inputs:
Path: '$(Build.SourcesDirectory)'
VersionNumber: '$(versionNumber)'
InjectVersion: true
FilenamePattern: 'AssemblyInfo.*'
OutputVersion: 'OutputedVersion'
There are a couple things to notice in this yaml.
First, I changed the name
to indicate that it will be dynamically updated.
Second, I added another task to the steps for setting the name of the build.
I’m a fan of PowerShell so I used a PowerShell task, but you could use Bash too (the syntax would be different though).
In the 3 lines of PowerShell you can see that I create a string of what I want the build name to be.
Here I opted to just include the version number and the git branch the build used, but you could use any of the other predefined variables as well.
Notice though that the predefined variables used here (i.e. $(Build.SourceBranchName)
) is different than those used directly in the name
element (i.e. $(SourceBranchName)
), as the name
only supports special tokens evaluated before runtime.
When the build is first queued, it’s name will show up as Set dynamically below in a task
until the PowerShell step to update it is executed.
Because of this, you may choose to have it show something else, like in the other examples above.
If you do this, I would add a comment to the name
saying that it gets updated in a task below.
Creating prerelease version numbers
At the start of this post I mentioned that I’m a fan of semantic versioning. Part of semantic versioning is supporting prerelease versions. While not everything supports prerelease versions, such as .Net assemblies, many things do, such as NuGet package versions.
Defining your prerelease version can be as simple as defining a new variable, like so:
variables:
version.MajorMinor: '1.2' # Manually adjust the version number as needed for semantic versioning. Patch is auto-incremented.
version.Patch: $[counter(variables['version.MajorMinor'], 0)]
versionNumber: '$(version.MajorMinor).$(version.Patch)'
prereleaseVersionNumber: '$(versionNumber)-$(Build.SourceVersion)'
Where $(Build.SourceVersion)
is the git commit SHA being built.
I typically like to include the date and time in my prerelease version number. Unfortunately, there isn’t a predefined variable that can be used to access the current date and time, so it takes a bit of extra effort.
Here is some yaml code that I typically use for my prerelease versions:
variables:
version.MajorMinor: '1.2' # Manually adjust the version number as needed for semantic versioning. Patch is auto-incremented.
version.Patch: $[counter(variables['version.MajorMinor'], 0)]
versionNumber: '$(version.MajorMinor).$(version.Patch)'
prereleaseVersionNumber: 'Set dynamically below in a task'
steps:
- task: PowerShell@2
displayName: Set the prereleaseVersionNumber variable value
inputs:
targetType: 'inline'
script: |
[string] $dateTime = (Get-Date -Format 'yyyyMMddTHHmmss')
[string] $prereleaseVersionNumber = "$(versionNumber)-ci$dateTime"
Write-Host "Setting the prerelease version number variable to '$prereleaseVersionNumber'."
Write-Host "##vso[task.setvariable variable=prereleaseVersionNumber]$prereleaseVersionNumber"
- task: VersionPowerShellModule@2
displayName: Update PowerShell Module Manifests version for Prerelease version
inputs:
Path: 'powerShell/Module/Directory/Path'
VersionNumber: '$(prereleaseVersionNumber)'
InjectVersion: true
Here I’ve introduced a new prereleaseVersionNumber
variable, as well as a PowerShell task step to set it.
The first line of the PowerShell gets the date and time in a format acceptable for prerelease semantic versions.
The second line then builds the complete prerelease version number, appending -ci$dateTime
to the regular version number.
I use ci
to indicate that it’s from a continuous integration build, but you don’t need to.
The fourth line then assigns the value back to the prereleaseVersionNumber
yaml variable so it can be used in later tasks.
In this example I’m using the prereleaseVersionNumber to version a PowerShell module, as it supports prerelease version numbers.
Extras
If you need a unique ID in your version number, you can use the $(Build.BuildId)
predefined variable.
This is an auto-incrementing integer that Azure DevOps increments after any build in your Azure DevOps organization; not just in your specific build pipeline.
No two builds created in your Azure DevOps should ever have the same Build ID.
Ready to use code
Above I’ve shown you a few different variations of ways to do version numbers in yaml templates. Hopefully I explained it well enough that you understand how to customize it for your specific needs. That said, here’s a few code snippets that are ready for direct copy-pasting into your yaml files, where you can then use the variables in any pipeline tasks.
Specifying a 3-part version number with an auto-incrementing patch:
name: 'Set dynamically below in a task'
variables:
version.MajorMinor: '1.0' # Manually adjust the version number as needed for semantic versioning. Patch is auto-incremented.
version.Patch: $[counter(variables['version.MajorMinor'], 0)]
versionNumber: '$(version.MajorMinor).$(version.Patch)'
steps:
- task: PowerShell@2
displayName: Set the name of the build (i.e. the Build.BuildNumber)
inputs:
targetType: 'inline'
script: |
[string] $buildName = "$(versionNumber)_$(Build.SourceBranchName)"
Write-Host "Setting the name of the build to '$buildName'."
Write-Host "##vso[build.updatebuildnumber]$buildName"
Specifying a 4-part version number with an auto-incrementing patch:
name: 'Set dynamically below in a task'
variables:
version.MajorMinor: '1.0' # Manually adjust the version number as needed. Patch is auto-incremented.
version.Patch: $[counter(variables['version.MajorMinor'], 0)]
versionNumber: '$(version.MajorMinor).$(version.Patch).$(Build.BuildId)'
steps:
- task: PowerShell@2
displayName: Set the name of the build (i.e. the Build.BuildNumber)
inputs:
targetType: 'inline'
script: |
[string] $buildName = "$(versionNumber)_$(Build.SourceBranchName)"
Write-Host "Setting the name of the build to '$buildName'."
Write-Host "##vso[build.updatebuildnumber]$buildName"
Specifying a 3-part version number with an auto-incrementing patch, along with a prerelease version number that includes the date and time of the build and the Git commit SHA:
name: 'Set dynamically below in a task'
variables:
version.MajorMinor: '1.0' # Manually adjust the version number as needed for semantic versioning. Patch is auto-incremented.
version.Patch: $[counter(variables['version.MajorMinor'], 0)]
versionNumber: '$(version.MajorMinor).$(version.Patch)'
prereleaseVersionNumber: 'Set dynamically below in a task'
steps:
- task: PowerShell@2
displayName: Set the name of the build (i.e. the Build.BuildNumber)
inputs:
targetType: 'inline'
script: |
[string] $buildName = "$(versionNumber)_$(Build.SourceBranchName)"
Write-Host "Setting the name of the build to '$buildName'."
Write-Host "##vso[build.updatebuildnumber]$buildName"
- task: PowerShell@2
displayName: Set the prereleaseVersionNumber variable value
inputs:
targetType: 'inline'
script: |
[string] $dateTime = (Get-Date -Format 'yyyyMMddTHHmmss')
[string] $prereleaseVersionNumber = "$(versionNumber)-ci$dateTime+$(Build.SourceVersion)"
Write-Host "Setting the prerelease version number variable to '$prereleaseVersionNumber'."
Write-Host "##vso[task.setvariable variable=prereleaseVersionNumber]$prereleaseVersionNumber"
With the above, you can do things like determine whether to use the versionNumber
or the prereleaseVersionNumber
variables depending on if the $(Build.SourceBranchName)
is the default branch (e.g. main
or master
) or a feature branch.
The below example shows one way of how to do this, and sets the versionNumber
variable to the stableVersionNumber
if building the main
branch, or to the prereleaseVersionNumber
if building any other branch.
name: 'Set dynamically below in a task'
variables:
version.MajorMinor: '1.0' # Manually adjust the version number as needed for semantic versioning. Patch is auto-incremented.
version.Patch: $[counter(variables['version.MajorMinor'], 0)]
stableVersionNumber: '$(version.MajorMinor).$(version.Patch)'
prereleaseVersionNumber: 'Set dynamically below in a task'
versionNumber: 'Set dynamically below in a task' # Will be set to the stableVersionNumber or prereleaseVersionNumber based on the branch.
isMainBranch: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')] # Determine if we're building the 'main' branch or not.
steps:
- task: PowerShell@2
displayName: Set the prereleaseVersionNumber variable value
inputs:
targetType: 'inline'
script: |
[string] $dateTime = (Get-Date -Format 'yyyyMMddTHHmmss')
[string] $prereleaseVersionNumber = "$(stableVersionNumber)-ci$dateTime+$(Build.SourceVersion)"
Write-Host "Setting the prerelease version number variable to '$prereleaseVersionNumber'."
Write-Host "##vso[task.setvariable variable=prereleaseVersionNumber]$prereleaseVersionNumber"
- task: PowerShell@2
displayName: Set the versionNumber to the stable or prerelease version number based on if the 'main' branch is being built or not
inputs:
targetType: 'inline'
script: |
[bool] $isMainBranch = $$(isMainBranch)
[string] $versionNumber = "$(prereleaseVersionNumber)"
if ($isMainBranch)
{
$versionNumber = "$(stableVersionNumber)"
}
Write-Host "Setting the version number to use to '$versionNumber'."
Write-Host "##vso[task.setvariable variable=versionNumber]$versionNumber"
- task: PowerShell@2
displayName: Set the name of the build (i.e. the Build.BuildNumber)
inputs:
targetType: 'inline'
script: |
[string] $buildName = "$(versionNumber)_$(Build.SourceBranchName)"
Write-Host "Setting the name of the build to '$buildName'."
Write-Host "##vso[build.updatebuildnumber]$buildName"
You can also leverage expressions to determine at runtime what inputs to provide to later tasks (e.g. the stableVersionNumber
or the prereleaseVersionNumber
), or if a tasks should run at all by placing a condition on it.
If you like you could combine all 3 PowerShell tasks into a single task for brevity. I prefer to keep them separated for clarity.
Conclusion
Yaml builds are the future of Azure Pipelines. I enjoy them because you get your build definition stored in source control with your code, it’s easy to copy-paste the yaml to other projects (you can also use yaml templates, a topic for another post ;) ), and it makes showing off examples in blogs and gists easier.
With so many great reasons to start using yaml for your Azure Pipelines builds, I hope this information helps you get your version numbers and build names setup how you like them.
I’d also like to throw a shout out to Andrew Hoefling’s blog post that introduced me to the counter
function and helped me get started with using custom version numbers in my yaml builds.
Happy versioning!
Comments
Nathan
Just wanted to leave a quick thanks! I’d been pulling my hair out for nearly a day trying to dynamically set the build name, and your script fixed it in 10 minutes flat!
chandrika
Hi, In my YAML script i had written 3 counters to increment version numbers based on the value selected at run time. version format : major.minor.patch
major (major_version_no) & minor (minor_version_no) counters are working fine. But patch counter (patch_version_no) is not getting reset whenever ‘majorminor’ value changes. Patch counter keep increasing based on old value.
Can you please help me to find out the issue here ?
parameters:
trigger:
pool: vmImage: ‘ubuntu-latest’ stages:
Ralf
Hi Daniel,
nice work, well done. I think there is a bug in the last script, it hasn’t worked for me until I have initialized the variable prereleaseVersionNumber with the stabelVersionNumber instead of versionNumber whitch was still set to ‘Set dynamically below in task’ in line 17:
after that it worked verry well.
Thanks for sharing!!!
deadlydog
Thank you very much for pointing out the bug in the last code example @Ralf. It was a copy-pasta mistake and I’ve fixed it up 👍
Joshua Silverman
I’m curious how you’d access the version name if I wanted to show it on the UI?
Ian
Just a quick note to say thanks for a great article! Perfect balance of detail and brevity
David C
How would you go about putting this code in a template to be shared across multiple pipelines?
SkyHome
Hi,
I also needed to change the version build number, and ended up using the counter. But for me it works also for the name: definition in the yaml file. I have defined two variables in the GUI for the pipeline (Pipeline Variables), like:
buildMajorMin = 1.5 buildStartCounter = 22
In the top of the yaml file I have:
variables: buildRev: $[counter(variables[‘buildMajorMin’], variables[‘buildStartCounter’])]
name: $(buildMajorMin).$(buildRev)
Kyle
Great writeup/tutorial Daniel.
One quandary I had was in regards to the prerelease version.
If I’m understanding correctly,
Revision
will continue to update even on subsequent commits to the non-main (ex: feature) branch.For example the following git “actions” will produce the following versions:
In this case you would have the following stable versions:
1.1.0
,1.1.3
,1.1.5
.What would be ideal is something akin to the following:
In this case you would have the following stable versions:
1.1.0
,1.1.1
,1.1.2
.Furthermore, the prerelease versions are a lower precedence as per the SemVer 2.0.0 rules:
Perhaps what I would like cannot be supported using purely ADO (I get the feeling something like GitVersion is required based on some preliminary research).
I’m curious how you feel about my proposed versioning scheme. Does it make sense?
Dan
Hey @Kyle, yeah, only incrementing the
Revision
for builds on themain
branch is one thing I haven’t been able to figure out yet, and it’s bothered me since I first wrote this post.If you read the
counter
function docs it shows how to increment a counter based on if it’s a PR or not:But that ignores the
Major.Minor
version number part and thus the Revision counter won’t be the same as the last stable build. Also, if you include:in your yaml the counter gets incremented, even if you don’t use/display it, so the next stable build will still have it’s Revision incremented. So yeah, I really want what you’ve proposed, but haven’t figured out a way to do it yet.
Side note: I’m not sure why I called the 3rd version number part
Revision
when I wrote this post. SemVer defines the version parts as Major.Minor.Patch, and Microsoft defines them as Major.Minor.Build.Revision. I’m going to update the post to use the more appropriate names.Shoshana
Great and helpfully article
Leave a Comment
Your email address will not be published. Required fields are marked *