Simplifying NuGet Versioning with MinVer

Monday, May 1, 2023

In this blog post, I'll share with you a great tool that has been a real gem in my .NET development workflow. It's called MinVer, a minimalistic tool designed to automatically version your NuGet packages based on your Git history.

In the past, I've used GitVersion, a comprehensive tool that serves various advanced situations quite well. However, when it comes to simpler versioning scenarios, there is an alternative that's more straightforward and user-friendly. That's where MinVer comes in.

Semantic Versioning: A Quick Introduction

Before we dive into the how-to of MinVer, it's important to understand the basic concept of semantic versioning.

Semantic versioning, often abbreviated as SemVer, is a versioning scheme for software that aims to give meaning to the underlying changes in a release. It uses a three-part number format: X.Y.Z, where:

  • X is the 'Major' version number: This number is incremented when there are breaking-changes in the API, meaning that the changes will break the existing users' code and they will need to modify their code to accommodate these changes.

  • Y is the 'Minor' version number: This number is incremented when new, backwards-compatible functionality is introduced. Backwards-compatible in this context means that the changes won't break the existing users' code - the users can continue using their existing code, but there's new functionality available if they want to use it.

  • Z is the 'Patch' version number: This number is incremented when backwards-compatible bug fixes are introduced. These are fixes to existing functionality that don't introduce any new features.

ℹ️

An example of a SemVer version number is 4.10.9

The beauty of SemVer is that it sets clear expectations for the users of a library or API about the impact of updating to a new version.

If only the patch version has changed, the user can expect that no new functionality has been added, and no existing functionality has been changed. If the minor version has changed, there might be new features, but the existing code will continue to work. However, if the major version has changed, the user should be prepared to make changes to their code to accommodate the new version.

NuGet and Semantic Versioning

NuGet, also adopts semantic versioning, but with a slight twist to accommodate pre-release versions.

Before a "final" or "stable" release (the X.Y.Z in semantic versioning), there can be multiple pre-release versions. These could be "alpha" or "beta" releases, "release candidates", or any other version that is not considered "final".

In NuGet, a pre-release version is appended to the standard semantic version number with a hyphen followed by a string of alphanumeric characters and possibly more hyphens. For example, 1.0.0-alpha, 1.0.0-beta, 1.0.0-rc1, 1.0.0-alpha.1.2 are all valid NuGet pre-release versions. It's important to note that these pre-release versions are considered "lower" than the associated stable release. So, 1.0.0 would be considered an update to 1.0.0-rc1.

This method allows you to release, distribute, and use non-final versions of packages, which can be particularly useful in the testing phase of development.

MinVer Installation

To start using MinVer, you first need to install it. You can do this via the dotnet CLI using:

dotnet add package MinVer

Basic Usage

Once MinVer is installed, you can start versioning your packages. Use the dotnet pack command and you should see a version like 0.0.0-alpha.0.XX.

Understanding Height

MinVer has a concept of "height". In essence, the height is the number of commits made since the last tag was created in Git.

To illustrate, if you create a tag with git tag 0.1.0 and run dotnet pack, you should now see version 0.1.0.

After making changes to the code and committing it using git commit, running dotnet pack again will generate a new version: 0.1.1-alpha.0.1.

This is actually expected because you are working on the next version after 0.1.0, so the height is 1. The alpha.0 part is the default pre-release tag that MinVer uses.

Prerelease Tagging

For specific prerelease tagging, you can tag the version as beta (or any other term you prefer) using git tag 0.1.1-beta and then run dotnet pack. It will generate a version of 0.1.1-beta. After committing a change, this will become 0.1.1-beta.1 and after that 0.1.1-beta.2, etcetera.

Using MinVer in CI/CD Pipelines

The basic concept of MinVer is quite straightforward and works excellently for local development. However, it can become a bit more complex when implementing it in various branching/release strategies in CI/CD pipelines. Let's explore how to utilize MinVer in this context.

