Zum Inhalt springen

Never Release to Test: Warum wir unsere CI und KI-Agenten nach links verschieben mussten

16’300 CI-Minuten in einer Woche, 18.9 % kompletter Ausschuss und 22 Release-Versuche für eine einzige Anwendung: Warum Releases als Testumgebung missbraucht wurden, wieso Agenten ohne Governance lautlos Standards ignorieren, und wie ein dreistufiger Filter das Problem löst.

Patrick Lehmann
5 Min. Lesezeit
Drei Filterstufen gegen CI-Verschwendung: Lokale Schranken für Agenten (Layer −1), lückenlose Merge-Request-Reichweite (Layer 0) und ein evidenzbasierter Preflight-Gate vor jedem Tag (Layer 1).

In der zweiten Augustwoche 2026 lief unsere GitLab-CI-Infrastruktur heiss: 16’300 Runner-Minuten in sieben Tagen. Davon verbrannten 2’809 Minuten (18.9 %) in Pipelines, die nie einen Mehrwert lieferten, sondern mit vermeidbaren Fehlern abbrachen. Bei einer einzelnen Web-Anwendung zählten wir 22 Release-Tags innerhalb von sechs Tagen, nur um einen lauffähigen Stand auf Produktion zu bekommen.

Die intuitive Diagnose in solchen Momenten lautet fast immer: «Unsere Pipeline ist flaky.»

Die forensische Auswertung aller Pipeline-Läufe dieser Woche zeigte jedoch das exakte Gegenteil: Nur 10.3 % der Abbrüche waren echte Flakes (Netzwerk-Timeouts, Runner-Races). Ganze 78.4 % waren reale Fehler: Syntaxfehler, fehlende Imports, deplatzierte Konfigurationen und kaputte Builds, die Entwickler und autonome KI-Agenten erst nach einem 17-minütigen Remote-CI-Roundtrip bemerkten.

Wir hatten kein Flakiness-Problem. Wir hatten ein Feedback-Latenz-Problem. CI wurde als verlängerter Debugger missbraucht.

— Forensischer Audit-Report, August 2026

Dieser Artikel ist der erste Teil einer zweiteiligen Aufarbeitung. Er beschreibt, wie wir durch drei architektonische Hebel (die Durchsetzung von Agenten-Governance vor Turn 0, die lückenlose Schliessung von Validierungslücken in Merge Requests und ein striktes, evidenzbasiertes Preflight-Gate vor jedem Release) 1’150 Minuten wöchentlichen CI-Müll eliminiert haben.


Die Anatomie des Scheiterns: Was die Daten zeigten

Eine detaillierte Klassifikation aller 46 fehlgeschlagenen Pipelines zwischen dem 8. und 15. August ergab drei strukturelle Brandherde:

FehlerkategorieAnteil MinutenSymptom & Ursache
Tag-Kaskaden & Release-Blindheit27.3 % (767 min)Deploy-Jobs schlagen erst auf dem Tag fehl (Docker-Builds, Healthchecks), weil sie im Merge Request gar nicht ausgeführt wurden.
Agenten-Regelbrüche & triviale Fehler13.7 % (384 min)Syntaxfehler, ungetestete TypeScript-Typen und fehlende Dependencies in isolierten Worktrees.
Frontend- & E2E-Ressourcenstau49.0 % (1’377 min)Parallele Vitest- und Playwright-Läufe verstopfen 32-Kern-Runner ohne CPU-Budgetierung.
Tatsächliche CI-Flakes10.0 % (281 min)Vorübergehende Netzwerkfehler und Socket-Timeouts externer Registries.

Besonders verheerend war die Wechselwirkung zwischen autonomen KI-Agenten und unvollständigen Merge-Request-Pipelines.


Das Phänomen der «lautlosen Abwesenheit» bei KI-Agenten

In unserem Monorepo arbeiten Entwickler Hand in Hand mit autonomen LLM-Agenten (Claude Code, OpenCode, Codex), die in isolierten Git-Worktrees operieren. Sämtliche Squibble-Engineering-Standards (von Commit-Konventionen bis hin zu strikten TDD-Regeln: «Führe niemals einen Test runner direkt aus, nutze mise run <task>») sind zentral in einem gemeinsamen Submodul .agents/ hinterlegt.

