Акция

Xcode Product → Archive: полный разбор (и почему CI зависает на этом шаге)

CI Xcode Archive · iOS Release
2026-06-15 ~14 мин

Локально достаточно нажать Product → Archive — и через Organizer вы получаете IPA. В CI команда xcodebuild archive нередко висит 20 минут, падает по таймауту или «тихо» завершается с ошибкой на том же этапе. Дело не в том, знаете ли вы Xcode, а в том, воспринимаете ли Archive как наблюдаемый пайплайн, а не как чёрный ящик из одной строки в workflow.

В этой статье — разбор каждого этапа: что реально делает цепочка Build → Archive → Sign → Export, как xcodebuild воспроизводит пункт меню, и таблица из семи типовых сценариев «симптом → действие» для iOS- и Flutter-команд на GitHub Actions и self-hosted Mac runner’ах.

Схема пайплайна Xcode Archive и CI workflow
Локально Archive даёт обратную связь через GUI; в CI ту же цепочку нужно сегментировать в логах — иначе видно только «job крутился 40 минут».

Главное

  1. Archive ≠ Build: Archive всегда идёт в Release + Any iOS Device и создаёт .xcarchive. Успешный локальный Debug-сборка не гарантирует прохождение Archive.
  2. Зависания в CI группируются в четыре зоны: Scheme/конфигурация, кеш компиляции, подпись и keychain, exportArchive — сначала определите сегмент по логам, потом чините.
  3. xcodebuild archive эквивалентен пункту меню, но в CI нужно явно передать -scheme, -configuration Release и -destination 'generic/platform=iOS'.
  4. «Зависло без вывода» чаще означает ожидание авторизации keychain, скачивание provisioning profile или swap из-за нехватки памяти — а не «умерший» xcodebuild.
  5. 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 примерно соответствует:

  1. Выбор Shared Scheme с конфигурацией Release;
  2. Destination = Any iOS Device (не симулятор);
  3. Запуск xcodebuild archive с записью в ARCHIVE_PATH;
  4. (Опционально) 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-store method.
  • Нет 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: сначала сегмент, потом фикс

  1. Воспроизвести Release Archive локально: Product → Scheme → Edit Scheme → Archive использует Release; Destination — Any iOS Device. Если локально падает — не тратьте минуты CI.
  2. Добавить тайминг четырёх сегментов в CI: pod installarchiveexportArchiveupload; записывать длительность каждого в step summary.
  3. Сохранять полные логи: не используйте слишком «тихие» prettifier’ы; для сбоев подписи нужны сырые строки CodeSign.
  4. Зафиксировать версию Xcode: путь через xcode-select или sudo xcode-select -s; согласовать с .xcode-version или macos-15 в workflow.
  5. Валидация за 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’ом.

Тарифы облачного Mac · Спецификации M4 · Чеклист onboarding