Semantic Release

Lesezeit: 10 Minuten

Semantic Release ist ein Tool, mit dem sich Semantic Versioning automatisieren lässt. Für die Verwendung muss der Code der zu versionierenden Software in einem Git-Repository verwaltet und eine Continuous Integration Pipeline (CI Pipeline) zum Bauen der Software eingerichtet sein. Ich setze dafür GitLab ein.

Semantic Versioning

Der Begriff Semantic Versioning wurde von Tom Preston-Werner, einem Mitbegründer von Github, spezifiziert und beschreibt im Wesentlichen eine Versionssemantik, die aus drei aufeinanderfolgenden und durch Punkte voneinander getrennten natürlichen Zahlen besteht:

1.2.3

Die drei Stellen der Version stehen für die Major Version, die Minor Version und die Patch Version.

Major Version

Die Major Version wird inkrementiert, wenn eine neue Version Änderungen einführt, die nicht abwärtskompatibel sind. Dies kann bedeuten, dass in einer Java-Bibliothek eine öffentliche Klasse durch eine andere ersetzt worden ist oder sich die Signatur einer öffentlichen Methode geändert hat, oder dass sich die Rahmenbedingungen geändert haben und ein in der vorherigen Version noch valider Aufruf nun zu einer Exception führt. Bei einer REST-Schnittstelle kann es bedeuten, dass sich die Datenstruktur im Request oder der Response geändert hat, neue Pflichtfelder hinzugekommen sind oder sich die Url-Struktur geändert hat. Bei einer Software mit graphischer Benutzeroberfläche ist es denkbar, dass ein Upgrade mit einem Migrationsverfahren für die Datenbank oder die Konfiguration verbunden ist. Wird die Major Version inkrementiert, so werden die Minor und Patch Version auf 0 zurückgesetzt: 1.2.3 → 2.0.0

Minor Version

Die Minor Version sagt aus, dass neue Funktionalität hinzugekommen ist, die Software jedoch abwärtskompatibel bleibt. Es sind also keine manuellen Handgriffe notwendig, wenn man einfach die neue Version durch die alte ersetzt. Wird die Minor Version inkrementiert, so wird die Patch Version auf 0 zurückgesetzt: 1.2.3 → 1.3.0

Patch Version

Die Patch Version gibt an, dass die neue Version lediglich Korrekturen enthält, aber keine neue Funktionalität: 1.2.3 → 1.2.4

Alpha, Beta und Release Candidate

Bestimmte Softwareversionen werden manchmal als Alpha, Beta oder auch Release Candidates (rc) deklariert. Diese Bezeichnungen sollen deutlich machen, dass eine Software noch nicht für den produktiven Einsatz empfohlen werden kann, weil sie möglicherweise noch schwerwiegende Fehler enthält. Generell spricht man von einer Alpha-Version, wenn eine Software erstmalig an Dritte zum Testen herausgegeben wird. Eine Beta-Version ist ausgereifter und enthält schon die meisten wesentlichen Funktionen. Sie wird oft veröffentlicht, um Probleme bei den Anwendern frühzeitig zu entdecken, die während der Entwicklung nicht auffallen würden, weil sie etwa nur in Verbindung mit bestimmter Hardware oder anderer Software oder unerwarteter Bedienung durch den Anwender auftreten. Ein Release Candidate kann als Vorabversion bezeichnet werden und ist in der Regel feature complete. Vom Release Candidate hin zum Release werden dann nur noch Fehler behoben, aber keine neue Funktionalität mehr ergänzt.

Beispiele für semantische Alpha-, Beta- und RC-Versionen sind:

  • 0.1.0-alpha.1
  • 0.1.0-beta.1
  • 0.1.0-rc.1

Semantic Release

Semantic Release ist in Javascript geschrieben, kann aber für das Bauen beliebiger Software eingesetzt werden. Das Tool leitet die nächste Version von der letzten Release-Version und den zwischenzeitig hinzugekommenen Git-Commits ab. Semantic Release erstellt auf dieser Basis die nächste Version als Git-Tag.

Git-Commit-Semantik

Git-Commits für das Semantic Release müssen die Angular Git Commit Guidelines befolgen. Dabei wird einem Commit ein Präfix vorangestellt, welches kennzeichnet, ob es sich um eine Fehlerkorrektur, ein Feature oder einen Breaking Change, also eine nicht abwärtskompatible Änderung handelt.

Die Präfixes fix: (Korrektur) und perf: (Performance-Verbesserung) inkrementieren die Patch Version. Das Präfix feat: (Feature) inkrementiert die Minor Version. Die Zeichenkette BREAKING CHANGE: im Body der Commit-Nachricht inkrementiert die Major Version. Gibt es mehrere passende Commit-Nachrichten, so inkrementiert Semantic Release die größte dazu passende Versionsstelle um eins. Eine Version 1.2.3 wird bei zwei Commits mit dem Präfix feat: und drei Commits mit dem Präfix perf: auf 1.3.0 angehoben.

CI-Integration

Semantic Release verwendet für verschiede CI-Umgebungen verschiedene Plugins und benötigt ein Access Token, das als Umgebungsvariable zur Verfügung stehen muss. Das Token ist notwendig, um den Git-Tag zu pushen.

Plugins

Neben dem angesprochenen Plugin für die CI-Umgebung kann Semantic Release durch weitere Plugins erweitert werden. So lässt sich zum Beispiel steuern, aufgrund welcher Trigger in der Commit-Nachricht eine Patch-/Minor-/Major-Version gebaut wird und wie die Release Notes geschrieben werden.

Automatische Versionierung mit Semantic Release, GitLab-CI und Gradle

