Migrer Swift 6 sur Mac distants dédiés :Portes CI de concurrence stricte et runbook d'isolation Actor

Swift 6 déplace la correction de concurrence des plantages à l'exécution vers les erreurs à la compilation : avec -strict-concurrency=complete, les courses de données et les violations de frontière Actor font échouer la build. Cet article montre comment intégrer ce drapeau aux portes CI sur les nœuds Mac distants dédiés NUKCLOUD et fournit une checklist de migration progressive en six étapes pour que les PR passent la conformité de concurrence avant fusion.

Le mode langage Swift 6 fait du compilateur un auditeur de concurrence : au lieu de repérer les mauvais usages de @MainActor ou le partage inter-Actor en revue, xcodebuild échoue tôt. Il faut un nœud de build à calcul stable et version figée — compilations complètes sans jitter CPU des voisins sur la latence de queue. Cet article présente l'adoption progressive de la concurrence stricte Swift 6 sur les nœuds Mac distants dédiés NUKCLOUD et son verrouillage dans les portes de fusion PR.

00Pourquoi migrer maintenant — et pourquoi des nœuds dédiés

Les contrôles de concurrence stricte Swift 6 (-strict-concurrency=complete) évoluent avec la taille du code : 200 à 800 erreurs de compilation au premier passage est courant pour un projet iOS de taille moyenne. Les correctifs s'étalent sur des semaines ou des mois ; pendant cette période, le CI doit garantir :

  • Versions mineures Xcode figées : les diagnostics d'isolation Actor changent de formulation selon les versions Xcode ; figer la version rend les sorties CI comparables.
  • Pas de contention CPU des voisins : les compilations complètes en concurrence stricte durent 20 à 40 % de plus qu'une build debug typique ; les nœuds dédiés stabilisent la latence de queue P95.
  • Espaces de noms DerivedData isolés : plusieurs PR exécutant des contrôles de concurrence ont besoin de caches incrémentaux séparés, sinon les résultats deviennent peu fiables.
Conseil : si le CI utilise un pool macOS hébergé partagé à la minute, évaluez la file P95 avant d'activer les portes de concurrence stricte Swift 6 — rester bloqué en file est plus difficile à diagnostiquer qu'en compilation. Sémantique des nœuds : runbook nœuds Apple Silicon NUKCLOUD.

01Trois niveaux de contrôle de concurrence et un chemin de migration

Swift propose trois niveaux de drapeaux compilateur dans OTHER_SWIFT_FLAGS (réglages de build Xcode) ou swiftSettings dans Package.swift :

DrapeauSignificationPhase recommandée
-strict-concurrency=minimalVérifie uniquement la conformité syntaxique de base async/await, pas les frontières ActorÉchauffement du code existant (semaines 1–2)
-strict-concurrency=targetedVérifie les frontières Actor pour les types explicitement marqués Sendable, @MainActor, etc.Correction module par module (semaines 3–6)
-strict-concurrency=completeContrôles complets équivalents au mode langage Swift 6 ; tout partage concurrentiel non traité génère une erreurIntégrer aux portes après 0 erreur

En pratique, migrez module par module : activez complete d'abord sur les modules feuilles, puis sur le cœur. SwiftPM .enableUpcomingFeature("StrictConcurrency") contrôle précisément le niveau par cible.

02Frontières d'isolation Actor : trois types d'erreurs fréquents

Environ 90 % des erreurs de compilation en migration relèvent de ces trois catégories ; les corriger efface la plupart des builds rouges :

① Accès inter-Actor à des types non Sendable

Swift — exemple d'erreur typique
// ❌ Erreur : accès à l état @MainActor hors du contexte @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) }
    }
}

Correction : faire conformer UserModel à Sendable (les types valeur le satisfont souvent automatiquement), ou effectuer la conversion inter-frontière à l'intérieur de la frontière Actor.

② @MainActor manquant

Les sous-classes ViewController et ObservableObject de UIKit/SwiftUI tournaient implicitement sur le thread principal avant migration ; Swift 6 exige une annotation @MainActor explicite. Le bouton Corriger de Xcode peut traiter en lot, mais relisez chaque site pour ne pas tirer de la logique non UI sur le thread principal.

③ Accès concurrent aux variables globales

Swift — exemple de correction de variable globale
// ❌ En Swift 6, les globales doivent être Sendable ou nonisolated
var sharedCache: NSCache<NSString, AnyObject> = .init()

// ✅ nonisolated(unsafe) en transition
nonisolated(unsafe) var sharedCache: NSCache<NSString, AnyObject> = .init()

// ✅ Mieux : encapsuler dans un Actor avec API async
actor CacheStore {
    private let cache = NSCache<NSString, AnyObject>()
    func object(forKey key: String) -> AnyObject? { cache.object(forKey: key as NSString) }
}

03Configurer les portes CI sur Mac distant

Idée centrale pour les portes PR : exécuter les contrôles de concurrence dans un Job séparé des tests habituels. Les échecs ne polluent pas les rapports de test et vous pouvez appliquer des stratégies différentes pendant les correctifs (warn-only vs. block-merge). Sur NUKCLOUD : tarifs, puis commander — évitez les pics temporaires de pool à la minute pendant la migration.

