После подключения tee журнал сборки становится полным, но CI иногда помечает явно завершившийся ошибкой xcodebuild как успешный. Опаснее всего не потеря журнала, а то, что конвейер продолжает выполнять шаги загрузки, уведомления или развёртывания. Причина обычно проста: по умолчанию shell возвращает код завершения последней команды в конвейере, поэтому успешная запись через tee скрывает ошибку предыдущего процесса xcodebuild.
Сначала выясните, как теряется ошибка
Часто встречается такая конструкция:
xcodebuild test \
-workspace App.xcworkspace \
-scheme App \
-destination "$CI_DESTINATION" \
2>&1 | tee build.log
В этом конвейере два процесса. Слева запускаются тесты, а справа вывод одновременно записывается в файл и передаётся в стандартный поток вывода. Если в Bash не включён pipefail, статус всей команды по умолчанию определяется статусом tee. Даже когда тесты возвращают ненулевой код, при успешной записи журнала скрипт всё равно может получить 0.
Сначала выполните минимальную проверку на облачном Mac:
/bin/bash -c 'exit 65' | tee /tmp/pipeline-check.log
printf 'pipeline=%s\n' "$?"
Если вывод содержит pipeline=0, текущий скрипт допускает ложный успешный результат. Не ограничивайтесь поиском error: в журнале: формат вывода компилятора, тестового фреймворка и пользовательских скриптов может меняться. Код завершения — это стабильный интерфейс, по которому CI определяет успех или неудачу задачи.
Журнал объясняет причину ошибки, а код завершения объявляет о ней. Их нужно сохранять отдельно; поиск по тексту не должен заменять проверку состояния процесса.
Зафиксируйте shell, затем обработайте состояние конвейера
Интерактивный shell по умолчанию в macOS может отличаться от интерпретатора, который фактически выполняет скрипт CI. В Bash используется PIPESTATUS, в zsh — pipestatus; различается и индексация массивов. Надёжнее всего явно указать интерпретатор в скрипте репозитория и запускать этот файл непосредственно из конвейера, а не передавать многострочные команды неопределённому shell.
#!/bin/bash
set -uo pipefail
Параметр -u выявляет обращения к неопределённым переменным, а pipefail заставляет весь конвейер возвращать ненулевой код, если завершилась ошибкой любая из его команд. Однако одного pipefail недостаточно для точной диагностики: если tee завершится ошибкой из-за проблем с диском или правами доступа, а xcodebuild отработает успешно, итоговый $? сообщит лишь об ошибке конвейера, но не укажет, на каком этапе она произошла.
Поэтому сразу после завершения конвейера нужно скопировать PIPESTATUS. Любое присваивание, условная проверка или команда журналирования может обновить этот массив. Если обратиться к нему позже, в нём уже окажутся статусы другой команды.
Создайте переиспользуемую оболочку для xcodebuild
Следующий скрипт одновременно сохраняет исходный журнал, пакет результатов и фактический код завершения xcodebuild. Он временно отключает errexit, чтобы shell не завершился до сохранения статусов.
#!/bin/bash
set -uo pipefail
run_id="${CI_RUN_ID:-local-$(date +%s)}"
artifact_dir="${CI_ARTIFACT_DIR:-$PWD/artifacts/$run_id}"
result_path="$artifact_dir/TestResults.xcresult"
log_path="$artifact_dir/xcodebuild.log"
mkdir -p "$artifact_dir"
set +e
xcodebuild test \
-workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-destination "$CI_DESTINATION" \
-resultBundlePath "$result_path" \
2>&1 | tee "$log_path"
pipeline_status=("${PIPESTATUS[@]}")
xcode_status="${pipeline_status[0]}"
tee_status="${pipeline_status[1]}"
set -e
printf 'xcodebuild_exit=%s\n' "$xcode_status"
printf 'tee_exit=%s\n' "$tee_status"
if (( tee_status != 0 )); then
printf 'Log capture failed\n' >&2
fi
exit "$xcode_status"
Переменную CI_DESTINATION следует явно передавать из конфигурации задачи, например привязав её к заранее проверенной цели симулятора. Не ищите в скрипте первое доступное устройство по неточному шаблону: после обновления среды выполнения может быть выбрана другая версия системы. Все переменные с путями заключайте в кавычки, чтобы пробелы в рабочем каталоге не приводили к разделению аргументов.
В этом варианте итоговый статус задачи определяется статусом xcodebuild. Если сбой записи журнала также должен останавливать задачу, добавьте проверку перед выходом: возвращайте ошибку при ненулевом значении любого из двух статусов и отдельно записывайте оба значения в сводку задачи.
Сохраняйте пакет результатов как диагностическое доказательство
Консольный текст удобен для быстрого просмотра, а xcresult сохраняет иерархию тестов, вложения сбоев, журналы активности и диагностические данные. Эти артефакты служат разным целям, поэтому рекомендуется архивировать оба.
| Артефакт | Основное назначение | Сохранять при ошибке |
|---|---|---|
xcodebuild.log |
Поиск сообщений компилятора и журналов скриптов | Да |
TestResults.xcresult |
Просмотр тестов, вложений и журналов активности | Да |
exit-status.txt |
Запись статусов всех этапов конвейера | Да |
| Временный DerivedData | Кратковременное хранение для воспроизведения проблемы | Согласно политике задачи |
Для формирования машиночитаемой сводки можно использовать системную утилиту xcresulttool. Сначала убедитесь, что пакет результатов действительно существует, и только затем запускайте разбор, чтобы его ошибка не перезаписала исходный статус сборки.
if [[ -d "$result_path" ]]; then
xcrun xcresulttool get test-results summary \
--path "$result_path" \
> "$artifact_dir/test-summary.json" || true
fi
printf 'xcodebuild=%s\ntee=%s\n' \
"$xcode_status" "$tee_status" \
> "$artifact_dir/exit-status.txt"
Здесь || true позволяет продолжить архивирование только после ошибки формирования сводки. Его нельзя добавлять к основной команде xcodebuild. Ошибка вспомогательного отчёта не должна стирать уже сохранённый основной статус, а ошибка основной сборки не должна превращаться в успешный результат из-за вспомогательной команды.
Проверяйте аварийные сценарии, а не только успешные задачи
Перед добавлением скрипта-оболочки в основную ветку выполните как минимум четыре вида проверок:
Ошибка компиляции
Временно добавьте ссылку на несуществующий символ. Ожидаемый результат: xcodebuild возвращает ненулевой код, CI завершается ошибкой, журнал сохраняется, а наличие пакета результатов зависит от этапа, на котором произошёл сбой.
Ошибка теста
Добавьте заведомо неуспешную проверку. Компиляция должна завершиться, но тестовая задача — получить статус ошибки. В xcresult должен определяться неуспешный тест, а конвейер не должен переходить в ветку успешного выполнения.
Ошибка записи журнала
Укажите в качестве каталога артефактов расположение без прав на запись и убедитесь, что tee_status имеет ненулевое значение. Если команда требует полного набора диагностических артефактов перед выпуском, этот сценарий также должен останавливать задачу.
Прерывание и тайм-аут
Отправьте из CI сигнал завершения, убедитесь, что последующие шаги публикации не запускаются, и проверьте, что дочерние процессы завершены. Не представляйте тайм-аут как обычную ошибку тестов: эти события требуют разных стратегий повторного запуска и по-разному трактуются при анализе сбоев.
В завершение проверьте само определение задачи. Шаг загрузки диагностических артефактов можно настроить на выполнение даже после ошибки, но развёртывание, распространение и уведомление об успехе должны зависеть от фактического статуса, возвращённого скриптом-оболочкой. Тогда изменения фильтра вывода, средства архивирования или генератора сводки не помешают передать ошибку xcodebuild обратно в CI по единственному проверяемому пути.
Часто задаваемые вопросы
Зачем читать PIPESTATUS, если включён pipefail?
pipefail сообщает, что в конвейере произошла ошибка, но не гарантирует возврат именно статуса xcodebuild. PIPESTATUS[0] сохраняет код первой команды отдельно от tee и других фильтров.
Как проверить корректность оболочки запуска?
Запустите сценарии с ошибкой компиляции, проваленным тестом и недоступным каталогом журналов. Каждый сценарий должен завершить задание ошибкой и оставить ожидаемые диагностические файлы.
Перенесите следующую задачу разработки в облачный Mac
Выберите Berry M4 или Berry M4 Pro, проверьте пять узлов, срок аренды и дополнительные опции, затем оформите заказ.