Semantic Versioning - Eine Einführung

Semantic Versioning sieht mit MAJOR.MINOR.PATCH einfach aus, wird aber schnell spannend, sobald Abhängigkeiten, Paketmanager und Breaking Changes ins Spiel kommen. Dieser Blogartikel erklärt die Idee hinter Semantic Versioning, typische Fallstricke und alternative Versionierungsmodelle. Wir schauen uns außerdem konkrete Implementierungen für C#, JavaScript/TypeScript, Python und PHP an.

Einleitung

Versionsnummern gehören zu den Dingen, die in der Softwareentwicklung zunächst vollkommen harmlos wirken. Eine Anwendung hat Version 1.0, nach einigen Änderungen folgt 1.1, irgendwann erscheint 2.0, und wenn es richtig gut läuft, vielleicht sogar 3.0.

Das klingt irgendwie logisch. Fast schon langweilig.

Spannend wird es erst, wenn eine Anwendung nicht mehr allein auf weiter Flur steht, sondern von Dutzenden oder Hunderten anderer Komponenten abhängt. Plötzlich muss ein Paketmanager entscheiden, ob er statt 1.4.2 gefahrlos 1.5.0 installieren darf. Eine CI-Pipeline muss herausfinden, ob ein neues Release mit bestehenden Anwendungen kompatibel ist. Ein Entwickler möchte wissen, ob er ein Update am Freitag um 16:45 Uhr noch wagen kann oder ob er damit besser bis Montag wartet.

In diesem Moment sind Versionsnummern keine dekorativen Etiketten mehr. Sie werden zu einer Sprache, mit der Entwickler, Paketmanager und Build-Systeme über Veränderungen und Kompatibilität kommunizieren.

Semantic Versioning, kurz SemVer, versucht, dieser Sprache eine klare Grammatik zu geben. Das bekannte Schema

MAJOR.MINOR.PATCH

wirkt simpel. Hinter diesen drei Zahlen steckt jedoch ein überraschend komplexes Versprechen.

Warum überhaupt Versionsnummern?

Solange Software als einzelnes Programm auf Disketten, CDs oder später als Download ausgeliefert wurde, war Versionierung vor allem eine organisatorische und kommerzielle Angelegenheit. Eine neue Version erhielt eine neue Nummer, damit Kunden, Supportmitarbeiter und Entwickler wussten, worüber sie sprachen.

“Der Fehler tritt in Version 4 auf.”

“Haben Sie bereits Version 4.1 installiert?”

“Nein, auf der CD steht 2002 Professional Edition.”

Das war nicht immer konsistent, aber meistens ausreichend. Versionsnummern konnten chronologisch, technisch oder rein marketinggetrieben sein. Manche Hersteller verwendeten Jahreszahlen, andere fortlaufende Hauptversionen und wieder andere interne Buildnummern, die nur für Eingeweihte verständlich waren.

Für ein eigenständiges Desktopprogramm ist das zunächst kein großes Problem. Der Benutzer installiert eine bestimmte Version, und diese Version enthält normalerweise alle benötigten Komponenten.

Moderne Software sieht anders aus. Eine durchschnittliche Webanwendung besteht nicht nur aus dem Code, den das eigene Team geschrieben hat. Sie verwendet Frameworks, Datenbanktreiber, Logging-Bibliotheken, JSON-Parser, Testframeworks, Buildwerkzeuge, CSS-Bibliotheken und viele weitere Pakete. Diese Pakete hängen wiederum von anderen Paketen ab.

Eine kleine Anwendung könnte beispielsweise folgende Abhängigkeiten besitzen:

Shop-Anwendung
├── Web-Framework
│   ├── Routing-Bibliothek
│   └── Logging-Abstraktion
├── Datenbanktreiber
│   └── Netzwerkbibliothek
└── JSON-Bibliothek

In einem realen Projekt kann dieser Baum Hunderte oder Tausende Knoten enthalten. Dabei handelt es sich nicht nur um direkte Abhängigkeiten, die ein Entwickler bewusst ausgewählt hat. Viele davon sind transitive Abhängigkeiten: Pakete, die benötigt werden, weil eine andere Bibliothek sie verwendet.

Ein Paketmanager wie NuGet, npm, Composer oder pip muss aus diesem Geflecht eine konkrete und möglichst widerspruchsfreie Menge installierbarer Pakete bilden. Dafür benötigt er mehr als Namen. Er muss Versionen verstehen und vergleichen können.

Angenommen, eine Anwendung verwendet die Bibliothek ImageTools in Version 1.4.2. Nun erscheint Version 1.4.3. Darf ein Paketmanager diese Version automatisch installieren?

Bei einer reinen Buildnummer wie 4711 oder 4712 weiß er nur, welche Version neuer ist. Er weiß nicht, ob das Update einen kleinen Fehler behebt oder die komplette API neu gestaltet.

Auch Versionsnummern wie 1.0, 2.0 oder 2026.08 helfen nur begrenzt. Sie sagen etwas über eine Reihenfolge oder einen Veröffentlichungszeitpunkt aus, aber nicht zwangsläufig über Kompatibilität.

Genau hier setzt Semantic Versioning an. Die Versionsnummer soll nicht nur identifizieren, welche Ausgabe eines Pakets vorliegt. Sie soll auch ausdrücken, welche Art von Veränderung stattgefunden hat.

Damit werden Versionsnummern maschinenlesbare Kommunikation.

Die Idee hinter Semantic Versioning

Semantic Versioning wurde ursprünglich von Tom Preston-Werner formuliert, einem der Mitgründer von GitHub. Die offizielle Spezifikation beschreibt ein Regelwerk für Versionsnummern und deren Bedeutung. Frühere Fassungen entwickelten sich schließlich zur heute maßgeblichen SemVer-Spezifikation 2.0.0. Bereits die frühe Version 0.1.0 enthielt die Grundidee einer öffentlichen API und des Schemas X.Y.Z; SemVer 2.0.0 präzisierte unter anderem die Regeln für Pre-Releases, Build-Metadaten und die Vergleichsreihenfolge.

Der Ausgangspunkt ist der sogenannte Dependency Hell, also der Abhängigkeitshölle. Der Name klingt knallermäßig, aber jeder Entwickler, der schon einmal einen Nachmittag (oder die ganze Nacht) mit widersprüchlichen Paketversionen verbracht hat, weiß: Die Bezeichnung kommt nicht von ganz ungefähr.

