Laden...

[Artikel] Automatische, standardisierte Versionsvergabe in und mit .NET

Erstellt von Abt vor 3 Jahren Letzter Beitrag vor 3 Jahren 2.390 Views
Abt Themenstarter:in
16.827 Beiträge seit 2008
vor 3 Jahren
[Artikel] Automatische, standardisierte Versionsvergabe in und mit .NET

Automatische, standardisierte Versionsvergabe in .NET

Während kleine Applikationen (aka Tools) meist daraufsetzen, dass die Versionen einfach hart kodiert werden, weil zum Beispiel nur ein Entwickler am Code sitzt, setzen Teams in der Regel auf externe Tools, die Versionen erzeugen und so eine übergreifende Versionierung ermöglichen: Build, Docs, Code, Installer...
Mittlerweile gibt es aber sehr einfache Möglichkeiten mit keinem / kaum Aufwand eine integrierte Lösung für das automatische Erstellen von Versionen zu ermöglichen und so menschliche Fehler zu vermeiden.
Einmal aufgesetzt skalieren solche Lösungen auch in Zukunft meist sehr gut.

Dieser Artikel behandelt und beschreibt einen einfachen, weit verbreiteten Automatismus, der durchaus als Best Practise angesehen werden kann, um basierend auf der Git-Historie Versionen zu errechnen, die sowohl für Builds, Applikationen und NuGet-Pakete verwendet werden können.
Einen älteren Ansatz, der heute kaum noch genutzt wird, ist unter [FAQ] Automatisches Vergeben der Revisions- und Buildnummer beschrieben.

Der Sinn von Versionen

Der grundlegende Sinn von Versionsnummern sind technische Aspekte, sodass der Entwickler den Verlauf einer Software nachvollziehen kann. Es gibt jedoch viele weitere Bestandteile, je größer ein Projekt oder ein Produkt wird: zum Beispiel Betrieb, Support und Marketing.

  • Dem Entwickler zeigen sie, welcher Quellcode im Einsatz ist
  • Der Support sieht, zu
  • Der Benutzer kann erkennen, ob seine Software aktuell ist
  • Der Vertrieb hat ein Marketing-Instrument bei entsprechender Neuerung

Siehe auch Software Versionsnummern auf Wikipedia.

SemVer aka Semenatic Versioning

Semantic Versioning ist ein Muster zur standardisierten Vergabe von Versionen von Applikationen. SemVer gilt heute als Industrie-Standard und wird Technologie- und Tool-übergreifend verwendet.
Dies betrifft auch das gesamte Ökosystem von .NET inkl. Visual Studio sowie NuGet.

Die Grundlage der Versionsnummern bilden MAJOR.MINOR.PATCH und werden basierend auf folgenden Regeln erhöht:

  • MAJOR wird erhöht, wenn Schnittstellen-inkompatible Änderungen veröffentlicht werden, wozu auch entsprechende Methoden-Signaturen gehören
  • MINOR wird erhöht, wenn neue Funktionalitäten, welche kompatibel zur bisherigen Schnittstelle sind, veröffentlicht werden
  • PATCH wird erhöht, wenn die Änderungen ausschließlich Schnittstellen-kompatible Bugfixes umfassen

Des Weiteren gibt es noch Suffixe, die entsprechende Meta-Informationen (Alpha / Beta / Commit IDs...) enthalten können; aber nicht müssen.

Versionen in .NET Standard / .NET Core / .NET 5 und höher Applikationen über Projektdateien

Prinzipiell ist die Angabe von Versionsinformationen in Projektdateien optional.
Der Compiler verwendet diese Informationen um automatisiert die entsprechenden Assembly-Attribute zu generieren:

So wird aus

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AssemblyVersion>1.1.1</AssemblyVersion>
    <FileVersion>2.2.2</FileVersion>
    <Version>3.3.3-xyz</Version>
  </PropertyGroup>
</Project>

entsprechend


using System;
using System.Reflection;

