Главная / Блог / GitLab CI и сборка iOS: удалённый Mac, руководство 2026
ENGINEERING_BLOG · 2026.08.31

GitLab CI и сборка iOS: удалённый Mac, руководство 2026

Linux Runner завершает этап сборки, но iOS-задача сообщает «Xcode не найден».

Быстрое решение: направьте iOS Job на отдельный macOS Runner с Shell executor, сохраните пользовательскую сессию, зафиксируйте активный Xcode, изолируйте подпись и проверьте шесть условий — сессию, инструментальную цепочку, маршрутизацию, полномочия, артефакты и восстановление после перезапуска.

Эта статья для вас, если вы храните исходный код в GitLab, а текущий Runner работает только с Android или серверными проектами. Она также пригодится, если вы хотите заменить ручной Xcode Archive на воспроизводимый GitLab CI-процесс или оцениваете удалённый Mac как постоянный узел для небольшой команды.

SECTION 01Почему сборка iOS в GitLab CI начинается не с YAML

iOS Job нельзя считать рабочим только потому, что Runner зарегистрирован и Pipeline получил зелёный статус на подготовительном этапе. Реальная сборка должна попасть на macOS-хост, где доступны Xcode, iOS SDK, инструменты подписи и пользовательский Keychain. Linux Runner может подготовить исходники, запустить анализ или собрать backend, но он не заменяет macOS-среду для Xcode Build и Archive.

GitLab документирует запуск Runner в macOS через пользовательский LaunchAgent. Это важная граница: сервис не следует рассматривать как полностью независимый системный демон, одинаково работающий до входа пользователя и после него. Для некоторых операций процессу нужны права и окружение конкретной пользовательской сессии. Подробности о режиме службы и установке приведены в официальной документации GitLab для macOS Runner.

В производственной схеме проверяйте не один статус «online», а цепочку:

  1. нужный пользователь вошёл в macOS;
  2. LaunchAgent Runner запущен;
  3. Runner подключён к GitLab;
  4. он принимает задания с нужным тегом;
  5. активен ожидаемый Xcode;
  6. подпись, Archive и загрузка в TestFlight проходят после восстановления.

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

Что означает ошибка «Xcode не найден»

Сообщение может означать несколько разных проблем:

  • Job действительно попала на Linux Runner;
  • macOS Runner имеет тег, который не указан в задании;
  • Xcode установлен, но xcode-select указывает на другую директорию;
  • Runner запущен в пользовательской среде без ожидаемого PATH;
  • пользовательская сессия отсутствует, поэтому недоступен нужный Keychain;
  • проект требует SDK или компонент, отсутствующий в выбранной версии Xcode.

Проверяйте фактически вызываемый инструмент, а не список приложений в /Applications. В тестовом задании используйте обезличенный вывод:

set -eu

xcode-select -p
xcodebuild -version
xcrun --sdk iphoneos --show-sdk-path
which xcodebuild

Не публикуйте в открытом журнале домашний путь пользователя, внутренние имена хоста или значения переменных с секретами. Сам факт наличия нескольких каталогов Xcode не доказывает, что Pipeline использует нужный каталог. Важен результат команды внутри того же Shell executor и под той же учётной записью, которая будет выполнять релиз.

SECTION 02Первый критерий: сможет ли macOS Runner пережить перезапуск

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

После перезапуска действуйте по цепочке:

  1. войдите под выделенной учётной записью macOS либо проверьте утверждённый механизм восстановления;
  2. убедитесь, что пользовательский LaunchAgent снова запустил Runner;
  3. проверьте локальное состояние службы и подключение к GitLab;
  4. отправьте безопасный диагностический Job с тегом macOS;
  5. повторите проверку xcode-select, xcodebuild и xcrun;
  6. выполните небольшой тестовый шаг, не использующий производственные сертификаты;
  7. только затем запускайте Archive в защищённой ветке.

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

SECTION 03Второй критерий: совпадает ли инструментальная цепочка

