Git Squash-Merge kehrt nur Probleme unter den Teppich
Git Squash Merge hilft nicht wirklich, saubere Commit-Historien zu erstellen. Es ist nur eine Krücke, um eine sinnvolle Commit-Aufteilung zu umgehen.
Agent prüft in seiner Cloud-Session, Pull-Request-CI überspringt: Wie ein Commit-Trailer GitHub-Actions-Minuten spart – und welche Wege scheiterten.
Anthropic schenkt Pro- und Max-Abonnenten seit Ende September bis zu 250 Dollar Guthaben, nur für Claude-Code-Cloud-Sessions. Die Botschaft: Lasst den Agenten in der Cloud arbeiten. Was dabei leicht übersehen wird: Jede Session läuft in einer eigenen VM mit mehreren Kernen, ohne separate Abrechnung.
Der Agent hat also eine vollwertige Build-Maschine, schreibt dort den Code und führt die Tests aus – und schickt trotzdem jeden Push an GitHub Actions, wo dieselben Tests noch einmal laufen, auf Minuten, die man bezahlt. Dieser Artikel zeigt, wie wir die Session zum CI-Runner gemacht haben, an welchen vier Wegen uns der Session-Proxy scheitern ließ, und welcher blieb.
Aufgefallen ist es uns, als die inkludierten Action-Minuten aufgebraucht waren – nicht am Monatsende, sondern mittendrin, in einem privaten Repository, an dem hauptsächlich Agenten arbeiten. Wer nachsieht, warum, findet dieselbe Testsuite dreimal am selben Vormittag: einmal in der Session, in der der Agent sie geschrieben hat, einmal beim Push auf den Pull Request, einmal nach dem Merge.
So prüft das übliche Setup jede Änderung mindestens zweimal in der Cloud: bei jedem Push auf den Pull Request, damit man weiß, ob man mergen darf, und noch einmal nach dem Merge auf main, weil dort ein neuer Commit entsteht. Dabei ist nur der Lauf auf main wirklich nötig – er prüft, was ausgeliefert wird. Die PR-Läufe sind Vorabprüfungen. Hat der Agent dieselbe Prüfung schon in seiner Session gemacht, sind sie reine Wiederholung.
Eine Cloud-Session startet aus einem frischen Clone. Was sie dabei tut, steuert ein SessionStart-Hook im Repo, bei uns .claude/hooks/session-start.sh. Er macht drei Dinge:
uv sync, npm ci, pre-commit install. Damit laufen in der Session dieselben Commit- und Push-Hooks wie lokal.PREPUSH_CI=full. Damit führt der Pre-Push-Hook alles aus, was sonst die PR-CI prüft: die ganze Test-Suite, den Linter über das gesamte Repository, den Format-Check und alle weiteren Validierungen.Das Ergebnis: Ein git push aus der Session dauert gut zwei Minuten statt ein paar Sekunden – und hat danach alles geprüft, was GitHub Actions prüfen würde. Der Agent wartet die zwei Minuten ab, man muss ihm nur einen längeren Timeout für den Push sagen. Ohne Variable, also auf dem eigenen Laptop, bleibt der schnelle Pre-Push mit den Tests, die der Push berührt.
Die Session-VM kostet nichts extra – Cloud-Sessions zählen laut Anthropic zu den normalen Nutzungslimits des Abos. Die zwei Minuten Prüfung verbrauchen kaum Tokens, der Agent wartet und liest am Ende die Ausgabe des Hooks. Und die Maschine ist stärker als das, was GitHub einem privaten Repository hinstellt:
|
Cloud-Session |
GitHub-Runner, privat |
|
|---|---|---|
|
CPU / RAM |
4 vCPU, 15 GB |
2 vCPU, 7 GB |
|
Volle Prüfung, unser Repo |
2:07 min |
5:48 min |
|
Kosten pro Lauf |
im Abo enthalten |
6 abgerechnete Minuten, plus Nebenjobs |
Session-Werte mit nproc und free in einer laufenden Session gemessen, Runner-Daten aus der GitHub-Doku. Öffentliche Repositories bekommen 4 vCPU und 16 GB kostenlos – wer Open Source macht, hat das Minutenproblem nicht.
Bleibt die eigentliche Frage: Wie erfährt die PR-CI, dass sie sich die Arbeit sparen kann?
Neu ist der Gedanke nicht. Im April 2024 hat David Heinemeier Hansson die Remote-CI von 37signals abgeschafft und die Prüfung zurück auf die Entwicklerrechner geholt: Die Pipeline für HEY brauchte fünfeinhalb Minuten, sein Linux-Rechner weniger als die Hälfte. Für Codebasen in der Größe von Shopify gelte das nicht, räumt er ein.
Daraus wurden Werkzeuge: Basecamps gh-signoff setzt nach lokalen Tests per gh signoff einen grünen Commit-Status, den ein Ruleset für den Merge verlangt; Rails 8.1 bringt jeder neuen App bin/ci mit optionalem Signoff mit.
Der andere Ansatz spielt die Pipeline lokal nach: act führt GitHub-Actions-Workflows in Docker aus, Dagger schreibt Pipelines als Code, der lokal und in CI gleich läuft. Das spart Push-und-warte-Zyklen, aber keine Minuten – die Cloud-CI läuft danach trotzdem.
Und ja, das widerspricht auf den ersten Blick dem Klassiker. Martin Fowler fordert in Continuous Integration, dass jeder Push auf einer Integrationsmaschine gebaut wird – gerade weil „bei mir läuft’s“ nichts beweist. Genau dieser Einwand wird uns weiter unten wieder begegnen. Wir haben ihn nicht wegdiskutiert, sondern bewusst einen Teil davon in Kauf genommen.
Die Session muss nach dem Pre-Push einen Nachweis hinterlassen, den ein kleiner Gate-Job in der PR-CI liest. Klingt trivial. Aber eine Cloud-Session ist abgeschottet: Git und GitHub-API laufen über einen Proxy, der die Zugangsdaten außerhalb der VM hält und nur bestimmte Operationen durchlässt. Daran sind drei von vier Ideen gescheitert, die vierte an Git selbst:
refs/notes/checks pushen. Der Git-Proxy nimmt nur Branch-Pushes an und lehnt laut Doku „Pushes von allem außer Branches“ ab. HTTP 403./statuses mit 403; ein eigener Token als Network-Secret hilft nicht, Secrets werden nie an GitHub-Requests gehängt. Auf einem Laptop mit eigenem gh-Login wäre das die richtige Wahl – in einer Agent-Umgebung nicht.checks/<sha>, den der Gate-Job liest und löscht. Geht vermutlich, aber: Namensregel, Rekursionsschutz, Schreibrechte für die CI, Aufräumen. Zu viele Teile.--amend in die Message geschrieben. Ein Pre-Push-Hook kann den Commit nicht mehr ändern, die SHAs stehen. Man bräuchte ein Push-Skript, das alle benutzen; wer es vergisst, fällt still auf die volle CI zurück.Zwei Dinge haben uns überrascht. Die 403 auf die Status-API kommt nicht von GitHub – die Antwort hatte weder Server: github.com noch X-GitHub-Request-Id, der Proxy hat sie selbst erzeugt; ein lesender Aufruf derselben API lieferte 200. Und diese Sperre steht nirgends, dokumentiert sind nur GraphQL und fremde Repositories. Wer sich wundert, warum sein Token nichts bewirkt: erst auf die Response-Header schauen.
Die entscheidende Wendung kam aus einer simplen Rückfrage: Warum schreiben wir den Trailer nicht vorher? Wir gehen davon aus, dass die Prüfung durchläuft. Schlägt sie fehl, findet kein Push statt, und wir reparieren.
Damit ändert sich die Bedeutung. Prepush-CI: full heißt nicht „diese Prüfung ist bestanden“, sondern „dieser Commit darf den Rechner nur nach der vollen Prüfung verlassen“. Der Hook setzt das durch. Kommt ein Commit mit Trailer auf GitHub an, muss die Prüfung bestanden sein, sonst wäre er nicht da.
PREPUSH_CI gesetzt, schreibt ein commit-msg-Hook mit git interpret-trailers den Trailer in die Message. --where start setzt ihn vor Co-Authored-By, --if-exists doNothing macht ihn idempotent.PRE_COMMIT_TO_REF an den Hook durch.HEAD ist oder der Baum schmutzig ist.changes-Job liest git log -1 --format='%(trailers:key=Prepush-CI,valueonly)' vom Head-SHA. Steht dort full, werden alle anderen Jobs übersprungen. Bei Pushes auf main wird der Trailer ignoriert, dort läuft immer alles.Das Ergebnis: keine zusätzlichen Refs, keine API-Schreibrechte, kein Wrapper, kein Aufräumen. Ein Rebase auf main ist automatisch abgedeckt: Die neuen Commits behalten den Trailer, also läuft beim nächsten Push wieder die volle Prüfung auf dem neuen Stand. Im ersten echten Pull Request meldete der Gate-Job VERIFIED: true, beide Workflows übersprangen alles außer dem changes-Job.
Natürlich kann jeder den Trailer von Hand schreiben. Das gilt für Note, Status und Branch genauso, und auch gh-signoff vertraut am Ende dem, der unterschreibt. Lokale CI ist ein Vertrauensmodell, kein Sicherheitsmodell.
Hier kommt Fowlers Einwand zurück. Der Pre-Push prüft den Branch; die PR-CI hätte den Probe-Merge mit aktuellem main geprüft. Wurde dort inzwischen eine Funktion umbenannt, die der Branch noch aufruft, sind beide Hälften grün und zusammen rot. In der Praxis ist die Lücke klein: Wer vor dem Merge auf main rebased, löst wegen des Trailers die volle Prüfung auf genau dem Stand aus, der gemergt wird. Nur wer GitHub rebasen lässt, umgeht die Hooks – dann wird main rot, der Lauf dort blockiert das Deployment, jemand bessert nach. Produktion bleibt heil. Schließen ließe sich die Lücke, indem das Gate nur bei aktuellem Branch überspringt, aber das kostet wieder volle Läufe für jeden veralteten. Wir zählen stattdessen, wie oft main deswegen rot wird.
Wir haben die Idee durch eine Software Factory laufen lassen. Herausgekommen ist prepush-ci, gebaut von fünf Claude-Code-Loops auf Spool – dem Hub für dauerhaft laufende Claude-Code-Agenten, den unser Kollege Enes Alataş entwickelt –, seit heute auf PyPI: uv tool install prepush-ci, oder uvx prepush-ci skill und den Agenten das Repo einrichten lassen – er fragt nach dem Prüfbefehl und öffnet einen Pull Request.
Dabei ist mehr entstanden als im Entwurf oben: Hook-Manager egal (native, pre-commit, husky, lefthook), Gate für GitHub Actions und GitLab CI, PREPUSH_CI=off plus prepush-ci strip als dokumentierter Weg, wenn ein Push ausnahmsweise ohne Prüfung gehen muss, doctor für kaputte Einrichtungen. Variable PREPUSH_CI=full, Trailer Prepush-CI: full. Version 0.1.0, Pre-Alpha. Agenten haben ein Werkzeug gebaut, damit Agenten billiger pushen.
Claude-Cloud-Sessions waren der Anlass, nicht der Punkt. Cloudroom gibt jedem Agenten eine eigene Sandbox mit Admin-Rechten, 100 Cloud-Agenten im Monat kostenlos; HumanLayer lässt Sessions auf Cloud-Daemons weiterlaufen, wenn der Laptop zu ist; Spool steckt jeden seiner Loops in einen eigenen Container. Überall dort läuft die volle Prüfung, bevor der Push abgeht. PREPUSH_CI=full in der Sandbox ist alles, was fehlt, damit die PR-CI das weiß.
Je mehr Pushes von Agenten kommen, desto schiefer wird die alte Rechnung. Fowlers Integrationsmaschine bleibt – für main, wo der zusammengeführte Stand geprüft wird. Auf dem Pull Request lief sie nie aus diesem Grund mit, sondern weil der gehostete Runner die einzige Maschine war, die jeder hatte. Die hat der Agent jetzt selbst.
Prepush-CI: full schon beim Commit und setzt die volle Prüfung beim Push durch. Kommt ein Commit mit Trailer an, muss sie bestanden sein – sonst wäre er nicht da.main läuft weiterhin alles, denn das ist die Integrationsmaschine, die zählt.main. Wer vorher rebased, löst die volle Prüfung erneut aus; wer es GitHub überlässt, riskiert ein rotes main – und merkt es vor dem Deployment.Git Squash Merge hilft nicht wirklich, saubere Commit-Historien zu erstellen. Es ist nur eine Krücke, um eine sinnvolle Commit-Aufteilung zu umgehen.
Datenbasierte Analysen zeigen, dass Collective Code Ownership Schaden anrichten kann und betroffene Codebereiche wahrscheinlich zu Warungs-Hotspots...
Praktischer Letifaden darüber, wie man durch die richtige Auswahl von Issue-Typen die Softwarequalität verbessert und ein effektives Monitoring...