Wenn derselbe Cloud-Mac fortlaufend Build-Jobs ausführt, sind fehlgeschlagene Kompilierungen oft nicht das am schwersten zu erkennende Problem. Kritischer ist, wenn ein Job aufgrund des vom vorherigen Job hinterlassenen Zustands „zufällig erfolgreich“ ist: Abhängigkeiten befinden sich bereits im Cache, Hintergrunddienste belegen weiterhin Ports, temporäre Anmeldedaten wurden noch nicht entfernt oder benutzerspezifische Agents sind in einer grafischen Sitzung aktiv geblieben. Eine solche Pipeline wirkt kurzfristig stabil, doch beim Wechsel auf einen anderen Rechner oder nach einer Änderung der Job-Reihenfolge treten die Probleme geballt auf. Die Lösung besteht nicht darin, am Ende pauschal einen Löschbefehl anzuhängen. Stattdessen müssen zunächst klare Grenzen für Ausführungsidentität, Verzeichnisse, Caches und Sitzungsdomänen definiert und anschließend für jede Grenze Prüfungen eingerichtet werden.
Zuerst die erforderlichen Isolationsgrenzen definieren
Mindestens fünf Zustandsbereiche müssen geprüft werden: ausführender Benutzer, Arbeitsverzeichnis, temporäres Verzeichnis, Cache und Hintergrundprozesse. Der CI-Dienst selbst kann von einem systemweiten Daemon gestartet werden, die eigentlichen Builds sollten jedoch nicht dauerhaft mit Administratorrechten laufen. Robuster ist ein Standardbenutzer ohne Administratorrechte, beispielsweise ci_runner, unter dessen Identität Checkout, Kompilierung und Tests ausgeführt werden.
Das Ziel der Isolation besteht nicht darin, dass ein Job „den gesamten Rechner nicht sehen kann“. Vielmehr darf der von einem Job erzeugte veränderliche Zustand den nächsten Job nicht beeinflussen, sofern dies nicht ausdrücklich vorgesehen ist.
Erfassen Sie zunächst einen Ausgangszustand, damit spätere Untersuchungen einen Vergleichswert haben:
id
umask
printf 'HOME=%s\nTMPDIR=%s\n' "$HOME" "$TMPDIR"
launchctl print "gui/$(id -u)" >/tmp/launchctl-baseline.txt 2>&1 || true
ps -axo user,pid,ppid,command
Wenn der Runner einige wenige privilegierte Aktionen ausführen muss, sollten diese Befehle in einem kontrollierten Skript gebündelt und die Berechtigungen gezielt für die jeweiligen Aktionen vergeben werden. Vermeiden Sie es, den gesamten Build mit erhöhten Rechten auszuführen. Build-Skripte sollten außerdem weder systemweite Toolchains noch globale Netzwerkeinstellungen oder Verzeichnisse anderer Benutzer verändern.
Für jeden Job einen privaten Arbeitsbereich erstellen
Das Job-Verzeichnis sollte aus einer nicht vorhersagbaren, aber überprüfbaren Job-ID erzeugt werden. Schrägstriche, Leerzeichen und Steuerzeichen müssen unzulässig sein. Setzen Sie die Verzeichnisrechte auf 700 und legen Sie auch temporäre Dateien innerhalb des Job-Verzeichnisses ab, damit parallele Jobs keinen gemeinsamen temporären Systempfad verwenden.
set -eu
case "${RUN_ID:-}" in
""|*[!A-Za-z0-9._-]*)
echo "Invalid RUN_ID" >&2
exit 64
;;
esac
RUN_ROOT="/Users/ci_runner/Jobs/$RUN_ID"
install -d -m 700 "$RUN_ROOT"
install -d -m 700 "$RUN_ROOT/src" "$RUN_ROOT/tmp" "$RUN_ROOT/cache"
export HOME="/Users/ci_runner"
export TMPDIR="$RUN_ROOT/tmp/"
export XDG_CACHE_HOME="$RUN_ROOT/cache"
umask 077
Legen Sie den Arbeitsbereich nicht in einem gemeinsam genutzten Verzeichnis ab, in das alle Jobs schreiben dürfen. Falls ein schreibgeschützter Seed-Cache gemeinsam genutzt werden muss, kann dessen Inhalt zu Beginn des Jobs in einen lokalen Cache kopiert werden. Der Build verändert dann nur diese Kopie. So lassen sich vorgewärmte Daten nutzen, ohne dass ein fehlgeschlagener Job den gemeinsamen Ausgangszustand beschädigt.
Mit Berechtigungen statt Namenskonventionen isolieren
Ein Verzeichnisname mit Job-Nummer bietet noch keine sichere Isolation. Prüfen Sie Eigentümer und Berechtigungen mit stat -f '%Su %Sp %N' "$RUN_ROOT" und kontrollieren Sie zusätzliche ACLs mit ls -lde. Wenn vererbte Regeln vorhanden sind, muss zuerst ihre Herkunft geklärt werden. Nicht benötigte Berechtigungen sollten anschließend über einen kontrollierten Initialisierungsprozess entfernt werden. Die Rechte dürfen nicht rekursiv innerhalb des Build-Skripts gelockert werden.
Caches nach drei Lebensdauern trennen
Werden sämtliche Caches geleert, verlangsamt sich die Pipeline. Werden alle Caches wiederverwendet, steigt dagegen das Risiko einer Kontamination. In der Praxis bietet sich folgende Einteilung nach Lebensdauer an:
Ein Cache-Schlüssel sollte mindestens die Toolchain-Version, die Prüfsumme der Lockdatei für Abhängigkeiten und die Zielarchitektur enthalten. Wird nur der Branchname als Schlüssel verwendet, können nach einem Toolchain-Wechsel weiterhin alte Artefakte als Treffer gelten. Nach dem Wiederherstellen eines Caches ist eine kompakte Prüfung erforderlich, etwa der Prüfsumme der Lockdatei, der Architektur wichtiger Binärdateien und des Verzeichniseigentümers. Erfüllt die Kopie die Anforderungen nicht, sollte sie verworfen und nicht direkt am vorhandenen Inhalt repariert werden.
Sensible Daten gehören nicht in einen Cache. Kurzlebige Anmeldedaten sollten über die Job-Umgebung bereitgestellt werden und nur im Gültigkeitsbereich der Prozesse existieren, die sie tatsächlich benötigen. Auch Protokolle dürfen nicht die vollständige Umgebung ausgeben. Am Ende des Jobs müssen die betreffenden Variablen ausdrücklich mit unset entfernt werden. Prüfen Sie außerdem, dass erzeugte Dateien mit sensiblen Inhalten nicht in das Archivverzeichnis gelangt sind.
launchctl-Sitzungsdomänen richtig verstehen
Hintergrund-Agents unter macOS können zur Systemdomäne, zur Benutzerdomäne oder zur Domäne einer grafischen Sitzung gehören. Dass ein Build als ci_runner ausgeführt wird, bedeutet nicht automatisch, dass alle von ihm gestarteten Agents in der erwarteten Domäne laufen. Prüfen Sie zunächst die aktuelle Benutzer-ID und anschließend die Zieldomäne:
uid="$(id -u ci_runner)"
sudo -u ci_runner launchctl print "user/$uid" >/tmp/user-domain.txt
if launchctl print "gui/$uid" >/dev/null 2>&1; then
sudo -u ci_runner launchctl print "gui/$uid" >/tmp/gui-domain.txt
fi
Reine Kommandozeilen-Builds sind üblicherweise nicht von einer grafischen Sitzung abhängig. Für Tests mit Simulatoren oder grafischen Oberflächen muss dagegen zuerst geprüft werden, ob eine gültige Sitzung vorhanden ist. Eine fehlende Sitzung darf nicht durch wiederholtes Starten von Agents kaschiert werden. Für temporär vom Job gestartete Dienste ist die PID zu speichern. In der Abschlussphase sollte ihnen ein reguläres Beendigungssignal gesendet werden. Das pauschale Beenden anhand des Prozessnamens kann andere Jobs auf demselben Rechner beeinträchtigen.
Jobübergreifend verbliebene Prozesse erkennen
Erfassen Sie vor und nach dem Job jeweils einen ps-Snapshot und ordnen Sie die Prozesse anhand von Benutzer, Elternprozess und Arbeitsverzeichnis zu. Nur die belegten Ports zu prüfen, reicht nicht aus: Datei-Watcher und Test-Daemons ohne offenen Listening-Port verbrauchen ebenfalls Ressourcen. Prozesse, die nach Jobende weiterlaufen und deren Befehlszeile auf den aktuellen RUN_ROOT verweist, müssen als Fehler gelten und dürfen nicht stillschweigend ignoriert werden.
Fehlerfähige Abschlussprüfungen einrichten
Die Bereinigungsphase muss den ursprünglichen Exit-Code des Jobs erhalten und gleichzeitig Fehler bei der Bereinigung sichtbar machen. Es empfiehlt sich, die Prüfungen in einer festen Reihenfolge auszuführen:
- Vom Job gestartete Unterprozesse beenden und auf deren Abschluss warten.
- Den Arbeitsbereich auf Sockets, Mountpoints oder Dateien mit ungewöhnlichen Berechtigungen prüfen.
- Testberichte in ein kontrolliertes Archivverzeichnis außerhalb des Arbeitsbereichs kopieren.
- Temporäre Anmeldedaten und Umgebungsvariablen des Jobs entfernen.
- Das Job-Verzeichnis erst nach Prüfung des Pfadpräfixes löschen.
- Prozesse und launchctl-Zustand erneut erfassen und mit dem Ausgangszustand vergleichen.
Vor dem Löschen ist der Pfad unbedingt abzusichern:
cleanup() {
case "$RUN_ROOT" in
/Users/ci_runner/Jobs/*)
rm -rf -- "$RUN_ROOT"
;;
*)
echo "Refusing unsafe cleanup path" >&2
return 1
;;
esac
}
trap cleanup EXIT HUP INT TERM
Das Prü-Skript darf nicht alle Fehler unterdrücken, nur damit der Job erfolgreich erscheint. Fehler beim Archivieren, verbliebene Prozesse und Änderungen des Verzeichniseigentümers müssen zu einem eindeutigen Status ungleich null führen. Bei kontinuierlich auf SoarMac ausgeführter CI sollten Sie zunächst in der Konsole die aktuell verfügbaren Konfigurationen prüfen und anschließend anhand der Speicher- und Festplattenbelastung paralleler Jobs entscheiden, ob die Runner aufgeteilt werden müssen. Unabhängig von der gewählten Konfiguration müssen die Grenzen für Identität und Zustand identisch bleiben.
Checkliste vor der Inbetriebnahme
Führen Sie bei der ersten Integration zwei Jobs mit unterschiedlichen Inhalten, aber identischen Schritten direkt nacheinander aus. Lassen Sie den ersten Job absichtlich Cache-Dateien, einen Hintergrundprozess und temporäre Variablen erzeugen. Der zweite Job darf weder nicht deklarierte Dateien lesen noch Prozesse oder Anmeldedaten des vorherigen Jobs übernehmen können. Prüfen Sie abschließend folgende Punkte:
- Der Runner verwendet einen dedizierten Benutzer ohne Administratorrechte.
- Jedes Job-Verzeichnis hat die Berechtigung
700, und sein Pfad wurde einer Formatprüfung unterzogen. - Temporäre Verzeichnisse und beschreibbare Caches werden standardmäßig nicht jobübergreifend gemeinsam genutzt.
- Rechnerbezogene Seed-Caches sind für den Build-Benutzer schreibgeschützt.
- Für grafische Tests ist eindeutig festgelegt, von welcher launchctl-Sitzungsdomäne sie abhängen.
- Unterprozesse werden anhand ihrer PID oder ihrer Beziehung zum Job beendet, nicht pauschal anhand ihres Namens.
- Das Löschen erfolgt erst nach erfolgreicher Archivierung.
- Fehler bei der Bereinigung lassen den Job fehlschlagen, statt nur eine Protokollzeile zu erzeugen.
Wenn sich diese Prüfungen zuverlässig wiederholen lassen, beruht der Erfolg der Pipeline auf ausdrücklich deklarierten Eingaben und nicht auf einem zufällig auf dem Rechner verbliebenen Zustand. Werden später Parallelität, Job-Zuordnung oder Ausführungsverzeichnisse geändert, lässt sich mit denselben Grenzen schnell feststellen, woher die jeweiligen Abweichungen stammen.
Häufig gestellte Fragen
Benötigt jeder CI-Job einen eigenen macOS-Benutzer?
In der Regel nicht. Beginnen Sie mit einem Standardbenutzer ohne Administratorrechte für den Runner und einem Verzeichnis mit Modus 700 pro Job. Weitere Benutzer sind nur bei nicht vertrauenswürdigen Teams oder unterschiedlichen Schutzstufen nötig.
Warum reicht das Löschen des Arbeitsverzeichnisses nicht aus?
Ein Job kann Zustand in temporären Verzeichnissen, Benutzer-Caches, Zugangsdaten, Hintergrundprozessen und seiner launchctl-Sitzung hinterlassen. Beim Abschluss müssen alle Grenzen geprüft und nur validierte Job-Pfade gelöscht werden.
Wählen Sie den passenden Cloud-Mac für Ihre Workloads
Prüfen Sie Chip, Arbeitsspeicher, Speicherplatz, Mietdauer und Standort und bestellen Sie einen exklusiven physischen Apple-Silicon-Server.