Сборка iOS в GitHub Actions может выполняться на удалённом Mac через self-hosted runner, но регистрация Runner — только начало: для production нужно отдельно проверить Xcode, подпись, маршрутизацию заданий, Archive, экспорт и загрузку в TestFlight. Такой вариант подходит, если удалённый Mac остаётся доступным после разрыва сеанса и перезапуска, а рабочий процесс допускает к нему только доверенный код и нужные секреты.
Эта статья предназначена для независимых разработчиков, которые уже хранят код в GitHub и хотят автоматизировать сборку и публикацию iOS-приложения. Она также полезна разработчикам на Windows или Linux без локального Mac и небольшим командам, которым требуется контролируемая среда Xcode и безопасное хранение материалов подписи.
Что именно должно быть доказано до production
Надёжный iOS-пайплайн — это не команда, завершившаяся с кодом выхода 0. Его необходимо рассматривать как цепочку независимых состояний:
- исходный код и зависимости восстановлены;
- проект скомпилирован выбранным Xcode;
- создан Archive;
- архив экспортирован с подходящим способом подписи;
- сборка принята системой загрузки;
- обработка сборки в App Store Connect завершилась ожидаемым результатом.
Apple описывает Archive как результат сборки, который затем используется для экспорта и распространения приложения. Поэтому успешная компиляция ещё не подтверждает наличие корректного архива или действительных сертификатов. Подробная схема архивации и распространения приведена в официальной документации Apple о создании и распространении архива.
Для удалённого Mac следует заранее принять четыре условия:
- хост соответствует системным требованиям выбранного Xcode;
- Runner запускается как служба, а не только внутри временного терминала;
- задания публикации получают отдельную метку и не смешиваются с тестовыми задачами;
- секреты появляются только во время конкретной операции и удаляются после неё.
Если хотя бы одно условие не проверено реальным проектом, такой хост лучше считать тестовым, а не постоянным сервером сборки.
Первый шаг: проверить доступность удалённого Mac
GitHub Actions использует self-hosted runner как агент, который принимает задания от репозитория или организации. GitHub официально поддерживает регистрацию Runner на уровне репозитория, организации или предприятия; для этого используются настройки GitHub и одноразовый токен регистрации. Последовательность установки приведена в инструкции GitHub по добавлению self-hosted runner.
После подключения в настройках должен отображаться доступный Runner со статусом, позволяющим принимать задания. Важно не путать доступность macOS-сеанса с доступностью агента: закрытие VNC или SSH не должно останавливать процесс, если он запущен как системная служба.
На этом этапе проверяются следующие ограничения:
- Сетевой доступ. Runner должен устанавливать исходящее соединение с GitHub и обращаться к необходимым сервисам загрузки. Нестабильный канал проявится не только при скачивании зависимостей, но и во время передачи крупного архива.
- Постоянное выполнение. Интерактивный запуск в терминале зависит от открытой сессии, прав текущего пользователя и состояния удалённого подключения.
- Диск. Xcode, симуляторы, DerivedData, архивы и кэш зависимостей постепенно расходуют хранилище. Если место заканчивается во время Archive, ошибка может выглядеть как проблема проекта или подписи.
- Права. Пользователь Runner должен иметь доступ к рабочей директории, инструментам разработки и временному Keychain, но сам workflow не должен получать лишние административные возможности без необходимости.
- Перезапуск. После перезагрузки хоста служба должна запускаться автоматически, иначе очередь GitHub Actions будет расти до ручного вмешательства.
Для первичной проверки достаточно создать безопасное диагностическое задание, которое выводит версию macOS, выбранный путь к Xcode и состояние Runner, но не печатает секреты:
name: Проверка удалённого Mac
on:
workflow_dispatch:
jobs:
inspect-runner:
runs-on: [self-hosted, macOS, ARM64, ios-release]
steps:
- name: Проверить среду
run: |
sw_vers
xcode-select -p
xcodebuild -version
uname -m
Здесь ios-release — пример пользовательской метки, а не готовое имя для копирования. Название должно соответствовать метке, назначенной конкретному Runner.
Как направить iOS-задание на правильный self-hosted runner
GitHub сопоставляет значения runs-on с метками Runner. Обычно используются метки, описывающие операционную систему и архитектуру, а для производственных задач добавляется собственная метка назначения. Правила назначения и применения пользовательских меток описаны в документации GitHub о labels.
Без пользовательской метки workflow может попасть на другой macOS Runner, предназначенный для тестирования. Это особенно опасно, когда на разных хостах установлены разные версии Xcode, разные сертификаты или различающиеся переменные окружения.
Пример маршрутизации:
| Задание | Метки в runs-on |
Для чего подходит | Риск |
|---|---|---|---|
| Бысточная проверка кода | self-hosted, macOS, ARM64, ios-test |
Компиляция без публикации | Нельзя считать результатом релиза |
| Подготовка релиза | self-hosted, macOS, ARM64, ios-release |
Archive и экспорт | Требует доверенного репозитория |
| Изолированный проект | self-hosted, macOS, ARM64, project-a-release |
Один проект или команда | Нужно поддерживать метки при замене хоста |
| Несовместимое окружение | Метка отсутствует у всех Runner | Диагностика настройки | Задание останется в очереди |
Если не существует Runner со всеми указанными метками, GitHub не «подберёт приблизительный» хост: задание будет ожидать подходящего агента. Поэтому после изменения меток полезно запускать workflow вручную и проверять, что он действительно принят нужным Runner.
Для производственного задания лучше использовать отдельную метку, связанную с назначением, а не с именем сотрудника или временным адресом. При замене хоста метку можно перенести, не переписывая весь workflow.
Второй шаг: зафиксировать Xcode и зависимости
Запрос «self-hosted runner как закрепить Xcode» на практике сводится к контролю трёх уровней: системной совместимости, активного пути разработчика и воспроизводимости проекта.
Apple публикует актуальные требования к сочетанию macOS, Xcode и SDK на странице системных требований Xcode. Нельзя заранее вписывать в workflow случайные версии: перед каждым обновлением нужно сверять официальную таблицу с минимальной системой и текущими требованиями App Store Connect.
Если на удалённом Mac установлено несколько версий Xcode, задание должно явно выбирать одну из них:
sudo xcode-select --switch "/Applications/Xcode.app"
xcodebuild -version
xcodebuild -showsdks
Путь выше является примером и должен быть заменён на фактический путь в окружении. Не следует полагаться на то, какой Xcode был выбран предыдущим пользователем или последним интерактивным сеансом.
Проверка инструментальной базы должна включать:
- фиксированный путь
xcode-select; - конкретный Scheme, предназначенный для архивирования;
- lock-файлы зависимостей;
- одинаковый способ восстановления Swift Package Manager, CocoaPods или другого менеджера;
- явную конфигурацию
Release; - контроль Bundle ID и команды разработки без вывода секретных значений в лог.
Сначала выполняется минимальная сборка без сертификатов и профилей. Она отвечает на вопрос, может ли Runner получить исходный код, восстановить зависимости и скомпилировать проект. Только после этого добавляются подпись и экспорт. Такой порядок отделяет ошибку toolchain от ошибки credential.
Для сохранения диагностической ценности не стоит сразу очищать весь DerivedData после каждого запуска. Лучше определить допустимый кэш и периодически удалять его по контролируемому правилу. Кэш ускоряет повторную сборку, но способен скрыть проблему, когда старые артефакты маскируют отсутствие зависимости или изменение настроек.
Где безопаснее хранить сертификаты iOS и секреты
Вопрос о том, где хранить сертификат подписи GitHub Actions, нельзя решать одной загрузкой API-ключа. Для публикации могут потребоваться разные материалы: сертификат и закрытый ключ, Provisioning Profile, а также отдельные данные для взаимодействия с App Store Connect. Apple объясняет назначение сертификатов и профилей в обзоре сертификатов и Provisioning Profile.
Ни один из этих материалов не должен попадать в репозиторий, историю коммитов, открытый пример workflow или обычный текстовый лог. В GitHub Secrets следует хранить только значения, необходимые конкретному заданию, а область доступа ограничивать репозиторием или окружением релиза. Для production полезно требовать ручное подтверждение окружения перед выдачей секретов.
Практическая схема разделения выглядит так:
- сертификат подписи и закрытый ключ — зашифрованное значение, импортируемое во временный Keychain;
- Provisioning Profile — временный файл с ограниченными правами;
- данные App Store Connect — отдельные секреты для загрузки;
- пароль временного Keychain — случайное значение, доступное только текущему заданию;
- идентификаторы проекта — обычные переменные, если они не являются секретом.
GitHub отдельно предупреждает о рисках self-hosted Runner: такой хост может быть опасен для выполнения недоверенного кода. Рекомендации по ограничению доступа и защите агента приведены в документе GitHub о безопасном использовании self-hosted Runner.
Поэтому постоянный Runner для публикации не следует подключать к общедоступному репозиторию, если workflow способен выполнять код внешних участников. Нельзя также разрешать произвольной ветке вызывать production-окружение. Минимальная граница должна включать доверенный приватный репозиторий, ограниченный список веток и отдельную метку ios-release.
Пример последовательности без реальных значений:
security create-keychain -p "$TEMP_KEYCHAIN_PASSWORD" build.keychain-db
security import "$SIGNING_CERTIFICATE_PATH" \
-k build.keychain-db \
-P "$CERTIFICATE_PASSWORD" \
-T /usr/bin/codesign
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
cp "$PROFILE_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles/"
После экспорта необходимо удалить временный Keychain, профиль, распакованные сертификаты и временные файлы:
security delete-keychain build.keychain-db || true
rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/"*.mobileprovision
rm -f "$SIGNING_CERTIFICATE_PATH" "$PROFILE_PATH"
В примерах намеренно отсутствуют Bundle ID, Team ID, имена сертификатов, токены, пароли и пути с реальными данными. Их нельзя заменять правдоподобными «тестовыми» значениями, если пример может быть случайно запущен в рабочем репозитории.
Как пережить перезапуск удалённого Mac
Для постоянного iOS-сервера важен не только первый запуск Runner, но и автоматическое восстановление. GitHub описывает установку и управление Runner как службой в официальной инструкции по настройке приложения Runner.
Интерактивный процесс подходит для диагностики: он показывает вывод на экране и помогает увидеть ошибку регистрации. Для постоянной работы он уязвим — закрытие терминала, разрыв SSH или выход пользователя остановит агент. Служба запускается независимо от удалённого графического сеанса и должна быть предпочтительным вариантом после первичной проверки.
Порядок проверки восстановления:
- зарегистрировать Runner на уровне нужного репозитория или организации;
- выполнить диагностическое задание и убедиться, что метки совпадают;
- установить Runner как службу согласно текущей инструкции GitHub;
- остановить интерактивный процесс, если он ещё запущен;
- перезапустить Mac;
- проверить, что служба вернулась в состояние готовности;
- отправить безопасное тестовое задание;
- отдельно проверить, что рабочий каталог не содержит оставшихся секретов.
Во время восстановления нужно учитывать не только сам Runner. После перезапуска могут измениться активный Xcode, доступ к Keychain, состояние кэша, права рабочей директории и доступность инструмента командной строки. Поэтому диагностический workflow после перезагрузки должен повторно выводить только несекретные сведения о системе и выбранном Xcode.
Для анализа зависших заданий применяются рекомендации GitHub по мониторингу и устранению неисправностей Runner. Полезно сохранять время старта задания, выбранные метки, этап остановки и ссылку на workflow. Это позволяет отличить отсутствие подходящего Runner от сбоя компиляции или проблем передачи архива.
Третий шаг: разделить сборку, Archive, экспорт и загрузку
Рабочий процесс должен состоять из этапов с отдельными логами, даже если часть команд выполняется в одном job. Минимальная схема:
jobs:
ios-release:
runs-on: [self-hosted, macOS, ARM64, ios-release]
timeout-minutes: 30
environment: production
steps:
- uses: actions/checkout@v4
- name: Восстановить зависимости
run: ./ci/restore-dependencies.sh
- name: Скомпилировать без подписи
run: ./ci/build-test.sh
- name: Создать Archive
run: ./ci/archive.sh
- name: Экспортировать подписанный пакет
run: ./ci/export.sh
- name: Загрузить сборку
run: ./ci/upload.sh
- name: Очистить временные данные
if: always()
run: ./ci/cleanup-secrets.sh
Версии action и команды в этом фрагменте являются структурным примером. В реальном проекте они должны быть закреплены и проверены, а не заменены непроверенными копируемыми рецептами.
Каждый этап должен оставлять понятный результат:
| Этап | Что подтверждает | Что сохранить для разбора | Что ещё не доказано |
|---|---|---|---|
| Восстановление | Доступ к исходному коду и зависимостям | Версии инструментов и краткий лог | Успешную подпись |
| Компиляция | Исходники собираются выбранным Xcode | Лог ошибок компилятора | Наличие Archive |
| Archive | Создан архив приложения | Имя и путь артефакта | Пригодность профиля |
| Экспорт | Получен пакет нужного типа | Метаданные экспорта без секретов | Приём сервером |
| Загрузка | Файл передан системе Apple | Идентификатор операции и статус | Завершение фоновой обработки |
| Обработка | Сборка появилась среди доступных сборок | Ссылка или номер сборки без токенов | Результат ручного тестирования |
Загрузка в TestFlight — не то же самое, что успешный экспорт .ipa. Apple отдельно документирует передачу сборок и дальнейшую работу с ними в инструкции по загрузке сборок в App Store Connect. В логах следует различать сетевую передачу, ответ сервиса и последующую обработку.
Для первого запуска лучше использовать обезличенный тестовый проект с той же схемой подписи и похожими зависимостями. После завершения загрузки нужно убедиться, что сборка отображается в App Store Connect, а не ограничиваться сообщением команды о завершении.
Как выбрать архитектуру пайплайна для команды
Ниже приведено сравнение подходов, которое помогает решить, когда удалённый Mac действительно нужен как постоянный Runner, а когда достаточно временного хоста.
| Вариант | Контроль Xcode и окружения | Постоянство | Контроль секретов | Когда выбирать |
|---|---|---|---|---|
| Общий хост для тестов и релиза | Низкий или средний | Зависит от других задач | Сложнее изолировать | Только для некритичных проверок |
| Отдельный удалённый Mac для релиза | Высокий | Можно настроить службой | Проще ограничить окружение | Для регулярных публикаций небольшого проекта |
| Временный удалённый Mac | Настраивается на запуск | Не рассчитан на постоянную очередь | Меньше долгоживущих данных | Для пробного релиза или миграции |
| Локальный Mac разработчика | Высокий | Зависит от рабочего места | Секреты находятся рядом с кодом | Для ручной разработки и отладки |
| Runner без фиксированных меток | Непредсказуемый | Формально доступен | Высокий риск ошибочного назначения | Для production не подходит |
Разумный путь для независимого разработчика — сначала проверить workflow на краткосрочной аренде удалённого Mac, а затем решить, нужен ли постоянный хост. На странице вариантов аренды RUVCLOUD можно сопоставить такой тест с покупкой отдельного Mac: сравнивать следует не только стоимость доступа, но и время настройки, необходимость обслуживания и возможность остановить среду после выпуска.
Удалённая среда особенно полезна, когда локальный компьютер работает на Windows или Linux, а Xcode, нативные зависимости и подпись нужны только для отдельных этапов. Если же проект требует ежедневной графической отладки симулятора, физического USB-устройства или постоянной тяжёлой сборки, локальный Mac может оказаться удобнее. В таком случае аренда не отменяет технических ограничений — она лишь переносит macOS-среду в дата-центр.
Четвёртый шаг: провести финальную приёмку по измеримым признакам
Перед переводом Runner в постоянный режим следует выполнить чек-лист. Галочки здесь означают не намерение, а подтверждение конкретным запуском и журналом.
- [ ] Версия macOS совместима с выбранным Xcode по актуальной таблице Apple.
- [ ] Активный путь
xcode-selectзадан явно. - [ ] Scheme для Archive не зависит от личных настроек разработчика.
- [ ] Lock-файлы зависимостей находятся в репозитории и используются в workflow.
- [ ] Минимальная сборка без подписи завершается на чистом рабочем каталоге.
- [ ] Archive создаётся в отдельной директории и сохраняется как артефакт по принятому правилу.
- [ ] Сертификат, закрытый ключ и Provisioning Profile не записываются в Git.
- [ ] Production-секреты доступны только доверенному окружению.
- [ ] Внешний pull request не может вызвать публикационный Runner.
- [ ] Пользовательская метка отличает релизный хост от тестового.
- [ ] При отсутствии совпадающих меток задание остаётся в очереди, а не запускается на случайном агенте.
- [ ] Runner запускается после перезагрузки Mac как служба.
- [ ] После восстановления повторно проверяются Xcode и рабочая директория.
- [ ] После задания удаляются временный Keychain, профиль и распакованные секреты.
- [ ] Отдельно зафиксированы статусы Archive, экспорта, загрузки и фоновой обработки.
- [ ] Команда знает, как отозвать регистрацию Runner и заменить секреты.
Итог приёмки стоит оформить одним из трёх решений:
| Результат | Условия | Следующее действие |
|---|---|---|
| Готов к постоянной работе | Все этапы прошли на доверенном проекте, перезапуск проверен, секреты очищаются | Разрешить ограниченные релизные ветки |
| Требует исправлений | Сборка проходит, но есть пробелы в логах, очистке или маршрутизации | Оставить Runner тестовым и повторить проверку |
| Не подходит для постоянного Runner | Хост не восстанавливается, Xcode несовместим или нельзя изолировать недоверенный код | Использовать другой хост или временную среду |
Нужно хранить запись о регистрации, изменениях меток, перезапусках и отзыве доступа. Такая история пригодится после смены проекта, сотрудника или сертификата и не требует помещать секреты в журнал.
Что выбрать после проверки
Если текущая схема строится на Windows или Linux, ручное подключение к отдельному Mac обычно оставляет несколько слабых мест: сборка зависит от присутствия разработчика, сертификаты приходится переносить вручную, а перезапуск и состояние Xcode трудно контролировать. Облачный workflow без закреплённой macOS-среды, в свою очередь, может не дать нужного контроля над нативными зависимостями и версиями инструментов.
Для короткого эксперимента или миграции практичнее взять удалённый Mac в RUVCLOUD, зарегистрировать на нём Runner и прогнать реальный Archive с подписанием и загрузкой. Если такой тест подтвердит восстановление после перезапуска, безопасное хранение ключей и понятную диагностику отказов, удалённый Mac можно оставить постоянным iOS-сервером; подходящие варианты доступны через страницу заказа RUVCLOUD. Если же проект требует физического устройства или постоянной тяжёлой интерактивной работы, сначала стоит сравнить аренду с собственным Mac и не переводить временный CI-хост в единственную среду разработки.
Главное решение принимается не в момент регистрации Runner, а после полного испытания: Xcode должен быть закреплён, подпись — изолирована, маршрут задания — однозначен, а TestFlight — подтверждён после обработки сборки. Только при выполнении этих условий GitHub Actions на удалённом Mac становится предсказуемым инструментом публикации, а не ещё одним ручным этапом.