Повторяемость сборки зависит от совместной фиксации нескольких компонентов:

  • активной директории Xcode;
  • iOS SDK и командных инструментов;
  • схемы и конфигурации проекта;
  • зависимостей Swift Package Manager, CocoaPods или другого менеджера;
  • переменных окружения и Shell-профиля;
  • параметров подписи и профиля распространения.

Не смешивайте три разных результата:

  • разрешение зависимостей прошло;
  • обычная компиляция прошла;
  • Archive для распространения успешно создан.

Первый результат подтверждает доступ к зависимостям, но ничего не говорит о сертификате. Второй подтверждает компиляцию, но не гарантирует корректный экспорт. Только Archive вместе с экспортом и загрузкой проверяет релизную цепочку.

Чтобы получить доказательство, возьмите один фиксированный коммит и запустите его два раза в чистом или заранее очищенном рабочем каталоге. Сравнивайте:

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

Файл блокировки должен участвовать в контроле изменений. Для Swift Package Manager это может быть Package.resolved, для других инструментов — соответствующий lock-файл. Не объявляйте кэш действительным после изменения файла блокировки только потому, что имя ветки осталось прежним.

SECTION 04Как выбрать маршрутизацию и границы изоляции

Для публикации используйте отдельный проектный Runner или узел, доступный только определённому проекту и группе проектов. Присвойте ему понятный тег, например ios-release-macos, но замените это значение на собственное нейтральное имя. В .gitlab-ci.yml тег должен явно направлять релизный Job на нужный хост:

ios_archive:
  tags:
    - ios-release-macos
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
  script:
    - ./ci/archive.sh

Пример содержит условные названия ветки и скрипта. Не переносите его в производство без проверки модели ветвления. Правила маршрутизации, теги и защищённые задания описаны в документации GitLab о конфигурации Runner.

Shell executor запускает команды непосредственно на хосте, поэтому его изоляция ограничена. Скрипт из задания потенциально взаимодействует с файлами пользователя, установленными программами, кэшем зависимостей и Keychain. Это означает, что релизный Runner нельзя бездумно подключать к непроверенным репозиториям или разрешать ему выполнять произвольные Merge Request без контроля.

Важно: защищённая переменная не превращает Shell executor в контейнер. Если недоверенный скрипт получает доступ к тому же пользователю, рабочему каталогу или Keychain, секреты и локальные файлы всё равно находятся в зоне риска.

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

SECTION 05Третий критерий: как не перепутать переменные и материалы подписи

В iOS-публикации участвуют разные сущности, и каждая закрывает свою задачу:

Компонент Назначение Что проверить в CI
CI/CD variable Передача значения или файла в Job Защищена ли переменная и скрывается ли значение в журнале
App Store Connect API Key Авторизация действий в App Store Connect Достаточна ли роль и не используется ли ключ шире необходимого
Сертификат Подтверждение права на конкретный тип подписи Совпадает ли назначение сертификата с операцией экспорта
Приватный ключ Криптографическая часть подписи Импортирован ли он в нужный Keychain и доступен ли процессу
Provisioning Profile Связывает приложение, команду, возможности и способ распространения Соответствуют ли Bundle ID и entitlements проекту
Keychain Хранилище ключей и сертификатов macOS Разблокирован ли он в пользовательской сессии

App Store Connect API Key не является заменой сертификату и приватному ключу. Он авторизует операции с сервисом Apple, тогда как локальная подпись приложения требует другого набора материалов. Правила создания ключа и доступных ролей проверяйте по официальной документации Apple для App Store Connect API Key.

Для CI/CD используйте защищённые переменные и переменные файлового типа, где это поддерживается вашей конфигурацией. Документация GitLab о CI/CD variables описывает различия между типами переменных, защитой и маскированием. Маскирование не делает секрет безопасным, если скрипт намеренно выводит его частями, записывает во временный файл с широкими правами или передаёт внешнему процессу.

Показывайте в документации только шаблон:

set -eu

KEYCHAIN_PATH="$HOME/Library/Keychains/ci-signing.keychain-db"
CERT_FILE="${CI_PROJECT_DIR}/ci-assets/PLACEHOLDER_CERTIFICATE.p12"
PROFILE_FILE="${CI_PROJECT_DIR}/ci-assets/PLACEHOLDER_PROFILE.mobileprovision"

