MiniRent Engineering-Leitfaden

Ein inkrementelles Swift-Style-Gate auf dem Cloud-Mac einrichten

Ein inkrementelles Swift-Style-Gate auf dem Cloud-Mac einrichten

Wenn ein mittelgroßes Swift-Repository auf einem Cloud-Mac vollständig auf Formatierungsfehler geprüft wird, liegt das Problem meist nicht an langsamen Werkzeugen. Vielmehr werden bei jedem Merge Request unveränderte Quelldateien, generierte Verzeichnisse und externe Abhängigkeiten erneut durchsucht. Zusätzlich erschweren unterschiedliche Versionen auf den lokalen Rechnern und in der CI die Arbeit: Dieselbe Codezeile kann dadurch je nach Umgebung unterschiedlich bewertet werden. Die Lösung besteht nicht darin, Regeln abzuschalten, sondern Werkzeugversionen, Konfigurationsdateien und die Ermittlung der Änderungen gemeinsam im Repository zu versionieren.

Zuständigkeiten der beiden Werkzeuge klar trennen

SwiftFormat eignet sich für eindeutig bestimmbare und automatisch korrigierbare Formatierungen wie Einrückungen, Zeilenumbrüche und überflüssige Leerzeichen. SwiftLint ist dagegen besser für Risikomuster wie erzwungene Typumwandlungen, zu lange Funktionen und Namenskonventionen geeignet. Wenn beide Werkzeuge dieselbe Regel verwalten, kann frisch formatierter Code unmittelbar danach von der statischen Prüfung abgelehnt werden.

Als Ausgangspunkt empfiehlt sich eine einfache Zuordnungstabelle:

Prüfungsart Zuständiges Werkzeug Verhalten im Merge Request
Einrückung, Leerraum und Zeilenumbrüche bei Parametern SwiftFormat Nur prüfen, nicht automatisch ändern
Erzwungene Typumwandlungen und erzwungenes Entpacken SwiftLint Fehlschlagen und Dateiposition ausgeben
Generierter Code und externe Abhängigkeiten Von beiden ausgeschlossen Nicht in die Liste geänderter Dateien aufnehmen
Bestehende Warnungen SwiftLint-Baseline Nur neue Probleme blockieren

In der CI sollten keine Befehle ausgeführt werden, die den Quellcode direkt verändern. Das Gate meldet lediglich Abweichungen. Erst nachdem Entwickler sie lokal korrigiert und die Änderungen eingereicht haben, bleibt der Repository-Zustand zuverlässig reproduzierbar.

Versionen und Regeln im Repository ablegen

Verlassen Sie sich nicht auf die Werkzeuge, die auf dem Cloud-Mac „derzeit installiert“ sind. Wählen Sie zunächst Versionen aus, die das Team bereits überprüft hat, und hinterlegen Sie die erwarteten Versionsnummern anschließend im Skript. Das folgende Beispiel verwendet SwiftFormat 0.54.3 und SwiftLint 0.55.1. In einem realen Projekt müssen diese Angaben durch die jeweils validierten Versionen ersetzt werden.

Die Datei .swiftformat kann kompakt bleiben:

--swiftversion 5.10
--indent 4
--wraparguments before-first
--exclude .build,DerivedData,Vendor,Generated

In .swiftlint.yml werden dagegen der Quellcodebereich und die auszuschließenden Verzeichnisse ausdrücklich festgelegt:

included:
  - Sources
  - Tests
excluded:
  - .build
  - DerivedData
  - Vendor
  - Generated
only_rules:
  - force_cast
  - force_try
  - trailing_whitespace
  - unused_import

Regeldateien müssen Teil des Code-Reviews sein. Bevor eine neue Regel aktiviert wird, sollte zunächst in einem separaten Branch ein vollständiger Repository-Scan erfolgen. Anhand der Anzahl der Warnungen lässt sich anschließend entscheiden, ob alle Probleme auf einmal behoben oder zunächst in einer Baseline erfasst werden. Hunderte bestehende Warnungen sollten nicht direkt in das Merge-Request-Gate übernommen werden. Andernfalls gewöhnt sich das Team lediglich daran, rote Statusanzeigen zu ignorieren.

Swift-Änderungen eines Merge Requests korrekt ermitteln

Die direkte Verwendung von git diff HEAD^ eignet sich nur für Branches mit einem einzelnen Commit. Enthält ein Merge Request mehrere Commits oder wurde der Branch rebased, können mit diesem Ansatz Dateien übersehen werden. Zuverlässiger ist es, zuerst den gemeinsamen Vorfahren des aktuellen Commits und des Ziel-Branches zu bestimmen. Anschließend werden hinzugefügte, kopierte, geänderte und umbenannte Dateien ermittelt.

set -euo pipefail

expected_format="0.54.3"
expected_lint="0.55.1"

[[ "$(swiftformat --version)" == "$expected_format" ]]
[[ "$(swiftlint version)" == "$expected_lint" ]]

base_ref="${LINT_BASE_REF:-origin/main}"
merge_base="$(git merge-base HEAD "$base_ref")"
changed=("${(@0)$(git diff --name-only -z --diff-filter=ACMR "$merge_base" HEAD)}")

