Воспроизводимое получение Git Submodule в Cloud Mac CI

Воспроизводимое получение Git Submodule в Cloud Mac CI

Проект iOS может без проблем клонироваться на машине разработчика, но после переноса в Cloud Mac CI процесс способен остановиться на подмодулях: основной репозиторий уже получен, приватный компонент сообщает об ошибке доступа, вложенная зависимость остаётся на старом коммите или shallow clone не находит указанный объект. Обычно причина не в Xcode, а в том, что при получении исходного кода не проводится чёткое различие между «текущей веткой основного репозитория» и «коммитом подмодуля, зафиксированным в основном репозитории».

Сначала определите границы воспроизводимости

Git Submodule хранит в основном репозитории путь, удалённый адрес и точный коммит. Цель воспроизводимой сборки — всегда получать один и тот же объект подмодуля для одного и того же коммита основного репозитория, а не при каждом запуске отслеживать последнее состояние ветки подмодуля.

Сначала зафиксируйте базовое состояние в среде разработки:

git submodule status --recursive
git config --file .gitmodules --get-regexp 'submodule\..*\.path'
git config --file .gitmodules --get-regexp 'submodule\..*\.url'

Символ - в начале строки состояния означает, что подмодуль ещё не инициализирован, + — что коммит в рабочем дереве не совпадает с записанным в основном репозитории, а U — что существует конфликт слияния. Перед запуском CI ни одно из этих состояний не должно считаться нормальным результатом.

Не запускайте git submodule update --remote в задаче сборки. Эта команда следует за изменениями настроенной ветки, из-за чего один и тот же коммит основного репозитория в разное время может получать разные зависимости.

Относительные адреса удобны при переносе репозиториев в пределах одной системы размещения кода, но разрешаются на основе удалённого адреса основного репозитория. Если задача временно переписывает origin, перед инициализацией выполните git submodule sync --recursive и выведите фактически разрешённые адреса в обезличенном виде для проверки.

Создайте единую точку входа для получения исходного кода

Не смешивайте clone --recurse-submodules, ручное получение изменений из каталогов и аварийные команды в сценарии сборки. Рекомендуется сначала получать основной репозиторий, а затем обрабатывать все уровни подмодулей через единую точку входа:

set -euo pipefail

git submodule sync --recursive
git submodule update --init --recursive --jobs 4

expected_file="$(mktemp)"
actual_file="$(mktemp)"
trap 'rm -f "$expected_file" "$actual_file"' EXIT

git ls-tree -r HEAD |
  awk '$2 == "commit" { print $3, $4 }' |
  sort > "$expected_file"

git submodule status --recursive |
  sed -E 's/^[ +\-U]([0-9a-f]+) ([^ ]+).*/\1 \2/' |
  sort > "$actual_file"

diff -u "$expected_file" "$actual_file"

Параметр --jobs 4 здесь лишь ограничивает число параллельных сетевых запросов и не является универсальной рекомендацией по производительности. Сеть узла, ограничения удалённого сервера и количество подмодулей могут различаться, поэтому начните с небольшого уровня параллелизма и корректируйте его с учётом частоты сбоев.

Перед сборкой сценарий также должен выполнять git diff --submodule=log --exit-code. Если какой-либо этап установки незаметно изменил рабочее дерево подмодуля, задача должна немедленно завершиться с ошибкой, а не продолжать создание архива из неизвестной версии исходного кода.

Осторожно используйте shallow clone

Shallow clone уменьшает объём передаваемых данных, но часто становится причиной ситуации, когда «коммит точно есть на удалённом сервере, однако CI его не находит». Объект подмодуля, записанный в основном репозитории, может находиться за текущей границей глубины или быть доступным только через историю уже удалённой ветки.

Сценарий Рекомендуемая стратегия Действия при сбое
Коммит подмодуля находится рядом с вершиной ветки Получение с ограниченной глубиной Увеличить глубину и повторить попытку
Диапазон истории непостоянен Полное получение подмодуля Не пытаться угадать фиксированную глубину
Несколько уровней вложенных подмодулей Проверять коммиты на каждом уровне Вывести путь сбоя и ID объекта
Удалённый объект больше недоступен Исправить ссылки репозитория Не маскировать проблему переключением ветки

