01

Решение на эту неделю

GitHub указывает, что задание в очереди self-hosted Runner может автоматически отмениться после 24 часов ожидания. Поэтому один «онлайн» Mac-сервер ещё не означает непрерывность публикации. В течение этой недели мы рекомендуем перевести production-сборки на двухконтурную схему: основной Mac-узел плюс резервный узел, который заранее проходит реальную сборку.

Код, зависимости, подпись и артефакты должны восстанавливаться независимо от состояния основного компьютера. Постоянную базовую нагрузку разумно держать на долгосрочном узле, а пик релизов, миграцию Xcode и аварийное окно закрывать дополнительным удалённым Mac.

Эта статья предназначена для трёх групп:

  • IT-руководителей, управляющих одним или несколькими Mac-узлами;
  • руководителей разработки и инженерной эффективности, отвечающих за iOS CI/CD, очереди и подпись;
  • CTO и технических директоров, которые согласуют резервирование, SLA и закупку Mac-ресурсов.
02

От единственного узла к проверяемому резерву

Сбой во время публикации обычно начинается не с полного выключения компьютера. Runner может быть недоступен, зависнуть после обновления, потерять сетевой доступ или продолжить работать без возможности подписать приложение. В каждом случае последствия различаются.

Мы фиксируем четыре параметра до выбора архитектуры:

  1. RTO — за какое время нужно восстановить возможность принимать новые сборки.
  2. RPO — какие результаты текущих заданий допустимо потерять.
  3. Объём блокировки — один pull request, релизная ветка или вся публикация.
  4. Точка подтверждения — какой артефакт доказывает, что восстановление завершено.

Эти параметры нельзя заменять одним обещанием «доступности сервера». Для production важнее подтвердить маршрут от остановки основного узла до подписанного и проверяемого артефакта.

Границы архитектур

Вариант Когда подходит Что ломается первым Условие допуска
Один Mac Низкая критичность, редкие сборки Любой сбой блокирует очередь Приемлема ручная пауза
Холодный резерв Редкие релизы, ограниченный бюджет Среда может устареть Регулярная проверка запуска
Тёплый резерв Production iOS CI/CD Нужно поддерживать две среды Реальная сборка по расписанию
Два активных узла Постоянная нагрузка и короткие окна публикации Сложнее маршрутизация и подпись Нужны правила очереди и раздельные labels

Для большинства команд переход от одного узла к тёплому резерву даёт лучший первый результат. Это не обязательно означает постоянную полную загрузку второго Mac. Но резервный узел должен быть включён в регулярную проверку, иначе при аварии мы обнаружим не резерв, а неготовую копию.

В GitHub Actions задания направляются на self-hosted Runner через labels и группы. Сервис выбирает доступный узел, соответствующий всем заданным признакам; если подходящего Runner нет, задание остаётся в очереди. Эти правила нужно проверять по документации конкретной CI-платформы, а не переносить между системами без проверки. (GitHub: маршрутизация заданий на self-hosted Runner)

03

Остановка публикации и переключение

Влияние

Если единственный Mac выходит из строя в момент релиза, уже запущенная задача может потерять рабочее состояние. Новые задачи будут ждать подходящий Runner. При неверно настроенном маршруте резервный узел может оставаться доступным, но не получать задания из-за несовпадающего label.

Мы отдельно проверяем:

  • какие задачи уже выполняются;
  • какие задания можно безопасно повторить;
  • где хранятся исходники и артефакты;
  • кто имеет право остановить приём новых задач;
  • какой узел считается единственным источником истины для статуса публикации.

Предотвращение

Основной и резервный Mac должны использовать одинаковые логические признаки, но не обязательно одинаковые имена. Например, для production-пайплайна можно задать общие labels self-hosted, macOS, ARM64 и отдельный label среды вроде ios-release.

Не следует направлять release-задачи только на имя конкретного компьютера. Иначе переключение потребует ручного изменения нескольких workflow-файлов. Лучше отделить требования задачи — архитектура, ОС, версия среды — от текущего физического узла.

