3. März 2023 von Paul Schüler
GitLab Pipelines für die automatisierte Veröffentlichung einer semantischen Version
Die Versionierung von Software ist ein praktisches Mittel, um den aktuellen Entwicklungsstand festzuhalten. Softwareversionen werden jedoch oft nur inkrementiert und bringen keinen Mehrwert. Da Versionierung in Regeln abgebildet werden kann, sollte dieser Prozess automatisiert werden. Semantische Versionierung gibt Softwareversionen eine Struktur, die allen Stakeholdern und dem Entwicklungsteam wichtige Informationen liefern kann.
Um diesen Blog-Beitrag verfolgen zu können, sollten Grundkenntnise in GitLab CI/CD und Docker vorhanden sein.
Die Hölle der Abhängigkeit
Software zu versionieren ist keine revolutionäre Idee. Angenommen, das Softwarepaket A wurde weiterentwickelt. Die Versionierung von Softwarepaket A erfolgt durch die Erhöhung einer fortlaufenden Nummer.
Softwarepaket A wird mit einer neuen Version veröffentlicht und Softwarepaket B aktualisiert blind seine Abhängigkeit von Softwarepaket A. Dabei kann es vorkommen, dass die Software von Paket B nicht mehr wie erwartet funktioniert. Die neue Version von Softwarepaket A enthält Änderungen, die die API grundlegend umstrukturiert haben.
Mit Hilfe einer semantischen Version wäre bei der Entwicklung von Softwarepaket B aufgefallen, dass die neue Version von Softwarepaket A wichtige Änderungen enthält.
Semantische Versionierung
Eine semantische Version wird auf Grundlage von MAJOR.MINOR.PATCH
gebildet. Die einzelnen Elemente werden wie folgt erhöht:
- 1. MAJOR wird erhöht, wenn sich die API grundlegend verändert hat.
- 2. MINOR wird erhöht, wenn neue Funktionalitäten eingebaut worden sind.
- 3. PATCH wird erhöht, wenn Änderungen nur Fehler an bestehenden Funktionalitäten beheben.
Diese Struktur ermöglicht es, die Nutzerinnen und Nutzer über Art und Umfang der Änderungen zu informieren. Wenn wir Softwareabhängigkeiten in einem unserer Projekte aktualisieren, müssen wir immer noch verantwortlich sein und überprüfen, ob die Softwarepakete wie beschrieben funktionieren.
Wir können die Ermittlung einer neuen Version eines Softwarepakets in einer CI-Pipeline automatisieren. Um diesen Prozess automatisieren zu können, benötigen wir eine Grundlage und ein festes Regelwerk, um festzustellen, was sich seit der letzten Version unserer Software geändert hat.
Git Commits und Commit Conventions
Um festzuhalten, welche Änderungen an der Software gemacht worden sind, benutzt man die Commit Messages von Git. Es gibt viele Strukturen, um die Commit Messages von Git zu strukturieren, ich verfolge dafür gerne die Angular Commit Conventions. Die Angular Commit Conventions definieren strikte Regeln, um die Commit Messages leserlicher zumachen.
Angular Commit Message Convention
Der Aufbau der Message ist einfach:
type(scope): body
Es gibt per Definition folgende Commit-Typen:
• build
• ci
• docs
• feat
• fix
• perf
• refactor
• style
• test
Der Scope ist optional und wird in Klammern geschrieben, dort können wir auf eine Ticketnummer aus unserem Projektmanagement-Tool verweisen. Im Body beschreiben wir kurz, was dieser Commit ändert. Das Besondere ist nun, dass wir aus den verschiedenen Typen ableiten können, wie sich die Version ändern wird. Zum Beispiel wird die folgende Commit-Meldung zu einer Erhöhung der Minor-Version führen:
feat(#TICKET_NUMMER): neues feature XY erarbeitet
GitLab-Pipelines zur Automatisierung der Erstellung einer semantischen Version
Pipelines sind ein wesentlicher Bestandteil der Continuous Integration. Eine Pipeline setzt sich aus Stages und Jobs zusammen. Eine Stage beschreibt, wann Jobs ausgeführt werden sollen, zum Beispiel bei einem Merge Request für automatische Tests oder nach einem Merge zum Main Branch.
In einer Stage können mehrere Jobs existieren. Ein Job enthält Code oder kann Skripte ausführen, z.B. können wir die Codequalität des Branches überprüfen. Jobs in einer Stage müssen erfolgreich sein, das heißt, sie müssen mit dem Exit-Code 0 enden, damit die Pipeline zur nächsten Stage übergehen oder erfolgreich beendet werden kann.
Stages und Jobs werden in einer YAML-Datei mit dem Namen .gitlab-ci.yaml
definiert.
Versioning-Stage
Um nun automatisch eine neue Version aus den Commit Messages zu erzeugen, legt man als erstes die .gitlab-ci.yaml-Datei
an, wenn sie noch nicht existiert.
Angelegt werden die Versioning-Stage
und einen Job für die Versioning-Stage
. Der Job benötigt als Basis ein Node.js Docker Image. Dafür muss der GitLab-Runner Zugang zur Docker Engine haben.
Um das einzurichten, verfolgt gerne dazu die GitLab-Dokumentation zum Docker-Executor .
Versioning-Stage und build-tag Job in .gitlab-ci.yaml
Als erstes erstellt ihr eine neue Stage und einen neuen Job, der die semantische Version erstellen lassen soll.
stages:
- versioning
Das stages
-Objekt kann eine Liste von Stages haben. Habt ihr die Stage angelegt, können nun Jobs für die Stage definiert werden.
Man legt einen neuen Job build-tag
an. Der Job wird wie folgt definiert und wird für uns eine neue Version erzeugen:
build-tag:
image: node:18.10-buster-slim
stage: versioning
before_script:
- apt get update && apt-get install -y --no-install-recommends git-core ca-certificates
- npm install -g semantic-release @semantic-release/gitlab @semantic-release/git
- echo "$RELEASE_RC" > .releaserc.json
script:
- semantic-release
rules:
- if: $CI_COMMIT_BRANCH == "main"
Im Job installieren wir das NPM-Paket Semantic-Release und installieren Git im Node.js Docker-Image. Semantic-Release nimmt uns die Arbeit ab und kümmert sich um die Bestimmung der nächsten Versionsnummer.
Der Vorteil ist, dass Semantic-Release menschliche Emotionen aus dem Versionierungsprozess herausnimmt und klaren Regeln folgt, den Regeln der semantischen Versionierung. Wir können jedoch festlegen, auf welche Commit Message Types das Paket reagieren soll, um eine neue Version zu definieren.
Um Regeln zu definieren, auf welche Typen Semantic-Release reagieren soll, muss eine .releaserc.json Datei
im aktuellen Ausführungspfad existieren. Hier sieht man einmal die Konfigurationsreferenz von Semantic-Release.
Meine Konfiguration ist in einer Umgebungsvariable $RELEASE_RC
gespeichert. Diese Variable wird in den CI/CD-Einstellungen des Repositories gespeichert.
Zusätzlich benötigt man eine Umgebungsvariable namens $GITLAB_TOKEN
. Die Variable $GITLAB_TOKEN
enthält ein Token für den Zugriff auf das Repository. Hier könnt ihr nachlesen, wie ihr einen Access Token erstellen könnt. Semantic-Release schaut nach dieser Variable, um einen Git Tag in eurem Repository zu veröffentlichen. Ein Git Tag ist eine Funktion, um Punkte in einer Versionshistorie als wichtig zu kennzeichnen.
Semantic-Release prüft zunächst, ob im Git-Repository Versions-Tags vorhanden sind. Wenn eine Version erzeugt wird, setzt Semantic-Release ein neues Git-Tag, das die Version enthält. Wird ein entsprechendes Versions-Tag gefunden, werden nur alle Commits bis zu diesem Tag untersucht. Für alle gefundenen Commits wird dann jeweils die Commit Message untersucht. Anhand der verwendeten Commit Message Types kann dann die Versionsnummer berechnet werden. Die neue semantische Version wird dann als Git Tag im Repository veröffentlicht.
Wird kein Tag gefunden, wird automatisch ein Git Tag mit der Version v1.0.0
erzeugt. Die initiale Version kann Semantic-Release in der Konfiguration übergeben werden.
Anwendungsfall: Bauen des Docker Image mit neuer Versionsnummer
Um ein Docker Image auf Basis unseres Codes zu erstellen, definieren wir eine neue Stage mit dem Namen deploy
und einen neuen Job mit dem Namen build_docker_image
. Der Job soll ausgeführt werden, sobald ein neues Git Tag im Repository erstellt wird und der Name des Tags eine semantische Versionsnummer ist.
Der Git Tag mit der semantischen Versionsnummer sollte im vorherigen Schritt erzeugt worden sein. Wurde kein neuer Git Tag erzeugt, so ist der Typ des Commit falsch oder die Struktur der Commit Message wurde nicht eingehalten.
Die neue Stage definiert man wie folgt:
stages:
- versioning
- deploy
Man hat das stages
-Objekt um eine weitere Stage deploy
ergänzt.
Der neue Job in der Deploy-Stage soll für uns diese Aufgabe übernehmen:
build_docker_image:
stage: deploy
variables:
IMAGE_TAG: '$CONTAINER_REGISTRY/tag-name:$CI_COMMIT_TAG'
script:
- echo $CONTAINER_REGISTRY_PASSWORD | docker login -u $CONTAINER_REGISTRY_USER $CONTAINER_REGISTRY --password-stdin
- docker build -t $IMAGE_TAG .
- docker push $IMAGE_TAG
rules:
- if: $CI_COMMIT_TAG && $CI_COMMIT_TAG =~ /^(v[0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?$/
Folgende Variablen haben wir wieder in den CI/CD-Einstellungen des Repository gespeichert:
• $CONTAINER_REGISTRY
(Domain eurer Docker Image Registry)
• $CONTAINER_REGISTRY_PASSWORD
(Passwort für die Registry)
• $CONTAINER_REGISTRY_USER
(Benutzername für die Registry)
Unser neuer Git Tag mit unserer neuen Versionsnummer ist in der GitLab Variable $CI_COMMIT_TAG
gespeichert. Besonders wichtig ist die Regel, wann der Job ausgeführt werden soll. Der Job darf nur ausgeführt werden, wenn die Variable $CI_COMMIT_TAG
einen Wert hat und dieser einer semantischen Version entspricht, also beispielsweise v1.2.5.
Mit diesem Git-Tag können wir nun während des Build-Prozesses unser neues Docker-Image markieren und in der Registry veröffentlichen.
Fazit
Durch eine definierte Commit-Message-Struktur können wir den Versionierungsprozess automatisieren. Außerdem wurde das Semantic-Release Tool verwendet, das für uns Git Tags aus den Commit Messages generiert. Mit dem neu veröffentlichten Git-Tag habe ich einen Anwendungsfall gezeigt, bei dem die neue semantische Version zum Bauen eines Docker-Images verwendet wurde.
Falls ihr Fragen zu dem Thema oder der Implementierung habt, könnt ihr mich gerne per E-Mail paul.schueler@adesso.de oder Teams kontaktieren.