Automatisierung des Release-Prozesses mit Commitizen und Semantic-release
Automatisierte Releases mit Semantic Release und Commitizen. So spart man Zeit und erhöht die Codequalität seiner Projekte.
Inhaltsverzeichnis
Einleitung
In der Welt der Softwareentwicklung existiert der Grundsatz, dass alles automatisiert werden sollte, was möglich ist. Selbst wenn man alleine an einem Projekt arbeitet und verschiedene Features entwickelt, muss man jedes Mal nicht nur die Funktion testen, sondern sollte auch einen Changelog erstellen und eine Versionssystematik festlegen. Manuell kann dieser Vorgang viel Zeit in Anspruch nehmen. Doch mit automatischen Releases passiert das alles automatisch.
Automatische Releases spielen eine bedeutende Rolle im Software-Entwicklungsprozess und bieten zahlreiche Vorteile. Zunächst ermöglicht die Automatisierung des Release-Prozesses eine schnellere und effizientere Bereitstellung von Softwareupdates und -verbesserungen. Darüber hinaus verbessern automatische Releases die Zusammenarbeit und Kommunikation innerhalb eines Entwicklungsteams.
In diesem Artikel zeige ich, wie man den Release-Prozess in einem Node.js Repository automatisiert. Dazu binde ich Commitizen und Semantic-release in ein Next.js Projekt ein, welches auf Github gehostet wird.
Beide Tools arbeiten Hand in Hand, um den Software-Release-Prozess zu rationalisieren und Entwicklern Zeit und Aufwand zu ersparen.
Vorteile von Semantic Release und Commitizen
Semantic Release und Commitizen bieten in Kombination eine Reihe von Vorteilen, die den Release-Prozess optimieren und Entwicklern Zeit und Aufwand ersparen:
- Automatisierte Versionierung: Semantic Release analysiert die Commit-Nachrichten und erhöht automatisch die Versionsnummer basierend auf den Änderungen. Dadurch wird sichergestellt, dass die Versionierung konsistent und regelbasiert erfolgt.
- Standardisierte Commit-Nachrichten: Commitizen führt Entwickler durch den Commit-Prozess und erzeugt strukturierte Commit-Nachrichten nach der Conventional Commits Spezifikation. Das verbessert die Lesbarkeit und erleichtert das Verständnis der Änderungen.
- Automatische Changelogs: Semantic Release generiert automatisch Changelogs aus den Commit-Nachrichten. Dadurch haben Entwickler und Nutzer einen schnellen Überblick über die Änderungen in jeder Version.
- Effizientere Zusammenarbeit: Durch standardisierte Commit-Nachrichten und automatisierte Releases wird die Kommunikation im Team verbessert. Jeder weiß genau, welche Änderungen in welcher Version enthalten sind.
- Zeitersparnis: Die Automatisierung von Versionierung, Changelogs und Releases spart wertvolle Zeit, die sonst für manuelle Schritte aufgewendet werden müsste. Entwickler können sich so auf ihre Kernaufgaben konzentrieren.
Semantic Release und Commitizen arbeiten Hand in Hand, um den Software-Release-Prozess zu rationalisieren. Die Kombination dieser Tools ermöglicht es Entwicklern, sich auf das Wesentliche zu konzentrieren und dennoch professionelle, regelkonforme Releases zu erstellen.
Was ist Semantic Release?
Semantic Release automatisiert den Release-Prozess. Es analysiert Git-Commits, um basierend auf den Commit-Nachrichten die neue Versionsnummer zu bestimmen und automatisch einen Release zu erzeugen. Dabei richtet es sich nach dem Semantic Versioning (SemVer), einem branchenweiten Standard, der sicherstellt, dass Versionsnummern und Änderungen auf sinnvolle Weise dargestellt werden. Semantic Release ist ein Open-Source-Tool, das in Node.js geschrieben ist und mit jedem Git-Repository funktioniert. Es kann Releases für verschiedene Arten von Software generieren, darunter Bibliotheken, Frameworks, Plugins, Apps und mehr. Zudem ist es kompatibel mit einer Vielzahl von CI/CD-Tools wie Github Actions, Travis CI, CircleCI, Jenkins und GitLab CI.
Semantic Versioning
Vor über zehn Jahren führte Tom Preston-Werner, Mitbegründer von Github, das Konzept der semantischen Versionierung ein.
Die Versionsnummer einer Software besteht aus drei Teilen: Major, Minor und Patch. Diese Teile sind durch Punkte getrennt. Hier ist ein Beispiel:
{
"name": "mein-projekt",
"version": "1.0.0"
}
Wenn ein neues Feature hinzugefügt wird, erhöht sich die Minor-Version (1.1.0). Wenn ein Fehler behoben wird, erhöht sich die Patch-Version (1.0.1). Wenn die Major-Version erhöht wird (2.0.0), bedeutet dies, dass die Software nicht mehr abwärtskompatibel ist.
Beispiel:
- 1.0.0: Erste Version
- 1.0.1: Fehlerbehebung
- 1.1.0: Neues Feature
- 2.0.0: Änderung, die bestehende Funktionalität beeinträchtigt
In seinem Blogpost im Jahr 2022 argumentierte Tom Preston-Werner, dass Entwickler oft zögern, die Hauptversionsnummer zu erhöhen, selbst wenn es bedeutende Änderungen gibt. Er betonte, dass die Hauptversionsnummer für jede Änderung verwendet werden sollte, die die Abwärtskompatibilität bricht und nicht für Marketingzwecke missbraucht werden sollte.
Was ist Commitizen?
Commitizen ist ein Open-Source-Tool, das entwickelt wurde, um einheitliche Git-Commit-Nachrichten zu erstellen. Es bietet Entwicklern eine standardisierte Struktur und Syntax für Commit-Nachrichten, indem es sie durch den gesamten Commit-Prozess führt. Die Verwendung der “Conventional Commits”-Spezifikation ermöglicht eine einfachere Verwaltung und Navigation durch die Commit-Historie, da Konsistenz und Übersichtlichkeit gewährleistet werden.
Syntax der Commit-Nachrichten
Die Syntax der Commit-Nachrichten ist wie folgt aufgebaut:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Die einzelnen Teile sind:
- type: Kategorie des Commits
- scope: Bereich des Commits (optional)
- description: Kurze Beschreibung des Commits
- body: Ausführlichere Beschreibung des Commits (optional)
- footer: Fußzeile des Commits (optional)
Beispiele
Beispiel für einen Commit zur Behebung eines Fehlers:
fix: resolve parsing error when uploading files
Beispiel für einen Commit zur Einführung einer neuen Funktion:
feat: add darkmode
Beispiel für einen Commit mit einer Breaking Change:
feat: replace obsolote API with new implementation
BREAKING CHANGE: replaced `getUserData` method with `fetchUserData` to allow more efficient data retrieval.
Beispiel für einen Commit mit einem optionalen Bereich (scope):
feat(api): implement endpoint to retrieve user profiles
Beispiel für einen Commit mit einer Fußzeile:
chore: update dependencies
Update dependencies to fix security vulnerabilities
closes #123
Mehr Informationen zu Conventional Commits und den genauen Regeln gibt es hier.
Integration in einem Next.js Projekt
Nun möchte ich diese beiden Tools in einem Next.js Projekt integrieren. Dafür erstelle ich ein neues Next.js Projekt und lege ein neues Github Repository an. Anschließend installiere ich die Dependencies und konfiguriere die Tools. Zum Schluss erstelle ich noch eine Github Action, die die Veröffentlichung automatisch durchführt.
Repository anlegen
Der erste Schritt besteht darin, ein leeres Repository in Github anzulegen. Dazu wie folgt vorgehen:
- Github besuchen und anmelden.
- “+”-Button in der oberen rechten Ecke anklicken und “New repository” auswählen.
- Einen Namen für das Repository eingeben und optional eine Beschreibung einfügen.
- Auswählen, ob das Repository öffentlich oder privat sein soll.
- Auf “Create repository” klicken, um das leere Repository zu erstellen.
Nachdem das Repository erfolgreich erstellt ist, können wir mit der Einrichtung des Next.js-Projekts fortfahren.
Next.js Projekt erstellen
Um ein neues Next.js-Projekt zu erstellen, können folgende Schritte ausgeführt werden:
- Terminal öffnen und zum Verzeichnis wechseln, in dem das Projekt erstellt werden soll.
- Folgenden Befehl ausführen, um ein neues Next.js-Projekt zu erstellen:
npx create-next-app next-semantic-release
Repository als Remote hinzufügen
Danach navigiere ich in das Projektverzeichnis in einem Terminal und wechsle meine Branch auf main
.
git branch -M main
Jetzt füge ich das Github Repository als Remote hinzu.
git remote add origin https://github.com/mein-benutzername/mein-repository.git
In VS Code sollte es nun so aussehen:
Unten sehe ich “main” als aktuelle Branch und als Remote meine Github Repository.
Jetzt kann ich den Code in Github pushen.
git push -u origin main
Jetzt sollte ich in Github den Code sehen können.
Dependencies installieren
Als Nächstes installiere ich die Dependencies für Semantic Release und Commitizen.
npm install --save-dev semantic-release @semantic-release/changelog @semantic-release/git @commitlint/cli @commitlint/config-conventional @commitlint/cz-commitlint commitizen cz-conventional-changelog
Dieser Befehl wird die benötigten Pakete herunterladen und in der package.json
Datei speichern. Hier eine krurze Erklärung der einzelnen Pakete:
- semantic-release: Dieses Paket ermöglicht automatische Releases basierend auf den Commit-Nachrichten.
- @semantic-release/changelog: Dieses Paket generiert automatisch ein Changelog für jede Veröffentlichung.
- @semantic-release/git: Dieses Paket ermöglicht das automatische Hinzufügen von Änderungen zum Git-Repository während des Release-Prozesses.
- @commitlint/cli: Dieses Paket stellt eine CLI für das Validieren von Commit-Nachrichten gemäß den Commitlint-Regeln bereit.
- @commitlint/config-conventional: Dieses Paket enthält die konventionellen Commitlint-Regeln für die Validierung von Commit-Nachrichten.
- @commitlint/cz-commitlint: Dieses Paket integriert Commitlint mit Commitizen, um die Validierung von Commit-Nachrichten während des Commit-Prozesses zu ermöglichen.
- commitizen: Dieses Paket bietet eine interaktive Benutzeroberfläche für das Erstellen von standardisierten Commit-Nachrichten.
- cz-conventional-changelog: Dieses Paket stellt die konventionellen Commitizen-Vorlagen bereit, die den Commit-Nachrichten eine standardisierte Struktur geben.
Package.json anpassen
In der package.json
füge ich folgende Zeilen hinzu und ändere einige Werte:
- Unter “version” ändere ich den Wert von “0.1.0” auf “0.0.0-development”.
- Unter “scripts” füge ich zwei weitere Skripte hinzu: “semantic-release”, um Semantic Release auszuführen, und “commit”, um Commitizen auszuführen.
- Im “config” Abschnitt gebe ich den Pfad zu Commitizen an.
- Schließlich füge ich ein “release” Objekt hinzu, in dem die Plugins für Semantic Release angegeben werden. Es ist wichtig, die Branches anzugeben, für die Veröffentlichungen durchgeführt werden sollen. In meinem Fall ist das der
main
-Branch. Außerdem gebe ich an, dass die Veröffentlichungen nicht auf npm veröffentlicht werden sollen. Das mache ich, weil ich das Projekt nicht als npm-Paket veröffentlichen möchte.
Insgesamt sieht die package.json
dann ungefähr so aus:
{
"name": "next-semantic-release",
"version": "0.0.0-development",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"semantic-release": "semantic-release",
"commit": "cz"
},
"dependencies": {
//[...]
},
"devDependencies": {
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@commitlint/cz-commitlint": "^17.7.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"semantic-release": "^21.1.1"
},
"repository": {
"type": "git",
"url": "https://meine-repository-url.git"
},
"publishConfig": {
"access": "restricted"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"release": {
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
[
"@semantic-release/npm",
{
"npmPublish": false
}
],
"@semantic-release/git",
"@semantic-release/github"
]
}
}
Commitizen konfigurieren
Bei der Konfiguration von Commitizen gibt es verschiedene Möglichkeiten und Erweiterungen. Eine gängige Konvention für Commitizen sind die sogenannten “konventionellen Commits”. Dabei wird der Commit in verschiedene Kategorien unterteilt. Hier sind einige der gängigen Kategorien:
- fix: Bugfix
- feat: Neue Funktion
- BREAKING CHANGE: Änderung, die die bestehende Funktionalität bricht
- docs: Änderungen in der Dokumentation
- style: Änderungen, die keinen Einfluss auf den Code haben (z.B. Leerzeichen, Formatierung, Semikolons)
- refactor: Code-Änderungen, die weder einen Bug beheben noch eine Funktion hinzufügen
- perf: Code-Änderungen, die die Performance verbessern
- test: Hinzufügen oder Ändern von Tests
- build: Änderungen, die das Build-System oder externe Abhängigkeiten betreffen
- ci: Änderungen an der CI-Konfiguration oder Skripten
- chore: Sonstige Änderungen, die nicht in die anderen Kategorien passen
Manchmal ist es sinnvoll, die Kategorien zu erweitern. Zum Beispiel könnte die Kategorie deps
für Updates der node_modules
genutzt werden, wenn man diese nicht in build oder chore einsortieren möchte. Wenn ich Webseiten erstelle und der Inhalt teilweise oder vollständig im Repository liegt, hätte ich gerne eine eigene Kategorie für content. Die Kategorie revert
ist für das Rückgängigmachen von Commits gedacht.
Derzeit (Version 3.3.0) gibt es in Commitizen keine eingebaute Möglichkeit, neue Kategorien hinzuzufügen, ohne die vorhandenen zu überschreiben. Legt man Types
fest, werden die ursprünglichen Kategorien überschrieben. Ein offener Pull Request zu diesem Thema wurde bisher nicht übernommen.
In dieser Situation gibt es zwei Möglichkeiten. Man kann bei den vorhandenen Kategorien bleiben und den scope
-Parameter verwenden, um Änderungen genauer zu kategorisieren (z. B. chore(content): aktualisiere die Inhalte
) oder man kann die Kategorien, die man nicht verlieren möchte, noch einmal auflisten:
package.json:
{
//[...]
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog",
"types": {
"feat": {
"description": "A new feature",
"title": "Features"
},
"fix": {
"description": "A bug fix",
"title": "Bug Fixes"
},
"docs": {
"description": "Documentation only changes",
"title": "Documentation"
},
"style": {
"description": "Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)",
"title": "Styles"
},
"refactor": {
"description": "A code change that neither fixes a bug nor adds a feature",
"title": "Code Refactoring"
},
"perf": {
"description": "A code change that improves performance",
"title": "Performance Improvements"
},
"test": {
"description": "Adding missing tests or correcting existing tests",
"title": "Tests"
},
"build": {
"description": "Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)",
"title": "Builds"
},
"ci": {
"description": "Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)",
"title": "Continuous Integrations"
},
"deps": {
"description": "Updates to dependencies",
"title": "Dependencies"
},
"content": {
"description": "Changes that affect the content of the site",
"title": "Content"
},
"chore": {
"description": "Other changes that don't modify src or test files",
"title": "Chores"
},
"revert": {
"description": "Reverts a previous commit",
"title": "Reverts"
}
}
}
}
//[...]
}
Wenn ich nun npm run commit
ausführe, wird mir die Auswahl der Kategorien angezeigt.
Semantic Release konfigurieren
Auch für Semantic Release gibt es einige Einstellmöglichkeiten. Interessant sind hier insbesondere die Branches und das Plugin Commit Analyzer.
Branches
In der package.json
habe ich bereits die Branches angegeben, die veröffentlicht werden sollen. Hier kann ich auch angeben, dass nur bestimmte Branches veröffentlicht werden sollen. In meinem Fall ist das der main
Branch.
{
//[...]
"release": {
"branches": ["main"]
//[...]
}
//[...]
}
Man kann natürlich auch mehrere Branches für eine Veröffentlichung angeben.
{
//[...]
"release": {
"branches": [
"+([0-9])?(.{+([0-9]),x}).x",
"main",
"next",
"next-major",
{ "name": "beta", "prerelease": true },
{ "name": "alpha", "prerelease": true }
]
//[...]
}
//[...]
}
Hier werden alle Branches veröffentlicht, die mit einer Zahl beginnen. Also z. B. 1.0.0
, 2.0.0
usw. Außerdem werden die Branches main
, next
, next-major
, beta
und alpha
veröffentlicht. Die Branches beta
und alpha
werden als Prerelease veröffentlicht.
Commit Analyzer
Das Plugin “Commit Analyzer” in Semantic Release analysiert die Commit-Nachrichten, um die nächste Version zu ermitteln. Dabei werden die Kategorien der Commits berücksichtigt. Je nach Kategorie wird die Versionsnummer entsprechend erhöht:
- Wenn ein Commit mit der Kategorie “fix” erstellt wird, erhöht sich die Patch-Version. Dies deutet auf einen Commit hin, der einen Fehler behebt, ohne dabei neue Funktionen einzuführen oder bestehende Funktionalitäten zu brechen.
- Wenn ein Commit mit der Kategorie “feat” erstellt wird, erhöht sich die Minor-Version. Dies bedeutet, dass eine neue Funktion hinzugefügt wurde, ohne dass dabei bestehende Funktionalitäten beeinflusst oder gebrochen wurden.
- Wenn ein Commit den Zusatz “BREAKING CHANGE” enthält, erhöht sich die Major-Version. Dies weist darauf hin, dass eine Änderung vorgenommen wurde, die bestehende Funktionalitäten bricht und eventuell Anpassungen erfordert.
Dieses Verhalten kann angepasst werden:
{
//[...]
"release": {
"branches": ["main"],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"releaseRules": [
{ "type": "deps", "release": "patch" },
{ "scope": "no-release", "release": false }
]
}
]
//[...]
]
}
//[...]
}
An dieser Stelle wird zusätzlich die Patch-Version erhöht, wenn die Kategorie deps
verwendet wird. Außerdem wird die Version nicht erhöht, wenn bei Scope no-release
angegeben wird (z. B. fix(no-release): ...
). Jedoch kann dieses Verhalten zu einem unerwünschten Ergebnis führen. Wenn man ein “breaking change” mit der Kategorie “deps” veröffentlicht, wird das als “Patch-Release” behandelt. Möchte man auch hier ein Major-Release, muss man noch eine Zeile zur Konfiguration hinzufügen:
{
//[...]
"release": {
"branches": ["main"],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"releaseRules": [
{ "breaking": true, "release": "major"}
{ "type": "deps", "release": "patch" },
{ "scope": "no-release", "release": false }
]
}
]
//[...]
]
}
//[...]
}
Github Action erstellen
Als Letztes muss ich nur noch dafür sorgen, dass Github die Veröffentlichung automatisch durchführt.
Zunächst gehe ich in die Einstellungen meines Repositorys und wähle den Punkt “Actions” und dann “General” aus. Hier ändere ich die “Workflow permissions” auf “Read and write permissions”.
Als Nächstes erstelle ich eine neue Datei .github/workflows/release.yml
und füge folgenden Inhalt ein:
name: Release
on:
push:
branches:
- main
permissions:
contents: read
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm clean-install
- name: Build Next app
run: npm run build
- name: Semantic Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
Die Veröffentlichung wird nur für den main
Branch ausgeführt. Zudem wird die Github Action nur ausgeführt, wenn der Code in Github gepusht wird. Wenn ich lokal einen Commit erstelle und pushe, wird die Github Action nicht ausgeführt. Nur, wenn der Build-Prozess erfolgreich ist, wird Semantic Release ausgeführt.
Test
Nun wird es Zeit für den ersten Commit.
git add .
npm run commit
Ich wähle die Kategorie “feat” aus und gebe eine kurze Beschreibung ein.
git push
# oder genauer
git push -u origin main
Jetzt sehe ich in Github, dass mein Repository drei Commits enthält. Außerdem wurde ein Release erstellt und CHANGELOG.md
-Dokument generiert.
Möchte man semantic-release lokal verwenden, kann man das mit dem Befehl npm run semantic-release
. Hierbei ist zu beachten, dass ein GITHUB_TOKEN erstellt und in der .env
Datei gespeichert werden muss. Dieser Token muss dann auch als Secret in Github hinterlegt werden.
Es ist wichtig darauf zu achten, dass der Token nicht versehentlich veröffentlicht wird. In meinem Fall habe ich den Token in der .env
Datei hinterlegt und diese Datei in der .gitignore
Datei eingetragen. So wird die Datei nicht in Github veröffentlicht.
Zusammenfassung der Vorteile von automatischen Releases
Die automatische Generierung von Releases mithilfe von Tools wie Commitizen und Semantic-release bietet eine Vielzahl von Vorteilen.
-
Automatisierter Prozess: Durch die Verwendung von Commitizen und Semantic-release wird der gesamte Prozess der Release-Erstellung automatisiert. Neue Versionen werden basierend auf den definierten Regeln direkt aus dem Code-Repository generiert.
-
Klare und konsistente Commit-Meldungen: Commitizen stellt sicher, dass die Commit-Nachrichten klar, konsistent und gemäß den Conventional Commits-Spezifikationen strukturiert sind. Dadurch wird die Verwaltung von Softwareversionen vereinfacht und Änderungen lassen sich leichter nachverfolgen.
-
Automatische Versionsinkrementierung: Semantic-release analysiert die Commit-Meldungen und erhöht die Versionsnummern automatisch entsprechend der Art der Änderungen. Dadurch wird sichergestellt, dass die Versionsnummern korrekt und konsistent sind.
-
Generierung von Änderungsprotokollen: Semantic-release erstellt automatisch Änderungsprotokolle (Changelogs) basierend auf den Commit-Meldungen. Dadurch können Entwickler und Benutzer die durchgeführten Änderungen leichter nachvollziehen.
-
Verbesserte Softwarequalität: Durch die Automatisierung des Release-Prozesses wird die Bereitstellung neuer Funktionen beschleunigt und gleichzeitig sichergestellt, dass der Betrieb reibungslos läuft. Dies trägt zur Verbesserung der Softwarequalität bei und minimiert mögliche Fehler, die bei manuellen Prozessen auftreten könnten.
Insgesamt bieten Commitizen und Semantic-release einen strukturierten, zuverlässigen und effizienten Ansatz zur Verwaltung von Software-Releases. Sie ermöglichen eine präzisere Versionsverwaltung, beschleunigen den Entwicklungsprozess und tragen zur Steigerung der Softwarequalität bei.
Bei Anmerkungen kann gerne ein Kommentar hinterlassen werden. Ich freue mich über Feedback.