Labels

Thursday, 24 March 2016

Publishing a .NET project with Appveyor and NuGet

Creating a complete build system for an open source project with GitHub, versioning according to the Semantic Versioning ("semver") specification and publishing related NuGet packages on nuget.org, including a capability to publish pre-release packages suitable for a project with parallel releases.


This walkthrough requires basic understanding git branching, using the GitHub and authoring .NET/C# solutions with Visual Studio.

Versioning

So you have a project on GitHub and would like to make it available in a binary form.
Great, let's decide on a versioning strategy.

Semantic Versioning is an established standard, nuget.org uses it, let's use it in our project too. Actually, we are bound to use it, as it's used by the NuGet, but it's also important to obey the rules, not just use the version strings according to a regular expression.
I will not elaborate on the standard, it's pretty well documented; I will summarize just what's important for our process here.

In this post, Semantic Versioning 1.0.0 is used as it is the one supported by nuget.org at the time of the writing. I encourage you to read the semver specification to understand version precedence. It's important if you need pre-releases such as alpha, beta, RC, RTM etc.

Pre-release versions

TL;DR
If you think you don't need all the pre-release stuff, you can skip right to the 'DLL versions' section.

A semver in its general form looks like:

v$Major.$Minor.$Patch[-$PreReleaseToken$Build|.$Build]

Examples:
v1.2.3-alpha[456]
v1.2.3[.457]

There are basically three components (Major, Minor, Patch) used for precise control, and one for build numbering, with the first two, when combined, are sometimes also called a "Marketing Version".
A major release number is incremented on an API change, a minor release number is incremented on a backward-compatible change, and a patch number is incremented on a backward-compatible bug fix or improvement.

In addition, you can specify a pre-release version with a hyphen and a token after the first three, and that would be used for marking your build as a pre-release. Nuget.org makes use of this one to disallow referencing such a package in production code by mistake and also limits referencing pre-release dependency packages from a proper release package, so you would need to pass a special command line switch -Prerelease (-Pre) when installing such a package.

UPDATE:
For I have replaced the overcomplicated versioning scheme with a simpler one.

Let's assume we want our NuGet feed to look like the following at some point in future as our project progresses:

MyLib 1.0.0-alpha1-000001
MyLib 1.0.0-alpha2-000002
MyLib 1.0.0-beta1-000003
MyLib 1.0.0.4 - making a release build
MyLib 1.0.0.5

DLL versions

A NuGet package serves the need to bring a .dll file into the binaries folder for the .NET type system to reference during the build, and during the build process a client could be referencing an exact version with all of the four components of the version string.

When you publish this client project as a NuGet package, this reference in embedded in the CLR metadata of the DLL, and another build of MyLib, for example, 1.0.1.1 would not be suitable for this particular binary release. This is not always what we want because we could end up having another dependent package, which could have a reference to yet another version for the same client DLL.

At the same time its perfectly valid to have a DLL reference using just the first three components, as the incremented build numbers must not bring any breaking changes according to the semver specification.

For our process we will just flatten the DLL version used by the .NET type system to ignore the Build number, it will always be zero. All of the following versions: 1.0.1.0, 1.0.1.1, 1.0.1.2 will use the same DLL version 1.0.1.0. We could also flatten the Patch number, as it should not introduce breaking changes too, but it would require us to be extremely precise about introducing breaking changes with new builds numbers.

To summarize this DLL versioning strategy:
  1. We are free to make new builds of a version without changing the DLL references.
  2. We must not introduce API-breaking changes with new build numbers.
  3. The versioning system will forgive us introducing a breaking change with a new Patch number, as it embedded in the DLL version and would require adjusting the DLL references in projects; such an is being caught during compilation.
To learn more about the .NET type system breaking change, I would recommend A definitive guide to API-breaking changes in .NET

Creating a NuGet specification

Preparing AssemblyInfo.cs

We use what is called 'patching' to rewrite the version strings in the files, so we don't need to commit a change for AssemblyInfo.cs to change version.

Let's assume you have your AssemblyInfo file as Visual Studio initially created it for you.

