Zurück zum Blog

Zwei grüne Pull Requests, zusammen rot: was Zweig-CI nicht messen kann

Zwei Pull Requests, jeder einzeln grün, beide gemergt — und die Anlage startet nicht mehr. Die CI hat nicht gelogen, sie hat eine andere Frage beantwortet. Über Migrationsköpfe, Versionsnummern ohne Konflikt und drei Messungen, die billiger sind als ein kaputter Start.

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

Zwei Pull Requests, beide fertig, beide reviewt, beide mit einem grünen Haken an jedem einzelnen Prüfschritt. Nacheinander in die Hauptlinie gemergt — und beim nächsten Hochlauf bricht die Anwendung ab, bevor sie den ersten Request sieht. Die CI hat dabei nicht gelogen. Sie hat nur eine Frage beantwortet, die ich ihr nie gestellt habe: nicht „passen diese beiden Änderungen zueinander", sondern „passt diese eine Änderung zum aktuellen Stand".

Einzahl und Mehrzahl

Der sauberste Fall dieser Klasse ist mir bei Datenbankmigrationen begegnet. Ein Schema-Werkzeug wie Alembic führt die Migrationen als verkettete Liste: jede Datei nennt ihren Vorgänger. Zweigen zwei Entwicklungsstränge von derselben letzten Migration ab und bringt jeder eine eigene neue mit, dann hat die Kette nach dem Merge nicht mehr ein Ende, sondern zwei.

Und jetzt kommt der Unterschied, der alles erklärt, und er besteht aus einem einzigen Buchstaben. Der Prüfschritt in der Pipeline rief alembic upgrade heads auf — Mehrzahl, mehrere Enden ausdrücklich erlaubt. Der Start der Anwendung ruft alembic upgrade head auf — Einzahl. Bei zwei Enden weiß das Werkzeug nicht, welches gemeint ist, und bricht mit Multiple head revisions are present ab. Beide Zweige waren grün, weil beide Zweige einzeln genau ein Ende hatten. Die Mehrzahl in der Pipeline war einmal eine Bequemlichkeit und ist dadurch zu einer Brille geworden, die genau die Fehlerklasse ausblendet, die sie finden sollte.

Die Versionsnummer, die nicht konfligiert

Derselbe Mechanismus, nur fieser, trifft die Release-Spur. Wir vergeben für jede Änderung eine Versionsnummer, die an drei Stellen stehen muss: in einer VERSION-Datei, als Label im Container-Abbild und als Eintrag im Changelog. Zwei parallel arbeitende Zweige greifen beide zur nächsten freien Nummer — und das ist für beide dieselbe, denn beide haben in dieselbe Hauptlinie geschaut.

Beim Rebase des zweiten Zweigs passiert dann nichts. Kein Konflikt, keine Warnung, nichts zu lösen: In der VERSION-Datei steht auf beiden Seiten derselbe Text, und gleicher Inhalt ist für Git kein Widerspruch. Konflikte gibt es nur im Changelog, weil dort zwei verschiedene Absätze stehen. Wer die Konfliktliste abarbeitet und danach „sauber" liest, hat die doppelt vergebene Nummer durchgewinkt. Das Ergebnis sind zwei Releases mit derselben Nummer — und ein Betreiber, der an keiner Stelle sehen kann, welcher Stand bei ihm eigentlich läuft.

Eine Warnung aus eigener Erfahrung noch dazu: Repariere eine Datei mit Konfliktmarkern nie mit sed. Ein Suchmuster trifft beide Konfliktseiten, schreibt in beide denselben Wert, lässt die Marker stehen — und eine Prüfung, die nur „steht die neue Nummer drin?" fragt, meldet artig Erfolg.

Was eine Zweig-Prüfung grundsätzlich nicht sehen kann

Beide Fälle sind keine Nachlässigkeit im Test, sondern eine Eigenschaft des Aufbaus. Eine Zweig-CI misst immer die Paarung Zweig plus Hauptlinie. Die Paarung Zweig plus anderer offener Zweig kommt in ihrem Weltbild nicht vor. Sichtbar wird das überall, wo sich zwei Änderungen um eine Sache streiten, die es nur einmal geben kann:

  • das eine Ende der Migrationskette,
  • die eine nächste Versionsnummer,
  • ein global eindeutiger Name oder ein fester Port,
  • dieselbe Minute in zwei neuen Zeitplänen,
  • ein Eintrag in einer von Hand gepflegten Liste, die beide Zweige erweitern.

Die gemeinsame Signatur: Jede Änderung ist für sich korrekt, und die Summe ist es nicht. Je mehr parallel gearbeitet wird — und mit Agenten, die mehrere Stränge gleichzeitig vorantreiben, wird parallel gearbeitet —, desto häufiger wird diese Klasse.

Drei Messungen, die billiger sind als ein kaputter Start

Erstens: die Köpfe über alle offenen Zweige zählen, nicht nur im eigenen. Das geht ohne Datenbank, allein aus den Dateien — ein Kopf ist eine Revision, die von keiner anderen als Vorgänger genannt wird:

python
import re, pathlib

rev, parents = set(), set()
for f in pathlib.Path("migrations/versions").glob("*.py"):
    t = f.read_text(encoding="utf-8")
    m = re.search(r'^revision\s*[:=].*?["\']([^"\']+)', t, re.M)
    if m:
        rev.add(m.group(1))
    for d in re.findall(r'^down_revision\s*[:=].*?["\']([^"\']+)', t, re.M):
        parents.add(d)

print(sorted(rev - parents))   # mehr als ein Eintrag = der Start bricht ab

Zweitens: einen Trocken-Merge in einem Wegwerf-Arbeitsbaum fahren, bevor irgendetwas in die Hauptlinie geht. Nicht „sieht konfliktfrei aus", sondern den echten Startbefehl im zusammengeführten Stand laufen lassen. Die interessanten Fehler zeigen sich erst in der Kombination, und ein Arbeitsbaum, den man danach löscht, kostet nichts.

Drittens: die nächste freie Versionsnummer nicht aus der Hauptlinie ableiten, sondern aus den offenen Pull Requests. Die Hauptlinie sagt, welche Nummern vergeben sind; die offenen Zweige sagen, welche Nummern bereits beansprucht sind. Dazu gehört, die Reihenfolge der Merges vorher festzulegen, statt sie dem Zufall zu überlassen — wer zuerst kommt, nummeriert, der zweite zieht nach.

Fazit

Ein grüner Haken ist eine Antwort auf eine konkrete Frage, und bei parallelen Zweigen ist es fast nie die Frage, die man meint. Es hilft, sich vor jedem Merge einmal zu fragen, welche Sache in dieser Änderung nur einmal existieren darf — und ob gerade jemand anders dieselbe Sache anfasst. Das sind zwei Minuten. Ein Hochlauf, der an einer doppelten Kopfzeile scheitert, ist teurer, und zwar genau dann, wenn niemand hinschaut.

Weiterlesen