После завершения архивации на CI-узле артефакты необходимо передать в задачи тестирования, публикации или длительного хранения. Самое очевидное решение — назначить общий SMB-каталог рабочей областью, чтобы несколько облачных Mac читали и изменяли один и тот же проект. Для небольшого репозитория это может некоторое время работать, но при нестабильной сети, задержках блокировки файлов или сбое монтирования промежуточные файлы Xcode могут повредиться. Более того, результаты могут оказаться в локальном пустом каталоге с тем же именем.
Надёжнее провести чёткую границу: SMB используется только для получения снимков входных данных и передачи готовых артефактов, а распаковка исходного кода, разрешение зависимостей, DerivedData и временные файлы остаются на локальном диске.
Сначала разделите каталоги на три категории
Для каждой задачи нужны как минимум три независимых пути, которые не используются для разных целей:
- Область входных данных только для чтения: хранит снимки исходного кода или пакеты зависимостей с зафиксированными версиями.
- Локальная рабочая область: содержит извлечённый исходный код, DerivedData, результаты тестирования и временные файлы.
- Удалённая область доставки: хранит только проверенные архивы, сводки журналов и манифесты.
Не позволяйте двум параллельным задачам использовать один каталог DerivedData. Даже если они собирают один и тот же коммит, индекс, кэш модулей и база данных сборки могут изменяться одновременно. Рекомендуется включать идентификатор задачи в путь, например ~/ci-work/$RUN_ID, а после завершения задачи очищать каталог в соответствии с политикой хранения.
Сетевой ресурс следует считать границей доставки, а не прозрачной заменой локального диска. Успешная сборка и успешная загрузка также должны регистрироваться как два независимых состояния.
Управляйте SMB-монтированием через постоянный процесс
Автоматические задачи не должны передавать имя пользователя и пароль в аргументах командной строки. Лучше заранее монтировать SMB с помощью контролируемого постоянного процесса, который получает учётные данные из отдельной связки ключей или защищённой конфигурации. Обычным задачам сборки при этом предоставляются только минимально необходимые права на запись в целевой каталог.
В начале задачи недостаточно проверить лишь наличие каталога. После отключения SMB точка монтирования может превратиться в обычный локальный каталог. Как минимум следует проверить тип файловой системы, возможность записи и двустороннюю передачу небольшого файла:
set -euo pipefail
SHARE_ROOT="/Volumes/ci-artifacts"
PROBE="$SHARE_ROOT/.probe-${RUN_ID}"
fs_type="$(stat -f '%T' "$SHARE_ROOT")"
test "$fs_type" = "smbfs"
test -w "$SHARE_ROOT"
printf '%s\n' "$RUN_ID" > "$PROBE"
test "$(cat "$PROBE")" = "$RUN_ID"
rm -f "$PROBE"
Сбой на любом этапе должен немедленно останавливать задачу. Не следует автоматически создавать точку монтирования и продолжать выполнение: при отключении общего ресурса это может привести к ситуации, когда «сборка завершена успешно», но артефакты остались только на локальном компьютере.
Выполняйте сборку только на локальном диске
Сначала синхронизируйте зафиксированный коммит с локальным временным каталогом, а затем запускайте разрешение зависимостей и сборку. Каталог задачи, DerivedData и пакет результатов должны содержать уникальный идентификатор задачи, чтобы параллельные процессы не перезаписывали данные друг друга.
LOCAL_ROOT="$(mktemp -d "$TMPDIR/vmcache-ci.XXXXXX")"
trap 'rm -rf "$LOCAL_ROOT"' EXIT
rsync -a --delete "$SOURCE_SNAPSHOT/" "$LOCAL_ROOT/repo/"
xcodebuild \
-workspace "$LOCAL_ROOT/repo/App.xcworkspace" \
-scheme App \
-configuration Release \
-derivedDataPath "$LOCAL_ROOT/DerivedData" \
-resultBundlePath "$LOCAL_ROOT/TestResults.xcresult" \
build
Значение SOURCE_SNAPSHOT должно соответствовать неизменяемому коммиту, а не активному каталогу, который могут продолжать изменять другие задачи. Если входные данные поступают с общего тома, перед синхронизацией можно проверить идентификатор коммита или манифест.
Не кэшируйте DerivedData целиком
Копирование всего каталога DerivedData между задачами обычно приносит больше проблем, чем пользы: кэш занимает много места, а внутренние пути и параметры сборки могут меняться. Проще обслуживать кэш, содержащий только явно переиспользуемые каталоги загруженных зависимостей. В ключ кэша следует включать версию Xcode, архитектуру и хеш файла блокировки. Если ключи не совпадают, кэш нужно создать заново, не пытаясь использовать приблизительно подходящие данные.
Используйте временное имя для атомарной доставки
Если сразу копировать файл под окончательным именем, незавершённый артефакт станет доступен другим задачам. Они могут начать чтение сразу после появления файла, хотя передача данных ещё продолжается. Правильный порядок таков: упаковать данные локально, локально вычислить хеш, скопировать файл на общий том под временным именем, проверить его на общем томе и лишь затем переименовать в том же каталоге.
ARTIFACT="$LOCAL_ROOT/App-release.zip"
ditto -c -k --norsrc "$LOCAL_ROOT/DerivedData/Build/Products/Release" "$ARTIFACT"
REMOTE_DIR="$SHARE_ROOT/releases/$GIT_COMMIT"
REMOTE_TMP="$REMOTE_DIR/.App-release.zip.${RUN_ID}.partial"
REMOTE_FINAL="$REMOTE_DIR/App-release.zip"
mkdir -p "$REMOTE_DIR"
cp "$ARTIFACT" "$REMOTE_TMP"
local_hash="$(shasum -a 256 "$ARTIFACT" | awk '{print $1}')"
remote_hash="$(shasum -a 256 "$REMOTE_TMP" | awk '{print $1}')"
test "$local_hash" = "$remote_hash"
mv "$REMOTE_TMP" "$REMOTE_FINAL"
printf '%s %s\n' "$remote_hash" "App-release.zip" \
> "$REMOTE_DIR/SHA256SUMS.${RUN_ID}"
Атомарное переименование возможно только в том случае, если временный и окончательный файлы находятся на одном общем томе, а лучше — в одном каталоге. Не записывайте файл сначала локально, чтобы затем выполнить межтомную операцию mv: при перемещении между томами она превращается в копирование с последующим удалением.
Сделайте сбои наблюдаемыми и предусмотрите очистку
После сбоя загрузки локальные результаты сборки следует сохранять в течение ограниченного времени, чтобы передачу можно было повторить. Однако такую задачу нельзя отмечать как полностью доставленную. Рекомендуется отдельно регистрировать состояния build_completed, artifact_verified и publish_completed; последующие задачи должны принимать только последнее из них.
Также необходимо регулярно удалять файлы .partial, срок хранения которых истёк. Задача очистки должна проверять формат имени и время изменения файла, обрабатывая только временный суффикс и не сканируя и не удаляя готовые артефакты. Если общий том стал доступен только для чтения, проверка выявляет несовпадение или подключение постоянно прерывается, очередь публикации следует остановить, предварительно сохранив локальные результаты и диагностические данные монтирования.
При внедрении этого процесса на облачных Mac VMCache главное — не конкретная команда монтирования, а чёткое разделение ответственности: сетевой том служит для обмена, локальный диск — для сборки, а окончательное имя файла присваивается только проверенному артефакту. Тогда даже кратковременный сбой SMB не смешает состояние рабочей сборки с состоянием доставки.
Часто задаваемые вопросы
Можно ли хранить DerivedData на SMB-ресурсе?
Не стоит. Xcode часто создаёт мелкие файлы, блокировки и метаданные, поэтому сетевой том добавляет нестабильность. Для каждого задания лучше выделять отдельный локальный каталог.
Как не дать следующему заданию прочитать недокопированный архив?
Сначала архив копируют во временный файл на SMB, затем создают контрольную сумму и манифест завершения. После проверки временный файл переименовывают в итоговый в том же каталоге.
Что делать, если точка монтирования существует, но SMB отключён?
До сборки нужно проверить тип файловой системы и выполнить тест записи с обратным чтением. При ошибке задание завершается, иначе файлы могут попасть в обычный локальный каталог с тем же путём.
Выберите облачный Mac для сборки, тестирования и инференса MLX
Выберите M4, объём памяти, хранилище, узел и срок аренды под свои задачи. Каждая аренда включает выделенный физический компьютер, а не виртуальную машину.