VPSSpark Cloud Mac CI, выпуск #9. Вопрос, который часто задают Flutter-команды в России и СНГ: хватит ли двух Cloud Mac для стабильной CI при 8–15 разработчиках? Да — если разделить Android и iOS: сборки Android остаются на хостинге GitHub (ubuntu-latest), на macOS — два self-hosted runner в пулах macos-fast и macos-archive, и железное правило: никакого archive на PR. Ниже — модель нагрузки, уровни job, фрагмент workflow и чеклист внедрения. Диагностика очередей и подключение runner — в серии #2 и в кейсе три Cloud Mac при высоком объёме iOS CI.
1 · Неочевидный вывод: узкое место Flutter CI — не Dart
Большинство красных прогонов Flutter CI падают не из‑за медленного flutter test, а потому что iOS-джобы занимают слоты macOS runner — часто из‑за flutter build ipa на PR.
Кроссплатформенность создаёт иллюзию «один workflow на всё». На практике flutter analyze и unit-тесты идут и на Linux; macOS нужен для CocoaPods, сборки Xcode, подписи и экспорта IPA. Если archive попадает в путь PR, один Cloud Mac блокируется на 25–40 минут — остальные PR встают в Queued. То же, что у native iOS-команд; формула диагностики: wait time >> run time (см. диагностику очереди macOS runner).
Два Cloud Mac — не «две универсальные машины», а осознанная двухпуловая топология: быстрый feedback и тяжёлые release-задачи. APK/AAB остаются на Linux — не сжигать macOS-слоты на Gradle.
2 · Модель нагрузки: сколько параллелизма macOS нужно команде 8–15 человек?
База — умеренно активная Flutter-команда (1–2 PR на человека в день, nightly на main, еженедельные релизы):
В fast-пуле (macos-fast): analyze, тесты, сборка под iOS-симулятор — цель 8–14 минут wall time с кэшем pub/DerivedData. В archive-пуле (macos-archive): release IPA + нотаризация — 20–35 минут, но редко (main, теги, nightly). Смешанные пулы: archive душит fast; при разделении P95 ожидания PR обычно укладывается в 10 минут — достаточно для ритма code review в распределённых командах СНГ.
При 15+ людях или множестве flavor в matrix monorepo имеет смысл третья archive-машина — планирование как в сценарии 500 сборок/день, не тема этого старта с двух машин.
| Платформа | Runner | Типичный job | На PR? |
|---|---|---|---|
| Android | ubuntu-latest (хостинг) |
flutter build apk/appbundle, Android-тесты |
Да |
| iOS быстрый feedback | Cloud Mac #1 · macos-fast |
analyze, тесты, build ios --simulator |
Да |
| iOS release | Cloud Mac #2 · macos-archive |
build ipa, нотаризация, TestFlight |
Нет |
3 · Три уровня Flutter job: CI Hard Rules
Сопоставьте pipeline Flutter с L0/L1/L2 — в духе on-call руководства:
| Уровень | Содержимое Flutter | Пул | Триггер на PR? |
|---|---|---|---|
| L0 | dart format --set-exit-if-changed, flutter analyze, лёгкие тесты |
Linux или macos-fast |
Да |
| L1 | Интеграционные тесты, flutter build ios --simulator, widget-тесты |
macos-fast |
Да · без archive |
| L2 | flutter build ipa, App Store Connect, нотаризация |
macos-archive |
Нет · только main/tag/schedule |
Три правила как у native iOS: Rule 1 — без L2 на PR; Rule 2 — L2 только в изолированном пуле; Rule 3 — fast-пул не блокируется archive. Особенность Flutter: многое из L0 идёт на Linux — привязка всего к macos-fast тратит macOS-конкурентность впустую. На PR: Linux и macOS job параллельно; macOS только для «нельзя на Linux».
4 · Топология из двух машин: роли Cloud Mac #1 и #2
Рис. 1 · Типичная топология: два Cloud Mac для CI Flutter-команды
macos-fastL0/L1 · 8–14 мин/jobmacos-archiveL2 · только main/tagCloud Mac #1 (fast-пул): labels [self-hosted, macOS, macos-fast, flutter]. Flutter SDK зафиксирован (fvm или pin в образе), Xcode, CocoaPods. Runner как постоянный сервис с автозапуском; держите pub cache и DerivedData тёплыми — короткие job, высокий throughput.
Cloud Mac #2 (archive-пул): labels [self-hosted, macOS, macos-archive, flutter-release]. Физическое разделение — никогда два runner разных пулов на одном Mac (иначе Rule 2 бессмысленна). Distribution-сертификаты и App Store Connect API Key в Keychain; шаги подключения — как в практическом onboarding runner. Перед релизом: flutter doctor -v и dry-run archive — не заражать fast-пул.
Один регион, одна мажорная версия Xcode — меньше «fast зелёный, archive красный». Cloud Mac: фиксированный образ, стабильный канал к GitHub, без офисного UPS — удобно для CI 7×24. Про границы безопасности self-hosted runner: archive-машину ограничьте одним repo или org.
5 · Пример workflow: разделение PR и main
Ядро маршрутизации (полный pipeline добавит cache, secrets, artifacts). Документация CD Flutter по подписи iOS; сертификаты уже в Keychain archive.
jobs: android-pr: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - run: flutter analyze && flutter test - run: flutter build appbundle --release ios-pr-fast: if: github.event_name == 'pull_request' runs-on: [self-hosted, macOS, macos-fast, flutter] steps: - run: flutter analyze - run: flutter test - run: flutter build ios --simulator --no-codesign ios-release: if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') runs-on: [self-hosted, macOS, macos-archive, flutter-release] steps: - run: flutter build ipa --export-options-plist=ExportOptions.plist # TestFlight / нотаризация — только L2
В ios-pr-fast намеренно нет build ipa. Сборка под устройство на PR — только через workflow_dispatch, всё равно archive-пул, не стандартное PR-событие. С concurrency в синтаксисе workflow отменяйте старые PR-run — часто ~20 % меньше бессмысленного ожидания.
6 · Чеклист кэша: четыре каталога для Flutter-команд
Два Cloud Mac держатся на хорошем кэше лучше, чем на новейшем CPU. Персистентно в home runner (или nightly snapshot):
PUB_CACHE/~/.pub-cache— зависимости Dart; инкремент при сменеpubspec.lock- Каталог Flutter SDK —
fvmфиксирует версию; безflutter upgradeв CI ios/Pods+ кэш CocoaPods —pod installчасто длинный хвост iOS- Xcode DerivedData — заметный выигрыш при том же
ios/Podfile.lock
Fast и archive не делят один DerivedData — смешение Debug/Simulator и Release/Archive даёт призрачные ошибки линковки. Gradle-кэш Android на ubuntu-latest через Actions Cache; берегите диск Cloud Mac.
pod repo update — сначала на archive, потом синхронизация fast-пула. Ежедневные PR только переиспользуют кэш; не ставьте flutter pub upgrade дефолтом в CI.
7 · Чеклист внедрения: от нуля к двум подключённым машинам
За 1–2 рабочих дня до PoC:
- 2 Cloud Mac одинаковой spec (M4 + 16 ГБ; пики iOS-сборки часто 12 ГБ+)
- #1: Flutter/Xcode/CocoaPods, runner
macos-fast; #2: материалы подписи, runnermacos-archive - Разделить workflow: Android
ubuntu-latest; PR только L1; main/tag — L2 - Персистентность pub/DerivedData/Pods; 3 дня смотреть P95 wait < 10 мин
- Если
wait >> runсохраняется: сначала L2 в PR, потом добавление машин
PoC с одной Cloud Mac на fast-пул; archive временно на хостинговом macos-latest (принять очередь) — проверить логику, затем вторая машина для SLA релизов. Не противоречит «двум машинам»: вторая страхует releases, не PR-feedback.
8 · FAQ
Все Flutter-тесты на Linux — хватит одного Cloud Mac?
Без macOS-сборки на PR теоретически да — большинство команд гоняют build ios --simulator для native-плагинов. Один Mac на L1 возможен; archive должен быть изолирован от быстрого feedback, иначе релизы блокируют все PR. Две машины — надёжный выбор.
Два Cloud Mac vs два Mac mini в офисе?
Топология та же; разница в эксплуатации: Cloud Mac без закупки железа, снимаемые образы, часто стабильнее канал к GitHub. Офисный Mac при 7×24 и серверной; распределённые команды СНГ чаще берут Cloud Mac — см. также масштабирование при высоком объёме job.
Оставлять Codemagic или хостинговый GitHub macOS?
Как резерв archive или overflow PR. После основного self-hosting хостинговый macOS уместен при < 4 релизах/месяц; активные Flutter-команды долгосрочно часто экономят на своих runner.
Monorepo с несколькими app — как считать job?
Matrix × flavors перемножаются. Больше четырёх macOS job на PR — расширять fast-пул или сжимать matrix, а не впихивать archive в PR.
Два Cloud Mac — CI Flutter для Android и iOS без конфликтов
Android остаётся на Linux; сборка iOS, подпись и экспорт IPA требуют macOS. Два Cloud Mac mini M4 с macos-fast и macos-archive разводят PR-feedback и release-job — выгоднее, чем archive в каждом PR и Queued для всех. Apple Silicon ускоряет flutter build ios и линковку Xcode; ~4 В в простое подходят для CI 7×24.
Против офисной «мак-стойки»: Cloud Mac по тарифу, клонируемые образы, стабильный канал к GitHub — удобно распределённым Flutter-командам в РФ и СНГ. Нативный Unix для Flutter, CocoaPods, Fastlane без лишней виртуализации.
При миграции iOS CI вашей Flutter-команды: старт с двух VPSSpark Cloud Mac mini M4 как минимальная топология — смотреть тарифы, PoC fast-пула, затем archive-пул для релизов.