GIT – Semantische Versionierung und Conventional Commits


GIT ist ein Open-Source-Werkzeug zur Versionsverwaltung und gehört heute in den Werkzeugkasten jeder Informatikerin und jedes Informatikers. In diesem Beitrag geht es um die automatische Versionierung von Software-Projekten.
Stellen Sie sich vor, Sie erstellen einen Git-Commit mit der Nachricht «fix: fix issue in login form».
Ihre CI/CD-Pipeline erkennt automatisch, dass es sich um einen Bugfix handelt («fix:»), erhöht deshalb die Softwareversion von 1.3.5 auf 1.3.6 und erstellt im GIT-Repository das passende Release. Im Changelog entsteht automatisch ein Eintrag. Alles ist korrekt versioniert. Um genau diesen Automatismus geht es heute.
opensight.ch – roman hüsler
Wenn Sie eine allgemeine GIT-Schulung suchen, kann ich Ihnen den Online-Kurs von Mosh Hamedani empfehlen. Dieser Beitrag erklärt nicht die Bedienung des Werkzeugs GIT, sondern Best Practices und Gestaltung rund um Commit-Messages und Versions-Tags in einem GIT-Repository.
Ein grosser Teil der Community setzt bei der Versionierung auf «Conventional Commits» und «Semantic Releases». Daraus ergeben sich folgende Vorteile, auf die wir gleich genauer eingehen:
- Automatisch erzeugte CHANGELOGs
- Automatisch berechnete nächste semantische Versionsnummer
(anhand des Commit-Typs «fix», «feat», «chore») - Teammitglieder erkennen die Art der Änderung auf einen Blick («fix», «feat», «docs» usw.)
- Automatisch ausgelöste Builds und bessere Integration in CI/CD-Pipelines
- Eine verständlichere Versionshistorie und damit einfachere Zusammenarbeit
Inhalt
- Beispiel: schlechte Versionshistorie
- Wann erstellt man einen Commit?
- Semantic Releases
- Conventional Commits
- Versionierung in CI/CD-Pipelines
Beispiel: schlechte Versionshistorie
Ein Blick auf meine älteren GIT-Repositories und Commits hat mich dazu gebracht, mich genauer mit Standards und Best Practices zu befassen. Eine nachvollziehbare Versionshistorie mit verständlichen Commit-Messages hilft beim Verstehen und Nachverfolgen von Änderungen und erleichtert die Zusammenarbeit mit anderen Entwicklerinnen und Entwicklern.
opensight.ch – roman hüsler

Um zu zeigen, worum es in diesem Beitrag geht, hier ein Beispiel für eine schlechte GIT-Versionshistorie. Die Commit-Messages verraten kaum, was sich jeweils geändert hat.
Wann erstellt man einen Commit?
Wann sollte man überhaupt einen Commit erstellen?
Ein Commit gehört immer dann erstellt, wenn eine Arbeitseinheit abgeschlossen ist, zum Beispiel ein neues Feature integriert, die Dokumentation aktualisiert oder ein Bug behoben. Ausserdem sollten verschiedene Änderungen, die nicht direkt zusammenhängen, nicht in einem Commit gebündelt werden.
Im Bild oben sehen Sie zwei Commit-Messages:
- added google maps feature
- bugfix in google maps feature
Diese beiden Aktionen gehören zur selben Arbeitseinheit und sollten deshalb zu einem Commit zusammengefasst (gesquasht) werden.
Semantic Releases
# Creation of an annotated tag
git tag -a v2.0.0 -m "Release v2.0.0 (2021-12-12)"
Mit «Annotated Tags» markieren Sie Commits in GIT und versehen sie mit einer Nachricht. Ein Semantic Release besteht aus einem Annotated Tag, dessen Format nach der Semantic-Versioning-Spezifikation festgelegt ist. Die Nachricht führt meist auch alle Änderungen seit dem letzten Release auf.


Eine semantische Version besteht aus mindestens drei Teilen:
-
Major Release
Wird ein «Breaking Change» eingeführt, muss die Major-Nummer erhöht werden. -
Minor Release
Neue Features, die abwärtskompatibel sind (keine «Breaking Changes»). -
Patch Release
Bugfixes (keine «Breaking Changes»).
Erstellen wir als Beispiel ein Semantic Release: Wir sind aktuell auf Version «1.7.45» und haben «Breaking Changes» eingebaut. Für die nächste Version brauchen wir also ein neues Major Release (2.0.0).
Zuerst erzeugen wir eine Liste aller Commits seit dem letzten Release:
git log --pretty=format:%s v1.7.45...HEAD --no-merges

Diese Liste der Änderungen seit dem letzten Release (Bild oben) kopieren wir und erstellen damit das neue Release.
git tag -a v2.0.0
Alle Änderungen halten wir in der Tag-Message fest. Zusätzlich tragen wir dieselben Änderungen in die Datei CHANGELOG.md ein.
Release v2.0.0 (2021-12-12)
- feat!: upgrade to node 17
- fix: fix the bug with logfile generation
Alles, was wir hier von Hand erledigt haben – die nächste Versionsnummer berechnen, den Annotated Tag formatieren, das Changelog nachführen –, lässt sich mit semantic-release auch automatisch erledigen (automatische Versionierung). Mehr dazu im Kapitel «Versionierung in CI/CD-Pipelines».
Conventional Commits