Open the AssemblyInfo.cs file of your project, and find the following lines:
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

There is one thing missing here. It's the AssemblyInformationalVersionAttribute.
Let's add one, so the Appveyor version patching process would find and patch it properly:
[assembly: AssemblyInformationalVersion("1.0.0-alpha")]

Basically we just explicitly specified a pre-release version for the .dll file as understood by the NuGet.

You can read more about versioning a NuGet package in the NuGet docs:
https://docs.nuget.org/create/versioning

Creating a NuGet specification file (.nuspec)

Open a PowerShell console, change directory to the folder containing your MyLib.csproj file and run:
NuGet spec. You will notice the MyLib.nuspec file created for your project. Open it in a text editor.
You can optionally choose to add it as a solution item so it would be visible inside the Visual Studio Solution Explorer; it does not affect the build process, just makes it handy to access the file in the IDE.

You can change it as you wish. It's well documented, I find the following most useful:
https://docs.nuget.org/Create/NuSpec-Reference

The important thing for our process is to leave the $version$ part unchanged as it is needed to inherit metadata of the .dll file when packaging in Appveyor.

Try to create a package locally:
NuGet pack ./MyLib/MyLib.csproj
The project should have been built by that time.
You probably run into the following problem:
The replacement token 'author' has no value.
For that to pass, you need to specify non-empty values for the following attributes in AssemblyInfo.cs:
[assembly: AssemblyDescription("My Library")]
[assembly: AssemblyCompany("Me")]
Also, watch for the warnings similar to:
...http://PROJECT_URL_HERE_OR_DELETE_THIS_LINE...
You should take your time to figure out those ones.

Worth noticing, you should configure your .gitignore not to include the .nuspec .nupkg files as well as the packages folder, so the both the downloaded NuGet packages in your project and your local experiments won't end up in the repository. Add the following lines in your .gitignore:

packages
*.nupkg

Now, when you finally get your package created locally, it could probably be created by appveyor too.

Commit and push your .nuspec and AssemblyInfo.cs and .sln changes.

Making a NuGet specification with a dependency

Say we have another project in our solution, named MyLib.Model which the MyLib depends on, and we would like it to go into its own separate NuGet package. This scenario is covered with our process.
When we make the solution, the client project MyLib normally references the other one, but in NuGet, we need to specify a dependency.
First, create a .nuspec file for MyLib.Model.csproj project similarly to what you have already done for MyLib.csproj, then add the following to the MyLib.nuspec:
    <dependencies>
      <dependency id="TrivialAst.Model" version="[1.0.0, 2.0)" />
    </dependencies>
This limits the allowed version numbers of the dependent project used when installing the client NuGet package to the version range starting from 1.0.0-alpha and not including 2.0.0. This way after installing the MyLib 1.0.0-alpha, we will have both the
MyLib 1.0.0-alpha and 
MyLib.Model 1.0.0-alpha
packages installed. Then, when MyLib.Model 1.1.1-beta is available, we can choose to upgrade only the MyLib.Model to that version, and with the dependency version range specified it is possible according to the semver 1.0.0 specification which is obeyed by the NuGet.

Setting up an Appveyor project

Create an Appveyor project pointing to your GitHub repository.
Appveyor supports both the configurations in the Yaml files (appveyor.yml) and configurations with the UI, which are mutually exclusive for the most part as described in the Appveyor docs.

In our process, we will use the UI. The reason for this decision is that I want as little build-related in the repository as possible. With the Appveyor, you can export the .yml file later and include it in the repository if you ever need it the other way, but in my opinion, a build setup is not something that should be ever set in stone.

Either way, you can use this gist of such an exported appveyor.yml.
You can use it for reviewing the changes you make in the Appveyor UI or use it directly if you choose to use an appveyor.yml in your repository.

Deriving version strings

As we already know, there are three versions in used in our build process, and we need to derive those from the branch name and the Appveyor build number.

We need a Powershell script for that:
Put this script in the Init script field in the Environment build settings menu:



Here is a quick overview of the script.

