Объединяем 22 сабмодуля в одну монорепу — уроки миграции

← All posts

Восемь месяцев KB Labs состоял из 22 git-репозиториев, склеенных сабмодулями. У каждого свой lockfile, свой CI, своя история версий. Кросс-репо зависимости жили на link:-путях, управляемых кастомным инструментом (DevLink), которому для переключения между dev и npm режимами требовался 11-шаговый пайплайн.

Мы объединили всё в одну монорепу. Вот почему, как — и что нас удивило.

Почему сабмодули казались правильным решением (и почему нет)

Логика была здравой: отдельные репозитории для отдельных задач. Плагин-система независима от CLI-фреймворка, который независим от адаптеров. Чистые границы.

На практике эти границы создали 204 кросс-репо link:-зависимости, 22 lockfile с независимым дрейфом версий и инструмент DevLink, которому приходилось:

  1. Обнаруживать все репозитории через .gitmodules
  2. Конвертировать кросс-репо workspace:* в link:-пути
  3. Генерировать pnpm-workspace.yaml для каждого репо
  4. Чистить устаревшие node_modules (захардкоженные shim-пути из старых раскладок)
  5. Запускать root install + install по каждому репо
  6. Валидировать диагностику (битые ссылки, устаревшие lockfile, кросс-репо несоответствия)

Это инфраструктура для обходного пути вокруг мультирепо — а не для разработки продукта. Для небольшой команды каждый час на DevLink — это час не на фичи.

Решение: чистый старт, не слияние истории

Мы рассматривали git filter-repo для объединения историй. Математика не сошлась: 22 репо × переписывание истории × переотображение путей × разрешение конфликтов = недели работы ради косметического преимущества. История git живёт в архивных репозиториях на GitHub. Новое репо начинается чисто.

Миграция шла поэтапно: создать новое репо → скопировать код → заменить все link: на workspace:* → проверить сборки → архивировать старые репо.

Новая структура

kb-labs/
├── core/          # основа (типы, рантайм, конфиг, дискавери, реестр)
├── sdk/           # публичный API для авторов плагинов
├── cli/           # CLI-фреймворк
├── shared/        # утилиты (cli-ui, http, testing, git)
├── plugins/       # ВСЯ опциональная функциональность
├── adapters/      # реализации интерфейсов (llm-openai, redis, sqlite, ...)
├── tools/         # Go-бинари (kb-devkit, kb-dev, kb-create)
├── studio/        # веб-интерфейс
├── sites/         # сайты
└── docs/          # ADR, архитектурные решения

Группировка — по архитектурной роли, не по происхождению. Зависимости текут строго вниз: core → sdk → plugins; core → adapters. «Правило утиной типизации» для плагинов: если использует SDK, регистрирует команды и имеет манифест — это плагин.

Что умерло

Что нас удивило

node_modules содержит захардкоженные пути

Поднятые пакеты в node_modules иногда содержат сгенерированные shim-файлы с захардкоженными абсолютными путями из старой плоской раскладки. Простой pnpm install их не исправляет — нужно полностью удалить node_modules и переустановить. Мы словили это как тихие ошибки разрешения импортов, которые проявлялись только в рантайме.

Сервисы нужно запускать через node ./path, не через pnpm --filter

pnpm --filter @kb-labs/rest-api start устанавливает cwd в директорию пакета, но не подхватывает dotenv из корня репо. Сервисы, загружающие конфиг через loadEnvFromRoot(repoRoot), нужно запускать с явным путём, чтобы корневое разрешение работало корректно.

Сборка стала быстрее

22 репо означало 22 шага install, 22 шага build, 22 шага type-check. Одно репо — один install (общий node_modules), одна топологически упорядоченная сборка (kb-devkit управляет DAG) и один проход type-check. Миграция сократила время CI примерно на 40%.

Оно того стоило?

Однозначно да. Миграция заняла около недели. Она устранила целый слой тулинга (DevLink, синхронизацию сабмодулей, управление несколькими lockfile) и упростила каждую ежедневную операцию — install, build, test, publish. В монорепо — одна команда для каждого действия, а не 22.

Если вы небольшая команда с несколькими репозиториями и тратите больше времени на межрепо-сантехнику, чем на фичи — объединяйтесь. Боль фронт-лоадна и конечна. Упрощение — постоянно.