Почему настройка монорепозитория ломается при росте команды
Первый год единый репозиторий выглядит выигрышем: одна ветка, общий набор проверок, пакеты переиспользуют код друг друга без публикации в реестр. К моменту, когда над ним работает несколько команд, выпуск растягивается с минут до часов, а правка в одном модуле пересобирает половину репозитория.
Дело не в инструменте сборки — смена сборщика картину не меняет. Кэш инвалидируется целиком, потому что нигде не зафиксировано, какие пакеты от чего зависят: граф сборки разрастается за счёт неявных связей через общие модули утилит и типов. Воспроизводимость сборки уходит — локально задача проходит, в общей среде падает, и причину ищут вручную по логам.
Проверки на каждый коммит идут часами, потому что пересчитываются все цели, а не затронутые. Запускать их частями никто не рискует: без схемы зависимостей непонятно, что действительно можно не собирать. Время выпуска перестаёт зависеть от объёма правки и начинает зависеть от размера репозитория.
Сверху добавляются ручные шаги: порядок выката сервисов держится в голове у одного инженера, версии общих пакетов правятся руками, а расхождение окружений ловится уже на бою. Разбираем, как это закрывается схемой зависимостей, явными границами пакетов и точной инвалидацией кэша.
Что даёт выстроенная сборка: измеримые эффекты
Настройка монорепозитория измеряется не числом пакетов, а временем, которое команда тратит на ожидание и разбор поломок. Эффекты ниже видно в метриках уже в первые недели работы.
Время сборки и проверок
Граф зависимостей строится по исходникам, поэтому пересобираются только затронутые проекты, остальное отдаётся из кэша. Доля попаданий в кэш на типовом изменении выходит в пределах 70–90%, и полный прогон перестаёт быть обязательным ритуалом перед коммитом.
Проверки идут по тому же графу: линтеры, типы и тесты запускаются только для изменённых пакетов и их потребителей. Время проверок падает с десятков минут до минут, а очередь на слияние перестаёт быть узким местом.
Стоимость простоя и ушедшие классы отказов
Кэш привязан к содержимому и окружению: меняется конфигурация или версия инструментов — он инвалидируется сам, а не отдаёт устаревший артефакт. Уходит класс отказов «собралось локально, упало на общей ветке».
Второй ушедший класс — рассинхрон общих библиотек: единый реестр зависимостей на репозиторий не даёт сервисам разойтись по несовместимым мажорам. Простой здесь считают в часах заблокированного конвейера и в числе людей, которые ждут разбора.
Отсюда пятый эффект: выпуск становится предсказуемым событием с понятным составом изменений, и частота выпусков растёт без роста ручной работы. Опора не на нашу оценку: Google держит основной код в одном репозитории и применяет общий граф сборки на масштабе тысяч инженеров — та же логика работает и вниз, до команды из десяти человек.
Варианты организации сборки: сравнение подходов
Оркестратор выбирают по графу зависимостей: сколько пакетов, на скольких языках, как часто меняются общие библиотеки. Ниже — четыре рабочих режима, включая вариант без оркестратора.
| Вариант | Когда подходит | Ограничения |
|---|---|---|
| Оркестратор семейства Turborepo | Один язык, десятки пакетов, нужны локальный кэш и простая конфигурация без обучения команды | Правила видимости пакетов слабее, нестандартные сценарии сборки ложатся плохо |
| Nx | Несколько приложений и общих библиотек, нужны строгий граф зависимостей и запрет сквозных импортов | Выше порог входа: конфигурацию нужно поддерживать и ревьюить |
| Bazel | Много языков в одном репозитории, удалённый кэш и распределённая сборка на сотни пакетов | Настройка измеряется неделями, нужен выделенный инженер |
| Без оркестратора | Один-два пакета, редкие релизы, короткий цикл сборки | Локального кэша нет: каждый прогон пересобирает всё, время растёт линейно |
Как выбрать режим под размер команды
Небольшой команде обычно достаточно оркестратора семейства Turborepo: локальный кэш закрывает повторные сборки, а конфигурация читается целиком за один вечер. При десятках пакетов и нескольких приложениях выигрывает Nx — правила видимости не дают приложению утащить внутренности чужой библиотеки, и граф зависимостей остаётся предсказуемым. Bazel оправдан, когда пакеты написаны на разных языках и одиночный прогон тянется десятки минут: удалённый кэш и распределённая сборка возвращают минуты, но требуют инженера, который живёт этой сборкой.
Переход между режимами не обязан быть одноразовым: начинаем с графа и локального кэша, а удалённый кэш подключаем тогда, когда прогон перестаёт укладываться в комфортное время.
Настройка монорепозитория: этапы инженерной работы
Работу ведём этапами: сначала измеряем, потом меняем структуру, и только затем трогаем сборку и проверки. Переезд в один репозиторий без замеров превращается в лотерею с релизами, поэтому каждое решение у нас опирается на цифры до и после.
Аудит и схема зависимостей
- Аудит сборки и графа зависимостей. Замеряем полную и инкрементальную сборку, смотрим, какие пакеты тянут за собой весь репозиторий, ищем циклы и неявные связи между модулями.
- Границы пакетов. Размечаем, что становится самостоятельным пакетом, что — общей библиотекой, и фиксируем правила импорта: приложение не лезет во внутренности чужого модуля.
- План внедрения. Переводим репозиторий частями — сначала одно приложение с его зависимостями, затем остальные, без остановки релизов.
Настройка сборки и проверка
- Оркестратор и кэш. Настраиваем Turborepo, Nx или Bazel под получившуюся структуру: кэш по содержимому входов и общий кэш для всех машин, чтобы неизменное не пересобиралось.
- Разделение задач по затронутым проектам. Конвейер проверок гоняет тесты и сборку только для изменённых пакетов и тех, кто от них зависит, — время проверки перестаёт расти вместе с репозиторием.
- Проверка на реальных сценариях. Прогоняем типовые изменения: правку общей библиотеки, релиз, откат, параллельные ветки. Отдельно убеждаемся, что кэш не отдаёт устаревший результат.
- Приёмка результата и передача. Сравниваем замеры до и после, отдаём схему зависимостей, правила границ, документацию по сборке и проводим обучение команды.
Что передаём на руки: конфигурации, схемы, регламенты
Передача репозитория — это состояние, которое другая команда воспроизводит без автора настройки. Каждый артефакт ниже описывает не «как получилось», а почему сделано именно так и что сломается при изменении.
Конфигурации и схемы
- Конфигурация сборки и кэша: явные входы и выходы задач, области влияния, правила инвалидации — повторный прогон не пересобирает неизменённые пакеты.
- Схема зависимостей: граф пакетов, границы модулей, запрет циклических связей и импорта в обход публичного интерфейса.
- Политики доступа к кэшу: права чтения и записи, область видимости артефактов, роли для релизной ветки и внешних подрядчиков.
- Описание локального окружения: закреплённые версии платформы и менеджера пакетов, подъём репозитория одной командой, набор проверок перед коммитом.
Регламенты и документация
- Регламент добавления проекта: критерии выделения нового пакета, шаблон заготовки, обязательные поля конфигурации, порядок ревью.
- Обновление зависимостей и инструментов: порядок проверки совместимости, критерий отката, периодичность миграций.
- Восстановление сборки: точки входа, типовые сбои кэша, холодная сборка с нуля, проверка целостности артефактов.
- Онбординг разработчика: маршрут первого дня, разбор частых ошибок, владельцы сервисов и порядок эскалации.
Все артефакты лежат в репозитории рядом с кодом и обновляются в том же ревью, что и изменения сборки — иначе инструкция расходится с реальностью за пару недель.
Кейс: как ушёл класс отказов при сборке и выпуске
К нам пришла команда из шести продуктовых подразделений — больше сорока инженеров в одном монорепозитории на несколько приложений и общих библиотек. Сборка запускалась целиком на каждое изменение: правка в пакете с общими типами вытягивала пересборку всех продуктов, полный прогон занимал около сорока минут, а ожидание свободного сборочного агента добавляло ещё до получаса. Разработчики перестали запускать проверки локально, а выпуски шли «на удачу».
Ситуация до
Кэш сборки формально был включён, но ключ считался по всему дереву сразу, поэтому попаданий почти не случалось: доля повторных сборок держалась на 12 %, то есть почти каждый прогон считался с нуля. Отказы приходили на выпуске, а не на проверке: примерно треть релизов откатывалась из-за падения сборки или несовместимых версий общих пакетов. Корень был не в инструменте, а в схеме — у пакетов не было явных границ, зависимости шли сквозь приложения.
Что изменилось после
Мы развели монорепозиторий на слои — общие библиотеки, доменные пакеты, приложения — и зафиксировали правила зависимостей: приложение может зависеть от домена, домен от библиотеки, обратное запрещено и проверяется автоматически. Ключ кэша привязали к содержимому пакета и его зависимостей, поэтому неизменённые ветки графа переиспользуют готовый результат, а собирается только затронутая часть. Конвейер выпуска собрали на Nx с общим удалённым кэшем для всех команд.
Через квартал замер повторили на втором продукте и сверились с публичными замерами по крупным монорепозиториям — порядок цифр сошёлся, то есть результат воспроизводится, а не был случайностью одной команды. Время полной сборки сократилось с 40 до 7 минут, доля повторных сборок выросла с 12 до 78 %, отказы на выпуске — с 30 до 3 % и остаются на этом уровне.
Технические сомнения: короткие ответы инженерам
Монорепозиторий — это не «сложить папки в один репозиторий», а смена модели работы: единый граф зависимостей, общие правила сборки, общий кэш результатов. Ниже — пять вопросов, которые чаще всего задают техлиды перед стартом миграции репозиториев, и короткие ответы по существу.
Как переносится история из раздельных репозиториев?
История сохраняется: переносим не только файлы, но и коммиты с авторами, ветками и тегами релизов — археология и blame остаются рабочими. Репозитории подключаются по одному, старые до конца миграции живут в режиме только чтения, поэтому параллельная работа команд не останавливается.
Не вырастет ли время сборки после объединения?
Растёт только при неверно объявленных зависимостях между пакетами. Когда граф описан честно, пересобирается и проверяется лишь затронутая часть, а остальное поднимается из кэша по содержимому входов. Если правило получается слишком широким, сужаем области влияния пакетов, а не отказываемся от объединения.
Как переводить команды без остановки релизов?
Поэтапный переход: сначала одна команда-пионер переносит свой сервис и обкатывает правила, затем подключаются остальные. Новый код идёт уже в монорепозиторий, а старые ветки доживают свой цикл — переключение не ломает график релизов.
Как кэширование влияет на повторные проверки?
Повторные проверки переиспользуют результат, если входы задачи не изменились, — один и тот же набор тестов не гоняется дважды на разных ветках и агентах. Удалённый кэш общий для сборочных машин и локальных окружений, поэтому разработчик видит результат, уже посчитанный на сервере.
Кто отвечает за правила сборки после передачи поддержки?
Правила живут в репозитории как код, а не в чьей-то голове: изменение проходит через ревью, владелец каждого участка зафиксирован. При передаче поддержки отдаём конфигурации, схему графа зависимостей, документацию по типовым правкам и проводим обучение команды.
Обсудим вашу задачу сборки и выпуска
Чтобы вернуться с оценкой объёма работ, а не с общими словами, инженерам нужен срез вашей текущей сборки. По нему видно, где теряется время: на установке зависимостей, на полном пересборе пакетов при правке одного модуля или на ручном согласовании релизов между командами. Соберём этот срез за один разговор — от вас входные данные и целевые показатели, от нас план внедрения с этапами и точками отката.
- Текущая схема репозиториев: один общий, набор отдельных или смешанная; как связаны пакеты и общие библиотеки.
- Узкие места сборки: время полной и инкрементальной сборки, доля кэшируемых задач, что пересобирается без изменений.
- Состав команд и зоны ответственности: кто владеет пакетами, как согласуют правки в общих модулях.
- Ограничения по срокам выпуска: как часто релизите, есть ли окна, когда сборку нельзя останавливать.
- Целевые показатели: время сборки и выпуска, доля покрытых проверками путей, класс отказов, который нужно исключить.
- Инфраструктура сборки: где работают агенты — свои машины, облако или смешанно, есть ли ограничения по доступам.
- Текущий инструмент (Turborepo, Nx или Bazel) и что в нём перестало устраивать.
Опишите задачу в том виде, в каком она есть сейчас: фрагменты конфигураций, логи сборки или просто описание процесса. Мы вернёмся с оценкой объёма работ, планом внедрения и вариантами перехода без остановки выпуска — обсудим задачу и договоримся о следующем шаге.
Чем единый репозиторий отличается от раздельных проектов?
Выбор модели репозиториев — это не про удобство разработчиков, а про то, как по системе распространяются сквозные изменения. Когда общий модуль лежит в отдельном репозитории, правка в нём превращается в цепочку согласованных релизов: обновить пакет, дождаться сборок у всех потребителей, синхронизировать версии. В едином дереве та же правка проверяется одной сборкой по графу зависимостей, и несовместимость всплывает до попадания в основную ветку.
| Вариант | Когда подходит | Ограничения |
|---|---|---|
| Единый репозиторий | Несколько продуктов делят библиотеки и контракты, сквозные изменения идут постоянно | Растёт время сборки и клонирования; нужны кэш задач и разграничение прав по каталогам |
| Раздельные проекты | Команды почти не пересекаются по коду и живут в своих циклах релизов | Каждое сквозное изменение — серия релизов; версии общих модулей постепенно расходятся |
| Гибрид по доменам | Есть ядро с общими модулями и независимые продукты вокруг | Границы доменов приходится удерживать вручную, иначе получается тот же монолит |
Изоляция команд решается не числом репозиториев, а правами доступа и границами пакетов: в едином дереве доступ выдают на каталог, а правки в чужой области отсекают проверками на слиянии. Инструменты вроде Turborepo, Nx или Bazel добавляют к дереву кэш задач и граф зависимостей — без них сборка всего репозитория на каждое изменение становится узким местом.
Цена ошибки в общих правилах сборки выше всего: неудачный шаг в едином дереве останавливает сразу все команды, поэтому правила держат рядом с кодом и меняют отдельным проверяемым изменением. Мы начинаем с карты зависимостей и частоты сквозных правок — она и определяет, где проходит граница между общим деревом и отдельными проектами.
- Складывают в один реестр всё подряд — приложения, библиотеки, служебные скрипты, черновики — и не задают границы владения. Любая правка запускает пересборку целого набора, и время сборки растёт линейно вместе с командой.
- Настраивают кэш «чтобы был», но не следят за достоверностью его ключей: задача помечается выполненной, хотя входные данные изменились. Ложное попадание возвращает старый результат — со стороны это выглядит как загадочный откат.
- Описывают связи между пакетами на глаз. Появляются круговые зависимости и обращения к внутренним частям чужого пакета: локально всё сходится, а на общей машине порядок сборки рассыпается.
- Держат общий код внутри приложения, а потом копируют его между пакетами. Правка в одном месте не доезжает до второго, и поведение продуктов расходится.
- Не разводят окружения: на машине инженера одни настройки, на сборочном сервере другие. Класс отказов «у меня работает» не исчезает, а маскируется до выпуска.
- Переносят всё одним заходом и останавливают выпуск продукта на время переезда. Если что-то идёт не так, откатываться некуда — прежний контур уже тронут.
- Не закрепляют регламенты: порядок добавления пакетов, зоны ответственности, требования к именованию живут в голове одного инженера. После его ухода сборка снова разъезжается.
- Не ограничивают срок жизни собранных результатов и рабочих веток — реестр разрастается, а нужный результат теряется среди устаревших.







