Versioning NuGet packages in a continuous delivery world: part 2
This is part 2 in a series of blog posts covering strategies for versioning a NuGet package. If you missed part 1, pick it up here. Today’s post talks about future improvements we’d like to make to the versioning and releasing flows. This post discusses future work that we haven’t fully designed yet, and we need your input. Along the way, you’ll spot a few specific calls for feedback. Email us, Send-a-Smile from within the product, or post comments here in the blog.
Using Release Management to promote packages
When we left off last time, we’d set up a repo, a build, and a feed to hold CI packages. The final paragraph glossed over how to share packages using Release Management. Let’s take a closer look at that flow to understand the positives and negatives. That will set the stage for talking about improvements our team wants to make in the coming months.
The previous feed was named “VersioningDemo.CI”, since it holds CI outputs. Create a second feed, “VersioningDemo.Release”, which will hold released packages. Copy the NuGet v3 URL of this new feed; you’ll need it in a few steps.
Before any of this will work, we need to make one additional alteration to the build definition. Go to the build definition from last time, find the Copy files step, and add a new line to the Contents entry. That line should read:
*.nupkg. This will ensure that Release Management gets access to the built packages (but not the restored packages that were inputs to the build).
Then, switch over to the Release tab and create a new release definition using the Empty template. Set the artifact source to Build and the build definition to the one you created last time.
Choose Add task, and from the Package category, choose NuGet Publisher. Like last time, switch the feed type to Internal. Paste the NuGet v3 URL of your release feed into the box. (Future feature: A feed picker so you don’t have to do the copy-and-paste dance with raw URLs.)
Create a new release (the “+ Release” button), and select a recent run of your build definition. This will stand in for our “known good, ready to release” package. Assume you’ve done automated testing on the package, shared it with your canary testers, or done whatever else you normally do to validate a release.
Navigate to the release (in my case, it was called Release 1), and if you set Environment 1 to be automatic as in the screenshot above, you should see the release either In Progress or Succeeded. And when you check back in the release feed, you should see your package pushed there. Success!
A better way to indicate quality: descriptors
Today, to share packages with other teams, you’d suggest that they take a dependency on your Release feed. Now you’re stuck managing multiple feeds with copies of the same package. Also, you’re going back to the build outputs and re-pushing the package to another feed, which feels wrong conceptually. What you’re really trying to express is a promotion flow – you’ve promoted a package from being a CI output to being a released artifact.
We’re working on a feature which will better express this promotion flow. Internally we’ve been calling it a quality descriptor¸ or more recently just a descriptor. A descriptor is lightweight metadata tag* attached to the package. We envision having a small default set of descriptors like “CI”, “beta”, and “release” to help you express quality gates – and of course we’d let you define your own.
What really makes descriptors powerful is the ability to filter on them. You would give your partner teams a filtered view of your feed. The filtered feed would appear to contain only packages which you deem have met that particular quality gate.
The flow I described above would no longer rely on re-pushing the package. Instead, the process would simply toggle the “release” descriptor. The package would then immediately become available to any partner team consuming the filtered feed.
This is call for feedback #1: What questions or concerns do you have with descriptors?
*We’d love to call the feature “tags” instead of “descriptors”, but decided that the word tag is already overloaded in the packaging world. NuGet has one kind of tags, npm has a different kind of tags, and of course there are already work item tags in VSTS/TFS.
Selecting version numbers
Even with descriptors, I have another gripe with this flow. In order to make my package versions unique, we spent most of part 1 generating a prerelease version string. And so long as your consumers don’t mind taking “prerelease” packages (even when you’ve decorated them with a release descriptor), that’s OK. But there are many reasons to want to strip off the prerelease version when you’re truly ready to release.
Today, you could add conditional logic to your version-number-generating script, then re-run the build to produce a non-prerelease package. But a new build means (potentially) different output which you haven’t tested. As a team, we see two options: Either be smarter at picking version numbers initially, or create a re-versioning tool that can safely strip the prerelease segment of a version number.
Better selection of version numbers
The first option is to go back to the drawing board on version numbering. Instead of directly controlling major.minor.patch, you could cede control of patch to a tool which would ensure monotonically increasing numbers. Advantage: Really simple to understand – each build gets a new, unique version number. Disadvantage: From your consumers’ perspective, you’ll have gaps in your version numbering (because it’s unlikely you’ll release every single build you crank out).
Let’s say you’ve specified major and minor versions somehow; call that X.Y. We imagine a tool that requests the next available patch number, which would start at 0 and work upwards. This would generate versions X.Y.0, X.Y.1, and so on. When you need to change X or Y, the next available patch number would reset to 0.
Stripping prerelease segments
The other option is to keep generating prerelease versions, then remove them when ready to release. Advantage: Semantically, this is more accurate – you’re “working toward 1.0.0” right up until you determine you’ve landed there. Disadvantage: It’s tricky to actually pull off.
Not surprisingly, in the simple case it’s simple: Open up the nupkg, rewrite the
<version> element in the nuspec, and you’re done. But what if your build produced multiple packages, some of which depend on each other? Now you need to discover the dependency graph and rewrite the appropriate
<dependency> elements. It’s not impossible to get this right, but rewriting NuGet nuspecs isn’t your core business, is it? Our team would build a REST API (and associated buttons, build tasks, and release tasks) to do the heavy lifting here.
Call for feedback #2: How do you feel about each of these version numbering ideas?
The last improvement we’d like to make is a direct result of customer feedback and real-world testing. We’ve long held – and most package management systems in the world agree – that once pushed, a package is immutable. You’re stuck with it forever. Sure, some places let you unlist or even delete a package, but you cannot change its contents.
But what if you accidentally shipped a bad package, perhaps one containing malicious content? The standard packaging answer is to unpublish/delete and publish again with a new version number. And for a public registry like NuGet.org, that’s the only reasonable stance. What’s worse than having a bad package called “foo 1.0.0” in the wild? Having both a bad and a good package in the wild and being unable to tell at a glance who has the good one and who has the bad thanks to multiple levels of caching, the exact timing of package installs, and more.
As a private package host, though, our customers know their scenarios and their ecosystem best. Many folks have told us they want an option to make feeds mutable or immutable, and they’re willing to deal with any consequences of making that decision. Some people don’t want to “burn” a good version number like 1.0.0 just because they accidentally published the wrong package.
We are considering just such a feature. We would still make feeds immutable by default and would issue a strongly-worded warning when you turn it off. We’re still deciding whether we should require an explicit delete-then-repush flow, or if we would allow direct overwrites upon a new push.
This feature should be used only on non-shared, staging-area feeds with a promotion flow that copies packages to an immutable feed for public consumption.
Call for feedback #3: How do you envision using mutable feeds?
Thanks in advance for feedback (email, Send-a-Smile, or here in the comments) on these future capabilities. And if you’re a Git user, go read part 3, the final in this series, where we’ll take a walk through GitVersion’s features.