Say you’re working on some dotnet projects that you would like to share with the world. Or that you would like to share with other teams in your company. Or your project has multiple git repos and the output of one is the input of another. Creating a NuGet package is an excellent way to zip up and redistribute your build output, but there are a few details about versioning to sort out before that process is streamlined and effortless.
For goals – we’d like new NuGet packages as often as every single time a change is merged. It’d also be ideal if every single package had a unique version number that is related to the build which produced it.
To show an example of setting this up lets build up a solution that publishes a pair of assemblies named
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> </PropertyGroup> </Project>
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\Example.Hello\Example.Hello.csproj" /> </ItemGroup> </Project>
Aside, isn’t it nice how clean the modern project files are? They’ve become very powerful too. In the past you would also need to create a
.nuspec for each package. Today, just the two files above should be enough to create packages!
> dotnet pack src\Example.Hello\Example.Hello.csproj Microsoft (R) Build Engine version 15.8.166+gd4e8d81a88 for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. Restoring packages for C:\...\Example.Hello.csproj... ... Example.Hello -> C:\...\Example.Hello.dll Successfully created package C:\...\Example.Hello.1.0.0.nupkg'. > dotnet pack src\Example.World\Example.World.csproj Microsoft (R) Build Engine version 15.8.166+gd4e8d81a88 for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. Restoring packages for C:\...\Example.World.csproj... ... Example.World -> C:\...\Example.World.dll Successfully created package 'C:\...\Example.World.1.0.0.nupkg'.
Assign the version from the build number
Next, let’s do something about those version numbers. There are a lot of version numbers involved but they are all based on a
Version property. There are also
VersionSuffix properties available that can be assigned instead, in which case the
Version property defaults to
The dash means the
VersionSuffix becomes the SemVer pre-release tag. A good use of the pre-release tag is to label a package as a “ci”, “beta”, or “release candidate”. This enables the consumers to distinguish between supported stable builds and latest edge builds. Packages with a pre-release tag will not be shown on NuGet clients by default until the consumer opts-in with a check-box or command line switch.
Because these two projects we are building are a “suite” that are compiled and shipped together we will want them to have the same version number. The
Directory.Build.targets files are a good place to assign those values repo-wide. We’re going to be using Azure DevOps pipelines, formerly VSTS, so lets assume there will be a
BUILD_BUILDID environment variable that is going to have a unique number we can include.
<Project> <PropertyGroup> <MsBuildAllProjects>$(MsBuildAllProjects);$(MsBuildThisFileFullPath)</MsBuildAllProjects> </PropertyGroup> <PropertyGroup> <!-- edit this value to change the current major.minor version --> <VersionPrefix>0.1</VersionPrefix> <!-- append the build number if it is available --> <VersionPrefix Condition=" '$(BUILD_BUILDID)' != '' ">$(VersionPrefix).$(BUILD_BUILDID)</VersionPrefix> </PropertyGroup> </Project>
With that we can create a sln file for convenience, set the environment variable to a test value, and rebuild the projects.
> dotnet new sln The template "Solution File" was created successfully. >dotnet sln add src\Example.Hello\Example.Hello.csproj Project `src\Example.Hello\Example.Hello.csproj` added to the solution. >dotnet sln add src\Example.World\Example.World.csproj Project `src\Example.World\Example.World.csproj` added to the solution. >set BUILD_BUILDID=1234 (or) export BUILD_BUILDID=1234 >dotnet pack Microsoft (R) Build Engine version 15.8.166+gd4e8d81a88 for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. Restore completed in 39.46 ms for C:\...\Example.World.csproj. Restore completed in 39.46 ms for C:\...\Example.Hello.csproj. Example.Hello -> C:\...\Example.Hello.dll Successfully created package 'C:\...\Example.Hello.0.1.1234.nupkg'. Example.World -> C:\...\Example.World.dll Successfully created package 'C:\...\Example.World.0.1.1234.nupkg'.
That doesn’t just change the NuGet package version number. The assembly’s version numbers, and the various version numbers in the PE file header, are all affected.
Use prerelease tag for continuous integration builds
There is one more thing we should add. Let’s introduce a
Prerelease environment variable which can be set. When it is set we should put the build number in the
VerionSuffix instead of in the
VersionPrefix. When we do that we should also left-pad the build number. That’s because in NuGet the pre-release tag is sorted as a string rather than as a number, and otherwise when you jump from
build100 the sorting used to determine the latest build will be wrong.
<Project> <PropertyGroup> <MsBuildAllProjects>$(MsBuildAllProjects);$(MsBuildThisFileFullPath)</MsBuildAllProjects> </PropertyGroup> <PropertyGroup> <!-- edit this value to change the current MAJOR.MINOR version --> <VersionPrefix>0.1</VersionPrefix> </PropertyGroup> <Choose> <When Condition=" '$(Prerelease)' != '' "> <PropertyGroup> <!-- Prerelease version numbers are MAJOR.MINOR.0-pre-build###### --> <VersionSuffix>$(Prerelease)-build$(BUILD_BUILDID.PadLeft(6, '0'))</VersionSuffix> </PropertyGroup> </When> <Otherwise> <PropertyGroup> <!-- Release version numbers are MAJOR.MINOR.# --> <VersionPrefix>$(VersionPrefix).$(BUILD_BUILDID.PadLeft(1, '0'))</VersionPrefix> </PropertyGroup> </Otherwise> </Choose> </Project>
There! Now on the console you can
set Prerelease=ci or
export Prerelease=ci and the packages created will be like
Example.World.0.1.0-ci-build001234.nupkg. If you change the file extension to
.zip you can take a look at the generated
.nuspec file it contains. In that you’ll see how values like the package version is correct and the ProjectReference has become a dependency with the correct version as well.
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> <metadata> <id>Example.World</id> <version>0.1.0-ci-build001234</version> <!-- ... --> <dependencies> <group targetFramework=".NETStandard2.0"> <dependency id="Example.Hello" version="0.1.0-ci-build001234" exclude="Build,Analyzers" /> </group> </dependencies> </metadata> </package>
Automating build and publish with Azure DevOps
With the automatic versioning of packages in place we can now take advantage of it when we automate our build pipelines. To prevent this article from becoming too long, I’ll list the steps I took to set this up, but I won’t go into them in detail here. If there’s interest I can follow up with additional articles detailing how these were configured.
- Created a public GitHub repo and pushed the code
- Created an Azure DevOps account
- Created a public project in that account
- Created a YAML CI build definition
- Created a public MyGet feedhttps://www.myget.org/F/lodejard-greeting
To create the YAML CI build definition, a
.vsts-ci.yaml is added to the repo. This file lists the steps that the build agent needs to take. It’s a pretty nice file format and fairly easy to read. Note: when the CI build definition is created it is also given two build variables of
steps: - task: DotNetCoreInstaller@0 displayName: install dotnet 2.1.401 inputs: packageType: 'sdk' version: '2.1.401' - task: DotNetCoreCLI@2 displayName: dotnet build inputs: command: build - task: DotNetCoreCLI@2 displayName: dotnet test inputs: command: test projects: 'test/**/*.csproj' - task: DotNetCoreCLI@2 displayName: dotnet pack inputs: command: pack packDirectory: '$(Build.ArtifactStagingDirectory)/packages' - task: PublishBuildArtifacts@1 displayName: publish artifacts condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)'
Those steps are enough to build, test, and pack the projects in the repo. The PublishBuildArtifacts task will post the generated nupkg files which can be downloaded from the artifacts drop-down on any build that succeeded.
The very last thing to hook up is a task to push the packages to a public NuGet feed hosted on MyGet.org. This involves adding a build definition variable named
myget-apikey and adding the following step to the CI YAML. Be sure to mark the build definition variable as secret by clicking the padlock when you add it! That ensures it will never be revealed.
- task: DotNetCoreCLI@2 displayName: dotnet nuget push condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) inputs: command: custom custom: nuget arguments: push $(Build.ArtifactStagingDirectory)/packages/*.nupkg --source https://www.myget.org/F/lodejard-greeting/api/v3/index.json --no-service-endpoint --api-key $(myget-apikey)
And there you have it! Every time code is moved to the master branch a new set of NuGet packages are created and published. To depend on the latest stable release a consumer can use
Version="0.1.*" in their dependency, and to depend on the absolute latest CI build
Version="0.1.0-ci-*" can be used.
This examples used public GitHub for code and public MyGet for packages, but those services have private options as well. Or if what you’re doing is more work-related Azure DevOps provides options for git repos and NuGet feeds which are private and secured using your company’s AAD login credentials.