swift_files=()
for file in "${changed[@]}"; do
  [[ "$file" == *.swift ]] || continue
  [[ "$file" == .build/* ]] && continue
  [[ "$file" == DerivedData/* ]] && continue
  [[ "$file" == Vendor/* ]] && continue
  [[ "$file" == Generated/* ]] && continue
  swift_files+=("$file")
done

(( ${#swift_files[@]} > 0 )) || exit 0

swiftformat --lint "${swift_files[@]}"
swiftlint lint --strict --force-exclude "${swift_files[@]}"

Die Dateiliste ist hier durch Nullzeichen getrennt, sodass auch Pfade mit Leerzeichen korrekt verarbeitet werden. --diff-filter=ACMR schließt gelöschte Dateien aus und verhindert damit, dass die Prüfwerkzeuge nicht mehr vorhandene Pfade erhalten. Der Name des Ziel-Branches wird über eine Umgebungsvariable übergeben. Dadurch kann dasselbe Skript sowohl mit dem Standard-Branch als auch mit Release-Branches verwendet werden.

Flache Klone behandeln

Wenn git merge-base keinen gemeinsamen Vorfahren findet, ist meist nicht das Skript fehlerhaft. Häufig hat die CI lediglich einen sehr kleinen Ausschnitt des Commit-Verlaufs ausgecheckt. In diesem Fall muss zuerst die Checkout-Tiefe erhöht und sichergestellt werden, dass die Referenz des Ziel-Branches vorhanden ist. Erst danach sollte das Skript ausgeführt werden. Ein Rückfall auf HEAD^ ist keine Lösung, weil das Gate dann abhängig von der jeweiligen Branch-Struktur unterschiedliche Ergebnisse liefert.

Fehler auffindbar und reproduzierbar machen

Nach einem fehlgeschlagenen Gate muss das Protokoll mindestens drei Fragen beantworten: Welche Werkzeugversionen wurden verwendet, mit welchem Basis-Commit wurde verglichen und welche Dateien wurden geprüft? Bei einer abweichenden Version muss der Vorgang sofort beendet werden, anstatt weiterzulaufen und zahlreiche Formatierungsunterschiede zu erzeugen.

Zur lokalen Reproduktion müssen Entwickler lediglich die Referenz des Ziel-Branches abrufen und dasselbe Skript ausführen:

git fetch origin main
LINT_BASE_REF=origin/main zsh Scripts/lint-changed-swift.zsh

Ein häufiger Fehler besteht darin, den Prüfbefehl an eine Pipeline anzuhängen und anschließend den falschen Exit-Code auszulesen. Ebenso problematisch ist || true, das mitunter für ansprechendere Protokolle eingesetzt wird, dabei aber echte Fehler verschluckt. Mit aktiviertem set -euo pipefail beendet jeder von einem Werkzeug zurückgegebene Status ungleich null den Job. Muss die CI das Protokoll aufbereiten, sollte sie zunächst den ursprünglichen Exit-Code speichern, danach die Zusammenfassung ausgeben und schließlich mit dem gespeicherten Wert enden.

Langfristige Kosten mit einer zweistufigen Strategie kontrollieren

Ein inkrementelles Gate garantiert nur, dass „diese Änderung keine neuen Probleme einführt“. Es kann nicht belegen, dass der gesamte bestehende Code den aktuellen Regeln entspricht. Deshalb lässt sich die Prüfung in zwei Ebenen aufteilen:

  1. Jeder Merge Request führt eine Prüfung der Änderungen aus. Sie soll schnell und stabil sein und sich lokal vollständig reproduzieren lassen.
  2. Ein geplanter Job prüft regelmäßig das gesamte Repository, um Abweichungen von der Baseline, unwirksame Ausschlüsse und lange unbearbeitete Altwarnungen zu erkennen.
  3. Regelaktualisierungen werden separat eingereicht und nicht mit fachlichen Änderungen vermischt. Dadurch lassen sich großflächige Formatierungsänderungen leichter prüfen.
  4. Generierte Verzeichnisse müssen eine eindeutig definierte Quelle haben. Sie werden sowohl in der Werkzeugkonfiguration ausgeschlossen als auch im Diff-Skript gefiltert, damit die Reihenfolge der Generierungsjobs das Ergebnis nicht beeinflusst.

Vor der Einführung sollte nochmals geprüft werden, ob die Referenz des Ziel-Branches vorhanden ist, die Werkzeugversionen exakt übereinstimmen, umbenannte Dateien berücksichtigt und gelöschte Dateien ausgeschlossen werden, Pfade mit Leerzeichen sicher verarbeitet werden und das Skript bei fehlenden Swift-Änderungen ordnungsgemäß endet. Erst dann wird aus einem „gelegentlich fehlschlagenden Skript“ ein zuverlässiges Gate für neue Commits.

Häufig gestellte Fragen

Ersetzt die inkrementelle Prüfung einen vollständigen Repository-Scan?

Nein. Sie beschleunigt Merge Requests, erkennt aber keine Verstöße in unveränderten Altdateien. Dafür sollte regelmäßig ein vollständiger Scan laufen.

Sollten SwiftFormat und SwiftLint dieselben Regeln prüfen?

Nein. SwiftFormat sollte deterministische Formatierung übernehmen, SwiftLint riskante Muster und Teamregeln. Überschneidungen werden genau einem Werkzeug zugeordnet.

Exklusive physische Geräte

Die nächste Entwicklungs- oder Build-Aufgabe auf einem Cloud-Mac mini ausführen

Wählen Sie zwischen zwei M4-Konfigurationen, vier Mietlaufzeiten und fünf verfügbaren Standorten. Maßgeblich ist der vom Control Panel in Echtzeit angezeigte Status.

Konfiguration auswählen und mieten