Когда iOS-репозиторий с многолетней историей коммитов, исходниками дизайна, записями экрана и тестовыми фикстурами впервые переносят на облачный Mac, долгую подготовку машины легко принять за проблему с CPU или диском. На практике узкое место часто возникает ещё до сборки: обычные объекты Git загружаются целиком, Git LFS автоматически скачивает все крупные файлы при извлечении, хотя в итоге CI собирает только одно приложение. Решение — не в бездумном кэшировании всего каталога, а в фиксации входных данных и раздельном управлении объектами коммитов, путями рабочего дерева и содержимым LFS.
Сначала измерьте стоимость извлечения
Не ограничивайтесь замером времени выполнения git clone. Как минимум разделите процесс на три этапа: передачу данных по сети, развёртывание рабочего дерева и загрузку LFS. Сохраните следующие результаты:
git --version
git lfs version
du -sh .git
du -sh .git/lfs 2>/dev/null || true
du -sh .
git count-objects -vH
git lfs ls-files | wc -l
Первый запуск и повторное использование рабочего дерева следует измерять отдельно. Первый запуск показывает стоимость передачи данных с удалённого сервера, а повторный помогает выявить неэффективную очистку, накопление исторических объектов и разрастание кэша LFS. Также записывайте итоговый идентификатор коммита, а не только имя ветки:
git rev-parse HEAD
git status --short
Цель оптимизации — не просто уменьшить размер каталога, а обеспечить стабильное воспроизведение одного и того же коммита, одного и того же набора каталогов и одного и того же набора объектов LFS.
Объедините частичное клонирование и разреженное извлечение
Фильтр blob:none при частичном клонировании откладывает загрузку содержимого обычных файлов, а разреженное извлечение ограничивает набор каталогов, которые фактически разворачиваются в рабочем дереве. Эти механизмы можно использовать вместе, но они не ограничивают Git LFS автоматически, поэтому при первом извлечении необходимо явно отключить smudge.
export GIT_LFS_SKIP_SMUDGE=1
git clone --filter=blob:none --no-checkout "$REPO_URL" app
cd app
git lfs install --local
git sparse-checkout init --cone
git sparse-checkout set App Packages Shared
git fetch --depth=1 origin "$BUILD_REF"
git checkout --detach FETCH_HEAD
git rev-parse HEAD
Режим --cone подходит для проектов, организованных по каталогам: его правила проще, а файлы в родительских каталогах реже исчезают неожиданным образом. App, Packages и Shared необходимо заменить реальными зависимостями сборки. Если рабочее пространство, скрипты или конфигурационные файлы находятся в корне репозитория, убедитесь, что они по-прежнему включены.
Для частичного клонирования удалённый сервер должен поддерживать фильтрацию объектов. Если команда выводит предупреждение об отсутствии такой возможности, считайте, что произошёл откат к обычному клонированию, и повторно измерьте размер .git. Нельзя предполагать, что оптимизация уже работает.
Загружайте только объекты LFS, необходимые текущей задаче
После извлечения файлы LFS всё ещё могут оставаться указателями. Сначала выведите объекты, на которые ссылается текущий коммит, а затем загрузите их с фильтрацией по путям задачи:
git lfs ls-files
git lfs pull --include="App/Assets/**,Shared/Fixtures/**"
git lfs fsck
Фильтры путей следует определять по входным данным сборки, а не угадывать по расширениям файлов. UI-тесты могут зависеть от изображений, видео и локализованных фикстур. Если загрузить только каталоги с исходным кодом, компиляция может завершиться успешно, но тесты упадут во время выполнения.
| Проверка | Ожидаемый результат | Типичная проблема |
|---|---|---|
git lfs ls-files |
Отображаются файлы текущего коммита, отслеживаемые через LFS | .gitattributes не извлечён вместе с целевым коммитом |
| Проверка начала файла | Файл содержит реальные двоичные данные | Файл по-прежнему является указателем с oid sha256 |
git lfs fsck |
Локальные объекты успешно проходят проверку | Загрузка прервана или каталог объектов повреждён |
git status --short |
В рабочем дереве нет неожиданных изменений | После извлечения инструмент перезаписал файлы ресурсов |
Не помещайте git lfs pull без ограничений по путям в общий скрипт инициализации, иначе каждая задача будет загружать не относящиеся к ней ресурсы. Надёжнее, если каждый конвейер явно объявляет собственный набор входных данных LFS.
Управляйте параллельными задачами и учётными данными в CI
Каждая задача CI должна использовать отдельное рабочее дерево. Если несколько задач совместно используют один репозиторий с правом записи, правила разреженного извлечения, блокировки индекса и временные файлы LFS могут мешать друг другу. Даже если задачи читают один и тот же код, не позволяйте им одновременно изменять .git/info/sparse-checkout.
Передавайте учётные данные только на этапе загрузки и ограничивайте их разрешения теми, которые необходимы для целевого репозитория. После выполнения git fetch и git lfs pull удаляйте временные переменные окружения и вспомогательные настройки учётных данных. Не выводите в журналы адреса удалённых репозиториев с токенами. Безопасность отображаемых значений можно проверить следующими командами:
git remote -v
git config --local --get-regexp 'credential|lfs' || true
Подмодули требуют отдельной настройки. Параметры частичного клонирования и правила разреженного извлечения основного репозитория не передаются подмодулям автоматически. Если подмодуль также использует LFS, установите локальную конфигурацию LFS в его каталоге и выполните соответствующую загрузку.
Избегайте небезопасной очистки
git lfs prune подходит для репозитория с единственным пользователем, но не для каталога объектов LFS, совместно используемого несколькими задачами. В общей среде безопаснее создавать для каждой задачи отдельный каталог и полностью удалять его после завершения. Если рабочее дерево всё же необходимо использовать повторно, сначала убедитесь в отсутствии параллельных задач и только затем выполняйте очистку и проверку целостности.
Проверяйте входные данные до сборки, а не ищите причины постфактум
Перед запуском xcodebuild добавьте лёгкую приёмочную проверку. Она должна как минимум подтверждать правильность идентификатора коммита, чистоту рабочего дерева, наличие ключевых файлов проекта и замену указателей LFS реальным содержимым, а также выводить объём занятого дискового пространства. Например:
test -f App/App.xcodeproj/project.pbxproj
test -s App/Assets/LaunchVideo.mov
if grep -q "oid sha256:" App/Assets/LaunchVideo.mov; then
exit 1
fi
git diff --exit-code
git lfs fsck
du -sh . .git .git/lfs
В завершение сохраняйте четыре набора данных: хеш коммита, список разреженно извлечённых каталогов, правила LFS include и результат приёмочной проверки. Тогда при сбое задачи на облачном Mac можно сначала проверить полноту входных данных и лишь затем переходить к журналам компилятора, подписи или тестов. Это позволяет не маскировать ошибки подготовки репозитория под случайные сбои сборки.
Часто задаваемые вопросы
Заменяют ли частичное клонирование и разреженное извлечение Git LFS?
Нет. Частичное клонирование ограничивает обычные объекты Git, разреженное извлечение управляет путями рабочей копии, а Git LFS загружает содержимое крупных файлов.
Почему после извлечения остались только указатели Git LFS?
Вероятно, автоматическая загрузка была отключена, а git lfs pull не запускался. Проверьте коммит, шаблоны включения, учётные данные и настройки LFS.
Безопасно ли запускать git lfs prune после каждого задания CI?
Только если репозиторий и каталог объектов LFS принадлежат одному заданию. При общем хранилище лучше удалять отдельную рабочую копию целиком.
Облачный Mac на выделенном физическом узле
Запускайте сборки на выделенном физическом узле
Выбирайте MiniRents M4 или MiniRents M4 Pro с оплатой за день, неделю, месяц или квартал, а узел — с учётом команды и расположения репозитория. Устройства работают на выделенных физических машинах, а не в виртуальной среде.