Nur kurz die Ausgabe ansehen: wie ein Schreibskript zweimal lief
Ein Stapelskript lief sauber durch. Danach wollte ich nur noch die Ausgabe ansehen, startete es dabei ein zweites Mal und hatte neun doppelte Kommentare. Was ein Trockenlauf nicht abdeckt und wie Schreibskripte einen zweiten Lauf aushalten.
Ein Skript sollte auf einer Liste von Issues je einen Belegkommentar setzen, Labels ändern und einige Karten schließen. Es lief einmal sauber durch. Danach wollte ich mir nur noch die Ausgabe ansehen und hatte damit neun Kommentare doppelt geschrieben.
Die Ausgangslage
Das Skript war eigentlich vorsichtig gebaut. Ohne Schalter macht es einen Trockenlauf und zeigt nur, was passieren würde. Erst mit --apply spricht es mit der API. Der Plan lag als JSON-Datei daneben: je Karte ein Kommentar, Labels dazu oder weg, eventuell schließen. Gegen Fehler im Plan schützt so ein Trockenlauf gut. Gegen das, was dann passierte, schützt er nicht.
Der echte Lauf ging fehlerfrei durch. Dann wollte ich noch die Gegenprobe sehen und schrieb dafür eine Zeile, die ich für einen Anzeigebefehl hielt:
python3 triage_apply.py --apply 2>&1 | head -25 > /dev/null; <gegenprobe>Gedacht war das als „Ausgabe kürzen und weiter“, wobei > /dev/null sie sogar ganz verschluckt. Tatsächlich ist es ein zweiter vollständiger Schreiblauf mit --apply. Das | head zeigt das alte Ergebnis nicht an. Es startet das Programm neu und schaut ihm beim Arbeiten zu.
Warum es bei neun blieb
Den Abbruch hat die Pipe ausgelöst. head -25 liest 25 Zeilen und beendet sich. Das Skript schreibt weiter in eine Pipe, an deren anderem Ende niemand mehr liest. Weil Python in eine Pipe gepuffert schreibt, merkt es das nicht sofort, sondern erst beim nächsten Leeren des Puffers. Dann kommt ein BrokenPipeError, und das Skript bricht ab. Wo genau das passiert, hängt also am Puffer und nicht an den 25 Zeilen. Bis dahin hatte der zweite Lauf die ersten neun Karten des Plans abgearbeitet.
Je Operation wirkt ein zweiter Lauf unterschiedlich:
- Label hinzufügen: idempotent. Ist das Label schon da, ändert sich nichts.
- Label entfernen: beim zweiten Mal kommt ein Fehlercode zurück, sonst passiert nichts.
- Issue schließen: ein geschlossenes Issue noch einmal zu schließen, schadet nicht.
- Kommentar schreiben: nicht idempotent. Jeder Aufruf erzeugt einen neuen Kommentar, byte-gleich mit dem vorigen.
Für die Doppelkommentare kann die Pipe nichts, die gingen aufs Konto des zweiten Laufs. Dass es nicht mehr wurden, lag dagegen an der Pipe. Ohne head wäre das Skript bis zum Ende gelaufen und hätte am Schluss auch die neuen Karten ein zweites Mal angelegt. Die Pipe hat also Schlimmeres verhindert, nur aus dem falschen Grund. Darauf sollte man sich nicht verlassen.
Die Reparatur
Doppelte Kommentare lassen sich über die API gezielt löschen. Man holt die Kommentare jeder betroffenen Karte, sucht die Paare gleicher Texte und löscht die Dublette mit DELETE /repos/{owner}/{repo}/issues/comments/{id}, Antwort 204. Sinnvoll ist, jeweils den jüngeren zu löschen, denn auf den älteren hat vielleicht schon jemand reagiert. Danach gehört eine Zählung dazu: je Karte genau ein Belegkommentar.
Das ging schnell. Wer das Repo beobachtet, hat aber in der Zwischenzeit womöglich neun Benachrichtigungen mehr bekommen, und die lassen sich nicht zurückholen.
Was ich daraus ableite
Die erste Regel betrifft die Shell: Einen Schreiblauf starte ich genau einmal und ohne Pipe. Die Ausgabe geht in eine Datei, und angesehen wird dann nur diese Datei.
python3 triage_apply.py --apply > lauf_apply.log 2>&1
echo "rc=$?"
head -25 lauf_apply.logDamit ist head wieder das, wofür ich es gehalten hatte: ein Lesebefehl. Eine Gewohnheit ist trotzdem kein Schutz. Beim nächsten Mal stehen dieselben Wörter in einer etwas anderen Zeile, und man liest sie wieder als Anzeigebefehl.
Die zweite Regel sollte deshalb ins Skript: Jede nicht idempotente Schreiboperation prüft vorher, ob sie schon passiert ist. Für Kommentare reicht dazu eine unsichtbare Markierung im Text, etwa ein HTML-Kommentar mit einer festen Kennung des Plans. Eine Kennung je Lauf wäre falsch, denn dann fände ein neuer Lauf die Kommentare des alten nicht wieder. Vor dem Schreiben sucht das Skript diese Markierung in den vorhandenen Kommentaren:
MARKE = "<!-- triage-plan:abnahme-1 -->"
def kommentar_einmal(nr, text):
vorhanden = api.list_comments(nr, alle_seiten=True)
if any(MARKE in k["body"] for k in vorhanden):
print(f"#{nr}: Kommentar schon da, übersprungen")
return
api.comment(nr, text + "\n\n" + MARKE)Das kostet je Karte einen zusätzlichen Lesezugriff. Dafür schreibt das Skript keinen Kommentar doppelt, wenn es erneut startet, vorausgesetzt, es liest wirklich alle Seiten der Kommentarliste (die GitHub-API liefert ohne Seitenabruf nur die ersten 30). Das gilt, ob der Neustart aus Versehen, nach einem Abbruch in der Mitte oder deshalb kommt, weil ein Agent den Auftrag wiederholt. Gerade der letzte Fall ist bei automatisierten Abläufen nicht theoretisch: Wird ein Lauf neu gestartet, beginnt er sonst von vorn, mit allem, was schon erledigt war.
Die dritte Regel betrifft das Neuanlegen von Objekten: Vor dem Anlegen wird nach einem Objekt mit demselben Titel oder derselben Markierung gesucht. Das ist dieselbe Idee wie beim Kommentar, nur teurer, wenn man sie vergisst.
Der Trockenlauf reicht nicht
Bisher hatte ich den Trockenlauf als die Sicherung eines Stapelskripts betrachtet. Er beantwortet aber nur die Frage, ob der Plan stimmt. Ob es schlimm ist, wenn der Plan zweimal ausgeführt wird, beantwortet er nicht. Diese zweite Frage stellt sich bei jedem Skript, das man mehr als einmal aufrufen kann, also bei allen.
Fazit
Ein Schreibskript, dessen Ausgabe man noch einmal sehen will, startet man nicht noch einmal, man liest sein Protokoll. Und weil man genau das irgendwann trotzdem tut, sollte das Skript den zweiten Lauf aushalten: Es prüft vorher, was schon da ist, und überspringt es. Bei mir hat ein head das zufällig abgefangen, und darauf möchte ich kein Verfahren aufbauen.
- #Idempotenz
- #Shell
- #GitHub API
- #Stapelskripte
- #Trockenlauf
- #Python
Weiterlesen
Leitplanken für Agenten: eine Regel, die man lesen muss, ist keine Leitplanke
17. Oktober 2026 · KI-Agenten, Automatisierung, Engineering · 5 Min. Lesezeit
Der Rückweg zerstört die Messung: wie git checkout meine Mutationsbatterie entwertete
9. November 2026 · Engineering, Testing, KI-Agenten · 4 Min. Lesezeit
Die Frist, die den Schlüssel wegwirft: Timeouts um einen OAuth-Token-Tausch
2. November 2026 · KI-Agenten, Engineering, Integration · 5 Min. Lesezeit