Для конфигурации проекта используйте текстовые .xcconfig-файлы. Apple описывает их как способ хранить параметры сборки в исходном коде и разделять настройки для разных конфигураций. Это уменьшает зависимость от ручных значений в интерфейсе Xcode. (Apple: файлы конфигурации сборки Xcode)

Действие при отказе

Наш runbook переключения должен выглядеть так:

  1. Запретить основному Runner принимать новые задания.
  2. Зафиксировать список выполнявшихся и ожидающих задач.
  3. Проверить статус резервного узла в CI-платформе.
  4. Проверить доступ к репозиторию и хранилищу артефактов.
  5. Проверить версии macOS, Xcode и SDK.
  6. Проверить Keychain, сертификаты и provisioning profiles.
  7. Запустить smoke-сборку из той же ветки.
  8. Перевести production-задачи на резервный label или группу.
  9. Сохранить логи, идентификатор задания и полученный артефакт.
  10. Разрешить повтор публикации только после ручного подтверждения.

Первые две задачи после переключения не должны считаться доказательством успеха только потому, что они завершились без ошибки Runner. Нам нужен артефакт, который проходит ожидаемую проверку подписи и загрузки.

Доказательство

Минимальный набор подтверждений:

  • журнал остановки приёма задач;
  • лог регистрации и состояния резервного Runner;
  • результат smoke-сборки;
  • результат архивирования;
  • проверка подписи;
  • ссылка на артефакт;
  • отметка о ручном вмешательстве.
04

Обновление macOS и Xcode

Влияние

Плановое обновление способно создать такой же перерыв, как аппаратный отказ. Новая версия Xcode может изменить инструменты, SDK, build settings или требования подписи. Apple отмечает, что параметры сборки управляют компиляцией, связыванием, отладочной информацией и упаковкой продукта. Поэтому простое совпадение названия версии Xcode ещё не доказывает идентичность среды. (Apple: справочник параметров сборки)

Мы используем двухколейную модель:

  • основной узел сохраняет стабильную версию для текущих production-сборок;
  • резервный узел первым получает обновление и выполняет реальный проект.

Тестовый проект из одного шаблонного приложения здесь недостаточен. Нужны реальные зависимости, плагины, схемы, приватные пакеты, тесты и этап экспорта.

Базовая таблица среды

Перед изменением версии мы фиксируем в репозитории или внутреннем реестре:

  • версию macOS;
  • версию Xcode;
  • используемый SDK;
  • архитектуру Apple Silicon или другую поддерживаемую архитектуру;
  • версии Swift Package и lock-файлы;
  • параметры .xcconfig;
  • bundle identifier и team ID;
  • сертификаты и provisioning profiles;
  • команды xcodebuild;
  • версии Runner и shell-скриптов;
  • адреса приватных репозиториев и правила доступа.

Данные о сертификатах и профилях нельзя оставлять только на диске Mac. Apple указывает, что provisioning profile связан с bundle ID, сертификатами и идентификаторами устройств, а его удаление или обновление влияет на последующую подпись приложения. (Apple: управление provisioning profiles)

Приёмка обновления

Резервный узел допускается в production только после прохождения одинакового набора:

  • сборка Debug;
  • сборка Release;
  • unit-тесты;
  • UI-тесты, если они входят в публикацию;
  • архивирование;
  • экспорт;
  • проверка подписи;
  • загрузка в используемый канал распространения;
  • публикация тестового артефакта в хранилище.

Если обновление не прошло, основной узел остаётся на стабильной версии. Мы не меняем два узла одновременно: иначе при ошибке невозможно определить, вызвана ли проблема Xcode, macOS, зависимостью или маршрутизацией.

05

Перезапуск и недоступность хоста

Разные виды отказа

«Mac не отвечает» — слишком общее описание. Для восстановления нужно различать:

  • отключение питания;
  • зависание macOS;
  • остановку Runner;
  • потерю SSH или VNC;
  • ошибку запуска сервиса;
  • блокировку зашифрованного тома;
  • отсутствие сетевого маршрута после перезапуска.

