Миграция Swift 6 на выделенных удалённых Mac:Строгие ворота CI по конкурентности и Runbook по изоляции Actor

Swift 6 переносит корректность конкурентности с падений в runtime на ошибки компиляции: при -strict-concurrency=complete гонки данных и нарушения границ Actor роняют сборку. В статье — как включить этот флаг в ворота CI на выделенных удалённых узлах Mac NUKCLOUD и шестишаговый чеклист постепенной миграции, чтобы PR проходили проверку конкурентности до слияния.

Режим языка Swift 6 превращает компилятор в «аудитора конкурентности»: вместо поиска неправильного @MainActor или меж-Actor доступа на code review xcodebuild падает сразу. Нужен узел сборки с стабильной мощностью и зафиксированной версией — полная компиляция без соседского джиттера CPU на хвосте задержек. Материал показывает поэтапное подключение строгих проверок Swift 6 на выделенных удалённых узлах Mac NUKCLOUD и фиксацию в воротах слияния PR.

00Зачем мигрировать сейчас — и зачем выделенные узлы

Строгие проверки конкурентности Swift 6 (-strict-concurrency=complete) масштабируются с размером кодовой базы: при первом включении у среднего iOS-проекта 200–800 ошибок компиляции — обычное дело. Исправления тянутся неделями и месяцами; в этот период CI должен гарантировать:

  • Зафиксированные минорные версии Xcode: формулировки диагностики изоляции Actor меняются между версиями Xcode; фиксация делает вывод CI сопоставимым.
  • Без конкуренции за CPU с соседями: полная компиляция со strict-concurrency на 20–40% дольше обычной debug-сборки; выделенные узлы стабилизируют P95 хвоста задержек.
  • Изолированные пространства имён DerivedData: несколько PR с проверками конкурентности нуждаются в отдельных инкрементальных кэшах, иначе результаты ненадёжны.
Совет: если CI использует общий пул macOS «по минутам», оцените P95 очереди до включения строгих ворот Swift 6 — застрять в очереди сложнее отладить, чем застрять в компиляции. Семантика узлов: runbook узлов Apple Silicon NUKCLOUD.

01Три уровня проверки конкурентности и путь миграции

В Swift три уровня флагов компилятора в OTHER_SWIFT_FLAGS (Xcode Build Settings) или swiftSettings в Package.swift:

ФлагСмыслРекомендуемый этап
-strict-concurrency=minimalПроверяет только базовое соответствие синтаксиса async/await, без границ ActorРазогрев legacy-кода (недели 1–2)
-strict-concurrency=targetedПроверяет границы Actor для типов с явными Sendable, @MainActor и т. п.Пофайловое исправление (недели 3–6)
-strict-concurrency=completeПолная проверка, эквивалент режима языка Swift 6; необработанный совместный доступ в конкурентности — ошибкаВ ворота после достижения 0 ошибок

На практике мигрируйте по модулям: сначала complete на листовых модулях, затем на ядре. В SwiftPM .enableUpcomingFeature("StrictConcurrency") точно задаёт уровень для каждого target.

02Границы изоляции Actor: три частых типа ошибок

Около 90% ошибок компиляции в период миграции — из этих трёх категорий; их исправление снимает большую часть «красных» сборок:

① Меж-Actor доступ к non-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 (value-типы часто подходят автоматически) или выполнять преобразование на границе Actor внутри границы.

② Пропущенный @MainActor

Подклассы UIKit/SwiftUI ViewController и ObservableObject до миграции неявно работали в главном потоке; Swift 6 требует явной аннотации @MainActor. Кнопка Fix в Xcode массово правит, но каждое место лучше проверить вручную, чтобы не перенести на главный поток логику, которой там быть не должно.

③ Конкурентный доступ к глобальным переменным

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Настройка ворот CI на удалённом Mac

Суть ворот для PR: отдельный Job для проверок конкурентности, не смешивать с обычными тестами. Тогда сбой проверки не засоряет отчёт тестов, а на этапе исправлений можно задать warn-only или block-merge. На NUKCLOUD: тарифы, затем заказ — не полагайтесь на временные всплески пула по минутам в миграции.

Окно исправлений часто длится неделями: логируйте счётчик ошибок конкурентности, wall time полной сборки и размер DerivedData, связывайте с изменением 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          # Выделенный удалённый Mac Runner NUKCLOUD
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4

      - name: Выбрать Xcode (зафиксировать minor)
        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 отдельно от обычных сборок, чтобы артефакты проверки конкурентности не портили debug-кэш. При нескольких PR добавьте runner.name или номер PR для дополнительной изоляции.

