Conventional Commits als Datengrundlage nutzen
Ein guter Changelog Generator liest die Commit-History aus, erkennt Typ und Umfang jeder Änderung und baut daraus ein strukturiertes CHANGELOG.md, ganz ohne manuelles Nachtragen. Dieser Artikel zeigt, wie ein solcher Changelog Generator als reines Bash-Skript entsteht, welche Fallstricke beim Parsen von Conventional Commits lauern und wie sich das Ergebnis sauber in eine Release-Pipeline einhängen lässt.
Inhaltsverzeichnis
- 1. Warum ein Changelog Generator sich lohnt
- 2. Conventional Commits als strukturierte Datenquelle
- 3. Git Log parsen: das Rohmaterial extrahieren
- 4. Commits nach Typ gruppieren
- 5. Versionsvorschlag aus Commit Typen ableiten
- 6. Markdown Ausgabe und bestehende Datei zusammenführen
- 7. Den Changelog Generator in CI/CD einbinden
- 8. Edge Cases und typische Fehlerquellen
- 9. Changelog Generator im Vergleich zu Alternativen
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein Changelog Generator sich lohnt
In vielen Projekten wird ein CHANGELOG.md manuell gepflegt, meist als letzter Schritt vor einem Release und meist unvollständig. Wer vergisst, den Eintrag zu ergänzen, sorgt dafür, dass Nutzer und Kollegen nicht nachvollziehen können, was sich zwischen zwei Versionen geändert hat. Ein automatischer Changelog Generator löst genau dieses Problem, indem er die Informationen dort abholt, wo sie ohnehin schon vorhanden sind: in der Git-Historie selbst.
Der Vorteil eines Changelog Generators auf Bash-Basis liegt in der Kontrolle. Statt ein Node-Paket mit Dutzenden Abhängigkeiten zu installieren, reicht ein einziges, lesbares Skript, das mit den ohnehin vorhandenen Bordmitteln git, awk und sed arbeitet. Für Teams, die viele kleine Microservices pflegen, ist das ein Skript, das überall funktioniert, ohne Runtime-Abhängigkeiten und ohne Versionskonflikte zwischen Projekten.
Damit ein Changelog Generator zuverlässig arbeitet, braucht er eine strukturierte Eingabe. Genau hier kommen Conventional Commits ins Spiel, ein Format, das Commit-Nachrichten in Typ, Umfang und Beschreibung aufteilt und damit maschinenlesbar macht. Die folgenden Abschnitte bauen Schritt für Schritt einen vollständigen Changelog Generator auf, von der Rohdatenextraktion bis zur fertigen Markdown-Datei.
2. Conventional Commits als strukturierte Datenquelle
Conventional Commits definieren ein festes Präfix für jede Commit-Nachricht, etwa feat:, fix:, docs:, refactor: oder chore:, optional gefolgt von einem Scope in Klammern wie feat(api):. Ein Changelog Generator nutzt genau dieses Präfix, um jede Zeile der Historie automatisch der richtigen Kategorie zuzuordnen, ohne dass jemand die Änderung nachträglich klassifizieren muss.
Wichtig ist ein Breaking-Change-Marker: entweder ein Ausrufezeichen direkt nach dem Typ, also feat!:, oder ein BREAKING CHANGE: Absatz im erweiterten Commit-Body. Ein sauberer Changelog Generator muss beide Varianten erkennen, denn Breaking Changes gehören immer in einen eigenen, prominent platzierten Abschnitt am Anfang des Changelogs, unabhängig von der restlichen Sortierung.
Teams, die neu auf Conventional Commits umsteigen, sollten die Konvention über einen Commit-Hook wie commit-msg mit einem regulären Ausdruck erzwingen. So bleibt garantiert, dass der Changelog Generator später keine unstrukturierten Nachrichten verarbeiten muss, die er nur schwer richtig einordnen kann.
#!/usr/bin/env bash
# commit-msg hook — enforces Conventional Commits format
set -euo pipefail
commit_msg_file="$1"
first_line="$(head -n 1 "$commit_msg_file")"
pattern='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\([a-z0-9_-]+\))?!?: .{1,72}$'
if [[ ! "$first_line" =~ $pattern ]]; then
echo "[ERROR] Commit message does not follow Conventional Commits:" >&2
echo " $first_line" >&2
echo " Expected: type(scope): description" >&2
echo " Allowed types: feat fix docs style refactor perf test build ci chore" >&2
exit 1
fi
3. Git Log parsen: das Rohmaterial extrahieren
Der erste funktionale Schritt jedes Changelog Generators ist ein sauberer git log Aufruf mit definierten Trennzeichen, damit die Ausgabe zeilenweise weiterverarbeitet werden kann. Statt der Standardausgabe nutzt man --pretty=format mit einem eindeutigen Trennzeichen wie einem Pipe-Symbol, um Hash, Betreffzeile und Autor sauber voneinander zu isolieren.
Ein häufiger Fehler ist, den kompletten Log-Bereich seit dem letzten Tag nicht korrekt einzugrenzen. Der Changelog Generator muss den letzten annotierten Tag ermitteln und nur die Commits danach verarbeiten, sonst tauchen bereits veröffentlichte Einträge erneut im neuen Abschnitt auf. git describe --tags --abbrev=0 liefert den letzten Tag, und git log ${last_tag}..HEAD grenzt den Bereich exakt ein.
#!/usr/bin/env bash
# extract-commits.sh — pull raw commit data since last tag
set -euo pipefail
readonly SEP=$'\x1f' # unit separator, never appears in commit text
readonly last_tag="$(git describe --tags --abbrev=0 2>/dev/null || echo "")"
readonly range="${last_tag:+${last_tag}..}HEAD"
echo "Generating changelog for range: ${range}" >&2
# Format: hash|type|scope|breaking|subject|author
git log "$range" --no-merges \
--pretty=format:"%H${SEP}%s${SEP}%an" |
while IFS="$SEP" read -r hash subject author; do
if [[ "$subject" =~ ^([a-z]+)(\(([a-z0-9_-]+)\))?(!)?:\ (.+)$ ]]; then
type="${BASH_REMATCH[1]}"
scope="${BASH_REMATCH[3]:-}"
breaking="${BASH_REMATCH[4]:-}"
description="${BASH_REMATCH[5]}"
printf '%s|%s|%s|%s|%s|%s\n' "$hash" "$type" "$scope" "$breaking" "$description" "$author"
fi
done
Die Verwendung des Unit-Separator-Zeichens \x1f statt eines gewöhnlichen Pipe-Symbols verhindert, dass der Changelog Generator an Commit-Nachrichten zerbricht, die zufällig ein Pipe-Zeichen enthalten. Dieses unsichtbare Steuerzeichen taucht in normalen Texten praktisch nie auf und ist damit ein robustes, aber unauffälliges Trennzeichen für die interne Datenverarbeitung.
4. Commits nach Typ gruppieren
Nach der Extraktion liegen alle Commits als Pipe-getrennte Datensätze vor. Der Changelog Generator muss sie jetzt nach Typ bündeln, sodass am Ende Abschnitte wie Features, Bugfixes und Breaking Changes entstehen, jeweils mit den passenden Einträgen darunter. Assoziative Arrays in Bash eignen sich hierfür hervorragend, weil sie Schlüssel-Wert-Zuordnungen ohne externe Werkzeuge ermöglichen.
Ein cleverer Changelog Generator hängt für jeden Typ eine wachsende Liste an einen Array-Eintrag an, statt für jeden Commit eine eigene Datei zu schreiben. Das hält die Verarbeitung im Arbeitsspeicher und vermeidet unnötige Dateisystemzugriffe, was besonders bei Repositories mit mehreren tausend Commits einen spürbaren Geschwindigkeitsunterschied macht.
#!/usr/bin/env bash
# group-by-type.sh — bucket commits into changelog sections
set -euo pipefail
declare -A sections=(
[feat]="Neue Funktionen"
[fix]="Fehlerbehebungen"
[perf]="Performance"
[refactor]="Refactoring"
)
declare -A grouped=()
declare -a breaking_changes=()
while IFS='|' read -r hash type scope breaking description author; do
[[ -z "${sections[$type]:-}" ]] && continue # skip chore, docs, style, test etc.
line="- ${description} (${scope:-general}) [${hash:0:7}]"
grouped["$type"]+="${line}"$'\n'
if [[ -n "$breaking" ]]; then
breaking_changes+=("- ${description}")
fi
done < commits.txt
for type in feat fix perf refactor; do
if [[ -n "${grouped[$type]:-}" ]]; then
echo "### ${sections[$type]}"
echo "${grouped[$type]}"
fi
done
5. Versionsvorschlag aus Commit Typen ableiten
Semantic Versioning legt fest, wann ein Major, Minor oder Patch Release nötig ist, und ein guter Changelog Generator kann diesen Vorschlag automatisch ableiten. Jeder Breaking Change erzwingt ein Major Release, jeder neue feat Commit ein Minor Release, alles andere reicht als Patch Release. Diese Logik lässt sich in wenigen Zeilen Bash direkt aus den gesammelten Typen berechnen.
Die Reihenfolge der Prüfung ist entscheidend: Der Changelog Generator muss zuerst auf Breaking Changes prüfen, danach auf Features, erst zuletzt auf alles Übrige. Wird diese Reihenfolge vertauscht, kann ein Minor Release fälschlich als Patch klassifiziert werden, was in halbautomatisierten Release-Prozessen zu falschen Versionssprüngen führt.
#!/usr/bin/env bash
# suggest-version.sh — derive next semver bump from commit types
set -euo pipefail
current_version="$1" # e.g. 2.4.1
IFS='.' read -r major minor patch <<< "$current_version"
has_breaking=0
has_feat=0
while IFS='|' read -r hash type scope breaking description author; do
[[ -n "$breaking" ]] && has_breaking=1
[[ "$type" == "feat" ]] && has_feat=1
done < commits.txt
if (( has_breaking )); then
echo "$((major + 1)).0.0"
elif (( has_feat )); then
echo "${major}.$((minor + 1)).0"
else
echo "${major}.${minor}.$((patch + 1))"
fi
6. Markdown Ausgabe und bestehende Datei zusammenführen
Ein Changelog Generator soll niemals die bisherige CHANGELOG.md komplett überschreiben. Stattdessen wird der neue Abschnitt oben eingefügt, direkt nach der Titelzeile, während der Rest der Datei erhalten bleibt. Dafür eignet sich ein temporäres Zusammenführen mit cat, das Kopfzeile, neuen Abschnitt und Altbestand in genau dieser Reihenfolge in eine neue temporäre Datei schreibt.
Der Changelog Generator sollte außerdem einen Vergleichslink zur vorherigen Version einfügen, etwa in Form von [2.5.0]: https://github.com/org/repo/compare/v2.4.1...v2.5.0. Solche Links machen es möglich, direkt aus dem Changelog heraus den vollständigen Diff einer Version im Repository-Hoster zu öffnen, ohne die Tags manuell suchen zu müssen.
#!/usr/bin/env bash
# merge-changelog.sh — prepend new section, keep history intact
set -euo pipefail
readonly CHANGELOG="CHANGELOG.md"
readonly TMP_FILE="$(mktemp)"
trap 'rm -f "$TMP_FILE"' EXIT
new_version="$1"
new_section_file="$2" # generated markdown from group-by-type.sh
{
echo "# Changelog"
echo
echo "## [${new_version}] - $(date +%F)"
echo
cat "$new_section_file"
echo
# Skip the first line ("# Changelog") of the existing file
tail -n +2 "$CHANGELOG" 2>/dev/null || true
} > "$TMP_FILE"
mv "$TMP_FILE" "$CHANGELOG"
echo "[OK] CHANGELOG.md updated to version ${new_version}"
7. Den Changelog Generator in CI/CD einbinden
Damit der Changelog Generator nicht manuell aufgerufen werden muss, gehört er als eigener Schritt in die Release-Pipeline, ausgeführt nach dem Merge in den Main-Branch und vor dem eigentlichen Tag-Push. In GitLab CI oder GitHub Actions genügt ein Job, der das Skript ausführt, den Commit mit dem aktualisierten CHANGELOG.md erstellt und den neuen Tag setzt.
Ein wichtiger Punkt: Der Changelog Generator darf keinen unendlichen Loop in der Pipeline auslösen. Da der Commit mit der aktualisierten CHANGELOG.md selbst wieder einen Pipeline-Lauf triggern würde, muss die Commit-Nachricht ein [skip ci] Flag enthalten oder der Job an einen separaten, manuell ausgelösten Trigger gebunden werden.
#!/usr/bin/env bash
# release.sh — CI entry point that runs the full changelog generator chain
set -euo pipefail
version="$(bash suggest-version.sh "$(git describe --tags --abbrev=0)")"
bash extract-commits.sh > commits.txt
bash group-by-type.sh > new-section.md
bash merge-changelog.sh "$version" new-section.md
git add CHANGELOG.md
git commit -m "chore(release): update changelog for v${version} [skip ci]"
git tag -a "v${version}" -m "Release v${version}"
git push origin HEAD --tags
8. Edge Cases und typische Fehlerquellen
Ein Changelog Generator, der nur auf dem Happy Path getestet wurde, bricht schnell an Real-World-Commits. Merge-Commits ohne Conventional-Commits-Präfix sollten mit --no-merges von vornherein ausgeschlossen werden, sonst landen generische Nachrichten wie Merge branch main als nicht kategorisierbare Zeilen im Ergebnis. Revert-Commits verdienen eine eigene Behandlung, da git revert automatisch ein Revert "..." Präfix erzeugt, das kein Conventional-Commits-Typ ist.
Ein weiterer Stolperstein: mehrzeilige Commit-Bodies mit einem BREAKING CHANGE: Absatz. Wer nur die erste Zeile mit git log --pretty=format:%s ausliest, übersieht diesen Marker vollständig. Der Changelog Generator muss deshalb zusätzlich %b auslesen und separat auf das Schlüsselwort prüfen, sonst werden Breaking Changes im schlimmsten Fall stillschweigend als normale Patches eingestuft.
9. Changelog Generator im Vergleich zu Alternativen
Es gibt fertige Tools wie conventional-changelog-cli oder git-cliff, die dieselbe Aufgabe lösen, aber jeweils zusätzliche Laufzeitumgebungen oder Binärabhängigkeiten mitbringen. Ein selbst gebauter Changelog Generator in Bash ist dagegen sofort in jedem Container mit Git verfügbar, ohne zusätzliche Installation, und lässt sich exakt an die eigene Formatierung anpassen.
| Ansatz | Abhängigkeiten | Anpassbarkeit | Geeignet für |
|---|---|---|---|
| Manuelles CHANGELOG.md | Keine | Vollständig, aber fehleranfällig | Sehr kleine Projekte |
| Bash Changelog Generator | Nur Git | Vollständig, eigener Code | Microservices, Container ohne Node |
| conventional-changelog-cli | Node.js, npm-Pakete | Über Presets konfigurierbar | JS-lastige Monorepos |
| git-cliff (Rust) | Separates Binary | Template-basiert, mächtig | Große Multi-Repo-Setups |
Für Teams, die ohnehin auf Bash-basierte Deployment-Skripte setzen, fügt sich ein eigener Changelog Generator nahtlos in die bestehende Toolchain ein, ohne zusätzliche Paketmanager oder Laufzeitumgebungen zu benötigen. Die Wartung bleibt bei genau demselben Team, das auch die restliche Automatisierung pflegt.
Mironsoft
Shell-Automatisierung, Release-Tooling und Deployment-Infrastruktur
Ein Changelog Generator, der zu eurem Release-Prozess passt?
Wir bauen euch einen maßgeschneiderten Changelog Generator, binden ihn in eure CI/CD-Pipeline ein und sorgen dafür, dass Conventional Commits im ganzen Team konsequent eingehalten werden.
Skript-Entwicklung
Individueller Changelog Generator passend zu eurem Commit-Format
CI-Integration
Nahtlose Einbindung in GitLab CI, GitHub Actions oder Jenkins
Commit-Konventionen
Hooks und Linting, damit Conventional Commits im Team greifen
10. Zusammenfassung
Ein selbst gebauter Changelog Generator auf Bash-Basis nimmt Teams die manuelle, fehleranfällige Pflege eines CHANGELOG.md ab. Voraussetzung ist eine strukturierte Datengrundlage in Form von Conventional Commits, aus denen sich Typ, Umfang und Breaking-Change-Status zuverlässig extrahieren lassen. Git Log liefert das Rohmaterial, assoziative Arrays gruppieren es, und ein einfacher Merge-Schritt fügt den neuen Abschnitt vor der bestehenden Historie ein.
Der größte Nutzen entsteht, sobald der Changelog Generator fest in der Release-Pipeline verankert ist: Jeder Merge in den Main-Branch kann automatisch die richtige nächste Version vorschlagen und den passenden Abschnitt im Changelog anlegen, ganz ohne dass jemand manuell eingreifen muss. Für Teams mit vielen Repositories reduziert das den Pflegeaufwand erheblich und sorgt für konsistente, lückenlose Release-Dokumentation.
Changelog Generator mit Bash — Das Wichtigste auf einen Blick
Datengrundlage
Conventional Commits liefern Typ, Scope und Breaking-Change-Marker als maschinenlesbares Präfix.
Extraktion
git log --pretty=format mit Unit-Separator, eingegrenzt auf den Bereich seit dem letzten Tag.
Gruppierung
Assoziative Arrays bündeln Commits nach Typ, Breaking Changes erhalten einen eigenen Abschnitt.
CI-Integration
Ausführung nach jedem Merge in Main, Commit mit [skip ci], um Endlos-Pipelines zu vermeiden.