Если shallow clone необходимо сохранить, сначала попробуйте:

git submodule update --init --recursive --depth 50

После сбоя не переходите сразу на --remote. Откройте каталог, в котором возникла ошибка, выполните git fetch --deepen=100, а затем проверьте объект командой git cat-file -e <commit>^{commit}. Если он по-прежнему отсутствует, необходимо проверить сохранность объекта на удалённом сервере или запись в основном репозитории, а не добавлять новые случайные повторы.

Изолируйте учётные данные приватных подмодулей

Подмодули могут находиться в разных контурах доступа. Наиболее надёжный подход — создавать для каждой задачи временную конфигурацию Git, передавать её только на время жизни процесса и не изменять глобальную конфигурацию узла.

git_config="$(mktemp)"
chmod 600 "$git_config"
trap 'rm -f "$git_config"' EXIT

git config --file "$git_config" credential.helper ""
git config --file "$git_config" protocol.version 2

GIT_CONFIG_GLOBAL="$git_config" \
git submodule update --init --recursive

Данные аутентификации должны поступать из защищённых переменных CI или временных файлов. Их нельзя записывать в .gitmodules, удалённые адреса, аргументы команд или журналы сборки. После завершения задачи необходимо как минимум проверить:

git config --global --list
git remote -v
git submodule foreach --recursive 'git remote -v'

Перед анализом вывод следует обезличить. Если учётные данные когда-либо добавлялись непосредственно в URL, одного удаления временного файла недостаточно: также восстановите удалённые адреса и проверьте историю Shell, диагностические пакеты и каталоги кэша.

Превратите сведения о сбое в пригодные для диагностики данные

При сбое подмодуля недостаточно сохранить только сообщение «код выхода 128». Рекомендуется фиксировать коммит основного репозитория, путь сбоя, ожидаемый и фактический объекты, разрешённое имя хоста, глубину получения и этап, на котором произошла ошибка. При этом нельзя записывать имя пользователя, токен или полный адрес с данными аутентификации.

Порядок диагностики можно стандартизировать следующим образом:

  1. С помощью git ls-tree HEAD <path> проверьте объект, ожидаемый основным репозиторием.
  2. По .gitmodules и локальной конфигурации Git определите итоговый адрес.
  3. С помощью git cat-file -e проверьте, присутствует ли объект локально.
  4. Проверьте доступ к удалённому серверу, используя учётные данные только для чтения.
  5. Проверьте границы shallow clone и состояние вложенных подмодулей.
  6. Очистите рабочее дерево и снова выполните тот же сценарий получения исходного кода.

В завершение сохраните вывод git submodule status --recursive в метаданных сборки. Он занимает совсем немного места, но позволяет точно определить, какие коммиты зависимостей использовались в конкретной сборке. Задачи на узлах SDKMac должны следовать тому же принципу: сначала фиксировать входное состояние исходного кода и только затем запускать Xcode или другую цепочку инструментов, сохраняя чёткую границу между ошибками получения исходного кода и ошибками компиляции.

Часто задаваемые вопросы

Почему закреплённый коммит Submodule всё равно не удаётся получить?

Родительский репозиторий хранит только идентификатор объекта. Нужно отдельно проверить URL, права, глубину клона и наличие этого объекта в удалённом репозитории.

Нужно ли запускать git submodule update --remote в CI?

Для воспроизводимой сборки не нужно. Команда следует за настроенной веткой, поэтому один коммит родительского репозитория со временем может получить разные зависимости.

Как удалить данные доступа к Submodule после задания?

Передавайте их через временную конфигурацию конкретного задания, удаляйте обработчиком trap и проверяйте глобальные настройки Git, адреса remote и журналы на остатки секретов.

Эксклюзивный физический узел

Выберите облачный Mac для непрерывной сборки

Сравните конфигурации, регионы и четыре варианта расчётного периода SDKMac M4 и SDKMac M4 Pro, затем оформите заказ.

Выбрать тариф