SMB-Freigaben in Cloud-Mac-CI ohne beschädigte Builds nutzen

SMB-Freigaben in Cloud-Mac-CI ohne beschädigte Builds nutzen

Nachdem ein CI-Knoten die Archivierung abgeschlossen hat, müssen die Artefakte an nachgelagerte Test-, Veröffentlichungs- oder Aufbewahrungsjobs übergeben werden. Naheliegend wäre es, eine SMB-Freigabe direkt als Arbeitsverzeichnis zu verwenden, sodass mehrere Cloud-Macs dasselbe Projekt lesen und bearbeiten können. Bei kleinen Repositorys mag das vorübergehend funktionieren. Sobald jedoch das Netzwerk instabil wird, Dateisperren verzögert greifen oder der Mount ausfällt, können Xcode-Zwischendateien beschädigt werden. Im schlimmsten Fall landen die Ergebnisse in einem gleichnamigen, leeren lokalen Verzeichnis.

Die zuverlässigere Abgrenzung lautet: SMB nimmt ausschließlich Eingabe-Snapshots und fertige Artefakte auf. Das Entpacken des Quellcodes, die Auflösung von Abhängigkeiten, DerivedData und temporäre Dateien bleiben vollständig auf dem lokalen Datenträger.

Zuerst drei Verzeichniskategorien trennen

Jeder Job benötigt mindestens drei Pfade, die nicht miteinander vermischt werden dürfen:

  • Schreibgeschützter Eingabebereich: enthält Snapshots des Quellcodes oder Abhängigkeitspakete mit festgelegter Version.
  • Lokales Arbeitsverzeichnis: enthält den ausgecheckten Quellcode, DerivedData, Testergebnisse und temporäre Dateien.
  • Entfernter Übergabebereich: enthält ausschließlich bereits geprüfte Archive, Protokollzusammenfassungen und Manifeste.

Zwei gleichzeitig ausgeführte Jobs dürfen niemals dasselbe DerivedData-Verzeichnis verwenden. Selbst wenn beide denselben Commit bauen, können Index, Modul-Cache und Build-Datenbank gleichzeitig verändert werden. Die Job-ID sollte daher Bestandteil des Pfads sein, beispielsweise ~/ci-work/$RUN_ID. Nach Abschluss des Jobs wird das Verzeichnis entsprechend der Aufbewahrungsrichtlinie bereinigt.

Eine Netzwerkfreigabe ist als Übergabegrenze zu behandeln, nicht als transparenter Ersatz für einen lokalen Datenträger. Ein erfolgreicher Build und ein erfolgreicher Upload müssen außerdem zwei voneinander unabhängige Zustände sein.

Den SMB-Mount durch einen dauerhaften Prozess verwalten

Unbeaufsichtigte Jobs dürfen Benutzernamen und Passwörter nicht in Befehlszeilenargumente einbetten. Stattdessen sollte ein kontrollierter, dauerhaft laufender Prozess die SMB-Freigabe vorab mounten und die Zugangsdaten aus einem separaten Schlüsselbund oder einer zugriffsbeschränkten Konfiguration lesen. Reguläre Build-Jobs erhalten nur die minimal erforderlichen Schreibrechte für das Zielverzeichnis.

Zu Beginn eines Jobs reicht es nicht aus, lediglich zu prüfen, ob das Verzeichnis vorhanden ist. Nach einer SMB-Trennung kann der Mountpunkt zu einem gewöhnlichen lokalen Verzeichnis zurückfallen. Mindestens geprüft werden müssen der Dateisystemtyp, die Schreibberechtigung und ein Schreib-Lese-Test mit einer kleinen Datei:

set -euo pipefail

SHARE_ROOT="/Volumes/ci-artifacts"
PROBE="$SHARE_ROOT/.probe-${RUN_ID}"

fs_type="$(stat -f '%T' "$SHARE_ROOT")"
test "$fs_type" = "smbfs"
test -w "$SHARE_ROOT"

