Почему self-hosted Runner лучше для iOS-сборки, чем hosted Runner GitHub?
Hosted macOS Runner в GitHub Actions теоретически избавляет от обслуживания железа, но в продакшене есть несколько неизбежных проблем.
Первое — ожидание в очереди. Пул macOS Runner меньше Linux; в пик (UTC 12–20) от runs-on: macos-latest до готовности — медиана 8–15 мин, иногда 20+. Clean Build 6 мин — очередь длиннее самой сборки.
Второе — неконтролируемая версия Xcode. macos-latest тихо меняет Xcode; были апгрейды 15→16 без уведомления — нужен xcode-select и проверка совместимости.
Третье — быстрый расход бесплатного лимита. macOS Runner в 10 раз дороже Linux (документация GitHub); 2000 минут/мес ≈ 200 минут macOS — интенсивный CI быстро уходит в перерасход.
Self-hosted Runner на своём Mac снимает эти три проблемы: Runner всегда онлайн, версию Xcode контролируете вы, время выполнения не тарифицируется. Нужна постоянно работающая macOS-машина — это и решает статья.
Подготовка: активация облачного Mac-узла и базовая среда
До iOS CI/CD нужен macOS с SSH и 24/7. Пример — выделенный Mac mini M4 VPSRox (или свой Mac mini/Studio); после доставки в консоли шаги ниже — за 5 минут.
SSH-доступ и Xcode Command Line Tools
-
01
SSH-ключи
Скопируйте публичный IP в консоли и выполните локально:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@<IP-узла>Затем отключите вход по паролю (
PasswordAuthentication no), чтобы защититься от brute force. -
02
Xcode Command Line Tools
xcode-select --installДля полного Xcode GUI скачайте
.xipс developer.apple.com/downloads, в/Applications/и переключите путь:sudo xcode-select -s /Applications/Xcode.app/Contents/Developer -
03
Установить Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"После установки добавьте
/opt/homebrew/binв PATH, пропишите в~/.zprofile. -
04
Проверить цепочку сборки Xcode
xcodebuild -versionУбедитесь, что выводятся версия Xcode и Build Version — в YAML можно зафиксировать версию переменной окружения
XCODE_VERSION.
brew install xcodesorg/made/xcodes — быстрая смена Xcode. В CI: xcodes select 16.2 для фиксации версии.
Установка и регистрация self-hosted Runner GitHub Actions
Пакет self-hosted Runner от GitHub запускает на Mac polling-демон: при срабатывании workflow job выполняется локально; открывать порты наружу не нужно.
Получите registration token в репозитории GitHub
Репозиторий → Settings → Actions → Runners → New self-hosted runner, macOS, скопируйте команды и token (1 час).
-
01
Скачать и распаковать Runner
mkdir actions-runner && cd actions-runnercurl -o actions-runner-osx-arm64-2.319.1.tar.gz -L https://github.com/actions/runner/releases/download/v2.319.1/actions-runner-osx-arm64-2.319.1.tar.gztar xzf ./actions-runner-osx-arm64-2.319.1.tar.gzВыберите пакет
osx-arm64(Apple Silicon); версию смотрите на странице GitHub. -
02
Зарегистрировать Runner
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO --token <TOKEN> --name mac-m4-vpsrox --labels mac,xcode16,iosПользовательские метки
--labelsпозже указываются вruns-onYAML для точной маршрутизации на эту машину. -
03
Служба launchd (автозапуск)
./svc.sh install./svc.sh startlaunchd поднимет процесс Runner после входа пользователя. Статус:
./svc.sh status.
Указать self-hosted Runner в Workflow YAML
После регистрации в .github/workflows/ios-ci.yml замените runs-on на ваши метки:
jobs:
build:
runs-on: [self-hosted, mac, xcode16]
steps:
- uses: actions/checkout@v4
- name: Show Xcode version
run: xcodebuild -version
Self-hosted Runner в public repo может выполнять fork PR на вашем Mac. Рекомендации: ① private/org; ② не admin account; ③ аудит ~/.bash_history и ~/actions-runner/_diag/.
Настройка среды сборки Xcode и управление зависимостями
Чистая iOS-сборка обычно требует: зависимости, кэш компиляции, изоляцию Derived Data.
CocoaPods vs Swift Package Manager
Если проект использует CocoaPods, достаточно один раз установить на машине Runner:
sudo gem install cocoapods
В шаге CI:
- name: Install CocoaPods
run: pod install --repo-update
working-directory: ./ios
При Swift Package Manager Xcode разрешает зависимости при первой сборке. Для ускорения CI кэшируйте результат разрешения SPM в job:
- uses: actions/cache@v4
with:
path: ~/Library/Developer/Xcode/DerivedData
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
Изоляция пути Derived Data
При параллельных сборках нескольких проектов на одном Runner общий DerivedData мешает. Явно задайте путь в xcodebuild:
xcodebuild \
-project MyApp.xcodeproj \
-scheme MyApp \
-sdk iphoneos \
-configuration Release \
-derivedDataPath ./DerivedData \
clean build
Настройка параллельных сборок
Mac mini M4 — 10 ядер CPU (4 performance + 6 efficiency); Xcode по умолчанию ставит параллелизм по числу ядер. Для крупных проектов можно снизить, чтобы не перегружать память:
defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks 6
SwiftUI-проект из 80 Swift-файлов на M4 с 16 GB RAM: Clean Build ~4 мин 12 с, без нагрузки на swap.
Подпись кода и Keychain: полный цикл Archive
Подпись кода — самый частый источник ошибок в iOS CI/CD. В CI без GUI поведение macOS Keychain отличается от обычной среды разработки; Keychain нужно явно управлять.
Сравнение двух основных схем подписи
| Вариант | Сценарии применения | Плюсы | Минусы |
|---|---|---|---|
| Ручной импорт p12-сертификата | CI на одной машине для малой команды | Простая настройка, без внешних зависимостей | Ротация сертификатов — ручной re-import |
| fastlane match | Командная работа / несколько CI-узлов | Единое шифрованное хранение сертификатов, автоматическая ротация | Нужен Git-репозиторий или S3 backend |
Ручная схема p12: создание временного Keychain
В CI надёжнее создавать временный Keychain на каждый job и удалять после сборки:
# Create a temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
# Import signing certificate
security import "$CERTIFICATE_P12_PATH" \
-k build.keychain \
-P "$P12_PASSWORD" \
-T /usr/bin/codesign \
-T /usr/bin/xcodebuild
# Allow xcodebuild to access the key without UI prompt
security set-key-partition-list \
-S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASSWORD" build.keychain
KEYCHAIN_PASSWORD, P12_PASSWORD и др. — только в GitHub Repository Secrets через ${{ secrets.KEYCHAIN_PASSWORD }}, не в YAML.
Выполнить Archive и экспорт IPA
Archive → Export в два шага: xcodebuild archive для .xcarchive, затем -exportArchive для IPA.
# Step 1: Archive
xcodebuild archive \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-sdk iphoneos \
-archivePath ./build/MyApp.xcarchive \
CODE_SIGN_IDENTITY="Apple Distribution: Your Name (XXXXXXXX)" \
DEVELOPMENT_TEAM="XXXXXXXX"
# Step 2: Export IPA
xcodebuild -exportArchive \
-archivePath ./build/MyApp.xcarchive \
-exportOptionsPlist ExportOptions.plist \
-exportPath ./build/
ExportOptions.plist: method (app-store/ad-hoc), teamID, signingStyle. Шаблон — после ручного Export в Xcode.
Подключение fastlane для автозагрузки в TestFlight
После IPA — загрузка в TestFlight через fastlane pilot / upload_to_testflight с App Store Connect API Key, без пароля Apple ID.
Установка fastlane
Рекомендуется Bundler для версии fastlane и избежания конфликтов глобальной установки:
# In project root, create Gemfile
source "https://rubygems.org"
gem "fastlane"
Затем bundle install; fastlane только через bundle exec fastlane ....
Настройка App Store Connect API Key
App Store Connect → Users and Access → Integrations → API Key, скачайте .p8, Key ID и Issuer ID.
Задайте в GitHub Secrets следующие переменные:
ASC_API_KEY_ID: ID API KeyASC_API_ISSUER_ID:Issuer IDASC_API_KEY_CONTENT:.p8Содержимое файла в кодировке Base64
Пример конфигурации Fastfile
lane :beta do
api_key = app_store_connect_api_key(
key_id: ENV["ASC_API_KEY_ID"],
issuer_id: ENV["ASC_API_ISSUER_ID"],
key_content: Base64.decode64(ENV["ASC_API_KEY_CONTENT"])
)
upload_to_testflight(
api_key: api_key,
ipa: "./build/MyApp.ipa",
skip_waiting_for_build_processing: true
)
end
Вызов в YAML GitHub Actions:
- name: Upload to TestFlight
env:
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
ASC_API_ISSUER_ID: ${{ secrets.ASC_API_ISSUER_ID }}
ASC_API_KEY_CONTENT: ${{ secrets.ASC_API_KEY_CONTENT }}
run: bundle exec fastlane beta
skip_waiting_for_build_processing: true — fastlane завершается после upload, без ожидания обработки IPA в App Store Connect (10–30 мин). Если следующий шаг не ждёт результат — заметно короче CI.
Типичные ошибки Keychain: централизованная диагностика
Сообщения об ошибках подписи кода часто неясны. Ниже — самые частые в CI без GUI, их причины и решения.
| Сообщение об ошибке | Причина | Решение |
|---|---|---|
errSecItemNotFound |
Сертификат не импортирован в активный Keychain или временный Keychain уже удалён GC | Проверьте security default-keychain -s build.keychain; перед archive добавьте security unlock-keychain |
| could not find signing certificate for "Apple Distribution" | Имя Identity сертификата не совпадает с CODE_SIGN_IDENTITY в проекте Xcode |
Выполните security find-identity -v -p codesigning build.keychain, скопируйте полную строку Identity |
| User interaction is not allowed | Keychain заблокирован, codesign требует UI-диалог авторизации (в headless-среде невозможен) | Добавьте security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain — codesign без UI |
| Provisioning profile doesn't include the entitlement | Provisioning profile не совпадает с Entitlements проекта (Push Notifications, App Groups и т.д.) | Пересоздайте provisioning profile в Apple Developer, включите все Capability, скачайте и установите заново |
| No signing certificate "iOS Distribution" found | Provisioning profile не совпадает с типом сертификата (Distribution vs Development) | В ExportOptions.plist проверьте method (app-store требует Distribution-сертификат) |
Отладка: полная команда подписи в логах сборки
После xcodebuild — | xcpretty -r json-compilation-database или сырой лог с CodeSign — больше деталей, чем в GUI Xcode.
Ещё одна распространённая команда отладки:
codesign -dv --verbose=4 ./build/MyApp.xcarchive/Products/Applications/MyApp.app
Проверьте встроенную подпись в .app — полна ли цепочка сертификатов.
Jenkins Agent: сравнение, миграция и компромиссы
Если у команды уже есть Jenkins или политика безопасности не позволяет пушить код в GitHub, Jenkins on Mac — распространённая альтернатива. Ключевые различия — по нескольким критериям.
| Критерий | Self-hosted Runner GitHub Actions | Jenkins Agent (macOS) |
|---|---|---|
| Порог настройки | Низкий, регистрация за 5 минут | Средний: сначала нужен Jenkins master (обычно Linux/Docker) |
| Интеграция с репозиторием | Нативная интеграция GitHub, статус PR-проверок пишется автоматически | Нужен плагин GitHub Branch Source |
| Смешанные платформы | В одном workflow можно смешивать macOS / Linux / Windows | Нужна маршрутизация по Label, настройка сложнее |
| Визуализация pipeline | Нативный UI GitHub Actions, наглядно | Плагин Blue Ocean хорош, но требует обслуживания |
| Управление секретами | GitHub Secrets + OIDC | Jenkins Credentials, интеграция с Vault |
| Офлайн/внутренняя сборка | Не поддерживается (нужен доступ к GitHub) | Поддерживается, подходит для строгого compliance |
| Требования к железу | Нужна онлайн-машина macOS (хост Runner) | Нужна онлайн-машина macOS (хост Jenkins Agent) |
Миграция с Jenkins: sh 'xcodebuild ...' ≈ run: в YAML. Различия — триггеры и concurrency: when { branch ... } → on: push: branches:; lockable-resources → concurrency:.
Общее условие обеих схем: постоянно онлайн физический Mac с фиксированной версией macOS как Agent/Runner. Стабильность CI зависит от этой машины — перегрев или дрейф версий ломают сборки.
Реальная проблема многих команд — не выбор схемы, а нет Mac для постоянного CI. MacBook в офисе часто 8 GB RAM, throttling при сборке; новый Mac mini — закупка, площадь, электричество.
Выделенный облачный Mac: фиксированная сборка без своего железа
В такой ситуации аренда выделенного облачного Mac — вариант, который стоит оценить: без начальных вложений в железо, но с выделенным macOS-узлом сборки с фиксированной средой.
Пример узла VPSRox Mac mini M4: 16 GB unified memory + 256 GB NVMe SSD, 1 Gbps выделенный канал, от $21.8/день, доставка 1–5 минут, без контракта.
С точки зрения CI/CD это аренда всегда онлайн Mac mini с выделенной мощностью — возможности те же, что у локальной физической машины:
- Любая версия Xcode (xcodes), полностью фиксированная среда сборки
- GitHub Actions self-hosted Runner или Jenkins Agent — шаги статьи применимы напрямую
- 10 ядер M4 и 38 TOPS Neural Engine — лучше для проектов с Core ML
- Thunderbolt 5: кластер Mac mini для крупных команд
По сравнению с hosted Runner GitHub: при частых сборках (30+ в день) аренда по дням часто дешевле macOS Runner; по сравнению с покупкой Mac mini — без CAPEX, гибкий срок (больше машин на спринт, меньше после релиза).
Пять глобальных узлов (Сингапур, Токио, Сеул, Гонконг, восток США) — выберите ближайший к команде, чтобы снизить задержку git fetch и загрузки зависимостей.
Облачный Mac не только для CI: удалённый рабочий стол (VNC/SSH), Xcode, Instruments, симулятор без локального Mac — выше утилизация узла и ниже стоимость дня.
Запустить этот CI/CD pipeline
Все шаги проверены на выделенном Mac mini M4 VPSRox. Узел → SSH → шаги статьи: от нуля до автозагрузки в TestFlight — за один день. Аренда по дням, без контракта.