dotnet semantic versioning in a continuous deployment environment
Asked Answered
G

2

7

I am configuring semantic versioning with GitLab for my dotnet core apps and netstandard 2.0 packages.

After reading quite a bit of opinions, some of them contradictory, this is what is clear to me. A semantic version should be something like M.m.P.B-abc123 where

  • M is major version
  • m is minor version
  • P is patch version
  • B is build version (optional)
  • -abc123 is suffix (optional) in case I use pre-releases. It must start with letter

So the following package versions would be valid:

  • 1.0.0
  • 1.0.1.20190301123
  • 1.0.1.20190301123-beta
  • 1.0.1-rc1

I have the following gitlab script for my versioning

#Stages
stages:
  - ci
  - pack

#Global variables
variables:
  GITLAB_RUNNER_DOTNET_CORE: mcr.microsoft.com/dotnet/core/sdk:2.2
  NUGET_REPOSITORY: $NEXUS_NUGET_REPOSITORY
  NUGET_API_KEY: $NEXUS_API_KEY
  NUGET_FOLDER_NAME: nupkgs

#Docker image
image: $GITLAB_RUNNER_DOTNET_CORE

#Jobs
ci:
  stage: ci
  script:
    - dotnet restore --no-cache --force
    - dotnet build --configuration Release
    - dotnet vstest *Tests/bin/Release/**/*Tests.dll

pack-beta-nuget:
  stage: pack
  script:
    - export VERSION_SUFFIX=beta$CI_PIPELINE_ID
    - dotnet pack *.sln --configuration Release --output $NUGET_FOLDER_NAME --version-suffix $VERSION_SUFFIX --include-symbols
    - dotnet nuget push **/*.nupkg --api-key $NUGET_API_KEY --source $NUGET_REPOSITORY
  except:
    - master

pack-nuget:
  stage: pack
  script:
    - dotnet restore
    - dotnet pack *.sln --configuration Release --output $NUGET_FOLDER_NAME
    - dotnet nuget push **/*.nupkg --api-key $NUGET_API_KEY --source $NUGET_REPOSITORY
  only:
    - master

This generates packages such as: 1.0.0 for master branch (stable or production ready) and 1.0.0-beta1234567 for any other branch.

The problem with my approach is that I have VS solutions with multiple projects, each project will be a nuget package and each one has its own version. Sometimes I modify one project but not the other, therefore in theory I shouldn't need to produce a new artifact of the project that I didn't touch nor a new version, of course.

Right now my nuget repository prevents overwriting packages, so If there is a XXX.YYY 1.0.0 and I generate another XXX.YYY 1.0.0 and push it to the repository, it will throw an error and the pipeline will fail.

I have thought that maybe it's not such a bad idea to generate a new package each time I run the CI/CD pipeline, so I considered introducing the build number and have something like XXX.YYY 1.0.0.12345 and, even if I don't touch anything there, the next time a new package XXX.YYY 1.0.0.123499 would be produced.

Is this a correct approach in a continuous deployment scenario? or should I look for a way to make my script smarter and not to produce a new artifact if there is already one with the same version in my nuget repository?

Assuming it's ok to use build numbers always, how do I make sure that only the build number is retrieved from the pipeline but the M.m.P version numbers remain in my csproj as per the following?

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <Description>Whatever</Description>
    <VersionPrefix>1.0.1</VersionPrefix>
  </PropertyGroup>
</Project>

I would need something like: dotnet pack *.sln --configuration Release -p:PackageVersion=$FIXED_VERSION.$CI_PIPELINE_ID --output nupkg

but I don't know how to retrieve the <VersionPrefix> content from the csproj through the CLI.

Any advice, good read or solution for my approach assuming it's valid?

Thanks

Gander answered 9/4, 2019 at 15:10 Comment(0)
V
5

The problem with my approach is that I have VS solutions with multiple projects, each project will be a nuget package and each one has its own version. Sometimes I modify one project but not the other, therefore in theory I shouldn't need to produce a new artifact of the project that I didn't touch nor a new version, of course.

Since a continuous integration pipeline is unable to determine if your code should be a new minor or major version you will always have to determine which semantic version your package should get. This makes the whole process a lot easier.

