CI монорепозитория для более быстрой ежедневной доставки в больших репозиториях
CI монорепозитория ускоряет большие репозитории, тестируя только затронутые сервисы через фильтры по путям, определение затронутых проектов и чёткие правила релиза.

Почему тестирование всего репозитория замедляет ежедневные релизы
Большой репозиторий быстро становится медленным, когда каждый коммит пробуждает все задачи. Маленькое изменение в общем конфиге, на веб‑странице или в одном бэкенд‑сервисе может запустить тесты, сборки и проверки для частей кода, которые в этот коммит не тронуты.
Это звучит безопасно, но обычно просто тратит время. Если в репозитории живут 40 сервисов и разработчик изменил только один из них, запуск всех 40 наборов тестов не даёт лучшей обратной связи. Он даёт более медленную обратную связь.
Боль сначала проявляется в очереди. Несколько небольших пул‑реквестов приходят утром, и каждый запускает полный пайплайн. Раннеры загружаются. Поздние коммиты ждут за работой, не имеющей отношения к ним. К вечеру пятиминутная проверка может превратиться в 35‑минутное ожидание.
Команды чувствуют эту задержку в повседневной работе. Люди реже мёрджат, потому что каждое изменение тянет за собой ожидание. Они пачками собирают изменения, чтобы избежать повторных задержек — это делает пул‑реквесты больше и сложнее для ревью. Когда тест наконец падает, автор уже переключился на другую задачу и потерял контекст.
Полный пайплайн также создаёт шум. Если падают несвязанные задачи, разработчикам приходится просматривать страницы вывода, чтобы понять, повлиял ли их коммит на что‑то. Доверие к CI в монорепозитории начинает таять. Как только люди ожидают медленных, шумных результатов, они перестают воспринимать CI как быстрый предохранитель и начинают относиться к нему как к бумажной рутине.
Паттерн легко заметить. Кто‑то обновляет тексты в одном customer‑facing приложении, но пайплайн перестраивает мобильные пакеты, запускает интеграционные тесты для внутренних инструментов и проверяет шаги деплоя для сервисов, не затронутых правкой. С этими задачами нет ничего не так — просто они не относятся к этому коммиту.
Быстрая обратная связь меняет поведение. Когда проверки завершаются быстро, разработчики отправляют меньше, делают ревью чаще и фиксируют проблемы, пока код ещё свеж в голове. Вот почему селективное тестирование важно. Оно экономит время раннеров, но главное — сохраняет скорость команды.
Что делают фильтры по путям
Фильтры по путям смотрят на файлы в коммите и решают, какие CI‑задачи нужно запустить. Они не пытаются угадать, безопасен ли код. Они отвечают на более простой вопрос: "Какие части репозитория затронуло это изменение?"
Это простая проверка сокращает много лишней работы в CI для монорепо. Если кто‑то меняет services/api/, пайплайн может запустить линтинг, тесты и шаги сборки для этого сервиса. Если изменение касается только apps/web/, мобильные задачи могут оставаться бездействующими вместо того, чтобы тратить дополнительные 15 минут.
Чистая конфигурация сопоставляет папки и файлы с задачами, которые от них зависят. Команды обычно начинают с очевидных путей: API, web, mobile и worker. Потом добавляют правила для областей, которые часто сбивают пайплайны с толку — документация, конфиги CI, инфраструктурный код и общие пакеты.
Общий код требует дополнительного внимания. Если меняется packages/ui/, и web и mobile оба его используют, тесты нужны в обоих местах. Если меняется packages/auth/, это может затронуть API, фоновые джобы и end‑to‑end тесты. В этот момент фильтры по путям перестают быть просто «папка равно задача» и становятся «зависимость равно задача».
Документация и конфиги тоже заслуживают своих правил. Изменение в docs/ чаще всего не требует продуктовых тестов. Изменение в CI‑конфиге, Docker‑файлах или скриптах деплоя может потребовать небольшой набора верификационных задач, даже если код приложений не тронут.
На практике правила обычно просты. Изменения в коде API запускают тесты и сборки API. Изменения только в web запускают web‑проверки и пропускают мобильные. Изменения только в документации могут запускать орфографию или проверки Markdown. Изменения в общих пакетах разносятся на все приложения, которые эти пакеты импортируют.
Фильтры по путям — не то, что ставят один раз и забывают. Репозитории меняются, папки перемещают, команды разделяют сервисы или объединяют пакеты. Если никто не проверяет правила после реструктуризации, пайплайн начинает пропускать нужные задачи или запускать слишком много лишних. Оба сценария вредят ежедневной доставке.
Хорошие фильтры по путям делают CI скучным в лучшем смысле. Разработчик пушит небольшое изменение, репозиторий запускает соответствующие задачи, и main остаётся достаточно быстрым для ежедневных релизов.
Как работает определение затронутых проектов
Определение затронутых проектов начинается с простого вопроса: какие файлы изменились в этом коммите или пул‑реквесте? CI сначала читает этот список файлов. Если изменение касается только services/billing, нет смысла запускать тесты для мобильного приложения, сайта документации и внутренней панели администрирования.
Следующий шаг — сопоставить каждый изменённый файл с проектом‑владельцем. В монорепо это обычно сервис, пакет, приложение или общая библиотека. Файл в apps/web принадлежит веб‑приложению. Файл в packages/auth принадлежит пакету auth.
Это сопоставление — только начало. Нужны ещё правила зависимостей, потому что один общий пакет может затронуть множество проектов. Если меняется пакет auth, а API и web импортируют его, CI должен протестировать все три проекта. Если меняется страница документации, CI может пропустить продуктовые тесты.
Поток прост: соберите изменённые файлы из ветки или PR, сопоставьте их с принадлежащими проектами, пройдите по графу зависимостей, чтобы найти проекты, которые зависят от них, и запустите сборку, тесты и линт только для этого набора.
Это лучше всего работает, когда у репозитория есть реальная карта зависимостей в коде. Не держите её в чьей‑то голове или на устаревшей вики. Сохраняйте границы проектов и зависимости в конфигурации workspace, файлах сборки или в небольшом манифесте, который CI может читать при каждом запуске.
Пропуск неизменённых проектов должен считаться нормальным результатом, а не ошибкой и не предупреждением. Это звучит мелко, но меняет то, как команды воспринимают результаты пайплайна. Зелёный статус должен означать "проверены все то, что могло сломаться", а не "мы запустили все задачи, какие есть".
Представьте растущую команду с веб‑приложением, API, воркером и пятью общими пакетами. Один PR меняет helper для retry, который используется только воркером. Хорошее определение затронутых проектов запустит задачи воркера, тесты пакета‑хелпера и, возможно, небольшую интеграционную проверку. Остальное останется без внимания. Именно так ежедневная доставка остаётся быстрой, не превращая CI в догадку.
Простой пайплайн, который вы можете настроить
Начните с карты репозитория, а не с инструмента CI. Запишите каждое деплойбл‑приложение, каждый сервис и каждый общий пакет. Если папка может сломать продакшен или блокировать релиз, CI должен знать, что она принадлежит проекту.
Начните с явных правил для папок
Большинство команд пытаются усложнить слишком рано. Это делает первую версию хрупкой. Начните с простых правил по путям для папок, которые всем понятны: apps/web, services/billing, packages/ui.
Затем добавьте шаг с git diff. Сравните ветку с main, соберите изменённые файлы и превратите их в список затронутых проектов. Этот список станет входом для остального пайплайна.
Практическая настройка часто выглядит так:
- Определите изменённые файлы с помощью
git diff. - Сопоставьте эти файлы с проектами через правила по путям.
- Расширьте список, если изменился общий пакет и downstream‑приложения от него зависят.
- Запустите линт, тесты и сборки только для затронутых проектов.
- Если ничего важного не поменялось, выполните небольшой smoke‑чек и остановитесь.
Это работает, потому что сначала обрабатывает очевидные случаи. Правка в документации не должна собирать десять сервисов. Изменение в packages/auth вероятно должно протестировать все приложения, которые его импортируют.
Не убирайте сетку безопасности. Оставьте один запланированный полный прогон репозитория, обычно раз в ночь или раз в день. Эта задача ловит плохие правила по путям, пропущенные зависимости и крайние случаи, которые селективное тестирование может не увидеть.
Если запланированный полный прогон падает, исправьте карту прежде чем снова доверять селективным задачам. Эта дисциплина важнее конкретного CI‑продукта, который вы используете. Простая настройка с понятными правилами часто достаточно, чтобы большой монорепо снова почувствовало себя быстрым.
Правила релиза, которые держат main стабильным
Быстрый монорепо CI работает только если правила мёрджа и релиза остаются строгими. Когда пайплайн пропускает работу, контракт должен быть ясен: каждый затронутый проект должен пройти проверку до попадания кода в main, и каждое изменение в общем коде должно распространиться на приложения, которые от него зависят.
Мёрдж‑гейты
Пул‑реквест не должен вливаться, пока CI не докажет, что изменённый код и его зависимые модули работают. Фильтры по путям помогают решить, с чего начинать, но сами по себе этого мало. Если разработчик редактирует общий пакет auth, API, воркер и админ‑приложение, которые его используют, должны пройти сборку и тесты, даже если их папки не менялись.
Это правило делает main скучным — а это именно то, что нужно. Команды попадают в беду, когда тестируют только ту папку, что поменялась, и игнорируют граф зависимостей. Пул‑реквест выглядит зелёным, а затем при деплое ломается downstream‑сервис.
Простая политика подходит для большинства команд. Блокируйте мёрджи, пока не пройдут все затронутые приложения, пакеты и тест‑наборы. Расширяйте список затронутых при изменении общей библиотеки, схемы или конфигурации сборки. Обращайтесь с CI‑конфигами, Docker‑файлами и корневыми package‑файлами как с репо‑широкими изменениями. Правки только в документации пусть пропускают большинство проверок, но только когда это явно прописано.
Триггеры релизов
Релизы тоже должны оставаться селективными. Если изменился один сервис — публикуйте этот сервис и оставьте остальные в покое. Это сокращает время деплоя и предотвращает бесполезные bump‑версии по всему репозиторию.
Теги помогают, когда они соответствуют структуре репозитория. Если apps/api публикуется как api‑v1.8.2, а apps/web как web‑v3.4.0, никому не нужно гадать, к чему относится каждый тег. Та же идея работает для общих пакетов. Явные имена тегов упрощают откат и аудит.
Один случай требует более широких проверок: крупные обновления зависимостей. Если вы повышаете версию фреймворка, меняете базовый образ или меняете инструмент сборки, выполните полный релиз‑чек по всему репозиторию. Это дороже для одного изменения, но спасает от обнаружения поломок после релиза.
Именно так монорепо CI заслуживает доверие. Он пропускает работу при обычных коммитах и расширяет проверки только тогда, когда изменение может распространиться по репозиторию. Этот баланс позволяет команде доставлять каждый день, не превращая main в лотерею.
Реалистичный пример из растущей продуктовой команды
Представьте команду с одним репозиторием для нескольких приложений и общих пакетов. Во вторник утром разработчик обновляет экраны оформления заказа в web и меняет общий пакет ценообразования, который рассчитывает скидки и налоговые правила.
В медленном пайплайне это небольшое изменение разбудило бы все задачи репозитория. Поиск (search) собирался бы. Админ тестировался бы. Мобильная часть выполняла бы свои проверки. Половина работы не имела бы отношения к изменению, но команда всё равно ждала бы.
С монорепо CI репозиторий реагирует на то, что реально изменилось. Фильтры по путям замечают правки в области оформления заказа web и в пакете pricing. Определение затронутых проектов затем проверяет, какие проекты зависят от этого кода pricing.
В результате план тестирования гораздо меньше: тесты web для сценариев оформления заказа, юнит‑тесты библиотеки pricing и быстрый контракт‑чек между web и billing API. Search, admin и mobile остаются бездействующими, потому что ни пути изменения, ни граф зависимостей не указывают на них.
Это важно. Пропуск задач — не угадывание. Репозиторий уже знает, какой код использует каждый проект, поэтому он может избежать нерелевантной работы по очевидной причине.
Контракт‑чек важнее, чем многие ожидают. UI оформления заказа может проходить свои тесты, но после изменения ценообразования отправлять неверный payload в billing. Быстрая проверка на границе между системами ловит такие ошибки, которые часто ускользают от локальных тестов.
Правила релиза тоже остаются узкими. Когда ветка влита, пайплайн публикует новую сборку web и релизит пакет pricing. Он не трогает админ‑панель. Не перестраивает мобильные артефакты. Оставляет сервис search в покое.
Это делает релиз чистым. Заметка о деплое короткая, путь отката очевиден, и команда точно знает, что изменилось.
Такая настройка может превратить 45‑минутное ожидание в 10–12 минут. Эта разница меняет поведение. Люди перестают пачками собирать несвязанные изменения, чтобы избежать долгих CI‑прогонов. Они выпускают исправление оформления заказа в тот же день, проверяют релиз пакета pricing и переходят к следующей задаче, вместо того чтобы присматривать за полным пайплайном.
Для растущей команды это обычно реальная выгода. Селективное тестирование экономит ресурсы, но большая победа — это скорость. Небольшие изменения остаются малыми до самого продакшена.
Ошибки, которые ломают селективный CI
Селективный CI экономит время, пока одно небольшое правило не устареет. Тогда пайплайн начинает давать ложное ощущение безопасности: коммит выглядит безопасным, задачи зелёные, а сломанный сервис появляется позже, когда кто‑то пытается запушить релиз.
Общий код приносит большую часть проблем. Команды часто пишут понятные фильтры по папкам для каждого приложения, но забывают такие каталоги, как packages/auth, libs/ui, общие схемы или скрипты сборки. Одно изменение в общем пакете может повлиять на множество сервисов одновременно. Если downstream‑тесты никогда не запускаются, баг проскальзывает с зелёной галочкой.
Переименования создают более тихую проблему. Команда перемещает packages/core в libs/core, обновляет импорты и мержит рефакторинг. Правила CI всё ещё следят за старым путём. В пайплайне ничего не падает, но новая папка больше не триггерит нужные задачи.
Сгенерированные файлы могут столь же сильно тратить время. Codegen‑артефакты, lock‑файлы, снапшоты и сгенерированные SDK часто затрагивают множество директорий в одном коммите. Если правила трактуют эти файлы как обычный исходник, они разбудят задачи, которые никому не нужны. Скоро быстрый пайплайн снова начнёт тупить.
Несколько привычек предотвращают большинство проблем. Сопоставляйте общие папки со всеми сервисами, которые от них зависят. Пересматривайте фильтры после каждого переименования или перемещения папок. Исключайте сгенерированные артефакты, если они не влияют на поведение во время выполнения. Тестируйте логику пропусков во время рефакторингов, а не через неделю.
Правила релизов дрейфуют быстрее, чем команды думают. Сервис может публиковаться только при изменении собственной папки, хотя он зависит от общих API‑контрактов, миграций или сгенерированных клиентов. Репозиторий остаётся зелёным, но релиз получает несовместимость. Так команды начинают дебажить «случайные» поломки, которые CI должен был поймать.
Большие чистки репозитория ухудшают ситуацию. Когда инженеры делят пакеты, объединяют сервисы или реорганизуют директории, они обычно ревьюят код приложений и забывают о логике пайплайна. Монорепо CI требует того же уровня ревью, что и код, который он защищает.
Относитесь к правилам селективного CI как к продукт‑коду. Выносите их в код‑ревью, добавляйте тесты и назначьте явного владельца. Если у правил нет владельца, они перестают соответствовать реальности.
Быстрая чек‑листа перед тем как полагаться на это
Селективный CI экономит время только тогда, когда люди могут объяснить, почему пайплайн отработал так, а не иначе. Если задачи кажутся случайными, команда перестаёт доверять результатам и снова начнёт требовать полных прогонов.
Перед тем как полагаться на монорепо CI для ежедневной доставки, проверьте эти пять пунктов:
- Разработчик может объяснить каждый запуск или пропуск за минуту.
- Общие библиотеки следуют ясным правилам зависимостей.
- Запланированный полный прогон всё ещё проходит.
- Скрипт релиза может назвать изменённый сервис без угадывания.
- В логах CI рано печатается список затронутых проектов.
Команды часто пропускают вторую и пятую проверки. Они добавляют фильтры по путям, видят ускорение и думают, что проблема решена. Затем меняется общий пакет, один сервис пропускает тесты, и баг попадает в main, потому что никто не увидел явный список затронутых проектов в логах.
Простое правило помогает: если новый инженер не может прочитать один коммит и объяснить результат пайплайна, настройка ещё не готова. Быстрые пайплайны приятны. Но понятные пайплайны — те, которыми команды продолжают пользоваться.
Что делать дальше
Выберите самый медленный пайплайн в репозитории и исправьте его первым. Чаще всего команды ошибаются, пытаясь перестроить CI для всех сервисов сразу. Начните там, где боль очевидна — например, сервис, который ждёт 20 минут результатов тестов после небольшой правки.
Держите первый rollout простым для отладки. Фильтр по пути, который смотрит только на одну папку, легче доверять, чем плотный набор правил с общими зависимостями, тегами и исключениями. Если кто‑то спрашивает, почему задача запустилась или нет, вы должны уметь ответить одним коротким предложением.
Собирайте несколько метрик с самого начала: время ожидания в очереди до старта задач, общее время тестов для обычного коммита, пропущенные задачи, которые должны были выполниться, и ошибки релиза после мёрджа. Эти числа важнее субъективных ощущений команды. Если время в очереди падает, а ошибки релиза остаются на том же уровне, ваша монорепо CI движется в правильном направлении. Если пропущенные задачи начинают пускать баги, остановитесь и найдите точное условие, которое вызвало пропуск, прежде чем добавлять новые правила.
Дайте настройке две недели реальных коммитов перед тем, как её ужесточать. Живая история быстро покажет неудобные случаи: общие пакеты, обновления схем, сгенерированные файлы и особенности релизных веток. После этого уберите шумные задачи, добавьте недостающие ребра зависимостей и зафиксируйте исключения, чтобы никто не гадал.
Растущая команда может начать с одного правила: изменения фронтенда запускают фронтенд‑тесты, изменения API — API‑тесты, изменения в документации — почти ничего. Это уже сократит трату ресурсов, не делая пайплайн трудным для понимания. Позже командa может добавить определение затронутых проектов для общего кода и ужесточить правила релиза для main.
Если вашей команде нужен второй взгляд, Oleg Sotnikov at oleg.is работает с стартапами и небольшими компаниями как Fractional CTO и часто ревьюит репозитории, CI‑настройки и процессы релиза. Внешний аудит может помочь сократить время ожидания, не сделав пайплайн хрупким.
Не гоняйтесь за хитрой конфигурацией. Используйте ту, которую команда может объяснить на доске и исправить под давлением. Именно тогда селективное тестирование начинает действительно экономить время, а не порождать новую путаницу.
Часто задаваемые вопросы
What does selective CI mean in a monorepo?
Selective CI запускает проверки только для тех приложений, сервисов и пакетов, которые затронул ваш коммит. Это ускоряет обратную связь, сокращает время ожидания в очереди и позволяет разработчикам отправлять небольшие изменения, не дожидаясь выполнения нерелевантных задач.
How are path filters different from affected project detection?
Фильтры по путям анализируют изменённые файлы и сопоставляют их с папками или проектами. Определение затронутых проектов идёт дальше: оно учитывает зависимости, поэтому изменение в общем пакете также запускает проверки для всех приложений, которые его импортируют.
Can docs-only changes skip most tests?
Пропускайте продуктовые тесты, если изменение действительно ограничивается только документацией и не трогает общие пакеты, файлы сборки или скрипты деплоя. При этом запустите небольшой набор проверок для документации или орфографии, чтобы пайплайн всё же верифицировал что‑то полезное.
What changes should trigger a wider pipeline?
Рассматривайте как репо‑широкие изменения корневые конфиги, файлы CI, Docker‑файлы, базовые образы, общие схемы и изменения инструментов сборки, если вы не уверены в их влиянии. Эти файлы часто затрагивают множество проектов, даже если папки приложений не менялись.
How should I handle shared packages in CI?
Сопоставляйте каждый общий пакет со всеми приложениями и сервисами, которые от него зависят. Храните эту карту зависимостей в коде или в конфигурации workspace, чтобы CI мог расширять набор затронутых проектов от изменённого пакета к его downstream‑зависимостям.
Do I still need a full repo run?
Да. Запускайте полный прогон репозитория по расписанию, обычно раз в сутки или ночью. Эта задача ловит устаревшие правила, пропущенные связи зависимостей и изменения в структуре папок до того, как они станут проблемой при релизе.
How do I debug why a job ran or got skipped?
Начинайте с логов: печатайте список изменённых файлов, сопоставленные проекты и финальный список затронутых модулей в начале каждого запуска. Когда люди видят эти данные сразу, они в секунды объясняют, почему задача запустилась или была пропущена.
What usually breaks selective CI?
Обычно команды забывают про общий код, пропускают переименования папок или считают сгенерированные файлы исходниками. Эти ошибки либо пропускают проверки, которые должны были выполниться, либо будят слишком много задач и возвращают пайплайн к медленному состоянию.
Should release rules use the same affected logic?
Да. Если изменился один сервис, публикуйте только его, не трогая остальные. Когда обновление библиотеки, схемы или фреймворка может распространиться по всему репозиторию, заранее расширьте проверки релиза для такого изменения.
What is the best first step to adopt this setup?
Выберите самое медленное место в репозитории и сначала добавьте простые правила для него. Сравните ветку с main, соберите список изменений через git diff, постройте список затронутых проектов и держите первую версию правил достаточно простой, чтобы объяснить её на доске.