Zurück zum Blog

Gemergt ist nicht ausgeliefert: wie man beweist, dass ein Commit wirklich läuft

Ein Fix lag bei mir rund zehn Tage im Hauptzweig, ohne je bei einem Nutzer anzukommen — und drei automatische Prüfläufe meldeten brav „nichts zu tun“. Warum die naheliegenden Proben das nicht merken, und welche Messung den Nachweis wirklich trägt.

5 Min. Lesezeitvon Daniel AlischEngineering, Deployment, KI-Agenten

Ein Commit, der in main liegt, fühlt sich fertig an. Bei mir lag ein Fix rund zehn Tage im Hauptzweig, ohne dass ihn ein einziger Nutzer je gesehen hat, und drei automatische Prüfläufe meldeten in dieser Zeit zuverlässig „nichts zu tun“. Der Fehler saß nicht im Code und nicht in der Pipeline, sondern in der Frage, die ich versehentlich gestellt hatte.

Die Frage, die man versehentlich stellt

Der Prüflauf verglich den lokalen Stand des Arbeitsverzeichnisses mit origin. Beide identisch, also kein Handlungsbedarf. Das ist eine völlig korrekte Antwort — nur auf eine andere Frage. Gemessen wurde „ist mein Arbeitsstand aktuell“. Wissen wollte ich „läuft mein Stand“. Diese zwei Zustände fallen im Alltag fast immer zusammen, und genau deshalb fällt es nicht auf, wenn sie es einmal nicht tun.

Das Tückische daran ist die Richtung des Irrtums. Ein Check, der im Zweifel „alles in Ordnung“ sagt, erzeugt keine Arbeit, keinen Alarm und keine Spur. Er ist angenehm, bis man ihn braucht. Ein Check, der im Zweifel meckert, kostet Zeit — und wird repariert, weil er nervt. Fehlertoleranz nach oben ist bei Prüfungen also kein Komfort, sondern eine Blindstelle mit Terminkalender.

Warum die naheliegenden Proben nichts beweisen

Drei Wege liegen nahe, und alle drei hielten in diesem Fall nicht:

  • Ein Versions-Endpunkt. Hätte die Frage in einem Aufruf geklärt. Die App hatte keinen.
  • Die Hashes der ausgelieferten Client-Dateien. Ändert sich ein Chunk-Name, wurde neu gebaut. Nur: die betreffenden Commits waren rein serverseitig. Die Hashes bleiben dann gleich, und der Vergleich meldet „unverändert“, obwohl er für diese Art Änderung grundsätzlich blind ist. Eine Probe, die bei einer ganzen Änderungsklasse systematisch nichts sieht, darf nicht als Beleg zählen.
  • Einloggen und nachsehen. Scheidet aus, wenn dort echte Nutzerdaten liegen. Und selbst wenn nicht: „sieht richtig aus“ beantwortet nicht die Frage, welcher Commit da eigentlich läuft.

Die Messung, die trägt

Was am Ende funktioniert hat, ist unspektakulär: die älteste Logzeile des laufenden Containers gegen den Zeitstempel des Commits. Die erste Logzeile markiert, wann der Prozess gestartet ist. Liegt sie vor dem Commit, dann kann dieser Commit in diesem Prozess unmöglich stecken. Das ist kein Indiz, das ist ein Ausschluss.

bash
# Zeitpunkt des Commits, der ausgeliefert sein soll
git log -1 --format=%cI -- path/to/changed/file

# Zeitpunkt des Prozessstarts: erste Zeile im Log des laufenden Containers
# (docker logs --timestamps ... | head -n 1, oder das Log-Werkzeug der Plattform)

# Logzeile aelter als Commit  =>  der Commit laeuft nicht. Beweisbar.

Zwei Dinge gehören zur Ehrlichkeit dieser Messung. Erstens: sie ist einseitig. Sie beweist das Negativ sauber, das Positiv nur schwach — ein Prozessstart nach dem Commit heißt noch nicht, dass das Image mit diesem Commit gebaut wurde. Ein Neustart ohne Neubau sieht genauso aus. Zweitens: man braucht wirklich die älteste Zeile, nicht die erste aus einem Fenster, das irgendein Werkzeug bequem gewählt hat. Rotiert das Log, lügt die Messung in die harmlose Richtung.

Das Positiv braucht einen eigenen Beleg

Deshalb ist die Logzeile für mich ein Notnagel, nicht die Lösung. Die Lösung ist, dass die laufende Anwendung selbst sagen kann, wer sie ist: eine Versionsnummer aus der Datei und der Commit-Hash als Build-Argument ins Image, ausgegeben über einen schlichten Endpunkt. Danach ist die ganze Frage ein curl und eine Zeichenkette, die man vergleicht — statt einer Indizienkette, die man jedes Mal neu zusammensucht.

Das wirkt nach Zusatzarbeit für ein Problem, das selten auftritt. Der Vergleichsmaßstab ist aber nicht der Normalfall, sondern der Fall, in dem man es wissen muss: wenn ein Fehler weiterhin auftritt, obwohl der Fix gemergt ist. Ohne Versionsauskunft stehen dann zwei plausible Geschichten gegeneinander — der Fix greift nicht, oder der Fix ist nicht da. Das sind völlig verschiedene Arbeitstage.

Die Regel, die ich daraus mitgenommen habe

Jede Prüfung, die „nichts zu tun“ sagen darf, muss mitliefern, welchen Zustand sie gemessen hat. „Repo unverändert“ ist kein „ausgeliefert“. „Build grün“ ist kein „läuft“. „Gemergt“ ist erst recht keins von beidem. Das klingt wie Wortklauberei und ist in der Praxis der Unterschied zwischen einem Fix und einem Fix, von dem nur das Repository weiß.

Und eine zweite Regel, die mich dasselbe Thema in anderer Verkleidung schon mehrfach gekostet hat: wenn eine Messung bei einer Änderungsklasse nichts sehen kann, dann ist ihr Nullbefund für diese Klasse kein Ergebnis, sondern eine verweigerte Messung. Die Client-Hashes waren nicht falsch. Sie waren für serverseitige Änderungen schlicht nicht zuständig — und haben das nicht gesagt, weil Werkzeuge ihre Zuständigkeit selten von selbst einschränken.

Fazit

Der Weg vom Commit zur laufenden Anwendung hat mehr Stationen, als sich beim Mergen anfühlt, und jede davon kann still stehen bleiben. Billig prüfbar ist das Negativ: ältester Logeintrag gegen Commit-Zeit, und man weiß in zwei Minuten, ob überhaupt neu gestartet wurde. Belastbar wird es erst, wenn die Anwendung ihre eigene Version nennen kann. Bis dahin gilt der nüchterne Satz, den ich mir inzwischen aufgeschrieben habe: committet, gepusht und gemergt sind drei Zustände, und keiner davon heißt ausgeliefert.

Weiterlesen