Главное
- Archive ≠ Build: Archive всегда идёт в Release + Any iOS Device и создаёт
.xcarchive. Успешный локальный Debug-сборка не гарантирует прохождение Archive. - Зависания в CI группируются в четыре зоны: Scheme/конфигурация, кеш компиляции, подпись и keychain, exportArchive — сначала определите сегмент по логам, потом чините.
xcodebuild archiveэквивалентен пункту меню, но в CI нужно явно передать-scheme,-configuration Releaseи-destination 'generic/platform=iOS'.- «Зависло без вывода» чаще означает ожидание авторизации keychain, скачивание provisioning profile или swap из-за нехватки памяти — а не «умерший» xcodebuild.
- Self-hosted runner на облачном Mac с персистентным DerivedData и отдельной signing keychain часто на порядок стабильнее, чем холодный Archive на эфемерных hosted runner’ах.
1. Archive и Build: что меняется на самом деле
Многие команды сводят падения CI к формулировке «xcodebuild сломался». На практике Cmd+B Build и Product → Archive — это два разных пути:
| Параметр | Build (⌘B) | Archive |
|---|---|---|
| Конфигурация | Debug (локальная разработка) | Release (App Store / TestFlight) |
| Destination | Симулятор или подключённое устройство | Any iOS Device (arm64) |
| Результат | .app в DerivedData |
.xcarchive + экспортируемый IPA |
| Подпись | Development-сертификат может «протащить» часть таргетов | Нужны Distribution-сертификат + provisioning profile |
| Оптимизация | Минимальная — быстрая компиляция | Полная оптимизация — существенно дольше |
Поэтому «3 минуты локально» и «20 минут в CI с падением» не противоречат друг другу: локально вы могли собирать Debug + симулятор, а в CI идёт Release + device-архитектура + полная цепочка подписи. Сначала выровняйте условия сравнения, потом оптимизируйте. Про разрыв по времени компиляции — в материале почему xcodebuild в CI в 2–3 раза медленнее локально.
2. От меню к CLI: эквивалент Product → Archive
Клик по Product → Archive в Xcode примерно соответствует:
- Выбор Shared Scheme с конфигурацией Release;
- Destination = Any iOS Device (не симулятор);
- Запуск
xcodebuild archiveс записью вARCHIVE_PATH; - (Опционально) Distribute App в Organizer → аналог
xcodebuild -exportArchive.
Минимальная команда Archive для CI (пути подставьте под свой проект):
xcodebuild archive \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM=XXXXXXXXXX \
| tee archive.log
Три параметра, которые часто забывают:
-destination 'generic/platform=iOS': без него xcodebuild может выбрать симулятор — Archive падает или ведёт себя непредсказуемо.-archivePath: путь должен быть доступен для записи; на hosted runner’ах проблемы с правами на temp дают «скомпилировалось, но архив не записался».- Scheme должен быть Shared и закоммичен в Git: локальный unshared Scheme после checkout в CI не существует →
scheme not found.
Официальные материалы: Apple — Building your app, Distributing your app.
3. Четыре этапа пайплайна
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 1. Resolve │ → │ 2. Archive │ → │ 3. Sign │ → │ 4. Export │
│ SPM/Pods │ │ Release full │ │ Certs/profiles│ │ IPA / Upload │
│ DerivedData │ │ → .xcarchive │ │ Keychain auth │ │ TestFlight │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
pod install xcodebuild codesign exportArchive
часто 5–15 мин archive 8–25 мин зона «тишины» ошибки plist
Когда CI «зависает здесь», спросите себя: к какому этапу относится последняя строка лога? Остановка на CompileSwift — это компиляция; на CodeSign — подпись; на exportArchive — конфиг экспорта. Смешивание этапов приводит к бесконечной очистке кешей.
4. Этап 1: Resolve и Compile (почему CI медленнее)
Разрешение зависимостей и компиляция перед Archive локально обычно «прогреты», а в CI — «холодные»:
- CocoaPods / SPM: CI каждый job запускает
pod installс нуля — в логахInstalling …, и 10 минут уходит ещё до Archive. См. кеширование Pods и DerivedData во Flutter iOS CI. - Холодный DerivedData: полная Release-сборка без кеша перекомпилирует каждый native-модуль Pod’ов. Фиксация
DERIVED_DATA_PATHна SSD self-hosted runner’а часто сокращает второй Archive вдвое. - Давление на память: Archive потребляет больше, чем Debug; Mac на 16 ГБ с параллельными job’ами уходит в swap — CPU низкий, а прогресса нет. См. управление памятью и swap на runner’е.
Быстрая проверка: если в логах много CompileC / SwiftCompile для сторонних pod’ов, которые вы не трогали, сначала чините кеш — не подпись.
5. Этап 2: Archive (создание .xcarchive)
После успешного xcodebuild archive по пути -archivePath должна появиться такая структура:
MyApp.xcarchive/
Info.plist
Products/Applications/MyApp.app
dSYMs/...
Типичные сбои:
- В Archive action Scheme не отмечен target: локально собирается один Scheme, а CI-Scheme архивирует пустоту.
- Multi-target / extension: основное приложение проходит, Notification Service Extension падает на подписи — весь Archive считается failed.
- Конфликт build number / version: CI не инкрементирует
CFBundleVersion; upload падает позже, а винят «медленный Archive».
Проверяйте сразу: ls -la "$ARCHIVE_PATH" и plutil -p "$ARCHIVE_PATH/Info.plist" — убедитесь, что есть ApplicationProperties. Не ждите падения export, чтобы обнаружить неполный архив.
6. Этап 3: Code Sign (главная ловушка CI)
Локально Archive может запросить доступ к keychain — вы жмёте «Always Allow», и пайплайн идёт дальше. Безконсольный CI такого шага не имеет, поэтому:
errSecInternalComponent,User interaction is not allowed: приватный ключ в login keychain, CI-пользователь не может авторизовать через GUI.- Просроченный profile / несовпадение Bundle ID: лог замирает на
CodeSignсProvisioning profile … doesn't match. - Случайное включение
-allowProvisioningUpdates: требует интерактивный вход Apple ID; headless-среда ждёт бесконечно. - Keychain не разблокирован: в job нет
security unlock-keychain/set-key-partition-listв начале.
На облачном Mac / self-hosted runner’е рекомендуем: отдельный keychain-файл (не login keychain), импорт distribution-сертификата + приватного ключа, разблокировка и установка default в начале job. Полная цепочка — в статье iOS CI на облачном Mac: codesign и границы keychain.
# Начало job (пароль — через CI Secret)
KEYCHAIN=$RUNNER_TEMP/ci-signing.keychain-db
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security import dist.p12 -k "$KEYCHAIN" -P "$P12_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security list-keychains -s "$KEYCHAIN" login.keychain-db
Если используете fastlane match или Xcode Cloud для credentials — принцип тот же: процесс CI должен получить доступ к приватному ключу без участия человека, а не просто «сертификат где-то лежит на машине». См. fastlane match.
7. Этап 4: Export IPA (exportArchive)
Archive создаёт только .xcarchive; для загрузки в TestFlight нужен ещё Export. Distribute App в меню соответствует:
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
-exportPath "$RUNNER_TEMP/export" \
-exportOptionsPlist ExportOptions.plist
Ключ method в ExportOptions.plist задаёт тип экспорта (app-store, ad-hoc, development и т.д.). Частые проблемы в CI:
methodне совпадает с типом profile: development profile +app-storemethod.- Нет
teamID/signingCertificate: ошибки Manual signing всплывают на export и маскируются под «медленный Archive». - Upload в одном step с export: сетевая задержка
altool/ Transporter попадает в «таймаут Archive».
Разделите archive, export и upload на три step’а с замером времени. Upload через xcrun altool --upload-app или xcrun notarytool (для macOS) — отдельно от Archive.
8. Семь причин зависания: симптом / этап / действие
| Симптом / ключевое слово в логе | Этап | Первое действие |
|---|---|---|
| Долгая тишина, низкий CPU | Sign | Проверить unlock keychain; отключить auto-update provisioning, завязанный на GUI |
scheme 'Foo' not found |
Pre-flight | Отметить Scheme как Shared и закоммитить; проверить -list в CI |
No profiles for … were found |
Sign | Проверить Bundle ID; убедиться, что profiles в репо или match fetch прошёл |
Много CompileC на неизменённых Pod’ах |
Compile | Персистентный DerivedData; не делать clean внутри job |
pod install > 8 минут |
Resolve | Кешировать ios/Pods; использовать pod install --deployment |
| Job убит платформой (без stack trace) | Global | Увеличить timeout; проверить swap / переполнение диска (управление диском) |
exportArchive failed + ошибки plist |
Export | Запустить export изолированно; проверить method в ExportOptions.plist |
На дежурстве: сканируйте лог снизу вверх, найдите первый error:, сопоставьте с таблицей — быстрее, чем слепой flutter clean.
9. Runbook: сначала сегмент, потом фикс
- Воспроизвести Release Archive локально: Product → Scheme → Edit Scheme → Archive использует Release; Destination — Any iOS Device. Если локально падает — не тратьте минуты CI.
- Добавить тайминг четырёх сегментов в CI:
pod install→archive→exportArchive→upload; записывать длительность каждого в step summary. - Сохранять полные логи: не используйте слишком «тихие» prettifier’ы; для сбоев подписи нужны сырые строки
CodeSign. - Зафиксировать версию Xcode: путь через
xcode-selectилиsudo xcode-select -s; согласовать с.xcode-versionилиmacos-15в workflow. - Валидация за 48 часов: два Archive одного и того же commit’а; второй прогон должен быть заметно быстрее (cache hit). Если нет — проверьте память и диск.
Для release sprint’ов или временной ёмкости сборки рассмотрите краткосрочный удалённый Mac под релизный спринт вместо бесконечной борьбы с таймаутами Archive на эфемерных hosted runner’ах.
10. Шаблон GitHub Actions
jobs:
archive-ios:
runs-on: [self-hosted, macOS, ios] # или macos-14 hosted runner
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: Install pods
run: pod install --deployment
working-directory: ios
- name: Archive
run: |
set -o pipefail
xcodebuild archive \
-workspace ios/MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
| tee archive.log
env:
DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer
- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
-exportPath "$RUNNER_TEMP/export" \
-exportOptionsPlist ios/ExportOptions.plist
Про маршрутизацию label’ов self-hosted runner’а, keepalive через launchd и изоляцию CI-пользователя — в материалах настройка Mac mini self-hosted runner и архитектура Flutter + GitHub Actions.
11. Заключение
Product → Archive — это не «собрать чуть дольше», а полная цепочка Release-компиляции + архива + distribution-подписи + (опционально) export. Когда CI зависает здесь, Xcode редко «сломан». Чаще команды переносят привычки Debug на Release, считают подпись локальной GUI-задачей или склеивают export и archive в один непрозрачный step.
Практичный порядок действий: локальный Archive проходит → четырёхсегментный тайминг в CI → отдельный keychain + персистентный кеш → затем добавление машин или апгрейд спецификаций. Для Apple-платформенных команд self-hosted runner на облачном Mac часто экономит больше суммарного времени, чем бесконечная настройка кешей на hosted runner’ах — Archive нужен стабильный горячий путь, а не разовый самый быстрый чип.
Прогоните весь Archive-пайплайн на облачном Mac
Выделенный M4 bare metal для xcodebuild archive, отдельной signing keychain и персистентного DerivedData. Посуточная аренда — чтобы замерить время и стабильность Archive; при необходимости переходите на месячный тариф с постоянным runner’ом.