Рост на пять репозиториев: gitlink'и и дублирующийся CI
· 4 min read
Обычно инфраструктурные истории начинаются со слов: “Наш проект стал слишком большим, и мы решили разделить его на части”. Но у нас всё было иначе.
С самой первой минуты проекта мы осознанно выбрали архитектуру россыпи независимых репозиториев. Проекты Pipeline, engine, design-docs и user-docs были созданы практически одновременно, а конфигурационный файл .gitmodules лежал в самом первом коммите головного репозитория.
Ради чистоты кода, изоляции CI, независимой истории и разделения прав доступа мы с первого дня построили систему на базе Git Submodules. Идея выглядела безупречно на бумаге. А вот за что нам пришлось заплатить на практике.

Ловушка Gitlink: Призрак локального коммита
Первой платой стала неочевидная механика синхронизации состояний. Когда вы используете сабмодули, рабочий процесс требует строгой дисциплины. Если вы правите код внутри engine/ (физически находясь в папке головного репозитория Pipeline), вам нужно сделать два пуша. Сначала git push из папки самого сабмодуля, а затем, поднявшись в корень Pipeline — закоммитить обновленный указатель (gitlink) на новый коммит engine и запушить сам Pipeline.
Забыть первый пуш невероятно легко. На вашем локальном диске коммит физически существует. Всё собирается, тесты зеленые, Pipeline счастливо отправляется в облако. Но всплывает это только на свежем чекауте в CI. Команда git submodule update --init падает с текстом:
fatal: remote error: upload-pack: not our ref <sha>
``` ext
GitHub честно отвечает: "Такого коммита в удаленном репозитории не существует".
В один из дней в эту ловушку угодили сразу три сабмодуля одновременно (`design-docs`, `engine`, `user-docs`). Причина была идентична — мелкие коммиты были сделаны прямо внутри папок сабмодулей, gitlink в `Pipeline` был синхронизирован, но локальные коммиты так и не покинули ноутбук разработчика.
Был и колоритный нюанс: четвертый сабмодуль, `ude_promotion` (хранящий сайт), сломался настолько сильно, что обычным пушем его было не починить — его локальная ветка так далеко отстала от `upstream`, что перед пушем потребовался интерактивный `rebase`.
## Дублирующийся CI: Один код в двух личностях

Вторая расплата пришла от CI. Репозиторий `engine` (наше ядро) имел две личности. С одной стороны, это был самостоятельный репозиторий со своим собственным процессом сборки, который чекаутил только этот репозиторий. С другой — это был сабмодуль внутри `Pipeline`, который тестировался в контексте всего головного проекта.
Эти две личности неизбежно вступили в конфликт, и произошло это в два разных этапа.
**Первый удар: Отсутствующие зависимости**
Самый простой случай. Тест `test_integration_scripts.py` пытался дотянуться до модулей `verify_pages/check_links`, которые существовали только в родительском репозитории `Pipeline`. При одиночном (standalone) чекауте `engine` этих файлов просто не было на диске. Мы починили это грубо, но эффективно — добавили одну строку в конфигурацию `ci.yml`, жестко исключив этот тест из standalone-прогона (`pytest --ignore=...`). Но это был разовый патч, а не системное решение.
**Фатальный подъем по директориям**
Вскоре баг вернулся в новом обличье. Один за другим начали падать тесты, которые использовали классический трюк для поиска родительских артефактов:
```python
Path(__file__).resolve().parents[2]
``` ext
Во вложенном прогоне этот код поднимался ровно до корня родительского репозитория. Но в standalone-сборке `engine` он выпрыгивал на уровень **выше** корня репозитория, прямиком в системную файловую систему CI-раннера, и падал с жестким `AssertionError`.
Именно после второго повторения бага мы поняли, что точечные патчи больше не работают. Нам нужна была общая конвенция.
Мы ввели изящный маркерный хак: тесты стали проверять наличие нашей системной маркерной директории (назовем её `.workspace_config`) на поднятом уровне. Если директория есть — значит, мы во вложенном контексте, и тесты выполняются. Если её нет (standalone) — мы вежливо используем `pytest.mark.skipif`, и тесты тихо пропускаются без падений с красным статусом.
## Итог

Архитектура Multi-repo (несколько репозиториев) не дает вам магическую "слабую связанность" бесплатно с первого дня.
Она требует безупречной дисциплины синхронизации пушей и заставляет вас проектировать код так, чтобы он был готов работать в нескольких разных контекстах запуска одновременно. Эти уроки дались нам ценой сломанных пайплайнов, но без них система не выжила бы при дальнейшем росте.
Но проблемы с Git и двойным CI оказались лишь прелюдией. Настоящая мистика началась, когда в дело вмешалась сеть, и наш пайплайн стал падать из-за «призрака» в проводах. О том, как мы расследовали неуловимый сетевой баг — читайте в следующем эпизоде.