Число 65 в завершении xcodebuild сообщает только о неуспешном действии, но не называет его причину — это следует проверять по полному журналу и результату тестирования, а не по последней строке CI (описание командной строки xcodebuild от Apple). Поэтому на этой неделе не удаляйте DerivedData и не пересоздавайте узел сразу: сохраните исходную команду, файл xcresult, окружение и найдите первый отказавший этап. Затем сравните Scheme, зависимости, Destination, подпись и состояние удалённого Mac; к перестройке узла переходите только тогда, когда ошибка не воспроизводится в чистой среде.

Кому нужен этот разбор

Эта инструкция предназначена для разработчика, у которого проект собирается в локальном Xcode, но удалённый Mac CI возвращает xcodebuild Exit Code 65.

Она также пригодится DevOps-инженеру, который обслуживает общий macOS-узел и должен удерживать одинаковыми Xcode, права, зависимости и параметры запуска. Релизный инженер найдёт здесь границу между ошибкой компиляции, тестирования, подписи и архивации.

Временная шкала диагностики: от факта сбоя до подтверждённого исправления

Не превращайте расследование в последовательность случайных очисток. Разделите работу на контрольные точки:

  • Фиксация: сохраните commit, точную команду xcodebuild, рабочий каталог, путь к Xcode, аккаунт CI и момент запуска.
  • Локализация: определите первое действие, которое завершилось ошибкой: разрешение пакетов, компиляция, запуск тестов, подпись или экспорт.
  • Минимальный прогон: уберите только необязательные параметры, но сохраните тот же проект, Scheme и Destination.
  • Исправление: меняйте одну причину за раз и записывайте, какой симптом исчез.
  • Повторная проверка: выполните исходную команду, затем минимальную команду и повторный запуск после перезапуска узла.

Такой порядок важнее самого кода завершения. Последующие сообщения часто являются следствием первого сбоя: например, тестовая цель не запускается потому, что приложение не было собрано. Сообщение оболочки CI о ненулевом статусе — это ещё один слой упаковки, а не доказательство конкретной ошибки Xcode.

Первый этап: извлеките реальную ошибку из журнала

Сначала сохраните весь stdout и stderr, а не только финальные строки. В команде должны быть видны фактические значения:

xcodebuild \
  -workspace "App.xcworkspace" \
  -scheme "AppScheme" \
  -configuration "Release" \
  -destination "platform=iOS Simulator,id=DEVICE_IDENTIFIER" \
  -resultBundlePath "artifacts/result.xcresult" \
  test

Названия workspace, Scheme, устройства и каталогов здесь условные. Параметры -workspace, -scheme, -configuration, -destination и -resultBundlePath относятся к командному интерфейсу xcodebuild; их назначение сверяйте с техническим описанием командной строки Apple.

Ищите первое сообщение с формулировкой error:, отказ команды скрипта или недоступную цель. Затем отделяйте его от:

  • повторного сообщения о той же проблеме;
  • ошибок зависимых целей;
  • финального ** TEST FAILED **;
  • строки CI-оболочки о коде завершения.

xcresult нельзя заменять коротким текстовым логом. В нём могут находиться сведения о тестовой сессии, цели, устройстве и диагностике. Apple описывает чтение результатов тестирования и структуру диагностических данных в документации по интерпретации результатов Xcode.

Локальная сборка против удалённого запуска: где расходятся входные данные

Фраза «локально всё работает» не доказывает эквивалентность двух запусков. В локальном Xcode часть значений берётся из выбранной схемы, графической сессии, локальной связки ключей и уже загруженных пакетов. CI может использовать другой workspace, аккаунт, каталог, SDK или набор переменных.

Scheme, workspace и Build Settings

Проверьте следующие пары:

Объект проверки Локальный запуск Удалённый Mac CI Что подтверждает проблему
Файл проекта .xcodeproj или .xcworkspace Файл, указанный в скрипте Разные графы целей и пакетов
Scheme Выбранная в Xcode Переданная через -scheme Scheme не существует или не shared
Configuration Например, Debug или Release Значение из команды или CI Отличаются флаги, подпись или скрипты
SDK и Destination Выбор Xcode Значение -destination Цель недоступна на узле
Build Settings Проект и конфигурация Проект плюс параметры команды Команда переопределяет проект