security create-keychain -p "$PLACEHOLDER_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security unlock-keychain -p "$PLACEHOLDER_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_FILE" \
  -k "$KEYCHAIN_PATH" \
  -P "$PLACEHOLDER_CERTIFICATE_PASSWORD" \
  -T /usr/bin/codesign

mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
cp "$PROFILE_FILE" "$HOME/Library/MobileDevice/Provisioning Profiles/"

Здесь все имена являются заглушками. Не заменяйте их в статье, тикете или публичном примере реальными паролями, токенами, Team ID, Key ID, Bundle ID и названиями сертификатов. В рабочей процедуре добавьте очистку временных файлов и удаление созданного Keychain после завершения, если модель узла позволяет это без потери управляемости.

SECTION 06Четвёртый критерий: Archive, экспорт и TestFlight должны проверяться вместе

Обычный Debug Build не является приемлемым доказательством готовности публикации. В настоящей проверке защищённая ветка должна последовательно выполнить:

  1. разрешение зависимостей;
  2. компиляцию схемы в релизной конфигурации;
  3. xcodebuild archive;
  4. экспорт из xcarchive;
  5. проверку подписи и entitlements;
  6. загрузку сборки в App Store Connect;
  7. проверку статуса обработки сборки.

Apple описывает назначение Archive и варианты распространения в документации Xcode о публикации приложений. Требования к передаче сборок проверяйте по официальной инструкции Apple по загрузке builds.

Схематичный вызов должен использовать заглушки, а не реальные данные:

set -eu

ARCHIVE_PATH="$CI_PROJECT_DIR/output/PLACEHOLDER_APP.xcarchive"
EXPORT_PATH="$CI_PROJECT_DIR/output/exported"

xcodebuild archive \
  -workspace "PLACEHOLDER_WORKSPACE.xcworkspace" \
  -scheme "PLACEHOLDER_SCHEME" \
  -configuration Release \
  -archivePath "$ARCHIVE_PATH" \
  -allowProvisioningUpdates NO

xcodebuild -exportArchive \
  -archivePath "$ARCHIVE_PATH" \
  -exportOptionsPlist "PLACEHOLDER_ExportOptions.plist" \
  -exportPath "$EXPORT_PATH"

-allowProvisioningUpdates нельзя включать автоматически только ради устранения ошибки. Сначала установите, какие профили и сертификаты должны существовать на узле, кто отвечает за их ротацию и как будет отозван доступ. После экспорта сохраните связь между xcarchive, экспортированным пакетом и xcresult. Это облегчает расследование, когда бинарный файл принят, но обработка в App Store Connect завершилась ошибкой.

Успешный выход команды загрузки означает лишь, что файл передан. Он не доказывает, что Apple завершила обработку, сопоставила сборку с нужным приложением и сделала её доступной тестировщикам. Поэтому финальная проверка должна включать состояние сборки в App Store Connect, а не только код завершения команды.

SECTION 07Пятый критерий: что кэшировать, а что передавать как артефакт

Кэш ускоряет повторное получение зависимостей, но не должен становиться хранилищем единственной копии релизного архива или секретов. GitLab различает Cache и Artifacts: назначение, доступность между этапами и срок хранения у них разные. Перед реализацией схемы сверяйтесь с актуальной документацией GitLab о кэше и артефактах, а не переносите настройки из чужого Pipeline.

Разделите данные так:

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

Ключ кэша должен зависеть от lock-файла и существенных параметров среды. Если поменялись зависимости, версия SDK или формат сборки, старый кэш не должен незаметно считаться совместимым. Срок хранения и допустимый объём задавайте как проектные параметры только после проверки настроек вашей инсталляции GitLab; не выдавайте выбранный вами срок за универсальное правило платформы.

SECTION 08Как принять решение по схеме развёртывания