ℹ️

Remember, MinVer is oblivious of the branch you're working on and has no knowledge of the release process. This might require a shift in perspective, especially if you're accustomed to automatic versions like GitVersion or Azure DevOps' byBuildNumber versioning scheme.

Assume you're working with a basic Git Flow branching strategy, aiming to release a new version of your package when a Pull Request from develop to main is completed. Typically, you would have an automated build and release script running, which triggers on a commit to the main branch.

Your process might look like this:

  1. Create a new feature on the develop branch.
  2. Commit and push changes to the develop branch on your remote (origin) repository.
  3. Create a new tag 0.1.0 on the develop branch and push this tag to the remote (origin) repository.
  4. Initiate a Pull Request to merge the develop branch into main.
  5. Merge the Pull Request into the main branch.
  6. The CI/CD pipeline triggers and builds the new version of the package.

However, this will generate a version number of 0.1.1-alpha.0.1 for the NuGet package.

And actually, this is quite clear from the logs you can collect in MinVer CLI using the --verbosity d option:

MinVer: Using    { Commit: 0c4ef1b, Tag: '0.1.0', Version: 0.1.0, Height: 1 }.
MinVer: The calculated version 0.1.1-alpha.0.1 satisfies the minimum major minor 0.0.
MinVer: Calculated version 0.1.1-alpha.0.1.

What happens is that you perform the pull-request merge on the main branch, but the last tag is still on the commit before that. So MinVer will see the merge as a new commit after the tag and increment the version, as it should!

How to solve this?

It's actually pretty simple: you should tag the commit that is the merge commit of the Pull Request. So, after merging the Pull Request, you should tag the commit on the main branch that is the merge commit.

This might involve adjusting your process slightly. I've changed our release process to:

  1. Create a new feature on the develop branch.
  2. Commit and push changes to the develop branch on your remote (origin) repository.
  3. Create a Pull Request to merge the develop branch into main.
  4. Merge the Pull Request into the main branch.
  5. The CI/CD pipeline triggers and builds the new version of the package with a preview version: 0.1.0-alpha.0.1.
  6. Create a new tag 0.1.0 on the latest commit in the main branch and push that tag to the remote repository, using the following script:
  git checkout main
  git pull
  git tag 0.1.0
  git push origin 0.1.0
  1. Manually kick off the CI/CD pipeline and build the new version of the package with the correct final version of 0.1.0.

This approach offers a couple of additional benefits:

  • The preview version, which is built and pushed to NuGet, can be used for testing in other environments before building the real release version.
  • By manually considering the tag for the version number, you're encouraged to maintain SemVer versioning consistency when deciding if it's a patch, minor, or major release.

A Heads-Up for Azure DevOps and GitHub Actions Users

When using MinVer with Azure DevOps or GitHub actions, you might encounter a scenario where the default version is used instead of the version tag from the history. You can read more about this issue and there is a simple workaround for it:

Azure DevOps

In your Azure DevOps pipeline YAML file, add the fetchDepth parameter with a value of 0 to the checkout step:

Azure Pipeline
steps:
  - checkout: self
    fetchDepth: 0
    fetchTags: true

GitHub Actions

In your GitHub Actions workflow YAML file, use the actions/checkout action with the fetch-depth parameter set to 0:

GitHub Actions
steps:
  - name: Checkout repository
    uses: actions/checkout@v2
    with:
      fetch-depth: 0

This makes sure that the full Git history is fetched and MinVer will be able to find the version tags and generate the correct version for your NuGet packages.

Customizations

Although MinVer is designed to be minimalistic, it does offer some basic customizations via its Options. However, in my experience, the default options provided by MinVer have proven to be quite effective for most use cases.

Wrapping Up

In conclusion, I think MinVer offers an easy-to-use and practical solution for versioning .NET assemblies based on Git history. It's a powerful tool that can handle both simple and complex versioning scenarios.