I am using the ErrorActionPreference to fail the build in case of an error in the script, and it's explicitly used when the branch does not match a semver specification, so you get an error message similar to the following in case of incorrectly specified semver after the "release/" part:
Branch name is not correct semver (1.0.0): release/foo

The rx variable is a regex string for a correct semver (1.0.0).

The script takes two environment variables passed by the Appveyor and derives the three environment variables used for patching:
$branch = $env:APPVEYOR_REPO_BRANCH;
$buildNumber = $env:APPVEYOR_BUILD_NUMBER;

$env:release_AssemblyVersion = $assemblyVersion;
$env:release_FileVersion = $fileVersion;
$env:release_InformationalVersion = $informationalVersion;

Following is required for a nice header for our builds:
Update-AppveyorBuild -Version $displayVersion

When the build is scheduled, the displayed version will reflect what you specify in the Version field. Then it switches to a nicer thing, reflecting the actual version as the build starts to run. Note that I'm prefixing the build with strange "debug 01" which just stands there to group the builds made while debugging, you can remove that after you set up everything correctly and make sure you get your build process running.

We need to specify those in the patching process in the build settings menu:

I'm duplicating the references here, so they can be easily copy-pasted:
$(release_AssemblyVersion)
$(release_FileVersion)
$(release_InformationalVersion)

There are few more changes needed for building only what wee need making the NuGet packages.

Set the Version field to the following value: {build}, so at the beginning of the build, the header looks like just the build number, until it's changed by the init script you have specified.

Another thing you need to specify is the branch specification.
Set the "Build only following branches" field to following:
/release\/.*/
This way the Appveyor project will only build the branches according to the regex: release/*, and that is what we need.

The last thing we need to set up is Automatic packaging feature so the Appveyor builds all the .nuspec projects in the repository.
In the Build settings menu, check the Package NuGet projects mark, optionally turning on Include NuGet symbol packages.
If the solution contains other NuGet package references needed for the build, you can also add a "Before build script" to restore the packages before building. Put "nuget restore" in the "Before build script" field.

Publishing

Make a pre-release build

Let's create and push a new branch which would start the build:
git checkout -b release/v1.0.0-alpha
git push --set-upstream origin release/v1.0.0-alpha

Watch the build queued and starting executing...
Then when the build finishes you can check the artifacts tab of the build to find and download the packages. The packages are also published to the project's NuGet feed which you can use, and pushing to the nuget.org feed is also possible, but I would recommend experimenting enough and making sure it works for you before you publish there, because the packages on nuget.org are non-removable after being published.

Now you can try to issue the following command to list the packages of your feed, including the pre-release ones.

nuget list -source -pre -all

You should see something like:

MyLib 1.0.0-alpha1
MyLib.Model 1.0.0-alpha1

Make a new release build

git checkout -b release/v1.0.0
git push --set-upstream origin release/v1.0.0

Wow! We've got a message:

A stable release of a package should not have on a pre-release dependency. Either modify the version spec of dependency "MyLib.Model (1.0.0-alpha &amp;&amp; &lt; 2.0)" or update the version field.
Command exited with code 1

Very nice to have a policy against making a release with a pre-release version of the source.
A dependent package requires us to specify a valid allowed dependency version range.

- <dependency id="MyLib.Model" version="[1.0.0-alpha, 2.0)"></dependency>
+ <dependency id="MyLib.Model" version="[1.0.0, 2.0)"></dependency>


Note that we removed the pre-release token from the version, and now the release-versioned package can be built with such a dependency.

Publishing on nuget.org

This is the easiest part.
Just use Settings\Deployment ... Providers

Choose NuGet as the Deployment provider from the drop-down list, and put in your NuGet API key.
Then you can issue the builds and those would end up in the nuget.org feed.

Please be careful with this part, because the packages on nuget.org can not be removed after publishing. This part is easy to set up and not worth experimenting here.

If you don't have a ton of releases you might be better off putting the packages on nuget.org manually after downloading them from the artifacts.

Thanks for reading.
I hope you will enjoy going open source with a complete automated versioning and publishing.

No comments:

Post a Comment