Ein typisches Problem entsteht, wenn Abhängigkeiten zu eng festgelegt sind. Bibliothek A verlangt exakt Version 1.4.2 von Bibliothek C, während Bibliothek B exakt Version 1.4.3 benötigt. Obwohl sich beide Versionen möglicherweise nur durch einen kleinen Bugfix unterscheiden, kann der Paketmanager keine gemeinsame Lösung finden.

Werden Abhängigkeiten dagegen zu großzügig formuliert, kann eine Anwendung plötzlich eine neue Bibliotheksversion erhalten, deren API nicht mehr kompatibel ist.

SemVer versucht, zwischen diesen beiden Extremen eine verlässliche Konvention zu schaffen. Die Grundidee lautet:

MAJOR.MINOR.PATCH

Dabei wird:

  • MAJOR bei inkompatiblen Änderungen der öffentlichen API erhöht.
  • MINOR bei rückwärtskompatiblen neuen Funktionen erhöht.
  • PATCH bei rückwärtskompatiblen Fehlerkorrekturen erhöht.

Die offizielle Spezifikation verlangt dabei ausdrücklich, dass Software, die SemVer verwendet, eine öffentliche API definiert. Diese API kann durch Code oder Dokumentation beschrieben sein, sollte aber präzise und umfassend festgelegt werden.

Das ist entscheidend, denn ohne eine definierte öffentliche API kann niemand zuverlässig beurteilen, ob eine Änderung kompatibel ist.

Dieser Vertrag ist nicht technisch erzwingbar. Kein Paketserver kann sicherstellen, dass 1.4.3 wirklich nur kompatible Fehlerkorrekturen enthält. SemVer funktioniert nur, wenn Paketautoren die Regeln verstehen und verantwortungsvoll anwenden.

Die Versionsnummer ist also eine Behauptung über Kompatibilität. Idealerweise ist diese Behauptung wahr.

Was MAJOR.MINOR.PATCH genau bedeutet

Nehmen wir eine Bibliothek mit folgender Version:

3.7.4

Die 3 ist die Major-Version, die 7 die Minor-Version und die 4 die Patch-Version.

Eine neue rückwärtskompatible Fehlerkorrektur führt zu:

3.7.5

Eine neue rückwärtskompatible Funktion führt zu:

3.8.0

Eine inkompatible Änderung führt zu:

4.0.0

Sobald eine höhere Komponente erhöht wird, werden die rechts davon liegenden Komponenten auf null zurückgesetzt. Nach 3.7.4 folgt daher bei einer neuen Funktion nicht 3.8.4, sondern 3.8.0. Nach einer Breaking Change folgt nicht 4.7.4, sondern 4.0.0.

SemVer erlaubt für diese drei Bestandteile ausschließlich nicht negative ganze Zahlen ohne führende Nullen. Gültig sind beispielsweise:

0.1.0
1.0.0
2.14.7
10.0.3

Nicht gültig ist dagegen:

01.2.3

Die führende Null in 01 ist verboten. Eine Versionsnummer soll eindeutig dargestellt werden. Andernfalls könnten 1.2.3, 01.2.3 und 001.2.3 als unterschiedliche Zeichenketten erscheinen, obwohl sie denselben numerischen Wert ausdrücken.

Die Zahlen besitzen keine feste maximale Größe. Rein nach SemVer wäre auch folgende Version möglich:

123456789.987654321.42

Ob jede Implementierung solche Zahlen ohne Überlauf verarbeiten kann, ist eine andere Frage. Die Spezifikation selbst setzt keine sinnvolle Marketinggrenze. Ein Projekt darf also theoretisch schneller Major-Versionen produzieren als ein Browser.

Patch: Das Versprechen eines kompatiblen Bugfixes

Ein Patch-Release enthält rückwärtskompatible Fehlerkorrekturen.

