Подпись fastlane match на удалённом Mac в CI 2026

Подпись fastlane match на удалённом Mac в CI 2026

В документации setup_ci описаны три действия: создание временного keychain, перевод match в readonly и подготовка путей для логов и результатов тестов. (docs.fastlane.tools) На этом и строится рабочая схема: подпись fastlane match на удалённом Mac в CI должна выполняться через readonly-синхронизацию, изолированный или временный keychain, явное сопоставление профилей и очистку после сборки. Успешный lane сам по себе не доказывает готовность узла. Нужно отдельно проверить перезапуск, повторную сборку, параллельные задания и восстановление после замены сертификата.

Эта статья предназначена для DevOps-инженеров, которые поддерживают автоматическую упаковку iOS-приложений, но сталкиваются со сбоями подписи на удалённом узле. Она также пригодится инженерам релизов с несколькими приложениями, Extension или Apple Team и разработчикам, переводящим локальный fastlane-процесс на постоянно доступный удалённый Mac.

Почему локальная подпись проходит, а macOS CI ломается

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

Кодовая подпись состоит минимум из четырёх разных объектов:

Объект Где он используется Типичная причина сбоя
Сертификат Определяет доступную signing identity Сертификат установлен без соответствующего private key
Private key Хранится в keychain и реально подписывает код Runner не видит keychain или не имеет разрешения на доступ
Provisioning profile Связывает Bundle ID, entitlements и способ распространения Загружен профиль другого Target или другого типа
Build Settings Передают Xcode имя или UUID профиля В проекте осталось автоматическое или устаревшее сопоставление

Apple описывает keychain как защищённое хранилище для сертификатов, ключей и паролей. Поэтому наличие файла .cer в рабочем каталоге не означает, что процесс сборки получил доступ к private key. (developer.apple.com)

Второе ограничение — пользовательская сессия. Интерактивная команда через SSH может видеть разблокированный keychain, а фоновый сервис — работать от другого пользователя, с другим HOME или без доступа к графическому сеансу. Третий фактор — расположение provisioning profiles. В актуальной документации match указано, что для Xcode 16 путь изменился на ~/Library/Developer/Xcode/UserData/Provisioning Profiles; для более старых версий использовался каталог ~/Library/MobileDevice/Provisioning Profiles. (docs.fastlane.tools)

Наконец, автоматический выбор профиля создаёт недетерминированность. Документация fastlane отдельно предупреждает, что настройка Automatic может выбрать недавно обновлённый профиль, даже если установленный сертификат с ним не совпадает. (docs.fastlane.tools)

Минимальная схема для одного App Store-приложения

Для одного приложения разумно разделить обязанности так:

  • setup_ci готовит окружение CI и keychain;
  • match только получает заранее созданные сертификаты и профили;
  • build_app запускает архивирование и экспорт;
  • проверочные команды подтверждают identity, профиль и содержимое архива.

Не следует давать удалённому Runner права изменять Apple Developer Portal на каждой сборке. В CI используется readonly, а выпуск или обновление подписывающих активов выполняется отдельным контролируемым процессом.

Минимальный Fastfile может выглядеть так:

default_platform(:ios)

platform :ios do
  lane :release do
    setup_ci(
      timeout: 0,
      keychain_name: "ci_signing_keychain"
    )

    match(
      type: "appstore",
      readonly: true,
      app_identifier: "com.example.product"
    )

    build_app(
      scheme: "Product",
      workspace: "Product.xcworkspace",
      configuration: "Release",
      export_method: "app-store",
      clean: true,
      output_directory: "./build"
    )
  end
end

setup_ci по умолчанию создаёт временный keychain, переводит match в режим только чтения и настраивает пути для сборочных артефактов. У него также есть параметр timeout; в официальной документации значение по умолчанию указано как 3 600 секунд, а 0 отключает тайм-аут. Параметры нужно сверять с версией fastlane, установленной на конкретном узле. (docs.fastlane.tools)

Секреты не должны попадать в Fastfile. Пароль шифрования хранилища передаётся через MATCH_PASSWORD, а доступ к репозиторию сертификатов — через секрет CI или отдельный ключ с минимальными правами. В журнале допустимо сохранять:

Xcode version: <version>
DEVELOPER_DIR: <path>
Signing identity: <redacted identity name>
Bundle identifier: com.example.product
Profile UUID: <redacted or recorded securely>
Archive path: build/Product.xcarchive

Нельзя выводить пароль MATCH_PASSWORD, содержимое .p12, приватный ключ, токены, URL с секретом или полный дамп переменных окружения.

После match следует проверить, что identity действительно содержит private key:

security find-identity -v -p codesigning

Ожидаемый результат должен содержать действующую signing identity и число найденных identities. Само наличие сертификата в списке недостаточно: в диагностике должен подтверждаться рабочий private key.

Проверка профиля:

PROFILE_PATH="$(find "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles" \
  -name '*.mobileprovision' -print -quit)"

security cms -D -i "$PROFILE_PATH" > /tmp/profile.plist

/usr/libexec/PlistBuddy -c "Print:Entitlements:application-identifier" \
  /tmp/profile.plist

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

build_app отвечает за архивирование и экспорт. Если fastlane не может однозначно определить профили, официальная документация допускает явную карту Bundle ID к именам provisioning profiles через export_options. (docs.fastlane.tools)

Явное сопоставление Target надёжнее автоматического выбора

Приложение с Widget, Notification Service или Watch Target нельзя считать одним Bundle ID. Каждый Target может иметь собственные entitlements и provisioning profile. Поэтому в проекте должны быть зафиксированы три слоя:

  1. Matchfile — хранилище, Team и общие параметры синхронизации.
  2. Fastfile — список Bundle ID, тип подписи и порядок действий.
  3. Xcode Build Settings или export_options — связь каждого Bundle ID с конкретным профилем.

Пример для нескольких Target:

lane :sync_profiles do
  setup_ci(
    keychain_name: "ci_signing_keychain",
    timeout: 0
  )

  match(
    type: "appstore",
    readonly: true,
    app_identifier: [
      "com.example.product",
      "com.example.product.widget",
      "com.example.product.notification"
    ]
  )
end

lane :release do
  sync_profiles

  build_app(
    workspace: "Product.xcworkspace",
    scheme: "Product",
    export_method: "app-store",
    export_options: {
      provisioningProfiles: {
        "com.example.product" => "match AppStore com.example.product",
        "com.example.product.widget" => "match AppStore com.example.product.widget",
        "com.example.product.notification" => "match AppStore com.example.product.notification"
      }
    }
  )
end

Имена профилей в примере являются заполнителями. Их нельзя копировать вслепую. Сначала нужно получить фактические значения из окружения match, настроек проекта или экспортного лога.

Сценарий Что фиксировать явно Что проверять после архива
Один App Основной Bundle ID и appstore Identity, профиль, экспорт .ipa
App + Widget Два Bundle ID и два профиля Entitlements каждого Target
App + Notification Service Отдельный профиль сервиса Подпись вложенного .appex
Несколько приложений Team Раздельные идентификаторы и политика веток Team ID и профиль каждого архива

Проверка архива должна включать не только сообщение ARCHIVE SUCCEEDED. Например:

xcodebuild -showBuildSettings \
  -workspace Product.xcworkspace \
  -scheme Product \
  -configuration Release \
  | grep -E 'PRODUCT_BUNDLE_IDENTIFIER|CODE_SIGN|PROVISIONING_PROFILE'

codesign -dvvv --entitlements :- \
  build/Product.xcarchive/Products/Applications/Product.app

codesign --verify --deep --strict --verbose=2 \
  build/Product.xcarchive/Products/Applications/Product.app

Для Extension нужно проверить вложенный .appex, а не только основной .app:

find build/Product.xcarchive -name '*.appex' -print

Если основной App подписан правильно, а Extension — нет, экспорт может завершиться ошибкой уже после успешного архивирования. Это одна из причин, почему проверка должна охватывать содержимое архива.

Как настроить keychain, если fastlane match работает локально, но не в CI

На удалённом Mac keychain следует считать частью runtime-конфигурации, а не постоянным мусорным хранилищем. Рабочая последовательность выглядит так:

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