04Шестишаговый Runbook постепенной миграции

  1. 01
    Снимок базовой линии: на выделенном удалённом Mac выполните полную сборку с -strict-concurrency=minimal, зафиксируйте число ошибок и время компиляции как измеримую отправную точку.
  2. 02
    Сначала листовые модули: в графе SwiftPM выберите target без зависимых downstream и по одному включайте complete. После исправления каждого target — отдельный PR с обозримым diff.
  3. 03
    Стратегия веток CI: на долгоживущей ветке feature/swift6-migration запускайте Job проверки конкурентности как warn-only (continue-on-error: true), не блокируя ежедневные слияния PR. Переводите модуль на block-merge только при 0 ошибок.
  4. 04
    Зафиксируйте версию Xcode: запишите версию xcode-select в Runbook и README, чтобы локальные Mac разработчиков и узлы CI использовали один минор — меньше шума «локально прошло, в CI упало».
  5. 05
    Дашборд тренда ошибок: пишите число ошибок Swift 6 за каждый прогон CI в структурированные логи или Build Metrics, отслеживайте миграцию линейными графиками и недельными целями «снизить на X ошибок».
  6. 06
    Приёмка полных ворот: когда все модули на 0 ошибок, включите Require swift6-concurrency-check to pass в Branch Protection для main и задайте SWIFT_VERSION = 6 во всех target — формальный переход на режим языка Swift 6.

05Локальный Mac и выделенный удалённый узел

Миграции Swift 6 нужны и быстрые локальные итерации, и достоверная полная проверка в CI; требования к узлам различаются:

ИзмерениеЛокальный Mac разработчикаВыделенный удалённый Mac NUKCLOUD (CI)
Версия XcodeНа усмотрении разработчика, возможен разбросМинор зафиксирован скриптом, едино для команды
DerivedDataОбщий с ежедневной разработкой; инкрементальный кэш может испортитьсяОтдельные пути, «ведра» по PR или Runner
Доступность CPUКонкуренция с другими приложениями, время сборки плаваетBare-metal выделенный, предсказуемый P95 задержек
Доверие к результату
«Локально прошло» не означает прохождение CIОкончательный арбитр на этапе ворот
Лучше всего дляБыстрые пробы одного модуля и чтение диагностики XcodeПолные ворота проверки конкурентности и ежедневная регрессия

06Часто задаваемые вопросы

Строгие проверки конкурентности Swift 6 и «режим языка Swift 6» — одно и то же?
Не совсем. -strict-concurrency=complete — флаг компилятора, его можно включить в режиме языка Swift 5, чтобы увидеть все ошибки до переключения. Формальный режим Swift 6 (SWIFT_VERSION = 6) ещё убирает устаревший синтаксис. Сначала исправьте по флагу complete, затем переключайте режим языка.
Что делать, если сторонние зависимости (CocoaPods / SPM) ещё не поддерживают Swift 6?
Понизьте уровень проверки для конкретной зависимости через .unsafeFlags(["-strict-concurrency=minimal"]) в Package.swift или SWIFT_STRICT_CONCURRENCY = minimal в hook post_install Podfile. Большинство крупных библиотек завершили миграцию к 2026 году — сначала смотрите Release Notes.
Могут ли узлы NUKCLOUD одновременно гонять проверки конкурентности для нескольких PR?
Да. При регистрации Runner задайте --concurrent-jobs N и разнесите DerivedData по PR (-derivedDataPath ~/DerivedData/pr-${{ github.event.number }}), чтобы Job компилировались независимо. Выделенные bare-metal узлы сильны здесь: нет соседской борьбы за CPU и низкая дисперсия между параллельными Job.
Где смотреть тарифы и спецификации узлов?
См. тарифы, заказ и справочный центр; эта статья описывает только инженерную практику и не является обязательством по цене.
Пул по минутам, свой Mac или выделенный удалённый узел?
Пулы по минутам для коротких проб, но полные сканы Swift 6 усиливают очередь и CPU соседей; свои Mac плохо масштабируются с циклами проекта; настольные машины слабы как арбитры merge gate. При блокировке -strict-concurrency=complete на недели мультирегиональные bare-metal Mac / cloud Mac узлы NUKCLOUD стабильнее — с runbook консоли для приёмки.