Конвейер сообщает об успешном завершении, но загруженный IPA невозможно сопоставить с коммитом, который запустил сборку. Есть и более опасный сценарий: загрузка прерывается, после чего следующая задача забирает файл, записанный лишь наполовину. На облачном Mac компиляцию, экспорт и загрузку обычно выполняют разные скрипты. Если единственным критерием готовности считать код завершения команды 0, обе проблемы сложно обнаружить в рамках той же задачи.
Решение заключается не в добавлении ещё одной команды сжатия, а в определении артефакта как неделимого набора. IPA, манифест SHA-256, контекст сборки и результаты проверки должны создаваться вместе и публиковаться только после успешного прохождения всех проверок.
Сначала определите состав поставки
Каждая задача должна использовать уникальный BUILD_ID. Каталог поставки должен содержать как минимум следующие файлы:
| Файл | Назначение | Критерий приёмки |
|---|---|---|
App.ipa |
Артефакт для установки и распространения | Хеш совпадает, команда экспорта завершилась успешно |
SHA256SUMS |
Манифест целостности содержимого | shasum -a 256 -c завершается успешно |
build-context.tsv |
Отслеживание источника сборки | Коммит, версия Xcode и идентификатор задачи не пусты |
export.log |
Диагностика ошибок экспорта | Полный вывод сохранён, доступ к нему ограничен |
Контекст сборки следует формировать из переменных, уже определённых конвейером, а не пытаться угадать ветку внутри скрипта. Как минимум необходимо записывать полный хеш коммита, идентификатор задачи, Scheme, версию Xcode и время создания. Время служит только для сопоставления с журналами; идентичность артефакта по-прежнему определяется хешем коммита и контрольной суммой.
Хеш подтверждает, что файл не изменился между двумя точками проверки, но не доказывает, что изначально он был получен в доверенном процессе. Поэтому необходимы и проверка подписи кода, и контекст сборки.
Проверьте архив перед экспортом
Сначала убедитесь, что архив существует, затем найдите в нём .app и проверьте подпись кода. Не используйте --deep для повторного подписывания: здесь требуется лишь проверить существующую структуру подписи. Если приложение содержит расширения, команда проверки должна охватывать вложенный код, а любая ошибка должна блокировать дальнейшее выполнение.
Архив, каталог экспорта и итоговый каталог должны находиться на одном рабочем томе. Только в этом случае финальное переименование каталога будет атомарным. При выполнении на облачном Mac MiniRent каждой параллельной задаче также следует выделять отдельный корневой каталог, чтобы две задачи не использовали общий фиксированный путь вроде out/latest.
Скрипт, который можно сразу адаптировать
set -euo pipefail
: "${BUILD_ID:?BUILD_ID is required}"
: "${GIT_COMMIT:?GIT_COMMIT is required}"
: "${SCHEME:?SCHEME is required}"
ROOT="${ARTIFACT_ROOT:-$PWD/out}"
ARCHIVE="$ROOT/input/App.xcarchive"
EXPORT_OPTIONS="$ROOT/input/ExportOptions.plist"
STAGE="$ROOT/.stage-$BUILD_ID"
FINAL="$ROOT/releases/$BUILD_ID"
rm -rf "$STAGE"
mkdir -p "$STAGE" "$(dirname "$FINAL")"
trap 'rm -rf "$STAGE"' EXIT
test -d "$ARCHIVE"
test -f "$EXPORT_OPTIONS"
test ! -e "$FINAL"
APP_PATH="$(find "$ARCHIVE/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)"
test -n "$APP_PATH"
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
xcodebuild -exportArchive \
-archivePath "$ARCHIVE" \
-exportPath "$STAGE/export" \
-exportOptionsPlist "$EXPORT_OPTIONS" \
>"$STAGE/export.log" 2>&1
IPA_PATH="$(find "$STAGE/export" -maxdepth 1 -name '*.ipa' -print -quit)"
test -n "$IPA_PATH"
cp "$IPA_PATH" "$STAGE/App.ipa"
DIGEST="$(shasum -a 256 "$STAGE/App.ipa" | awk '{print $1}')"
printf '%s %s
' "$DIGEST" "App.ipa" >"$STAGE/SHA256SUMS"
XCODE_VERSION="$(xcodebuild -version | paste -sd ' ' -)"
printf 'build_id %s
commit %s
scheme %s
xcode %s
' \
"$BUILD_ID" "$GIT_COMMIT" "$SCHEME" "$XCODE_VERSION" \
>"$STAGE/build-context.tsv"
(
cd "$STAGE"
shasum -a 256 -c SHA256SUMS
)
mv "$STAGE" "$FINAL"
(
cd "$FINAL"
shasum -a 256 -c SHA256SUMS
)
Файл ExportOptions.plist в этом скрипте должен поступать из конфигурации, которая хранится под контролем версий в репозитории. Не записывайте в журналы или файлы контекста пароли, закрытые ключи, временные токены и полный набор переменных окружения.
Изолируйте незавершённые артефакты с помощью атомарной публикации
Главное правило: сначала завершите всю запись и проверку в скрытом временном каталоге, а затем сделайте итоговый каталог доступным одной операцией mv. Потребители должны сканировать только releases/ и не обращаться к .stage-*, поэтому они не смогут получить файл, который ещё создаётся.
Такая гарантия действует только в пределах одной файловой системы. Если каталог публикации подключён на другом томе, mv может фактически превратиться в копирование с последующим удалением. В этом случае временный каталог нужно создать на целевом томе, скопировать туда все файлы, повторно проверить хеши на стороне назначения и лишь затем переименовать каталог внутри того же тома. Если объектное хранилище или сервис артефактов не поддерживает семантику переименования каталогов, сначала загрузите объекты под временными ключами с идентификатором задачи, после проверки запишите небольшой маркер завершения, а потребители должны проверять этот маркер до чтения артефактов.
Не перезаписывайте latest
При использовании latest повторно запущенная задача легко может перезаписать более новый успешный результат. Надёжнее сохранять артефакты под неизменяемым BUILD_ID, а отдельно поддерживать файл-указатель, содержащий только идентификатор целевой задачи. Перед обновлением указателя необходимо убедиться, что целевой каталог прошёл проверку. При откате также следует менять только указатель, не затрагивая исторические артефакты.
Останавливайте сбой на правильном этапе
Ошибка подписи должна останавливать процесс до экспорта, ошибка экспорта — в пределах временного каталога, а несовпадение хеша — до публикации. Повторная проверка после загрузки позволяет обнаружить изменения, вызванные передачей данных, диском или выбором неправильного файла в скрипте.
К типичным ошибкам относятся выбор «первого файла» из каталога экспорта без ограничения по расширению, общий путь вывода для нескольких Scheme и повторное использование временного каталога, оставшегося от предыдущего запуска. В начале работы скрипт должен очищать временный каталог текущей задачи, но не удалять каталоги других задач с помощью слишком широких шаблонов.
Для журналов также нужны чёткие границы. Сохраняйте полный вывод xcodebuild и его фактический статус завершения, но не печатайте конфиденциальные переменные окружения. Перед отправкой запроса в поддержку оставьте только относящийся к проблеме временной интервал и удалите токены репозитория, внутренние идентификаторы из путей к материалам подписи и учётные данные для подключения.
Контрольный список перед поставкой
Прежде чем конвейер отметит задачу как успешную, последовательно проверьте следующее:
- App в архиве прошёл строгую проверку подписи кода.
- IPA экспортирован из архива текущей задачи, а не взят из старого каталога.
GIT_COMMITсодержит полный хеш коммита и совпадает с записью о запуске.SHA256SUMSпроверен один раз во временном и один раз в итоговом каталоге.- Итоговый каталог назван уникальным идентификатором задачи и не перезаписывается при повторном запуске.
- Потребители читают только опубликованные каталоги или объекты с маркером завершения.
- Журналы позволяют определить этап сбоя, но не содержат учётных данных или содержимого материалов подписи.
Этот процесс не устраняет все возможные сбои сборки, но разделяет вопросы «успешно ли завершилась компиляция» и «можно ли доверять поставленному артефакту» на две независимо проверяемые задачи. Когда правила работы с архивом, контрольными суммами, контекстом и границами публикации зафиксированы, повторные запуски и откаты оперируют конкретными объектами, а не зависят от постоянно меняющегося общего каталога.
Часто задаваемые вопросы
Доказывает ли SHA-256, что артефакт iOS создан на доверенной машине?
Нет. Эта сумма подтверждает лишь отсутствие изменений между двумя проверками. Дополнительно проверьте подпись кода и сохраните рядом коммит, версию Xcode, идентификатор задания и манифест.
Зачем сначала готовить временный каталог, а затем переименовывать его?
В пределах одной файловой системы переименование сразу открывает потребителям полностью готовый каталог. Между разными томами сначала выполните копирование и повторную проверку, а затем переименуйте каталог на целевом томе.
Облачный Mac на выделенном физическом узле
Запускайте сборки на выделенном физическом узле
Выбирайте MiniRents M4 или MiniRents M4 Pro с оплатой за день, неделю, месяц или квартал, а узел — с учётом команды и расположения репозитория. Устройства работают на выделенных физических машинах, а не в виртуальной среде.