The guys from visual studio team services have this to say about it:

Immutability and unique version numbers In NuGet, a particular package is identified by its name and version number. Once you publish a package at a particular version, you can never change its contents. But when you’re producing a CI package, you can’t know whether it will be version 1.2.3 or just a step along the way towards 1.2.3. You don’t want to burn the 1.2.3 version number on a package that still needs a few bug fixes.

SemVer to the rescue! In addition to Major.Minor.Patch, SemVer provides for a prerelease label. Prerelease labels are a “-” followed by whatever letters and numbers you want. Version 1.0.0-alpha, 1.0.0-beta, and 1.0.0-foo12345 are all prerelease versions of 1.0.0. Even better, SemVer specifies that when you sort by version number, those prerelease versions fit exactly where you’d expect: 0.99.999 < 1.0.0-alpha < 1.0.0 < 1.0.1-beta.

Xavier’s blog post describes “hijacking” the prerelease tag to use for CI. We don’t think it’s hijacking, though. This is exactly what we do on Visual Studio Team Services to create our CI packages. We’ll overcome the paradox by picking a version number, then producing prereleases of that version. Does it matter that we leave a prerelease tag in the version number? For most use cases, not really. If you’re pinning to a specific version number of a dependent package, there will be no impact, and if you use floating version ranges with project.json, it will mean a small change to the version range you specify. Source: https://devblogs.microsoft.com/devops/versioning-nuget-packages-cd-1/

As stated first you still need to pick a version yourself for you new package. For the process before the actual publishing of the final package (master branch in your case) you can use the pre release tag to include the build number as the pre release tag.

Without doing anything smart this will publish a new package for every pipeline that is run. The people at visual studio team services and I do not think that this is a bad thing but this will be up to everyone's personal taste

Valerivaleria answered 20/4, 2019 at 16:43 Comment(0)
P
1

I know it's a little bit late, but maybe others in the future also ask this question.

For background: I work in a small company and we started in the NodeJS world with NPM packages. After a while we also started building with .NET Core and we kind of adapted some things from the JavaScript world. A tool we used heavily is Semantic Release. It automates versioning by parsing commit messages. So you (and everyone developing on the project) use a special format for commits with a prefix type like fix, feat, chore and so on, and Semantic Release parses every commit message since the last release and determines the next version number.

Since in JS world you have to only update the package.json file it kinda works out of the box but for .NET there is a little bit more work to do. We use the Directory.Build.props file to set the version like you would do with package.json (but it would work in a csproj file too).

Now Semantic Release works with plugins and one plugin we wrote can update files with the version number. So we use it to update the verson number in Directory.Build.Props. But instead of the version in the file you could also directly give the version number to the dotnet nuget cli but we found it more convenient to have it in the file.

So here's the flow we use for our (internal) NuGet packages:

  • configure Semantic Release to update the version number in Directory.Build.props file (before creating the package)
  • create the nuget package (we have another plugin for that, but you could just use the exec plugin during the prepare stage of SR
  • publish the nuget package during the publish stage of SR

The CI script would then just a call to npx semantic-release in an environment that has NPM as well as the .NET SDK available (we use container images for that).

Especially for OP's situation it would be a little bit more complicated, since multiple projects exists in one repository and SR is made to work in a single repository since it works with git tags to determine the last version. So I would advise to split it in multiple repositories.

The benefits are:

  • automation of release whenever there is a new feature/bugfix
  • automation of version numbers that adhere to Semantic Versioning (and is unopinionated)
  • automatically generate a Changelog along with every release
  • depending on the Git Platform you can also create releases (there are plugins for GitHub, GitLab and others)
  • you can configure SR to create pre-releases in certain branches, for example you can have an alpha or beta or develop branch
  • you could also add the commit hash to the NuGet package so everyone using it would know exactly what is bundled in it
  • Semantic Release creates a commit with the updated package.json (you can configure it to also include the updated Directory.Build.props and I would advise to do so)

And all you have to do is to configure it one time and enforce a special Commit Message style (which one is not so important, you can configure SR accordingly).

Proconsul answered 17/12, 2021 at 13:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.