[assembly: System.Reflection.AssemblyCompanyAttribute("mycsharp")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("2.2.2")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("3.3.3-xyz")]
[assembly: System.Reflection.AssemblyProductAttribute("netcoreappver")]
[assembly: System.Reflection.AssemblyTitleAttribute("netcoreappver")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.1.1")]

Der Vollständigkeit halber für die .NET Framework Legacy-Welt
Versionen in .NET Framework / Mono / Portable Class Libraries etc

In der Legacy-Welt von .NET Framework werden die Versionsinhalte standardmäßig in der AssemblyInfo.cs gepflegt.

// Version information for an assembly consists of the following four values:
// Major Version
// Minor Version
// Build Number
// Revision
[assembly: AssemblyVersion("1.0.0.0")]  
[assembly: AssemblyFileVersion("1.0.0.0")]

Diese Inhalte verwendet MSBuild für das Setzen der entsprechenden Versionsinformationen in einer Exe oder in einer DLL, die dann zum Beispiel über die Windows-Explorer-Details ausgelesen werden können.

Versionen in .NET Standard / .NET Core / .NET 5 und höher Applikationen die CLI

Der deutlich einfacherer Weg und die Basis für den späteren Automatismus gilt jedoch die Versionierung über die CLI über den Build. Dieser Weg gilt als der empfohlene Weg!
.NET Core bzw. .NET 5 und höher haben entsprechende Parameter für MSBuild integriert, die die Versionierung über die CLI ermöglichen.

dotnet build --configuration Release /p:Version=1.2.3

Damit landen die Versionsinformationen ebenso in der Exe bzw. der Assembly.

Versionen in NuGet Paketen

Beim Erstellen von NuGet-Paketen müssen die Versionsinformationen entweder über die Projektdatei-Informationen geliefert werden oder auch hier über die empfohlene Variante: die Kommandozeile.
Hierfür kann dem publish-Befehl entsprechend wie dem Build die Version mitgegeben werden:

dotnet pack --configuration ${{ env.BuildConfig }} /p:Version=1.2.3

Automatisches Vergeben der Versionen: mit Git

Es gibt verschiedene Art und Weisen wie Versionen vergeben werden können:

  • Man verwendet den aktuellen Zeitstempel mit einer Build-Nummer
  • Integration von Marketing-Versionen wie zB. einer Jahreszahl oder einer Produktnummer
  • ... viele weitere Wege

Etabliert hat sich jedoch vor allem in den letzten Jahren das automatische Berechnen von Versionen anhand der Historie in der Quellcode-Verwaltung; meist aus Git.
Daher behandle ich an dieser Stelle auch nur Git.

Die Berechnung über die Historie sorgt dafür, dass automatisch immer ein eindeutiges Resultat berechnet werden kann. Ein sehr weit verbreitetes Tool und Teil der .NET Foundation Familie ist NerdBank.GitVersion von Andrew Arnott.
https://github.com/dotnet/Nerdbank.GitVersioning

dotnet/Nerdbank.GitVersioning

NBGV - wie das Kürzel von Nerdbank.GitVersioning lautet - bietet verschiedene Integrationsmöglichkeiten; allen voran ein dotnet tool, mit dessen Hilfe entsprechende Befehle über die Kommandozeile verfügbar werden. Es wird auf allen Betriebssystemen unterstützt, die .NET 5 und höher unterstützen.

Hierzu muss das Tool über dotnet tool install -g nbgv installiert werden:

C:\source\MyCSharp\AutoVersionSample [main =]> dotnet tool install -g nbgv                           
You can invoke the tool using the following command: nbgv 
Tool 'nbgv' (version '3.3.37') was successfully installed.

Mit Hilfe des Befehls nbgv install wird nun eine version.json (muss im Hauptverzeichnis/Root liegen) erzeugt, in der folgende Dinge zB. konfiguriert werden können.

{
    "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
    "version": "2021.1", // "product version" is fix because "versionIncrement" is set to "build"
    "nugetPackageVersion": {
      "semVer": 1 // optional. Set to either 1 or 2 to control how the NuGet package version string is generated. Default is 1.
    },
    "publicReleaseRefSpec": [
      "^refs/heads/main$", // we release out of master
    ],
    "cloudBuild": {
      "buildNumber": {
        "enabled": true
      }
    },
    "release": {
      "versionIncrement" : "build",
      "firstUnstableTag": "preview"
    }
  }

Diese Konfiguration sorgt nun dafür, dass anhand der Basis-Version 0.1 alle weiteren Versionen anhand der Git-Historie errechnet werden.
In dieser Config gilt der main-Branch als unser Produktiv-Branch (ältere Git-Repositories verwenden meist noch master) und ebenso wird der Build als das Element verwendet, das automatisch erhöht wird.
Damit bleibt die major-Version hier immer bei 2021 und kann/muss von Hand erhöht werden. Dies ist an dieser Stelle bewusst so, da dieses Beispiel den weit verbreiteten Ansatz verfolgt, dass wir mit einer Produkt-Version (hier 2021) arbeiten.

Des weiteren ist folgendes Konfiguriert:

  • Builds durch Build-Automatismen (Azure DevOps, GitHub, GitLab..) - sogenannte Cloud-Builds - sollen erkannt und ebenfalls behandelt werden
  • Jede generierte Version, die nicht aus dem main-Branch stammt - also kein Release darstellt - erhält ein Preview-Suffix
  • Bei der Generierung von Versionen wird SemVer auch für NuGet Pakete verwendet (die alte NuGet-Welt vor 4.3.0 hat dies nicht unterstützt)

Das vollständige Schema seht ihr hier: NerdBank.GitVersioning/version.schema.json

Manuelle Versions-Generierung

Sofern alles entsprechend konfiguriert ist und man sich innerhalb eines Git-Repositories befindet, kann nun über die Kommandozeile nbgv get-version[ die Version erzeugt und damit das Setup getestet werden:


C:\source\MyCSharp\AutoVersionSample [main =]> nbgv get-version
Version:                      0.1.2.5424
AssemblyVersion:              0.1.0.0
AssemblyInformationalVersion: 0.1.2+3015ad3ab6
NuGetPackageVersion:          0.1.2
NpmPackageVersion:            0.1.2

Sofern man nur einen spezifischen Wert haben möchte, weil man diesen vielleicht direkt über die Kommandozeile an dotnet build übergeben werden will, dann geht das entsprechend mit dem -v Parameter.

C:\source\MyCSharp\MyCSharp.BBCode [main =]> nbgv get-version -v version
0.1.2.5424

NBGV und Git

Wie bereits erwähnt verwendet NBGV die Git-Informationen des Repositories für die Erzeugung der Version.
Dabei wird primär beachtet: befindet man sich in einem Release Branch oder nicht. Feature-Branches sollen natürlich Unstable-Versionen erzeugen.

Ebenso beachtet NGBV die Historie, um die Nummer zu erzeugen; was ein Segen und ein Fluch zu gleich ist:
Bei Git-Versionierung versucht man prinzipiell die Hostorie schlank zu halten, was die Folge hat, dass Feature-Branches meistens mit der Merge-Strategie Squash konfiguriert sind.
Squash sorgt dafür, dass bei einem Merge alle vorhandenen Commits innerhalb des Feature-Branches zusammengepresst und zu einer einzigen Commit-Nachricht werden: aus vielen Commits und damit einer längeren Historie wird nur noch eine.

Da die Historie jedoch die Basis für die Berechnungen der Versions-Nummern darstellt sorgt gleiches dafür, dass die Berechnung von NBGV durcheinander kommt.
Es empfiehlt sich daher kein Squash zu nutzen, wenn Versionen automatisch anhand der Git-Historie berechnet werden sollen.

Wir haben während der Neuentwicklung leider genau das gemacht, was uns dann beim NuGet-Paket-Handling etwas Probleme gemacht hat, da neue Versionen plötzlich laut Versionsnummer plötzlich älter waren.

Automatische Versionierung mit Hilfe von Build-Systemen

Wie schon im Abschnitt der Konfiguration angerissen unterstützt NBGV auch direkt verschiedene Build-Systeme wie Azure DevOps, GitLab, GitHub und Co...
Auch hierfür wird NBGV wieder über das dotnet tool installiert; die Version wird aber durch den Befehl nbgv cloud erzeugt.

Als Beispiel verwende ich an dieser Stelle nun den Build des neuen myCSharp-Forums in Azure DevOps. Dafür existieren folgende Build Steps:


## Versioning
- script: dotnet tool install -g nbgv
  displayName: Install nbgv

- script: nbgv cloud
  displayName: Git Version


## Build
- script: dotnet build -c Release --no-restore /p:Version=$(NBGV_SemVer2)
  displayName: .NET Build

## Pack NuGet
- script: dotnet pack mycsharp.project.csproj --no-build /p:Version=$(NBGV_NuGetPackageVersion)
  displayName: .NET Pack

NBGV_SemVer2 ist dabei eine Umgebungsvariable, die automatisch durch den Befehl nbgv cloud erzeugt wird.
Eine vollständige Dokumentation für das Cloud-Build von NBGV seht ihr hier.

Script contents:
nbgv cloud
\========================== Starting Command Output ===========================
"C:\windows\system32\cmd.exe" /D /E:ON /V:OFF /S /C "CALL "D:\a\_temp\f8353bd7-e9f4-45db-853b-ac66b3e12272.cmd""
Async Command Start: Update Build Number
Update build number to 0.1.172 for build 1287
Async Command End: Update Build Number
Finishing: Build Git Version

Wir sehen hier also, dass 1287 mal unser Build angestoßen wurde und dafür die Version 0.1.172 erzeugt wurde. Wir erkennen also, dass wir aus einem Release-Branch heraus den Build angestoßen haben.
Beim Anstoßen des Builds über einen Feature-Branch wird beispielweise das der Output sein:

Generating script.
Script contents:
nbgv cloud
\========================== Starting Command Output ===========================
"C:\windows\system32\cmd.exe" /D /E:ON /V:OFF /S /C "CALL "D:\a\_temp\10e0c23b-7f8f-48a0-af33-fcc4f3a3bb7a.cmd""
Async Command Start: Update Build Number
Update build number to 0.1.175+3ec6d8eec1 for build 1285
Async Command End: Update Build Number
Finishing: Build Git Version

Wir sehen hier also einen Suffix (die Commit-ID, Kurzform), der anhand der Grundregeln von SemVer diese Version als Unstable deklariert: so wie es für einen Feature-Branch auch gewünscht ist.

Diese Version wird im Falle eines Buildsystems jedoch nicht nur für den Build verwendet; NBGV setzt in den entsprechenden Build-Systemen die Version auch direkt für den Build.
So wird beispielsweise in Azure DevOps nicht mehr die Build-Id in der UI angezeigt, sondern die hier generierte Build-Version, was das entsprechende Matching sehr übersichtlich macht.

NBGV ist ein sehr mächtiges Tool.
Eine vollständige Dokumentation könnt ihr auf GitHub entnehmen.

Statische Integration von Versionen in Anwendungen

NBGV bietet neben dem Tooling der Kommandozeile auch ein NuGet-Paket, das alle Versionsinformationen auch automatisch während dem Build auslesen und automatisch statisch in die entsprechende Assembly packen kann - vollständig ohne irgendwelchen zusätzlichen Befehle oder Übergabeparameter.
Dazu muss einfach nur das NuGet Paket Nerdbank.GitVersioning in euer Projekt eingebunden werden

Dieses Paket sorgt dafür, dass alle Assembly-Informationen automatisch gesetzt werden und darüber hinaus eine statische Klasse erzeugt wird, die entsprechende Versionsinformationen ganz ohne Assembly-Reflection verfügbar macht.

Beispiel:


[assembly: System.Reflection.AssemblyVersion("1.0")]
[assembly: System.Reflection.AssemblyFileVersion("1.0.24.15136")]
[assembly: System.Reflection.AssemblyInformationalVersion("1.0.24-alpha+g9a7eb6c819")]

internal sealed partial class ThisAssembly {
    internal const string AssemblyVersion = "1.0";
    internal const string AssemblyFileVersion = "1.0.24.15136";
    internal const string AssemblyInformationalVersion = "1.0.24-alpha+g9a7eb6c819";
    internal const string AssemblyName = "Microsoft.VisualStudio.Validation";
    internal const string PublicKey = @"0024000004800000940000...reallylongkey..2342394234982734928";
    internal const string PublicKeyToken = "b03f5f7f11d50a3a";
    internal const string AssemblyTitle = "Microsoft.VisualStudio.Validation";
    internal const string AssemblyConfiguration = "Debug";
    internal const string RootNamespace = "Microsoft";
}

Auch unser neues Forum verwendet dieses NuGet Paket, um Transparenz zu zeigen, welche Version - inkl. genauer Commit Id - wir im Betrieb haben.

<footer class="footer">
   Made with ? and ASP.NET Core. Running @(ThisAssembly.AssemblyInformationalVersion) on @(RuntimeInformation.OSDescription) with @(RuntimeInformation.FrameworkDescription)

Mehr unter NBGV for .NET


Entstanden zusammen mit gfoidl.

Historie:
2021-01-04: Veröffentlichung