Используйте следующие условия вместо общего совета «просто зарегистрируйте Runner»:

  • Если iOS Job направляется на macOS, пользовательская сессия восстанавливается, активный Xcode подтверждён командами, а Shell executor работает на выделенном узле, то можно переходить к проверке подписи и Archive.
  • Если Runner online, но после перезапуска не принимает задания, то сначала исправьте LaunchAgent, вход пользователя и теги; публикацию до этого не включайте.
  • Если обычная компиляция проходит, но Archive или экспорт падает, то проверяйте сертификат, приватный ключ, Provisioning Profile и entitlements, а не увеличивайте количество повторных запусков.
  • Если один аккаунт и один рабочий каталог обслуживают непроверенные Merge Request и релиз, то разделите хост или учётные записи; в противном случае такой узел не подходит для общего использования.
  • Если вам нужен редкий выпуск и нет постоянного Mac, то сначала используйте удалённый Mac для изолированного Runner и полного теста Pipeline.
  • Если сборки идут постоянно, секреты должны быть доступны без оператора, а восстановление после сбоя обязательно, то оформляйте отдельный постоянно доступный узел с регламентом ротации и резервного восстановления.
  • Если вам нужны физические устройства, USB-доступ или локальная интерактивная отладка, то удалённый Runner не заменит локальную лабораторию; разделите задачи между средами.

Перед запуском проверьте схему аренды удалённого Mac для CI-задач и сопоставьте её не только с числом сборок, но и с требованиями к постоянной сессии, доступу к Keychain и сроку хранения рабочих материалов. Общую информацию о вариантах доступа можно найти на русскоязычной странице MACNOX.

SECTION 09Сценарий приёмки от коммита до восстановления

Для независимого разработчика полезно оформить один тест как операционный акт, а не как впечатление от зелёного Pipeline. Возьмите обезличенный проект и защищённую ветку. Зафиксируйте идентификатор коммита, схему, конфигурацию и ожидаемый способ распространения. Затем выполните Archive, экспорт, загрузку и проверку статуса обработки.

После успешного выпуска перезапустите удалённый Mac в согласованное окно:

  • проверьте доступность пользовательской сессии;
  • убедитесь, что Runner снова подключился;
  • подтвердите тот же путь xcode-select;
  • проверьте доступ к выделенному Keychain;
  • повторите тестовое задание;
  • запустите следующий релизный Pipeline без ручного ремонта окружения.

Результат удобно разделить на три категории:

  • Пройдено — Job маршрутизируется правильно, Archive и TestFlight завершены, а после перезапуска повторная сборка не требует ручной настройки.
  • Требует исправления — отдельный этап проходит только вручную, зависит от случайного входа пользователя, общего каталога или незафиксированного Xcode.
  • Не подходит для общего хоста — непроверенные задания имеют доступ к релизным ключам, рабочим файлам или пользовательскому Keychain.

Такой тест показывает реальную пригодность среды лучше, чем количество установленных инструментов или один успешный Debug Build. Если меняется режим службы GitLab, состояние Shell executor, формальные требования Apple к загрузке или выпускается новая стабильная версия Xcode, процедуру нужно пересмотреть. Плановая проверка раз в квартал полезна даже без заметных изменений проекта.

SECTION 10Частые вопросы

Почему для iOS-задачи GitLab CI нужен именно Mac Runner?

Linux Runner может выполнять серверные, Android- или общие скрипты, но не заменяет macOS-инструменты Apple. Xcode, iOS SDK, codesign, Keychain и часть процесса Archive доступны в связке macOS и Xcode. Поэтому iOS-задание нужно направлять на отдельный Mac Runner с Shell executor, а не пытаться продолжать сборку на Linux.

Что проверить, если GitLab Runner после перезапуска Mac стал offline?

Проверьте по цепочке: пользователь вошёл в macOS, LaunchAgent Runner запущен, сервис виден локально, Runner подключён к GitLab и принимает задания по нужному тегу. Затем отдельно проверьте выбранный Xcode и доступ к Keychain. Автоматический вход может упростить восстановление, но его нельзя считать единственным или универсальным решением безопасности.

Как безопасно передать сертификат и Provisioning Profile в GitLab CI?