Для Runner выделяется отдельный пользователь macOS. Не следует запускать релизные задания из личной учётной записи разработчика. В переменных окружения и каталогах должны быть согласованы HOME, PATH, DEVELOPER_DIR и путь к fastlane.

Проверка:

whoami
echo "$HOME"
xcode-select -p
ruby -v
fastlane --version

Второй шаг: выбрать временный или выделенный keychain

Для одноразового Runner предпочтителен временный keychain, который исчезает вместе с узлом. Для постоянного удалённого Mac допустим выделенный keychain на команду или на изолированный процесс, если есть регламент очистки.

setup_ci подходит как базовый механизм, потому что он специально создаёт keychain для CI и переводит match в безопасный режим чтения. (docs.fastlane.tools)

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

Для App Store-сборки:

bundle exec fastlane match appstore \
  --readonly \
  --app_identifier com.example.product

Для разных целей нельзя смешивать development, adhoc и appstore без явного решения. Профиль может существовать, но не подходить для выбранного способа экспорта.

Четвёртый шаг: проверить доступ к private key

security find-identity -v -p codesigning
security list-keychains
security default-keychain

Если identity отсутствует, сначала проверяется keychain. Если identity есть, но codesign получает ошибку доступа, проверяются ACL, состояние блокировки и пользователь, от которого запущен Runner.

Пятый шаг: выполнить чистую сборку

bundle exec fastlane release --verbose

Подробный режим полезен для определения границы сбоя: синхронизация, импорт keychain, выбор Xcode, архивирование или экспорт. Пароли и токены при этом должны маскироваться средствами CI.

Шестой шаг: удалить следы после задания

После завершения должны удаляться временные профили, рабочие каталоги и временный keychain. Для постоянного узла очистка должна быть частью after или отдельного скрипта, который запускается даже после ошибки.

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

security delete-keychain "$HOME/Library/Keychains/ci_signing_keychain-db"

Команду нужно адаптировать к фактическому имени keychain. Ошибочное удаление login keychain может повредить локальную среду и усложнить восстановление.

Несколько команд и один удалённый Mac: разделяйте границы

match поддерживает несколько команд. Для Git-хранилища официальный способ — использовать отдельную ветку на Team и передавать уникальный git_branch. (docs.fastlane.tools) Однако ветки сами по себе не изолируют ключи, если все задания используют один пользовательский keychain.

Минимальная политика должна разделять:

  • Apple Team;
  • приложение или группу Bundle ID;
  • тип подписи;
  • секрет доступа к хранилищу;
  • keychain или очередь заданий;
  • каталог артефактов;
  • журнал аудита.

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

Автоматическая подпись подходит для локальной разработки, когда Xcode должен быстро зарегистрировать профиль или устройство. Для релизного macOS CI лучше использовать явные значения. Это упрощает аудит: по логу можно определить, какой Team, Bundle ID, профиль и identity участвовали в сборке.

При увольнении сотрудника или смене подрядчика проверяются не только доступы к исходному коду. Нужно отозвать доступ к хранилищу сертификатов, заменить пароль шифрования, удалить deploy key, проверить сохранённые секреты CI и определить, какие signing identities всё ещё действуют.

Сертификат истёк или узел пересоздан: безопасный порядок восстановления

Сбой после истечения сертификата не является основанием немедленно запускать match nuke. Документация fastlane предупреждает, что разрушительный сброс может отключить сборки для Ad Hoc или Enterprise, а уже распространённые через App Store или TestFlight приложения не исчезают автоматически. (docs.fastlane.tools)

Восстановление выполняется поэтапно:

  1. Зафиксировать ошибку и определить, что именно недействительно: сертификат, private key, профиль, Team ID или entitlements.
  2. Проверить статус активов в Apple Developer и срок действия локального профиля.
  3. Проверить резервную копию зашифрованного хранилища и доступ к нему.
  4. Запустить match с нужным типом подписи и сначала использовать безопасный режим чтения.
  5. Если требуется выпуск нового профиля, выполнить изменение из административного процесса, а не из обычного CI.
  6. Повторно синхронизировать удалённый Mac.
  7. Создать архив, проверить подпись и экспортированный файл.
  8. Проверить, что старые credentials больше не используются.
  9. Зафиксировать результат в журнале восстановления.