Bei der Analyse der Worktrees fiel uns ein gravierender Fehler auf: In 19 von 19 aktiven Agenten-Worktrees war das Submodul .agents/ überhaupt nicht initialisiert.

/worktrees/feat-auth/
├── .agents/          <-- LEER (Submodul nicht ausgecheckt)
├── AGENTS.md -> .agents/rules/AGENTS.md  <-- Toter Symlink!

Da Git-Worktrees Submodule standardmässig nicht rekursiv mit auschecken, zeigten die Symlinks für die Agenten ins Leere. Ohne Fehlermeldung starteten die Sprachmodelle in Turn 0 mit Standard-Prompts, ignorierten firmeninterne Konventionen, erstellten ungültige Commit-Formate und committeten ungeprüften Code direkt ins Repository.

Die Lösung: Ein Root-eigener, fail-closed Git-Shim (Layer −1)

Appelle an Agenten oder Entwickler, nach git worktree add manuell git submodule update --init aufzurufen, scheitern in der Praxis. Wir haben das Problem auf Betriebssystemebene gelöst.

Über Ansible haben wir vor dem eigentlichen git-Binary einen Service-spezifischen Wrapper platziert, der jeden Checkout abfängt:

infrastructure/ansible/roles/cluster01__development/files/t3-git-shim.sh
#!/usr/bin/env bash
set -euo pipefail

REAL_GIT="/usr/bin/git"

# Führe das originale Git-Kommando aus
"$REAL_GIT" "$@"
EXIT_CODE=$?

# Nach jedem Checkout oder Worktree-Befehl: Synchronisiere .agents zwingend
if [[ "$EXIT_CODE" -eq 0 && ("$*" =~ "checkout" || "$*" =~ "worktree add") ]]; then
  if [[ -f ".gitmodules" ]] && grep -q "\.agents" .gitmodules; then
    "$REAL_GIT" submodule update --init --recursive .agents >/dev/null 2>&1 || {
      echo "[FATAL] .agents Submodul konnte nicht initialisiert werden. Fail-closed." >&2
      exit 1
    }
  fi
fi

exit $EXIT_CODE

Dieser Shim arbeitet fail-closed: Kann das Regelwerk nicht synchron und vollständig initialisiert werden, schlägt der gesamte Worktree-Befehl fehl. Kein Agent kann auch nur eine Zeile Code schreiben, ohne dass die monorepo-weiten Guardrails im Kontext geladen sind.


Das Anti-Pattern: «Release to Test»

Der zweite grosse Hebel betraf unsere Release-Pipeline.

Historisch waren viele teure CI-Schritte (das Bauen von Docker-Images, das Hochfahren von Abhängigkeiten über Healthchecks und End-to-End-Delivery-Probes) ausschliesslich an Git-Tags (v*.*.*) gekoppelt. Der Grundgedanke war einst Ressourcenschonung: Man wollte in Feature-Branches keine schweren Container bauen.

Die Folge war fatal: Entwickler und Agenten pushten Feature-Branches, sahen eine grüne Merge-Request-Pipeline (die nur Unit-Tests prüfte), mergten auf main und erstellten sofort einen Release-Tag. Erst auf dem Tag schlug dann der Docker-Build fehl: beispielsweise weil ein COPY-Pfad im Dockerfile nach einem Refactoring nicht mehr stimmte.

Da ein fehlgeschlagener Tag unveränderlich ist, blieb nur ein Ausweg: Fix committen, mergen, neuer Tag (v1.2.1), hoffen.

So entstanden die 22 Release-Tags in sechs Tagen.

Layer 0: Volle Reichweite im Merge Request

Um diesen Teufelskreis zu durchbrechen, haben wir die CI-Matrix radikal umgebaut:

  1. Downstream-Bridges in MRs: Alle 10 Client-Frontends und Service-Anwendungen triggern bei code-relevanten Änderungen ihre vollständigen Validierungs-Pipelines bereits im Merge Request.
  2. Immutable Candidate Images: Docker-Images werden bereits im Merge Request gebaut und gegen Registry-Healthchecks getestet. Um Caches nicht zu vergiften, pushen MR-Pipelines unter immutable Tags (mr-<iid>-<sha>), während der mutable Tag :latest strikt für erfolgreiche main-Builds reserviert bleibt.
  3. Parallele Smoke-Tests: Auf dem main-Branch laufen Smoke-Tests ausschliesslich lesend gegen Staging, um gegenseitige Blockaden zu verhindern.

