Code Sharing Part 2: Automatic Semantic Versioning of NuGet Deploys

How to automatically incorporate semantic version information into NuGet libraries during building, packaging, and publishing of .Net libraries to private NuGet feeds.

In my last code sharing post I covered how create and consume private NuGet feeds in Azure DevOps via a script, but only locally, and manually.  This isn't close to good enough assuming you've bought into the DevOps dream of consistently and reliably deploying code into production up to hundreds of times per day (ala The Phoenix Project, which I simply can't recommend enough).

To get a continuous integration server automatically compiling, packaging, and deploying a library to NuGet, you'll first need to solve versioning.  Ideally the versioning information will contain semantic information such as whether an API was broken.  That type of information can only be provided by a human, and yet a human shouldn't have to manually specify the version number for each build.

In this article I'll show how to incorporate semantic versioning information into libraries and yet automate it.  I'll start with incorporating versioning information into the Cake scripts from the last post and then get into version automation with GitVersion.

Hard Coded Versioning


There's a subtle bug with the NuGetPush task from my last post.  If you run it twice you'll get this error:

Response status code does not indicate success: 409 (Conflict - The feed already contains 'LibAuthenticator 1.0.0'.

That's because I didn't specify a version in dotnet pack.  Dotnet pack then tried to pull version information from the .csproj, and since no version information existed in csproj, it defaulted to 1.0.0 every time.  There are two ways to solve this.

We could put version information in the .csproj.  However, modifying a csproj during a build feels yucky, and locally would require a git revert.  Uch.

Better, the dotnet pack command documentation shows that we can manually override MSBuild properties with a syntax like "/p:Property=value".  Cake supports this inside the DotNetCorePackSettings via the strongly typed MSBuildSettings property:

Task("Pack") .IsDependentOn("Build") .Does(() => { DotNetCorePack(projectDirectory, new DotNetCorePackSettings { NoBuild = true, IncludeSymbols = true, Configuration = configuration, MSBuildSettings = new DotNetCoreMSBuildSettings().SetVersion(version) }); });
The file name when we pack will now contain the version, so we'll additionally need to update the name of the file we're pushing like:

var nupkgFile = configDirectory + File("LibAuthenticator." + version + ".nupkg");

and put that in the NuGetPush task.

Assembly Meta-Data


There's now a small problem.  If we override PackageVersion during pack and fail to specify the version during build, then the source library's dll meta-data will always be marked as 1.0.0, even though git pulls the updated version e.g. 1.0.1.    Fortunately we can pull that "/p:" trick again this time on dotnet build to override the version.  The code looks nearly identical:

Task("Build") .IsDependentOn("Clean") .IsDependentOn("Version") .Does(() => { DotNetCoreBuild(projectDirectory, new DotNetCoreBuildSettings { Configuration = configuration, MSBuildSettings = new DotNetCoreMSBuildSettings().SetVersion(version) }); });
Now we just need to define and set the version variable.  If we hard-code one like var version = "1.0.1"; toward the top of the file, we'll be able to NuGetPush up a new version of our library.  Once at least.  Then we'll run into the 409 error again.  We could make variable an argument, but we'd have to specify it on every build, and that's not very automated.  There's a better way.

Calculating Version


If you're using git, and gitflow in particular, then all of the information you need should already be in source control history.  This is especially easy for official releases.  According to gitflow author Vincent Driessen:

origin/master [should] be the main branch where the source code of HEAD always reflects a production-ready state … When the source code in the develop branch reaches a stable point and is ready to be released, all of the changes should be merged back into master somehow and then tagged with a release number.

So for official releases, you could just extract and parse the git tag. But suppose you want to publish to alpha or beta NuGet feeds from non-master branches. The good news is that if you're following git flow everything you need is still available in git, and even better you can use the GitVersion tool to extract it for you. GitVersion returns semantic versions that make sense. Definitely read the documentation, because it's a bit counter-intuitive at first and it's very customizable, but here's a quick summary of the defaults:



  1. If you tag a branch (typically on master for an official release) like v1.0.3, then GitVersion will return "1.0.3"
  2. If you're on a feature branch off of develop then GitVersion will increment the minor number and append a suffix of the branch name and a 1 up counter
  3. As you merge PR's back into develop GitVersion updates the minor version from what it found in master and appends "alpha" and a 1 up counter.
  4. When you're ready to deploy and start a release branch, GitVersion parses the version number out of the branch name and appends "beta" and a 1 up counter
  5. When you merge the release branch back into master GitVersion recognizes the version from the merged release branch even before you're tagged
If you're suspicious about strings in version numbers, they do work in assembly meta-data:


And when you publish to NuGet even without a separate alpha or beta feed they show up but only if you include prerelease versions:




Now there's just the issue of implementing it.


GitVersion and Cake


Using GitVersion in Cake is extremely easy. First include the plugin like this:

#tool "nuget:?package=GitVersion.CommandLine"
Specify a version there for bonus points. Then just set a version variable, something like:

Task("Version") .Does(() => { var symVer = GitVersion(); Information($"SemVer: {symVer.SemVer}"); version = symVer.SemVer; }); 
And finally include the new dependency:

Task("Build") .IsDependentOn("Clean") .IsDependentOn("Version") .Does(() => { ... }
To DevOps

And now you can get run this from an Azure DevOps Build via a yml step like:

steps: - powershell: .\build.ps1 -target=NuGetPush -nugetUsername='$(nugetUsername)' -nugetPassword='$(nugetPassword)'
The full version of this entire project is open sourced and the source code for the cake solution is located here.

HouseCleaning

As a quick aside since my last post DevOps changed the NuGetFeed url. If this happens to you or you ever need to manually remove or debug your NuGet credentials, they live in: %appData%\NuGet\NuGet.Config

Summary

In this post I've shown how to incorporate version info into NuGet libraries during building and packaging in Cake tasks. I've also shown how you can extract semantic versioning info from source control using GitVersion. I hope this helps you to automate the versioning and deploying of your shared .Net code. Please hit me up on twitter or the comments if this was helpful or you have any questions or suggestions.

Comments

Thomas Levesque said…
I used GitVersion for a while, but it's overly complicated. I strongly suggest you try MinVer instead, which is much simpler and integrates seamlessly in the build process.
Lee Richardson said…
@Thomas Levesque Nice tip, thanks! One of the things I liked about GitVersion is it isn't limited to tags if e.g. if you're in a release branch but aren't ready to tag quite yet. But still a solid alternative.
Thomas Levesque said…
If the current commit is not tagged, MinVer generates a version number based on the latest tag and the number of commits ahead of it, e.g. 1.0.1-alpha.3 if you're 3 commits ahead of tag 1.0.0.
Lee Richardson said…
Gotcha, so basically it doesn't support #4 and #5 in my example above. In other words it would fail to pick up the 2.X from the branch name and would continue at 1.0.3 until you tag. Clearly not a major loss.