在獨佔遠程 Mac 上遷移 Swift 6:嚴格並發 CI 門禁與 Actor 隔離 Runbook

Swift 6 把並發正確性從「運行時崩潰」提前到編譯期錯誤:開啟 -strict-concurrency=complete 後,數據競爭與 Actor 邊界違反都會讓構建失敗。本文說明如何在 NUKCLOUD 獨佔遠程 Mac 節點上把這個標誌納入 CI 門禁,並給出六步漸進遷移清單,讓 PR 合併前先過並發合規關。

Swift 6 語言模式讓編譯器成為「並發審計員」:不再靠人工 code review 發現 @MainActor 誤用或跨 Actor 數據共享,而是在 xcodebuild 階段直接報錯。這意味著你需要一台算力穩定、環境可釘版本的構建節點——既能跑完整編譯,也不會因為鄰居爭 CPU 導致尾延遲抖動。本文結合 NUKCLOUD 獨佔遠程 Mac 節點,展示如何分階段接入 Swift 6 嚴格並發檢查,並將其鎖進 PR 合併門禁。

00為什麼現在遷移——以及為什麼需要獨佔節點

Swift 6 嚴格並發檢查(-strict-concurrency=complete)觸發的錯誤數量與代碼庫規模正相關:中等規模 iOS 項目首次啟用時,出現 200–800 個編譯錯誤並不罕見。這些錯誤需要逐模塊修復,修復期往往橫跨數週甚至數月。在此期間,CI 環境必須保證:

  • Xcode 小版本固定:Swift 6 的 Actor isolation 診斷在不同 Xcode 版本間措辭會變化,釘住版本才能讓 CI 輸出可比較。
  • 無鄰居 CPU 爭搶:嚴格並發全量編譯比普通 debug build 耗時多 20–40%,獨佔節點避免 P95 尾延遲爆表。
  • 磁碟 DerivedData 命名空間隔離:多個 PR 同時跑並發檢查,需要各自獨立的增量編譯緩存,否則緩存污染導致結果不可信。
  • 錯誤計數可追蹤:把每次構建的 Swift 6 錯誤數、修復 commit、剩餘模組列表寫入結構化日誌,便於向管理層證明遷移進度。

獨佔節點的價值在於:同一 Xcode 小版本下,錯誤措辭與行號在數週內保持穩定,方便對比「上週 412 個、本週 87 個」這類可審計曲線;分鐘池頻繁換鏡像則會把問題混成環境噪聲。撥備與驗收欄位可與節點 Runbook 六步一併對齊,並在合併前用定價頁確認磁碟與區域是否覆蓋全量掃描。

提示:如果你的 CI 當前用的是「共享分鐘池」託管 macOS,建議在引入 Swift 6 嚴格並發門禁前先評估隊列 P95——全量編譯卡在排隊比卡在編譯更難排查。節點撥備、租戶邊界與區域主鏈路的工程語義,見 NUKCLOUD 獨佔 Apple Silicon 節點 Runbook

01三種並發檢查級別與遷移路徑

Swift 提供三檔編譯器標誌,可在 OTHER_SWIFT_FLAGS(Xcode Build Settings)或 Package.swiftswiftSettings 裡設置:

標誌含義推薦階段
-strict-concurrency=minimal僅檢查 async/await 基礎語法合規,不檢查 Actor 邊界存量代碼熱身期(第 1–2 週)
-strict-concurrency=targeted檢查顯式標註了 Sendable@MainActor 等註解的類型的 Actor 邊界逐模塊修復期(第 3–6 週)
-strict-concurrency=complete全量檢查,等價於 Swift 6 語言模式;未顯式處理的並發共享均報錯達到 0 錯誤後納入門禁

實踐中推薦按模塊漸進:先在葉子模塊(無依賴或依賴少)開啟 complete,再向核心模塊推進。SwiftPM 的 .enableUpcomingFeature("StrictConcurrency") 可精確控制單個 target 的檢查級別。

02Actor 隔離邊界:三類高頻錯誤

遷移期 90% 的編譯錯誤集中在以下三類,先處理完這三類可清掉絕大部分紅字:

① 跨 Actor 訪問非 Sendable 類型

Swift — 典型錯誤示例
// ❌ 錯誤:從非 @MainActor 上下文存取 @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) }
    }
}

修復方式:讓 UserModel 實現 Sendable(值類型通常自動滿足),或將跨邊界的轉換提前到 Actor 邊界內完成。

② @MainActor 漏標

UIKit/SwiftUI 的 ViewControllerObservableObject 子類在遷移前隱式跑在主線程;Swift 6 要求顯式標註 @MainActor。批量修復可用 Xcode 的「Fix」按鈕,但建議人工 review 每一處,避免把不應在主線程的邏輯也拉了進來。

③ 全局變量並發訪問

Swift — 全局變量修復示例
// ❌ Swift 6 中全域常數須為 Sendable 或 nonisolated
var sharedCache: NSCache<NSString, AnyObject> = .init()

// ✅ 遷移期以 nonisolated(unsafe) 明示
nonisolated(unsafe) var sharedCache: NSCache<NSString, AnyObject> = .init()

// ✅ 更佳:包進 Actor 並以 async API 存取
actor CacheStore {
    private let cache = NSCache<NSString, AnyObject>()
    func object(forKey key: String) -> AnyObject? { cache.object(forKey: key as NSString) }
}

03在遠程 Mac 上配置 CI 門禁