Apple документирует удалённый перезапуск Mac через SSH командой sudo shutdown -r now. Но доступ по SSH не решает проблему, если система не загрузилась, сеть не поднялась или требуется разблокировка тома. (Apple: удалённый перезапуск Mac через Terminal)

На Mac с Apple Silicon и macOS 26 или более поздней версией Apple отдельно описывает возможность разблокировки FileVault через SSH после перезапуска при включённом Remote Login и доступной сети. Это условие нужно проверять именно для используемой версии macOS и политики управления устройствами. (Apple Platform Security: управление FileVault)

Подготовка до аварии

На каждом резервном узле мы заранее проверяем:

  • доступ администратора по SSH;
  • доступ через предусмотренный канал удалённого управления;
  • запуск Runner после перезагрузки;
  • автоматический запуск нужных сервисов;
  • сетевой доступ к репозиторию;
  • состояние зашифрованного диска;
  • наличие процедуры разблокировки;
  • очистку временных файлов после оборванной сборки;
  • повторную регистрацию Runner, если это требуется;
  • передачу логов во внешнее хранилище.

Не следует считать успешным тест, в котором инженер вручную вошёл в графическую сессию, открыл Xcode и исправил окружение. Такой сценарий показывает только возможность восстановления при наличии специалиста рядом.

Чек-лист приёмки

  • [ ] Основной узел можно вывести из маршрута без изменения исходного кода.
  • [ ] Резервный Runner виден как online и idle.
  • [ ] После перезапуска Runner возвращается автоматически.
  • [ ] SSH-доступ восстанавливается или существует подтверждённая альтернатива.
  • [ ] FileVault не создаёт неизвестную точку ручного вмешательства.
  • [ ] Реальный проект собирается после чистого запуска.
  • [ ] Логи перезапуска и Runner сохраняются.
  • [ ] Ответственный за эскалацию назначен заранее.
06

Пиковая очередь и временная ёмкость

Влияние

Очередь может расти по двум разным причинам:

  1. один Mac не справляется с объёмом параллельных задач;
  2. средняя длительность сборки выросла из-за кэша, зависимостей, тестов или внешнего сервиса.

Различать эти причины нужно по тренду: длина очереди, длительность задания, доля занятых Runner, время ожидания и число повторных запусков. Один результат локального бенчмарка не отвечает на вопрос о требуемом количестве узлов.

Для GitHub Actions важно учитывать, что задание без подходящего Runner остаётся в очереди, а после установленного сервисом периода ожидания отменяется. Кроме того, Runner должен поддерживать актуальную версию приложения: GitHub предупреждает, что при отсутствии обновления в течение 30 дней задания могут перестать направляться на такой узел. (GitHub: ограничения и очереди Actions; GitHub: требования к self-hosted Runner)

Стратегии ёмкости

Постоянный резерв имеет смысл, когда:

  • публикации идут регулярно;
  • окно релиза короткое;
  • простой напрямую блокирует доход;
  • среда содержит сложные приватные зависимости;
  • проверка восстановления должна быть автоматической.

Временный удалённый Mac подходит, когда:

  • нагрузка резко возрастает только в отдельные периоды;
  • происходит миграция Xcode;
  • нужно провести аварийную тренировку без покупки физического узла;
  • команда хочет проверить Apple Silicon до долгосрочного решения;
  • в офисе или дата-центре нет свободного резервного оборудования.

Для финансовой модели достаточно зафиксировать четыре переменные:

  • базовая ёмкость;
  • пиковая ёмкость;
  • длительность использования дополнительного узла;
  • часы эксплуатации и проверки.

Не стоит заранее обещать экономию или конкретный срок окупаемости. Сначала измеряется фактическая очередь и стоимость ручного восстановления. Затем сравниваются постоянный резерв, покупка Mac и периодическая аренда удалённого Mac.

07

Сертификаты, Keychain и зависимости

Почему компиляция не равна восстановлению