Damit die GIT-Versionshistorie verständlicher wird, hat sich die Community auf einen Standard geeinigt («Conventional Commits»), den man heute in vielen Repositories antrifft. Automatische Versionierung wird möglich, weil sich die Art der Änderung (Patch, Minor Release, Major Release) direkt aus der Commit-Message ablesen lässt.
Das Format eines Conventional Commit beschreiben wir hier nur kurz. Die Details finden Sie auf der Website (www.conventionalcommits.org).
Format
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Beispiel
feat(api)!: send confirmation email to the customer
BREAKING CHANGE: send an email to customer after order
Reviewed-by: Z
Refs: #123
Header
Der Header nennt zuerst die Art der Änderung: fix, feat, chore, docs, refactor, perf.
Dazu kommt eine kurze Beschreibung der Änderung.
Message Body
Weil es sich hier um einen «Breaking Change» handelt, ist ein «!» angehängt. Zusätzlich steht in der Nachricht «BREAKING CHANGE». Daran erkennt der Semantic-Versioning-Automatismus später, dass es sich um ein Major Release handelt.
Versionierung in CI/CD-Pipelines
Nun setzen wir die automatische Versionierung für ein Node.js-Projekt in einer CI/CD-Pipeline auf GitLab um. Die Idee: Wir pushen einen Commit (Conventional Commit) ins GitLab-Repository, und dieser startet die CI/CD-Pipeline. In der Pipeline wird die nächste Versionsnummer der Software berechnet. Einige Dateien werden angepasst (package.json, CHANGELOG.md) und vom Bot automatisch ins GIT-Repository zurückgeschrieben.
Zuerst legen wir ein NPM-Projekt an und installieren einige Abhängigkeiten:
npm init --yes
npm install --save-dev @semantic-release/changelog
npm install --save-dev @semantic-release/commit-analyzer
npm install --save-dev @semantic-release/exec
npm install --save-dev @semantic-release/git
npm install --save-dev @semantic-release/gitlab
npm install --save-dev @semantic-release/npm
npm install --save-dev @semantic-release/release-notes-generator
Dann ergänzen wir die package.json um folgende Zeilen:
"private": true,
"release": {
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
[
"@semantic-release/exec",
{
"prepareCmd": "sed 's/\"app_version\".*/\"app_version\":\"${nextRelease.version}\"/' src/configuration.json > configuration.json"
}
],
[
"@semantic-release/git",
{
"assets": [
"package.json",
"CHANGELOG.md",
"configuration.json"
],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
"@semantic-release/gitlab"
]
},
"branches": [
"master"
]
Diese Zeilen legen fest, was passiert, wenn später das Semantic-Versioning-Tool («npx semantic-release») läuft.
Eine kurze Erklärung aller verwendeten Plugins:
- commit-analyzer
Der Commit Analyzer wertet unsere Commit-Message aus. Sie sollte der Spezifikation «Conventional Commits» entsprechen. Anhand der Nachricht entscheidet das Tool, ob die nächste Version ein Patch, ein Minor Release oder ein Major Release wird. - release-notes-generator
Der Release Notes Generator sammelt alle Änderungen seit dem letzten Release (Commit-Log). - changelog
Schreibt die Release Notes (aus dem release-notes-generator) in die Datei CHANGELOG.md. - npm
Schreibt die nächste Versionsnummer in die package.json des NPM-Projekts. - exec
Führt beliebige Befehle aus. Im Beispiel oben schreiben wir die nächste Versionsnummer mit «sed» in die Datei «configuration.json». - git
Mit dem GIT-Plugin werden die Änderungen ins GIT-Repository committet. Wir committen also die angepasste package.json, das erzeugte CHANGELOG.md und die eben erstellte configuration.json. - gitlab
Das GitLab-Plugin pusht schliesslich alle Änderungen ins GitLab-Repository.
Hinweis zum GitLab-Plugin
Damit das GitLab-Plugin funktioniert, muss das Access Token im GitLab-Repository hinterlegt sein. Das Token braucht die Berechtigungen «api» und «write_repository».

Nun fügen wir dem Projekt die Datei «.gitlab-ci.yml» hinzu, also die Definition der GitLab-CI/CD-Pipeline.
stages:
- release
semantic_release:
image: node:latest
stage: release
only:
- master
script:
- npm i
- npx semantic-release
artifacts:
paths:
- configuration.json
Ändern und committen wir jetzt etwas im Git-Repository, läuft die CI/CD-Pipeline automatisch und erzeugt eine neue Versionsnummer.
git add testfile.txt
git commit -m "fix: fix bug in semantic release"
git push origin master


Nach dem Einchecken des Codes ist die CI/CD-Pipeline gelaufen, und der Bot hat die Dateien («CHANGELOG.md», «package.json», «configuration.json») automatisch angepasst.
![GitLab-Commit-Liste: auf den Commit «fix: fix bug in semantic release» folgt der Commit «chore(release): 1.2.5 [skip ci]» des semantic-release-bot](/content/images/2023/04/auto-release.png)