Храните секреты в защищённых CI/CD variables, а файлы сертификата и профиля передавайте как переменные файлового типа либо создавайте временно во время задания. Импортируйте их в отдельный Keychain, выдайте минимально необходимый доступ и удаляйте временные файлы после работы. Пароли, токены, Team ID и содержимое сертификатов не должны попадать в журнал.

Как GitLab Runner автоматически создаёт Archive и отправляет его в TestFlight?

В защищённом задании сначала выполните xcodebuild archive с заданной схемой и путём к xcarchive, затем экспортируйте подписанный пакет и передайте его через Xcode или Transporter. Для автоматизации загрузки используйте App Store Connect API Key с ограниченной ролью. Успешная команда загрузки ещё не означает завершённую обработку сборки в App Store Connect.

Нужно ли держать графическую сессию macOS открытой для GitLab Runner?

Для Shell executor на macOS пользовательская сессия важна, когда процессу нужны пользовательский Keychain, графические компоненты или действия, связанные с Xcode. Полагаться только на установленное приложение недостаточно. Проверьте реальный сценарий после выхода пользователя и перезапуска: если подпись или Xcode требуют активной сессии, Runner должен работать в специально подготовленной постоянной сессии.

Текущий вариант — личный Mac разработчика или случайный общий хост — обычно слабее для постоянной публикации: он может быть выключен, занят интерактивной работой, содержать смешанные сертификаты и не иметь предсказуемого восстановления после перезапуска. Linux Runner не решает проблему macOS-инструментов, а общий Shell executor добавляет риск доступа непроверенных заданий к рабочей среде. Если у вас нет отдельного Mac, который можно держать онлайн и проверить по всей цепочке, аренда удалённого Mac у MACNOX позволит сначала запустить изолированный GitLab Runner, пройти настоящий Archive и TestFlight, а затем выбрать недельный, месячный или более длительный период по фактической частоте сборок.

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

Почему для iOS-задачи GitLab CI нужен именно Mac Runner?

Linux Runner может выполнять серверные, Android- или общие скрипты, но не заменяет macOS-инструменты Apple. Xcode, iOS SDK, codesign, Keychain и часть процесса Archive доступны в связке macOS и Xcode. Поэтому iOS-задание нужно направлять на отдельный Mac Runner с Shell executor, а не пытаться продолжать сборку на Linux.

Что проверить, если GitLab Runner после перезапуска Mac стал offline?

Проверьте по цепочке: пользователь вошёл в macOS, LaunchAgent Runner запущен, сервис виден локально, Runner подключён к GitLab и принимает задания по нужному тегу. Затем отдельно проверьте выбранный Xcode и доступ к Keychain. Автоматический вход может упростить восстановление, но его нельзя считать единственным или универсальным решением безопасности.

Как безопасно передать сертификат и Provisioning Profile в GitLab CI?

Храните секреты в защищённых CI/CD variables, а файлы сертификата и профиля передавайте как переменные файлового типа либо создавайте временно во время задания. Импортируйте их в отдельный Keychain, выдайте минимально необходимый доступ и удаляйте временные файлы после работы. Пароли, токены, Team ID и содержимое сертификатов не должны попадать в журнал.

Как GitLab Runner автоматически создаёт Archive и отправляет его в TestFlight?

В защищённом задании сначала выполните xcodebuild archive с заданной схемой и путём к xcarchive, затем экспортируйте подписанный пакет и передайте его через Xcode или Transporter. Для автоматизации загрузки используйте App Store Connect API Key с ограниченной ролью. Успешная команда загрузки ещё не означает завершённую обработку сборки в App Store Connect.

Нужно ли держать графическую сессию macOS открытой для GitLab Runner?

Для Shell executor на macOS пользовательская сессия важна, когда процессу нужны пользовательский Keychain, графические компоненты или действия, связанные с Xcode. Полагаться только на установленное приложение недостаточно. Проверьте реальный сценарий после выхода пользователя и перезапуска: если подпись или Xcode требуют активной сессии, Runner должен работать в специально подготовленной постоянной сессии.