Angenommen, eine Bibliothek bietet folgende Methode zum Berechnen des Bruttopreises an (hier ein C#-Beispiel):

public static decimal CalculateGrossPrice(decimal netPrice, decimal taxRate)
{
    return netPrice * taxRate;
}

Die Methode ist offensichtlich falsch. Bei einem Nettopreis von 100 und einem Steuersatz von 0.19 liefert sie 19 statt 119.

Die Korrektur lautet:

public static decimal CalculateGrossPrice(decimal netPrice, decimal taxRate)
{
    return netPrice * (1 + taxRate);
}

Die öffentliche Signatur bleibt unverändert. Der dokumentierte Zweck der Methode wird nun korrekt erfüllt. Das ist ein klassischer Kandidat für ein Patch-Release:

1.3.2 → 1.3.3

Aber bereits dieses einfache Beispiel zeigt eine Schwierigkeit: Was ist, wenn ein Nutzer sich versehentlich auf das fehlerhafte Verhalten verlassen hat?

Vielleicht hat eine Anwendung das Ergebnis der Methode immer selbst zum Nettopreis addiert:

var grossPrice = netPrice + PriceCalculator.CalculateGrossPrice(netPrice, taxRate);

Nach der Fehlerkorrektur berechnet diese Anwendung plötzlich 219 statt 119.

Aus Sicht der dokumentierten API ist die Änderung kompatibel. Aus Sicht des tatsächlichen Laufzeitverhaltens dieses Nutzers ist sie es nicht.

SemVer kann nicht garantieren, dass niemand von einem Fehler abhängig ist. Ein Bugfix bedeutet deshalb nicht, dass sich überhaupt kein beobachtbares Verhalten ändert. Sonst könnte man Fehler niemals korrigieren. Entscheidend ist, ob das neue Verhalten dem dokumentierten Vertrag entspricht.

Ein Bugfix ist allerdings nicht automatisch ein Patch. Angenommen, eine Methode wirft bei ungültigen Eingaben bisher versehentlich NullReferenceException. Die Dokumentation verspricht keine bestimmte Exception. Eine Änderung zu ArgumentNullException ist wahrscheinlich kompatibel.

Verspricht die Dokumentation dagegen ausdrücklich NullReferenceException, ist die Exception Teil des Vertrages, auch wenn diese Entscheidung unglücklich war. Eine Änderung könnte dann inkompatibel sein.

Noch schwieriger wird es bei Sicherheitslücken. Manchmal lässt sich eine Sicherheitslücke nur schließen, indem bisher akzeptierte Eingaben abgewiesen, unsichere Algorithmen deaktiviert oder Standardwerte geändert werden. Fachlich handelt es sich um einen Bugfix. Für bestehende Nutzer kann die Änderung trotzdem inkompatibel sein.

Die ehrliche Antwort auf die Frage “Ist ein Bugfix immer Patch?” lautet daher: Nein. Nicht der Zweck der Änderung entscheidet allein, sondern ihre Auswirkung auf die öffentliche API und deren zugesagtes Verhalten.

Minor: Neue Funktionen ohne Bruch

Ein Minor-Release ergänzt rückwärtskompatible Funktionalität.

Eine Bibliothek besitzt beispielsweise:

public sealed class CsvWriter
{
    public void Write(
      Stream stream, 
      IEnumerable<string[]> rows)
    {
        // ...
    }
}

Nun kommt eine neue Methode hinzu:

public sealed class CsvWriter
{
    public void Write(
      Stream stream,
      IEnumerable<string[]> rows)
    {
        // ...
    }

    public Task WriteAsync(
        Stream stream,
        IEnumerable<string[]> rows,
        CancellationToken cancellationToken = default)
    {
        // ...
    }
}

Bestehender Code lässt sich weiterhin kompilieren und verhält sich wie zuvor. Neue Nutzer können zusätzlich asynchron schreiben. Das passt zu einem Minor-Release:

2.4.1 → 2.5.0

Auch Deprecations führen nach der SemVer-Spezifikation zu einer erhöhten Minor-Version. Wird eine vorhandene Funktion als veraltet markiert, bleibt sie zunächst verfügbar. Nutzer sollen jedoch erkennen, dass sie auf eine Alternative wechseln sollten.

Zum Beispiel:

[Obsolete("Use ParseAsync instead.")]
public ParseResult Parse(Stream stream)
{
    // ...
}

Die Methode existiert weiterhin. Bestehender Code funktioniert normalerweise noch, erzeugt aber eine Warnung. Das tatsächliche Entfernen der Methode wäre später eine Major-Änderung.

Neue Funktionen können dennoch unbeabsichtigt Breaking Changes verursachen. Betrachten wir ein Interface:

public interface IMessageFormatter
{
    string Format(string message);
}

Wird eine neue abstrakte Methode hinzugefügt,

public interface IMessageFormatter
{
    string Format(string message);

    string FormatError(Exception exception);
}

müssen alle bestehenden Implementierungen angepasst werden. Der Quellcode bricht beim Kompilieren. Obwohl eine Funktion hinzugefügt wurde, ist die Änderung nicht rückwärtskompatibel.

Das wäre daher eine Major- und keine Minor-Änderung.

In modernen C#-Versionen könnte eine Default-Implementierung die Situation abmildern:

public interface IMessageFormatter
{
    string Format(string message);

    string FormatError(Exception exception)
    {
        return Format(exception.Message);
    }
}

Doch selbst dann muss geprüft werden, ob binäre Kompatibilität, Zielplattformen und Laufzeitverhalten erhalten bleiben.

“Wir haben doch nur etwas hinzugefügt” ist also keine ausreichende Begründung für ein Minor-Release.

Major: Wenn bestehender Code angepasst werden muss

Eine Major-Version wird erhöht, wenn inkompatible Änderungen an der öffentlichen API veröffentlicht werden.

Ein offensichtliches Beispiel ist das Umbenennen einer Methode:

public void SendMessage(string text)

wird zu:

public void PublishMessage(string text)

Bestehender Code kompiliert nicht mehr. Die Änderung ist eindeutig inkompatibel.

Weitere typische Major-Änderungen sind:

// Vorher
public int GetCount()

// Nachher
public long GetCount()
// Vorher
public void Save(string path)

// Nachher
public Task SaveAsync(Uri destination)
// Vorher
public sealed class Customer

// Nachher
public abstract class Customer

Aber nicht jede Breaking Change ist in einer Methodensignatur sichtbar.

Angenommen, eine JSON-Bibliothek schreibt Eigenschaften bisher in PascalCase:

{
  "FirstName": "Franz",
  "LastName": "Beckenbauer"
}

Nach einem Update verwendet sie standardmäßig camelCase:

{
  "firstName": "Franz",
  "lastName": "Beckenbauer"
}

Die öffentliche C#-API könnte vollkommen unverändert sein. Trotzdem brechen externe Systeme, gespeicherte Dokumente oder Tests. Das Serialisierungsformat ist Teil des beobachtbaren Vertrages und damit praktisch Teil der öffentlichen API.

Dasselbe gilt für:

  • Konfigurationsschlüssel
  • Kommandozeilenargumente
  • HTTP-Endpunkte
  • Header
  • Datenbankschemata
  • Dateiformate
  • Ereignisnamen
  • Exitcodes
  • Exceptions
  • Standardwerte
  • zeitliches Verhalten
  • Sortierreihenfolgen
  • Thread-Safety-Zusagen

Eine öffentliche API besteht nicht nur aus Klassen und Methoden. Sie umfasst alles, worauf sich Nutzer legitimerweise verlassen dürfen.

Versionen kleiner als 1.0.0

SemVer behandelt die Major-Version 0 ausdrücklich als Phase der initialen Entwicklung. Eine Version 0.y.z gilt als instabil; alles kann sich jederzeit ändern, und die öffentliche API sollte noch nicht als stabil betrachtet werden. Version 1.0.0 definiert erstmals eine stabile öffentliche API.

Das führt häufig zu Versionen wie:

0.1.0
0.2.0
0.3.0
0.10.0
0.99.0

Manche Projekte bleiben jahrelang unter 1.0.0, obwohl sie millionenfach produktiv eingesetzt werden. Das ist formal erlaubt, untergräbt aber den kommunikativen Wert der Versionsnummer.

In der Praxis verwenden viele Projekte innerhalb der 0.x-Phase folgende informelle Konvention:

  • 0.MINOR.0 darf Breaking Changes enthalten.
  • 0.MINOR.PATCH enthält kompatible Bugfixes.

Danach wäre der Wechsel von 0.4.2 auf 0.5.0 vergleichbar mit einer Major-Änderung, während 0.4.2 auf 0.4.3 kompatibel bleiben sollte.

Das ist eine vernünftige Zusatzkonvention, aber keine Garantie der SemVer-Spezifikation. Diese sagt nur, dass sich während 0.y.z alles ändern kann.

Wer eine produktiv eingesetzte Bibliothek mit stabiler API pflegt, sollte sich daher fragen, ob 0.27.4 noch ehrlich ist oder ob die Angst vor 1.0.0 lediglich aufgeschobene Verantwortung darstellt.

Version 1.0.0 bedeutet nicht “perfekt”, “fertig für alle Zeiten” oder “garantiert fehlerfrei”. Sie bedeutet nur: Es gibt eine definierte öffentliche API, auf deren Kompatibilität sich Nutzer gemäß SemVer verlassen können.

Das ist bereits anspruchsvoll genug.

Pre-Releases: Alpha, Beta, RC und andere Warnschilder

Nicht jede Version ist für den allgemeinen produktiven Einsatz gedacht. SemVer erlaubt daher Pre-Release-Identifier.

Sie werden mit einem Bindestrich an die normale Version angehängt:

1.2.0-alpha
1.2.0-beta
1.2.0-rc.1
1.2.0-preview.5
1.2.0-alpha.3

Ein Pre-Release signalisiert, dass die Version instabil sein kann und möglicherweise nicht die Kompatibilitätsanforderungen der zugehörigen normalen Version erfüllt.

2.0.0-beta.1

bedeutet sinngemäß:

SemVer schreibt keine festen Begriffe wie alpha, beta oder rc vor. Folgende Version wäre syntaktisch ebenfalls gültig:

1.0.0-penguin

Ob sie eine hilfreiche Release-Strategie darstellt, ist eine andere Frage.

Pre-Release-Identifier bestehen aus durch Punkte getrennten Bezeichnern. Erlaubt sind ASCII-Buchstaben, Ziffern und Bindestriche:

1.0.0-alpha
1.0.0-alpha.1
1.0.0-x-y-z
1.0.0-rc.20260815

Numerische Identifier dürfen keine führenden Nullen besitzen. Daher ist

1.0.0-alpha.1

gültig, aber

1.0.0-alpha.01

ungültig.

Ein alphanumerischer Identifier darf dagegen Nullen enthalten:

1.0.0-alpha.01x

Er ist nicht rein numerisch und daher erlaubt.

Wichtig ist außerdem: Ein Pre-Release besitzt immer eine niedrigere Präzedenz als die zugehörige normale Version.

1.0.0-rc.1 < 1.0.0

Das fertige Release gewinnt also gegenüber allen Vorabversionen derselben Basisversion.

Build-Metadaten: Informationen ohne Sortierwirkung

Zusätzlich können Build-Metadaten mit einem Pluszeichen angehängt werden:

1.2.0+build42
1.2.0+20260815
1.2.0+git.a91f02c
1.2.0-beta.2+ci.1047

Build-Metadaten eignen sich beispielsweise für:

  • eine CI-Buildnummer
  • einen Git-Commit
  • einen Zeitstempel
  • eine Buildumgebung
  • eine interne Revisionskennung

Der entscheidende Punkt lautet: Build-Metadaten werden bei der SemVer-Präzedenz ignoriert.

Deshalb besitzen

1.2.0+build42

und

1.2.0+build99

dieselbe Präzedenz.

Das wirkt zunächst merkwürdig. Schließlich sehen die Zeichenketten unterschiedlich aus. SemVer unterscheidet aber zwischen Identität und Präzedenz.

Zwei Artefakte können unterschiedliche Buildinformationen tragen, ohne dass eines semantisch neuer oder kompatibler als das andere ist.

Build-Metadaten sind daher kein geeigneter Mechanismus, um tägliche Builds automatisch zu sortieren:

1.2.0+100
1.2.0+101
1.2.0+102

Nach SemVer sind alle drei hinsichtlich ihrer Präzedenz gleich. Wer eine Reihenfolge benötigt, sollte eine Pre-Release-Struktur verwenden:

1.2.0-ci.100
1.2.0-ci.101
1.2.0-ci.102

oder ein vom jeweiligen Ökosystem vorgesehenes anderes Versionsschema wählen.

SemVer-Versionen vergleichen

Versionsnummern dürfen nicht als normale Zeichenketten verglichen werden.

Ein lexikografischer Vergleich betrachtet Zeichen von links nach rechts. Dabei steht die Zeichenkette "10.0.0" häufig vor oder unter "2.0.0", weil das erste Zeichen 1 kleiner als 2 ist.

Numerisch ist jedoch klar:

10.0.0 > 2.0.0

SemVer vergleicht zunächst Major, dann Minor und schließlich Patch numerisch:

1.9.9 < 2.0.0
2.0.0 < 2.1.0
2.1.0 < 2.1.1
2.9.0 < 2.10.0

Besonders interessant wird der Vergleich bei Pre-Releases. Die offizielle Spezifikation nennt folgende Reihenfolge:

1.0.0-alpha
1.0.0-alpha.1
1.0.0-alpha.beta
1.0.0-beta
1.0.0-beta.2
1.0.0-beta.11
1.0.0-rc.1
1.0.0

Warum steht alpha vor alpha.1?

Sind alle bisherigen Identifier gleich und besitzt eine Version anschließend keine weiteren Identifier mehr, hat die kürzere Liste die niedrigere Präzedenz:

alpha < alpha.1

Warum steht alpha.1 vor alpha.beta?

Die ersten Identifier alpha sind gleich. Danach wird 1 mit beta verglichen. Ein numerischer Identifier hat eine niedrigere Präzedenz als ein nicht numerischer Identifier:

1 < beta

Warum steht beta.2 vor beta.11?

Beide zweiten Identifier sind numerisch. Deshalb werden sie als Zahlen und nicht als Strings verglichen:

2 < 11

Warum steht beta vor beta.2?

Wieder ist die gemeinsame Sequenz gleich, aber beta endet früher:

beta < beta.2

Warum steht rc.1 vor dem fertigen Release?

Jede Pre-Release-Version hat eine niedrigere Präzedenz als dieselbe normale Version:

1.0.0-rc.1 < 1.0.0

Nicht numerische Identifier werden lexikalisch anhand ihrer ASCII-Sortierung verglichen. SemVer weiß nicht, dass rc in menschlichen Releaseprozessen typischerweise nach beta kommt. Die gewünschte Reihenfolge entsteht hier zufällig passend durch die Bezeichnungen.

Folgende Reihenfolge wäre daher problematisch:

1.0.0-preview
1.0.0-rc
1.0.0-beta

SemVer sortiert nicht nach dem semantischen Reifegrad, den ein Team mit diesen Wörtern verbindet, sondern nach seinen formalen Regeln. Wer eine vorhersehbare Reihenfolge benötigt, sollte konsistente Identifier und gegebenenfalls numerische Stufen verwenden.

Probleme in der Praxis

Das SemVer-Schema ist präzise. Softwareentwicklung ist es nicht immer.

Das größte Problem lautet: Eine Versionsnummer wird von Menschen vergeben. Menschen übersehen Breaking Changes, interpretieren Kompatibilität unterschiedlich oder entscheiden sich bewusst gegen eine Major-Erhöhung.

Manchmal geschieht das aus Angst vor der Wirkung einer hohen Major-Version. Ein Projekt möchte nicht innerhalb von drei Jahren bei Version 17.0.0 landen und veröffentlicht deshalb inkompatible Änderungen heimlich als Minor-Releases.

Manchmal wird eine Änderung schlicht falsch eingeschätzt.

Ein Entwickler ergänzt einen optionalen Parameter:

public void Send(string message)

wird zu:

public void Send(string message, bool urgent = false)

Für direkten Quellcode sieht das kompatibel aus. Bestehende Aufrufe funktionieren weiterhin.

Doch Reflection-basierter Code sucht möglicherweise exakt nach einer Methode mit einem Parameter. Binär bereits kompilierte Anwendungen können sich anders verhalten. Mocking-Frameworks, Serialisierer oder Delegate-Bindings können betroffen sein.

Auch Überladungen können problematisch sein:

public void Write(string value)

wird ergänzt um:

public void Write(ReadOnlySpan<char> value)

Bestehende Aufrufe könnten nun mehrdeutig werden oder eine andere Überladung auswählen.

Eine vermeintlich kompatible Erweiterung kann also Quellcode- oder Verhaltenskompatibilität brechen.

Kompatibel in welcher Dimension?

Der Begriff “rückwärtskompatibel” ist nicht eindimensional. Mindestens folgende Perspektiven können relevant sein:

  • Quellcodekompatibilität: Bestehender Quellcode lässt sich weiterhin kompilieren.

  • Binärkompatibilität: Bereits kompilierter Code läuft weiterhin mit der neuen Bibliothek.

  • Verhaltenskompatibilität: Aufrufe liefern unter denselben Bedingungen weiterhin erwartbare Ergebnisse.

  • Datenkompatibilität: Gespeicherte oder übertragene Daten bleiben lesbar.

  • Konfigurationskompatibilität: Bestehende Konfigurationen funktionieren weiterhin.

  • Protokollkompatibilität: Andere Systeme können weiterhin kommunizieren.

  • Performancekompatibilität: Zeit- und Speicherverhalten bleiben innerhalb zugesagter Grenzen.

  • Plattformkompatibilität: Unterstützte Betriebssysteme, Laufzeiten oder Prozessorarchitekturen bleiben erhalten.

Eine Bibliothek kann in einer Dimension kompatibel und in einer anderen inkompatibel sein.

Angenommen, eine Suchmethode liefert bisher Ergebnisse in Einfügereihenfolge. Die Dokumentation sagt nichts über die Sortierung aus. Eine neue Implementierung liefert dieselben Elemente nun alphabetisch.

Formal könnte das als kompatibel gelten. Praktisch können Anwendungen brechen, weil sie sich auf die bisherige Reihenfolge verlassen haben.

Hier hilft nur eine klare Dokumentation der öffentlichen Zusagen.

Sind Performanceverbesserungen Breaking Changes?

Normalerweise ist eine schnellere Implementierung willkommen. Trotzdem kann selbst eine Performanceänderung problematisch sein.

Eine Bibliothek verarbeitet Ereignisse bisher langsam und nacheinander. Nach einer Optimierung arbeitet sie parallel. Die Methode liefert dieselben Ergebnisse, aber Callbacks treten nun in anderer Reihenfolge oder auf anderen Threads auf.

Oder eine Cache-Optimierung reduziert Datenbankzugriffe, hält Objekte dafür aber länger im Speicher. Anwendungen mit knappen Speichergrenzen verhalten sich anders.

Auch das Entfernen einer künstlichen Verzögerung kann Tests aufdecken, die versehentlich von Timing abhängig sind.

Eine reine Optimierung ohne vertraglich relevante Verhaltensänderung kann als Patch veröffentlicht werden. Ändert sie jedoch zugesicherte Threading-, Reihenfolge-, Speicher- oder Timing-Eigenschaften, kann eine Major-Version angemessen sein.

Sicherheitsupdates passen nicht immer ins Schema

Sicherheitskorrekturen werden häufig als Patch-Releases veröffentlicht, damit sie möglichst schnell in bestehende Versionsbereiche gelangen.

Das ist sinnvoll, solange die Korrektur kompatibel bleibt.

Aber nehmen wir an, eine TLS-Bibliothek erlaubt standardmäßig noch einen veralteten Algorithmus. Die sichere Änderung besteht darin, diesen Algorithmus zu deaktivieren. Alte Gegenstellen können sich danach nicht mehr verbinden.

Aus Sicherheitssicht ist die Änderung notwendig. Aus Kompatibilitätssicht ist sie möglicherweise ein Bruch.

Ein Major-Release würde jedoch bedeuten, dass viele Nutzer das Update nicht automatisch erhalten und verwundbar bleiben.

SemVer liefert hier keine magische Lösung. Projekte müssen Sicherheit, Kompatibilität und Verteilbarkeit abwägen. Wichtig ist vor allem, die Auswirkungen transparent zu dokumentieren.

Semantic Versioning in der Forschung

Eine groß angelegte Untersuchung des npm-Ökosystems zeigt, dass Semantic Versioning die automatische Weitergabe wichtiger Updates tatsächlich unterstützen kann. Bei korrekter Verwendung konnten kritische Updates wie Security-Patches in der Mehrzahl der untersuchten Fälle (laut Studie in 90,09 Prozent) schnell durch Abhängigkeitsketten weitergereicht werden. Probleme entstanden vor allem dann, wenn Entwickler Versionsbereiche ungeeignet definierten oder Änderungen mit einer falschen Major-, Minor- oder Patch-Version veröffentlichten.

Quelle:

Pinckney, Donald; Cassano, Federico; Guha, Arjun; Bell, Jonathan:  
A Large Scale Analysis of Semantic Versioning in NPM.
20th International Conference on Mining Software Repositories (MSR), 2023.  
DOI: 10.1109/MSR59073.2023.00073.  
Reprint: https://www.jonbell.net/preprint/msr23-npm.pdf

Andere Untersuchungen fanden ebenfalls nicht major-markierte Releases mit Breaking Changes. Das bestätigt eine unbequeme Wahrheit: SemVer kann Vertrauen strukturieren, aber nicht erzwingen.

Alternativen zu Semantic Versioning

SemVer passt besonders gut zu wiederverwendbaren Bibliotheken und Komponenten mit klarer öffentlicher API.

Nicht jede Software erfüllt diese Voraussetzungen.

Eine intern betriebene Webanwendung wird beispielsweise zentral ausgerollt. Nutzer wählen keine kompatiblen Versionen aus und binden die Anwendung nicht als Abhängigkeit ein. Ein Schema wie MAJOR.MINOR.PATCH kann dennoch hilfreich sein, besitzt aber weniger automatisierbare Bedeutung.

Auch Betriebssysteme, Browser und kontinuierlich ausgelieferte Dienste folgen häufig anderen Strategien.

Calendar Versioning

Beim Calendar Versioning, kurz CalVer, basiert die Versionsnummer auf dem Veröffentlichungszeitpunkt.

Ubuntu verwendet beispielsweise Versionen wie:

24.04
26.04

Die erste Zahl steht für das Jahr, die zweite für den Monat. Ein Nutzer erkennt sofort, wann die Version erschienen ist.

Weitere mögliche CalVer-Formate sind:

2026.8
2026.08.1
26.8
2026.218

CalVer eignet sich, wenn der Zeitpunkt einer Veröffentlichung wichtiger ist als die API-Kompatibilität. Das gilt häufig für:

  • Betriebssystemdistributionen
  • SaaS-Produkte
  • regelmäßig erscheinende Datenbestände
  • Firmware
  • monatliche Releasezüge
  • Produkte mit zeitgebundenem Support

Die Versionsnummer 2026.08 sagt nicht automatisch, ob sie mit 2026.07 kompatibel ist. Dafür kann sie unmittelbar das Alter und den Releasezyklus ausdrücken.

Buildnummern

Ein CI-System kann jedem Build eine fortlaufende Nummer geben:

1042
1043
1044

Das ist hervorragend für Eindeutigkeit und Nachverfolgbarkeit. Build 1044 ist eindeutig jünger als Build 1043.

Die Nummer sagt jedoch nichts über Art oder Kompatibilität der Änderungen aus. Sie eignet sich daher gut als interne Kennung oder zusätzliches Metadatum, aber selten als alleinige öffentliche Paketversion.

Ein hybrider Ansatz könnte lauten:

2.4.1+build.1044

Hier beschreibt 2.4.1 die semantische Version und build.1044 den konkreten Build. Allerdings bleibt zu beachten, dass Build-Metadaten die SemVer-Präzedenz nicht verändern.

Marketing-Versionen

Produktnamen und technische Versionen müssen nicht identisch sein.

Ein Hersteller kann ein Produkt als “Studio 2026” vermarkten, während intern folgende Versionen existieren:

14.3.7821
14.3.7902
14.4.8011

Marketing-Versionen sollen verständlich, einprägsam und verkäuflich sein. Technische Versionsnummern sollen vergleichbar und automatisierbar sein. Diese Ziele passen nicht immer zusammen.

Probleme entstehen, wenn dieselbe Nummer gleichzeitig beide Aufgaben erfüllen soll. Dann wird eine Major-Version möglicherweise nicht erhöht, weil das Marketing bereits einen Produktnamen festgelegt hat, oder sie springt ohne technische Notwendigkeit, weil ein neues Jahr begonnen hat.

Eine klare Trennung ist oft ehrlicher.

Browser und schnell laufende Major-Versionen

Moderne Web-Browser wie Chrome und Firefox verwenden hohe, regelmäßig steigende Hauptversionen. Eine neue Major-Version bedeutet dort nicht zwangsläufig denselben Kompatibilitätsbruch wie bei einer streng SemVer-versionierten Bibliothek.

Das ist nicht automatisch schlechte Versionierung. Web-Browser sind Endprodukte mit anderen Release- und Kompatibilitätsmodellen. Webstandards, Feature Flags, automatische Updates und kontrollierte Rollouts spielen dort eine größere Rolle als Paketabhängigkeitsbereiche.

Eine Zahl ist nur dann semantisch, wenn das Projekt klar erklärt, welche Bedeutung sie besitzt.

Linux, Windows und .NET

Bekannte Plattformen verwenden oft hybride oder historisch gewachsene Modelle.

Linux-Kernelversionen sehen zwar wie SemVer aus, ihre Komponenten folgen aber nicht zwingend dem SemVer-Vertrag einer Bibliotheks-API.

Windows besitzt Produktnamen, interne Buildnummern und verschiedene Versionsangaben parallel.

Bei .NET muss zwischen mehreren Ebenen unterschieden werden:

  • Produkt- beziehungsweise Runtime-Version
  • SDK-Version
  • NuGet-Paketversion
  • Assembly-Version
  • Datei-Version
  • Informational Version

Microsoft weist ausdrücklich darauf hin, dass NuGet-Paketversionen nach SemVer-Konventionen funktionieren, Assembly-Versionen dagegen vierteilige Nummern der Form Major.Minor.Build.Revision sind und für die Laufzeitbindung eine andere Aufgabe erfüllen.

Die wichtigste Lektion lautet daher nicht “Alle Projekte müssen SemVer verwenden”, sondern:

Semantic Versioning in der Programmierung

C#

In .NET begegnen Entwicklern mehrere Versionstypen, die ähnlich aussehen, aber unterschiedliche Regeln besitzen.

Warum System.Version kein SemVer-Typ ist

Die Klasse System.Version arbeitet mit zwei bis vier numerischen Komponenten:

Major.Minor
Major.Minor.Build
Major.Minor.Build.Revision

Beispielsweise:

var version = new Version("1.2.3.4");

Console.WriteLine(version.Major);    // 1
Console.WriteLine(version.Minor);    // 2
Console.WriteLine(version.Build);    // 3
Console.WriteLine(version.Revision); // 4

Das ist keine gültige SemVer-Version, denn SemVer besitzt genau drei numerische Kernkomponenten sowie optionale Pre-Release- und Build-Metadaten:

1.2.3-alpha.1+build.42

System.Version kann diese Zeichenfolge nicht als SemVer interpretieren:

var version = Version.Parse("1.2.3-alpha.1");
// FormatException

Umgekehrt ist

1.2.3.4

für System.Version normal, nach SemVer 2.0.0 aber ungültig.

System.Version ist nicht falsch. Es löst schlicht ein anderes Problem.

Die Bibliothek Enbrea.Semver

Das NuGet-Paket Enbrea.SemVer stellt die Klasse SemanticVersion bereit und orientiert sich möglichst strikt an SemVer 2.0.0.

Installation:

dotnet add package Enbrea.Semver

Eine Version lässt sich beispielsweise so parsen:

using Enbrea.SemVer;

var version = SemanticVersion.Parse(
    "1.4.0-rc.2+build.847");

Console.WriteLine(version.Major);      // 1
Console.WriteLine(version.Minor);      // 4
Console.WriteLine(version.Patch);      // 0
Console.WriteLine(version.Prerelease); // rc.2
Console.WriteLine(version.Metadata);   // build.847

Versionen können verglichen werden:

using Enbrea.SemVer;

var left = SemanticVersion.Parse("1.4.0-beta.2");
var right = SemanticVersion.Parse("1.4.0");

Console.WriteLine(left < right); // true

Für Eingaben aus externen Quellen kann die TryParse-Variante verwendet werden:

using Enbrea.SemVer;

if (!SemanticVersion.TryParse(input, out var version))
{
    Console.WriteLine("Ungültige SemVer-Version.");
}

Die Bibliothek NuGet.Versioning

Wer NuGet-Paketversionen oder NuGet-Versionbereiche verarbeitet, sollte NuGet.Versioning verwenden. Microsoft empfiehlt diese Bibliothek für die programmatische Arbeit mit NuGet-Versionen.

Installation:

dotnet add package NuGet.Versioning

Eine Version parsen:

using NuGet.Versioning;

var version = NuGetVersion.Parse("2.3.0-beta.4+build.91");

Console.WriteLine(version.Major);        // 2
Console.WriteLine(version.Minor);        // 3
Console.WriteLine(version.Patch);        // 0
Console.WriteLine(version.IsPrerelease); // true

Versionen vergleichen:

using NuGet.Versioning;

var beta = NuGetVersion.Parse("2.3.0-beta.4");
var release = NuGetVersion.Parse("2.3.0");

var result = VersionComparer.VersionRelease.Compare(beta, release);

Console.WriteLine(result < 0); // true

Einen NuGet-Bereich parsen:

using NuGet.Versioning;

var range = VersionRange.Parse("[1.2.0, 2.0.0)");

Console.WriteLine(range.Satisfies(NuGetVersion.Parse("1.8.0"))); // true
Console.WriteLine(range.Satisfies(NuGetVersion.Parse("2.0.0"))); // false

NuGet.Versioning ist die richtige Wahl, wenn die Semantik exakt dem NuGet-Ökosystem entsprechen soll. Für eine streng generische SemVer-Domäne kann Enbrea.Semver geeigneter sein.

Die Entscheidung sollte nicht allein anhand des Methodennamens Parse getroffen werden. Entscheidend ist, welches Versionsmodell verarbeitet wird.

TypeScript und JavaScript

Im JavaScript-Ökosystem ist das npm-Paket semver die zentrale Implementierung. Es wird auch von npm selbst verwendet und bietet Parsing, Validierung, Vergleich, Inkrementierung und umfangreiche Range-Unterstützung.

Installation:

npm install semver

In TypeScript oder modernem JavaScript:

import semver from "semver";

Abhängig von Modul- und Compilerkonfiguration kann auch ein Namespace-Import verwendet werden:

import * as semver from "semver";

Eine Version validieren:

import semver from "semver";

const version = semver.valid("1.2.3");

console.log(version); // "1.2.3"

Bei einer ungültigen Version liefert valid normalerweise null:

console.log(semver.valid("1.2"));   // null
console.log(semver.valid("1.2.3")); // "1.2.3"

Versionen vergleichen:

console.log(semver.gt("2.0.0", "1.9.9")); // true
console.log(semver.lt("1.0.0-beta", "1.0.0")); // true
console.log(semver.eq("1.2.3", "1.2.3")); // true

Sortieren:

const versions = [
  "1.0.0",
  "1.0.0-beta.2",
  "2.0.0",
  "1.5.0"
];

versions.sort(semver.compare);

console.log(versions);

Das Ergebnis lautet:

1.0.0-beta.2
1.0.0
1.5.0
2.0.0

Einen Bereich prüfen:

console.log(
  semver.satisfies("1.5.0", "^1.2.0")
); // true

console.log(
  semver.satisfies("2.0.0", "^1.2.0")
); // false

Eine Version erhöhen:

console.log(semver.inc("1.2.3", "patch")); // 1.2.4
console.log(semver.inc("1.2.3", "minor")); // 1.3.0
console.log(semver.inc("1.2.3", "major")); // 2.0.0

Pre-Releases lassen sich ebenfalls erhöhen:

console.log(
  semver.inc("1.2.3", "prerelease", "beta")
); // 1.2.4-beta.0

Ein wichtiger Stolperstein: Die npm-Bibliothek akzeptiert aus historischen Kompatibilitätsgründen teilweise ein führendes v und entfernt es. Ihr README weist darauf hin, dass diese Toleranz aus der Kompatibilität zu älteren SemVer-Konventionen stammt und nicht mehr verwendet werden sollte.

Daher kann Folgendes von der Bibliothek akzeptiert werden:

semver.valid("v1.2.3");

Nach der strengen SemVer-2.0.0-Spezifikation gehört das v jedoch nicht zur Versionsnummer. Ein Git-Tag namens v1.2.3 ist üblich und sinnvoll, aber die darin enthaltene SemVer-Version lautet 1.2.3.

Das ist ein gutes Beispiel für den Unterschied zwischen Spezifikation und Ökosystemkonvention.

Python

Im Python-Ökosystem ist besondere Vorsicht geboten. Python-Paketversionen werden typischerweise nach PEP 440 interpretiert. PEP 440 und SemVer überschneiden sich, sind aber nicht identisch.

Für Python-Pakete und Python-Abhängigkeitsauflösung ist daher meist packaging.version die richtige Wahl.

Installation:

python -m pip install packaging

Verwendung:

from packaging.version import Version

left = Version("1.2.3")
right = Version("1.3.0")

print(left < right)  # True

Pre-Releases:

from packaging.version import Version

beta = Version("1.0.0b2")
release = Version("1.0.0")

print(beta < release)  # True

PEP 440 verwendet typische Formen wie:

1.0.0a1
1.0.0b2
1.0.0rc1
1.0.0.post1
1.0.0.dev1

SemVer verwendet dagegen beispielsweise:

1.0.0-alpha.1
1.0.0-beta.2
1.0.0-rc.1

Manche Zeichenfolgen können normalisiert werden, andere besitzen unterschiedliche Bedeutungen. Besonders lokale Versionskennungen mit + sind in PEP 440 nicht einfach dasselbe wie SemVer-Build-Metadaten.

Wer eine echte SemVer-Domäne in Python implementiert, etwa einen Parser für eine sprachübergreifende Spezifikation, kann das Paket semver verwenden. Dessen Dokumentation beschreibt es als Python-Modul für SemVer und unterstützt Major, Minor, Patch, Pre-Release und Build-Metadaten.

Installation:

python -m pip install semver

Verwendung:

import semver

version = semver.Version.parse("1.2.3-beta.2+build.17")

print(version.major)      # 1
print(version.minor)      # 2
print(version.patch)      # 3
print(version.prerelease) # beta.2
print(version.build)      # build.17

Vergleich:

import semver

beta = semver.Version.parse("1.2.3-beta.2")
release = semver.Version.parse("1.2.3")

print(beta < release)  # True

Erhöhen:

import semver

version = semver.Version.parse("1.2.3")

print(version.bump_patch())  # 1.2.4
print(version.bump_minor())  # 1.3.0
print(version.bump_major())  # 2.0.0

Die Faustregel lautet:

  • Für Python-Paketversionen und Python-Paketmetadaten: PEP 440 und packaging.
  • Für ausdrücklich SemVer-konforme, sprachübergreifende Versionen: eine SemVer-Bibliothek wie semver.

Eine Bibliothek nur deshalb zu verwenden, weil ihre API angenehm aussieht, kann sonst zu subtil falscher Sortierung und Validierung führen.

PHP und Composer

Composer besitzt ein eigenes, mächtiges System für Versionen und Constraints. Das Paket composer/semver stellt die zugrunde liegenden Funktionen auch separat zur Verfügung.

Installation:

composer require composer/semver

Eine einfache Constraint-Prüfung:

<?php

require __DIR__ . '/vendor/autoload.php';

use Composer\Semver\Semver;

$result = Semver::satisfies(
    '1.5.0',
    '^1.2'
);

var_dump($result); // true

Versionen sortieren:

<?php

require __DIR__ . '/vendor/autoload.php';

use Composer\Semver\Semver;

$versions = [
    '1.0.0',
    '2.0.0',
    '1.5.0',
    '1.0.0-beta'
];

$sorted = Semver::sort($versions);

print_r($sorted);

Constraints können auch direkt verarbeitet werden:

<?php

require __DIR__ . '/vendor/autoload.php';

use Composer\Semver\VersionParser;

$parser = new VersionParser();
$constraint = $parser->parseConstraints('>=1.2 <2.0');

In einer composer.json könnte eine Abhängigkeit so aussehen:

{
  "require": {
    "vendor/package": "^1.4"
  }
}

Das Caret erlaubt dabei kompatible Aktualisierungen innerhalb des von Composer definierten Bereichs. Composer dokumentiert neben exakten Versionen unter anderem Vergleichsbereiche, Wildcards, Tilde- und Caret-Constraints.

Auch hier gilt: ^1.4 ist keine SemVer-Versionsnummer. Es ist ein Composer-Constraint, der SemVer-artige Versionen beschreibt.

Fazit

Semantic Versioning sieht aus wie eine kleine Rechenübung:

MAJOR.MINOR.PATCH

Patch für kompatible Bugfixes, Minor für kompatible neue Funktionen, Major für inkompatible Änderungen.

Doch sobald man genauer hinsieht, tauchen schwierige Fragen auf.

Was ist die öffentliche API? Gehört das JSON-Format dazu? Ist eine andere Exception ein Breaking Change? Darf ein Sicherheitsfix bestehende Verbindungen ablehnen? Ist eine schnellere parallele Implementierung noch verhaltenskompatibel? Wie behandelt ein Paketmanager Pre-Releases? Und warum akzeptiert eine Bibliothek v1.2.3, obwohl die Spezifikation das v nicht vorsieht?

Die Antworten zeigen, dass Semantic Versioning nicht primär von Zahlen handelt. Es handelt von Kommunikation und Vertrauen.

Bibliotheksautoren versprechen mit einer Versionsnummer eine bestimmte Art von Veränderung. Paketmanager verwenden dieses Versprechen, um Abhängigkeiten aufzulösen. CI-Systeme nutzen es für automatisierte Releases. Entwickler entscheiden damit, ob ein Update wahrscheinlich gefahrlos ist oder eine geplante Migration benötigt.

SemVer funktioniert dabei nicht, weil drei Zahlen alle Kompatibilitätsfragen lösen könnten. Es funktioniert, weil sich große Teile der Softwarewelt auf eine gemeinsame, nützliche Vereinfachung geeinigt haben.

Das System besitzt Grenzen. Menschen machen Fehler. Paketmanager verwenden unterschiedliche Range-Syntaxen. Ökosysteme wie NuGet, npm, Composer und Python ergänzen oder verändern einzelne Regeln. Manche Projekte sind mit Calendar Versioning oder Buildnummern besser bedient.

Trotzdem ist Semantic Versioning für wiederverwendbare Bibliotheken zu einem De-facto-Standard geworden. Nicht weil es perfekt wäre, sondern weil die Alternative häufig aus Versionsnummern besteht, die zwar ordentlich aussehen, aber niemandem etwas Verlässliches sagen.

Teile diesen Artikel