Swift 6 auf dedizierten Remote-Macs migrieren:Strikte-Concurrency-CI-Gates und Actor-Isolation-Runbook

Swift 6 verschiebt Concurrency-Korrektheit von Laufzeitabstürzen zu Compile-Zeit-Fehlern: Mit -strict-concurrency=complete scheitern Data Races und Actor-Grenzverletzungen am Build. Dieser Artikel zeigt, wie Sie das Flag in CI-Gates auf NUKCLOUD-Dediziert-Remote-Mac-Knoten verankern – plus eine sechsstufige Migrations-Checkliste, damit PRs die Concurrency-Compliance vor dem Merge bestehen.

Der Swift-6-Sprachmodus macht den Compiler zum Concurrency-Auditor: Statt @MainActor-Missbrauch oder Cross-Actor-Sharing im Review zu finden, bricht xcodebuild früh ab. Sie brauchen einen leistungsstabilen, versionsgepinnten Build-Knoten – volle Compiles ohne Nachbar-CPU-Jitter bei der Tail-Latency. Dieser Artikel zeigt die schrittweise Einführung strikter Swift-6-Concurrency-Checks auf NUKCLOUD-Dediziert-Remote-Mac-Knoten und deren Verankerung in PR-Merge-Gates.

00Warum jetzt migrieren – und warum Dediziert-Knoten

Swift-6-Strikte-Concurrency-Checks (-strict-concurrency=complete) skalieren mit der Codebasis: 200–800 Compile-Fehler beim ersten Aktivieren sind bei mittelgroßen iOS-Projekten üblich. Fixes dauern Wochen oder Monate; in dieser Phase muss CI garantieren:

  • Xcode-Minor-Versionen pinnen: Actor-Isolation-Diagnosen ändern Formulierungen zwischen Xcode-Versionen; Pinnen macht CI-Ausgaben vergleichbar.
  • Kein Nachbar-CPU-Streit: Volle Strikte-Concurrency-Compiles dauern 20–40 % länger als typische Debug-Builds; Dediziert-Knoten halten P95-Tail-Latency stabil.
  • Isolierte DerivedData-Namespaces: Mehrere PRs mit Concurrency-Checks brauchen getrennte inkrementelle Caches, sonst sind Ergebnisse unzuverlässig.
Tipp: Nutzt Ihr CI einen geteilten macOS-Minuten-Pool, prüfen Sie Queue-P95, bevor Sie Swift-6-Strikte-Concurrency-Gates aktivieren – in der Warteschlange hängen ist schwerer zu debuggen als beim Compilieren. Knoten-Semantik: NUKCLOUD-Apple-Silicon-Knoten-Runbook.

01Drei Concurrency-Check-Stufen und Migrationspfad

Swift bietet drei Compiler-Flag-Stufen in OTHER_SWIFT_FLAGS (Xcode Build Settings) oder swiftSettings in Package.swift:

FlagBedeutungEmpfohlene Phase
-strict-concurrency=minimalPrüft nur grundlegende async/await-Syntax, keine Actor-GrenzenLegacy-Aufwärmphase (Woche 1–2)
-strict-concurrency=targetedPrüft Actor-Grenzen für explizit markierte Typen (Sendable, @MainActor usw.)Modulweise Fixphase (Woche 3–6)
-strict-concurrency=completeVolle Prüfung wie Swift-6-Sprachmodus; unbehandeltes Concurrency-Sharing führt zu FehlernNach 0 Fehlern ins Gate aufnehmen

Praxis: modulweise migrieren – zuerst complete in Blattmodulen, dann Kernmodule. SwiftPM .enableUpcomingFeature("StrictConcurrency") steuert pro Target präzise.

02Actor-Isolation-Grenzen: drei häufige Fehlertypen

Etwa 90 % der Migrations-Compile-Fehler fallen in diese drei Kategorien; deren Behebung räumt die meisten roten Builds weg:

① Cross-Actor-Zugriff auf nicht-Sendable-Typen

Swift — typisches Fehlerbeispiel
// ❌ Fehler: @MainActor-isolierter Zustand außerhalb von @MainActor
actor DataFetcher {
    func load() async {
        let result = await networkClient.fetch()
        // ❌ passing argument of non-sendable type 'UserModel' across actor boundary
        await MainActor.run { viewModel.update(result) }
    }
}

