GIT – Semantische Versionierung und Conventional Commits

GIT – Semantische Versionierung und Conventional Commits
Logos von Node.js, GitLab und semantic-release

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

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

Terminal mit git log: Commit-Messages wie «typo», «updates» und «fix mistake», die kaum etwas über die Änderung aussagen
Bild: Bad Practice in der GIT-Versionshistorie

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.

GitHub-Release-Seite von semantic-release mit Version v18.0.1, einem Bugfix-Eintrag und den Source-Code-Assets
Bild: Beispiel für ein Semantic Release im GIT-Repository «semantic-release/semantic-release»
Versionsnummer 1.4.3, aufgeteilt in Major Release (1), Minor Release (4) und Patch Release (3)
Bild: Format einer semantischen Versionsnummer

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
Terminal mit der Ausgabe von git log seit v1.7.45: «feat!: upgrade to node 17» und «fix: fix the bug with logfile generation»
Bild: Änderungen bzw. Commits im GIT-Repository seit dem letzten Release

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

Terminal mit git log --oneline des Repositories semantic-release: jede Commit-Message beginnt mit einem Typ wie docs:, chore(deps): oder fix:
Bild: GIT-Versionshistorie im Format «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».

GitLab-Einstellungen CI/CD: im Abschnitt Variables ist die Variable GL_TOKEN hinterlegt
Bild: Token für den Zugriff auf das GitLab-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
CHANGELOG.md in GitLab mit den Versionen 1.2.5 bis 1.2.0, jeweils mit einem Abschnitt Bug Fixes, letzter Commit vom semantic-release-bot
Bild: CHANGELOG.md
GitLab-Release v1.2.5 mit Source-Code-Assets und dem Bugfix «fix bug in semantic release»
Bild: Release im GitLab-Repository

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
Bild: Automatische Anpassung der Versionsnummer und GitLab-Release durch den semantic-release-bot