Рост на пять репозиториев: gitlink'и и дублирующийся CI

 · 4 min read

Обычно инфраструктурные истории начинаются со слов: “Наш проект стал слишком большим, и мы решили разделить его на части”. Но у нас всё было иначе.

С самой первой минуты проекта мы осознанно выбрали архитектуру россыпи независимых репозиториев. Проекты Pipeline, engine, design-docs и user-docs были созданы практически одновременно, а конфигурационный файл .gitmodules лежал в самом первом коммите головного репозитория.

Ради чистоты кода, изоляции CI, независимой истории и разделения прав доступа мы с первого дня построили систему на базе Git Submodules. Идея выглядела безупречно на бумаге. А вот за что нам пришлось заплатить на практике.

Git Submodule Trap

Первой платой стала неочевидная механика синхронизации состояний. Когда вы используете сабмодули, рабочий процесс требует строгой дисциплины. Если вы правите код внутри 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: Один код в двух личностях


![Multi Repo Gitlinks Dual Ci Part 2](/images/multi_repo_gitlinks_dual_ci_part2.jpg)

Вторая расплата пришла от 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 Gitlinks Dual Ci Part 3](/images/multi_repo_gitlinks_dual_ci_part3.jpg)

Архитектура Multi-repo (несколько репозиториев) не дает вам магическую "слабую связанность" бесплатно с первого дня.

Она требует безупречной дисциплины синхронизации пушей и заставляет вас проектировать код так, чтобы он был готов работать в нескольких разных контекстах запуска одновременно. Эти уроки дались нам ценой сломанных пайплайнов, но без них система не выжила бы при дальнейшем росте.

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