把嚴格並發檢查納入 PR 門禁的核心思路:單獨開一個 Job 跑並發檢查,不與常規測試 Job 合併。這樣並發檢查失敗不會污染測試報告,也便於在修復期設置不同的失敗策略(warn-only vs. block-merge)。在 NUKCLOUD 上撥備節點時,建議先在 定價頁 選定磁碟與區域,再經 下單頁 交付 SSH 與 Runner 基線,避免遷移期臨時借用分鐘池導致 P95 不可比。

修復期通常持續數週:請把「並發錯誤計數」「全量編譯 wall time」「DerivedData 體積」三項寫入結構化日誌,並與節點規格變更(加盤、換區)關聯,否則很難判斷是程式回退還是環境漂移。

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 獨占遠端 Mac Runner
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4

      - name: 選擇 Xcode(釘住小版本)
        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 分桶規則:-derivedDataPath ~/DerivedData/swift6-check 與普通構建的 DerivedData 目錄分開,避免並發檢查的編譯產物污染調試構建緩存。多個 PR 同時跑時,加上 runner.name 或 PR 編號進一步隔離。

04六步漸進遷移 Runbook

  1. 01
    基線快照:在獨佔遠程 Mac 上用 -strict-concurrency=minimal 跑一次全量編譯,記錄錯誤數與編譯耗時,作為遷移起點的可量化基線。
  2. 02
    葉子模塊先行:從 SwiftPM 依賴圖中找出無下游依賴的 target,逐一開啟 complete。每個 target 修復完畢後提單獨 PR,保持 diff 可審查。
  3. 03
    CI 分支策略:在 Git 的 feature/swift6-migration 長期分支上以 warn-onlycontinue-on-error: true)運行並發檢查 Job,不阻斷日常 PR 合併。只有當某個模塊達到 0 錯誤時,才將該模塊的檢查級別升級為門禁(block-merge)。
  4. 04
    釘住 Xcode 版本:xcode-select 版本號寫入 Runbook 與 README,確保本地 Mac 開發者與 CI 節點使用相同小版本,避免「本地過、CI 掛」的錯誤擾動。
  5. 05
    錯誤趨勢看板:把每次 CI 運行的 Swift 6 錯誤數寫入結構化日誌或 Build Metrics,用折線圖跟蹤遷移進度;給團隊設置「本週減少 X 個錯誤」的週目標。
  6. 06
    全量門禁驗收:全部模塊達到 0 錯誤後,在 main 分支的 Branch Protection Rule 裡啟用「Require swift6-concurrency-check to pass」,同時把 SWIFT_VERSION = 6 寫入所有 target 的 Build Settings,正式切換 Swift 6 語言模式。

05本地 Mac 與獨佔遠程節點對照

Swift 6 遷移期同時需要開發者本地快速試錯CI 全量可信驗證,兩者對節點的需求不同:

維度開發者本地 MacNUKCLOUD 獨佔遠程 Mac(CI)
Xcode 版本開發者自行管理,可能存在差異腳本釘住小版本,全團隊一致
DerivedData與日常開發共享,增量緩存可能被污染獨立路徑,按 PR 或 Runner 分桶
CPU 可用性受其他應用爭搶,編譯耗時有方差裸金屬獨佔,P95 延遲可預期
結果可信度
「本地過」不代表 CI 過門禁階段的最終裁決環境
適合場景單模塊快速試錯與 Xcode 診斷閱讀全量並發檢查門禁與每日回歸

06常見問題

Swift 6 嚴格並發檢查和「Swift 6 語言模式」是同一件事嗎?
不完全一樣。-strict-concurrency=complete 是編譯器標誌,可以在 Swift 5 語言模式下啟用,讓你先看到所有錯誤再決定何時切換。正式的 Swift 6 語言模式(SWIFT_VERSION = 6)在此基礎上還會鎖定若干已移除的語法糖。推薦先用 complete 標誌修完錯誤,再切換語言模式。
第三方依賴(CocoaPods / SPM)還沒支持 Swift 6 怎麼辦?
可以在 Package.swift 裡用 .unsafeFlags(["-strict-concurrency=minimal"]) 為特定依賴降級檢查,或在 Podfile 裡用 post_install hook 設置 SWIFT_STRICT_CONCURRENCY = minimal。大多數主流庫在 2026 年已陸續完成遷移,可先檢查對應庫的 Release Notes。
NUKCLOUD 節點能同時跑多個 PR 的並發檢查嗎?
可以。在 Runner 註冊時配置 --concurrent-jobs N,配合 DerivedData 按 PR 編號分桶(-derivedDataPath ~/DerivedData/pr-${{ github.event.number }}),多個 Job 獨立編譯互不影響。獨佔裸金屬節點的核心優勢正在於此:無鄰居爭 CPU,並發 Job 的完成時間方差小。
定價與節點規格在哪裡查看?
請查看 定價頁下單頁幫助中心;本文只描述工程實踐,不構成報價承諾。
分鐘池、自購 Mac 與獨佔遠程節點,遷移期該怎麼選?
分鐘池適合短期試跑,但全量 Swift 6 掃描會放大排隊與鄰居 CPU 爭搶;自購 Mac 環境一致性好,卻難隨專案週期彈性擴縮;桌面機 不適合做法務可審計的門禁裁決環境。當團隊要把 -strict-concurrency=complete 鎖進合併門禁並維持數週修復節奏時,NUKCLOUD 多區域裸金屬 Mac / 雲端 Mac 節點在釘版本、分桶 DerivedData 與可觀測隊列上更穩,也更易與 控制台撥備 Runbook 裡的六步驗收對齊。