Technical Digest: Integrate after Push

von Björn Ostermann

In einem agilen Projekt ist ein Integrationsserver Pflicht. Er zeigt dem Team, dass der Quelltext auf dem Master-Branch erfolgreich gebaut werden kann. Fortgeschrittene Teams streben Continuous Deployment an und wollen dafür möglichst jederzeit das aktuelle Artefakt in die Produktionsumgebung geben können.

Deswegen ist es wichtig, dass der aktuelle Master-Branch erfolgreich kompiliert. Man möchte verhindern, dass durch einen Check-In Fehler im Master-Branch auftreten. Dazu wird üblicherweise auf dem Entwicklungsrechner zuerst der Quelltext gegen den Master-Branch aktualisiert, dann wird kompiliert und alle Tests werden durchlaufen.

Abbildung 1: Ablauf eines erfolgreichen Check-Ins

Das ist zeitaufwendig - aber notwendig. Das Update sollte unter einer Minute dauern, das lokale Kompilieren und Durchlaufen der Tests unter 10 Minuten. Ungünstig wirkt es sich aus, wenn die Entwickler im Team mehrfach funktionierende Zwischenstände in den Master-Branch einchecken wollen, während sie eine Story implementieren. Dann addieren sich die Zeiten für Update-Compile-Run-Tests. Entweder ich gebe mich mit etwa zwei Zyklen pro Stunde zufrieden oder die Entwickler tendieren dazu, seltener zu integrieren. Schlecht ist es, auf die lokale Integration, das Update, zu verzichten und ohne einzuchecken.

In einem agilen Entwicklungsteam ist es nicht akzeptabel, dass der Master-Branch rot wird. Dies zeigt an, dass nicht-funktionierender Quelltext eingecheckt wurde bzw. keine angemessenen Tests existieren. Als Konsequenz muss zuerst der Fehler behoben werden bevor andere Teammitglieder ihren Quelltext einchecken können. D.h. das gesamte Team unterbricht seine Arbeit, um dieses Problem zu beseitigen. Zwei wesentliche Behinderungen blockieren nun das Team: 1.) Niemand kann den Master-Branch mehr als Referenz nutzen, um Update-Compile-Run-Tests zu machen. Es ist ja bereits bekannt, dass dies zu einem Fehler führt. 2.) Würde jemand anderes jetzt weitere Änderungen einchecken, dann ließe sich die Ursache für den Fehler noch schlechter nachvollziehen.

Abbildung 2: Build- oder Test-Fehler

Für das Team ist es ungünstig, dass durch einen Fehler auf dem Master wiederholt der Arbeitsrhythmus unterbrochen wird. Aber nur durch eine gezielte Zusammenarbeit kann das Problem effektiv behoben werden.

Kümmert sich das Team nicht sofort um die Behebung des Fehlers im Master, dann wirken die Kräfte der Broken-Windows-Theory. Der Fehler wird nach-und-nach akzeptiert, als Normalität angesehen und die Qualität im Entwicklungsprozess sinkt, denn die Rückkopplungszyklen greifen nicht mehr. Weiteres Einchecken von neuem Code macht es immer schlimmer.

Abbildung 3: Behandlung eines Merge-Konflikts

Eine mögliche Lösung für unser Problem ist das Einrichten eines Stage-Branches pro Entwickler. Wir nehmen damit die Wartezeit vom Entwicklungsrechner und stellen trotzdem sicher, dass nichts ungeprüft im Master landet.

Dies lässt sich z.B. mit Git realisieren, indem wir es so konfigurieren, dass wir immer ein “push to Stage, pull from Master” machen. Der Build-Server lauscht dabei auf allen Stage-Branches. Wird eine Änderung in einem Stage-Branch festgestellt, läuft immer dieser Prozess ab:

  1. Bei Änderung jeweils pull from Master
  2. build
  3. tests
  4. Bei Erfolg push to Master

In Abbildung 1 wird dieser Ablauf veranschaulicht.

Gleichzeitig serialisiert der Build-Server die Aufträge aller Entwickler (queuing), um Master-Konflikte zu vermeiden.

Nach einem fehlgeschlagenen Build bekommt derjenige, der den Check-In gemacht hat, eine Benachrichtigung und kann sich um sein isoliertes Problem kümmern.

In Abbildung 2 wird der Ablauf bei einem durch Compile- oder Build-Fehler fehlgeschlagenem Build dargestellt.

Sollte es zu einem Merge-Konflikt kommen, weil zwei Entwickler an derselben Codestelle Änderungen vorgenommen haben, müssen die Änderungen einmal auf den lokalen Branch gepullt, der Merge-Konflikt behoben und anschließend die Änderungen erneut gepusht werden. Dieser Ablauf ist in Abbildung 3 zu sehen.

Mit unserem Ansatz haben wir eine Reihe positiver Effekte:

  1. Wir können den grünen Pfad auf dem Master sicherstellen.
  2. Andere Entwickler werden durch Broken-Check-Ins grundsätzlich nicht behindert.
  3. Auf der untersten Ebene bleibt das Team “voll-Release-fähig”.
  4. Wenn man innerhalb einer Story mehrfach einchecken möchte, dann wird das begünstigt.
  5. Zeitersparnis: Ich muss nicht mehr auf das Ausführen der Unit-Tests warten.

Dieser Ansatz unterscheidet sich von anderen Ansätzen, wie beispielsweise der Verwendung von Feature-Branches oder Code-Review-Systemen (z.B. Gerrit) durch seine Leichtgewichtigkeit. Die lokalen Stage-Branches dienen nur als Sicherheitsnetz für das fehlerfreie Bauen und Unit-Testen eines Code-Inkrements. Solange es keine Fehler gibt, unterscheiden sich Stage-Branch und Master nicht. Eine explizites Arbeiten auf dem Stage-Branch ist bei diesem Ansatz auch gar nicht erwünscht. Die Gefahr, dass sich Master- und Feature-Branch auseinander entwickeln, gibt es bei diesem Ansatz nicht, da jeder Check-In sofort automatisch wieder zu einem gültigen Stand zusammengefügt wird. Eine für das Zusammenführen der verschiedenen Code-Stände verantwortliche Rolle wird damit ebenfalls nicht benötigt.

In einem Kontext mit erfahrenen Softwareentwicklern ist das verzögerte Feedback kein relevantes Problem.

In der Summe leisten wir mit diesem Ansatz einen wichtigen Beitrag zu Continuous Deployment, wir mildern ein Problem und wir tragen dazu bei, häufiger technisches Feedback abzurufen.