Wenn ein Build im Remote-Terminal problemlos läuft, ein CI-Job jedoch mit command not found, einem falschen Arbeitsverzeichnis oder einem nur unter launchd fehlschlagenden Skript endet, ist meist nicht das Werkzeug selbst defekt. In der Regel erhalten die drei Ausführungswege unterschiedliche Umgebungen: Eine interaktive Shell liest die Benutzerkonfiguration, eine nicht interaktive Shell nur einen Teil davon, und launchd startet mit einer deutlich reduzierten Umgebung. Die Lösung besteht daher nicht darin, immer weitere PATH-Einträge an Konfigurationsdateien anzuhängen. Stattdessen sollten alle automatisierten Aufgaben über einen einzigen, eindeutig definierten Einstiegspunkt laufen.
Zuerst die Umgebungen aller drei Einstiegspunkte erfassen
Ändern Sie nicht sofort die Konfiguration. Erfassen Sie zunächst im Remote-Terminal, im CI-Job und unter launchd dieselben Informationen. So lässt sich feststellen, ob die Abweichung beim PATH, beim aktuellen Arbeitsverzeichnis oder bei der Auswahl der Werkzeuge entsteht.
#!/bin/zsh
set -eu
printf 'user=%s
' "$(id -un)"
printf 'uid=%s
' "$(id -u)"
printf 'shell=%s
' "${SHELL:-unset}"
printf 'home=%s
' "${HOME:-unset}"
printf 'pwd=%s
' "$PWD"
printf 'path=%s
' "${PATH:-unset}"
for tool in zsh git xcodebuild ruby python3; do
printf '%s=' "$tool"
command -v "$tool" || printf 'missing
'
done
/usr/bin/xcode-select -p 2>/dev/null || true
/usr/bin/sw_vers
Speichern Sie das Skript als scripts/inspect-env.sh, führen Sie es über alle drei Einstiegspunkte aus und bewahren Sie die jeweiligen Ausgaben getrennt auf. Verwenden Sie bei Produktionsjobs nicht einfach das vollständige Ergebnis von env, da es Token oder temporäre Zugangsdaten enthalten kann.
Wenn bei zwei Ausführungen unterschiedliche absolute Pfade für ein Werkzeug aufgelöst werden, liegt Umgebungsdrift vor – auch wenn die Versionsnummern aktuell noch übereinstimmen. Nach einem späteren Upgrade können die beiden Pfade unterschiedliche Ergebnisse liefern.
Das Problem mit einer minimalen Umgebung reproduzieren
Mit env -i lässt sich ein Job simulieren, der keine benutzerspezifische Komfortkonfiguration lädt:
/usr/bin/env -i \
HOME="$HOME" \
USER="$USER" \
PATH="/usr/bin:/bin:/usr/sbin:/sbin" \
/bin/zsh ./scripts/inspect-env.sh
Lässt sich das Problem damit zuverlässig reproduzieren, müssen die Werkzeuge nicht wiederholt neu installiert werden. Prüfen Sie stattdessen das Einstiegsskript und die PATH-Definition.
Ein einziges Einstiegsskript für alle Jobs einrichten
Automatisierte Jobs sollten nicht von .zshrc abhängen. Diese Datei ist für interaktive Sitzungen gedacht und enthält häufig Prompt-Konfigurationen, Aliasse, Terminalprüfungen und Logik, die nur in einer Login-Sitzung funktioniert. Legen Sie stattdessen ein separates Einstiegsskript im Repository an, beispielsweise scripts/run-ci.sh:
#!/bin/zsh
set -euo pipefail
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
export LANG="en_US.UTF-8"
export LC_ALL="en_US.UTF-8"
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
cd "$repo_root"
required_tools=(git xcodebuild)
for tool in "${required_tools[@]}"; do
if ! command -v "$tool" >/dev/null 2>&1; then
printf 'required tool missing: %s
' "$tool" >&2
exit 127
fi
done
exec ./scripts/build.sh
Die Reihenfolge im PATH muss fest definiert sein, und die Systemverzeichnisse müssen enthalten bleiben. Falls der Job weitere Werkzeuge benötigt, ermitteln Sie zunächst mit command -v deren tatsächlichen Speicherort auf dem Mac. Ergänzen Sie anschließend das Einstiegsskript, anstatt die gesamte Benutzerkonfiguration zu kopieren.
Das Einstiegsskript wechselt außerdem in das Stammverzeichnis des Repositorys. Relative Pfade in Build-Skripten hängen dadurch weder vom temporären Verzeichnis des CI-Executors noch vom Standardverzeichnis von launchd ab.
launchd ausschließlich zum Starten verwenden
Die launchd-Konfiguration sollte möglichst einfach bleiben: Sie legt den Interpreter, das Einstiegsskript, das Arbeitsverzeichnis und die Protokolldateien fest. Komplexe Befehle sollten nicht in der plist zusammengesetzt und vertrauliche Werte dort nicht gespeichert werden.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "file://localhost/System/Library/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.ci-runner</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>/Users/runner/project/scripts/run-ci.sh</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/runner/project</string>
<key>StandardOutPath</key>
<string>/Users/runner/Library/Logs/ci-runner.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/runner/Library/Logs/ci-runner.err.log</string>
</dict>
</plist>
Prüfen Sie vor dem Laden zunächst die Syntax:
plutil -lint ~/Library/LaunchAgents/com.example.ci-runner.plist
launchctl bootstrap "gui/$(id -u)" \
~/Library/LaunchAgents/com.example.ci-runner.plist
launchctl kickstart -k \
"gui/$(id -u)/com.example.ci-runner"
Nachdem Sie die plist geändert haben, entfernen Sie den vorhandenen Job zuerst mit bootout und laden ihn anschließend erneut mit bootstrap. Ein alleiniger Aufruf von kickstart aktualisiert eine bereits geladene alte Konfiguration nicht automatisch.
Werkzeugversionen und vertrauliche Variablen verwalten
Fest definierte Pfade garantieren noch keine festen Versionen. Zu Beginn eines Jobs sollte das Einstiegsskript nicht vertrauliche Informationen wie Werkzeugversionen, aufgelöste Pfade und das Arbeitsverzeichnis ausgeben. Damit lassen sich fehlgeschlagene Jobs vergleichen, ohne sämtliche Umgebungsvariablen zu protokollieren.
| Prüfpunkt | Empfohlene Vorgehensweise | Bei Fehlern aufbewahren |
|---|---|---|
| Werkzeugauflösung | command -v |
Absoluter Pfad des Werkzeugs |
| Werkzeugversion | Offiziellen Versionsparameter aufrufen | Versionsausgabe |
| Arbeitsverzeichnis | Explizit mit cd in das Repository-Stammverzeichnis wechseln |
Ausgabe von pwd |
| System-Toolchain | Aktuelles Entwicklerverzeichnis prüfen | Pfad und Exitcode |
| Vertrauliche Variablen | Nur prüfen, ob sie vorhanden sind | Variablenname, nicht den Wert |
Vertrauliche Werte sollten zur Laufzeit über einen kontrollierten Zugangsdatenprozess bereitgestellt werden. Ein Skript kann mit ${TOKEN:?TOKEN is required} sofort abbrechen, wenn ein Wert fehlt. Der Inhalt darf jedoch weder durch set -x oder env noch über eine Fehlermeldung offengelegt werden. Prüfen Sie nach der Diagnose außerdem, ob Protokolle versehentlich Request-Header, Befehlsargumente oder Pfade temporärer Dateien enthalten.
Das Problem mit einer Abnahmematrix abschließen
Nach der Korrektur muss dasselbe Skript über alle drei Einstiegspunkte ausgeführt werden. Ein Test ausschließlich im aktuellen Terminal reicht nicht aus.
- Führen Sie
./scripts/run-ci.shdirekt in einem interaktiven Terminal aus. - Starten Sie es mit
env -iund übergeben Sie dabei nur HOME, USER und einen grundlegenden PATH. - Starten Sie den Job mit
launchctl kickstartund prüfen Sie den Exitstatus sowie beide Protokolldateien. - Vergleichen Sie bei allen drei Ausführungen das Repository-Stammverzeichnis, die absoluten Werkzeugpfade, die Versionen und die Exitcodes.
- Starten Sie die Benutzersitzung einmal neu und führen Sie den Test erneut aus, um auszuschließen, dass der Job von temporär exportierten Variablen abhängt.
Ein häufiger Fehler besteht darin, die Korrektur in .zshrc einzutragen. Manuelle Tests funktionieren dann wieder, während der CI-Job weiterhin fehlschlägt. Ebenso problematisch ist es, einen separaten PATH in der plist zu pflegen, der nach einigen Wochen erneut vom Repository-Skript abweicht. Die robustere Aufgabentrennung lautet: Der Scheduler startet nur den Job, das Einstiegsskript definiert die Umgebung, und das eigentliche Build-Skript führt den Build aus. Sind diese drei Verantwortungsbereiche getrennt, lassen sich Umgebungsunterschiede protokollieren, reproduzieren und überprüfen.
Wenn Sie dieses Verfahren auf einem Remote-Mac von OwnAMac einrichten, prüfen Sie vor dem Erzeugen der plist ebenfalls zuerst den tatsächlich verwendeten Benutzer und den Repository-Pfad. Übernehmen Sie den Benutzernamen aus dem Beispiel nicht unverändert. Das endgültige Abnahmekriterium lautet nicht, dass der Build „im Terminal funktioniert“. Sowohl die minimale Umgebung als auch die Scheduler-Umgebung müssen dieselben Werkzeuge auflösen und konsistente Ergebnisse zurückgeben.
Häufig gestellte Fragen
Warum findet CI einen Befehl nicht, der im Terminal verfügbar ist?
Die interaktive Shell lädt häufig Benutzerprofile und erweitert PATH. Nicht interaktive Jobs und launchd tun das nicht zuverlässig. PATH und benötigte Variablen sollten deshalb im Einstiegsskript ausdrücklich gesetzt werden.
Dürfen Zugangstoken in einer launchd-plist stehen?
Nein, sensible Werte sollten nicht dauerhaft in der plist liegen. Sie werden besser zur Laufzeit über einen kontrollierten Prozess eingebunden, während das Skript nur ihre Existenz prüft.
Wie lässt sich die Korrektur belastbar prüfen?
Führen Sie dasselbe Prüfsystem interaktiv, in einer mit env -i reduzierten Umgebung und über launchd aus. Vergleichen Sie PATH, Arbeitsverzeichnis, absolute Werkzeugpfade und Exitcodes.
Den nächsten Build auf einem exklusiven physischen Apple-Silicon-Knoten ausführen
Wählen Sie je nach Aufgabenumfang aus drei Konfigurationen und fünf Knoten. Rechenleistung und Speicher werden nicht mit anderen Mietern geteilt. Alle Knoten laufen 365 Tage im Jahr zuverlässig; der tatsächlich verfügbare Status wird in Echtzeit von der Konsole zurückgegeben.