Seit das Build-Logging über tee läuft, ist die Terminalausgabe vollständig. Dennoch meldet die CI einen eindeutig fehlgeschlagenen xcodebuild-Aufruf gelegentlich als erfolgreich. Gefährlicher als ein unvollständiges Protokoll ist dabei, dass die Pipeline anschließend mit Upload-, Benachrichtigungs- oder Deployment-Schritten fortfährt. Die Ursache ist meist simpel: Standardmäßig liefert die Shell den Exitcode des letzten Befehls einer Pipe zurück. Wird die Ausgabe von tee erfolgreich geschrieben, überdeckt dessen Status den vorangegangenen Fehler von xcodebuild.
Zuerst prüfen, wie Fehler verschluckt werden
Dieses Muster ist weit verbreitet:
xcodebuild test \
-workspace App.xcworkspace \
-scheme App \
-destination "$CI_DESTINATION" \
2>&1 | tee build.log
Die Pipe umfasst zwei Prozesse. Links werden die Tests ausgeführt, rechts wird die Ausgabe gleichzeitig in eine Datei und auf die Standardausgabe geschrieben. Ohne aktiviertes pipefail übernimmt Bash für den gesamten Befehl standardmäßig den Status von tee. Selbst wenn die Tests einen Wert ungleich null zurückgeben, kann das Skript weiterhin 0 sehen, solange das Protokoll erfolgreich geschrieben wurde.
Ein Minimaltest auf dem Cloud-Mac schafft Klarheit:
/bin/bash -c 'exit 65' | tee /tmp/pipeline-check.log
printf 'pipeline=%s\n' "$?"
Erscheint pipeline=0, kann das aktuelle Skript Fehler fälschlich als Erfolg melden. Es genügt nicht, das Protokoll nach error: zu durchsuchen, denn die Ausgabeformate von Compilern, Test-Frameworks und eigenen Skripten können sich ändern. Der Exitcode ist die stabile Schnittstelle, über die eine CI den Erfolg oder Fehlschlag eines Jobs bewertet.
Das Protokoll erklärt den Fehler, der Exitcode meldet ihn. Beides muss separat gesichert werden; eine Textsuche darf den Prozessstatus nicht ersetzen.
Shell festlegen und danach den Pipe-Status auswerten
Die interaktive Standardshell von macOS muss nicht mit dem Interpreter übereinstimmen, der das CI-Skript tatsächlich ausführt. Bash verwendet PIPESTATUS, zsh dagegen pipestatus; auch die Array-Indizierung unterscheidet sich. Am zuverlässigsten ist es, den Interpreter direkt im Repository-Skript festzulegen und die Pipeline diese Datei ausführen zu lassen, statt mehrzeilige Befehle an eine nicht eindeutig bestimmte Shell zu übergeben.
#!/bin/bash
set -uo pipefail
-u fängt nicht definierte Variablen ab. Mit pipefail liefert die gesamte Pipe einen Wert ungleich null, sobald einer ihrer Befehle fehlschlägt. pipefail allein ist jedoch noch nicht präzise genug: Scheitert tee wegen eines Datenträger- oder Berechtigungsproblems, während xcodebuild erfolgreich war, zeigt das abschließende $? lediglich einen Fehler der Pipe an. Welcher Teil fehlgeschlagen ist, bleibt offen.
Deshalb muss PIPESTATUS unmittelbar nach dem Ende der Pipe kopiert werden. Jede Zuweisung, Bedingungsprüfung oder Protokollausgabe kann den Inhalt aktualisieren. Erfolgt das Auslesen auch nur einen Befehl später, enthält das Array bereits den Status eines anderen Befehls.
Einen wiederverwendbaren xcodebuild-Wrapper erstellen
Das folgende Skript sichert gleichzeitig das ursprüngliche Protokoll, das Ergebnispaket und den tatsächlichen Exitcode von xcodebuild. errexit wird vorübergehend deaktiviert, damit die Shell nicht beendet wird, bevor die Statuswerte gespeichert sind.
#!/bin/bash
set -uo pipefail
run_id="${CI_RUN_ID:-local-$(date +%s)}"
artifact_dir="${CI_ARTIFACT_DIR:-$PWD/artifacts/$run_id}"
result_path="$artifact_dir/TestResults.xcresult"
log_path="$artifact_dir/xcodebuild.log"
mkdir -p "$artifact_dir"
set +e
xcodebuild test \
-workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-destination "$CI_DESTINATION" \
-resultBundlePath "$result_path" \
2>&1 | tee "$log_path"
pipeline_status=("${PIPESTATUS[@]}")
xcode_status="${pipeline_status[0]}"
tee_status="${pipeline_status[1]}"
set -e
printf 'xcodebuild_exit=%s\n' "$xcode_status"
printf 'tee_exit=%s\n' "$tee_status"
if (( tee_status != 0 )); then
printf 'Log capture failed\n' >&2
fi
exit "$xcode_status"
CI_DESTINATION sollte von der Jobkonfiguration explizit übergeben werden, beispielsweise fest an ein bereits geprüftes Simulatorziel gebunden. Das Skript sollte nicht unscharf nach dem ersten verfügbaren Gerät suchen. Nach einer Aktualisierung der Laufzeitumgebung könnte sonst eine andere Systemversion ausgewählt werden. Sämtliche Pfadvariablen stehen in Anführungszeichen, damit Verzeichnisse mit Leerzeichen nicht zur Aufteilung von Argumenten führen.
In diesem Beispiel bestimmt der Status von xcodebuild den endgültigen Jobstatus. Muss auch ein fehlgeschlagenes Schreiben des Protokolls den Job stoppen, ist vor dem Beenden eine zusätzliche Prüfung erforderlich: Sobald einer der beiden Werte ungleich null ist, wird ein Fehler zurückgegeben. Beide Statuswerte sollten außerdem getrennt in die Jobzusammenfassung geschrieben werden.
Das Ergebnispaket als Diagnosebeleg behandeln
Die Konsolenausgabe eignet sich zum schnellen Lesen. xcresult bewahrt dagegen die Testhierarchie, Anhänge zu Fehlschlägen, Aktivitätsprotokolle und Diagnoseinformationen auf. Da beide Artefakte unterschiedlichen Zwecken dienen, sollten sie gemeinsam archiviert werden.
| Artefakt | Hauptzweck | Bei Fehler aufbewahren |
|---|---|---|
xcodebuild.log |
Compiler-Ausgaben und Skriptprotokolle durchsuchen | Ja |
TestResults.xcresult |
Tests, Anhänge und Aktivitätsprotokolle untersuchen | Ja |
exit-status.txt |
Status der einzelnen Pipe-Stufen festhalten | Ja |
| Temporäre DerivedData | Kurzzeitig für eine lokale Reproduktion aufbewahren | Gemäß Jobrichtlinie |
Für eine maschinenlesbare Zusammenfassung steht das systemeigene xcresulttool zur Verfügung. Vor der Auswertung muss geprüft werden, ob das Ergebnispaket tatsächlich vorhanden ist. Andernfalls könnte ein Fehler beim Parsen den ursprünglichen Build-Status überdecken.
if [[ -d "$result_path" ]]; then
xcrun xcresulttool get test-results summary \
--path "$result_path" \
> "$artifact_dir/test-summary.json" || true
fi
printf 'xcodebuild=%s\ntee=%s\n' \
"$xcode_status" "$tee_status" \
> "$artifact_dir/exit-status.txt"
Das hier verwendete || true erlaubt lediglich, nach einem Fehler beim Erstellen der Zusammenfassung mit der Archivierung fortzufahren. Hinter dem eigentlichen xcodebuild-Befehl darf es nicht stehen. Ein Fehler in einem Hilfsbericht darf den bereits gesicherten Hauptstatus nicht löschen. Ebenso wenig darf ein Hilfsbefehl einen fehlgeschlagenen Haupt-Build nachträglich als erfolgreich erscheinen lassen.
Mit Fehlerszenarien abnehmen, nicht nur mit erfolgreichen Jobs
Vor dem Zusammenführen des Wrapper-Skripts sollten mindestens vier Szenarien geprüft werden:
Compilerfehler
Vorübergehend auf ein nicht vorhandenes Symbol verweisen. Erwartet werden ein xcodebuild-Exitcode ungleich null, ein fehlgeschlagener CI-Job und ein vorhandenes Protokoll. Ob ein Ergebnispaket angelegt wird, hängt davon ab, in welcher Phase der Fehler auftritt.
Fehlgeschlagener Test
Eine garantiert fehlschlagende Assertion hinzufügen. Die Kompilierung sollte abgeschlossen werden, der Testjob jedoch fehlschlagen. Der betroffene Testfall muss sich in xcresult lokalisieren lassen, und die Pipeline darf nicht mit dem Erfolgszweig fortfahren.
Fehler beim Schreiben des Protokolls
Das Artefaktverzeichnis auf einen nicht beschreibbaren Pfad setzen und prüfen, ob tee_status ungleich null ist. Verlangt das Team vollständige Diagnoseartefakte als Voraussetzung für die Auslieferung, muss auch dieses Szenario den Job stoppen.
Abbruch und Zeitüberschreitung
Die CI sendet ein Beendigungssignal. Danach dürfen keine Veröffentlichungsschritte mehr laufen, und es ist zu prüfen, ob die Kindprozesse beendet wurden. Eine Zeitüberschreitung darf nicht als gewöhnlicher Testfehler behandelt werden: Beide Fälle haben unterschiedliche Konsequenzen für Wiederholungsstrategien und Fehleranalysen.
Abschließend ist auch die Jobdefinition selbst zu prüfen. Das Hochladen von Diagnoseartefakten kann so konfiguriert werden, dass es selbst nach einem Fehler noch ausgeführt wird. Deployment, Verteilung und Erfolgsmeldungen müssen dagegen vom tatsächlichen Rückgabestatus des Wrapper-Skripts abhängen. So gelangt ein Fehler von xcodebuild weiterhin über einen einzigen, überprüfbaren Pfad zurück zur CI – selbst wenn sich Ausgabefilter, Archivierungswerkzeuge oder Zusammenfassungsgeneratoren ändern.
Häufig gestellte Fragen
Warum ist PIPESTATUS trotz aktiviertem pipefail erforderlich?
pipefail erkennt einen Fehler in der Pipeline, liefert aber nicht zwingend den Status von xcodebuild. PIPESTATUS[0] hält den Exitcode des ersten Befehls getrennt von tee und weiteren Filtern fest.
Wie lässt sich der Wrapper zuverlässig testen?
Erzeugen Sie einen Compilerfehler, einen fehlgeschlagenen Test und ein nicht beschreibbares Logverzeichnis. Jeder Fall muss den Job fehlschlagen lassen und die vorgesehenen Diagnoseartefakte erzeugen.
Verlagere deine nächste Entwicklungsaufgabe auf einen Cloud-Mac
Wähle Berry M4 oder Berry M4 Pro, prüfe die fünf Standorte, die Mietdauer und Zusatzoptionen und gib anschließend die Bestellung auf.