Les fenêtres de correctif durent souvent des semaines : journalisez erreurs de concurrence, wall time de build complet et taille DerivedData, et liez-les aux changements de spec.

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          # Runner Mac distant dédié NUKCLOUD
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4

      - name: Sélectionner Xcode (version mineure figée)
        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/
Règles de compartimentation DerivedData : utilisez -derivedDataPath ~/DerivedData/swift6-check séparé des builds normales pour que les artefacts de concurrence ne contaminent pas les caches de debug. Quand plusieurs PR tournent, ajoutez runner.name ou le numéro de PR pour isoler davantage.

04Runbook de migration progressive en six étapes

  1. 01
    Instantané de base : sur un Mac distant dédié, lancez une build complète avec -strict-concurrency=minimal et enregistrez le nombre d'erreurs et le temps de compilation comme point de départ quantifié.
  2. 02
    Modules feuilles en premier : dans le graphe SwiftPM, choisissez les cibles sans dépendants aval et activez complete une par une. Soumettez une PR distincte par cible pour des diffs vérifiables.
  3. 03
    Stratégie de branche CI : sur une branche longue feature/swift6-migration, exécutez les Jobs de concurrence en warn-only (continue-on-error: true) sans bloquer les fusions PR quotidiennes. Passez une cible en block-merge uniquement quand elle atteint 0 erreur.
  4. 04
    Figez la version Xcode : inscrivez la version xcode-select dans le runbook et le README pour que les développeurs Mac locaux et les nœuds CI partagent la même version mineure et évitent le bruit « passe en local, échoue en CI ».
  5. 05
    Tableau de bord des tendances d'erreurs : journalisez le nombre d'erreurs Swift 6 par exécution CI dans des logs structurés ou des métriques de build ; suivez la migration par courbes et fixez des objectifs hebdomadaires d'équipe pour réduire X erreurs.
  6. 06
    Acceptation de porte complète : après 0 erreur sur tous les modules, activez Require swift6-concurrency-check to pass sur la protection de branche main et définissez SWIFT_VERSION = 6 sur toutes les cibles pour basculer en mode langage Swift 6.

05Mac local vs. nœud distant dédié

La migration Swift 6 exige à la fois une itération locale rapide et une validation CI complète fiable ; les besoins de nœud diffèrent :

DimensionMac local du développeurMac distant dédié NUKCLOUD (CI)
Version XcodeGérée par le développeur, peut divergerVersion mineure figée par script, identique pour toute l'équipe
DerivedDataPartagé avec le dev quotidien ; le cache incrémental peut se corrompreChemins séparés, compartimentés par PR ou Runner
Disponibilité CPUContention avec d'autres apps ; temps de compilation variableBare-metal dédié, latence P95 prévisible
Fiabilité du résultat
Réussite locale n'implique pas réussite CIArbitrage final à l'étape de porte
Idéal pourEssais rapides sur un module et lecture des diagnostics XcodeContrôles de porte de concurrence complets et régression quotidienne

06FAQ

Les contrôles de concurrence stricte Swift 6 sont-ils identiques au mode langage Swift 6 ?
Pas tout à fait. -strict-concurrency=complete est un drapeau compilateur activable en mode langage Swift 5 pour voir toutes les erreurs avant de basculer. Le mode langage Swift 6 formel (SWIFT_VERSION = 6) supprime aussi certaines syntaxes héritées. Corrigez d'abord avec le drapeau complete, puis changez de mode langage.
Et si les dépendances tierces (CocoaPods / SPM) ne supportent pas encore Swift 6 ?
Réduisez les contrôles par dépendance avec .unsafeFlags(["-strict-concurrency=minimal"]) dans Package.swift, ou définissez SWIFT_STRICT_CONCURRENCY = minimal dans un hook post_install du Podfile. La plupart des bibliothèques majeures ont terminé la migration en 2026 — consultez d'abord les notes de version.
Les nœuds NUKCLOUD peuvent-ils exécuter des contrôles de concurrence pour plusieurs PR en parallèle ?
Oui. Configurez --concurrent-jobs N à l'enregistrement du Runner et compartimentez DerivedData par PR (-derivedDataPath ~/DerivedData/pr-${{ github.event.number }}) pour que les Jobs compilent indépendamment. Les nœuds bare-metal dédiés excellent ici : pas de contention CPU des voisins et faible variance entre Jobs concurrents.
Où consulter les tarifs et les spécifications des nœuds ?
Voir tarifs, commander et le centre d'aide ; cet article décrit uniquement la pratique technique et ne constitue pas un engagement tarifaire.
Pool à la minute, Mac acheté ou nœud distant dédié ?
Pools à la minute pour essais courts, mais scans Swift 6 complets amplifient file et CPU voisine ; Mac achetés peinent à suivre les cycles projet ; postes de bureau sont de faibles arbitres de merge gate. Pour verrouiller -strict-concurrency=complete sur des semaines, les nœuds Mac bare-metal / cloud multi-régions NUKCLOUD stabilisent Xcode épinglé, compartiments DerivedData et files observables — avec le runbook console.