Резервный Mac может успешно собрать бинарник, но всё равно не пройти production-проверку. Типовые причины:

  • отсутствует приватный ключ;
  • provisioning profile просрочен;
  • Keychain закрыт;
  • у Apple-аккаунта недостаточно прав;
  • нет доступа к приватному Swift Package;
  • SSH-ключ не установлен;
  • кэш зависимостей недоступен;
  • переменная CI-секрета не передана в новый Runner.

Apple прямо связывает code signing с действительным сертификатом в Keychain, team ID, профилем и настройками подписи. Ошибка в одном элементе делает сборку непригодной для распространения. (Apple: настройка параметров target)

Контролируемое восстановление

Мы ведём отдельный реестр:

  • сертификат и его назначение;
  • владелец;
  • срок действия;
  • место контролируемого хранения;
  • связанный provisioning profile;
  • ответственный за выдачу;
  • процедура отзыва;
  • дата последней проверки;
  • результат тестовой подписи.

Секреты передаются только на конкретный резервный узел и только в требуемом объёме. Копирование всего ~/Library или полного пользовательского каталога запрещено как метод синхронизации: оно переносит лишние данные, скрытые настройки и неясные права доступа.

Зависимости должны восстанавливаться из lock-файлов и контролируемых источников. Кэш ускоряет сборку, но не должен быть единственным источником пакетов. Если резервный Mac может собрать проект только благодаря старому локальному кэшу, это не резервируемая среда.

08

Восстановительная тренировка

Мы проводим тренировку не на абстрактном тестовом проекте, а на реальном production-пайплайне с безопасным артефактом.

Порядок действий

  1. Назначить окно и ответственных.
  2. Зафиксировать версии macOS, Xcode и зависимостей.
  3. Запустить контрольную сборку на основном узле.
  4. Остановить приём новых задач основным Runner.
  5. Имитировать недоступность основного Mac.
  6. Проверить состояние резервного узла.
  7. Подключить резервный Mac к репозиторию и секретам.
  8. Запустить сборку из той же commit-версии.
  9. Проверить подпись, архив и экспорт.
  10. Сохранить логи и артефакты.
  11. Вернуть основной узел в безопасный режим.
  12. Повторно проверить обычную маршрутизацию.

В отчёте фиксируем не только успешные шаги. Обязательно записываем:

  • где потребовалось ручное действие;
  • какой секрет отсутствовал;
  • сколько задач пришлось повторить;
  • какой лог не сохранился;
  • кто имел право изменить маршрут;
  • какое действие невозможно выполнить удалённо;
  • какие версии нужно зафиксировать перед следующей тренировкой.

После первой тренировки команда выбирает между холодным резервом, тёплым резервом и дополнительной пиковой ёмкостью по фактическим результатам. Нельзя заранее объявлять одну схему самой дешёвой: стоимость зависит от частоты публикаций, очереди, ручных операций и требований к восстановлению.

09

FAQ для IT-руководителя

Вопросы ниже дополняют runbook и помогают использовать его как критерий допуска новой Mac-инфраструктуры.

10

Итоговая рекомендация

Если текущая схема строится вокруг одного физического Mac, её слабое место — не только вероятность аппаратной поломки. Есть ещё очередь, устаревшая версия Xcode, закрытый Keychain, недоступный приватный пакет, неработающий Runner и неизвестная процедура удалённого запуска.

Поэтому мы рекомендуем начать не с покупки максимального числа компьютеров, а с одной проверяемой пары: стабильный основной узел и резервный Mac с тем же реальным проектом. Для постоянной базовой нагрузки можно оставить долгосрочный узел, а для релизных пиков и миграции Xcode подключать дополнительную ёмкость по мере необходимости.

Если существующая инфраструктура не позволяет постоянно держать резервный Mac, разумно оценить удалённый Mac для временного CI/CD-резерва. Такой вариант не отменяет проверку сертификатов, доступа, Runner и артефактов, но позволяет провести полноценную тренировку и понять, сколько ресурсов действительно требуется до закупки собственного оборудования. Дополнительные параметры аренды и доступные варианты можно сопоставить со сценарием заказа Mac mini для команды.