Fix: UserModel an Sendable anbinden (Werttypen erfüllen das oft automatisch) oder Cross-Boundary-Konvertierung innerhalb der Actor-Grenze durchführen.

② Fehlendes @MainActor

UIKit-/SwiftUI-ViewController- und ObservableObject-Subklassen liefen vor der Migration implizit auf dem Main Thread; Swift 6 verlangt ein explizites @MainActor. Xcode „Fix“ kann stapeln – jede Stelle manuell prüfen, damit keine Nicht-UI-Logik auf den Main Actor wandert.

③ Gleichzeitiger Zugriff auf globale Variablen

Swift — Beispiel für globale Variablen
// ❌ In Swift 6 müssen Globals Sendable oder nonisolated sein
var sharedCache: NSCache<NSString, AnyObject> = .init()

// ✅ nonisolated(unsafe) als Übergang
nonisolated(unsafe) var sharedCache: NSCache<NSString, AnyObject> = .init()

// ✅ Besser: in einen Actor mit async API
actor CacheStore {
    private let cache = NSCache<NSString, AnyObject>()
    func object(forKey key: String) -> AnyObject? { cache.object(forKey: key as NSString) }
}

03CI-Gates auf Remote-Macs konfigurieren

Kernidee für PR-Gates: Concurrency-Checks in einem separaten Job von regulären Tests. Fehler verschmutzen keine Testberichte; in der Fixphase unterschiedliche Strategien (warn-only vs. block-merge). Auf NUKCLOUD zuerst Preisseite, dann Bestellung — keine temporären Minuten-Pool-Sprints während der Migration.

Fix-Fenster dauern oft Wochen: Concurrency-Fehlerzähler, Full-Build-Wall-Time und DerivedData-Größe strukturiert loggen und mit Spec-Änderungen verknüpfen.

GitHub Actions — .github/workflows/swift6-concurrency-check.yml
name: Swift 6 Concurrency Gate