Damit war sichergestellt: Was im MR nicht grün ist, kann main niemals erreichen.


Die letzte Schranke: Das release/preflight-Gate (Layer 1)

Selbst mit grünen MR-Pipelines blieb eine Restunsicherheit: In einem Monorepo mit 17 eigenständigen Deployables (Frontends, APIs, Parser, Hintergrunddienste) können unglückliche Merge-Reihenfolgen auf main subtile Integrationsbrüche erzeugen.

Früher bemerkte man das nach dem Setzen des Release-Tags. Heute verhindert das ein dediziertes Preflight-Skript, das als Pflicht-Job auf main läuft.

bin/release/preflight (Auszug)
# Sammle den Status aller 17 Deployables
DEPLOYABLES=$(nx show projects --with-target=build)

for app in $DEPLOYABLES; do
  echo "Prüfe CI-Evidenz für $app..."
  
  # Ermittle, ob die letzte Änderung an diesem Deployable 
  # eine nachweisbar grüne Pipeline auf main durchlaufen hat
  if ! verify_deployable_evidence "$app"; then
    echo "[BLOCK] $app hat ungetestete Änderungen oder rote Checks!" >&2
    exit 1
  fi
done

echo "[READY_TO_TAG] Alle 17 Deployables sind vollständig verifiziert."
exit 0

Das release/preflight-Gate prüft rein evidenzbasiert:

  • Wurde für jedes der 17 Deployables seit dem letzten Commit ein erfolgreicher Build- und Testnachweis erbracht?
  • Gibt es verwaiste Änderungen, die zwar gemergt, aber noch nicht durch die Pipeline gelaufen sind?

Erst wenn dieses Skript mit Exit-Code 0 durchläuft, wird die Schaltfläche oder Automation zum Taggen freigegeben.


Das Resultat

Durch die Implementierung der Stufen Layer −1, Layer 0 und Layer 1 haben wir die Stabilität unserer Pipelines messbar verändert:

  • 0 fehlgeschlagene Release-Tags seit Einführung des Preflight-Gates.
  • 1’151 Minuten (41 % des wöchentlichen CI-Ausschusses) wurden sofort eliminiert.
  • 100 % Konformität bei KI-Agenten: Durch den T3-Git-Shim startet kein Agent mehr ohne initialisierte Regeln und Typechecks.

Im kommenden zweiten Teil dieser Serie widmen wir uns den verbleibenden 1’377 Minuten: Wie wir durch cgroup-basierte Ressourcenisolation in Vitest, schlanke Playwright-Container und automatisierte wöchentliche DX-Scorecards auch den Frontend-Stau beseitigt haben.

Patrick Lehmann

Architecture & Governance Lead

Squibble GmbH

Bringt seit über zwanzig Jahren Struktur in gewachsene IT-Landschaften — als Architekt, Entwickler und Betreiber. Schreibt hier über die Systeme, die bei Squibble tatsächlich laufen, und über die Entscheide dahinter.

Weiterlesen

Boards: ein Kanban, das nicht weiss, wo seine Karten liegen

Jedes Board scheitert irgendwann daran, dass es zur zweiten Wahrheit wird: es merkt sich, in welcher Spalte eine Karte liegt, und ab da muss dieses Wissen gepflegt werden. Workbench merkt es sich gar nicht: Spalten sind Regeln, die beim Lesen ausgewertet werden.

Patrick Lehmann
9 Min. Lesezeit

Workbench: ein Delivery-Index, der falsch liegen darf

Wir haben eine mandantenfähige Sicht über mehrere GitLab-Instanzen gebaut, ohne zur zweiten Wahrheit zu werden. Die Regel, die das trägt: die lokale Datenbank ist wegwerfbar, und korrekt macht sie die Reconciliation, nicht die Webhooks.

Patrick Lehmann
5 Min. Lesezeit