Если узел пересоздан, важно проверить не только первую сборку. Удалённый Mac должен пройти три проверки: сборка после чистого подключения, сборка после перезапуска и повторная сборка после очистки рабочего каталога. Для постоянного Runner дополнительно проверяется запуск сервиса после загрузки системы.

Матрица приёмки вместо проверки «lane завершился успешно»

Минимальная приёмка должна включать такие доказательства:

Проверка Условие Доказательство допуска
Первичная настройка Новый пользователь и чистый keychain security find-identity, список профилей, успешный архив
Перезапуск Перезагрузка удалённого Mac Первый job после загрузки проходит без ручного входа
Повторная сборка Два последовательных задания Одинаковые Team ID, Bundle ID и ожидаемые профили
Ошибка задания Сборка намеренно завершается с ошибкой Временные ключи и профили удаляются
Несколько Target App и Extension подписываются вместе Проверены .app и каждый .appex
Замена сертификата Старый актив становится недействительным Новый архив использует новый identity
Пересоздание узла Рабочий диск очищен Синхронизация и сборка повторяются из секретов

Для каждого пункта сохраняются версия Xcode, версия fastlane, имя схемы, тип экспорта, Team ID, Bundle ID, идентификатор профиля и результат codesign. Пароли, private key и токены в отчёт не включаются.

Что выбрать для CI: временный Runner, постоянный Mac или облачная схема

Выбор зависит не от команды match, а от требований к состоянию среды.

Вариант Преимущество Риск Когда выбирать
Временный Runner Меньше остаточных ключей и профилей Дольше подготовка среды Релизы с жёсткими требованиями к изоляции
Постоянный удалённый Mac Быстрый повторный запуск и доступ по SSH Нужны очистка, мониторинг и контроль keychain Регулярные сборки и длительные задания
Локальный Mac mini Полный физический контроль Покупка, обслуживание и простой оборудования Длительная стабильная нагрузка
Виртуальный macOS-узел Быстрое масштабирование Ограничения по аппаратным функциям и воспроизводимости Тесты, которым не нужен полный физический Mac

Настоящий удалённый Mac с root-доступом удобен для миграции: можно проверить SSH, системный сервис Runner, Xcode, keychain и каталоги профилей без искусственного ограничения доступа. Если нужен только временный узел для перехода или проверки релизной процедуры, можно оценить аренду Mac mini на нужный период. Для постоянного процесса решение следует принимать после прохождения всей матрицы восстановления, а не после одной успешной сборки.

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

В официальной документации match указано, что сертификаты и provisioning profiles могут храниться в Git, объектном хранилище или другом поддерживаемом backend, а для CI рекомендуется режим readonly. (docs.fastlane.tools) При этом хранилище ключей должно быть приватным, а доступ — ограниченным чтением для обычной сборки.

Итог: что считать готовой подписью на удалённом Mac

Готовая схема — это не установленный сертификат и не зелёный статус одного задания. Она должна доказать четыре свойства:

  • match получает заранее подготовленные активы без права случайно перевыпустить их;
  • keychain доступен фоновому процессу и не смешивается с чужими заданиями;
  • каждый Target получает ожидаемый provisioning profile;
  • после перезапуска, очистки, замены сертификата и ошибки процесс восстанавливается предсказуемо.

В сравнении с покупкой физического Mac текущий локальный подход требует сразу оплачивать оборудование, самостоятельно обслуживать macOS-узел и принимать простой во время обновлений или ремонта. В сравнении с Linux CI он не может выполнить часть задач, завязанных на Xcode, Apple SDK и кодовую подпись. Виртуальная macOS-среда дополнительно может усложнить доступ к системным функциям и проверку реального поведения keychain.

Если для миграции, сезонного релиза или проверки нового пайплайна не требуется покупать Mac на годы, аренда удалённого Mac у SFTPMAC позволяет сначала проверить именно инженерную часть: root-доступ, SSH, перезапуск Runner, изоляцию keychain и восстановление fastlane match. Для долгой равномерной нагрузки собственный Mac остаётся более предсказуемым по владению, но для временного CI-проекта или узла масштабирования периодический доступ обычно практичнее. Начать можно с подбора удалённого Mac по сроку аренды и сценарию сборки.