on:
  pull_request:
    branches: [main, release/*]

jobs:
  concurrency-check:
    runs-on: self-hosted          # NUKCLOUD dedizierter Remote-Mac-Runner
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4

      - name: Xcode wählen (Minor-Version pinnen)
        run: sudo xcode-select -s /Applications/Xcode_16.3.app

      - name: Restore DerivedData cache
        uses: actions/cache@v4
        with:
          path: ~/DerivedData/swift6-check
          key: swift6-${{ runner.name }}-${{ hashFiles('**/*.xcodeproj/project.pbxproj', '**/Package.resolved') }}
          restore-keys: swift6-${{ runner.name }}-

      - name: Build with strict concurrency (complete)
        run: |
          xcodebuild build \
            -project MyApp.xcodeproj \
            -scheme MyApp \
            -destination 'generic/platform=iOS Simulator' \
            -derivedDataPath ~/DerivedData/swift6-check \
            OTHER_SWIFT_FLAGS="-strict-concurrency=complete" \
            | xcbeautify --renderer github-actions

      - name: Upload build log on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: swift6-build-log-${{ github.sha }}
          path: ~/DerivedData/swift6-check/Logs/Build/
DerivedData-Bucket-Regeln: -derivedDataPath ~/DerivedData/swift6-check getrennt von normalen Builds, damit Concurrency-Artefakte Debug-Caches nicht vergiften. Bei mehreren PRs zusätzlich runner.name oder PR-Nummer zur Isolation.

04Sechsstufiges Migrations-Runbook

  1. 01
    Baseline-Snapshot: Auf einem Dediziert-Remote-Mac einen Vollbuild mit -strict-concurrency=minimal fahren, Fehlerzahl und Compile-Zeit als quantifizierten Startpunkt protokollieren.
  2. 02
    Blattmodule zuerst: Aus dem SwiftPM-Graphen Targets ohne Downstream-Abhängige wählen und complete einzeln aktivieren. Pro Target ein eigener PR für reviewbare Diffs.
  3. 03
    CI-Branch-Strategie: Auf einem lang lebenden feature/swift6-migration-Branch Concurrency-Jobs als warn-only (continue-on-error: true) laufen lassen, ohne tägliche PR-Merges zu blockieren. Erst bei 0 Fehlern pro Modul auf block-merge upgraden.
  4. 04
    Xcode-Version pinnen: xcode-select-Version in Runbook und README festhalten, damit lokale Mac-Entwickler und CI-Knoten dieselbe Minor-Version nutzen – weniger „lokal grün, CI rot“.
  5. 05
    Fehlertrend-Dashboard: Swift-6-Fehlerzahlen pro CI-Lauf in strukturierte Logs oder Build Metrics schreiben; Fortschritt per Liniendiagramm und wöchentliche Teamziele („X Fehler weniger diese Woche“).
  6. 06
    Vollständige Gate-Abnahme: Nach 0 Fehlern in allen Modulen auf main „Require swift6-concurrency-check to pass“ in Branch Protection aktivieren und SWIFT_VERSION = 6 für alle Targets setzen – offizieller Wechsel in den Swift-6-Sprachmodus.

05Lokaler Mac vs. Dediziert-Remote-Knoten

Swift-6-Migration braucht schnelle lokale Iteration und verlässliche Voll-CI-Validierung – die Anforderungen an Knoten unterscheiden sich:

DimensionEntwickler-Lokal-MacNUKCLOUD Dediziert-Remote-Mac (CI)
Xcode-VersionVom Entwickler verwaltet, kann abweichenPer Skript gepinnte Minor-Version, teamweit einheitlich
DerivedDataMit Tagesentwicklung geteilt; inkrementeller Cache kann kippenGetrennte Pfade, nach PR oder Runner gebucketed
CPU-VerfügbarkeitKonkurrenz durch andere Apps; Compile-Zeit schwanktBare-Metal dediziert, planbare P95-Latency
Ergebnisvertrauen
Lokal grün heißt nicht CI grünFinale Instanz in der Gate-Phase
Ideal fürSchnelle Einzelmodul-Versuche und Xcode-Diagnosen lesenVolle Concurrency-Gate-Checks und tägliche Regression

06FAQ

Sind Swift-6-Strikte-Concurrency-Checks dasselbe wie der Swift-6-Sprachmodus?
Nicht ganz. -strict-concurrency=complete ist ein Compiler-Flag, aktivierbar im Swift-5-Sprachmodus, um alle Fehler vor dem Wechsel zu sehen. Der formale Swift-6-Sprachmodus (SWIFT_VERSION = 6) entfernt zusätzlich Legacy-Syntax. Zuerst mit dem complete-Flag fixen, dann Sprachmodus wechseln.
Was, wenn Drittanbieter-Deps (CocoaPods / SPM) Swift 6 noch nicht unterstützen?
Checks pro Dependency mit .unsafeFlags(["-strict-concurrency=minimal"]) in Package.swift absenken oder SWIFT_STRICT_CONCURRENCY = minimal in einem Podfile-post_install-Hook setzen. Die meisten großen Libraries waren 2026 migriert – zuerst Release Notes prüfen.
Können NUKCLOUD-Knoten Concurrency-Checks für mehrere PRs parallel fahren?
Ja. Beim Runner-Registrieren --concurrent-jobs N setzen und DerivedData nach PR bucketen (-derivedDataPath ~/DerivedData/pr-${{ github.event.number }}), damit Jobs unabhängig compilen. Dediziert-Bare-Metal-Knoten punkten hier: kein Nachbar-CPU-Streit, geringe Varianz bei parallelen Jobs.
Wo finde ich Preise und Knoten-Specs?
Siehe Preisseite, Bestellung und Hilfe-Center; dieser Artikel beschreibt nur Engineering-Praxis und ist kein Preisversprechen.
Minuten-Pool, eigener Mac oder dedizierter Remote-Knoten?
Minuten-Pools für kurze Trials, aber volle Swift-6-Scans vergrößern Queue und Nachbar-CPU; eigene Macs skalieren schlecht mit Projektzyklen; Schreibtisch-Macs sind schwache Merge-Gate-Schiedsrichter. Beim Sperren von -strict-concurrency=complete über Wochen sind NUKCLOUD Multi-Region Bare-Metal Mac / Cloud-Mac-Knoten stabiler — mit dem Konsole-Runbook zur Abnahme.