Все посты

Affected как режим работы платформы, а не оптимизация сборки

25 мая 2026 г.6 мин чтенияKB Labs
Share

Две сцены.

Сцена первая. Разработчик правит один файл в core/types/. Открывает PR. CI 12 минут билдит весь монорепо, потом ещё 4 минуты гоняет интеграционные тесты для приложений, которые этот пакет вообще не импортируют. Через 16 минут зелёная галка. Все привыкли — «у нас 80 пакетов, иначе никак».

Сцена вторая. Тот же разработчик ставит на PR лейбл preview. CI собирает три docker-образа, пушит в registry, поднимает на VPS web/api/docs контейнеры, рисует три субдомена. Reviewer открывает URL — там тот же сайт, что в main. Потому что изменения были в core/, к которому фронт не обращается. RAM съедена, preview-стек висит до закрытия PR.

Обе сцены — про одну дыру.

Обычно говорят: affected ускоряет сборку

Большинство монорепо-инструментов умеют affected detection: смотришь diff, находишь изменённые пакеты, добавляешь downstream'ы через граф зависимостей, пересобираешь только их. Turbo, Nx, kb-devkit — все умеют.

Обычная подача: «affected ускоряет сборку». Правда. Но это самый дешёвый из эффектов — build-skip экономит секунды. Если остановиться только на сборке, теряешь всё остальное.

На самом деле это сигнал

Affected — не «не пересобирай лишнее». Это ответ на вопрос: какая часть мира вообще относится к этому изменению?

И этот ответ одинаково полезен на каждом уровне стека.

Уровень 1: сборка

Решение: что пересобирать. Ответ движет компилятор. Секунды CPU.

kb-devkit run build --affected

Уровень 2: артефакты

Решение: какие docker-образы собирать и пушить. Ответ движет CI и сетевой трафик. Десятки секунд, место в registry, и production rollout не дёргается без причины.

# .kb/deploy.yaml
targets:
  kb-gateway:
    watch: [plugins/gateway/**, core/**, shared/**]
    image: kb-gateway

kb-deploy сравнивает watch: глобы с git diff. Ничего не попало — таргет пропущен. Никакого build, push, ssh.

Уровень 3: окружения

Решение: поднимать ли preview вообще. Ответ двигает целый docker compose: контейнеры, RAM, порты, nginx-роуты, субдомены. Минуты времени, ~1 ГБ RAM, и PR thread не засоряется комментарием «вот превью», который никто не откроет.

Vercel и Render рекламируют «preview на каждый PR» как фичу. Это выгодно им — ты платишь за runtime. Но PR, который трогает только серверную логику без user-facing изменений, не нуждается в трёх поднятых контейнерах.

В нашем CI это выглядит так:

CHANGED=$(git diff --name-only origin/main...HEAD)
AFFECTED=()
[[ "$CHANGED" =~ ^sites/web/apps/web/  ]] && AFFECTED+=(web)
[[ "$CHANGED" =~ ^sites/web/apps/docs/ ]] && AFFECTED+=(docs)
[[ "$CHANGED" =~ ^plugins/gateway/|^core/|^shared/ ]] && AFFECTED+=(gateway)
[[ ${#AFFECTED[@]} -eq 0 ]] && { echo "no UI affected — skipping preview"; exit 0; }

Восемь строк. Есть что-то видимое глазом — поднимаем. Нет — не поднимаем. Никто ничего не теряет.

Когда уровни складываются

PR с одним коммитом в sites/web/apps/web/components/Header.tsx:

  • Уровень 1: пересобрать только kb-labs-web и зависимые.
  • Уровень 2: запушить только kb-site-web. Gateway и docs — не трогать.
  • Уровень 3: поднять preview только с web-контейнером. Без gateway, без docs.

Один вопрос, три экономии. Каждая на порядок больше предыдущей: секунды → минуты → минуты + RAM + UX.

Как понять, что этого не хватает

Хороший признак — когда есть решение «делать или нет», но нет чёткого ответа. Обычно затыкают флагом: if: env.BRANCH == 'main', --all, manual approval. Флаги накапливаются, платформа перестаёт думать сама.

Если поймал себя на таком флаге — спроси: а что система уже знает про это изменение? Если знает — это вход. Если нет — это то, чего не хватает.

Заметка про реализацию

watch: глобы должны быть одинаковыми на всех уровнях. У нас это путь-глобы: plugins/gateway/**, sites/web/apps/web/**. Один синтаксис в kb-devkit, kb-deploy и preview-pipeline.

Когда один сигнал используется на трёх уровнях, у него должно быть одно представление. Иначе уровни расходятся и начинаются баги «build пропустил X, но deploy собрал X». Лечится только сведением к одной форме.


Для нас affected в итоге материализовался в три независимые реализации — на уровне сборки, артефактов и окружений. Все три выросли в разное время из разных задач. Когда я собирал их вместе для preview environments — стало видно, что это один и тот же приём, просто применённый трижды. Один сигнал, три применения, три класса экономии.

Affected — это не оптимизация сборки. Это вопрос «а нужно ли это делать вообще», заданный на каждом уровне.

Affected как режим работы платформы, а не оптимизация сборки