Сначала выведите доступные Scheme и Destination, затем раскройте фактические настройки:

xcodebuild -list -workspace "App.xcworkspace"
xcodebuild -showBuildSettings \
  -workspace "App.xcworkspace" \
  -scheme "AppScheme" \
  -configuration "Release"

Командная строка имеет приоритет над значениями, заданными в проекте и конфигурации. Поэтому ищите в CI такие переопределения, как CODE_SIGN_STYLE, PRODUCT_BUNDLE_IDENTIFIER, SDKROOT, SWIFT_VERSION или собственные переменные проекта. Правила областей и приоритетов описаны в документации Apple по Build Settings.

Не начинайте с удаления кеша. Если -showBuildSettings уже показывает неверную конфигурацию, очистка лишь удалит полезные следы и не исправит источник расхождения.

Зависимости и Run Script: ошибка может произойти до компилятора

Если первым отказало разрешение Swift Package, проверьте Package.resolved, доступ к приватному репозиторию, SSH-ключ CI и known_hosts. Удалённая машина может видеть тот же репозиторий, но работать под другим пользователем с другим HOME и другим каталогом .ssh.

Для воспроизводимости зафиксируйте состояние зависимостей в репозитории. Apple отдельно рассматривает сборку Swift Package и приложений с такими пакетами в руководстве по зависимостям в CI. Это не означает, что любой сетевой отказ является ошибкой Xcode: сначала подтвердите, какая команда разрешения пакета завершилась первой.

У Run Script Phase проверяйте:

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

Скрипт, который завершился с ошибкой или не создал заявленный результат, способен остановить сборку до компиляции исходников. В таком случае исправлять Swift-код или менять Simulator преждевременно.

Важно: успешное разрешение пакетов ещё не означает успешную сборку, а запуск Simulator не подтверждает работоспособность тестовой цепочки. На каждом этапе сохраняйте отдельное доказательство.

Сборка против тестирования: как Simulator превращает другой сбой в Exit Code 65

Когда приложение компилируется, но тестовая сессия не начинается, проверяйте не только наличие Simulator. Сопоставьте платформу, установленный runtime, идентификатор устройства и поддержку выбранной Scheme. Destination, подходящий для одной цели, может быть недоступен для другой.

Используйте тот же Destination в диагностическом и контрольном запусках:

xcodebuild \
  -workspace "App.xcworkspace" \
  -scheme "AppScheme" \
  -destination "platform=iOS Simulator,id=DEVICE_IDENTIFIER" \
  -resultBundlePath "artifacts/test-result.xcresult" \
  test

В xcresult ищите, была ли создана тестовая сессия, какое устройство выбрано и на каком этапе появилась ошибка. Apple описывает результаты тестирования как источник сведений о тестах, действиях и диагностике, поэтому финальная строка Exit Code 65 не должна быть единственным артефактом расследования (документация о результатах Xcode).

Полезное различие выглядит так:

Наблюдаемый симптом Вероятная зона проверки Следующий тест
Компилятор остановился до запуска приложения Исходники, зависимости, Build Settings Повторить build с той же Scheme
Приложение собрано, но тестовая сессия не создана Destination, runtime, тестовая цель Проверить список доступных целей и xcresult
Simulator стартует, но тесты не выполняются Scheme, тестовый bundle, окружение процесса Повторить test с тем же устройством
Сбой появляется на подписи перед запуском Сертификат, профиль, связка ключей Выполнить отдельный подписывающий прогон
CI показывает только код 65 Недостаточно диагностических артефактов Сохранить полный лог и результат

Вопрос о том, почему запуск Simulator способен закончиться этим кодом, решается не сбросом устройства, а определением границы отказа. Если xcresult показывает, что тестовый процесс не был запущен, сначала проверяйте Destination и runtime. Если тест начался и упал внутри приложения, причина уже находится в тестовой логике или среде выполнения.

Подпись и архив: отдельная граница между сборкой и выпуском

Проверяйте подпись только тогда, когда первый подтверждённый отказ указывает на сертификат, профиль, Team или связку ключей. Автоматическая подпись, успешно работающая в графическом Xcode под вашей учётной записью, не доказывает, что CI-пользователь имеет доступ к закрытому ключу.

