Когда один облачный Mac последовательно выполняет несколько заданий CI, самые трудно воспроизводимые сбои часто вызваны не ошибками в коде, а временными файлами, оставшимися после предыдущего задания. Старые сокеты, неосвобождённые блокировки, частично записанные кэши или каталоги экспорта с одинаковыми именами могут привести к тому, что следующая сборка того же коммита даст другой результат. Выделенный физический узел исключает влияние других аккаунтов, но не изолирует автоматически параллельные задания одного аккаунта. Поэтому границы файловой системы необходимо явно задавать в конвейере.
Сначала определите каталоги, данные из которых переходят между заданиями
Не считайте, что очистка рабочей области означает полное восстановление окружения. Инструменты macOS сохраняют состояние за пределами рабочей области, поэтому при диагностике следует проверить как минимум четыре категории расположений:
- временные файлы, сокеты и фрагменты загрузок в
TMPDIR; - DerivedData, индексы и базы данных сборки Xcode;
- кэш модулей Clang и кэши SwiftPM на уровне задания;
- каталоги экспорта, журналов и тестовых вложений, создаваемые скриптами.
Перед обычной сборкой и после неё выполните env | sort, чтобы зафиксировать фактически используемые HOME, TMPDIR и путь к рабочей области. Затем сравните размеры соответствующих каталогов с помощью du -sh, не начиная сразу с рекурсивного удаления. Если сбой возникает только при параллельном выполнении, в первую очередь ищите фиксированные имена файлов, порты и пути вывода.
Цель изоляции — не удалять все кэши перед каждым запуском, а предоставить заданию собственные каталоги с правом записи и очищать только созданное этим заданием содержимое.
Создайте уникальный корневой каталог для каждого задания
Идентификатор задания должен поступать из уникального номера, предоставляемого системой CI. Если надёжного номера нет, каталог можно создать с помощью mktemp. Не используйте только имя ветки: одна и та же ветка может одновременно запустить несколько сборок.
#!/bin/bash
set -euo pipefail
job_id="${CI_JOB_ID:-manual}"
job_root="$(mktemp -d "${TMPDIR:-/tmp}/surevm-ci.${job_id}.XXXXXX")"
export TMPDIR="${job_root}/tmp"
export DERIVED_DATA="${job_root}/DerivedData"
export MODULE_CACHE="${job_root}/ModuleCache"
export RESULT_BUNDLE="${job_root}/Results/Test.xcresult"
mkdir -p "$TMPDIR" "$DERIVED_DATA" "$MODULE_CACHE" "$(dirname "$RESULT_BUNDLE")"
cleanup() {
local status=$?
rm -rf "$job_root"
exit "$status"
}
trap cleanup EXIT INT TERM
Каталог, созданный командой mktemp, по умолчанию не совпадёт с каталогом другого задания. Функция очистки сначала сохраняет код завершения, затем удаляет каталог и завершается с исходным кодом, чтобы команда очистки не скрыла ошибку тестов. Переменные с путями необходимо заключать в кавычки, иначе имя рабочей области с пробелами будет разбито на несколько аргументов.
Не переопределяйте HOME без необходимости
Перенаправление HOME в каталог задания кажется способом добиться полной изоляции, но может лишить текущего пользователя доступа к Keychain, настройкам инструментов и уже выполненной системной инициализации. Для обычной компиляции можно назначить отдельные кэши конкретным менеджерам пакетов. Если используется подпись, обычно следует сохранить HOME и изолировать только артефакты сборки и кэши, которые можно безопасно перенести.
Явно задайте пути записи для Xcode
Одной настройки TMPDIR недостаточно для переноса DerivedData. Критически важные пути следует передавать в команду сборки как параметры, чтобы в журнале было сразу видно, какие каталоги использовало текущее задание.
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-derivedDataPath "$DERIVED_DATA" \
-clonedSourcePackagesDirPath "${job_root}/SourcePackages" \
COMPILER_INDEX_STORE_ENABLE=NO \
CLANG_MODULE_CACHE_PATH="$MODULE_CACHE" \
test \
-resultBundlePath "$RESULT_BUNDLE"
Если в CI не требуется индексирование кода, хранение индекса можно отключить, чтобы сократить лишние операции записи. Результаты тестов следует сохранять внутри корневого каталога задания. Однако если после сбоя нужно загрузить вложения, сначала необходимо завершить их архивацию и только потом запускать очистку. Более надёжная последовательность выглядит так: выполнить тесты, скопировать диагностические файлы в область артефактов конвейера, проверить результат копирования и завершить работу.
Общий кэш — только для чтения, кэш задания — для записи
Если зависимости действительно необходимо использовать повторно, проверенный общий кэш можно считать исходным: в начале задания скопировать или восстановить его в собственный каталог, во время сборки записывать только в копию задания, а после успешного завершения обновить общую версию отдельным шагом. Не позволяйте двум процессам xcodebuild одновременно записывать данные в один кэш модулей или одну базу данных сборки.
Сохраняйте данные для диагностики даже при аварийном завершении
Прямая установка trap 'rm -rf ...' EXIT удалит состояние окружения сразу после сбоя. На практике перед очисткой следует сформировать небольшой диагностический отчёт с кодом завершения, доступным местом на диске, размерами каталогов и незавершёнными связанными процессами, а затем решить, какие файлы сохранить. Не выводите в журнал полный набор переменных окружения: среди них могут находиться токены или значения, связанные с подписью.
Перед очисткой можно выполнить:
{
echo "exit_status=$status"
df -h "$job_root"
du -sh "$job_root"/* 2>/dev/null || true
find "$job_root" -type s -print
} > "${ARTIFACT_DIR}/job-cleanup.txt"
ARTIFACT_DIR должен находиться за пределами корневого каталога задания. За его загрузку и периодическую очистку должен отвечать конвейер. Если состояние после сбоя необходимо сохранить, упакуйте диагностический каталог перед удалением исходного, предварительно обезличив журналы и пути.
Проверьте изоляцию с помощью аудита остаточных данных
После внесения изменений дважды запустите один и тот же коммит и перед началом второго запуска убедитесь, что каталога с идентификатором предыдущего задания больше нет. При параллельном запуске дополнительно проверьте, что пути TMPDIR, DerivedData и пакетов результатов у двух заданий полностью различаются.
Контрольный список можно свести к пяти пунктам: каждое задание имеет уникальный корневой каталог; все пути Xcode указаны явно; в общий кэш нет параллельной записи; обработчик завершения запускается и при успехе, и при сбое; диагностические артефакты сначала копируются и только затем удаляются. Если использование диска продолжает расти, найдите по идентификатору задания каталоги инструментов, оставшиеся за пределами изоляции, вместо того чтобы расширять область удаления.
На выделенных физических Mac от SureVM этот подход особенно полезен для постоянно работающих узлов сборки: он позволяет сохранить системный набор инструментов, ограничивая изменяемое состояние каждого задания отслеживаемыми и удаляемыми каталогами. Итоговый критерий прост: предыдущее задание не должно менять входные условия следующего независимо от того, завершилось ли оно успешно, с ошибкой или было прервано.
Часто задаваемые вопросы
Почему задания CI не должны использовать один постоянный временный каталог?
Параллельные процессы могут перезаписывать одинаковые файлы, а после сбоя остаются блокировки, сокеты и промежуточные данные. Надёжнее создавать уникальный подкаталог для каждого задания.
Достаточно ли изменить только TMPDIR?
Нет. DerivedData и кэш модулей Xcode могут находиться в других местах, поэтому их пути следует задавать отдельно для каждого задания.
Нужно ли изолировать HOME в заданиях с подписью?
Обычно нет. Подпись может зависеть от Keychain текущего пользователя, поэтому безопаснее оставить HOME неизменным и изолировать только временные файлы, результаты сборки и переносимые кэши.
Выбирайте выделенные физические узлы Mac для сборки, разработки и экспериментов
Сравните две конфигурации Mac mini M4, четыре срока аренды и пять доступных узлов, а затем выберите вариант под свой рабочий процесс.