Eine CI/CD-Pipeline gehört zu den wichtigsten Handwerkszeugen in der Software-Entwicklung. Der CI-Teil (continuous integration) kann beim Einpflegen der neuesten Änderungen bestimmte Tests durchführen. Beispielsweise kann automatisch die Testsuite (etwa basierend auf RSpec) durchgeführt werden, oder es wird ein Linter wie Rubocop angewendet. Wenn eines davon fehlschlägt, wird die Übernahme der Änderungen verweigert, und die Entwickler müssen nacharbeiten. Der CD-Teil (continuous delivery oder continuous deployment) kann automatisiert die neue Version des Codes ausliefern, beispielsweise in eine Staging-Umgebung (oder, wenn die Tests vorher gut genug waren, direkt in eine Production-Umgebung).

Für beide Aufgaben gibt es eine Vielzahl an Werkzeugen. Wenn es aber um ganz einfache Anwendungsfälle geht, kann schon ein Bash-Skript mit ein paar Zeilen Code ausreichen.

Man kann beispielsweise ein bare Git Repository verwenden, also ein Repository, das selbst keinen ausgecheckten Code enthält. Von diesem bare Repository klont sich jeder Entwickler eine eigene Version, in der er oder sie arbeitet. Die fertigen Änderungen schiebt man dann etwa mit git push origin wieder in das bare Repository.

Wenn man jetzt eine “funktionsfähige” Version des Repositories braucht, beispielsweise weil ein darin verwaltetes Skript in einem Cronjob ausgeführt werden soll, kann man das bare Repository ein weiteres Mal auschecken. Das Szenario sieht dann also ungefähr so aus:

graph LR; bare[Bare Repository] --> prod[Production]; devA[Entwickler A] --> bare; devB[Entwickler B] --> bare; devC[Entwickler C] --> bare;

Wie aber sorgt man dafür, dass jede Änderung, die einer der Entwickler in das zentrale bare Repository pusht, automatisch in das Production Repository übertragen und dort ausgecheckt wird? Dazu kann man einen Git Hook verwenden, nämlich post-receive. Dieser Hook wird ausgeführt, wenn das Repository eben Daten in einem Push empfangen hat. Das Skript, das der Hook ausführen soll, ist sehr einfach:

#!/bin/bash
TARGET_DIR="/path/to/the/production/repo/"

if [ -d "${TARGET_DIR}" ]; then
  cd "${TARGET_DIR}"
  unset GIT_DIR
  git fetch origin
  git reset --hard origin/main
fi

Dieses kleine Skript geht davon aus, dass das bare Repository aus Sicht des Production Repositories origin ist, und der gewünschte Branch main heißt. Diese Parameter kann man natürlich beliebig anpassen, genauso wie den Pfad zum Production Repository.

So ziemlich das einzige, was nicht direkt offensichtlich ist, ist die Zeile unset GIT_DIR. Die hilft nämlich über ein kleines Problem hinweg, das einen sonst gegen die Wand laufen lässt. Ohne diese Zeile bekommt man nämlich eine Fehlermeldung wie

remote: Schwerwiegend: Kein Git-Repository: '.'

Huh? Das ist doch offenkundig ein Git-Repository, und der Pfad stimmt auch? Nun, was man hier wissen muss, ist dass die Umgebungsvariable GIT_DIR zwischen einem bare Repository und einem ausgecheckten Repository unterschiedlich ist. In einem bare repo finden sich die “eigentlichen” Git-Dateien direkt in ., in einem ausgecheckten Repo typischerweise im Unterverzeichnis .git/.

Der Hook befindet sich in einem bare repo, also hat GIT_DIR den Wert ., was das aktuelle Verzeichnis repräsentiert. Wenn wir mittels cd ins Production Repository wechseln, stimmt das aber nicht mehr, denn dort sind die Git-Daten in .git/. Wenn man die Umgebungsvariable entfernt (unset), wird der Wert neu ermittelt und ist dann korrekt.

Natürlich könnte man für denselben Zweck auch andere Werkzeuge verwenden, beispielsweise einen Git Worktree oder eben eine beliebige CI/CD-Lösung. Aber ein paar Zeilen Bash-Code sind für diesen Anwendungsfall bereits vollauf genug.

Tags: ,

Aktualisiert: