Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / DevOps / continuous-build

Automating NuGet Package Creation using AppVeyor

5.00/5 (4 votes)
11 Aug 2014CPOL5 min read 21.4K  
How to produce and publish a NuGet package using AppVeyor, the free continuous build service

I recently open-sourced a .NET library and I didn't provide a NuGet installer. I was shamed into providing one by Scott Hanselman, after he very kindly wrote a blog post about my project :)

I had never published a NuGet package before. It turns out it's really easy. I thought it'd be useful to describe how I did it for anyone else in the same situation.

There's basically three steps:

  1. Get an API Key from nuget.org
  2. Create a NuGet package
  3. Publish your package

Get an API Key

If you don't already have one, sign up for a NuGet account at nuget.org. If you have a Microsoft account, you can login with that.
Once you're logged in, your unique API Key is displayed alongside your account details. It's formatted like a GUID (e.g. ABCDEF12-ABCD-ABCD-ABCD-ABCDEF123456).

Keep the key private!

Create a NuGet package

A NuGet package contains two things: metadata and the files to install. In my case, I had just one file: MarkdownLog.dll

Here is the finished NuGet package:

MarkdownLog package, viewed using NuGet Package Explorer

I'm using the excellent NuGet Package Explorer to display its contents.

The left pane lists the metadata: name, version, copyright info - all the stuff you'd expect.

The package's contents are shown in the right pane. MarkdownLog.dll is the only file that needs to be installed. It's in a specially named folder, portable-windows8+net45. This name tells NuGet the .NET framework version it requires.

The NuGet package itself is built using nuget.exe. I downloaded nuget.exe to a subfolder of my project called NuGet. This is where I put all NuGet related files.

To create a package, NuGet needs to know what files and metadata to include. This is defined in a .nuspec file.

Here's the one I used (view on GitHub):

<code><?xml version="1.0"?>
<package >
  <metadata>
    <id>MarkdownLog</id>
    <version>$version$</version>
    <authors>Stuart Wheelwright (@wheelies)</authors>
    <licenseUrl>https://github.com/Wheelies/MarkdownLog/blob/master/LICENSE</licenseUrl>
    <projectUrl>https://github.com/Wheelies/MarkdownLog</projectUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Lightweight .NET component for programmatically generating Markdown. Useful for producing rich diagnostic logs with minimal dependencies.</description>
    <copyright>Copyright 2014 BlackJet Software Ltd</copyright>
    <tags>markdown diagnostics logging pcl</tags>
  </metadata>
  <files>
     <file src="..\MarkdownLog\bin\Release\MarkdownLog.dll" target="lib\portable-windows8+net45" />
  </files>
</package>
</code>

The contents of this file should be self-explanatory, but comprehensive documentation is available if you'd like to delve deeper.

I should explain the $version$ part though.

I could have hardcoded the version (e.g. 0.9.0), but then I'd have to manually keep it in sync with the version number in the DLL.
You see, when MarkdownLog.dll is produced, it is automatically given a unique version number by the automated build system.

I use the wonderful AppVeyor for continuous integration. It will automatically build, test and package up the project's output every time anyone commits a file to the project's GitHub. The service is free for open source projects and I'd thoroughly recommend it.

You can configure your project's build in AppVeyor in two ways: either using its easy to use web interface, or by creating a file called appveyor.yml in the root of your GitHub repository. I started with the web interface but moved to using a file once everything was working and I was comfortable with how it worked. Having the project's build configuration in a file means I can compare revisions, and rollback to an earlier version if anything goes wrong.

The complete build config can be seen in MarkdownLog's GitHub repository. This is the part that automatically injects the version information into the DLL:

<code>version: 0.9.0.{build}

assembly_info:
  patch: true
  file: AssemblyInfo.*
  assembly_version: "{version}"
  assembly_file_version: "{version}"
  assembly_informational_version: "{version}"
</code>

{build} is an automatically incrementing number, provided by AppVeyor's build system. This is used to create the full version number (e.g. 0.9.0.23), which is automatically injected into AssemblyInfo.cs, prior to the .NET solution build. If you look at the file properties of MarkdownLog.dll, you'll see the end result:

MarkdownLog.dll File Properties

The next part of the appveyor.yml build configuration specifies the name of the .NET solution to build:

<code>build:
  project: MarkdownLog.sln
</code>

If the .NET build completes successfully, the unit tests will run. If the tests pass, a script called pack.ps1 is executed:

<code>after_test:
  - ps: .\nuget\pack.ps1
</code>

pack.ps1 is a simple PowerShell script that creates the NuGet package:

<code>$root = (split-path -parent $MyInvocation.MyCommand.Definition) + '\..'
$version = [System.Reflection.Assembly]::LoadFile("$root\MarkdownLog\bin\Release\MarkdownLog.dll").GetName().Version
$versionStr = "{0}.{1}.{2}" -f ($version.Major, $version.Minor, $version.Build)

Write-Host "Setting .nuspec version tag to $versionStr"

$content = (Get-Content $root\NuGet\MarkdownLog.nuspec) 
$content = $content -replace '\$version\$',$versionStr

$content | Out-File $root\nuget\MarkdownLog.compiled.nuspec

& $root\NuGet\NuGet.exe pack $root\nuget\MarkdownLog.compiled.nuspec
</code>

The pack.ps1 script is heavily based on this one I found in another GitHub project.

pack.ps1 and MarkdownLog.nuspec both live in the NuGet folder, along with nuget.exe

The interesting part of pack.ps1 is right at the end, where a call is made to nuget pack. This call will build a .nupkg file using a copy of the .nuspec file that has been injected with the full version number.

A .nupkg file is created for every build. You can see an example on the MarkdownLog artifacts page

Publish your package

The final part of the process is the easiest.

Now we have an API Key and a .nupkg file, we can put them together and publish the package to NuGet.org

If we were doing this manually, we'd call a couple of NuGet commands:

<code>nuget setApiKey Your-API-Key
nuget push YourPackage.nupkg
</code>

To automate in AppVeyor, it's almost as simple, but there's one extra step. We first need to encrypt our API Key. This encrypted version can then become public without needing to expose the private key.

AppVeyor provides an online tool for encrypting the key. Enter your private key in the box, click Encrypt and you'll get a key that can be placed in your appveyor.yml build configuration file, like this:

<code>deploy:
  provider: NuGet
  api_key:
    secure: n/2gZwCGLcoLBr5nGu5akS0IFQ1YHciI6Wz0NGC+1mLTUtO08Uy+TWn7OtyLG9v3
  artifact: /.*\.nupkg/
</code>

The AppVeyor-encrypted version of the API Key is placed in the secure value. The artifact value tells AppVeyor where to find the package. I've used a regex pattern to match all files with a .nupkg extension.

And that's it.

When the build successfully completes, a NuGet package is automatically created and published to NuGet.org

But, you probably don't want a NuGet package to be published every time anything in the repository changes. It would be sensible to only publish packages that contain well-tested bug fixes or new features; not one every time a typo is fixed in a code comment!

Currently, I don't have a good solution to this. I currently control the publishing by commenting out/uncommenting the deployment section of the build config. I'd really like a more elegant solution. One option I'm considering is to create a new release branch in GitHub. The build for this branch would automatically publish the package, but the normal dev branch wouldn't. To publish, I'd simply do a merge from dev to release.

Can you suggest a better alternative?

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)