printf '%s\n' "$RUN_ID" > "$PROBE"
test "$(cat "$PROBE")" = "$RUN_ID"
rm -f "$PROBE"

Schlägt einer dieser Schritte fehl, muss der Job abgebrochen werden. Der Mountpunkt darf nicht automatisch erstellt und der Job anschließend fortgesetzt werden. Andernfalls kann eine unterbrochene Freigabe dazu führen, dass der Build als erfolgreich gilt, die Artefakte aber ausschließlich auf dem lokalen Rechner liegen.

Build-Aktivitäten auf den lokalen Datenträger beschränken

Synchronisieren Sie zunächst den festgelegten Commit in ein lokales temporäres Verzeichnis. Führen Sie erst danach die Auflösung der Abhängigkeiten und den Build aus. Jobverzeichnis, DerivedData und Ergebnispaket müssen jeweils eine eindeutige Job-ID enthalten, damit parallele Jobs ihre Daten nicht gegenseitig überschreiben.

LOCAL_ROOT="$(mktemp -d "$TMPDIR/vmcache-ci.XXXXXX")"
trap 'rm -rf "$LOCAL_ROOT"' EXIT

rsync -a --delete "$SOURCE_SNAPSHOT/" "$LOCAL_ROOT/repo/"

xcodebuild \
  -workspace "$LOCAL_ROOT/repo/App.xcworkspace" \
  -scheme App \
  -configuration Release \
  -derivedDataPath "$LOCAL_ROOT/DerivedData" \
  -resultBundlePath "$LOCAL_ROOT/TestResults.xcresult" \
  build

SOURCE_SNAPSHOT muss hier einem unveränderlichen Commit entsprechen und darf kein aktives Verzeichnis sein, das andere Jobs weiterhin verändern können. Stammt die Eingabe von einem gemeinsam genutzten Volume, sollte vor Beginn der Synchronisierung zunächst die Commit-ID oder das Manifest geprüft werden.

Nicht das gesamte DerivedData-Verzeichnis zwischenspeichern

Das vollständige DerivedData-Verzeichnis zwischen Jobs zu kopieren, lohnt sich meist nicht: Der Cache ist groß, während sich interne Pfade und Build-Einstellungen ändern können. Wartungsfreundlicher ist es, nur solche Download-Verzeichnisse für Abhängigkeiten zwischenzuspeichern, die nachweislich wiederverwendbar sind. Xcode-Version, Architektur und Prüfsumme der Lockdatei gehören dabei in den Cache-Schlüssel. Stimmt der Schlüssel nicht überein, wird der Cache neu erzeugt, statt ihn unscharf wiederzuverwenden.

Artefakte unter einem temporären Namen atomar veröffentlichen

Wird eine Datei direkt unter ihrem endgültigen Namen kopiert, ist ein unvollständiges Artefakt sichtbar. Nachgelagerte Jobs können die Datei sofort nach ihrem Erscheinen lesen, obwohl die Übertragung noch läuft. Die korrekte Reihenfolge lautet: lokal paketieren, lokal die Prüfsumme berechnen, unter einem temporären Namen auf das freigegebene Volume kopieren, dort überprüfen und schließlich im selben Verzeichnis umbenennen.

ARTIFACT="$LOCAL_ROOT/App-release.zip"
ditto -c -k --norsrc "$LOCAL_ROOT/DerivedData/Build/Products/Release" "$ARTIFACT"

REMOTE_DIR="$SHARE_ROOT/releases/$GIT_COMMIT"
REMOTE_TMP="$REMOTE_DIR/.App-release.zip.${RUN_ID}.partial"
REMOTE_FINAL="$REMOTE_DIR/App-release.zip"

mkdir -p "$REMOTE_DIR"
cp "$ARTIFACT" "$REMOTE_TMP"

