This post was most recently updated on August 31st, 2022.
6 min read.This article will briefly explain the different NuGet package versioning schemes – both automatic and manual – available. Then we’ll take a look at how to implement a nifty, and quite frankly, downright elegant automatic versioning scheme for your NuGet packages.
Okay – returning from quite a trip down another rabbit hole, I think it’s a good time to document some of my findings in regards to Azure DevOps NuGet package versioning!
This seems to be another area, where the documentation certainly does exist, but most of it just didn’t answer our questions. A task that should have been easy and straightforward to solve really wasn’t. At all.
So either our use case was unique, we were asking the wrong questions, or the documentation wasn’t comprehensive enough – I’ll leave that distinction to someone else!
So, what were we doing, then?
Description
We were configuring Azure DevOps build pipelines with the intention of publishing new package versions to our organization’s internal NuGet stream automatically with each successful Release build of the package. This would enable us to easily share different versions of our package by promoting each (somewhat) stable build to the PreRelease stream, from which different teams can effortlessly access them for testing purposes before picking the stable versions for their production builds.
My configuration and requirements were as follows:
- .NET Framework Project
- Azure DevOps pipeline configured with YAML
- An internal NuGet feed to publish the packages to
- Unique, incremental semver-compatible version numbers for each update of the package
- No need to bump the number manually
That said, onwards with the configuration! First, let’s take a look at our options.
Which versioning options do we have and how do they function?
There are a few different options to choose from when you’re configuring your build pipeline’s NuGet packaging -step’s versioning scheme. The next chapter is meant to help you choose the one that suits you the best!
Available versioning schemes
- byPrereleaseNumber
- Configure the package version with a number of other switches, namely:
- majorVersion
- minorVersion
- patchVersion
- packTimezone (documentation says this is required, but it just seemed to default to UTC without)
- Configure the package version with a number of other switches, namely:
- byEnvVar
- Reference a variable to set the package version
- byBuildNumber
- Build NAME is used as the number – it needs to be something that can be parsed as a build number. Note that this will also literally change the names of your builds in the build history and all emails.
- off
- Configure the package version using either the project file or a separate nuspec file.
Or like shown on docs.microsoft.com:
How to choose which versioning scheme to use for your Azure DevOps NuGet packaging?
I don’t know about you, but trying to figure out, based on the documentation, how the versions are going to look like with these options, turned out to be kind of complicated.
Quite truthfully, none of these options appeared to suit our needs. “ByBuildNumber” caused the side effect of the builds being renamed and limited us to using a few predefined formats. It’s also kind of stupid – instead of having a “date.revision – comments” as the build/pipeline execution name, you’ll have a number configured in the build “name” as the execution name, too.
What do I mean? I mean, you need to set this (or similar):
name: $(BuildDefinitionName)_$(Year:yyyy).$(Month).$(DayOfMonth)$(Rev:.r)
# much later...
- task: NuGetCommand@2
inputs:
command: pack
packagesToPack: '**/[projectname].csproj.nuspec'
versioningScheme: byBuildNumber
packDestination: '$(Build.ArtifactStagingDirectory)\nugetpkg'
That’ll define your NuGet package name. But it’ll also rename the build. And hell, I tried multiple different versions, but it just generates annoying results.
Modifying the name of the release definition (with the side effects it causes) to change the name of the generated NuGet package sounds… Wrong.
Besides, $(Rev:.r) will only increment for each build of the day. So your NuGet package versioning is going to be date + build number (or similar).
$(Rev:r) | 2 (The third run on this day will be 3, and so on.) Use $(Rev:r) to ensure that every completed build has a unique name. When a build is completed, if nothing else in the build number has changed, the Rev integer value is incremented by one. If you want to show prefix zeros in the number, you can add additional ‘r’ characters. For example, specify $(Rev:rr) if you want the Rev number to begin with 01, 02, and so on. |
On the other hand, “ByPrereleaseNumber” would’ve been great, but you actually can’t get rid of the date&time in the version – which was FRUSTRATING. “ByEnvVar” wasn’t too bad since you could configure a pipeline variable to do whatever you want, but making that into a semver-compatible incremental number… Well, let’s just say there’s no apparent way to do that. You’d have to always change the value of the variable per build, or write custom PowerShell logic in your build to change the value during the execution.
In the end, “Off” seemed like it was the closest one – with that, I could’ve moved the responsibility from Azure DevOps to myself, and just updated a nuspec file to always contain the desired version number. Sometimes, manual work is preferable to crappy automation, right?
Well, truthfully, that didn’t look very desirable to me either, as it would’ve required me to increment the version number manually, or trust an add-in to do it – something, that I know would eventually fail. Either I’d forget to update it, or someone without the add-in would commit a duplicate version number.
Blah.
Instead, we wanted to always have each build from the master (essentially, only after a successful merge) generate a new version of the package with a new semver-compatible version number. How do we do this?
This brings us to the actual question:
How to configure Azure DevOps to automatically increment NuGet package version using semver?
After quite a lot of googling, we encountered this masterpiece of an article:
https://kasunkodagoda.com/2019/04/03/hidden-gems-in-azure-pipelines-creating-your-own-rev-variable-using-counter-expression-in-azure-pipelines/ (opens in a new tab)
BOOM! This article describes exactly what we needed. Finding it took a while, but now this is a great opportunity to expand on the material, and document what we did and how it’s working for us!
The article outlines a simple way to build a semver-like versioning scheme based on a few pipeline variables, using the counter Expression Function. This function has the following syntax:
counter(name, seed)
The parameters are as follows:
- name is the name of the variable that is created behind the scene. And any time the same variable is referenced from the build/release, the value is incremented by 1.
- seed is the starting value for the variable. It defaults to 0, and you can set it to any integer value. When it first triggers, it’ll start from your seed value, and in the next trigger it will increment +1
The solution (in copy-pasteable format at the end of the article!) is as follows:
From this, you can probably guess that any time the variable name that we reference changes – in this case, Major.Minor version, it will create a new variable and start incrementing from the seed value, which is the trick we need to use to simulate the $(Rev) variable.
Brilliant! Using this format, we can build the package version we want and store it in an environment/build pipeline variable called PackageVersion.
Below is an example of our configuration in YAML:
- task: NuGetCommand@2
inputs:
command: pack
packagesToPack: '**/[projectname].csproj.nuspec'
versioningScheme: byEnvVar
versionEnvVar: PackageVersion
packDestination: '$(Build.ArtifactStagingDirectory)\nugetpkg'
And how it’s in the pipeline variables:
With this, I have an incremental, automatic Patch-version of my PackageVersion variable, with Major and Minor being updated manually by yours truly. I also have the optional PackageVersionType, in case I want to label a package explicitly as being “preview” or anything else.
The original article didn’t have this in text form at all, so I suppose I’m doing us all a service by spelling it out down below (in hopefully easily copy-pasteable form):
$[counter(format('{0}.{1}', variables['Major'], variables['Minor']), 0)]
That’s it for now!
References
This blog post was an interesting investigation and a deep dive into Microsoft’s documentation. Below are some of the more useful sources we went through:
- https://kasunkodagoda.com/2019/04/03/hidden-gems-in-azure-pipelines-creating-your-own-rev-variable-using-counter-expression-in-azure-pipelines/
- This is a brilliant tip by Kasun Kodagoda – much appreciate him documenting it.
- https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml
- Microsoft’s documentation for predefined variables is available for your pipelines.
- “Performing cleanup” – Excel is stuck with an old, conflicted file and will never recover. - November 12, 2024
- How to add multiple app URIs for your Entra app registration? - November 5, 2024
- How to access Environment Secrets with GitHub Actions? - October 29, 2024