Versioning and releasing NuGet packages

The VSTS Package Management team gets a fair amount of questions about the process of versioning, testing, and releasing NuGet packages, especially in the context of continuous integration builds that frequently produce new packages. We have several features that can help you create a CI/CD pipeline that produces, verifies, and releases packages. In this post, I’ll review some of the “obvious” solutions that turn out to be quite tricky and also details some recommended solutions.

Re-versioning packages, or ‘stripping the prerelease version’

We frequently hear variants of the question: “why can’t I re-version packages when I release them?” It usually comes from teams that are adhering to Semantic Versioning (SemVer) and want to create packages with prerelease versions (e.g. 1.0.0-beta1 or 1.0.0-20180906.1) from their continuous integration builds. On its face, this seems sensible: the prerelease part of a SemVer is explicitly designed to “indicate[] that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version.” [source]

However, an issue occurs once the team tests and wants to release their package(s). The simplest way to do that seems to be to remove the prerelease version segment. But, the nature of packages—especially archive formats like NuGet that contain the version and dependency versions inside the archive in a manifest—causes several problems with this strategy. First, what you’re releasing is no longer what you’ve tested. This is a bit pedantic, but it’s true: opening the package and modifying the version number means you’ve changed the bits you’re releasing, and you haven’t tested those changes. For a package with no dependencies, that’s not too scary. But, when you have packages that depend on other packages made in the same build, it gets a lot trickier, because you’ll need to change both the top-level package’s version and any dependency versions listed in that top-level package (and so on, recursively through the dependency graph of the packages you’re creating).

When we first created Package Management a few years ago, we tried for quite a while to come up with a solution that would do this for you programmatically. For packages without dependencies, this was a relatively straightforward proposition. But, for packages with dependencies we found this work to be a bug farm, and decided not to ship it.

Views can help

Hopefully, I’ve convinced you at this point that re-versioning packages once they’re created isn’t a great idea. Next, let’s look at some other workflows that can achieve the same goal of creating prerelease packages for testing, running integration tests, and the releasing non-prerelease packages to your consumers.

If you’re using VSTS Package Management and producing packages that are only used within your organization, views are a great option to consider. We built views to address this exact scenario. Views are a set of layers on top of your feed that can each contain a subset of the package-versions in the feed. Your consumers can select a view when adding your feed as an upstream source and limit their consumption to only packages that you’ve promoted into that view. They can also connect directly to a feed’s view if they’re using a package type (like NuGet) that supports multiple feeds in a single configuration file.

Views remove the need to use SemVer prerelease version segments. Instead, with views we recommend simply adopting an ever-increasing patch version number for your packages that increments with every build. You can set major and minor to whatever values you desire and then allow the build number to be the patch version.

In a nutshell, here’s the flow for working with views:

  • Create your package and publish it to your feed (we’ll sometimes call this the ‘base feed’ when working with views)
  • Run integration tests that consume the latest version of the package from your base feed
  • Promote the package into a view that denotes the quality you verified with your integration tests (e.g. @prerelease or @release, or create a view of your own description, based on your release process)
  • Then, your consumers use VSTS upstream sources to make that view’s packages available in their feed

Pros of this approach

  • You release exactly what you tested
  • You can create a workflow of sorts
  • It’s easy for users to subscribe to your prerelease feed
  • Works great with continuous integration by ‘hiding’ your CI exhaust in the base feed

Cons

  • Only works with VSTS Package Management
  • Doesn’t match SemVer’s notion of prerelease

Re-create the package with a new version

If you’re not using Package Management, or you don’t like the concept of views, we recommend re-publishing (with the non-prerelease version as an input) the package(s) you want to publish, once you’ve finished testing.

Here’s the flow for republishing:

  • Run a CI pipeline that compiles and unit tests your code and publishes your package (with a -prerelease version segment) to the feed
  • Run integration tests that consume the latest (prerelease) version of the package from your feed
  • Re-run the pipeline to publish a final package (without a -prerelease version segment)

This flow assumes that you have reproducible builds – that is, running your build process on the same commit produces the same outputs. It can be tricky to achieve reusable builds. Your build process must be the same, which is easy to achieve with a YAML build configuration file checked into the repo. Your build environment must also be the same, which can be achieved but is tricky with OS, Visual Studio, and other updates.

If you don’t have reproducible builds, there’s a similar flow you can use:

  • Run a CI pipeline that compiles and unit tests your code, publishes your package (with a -prerelease version segment) to the feed, and publishes all the inputs to your package (DLLs, scripts, manifests, etc.) as an artifact/output of the pipeline
  • Run integration tests that consume the latest (prerelease) version of the package from your feed
  • Run a new pipeline that takes the last pipeline’s artifacts as inputs and publishes a final package (without a -prerelease version segment)

Pros of this approach

  • You release almost exactly what you tested (same contents, but different package)
  • Works with any NuGet package feed
  • Works natively with the Visual Studio NuGet Package Manager’s prerelease version support

Cons

  • Requires reproducible builds or a transfer of all package inputs between pipelines
  • Every CI build’s version is visible to your consumers, so they must choose the version they want to consume

Summary

These recommendations can help you create a continuous integration pipeline for your NuGet packages. If you’re using another workflow, I’d love to hear about it in the comments.

 

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s