local_hash="$(shasum -a 256 "$ARTIFACT" | awk '{print $1}')"
remote_hash="$(shasum -a 256 "$REMOTE_TMP" | awk '{print $1}')"
test "$local_hash" = "$remote_hash"

mv "$REMOTE_TMP" "$REMOTE_FINAL"
printf '%s  %s\n' "$remote_hash" "App-release.zip" \
  > "$REMOTE_DIR/SHA256SUMS.${RUN_ID}"

Eine atomare Umbenennung setzt voraus, dass sich die temporäre und die endgültige Datei auf demselben freigegebenen Volume befinden, idealerweise auch im selben Verzeichnis. Die Datei darf nicht zunächst lokal geschrieben und anschließend mit mv über Volume-Grenzen hinweg verschoben werden. Ein solches Verschieben wird zu einem Kopier- und Löschvorgang.

Fehler eindeutig erkennbar und bereinigbar machen

Nach einem fehlgeschlagenen Upload sollten die lokalen Build-Ergebnisse für einen kontrollierten Zeitraum aufbewahrt werden, damit die Übertragung wiederholt werden kann. Der Job darf dabei jedoch nicht als vollständig veröffentlicht markiert werden. Es empfiehlt sich, build_completed, artifact_verified und publish_completed getrennt zu protokollieren. Nachgelagerte Jobs dürfen nur den letzten Zustand akzeptieren.

Auch .partial-Dateien, deren Aufbewahrungsfrist abgelaufen ist, müssen regelmäßig entfernt werden. Der Bereinigungsjob muss das Format des Dateinamens und den Änderungszeitpunkt prüfen und darf ausschließlich Dateien mit dem temporären Suffix verarbeiten. Endgültige Artefakte dürfen weder durchsucht noch gelöscht werden. Wird das freigegebene Volume schreibgeschützt, stimmen Prüfsummen nicht überein oder bricht die Verbindung wiederholt ab, muss die Veröffentlichungswarteschlange angehalten werden. Zunächst sind die lokalen Ergebnisse und die Diagnoseinformationen zum Mount zu sichern.

Bei der Umsetzung dieses Ablaufs auf den Cloud-Macs von VMCache kommt es nicht auf einen bestimmten Mount-Befehl an, sondern auf klare Grenzen: Das Netzwerk-Volume dient dem Datenaustausch, der lokale Datenträger dem Build, und ein endgültiger Dateiname steht ausschließlich für ein vollständig geprüftes Artefakt. So bleiben Build-Umgebung und Veröffentlichungsstatus selbst bei einer vorübergehenden SMB-Störung sauber voneinander getrennt.

Häufig gestellte Fragen

Sollte DerivedData auf einer SMB-Freigabe liegen?

Nein. Die vielen kleinen Dateien, Sperren und Metadatenänderungen machen Netzwerkfreigaben dafür ungeeignet. Verwenden Sie ein lokales Verzeichnis pro Job und löschen oder archivieren Sie es nach klaren Regeln.

Wie verhindert man, dass ein unvollständiges Artefakt verarbeitet wird?

Die CI kopiert zunächst unter einem temporären Namen, erzeugt Prüfsumme und Abschlussmanifest und benennt die Datei erst danach im selben Verzeichnis um. Nachfolgende Jobs akzeptieren ausschließlich den endgültigen Namen.

Was passiert bei einem fehlerhaften SMB-Mount?

Der Job muss vor dem Build Dateisystemtyp und Schreibzugriff prüfen. Schlägt der Test fehl, wird abgebrochen, damit kein gleichnamiges lokales Verzeichnis irrtümlich als Freigabe verwendet wird.

Dedizierter physischer Knoten

Wählen Sie einen Cloud-Mac für Builds, Tests und MLX-Inferenz

Wählen Sie M4, Arbeitsspeicher, Speicher, Knoten und Laufzeit passend zu Ihrer Aufgabe. Jede Miete umfasst einen dedizierten physischen Rechner, keine virtuelle Maschine.

Mietoption auswählen