Сопоставьте:

  • Team и идентификатор приложения;
  • способ подписи для выбранной конфигурации;
  • сертификат и соответствующий закрытый ключ;
  • provisioning profile;
  • связку ключей, доступную именно пользователю CI;
  • состояние разблокировки связки в момент выполнения.

Для архива и экспорта сохраняйте разные артефакты:

xcodebuild archive \
  -workspace "App.xcworkspace" \
  -scheme "AppScheme" \
  -configuration "Release" \
  -archivePath "artifacts/App.xcarchive"

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

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

Действия на удалённом Mac: ремонт, изоляция или пересоздание

После проверки проекта сравните среду выполнения. Зафиксируйте:

xcode-select -p
xcodebuild -version
printenv DEVELOPER_DIR
whoami
pwd
df -h

Пути, версии и имена пользователей зависят от вашего узла. Команда xcode-select и переменная DEVELOPER_DIR должны быть согласованы с тем Xcode, который ожидает CI. Apple описывает настройку инструментов командной строки в официальной документации Xcode.

Проверьте также:

  • права выполнения под фактическим CI-аккаунтом;
  • свободное место и доступность рабочего каталога;
  • остатки предыдущей сборки;
  • разницу между графической и SSH-сессией;
  • поведение после перезапуска;
  • наличие нескольких Xcode и неявное переключение инструментария.

Для удалённой работы важно различать интерактивную и автоматическую сессию. SSH-подключение, окружение оболочки и доступ к GUI-ресурсам могут отличаться; рекомендации Apple по автоматизации тестов и SSH приведены в документе о тестовых сессиях.

Условия принятия решения

Используйте следующую развилку, а не правило «ошибка есть — узел заменить»:

  • Если исходная команда и минимальный запуск падают на одном и том же первом действии, выбирайте исправление проекта или CI-конфигурации.
  • Если проблема связана только с одним рабочим каталогом, а чистая копия проходит, очистите или изолируйте этот каталог, предварительно сохранив xcresult и лог.
  • Если разные проекты на узле сталкиваются с разными отказами, а DEVELOPER_DIR, права или доступ к связке меняются между сессиями, изолируйте инструменты и аккаунты.
  • Если ошибка исчезает после перезапуска, но возвращается при следующем запуске, не объявляйте восстановление завершённым: ищите зависимость от состояния узла.
  • Если чистая рабочая копия, исходная команда и повтор после перезапуска дают одинаково стабильный результат, пересоздание узла не оправдано.
  • Если только новый чистый узел воспроизводимо исправляет сбой, переносите проект и фиксируйте различия среды, а старый узел не удаляйте до завершения проверки.

Для команд, которым нужен управляемый macOS-узел с полными правами и возможностью повторить один и тот же запуск, можно рассмотреть удалённый Mac для разработки и CI. Такой вариант полезен именно как контролируемая среда для проверки гипотезы, а не как замена расследованию.

Матрица повторной проверки: когда считать Exit Code 65 устранённым

Соберите один набор доказательств до изменения и один после него:

Проверка До исправления После исправления Критерий
Исходная команда Полный лог и xcresult Повторный полный лог и xcresult Первый отказ исчез
Минимальная команда Та же Scheme и цель Те же параметры Результат стабилен
Рабочая копия Состояние каталога зафиксировано Чистая копия проверена Нет скрытого остатка
Учётная запись Пользователь CI и окружение Те же права Нет зависимости от GUI
Узел после перезапуска Состояние до перезапуска Повтор после перезапуска Сбой не возвращается

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

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

Когда текущий узел постоянно теряет окружение, требует ручного входа в GUI, скрывает xcresult или не позволяет отделить проектную ошибку от состояния машины, вы тратите время не на сборку, а на восстановление инфраструктуры. В такой ситуации аренда Mac у VMSPIN может быть разумнее покупки отдельного Mac mini для временного проекта: вы получаете удалённый доступ, можете повторить тот же репозиторий и сначала проверить воспроизводимость, не превращая неисправный CI в долгосрочную аппаратную закупку. Для перехода к конкретной среде используйте варианты заказа удалённого Mac только после того, как определите нужные Scheme, Destination и требования к подписи.