Im Folgenden zeige ich, wie ich für eine Software eine automatisierte Versionierung aus der Build-Pipeline heraus mit Semantic Release konfiguriert habe. Bei der Software handelt es sich um eine Java-SE-Anwendung. Die deployte Version enthält die Versionskennung im Dateinamen und gibt diese beim Start auf der Konsole aus.

GitLab CI Secrets

Semantic Release benötigt ein GitLab Personal Access Token, um den Git-Tag zu pushen und eine Konfigurationsdatei, um bestimmte Einstellungen vorzunehmen. Beides habe ich in GitLab CI Secrets ausgelagert. Der Personal Access Token benötigt die Scopes api und write_repository. Ich habe das Secret als Type: Variable mit Key: GL_TOKEN abgelegt. Der Key ist nicht frei wählbar, für GitLab erwartet Semantic Release den Token in der Umgebungsvariablen GITLAB_TOKEN oder GL_TOKEN, bei Github z.B. gilt entsprechend GITHUB_TOKEN oder GH_TOKEN. Da ich nur aus protected Branches heraus versionieren möchte, habe ich auch die CI Secrets auf protected gesetzt.

Die Konfigurationsdatei habe ich als Type: File mit Key: SEM_RELEASE_OPTIONS angelegt. Sie hat folgende Struktur:

plugins:
- - "@semantic-release/commit-analyzer"
  - preset: angular
    releaseRules:
    - type: break
      release: major
    - type: feat
      release: minor
    - type: fix
      release: patch
    - type: perf
      release: patch
- "@semantic-release/release-notes-generator"
- "@semantic-release/gitlab"
branches:
- "master"

In der Konfigurationsdatei werden die zu ladenden Plugins aufgelistet. Die Reihenfolge in der Konfigurationsdatei entspricht dabei der Reihenfolge, in der Semantic Release die Plugins aufrufen wird. Weiterhin habe ich spezifiziert, dass Semantic Release nur Git-Commits vom Branch master auswerten soll. Das Plugin Commit Analyzer habe ich weiter konfiguriert, da mir die Standardeinstellung für Breaking Changes nicht gefällt. Mit dem Präfix break: wird nun also in meiner Pipeline ein Major Release erzeugt. Die Konfigurationsdatei lässt sich wahlweise auch im Json-Format angeben. Darüber hinaus muss sie nicht zwingend als CI Secret injiziert werden, es ist auch denkbar in der Pipeline ein eigenes Docker-Image zu verwenden, das die Konfigurationsdatei bereits enthält.

GitLab CI

In der gitlab-ci.yml spezifiziere ich das zu verwende Docker-Image, welches Semantic Release installiert und ausführt. Bei einem größeren Projekt empfehle ich hierfür ein eigenes Docker-Image zu erstellen. Für mein Projekt reicht mir ein in GitLab vorhandenes Standard-Image, in welchem ich für jeden Releasevorgang Semantic Release neu installiere:

semver:
  stage: release
  image: node:13
  before_script:
    - npm install @semantic-release/gitlab
    - cat $SEM_RELEASE_OPTIONS > .releaserc.yml
  script: npx semantic-release -t \${version}
  only:
    - master

Ich verwende das zum Zeitpunkt des Artikels aktuelle Docker-Image node:13, welches npm mitbringt, so dass ich die GitLab-Variante von Semantic Release installieren kann. Ausgeführt wird Semantic Release mit dem npm Package Runner npx. Als Parameter gebe ich das gewünschte Tag-Format mit. Der Default ist ein Präfix v im Tag, auf das ich gerne verzichte. ${version} ist die Versionsvariable, die für Semantic Release genau einmal im TagFormat-Parameter vorkommen muss. Aus der GitLab CI heraus muss ich das Dollar-Zeichen mit einem Backslash escapen. Der cat-Befehl liest das CI Secret SEM_RELEASE_OPTIONS ein und erstellt daraus die Konfigurationsdatei .releaserc.yml.

Das ist auch schon alles, damit Semantic Release die Git-Commits analysieren und ein neues Git-Tag pushen kann.

In einem zweiten Schritt in der GitLab CI wird aus dem Tag heraus die Software gebaut:

publish:
  stage: release
  script:
    - ./gradlew -Pversion=$CI_COMMIT_REF_NAME jar
  artifacts:
    paths:
      - build/libs/*
    expire_in: 1 week
  only:
    - tags

Die GitLab-Variable CI_COMMIT_REF_NAME entspricht dem Tag-Namen, da dieser Schritt genau dann ausgeführt wird, wenn der Trigger ein Tag ist (only: tags). Die Version wird als Parameter an den Gradle-Wrapper übergeben, der die Version automatisch in den Dateinamen setzt. Damit ich auf die Version zur Laufzeit zugreifen kann, weise ich Gradle in der Datei build.gradle meines Projektes an, die Version ins Manifest zu schreiben:

jar {
    manifest {
        attributes "Main-Class": "xxx",
       'Implementation-Version': version
    }

    from {
        configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

Zur Laufzeit meines Programmes habe ich nun über folgenden Aufruf Zugriff auf die gesetzte Version:

getClass().getPackage().getImplementationVersion();

Wie man aus meiner Beschreibung ableiten kann, lässt sich Semantic Release auch dazu verwenden, um die weiter oben im Artikel beschriebenen Pre-Release-Versionen zu bauen. Dazu würde ich einen dedizierten protected Branch anlegen und für den Code der Alpha-Versionen verwenden. Diesen Branch nehme ich dann in die .releaserc.yml auf und ändere den TagFormat-Parameter entsprechend ab, z.B. auf: \${version}-alpha.

- Semantic Release auf GitHub
- Angular Git Commit Guidelines