Инженерная практика
Долг флагов и дисциплина выкатки
Команда, перешедшая на trunk-based два года назад, открывает свой дашборд флагов: 400 флагов, и никто не может сказать, какие ещё важны. Флаг «new checkout» уже год на 100% — но старый путь кода всё ещё там, за else, непротестированный, изредка задеваемый опечаткой в конфиге. Инцидент трассируется к двум флагам, чью комбинацию никто никогда не пробовал. Они сбежали из ада слияний и зашли прямиком в другую тюрьму: флаги, которые они так и не удалили. Trunk-based не убрал долг. Он сменил его форму.
Долг не исчез — он сменил форму
Прошлые уроки сделали флаги героем: они отвязывают деплой от релиза и делают ежедневную интеграцию возможной. Сеньорская оговорка в том, что каждый флаг — это ещё и ветка в вашем рантайме. Долгоживущая git-ветка расщепляет вашу кодовую базу на расходящиеся истории; долгоживущий флаг расщепляет вашу работающую систему на расходящиеся пути исполнения. Вы переместили развилку из системы контроля версий в оператор if, который труднее увидеть и труднее удалить, потому что он живёт в проде и кто-то может на него опираться.
Так что release-флаг, переживший свою фичу, — не нейтральный остаток, это долгоживущая ветка в маскировке, с той же проблемой разрыва, всплывающей как непротестированные пути кода. Старый путь за else гниёт: никто его не запускает, никто не обновляет, но он всё ещё достижим. Trunk-based приносит свою выгоду, только если вы удаляете флаги так же агрессивно, как удаляете ветки: удали ветку при слиянии, удали флаг при полной выкатке. Зачистка — не гигиена, которую делают, когда есть время; это вторая половина сделки, которая делает всю практику net-positive.
Почему устаревшие флаги взрываются комбинаторно
Цена долга флагов не линейна по числу флагов — она экспоненциальна. Каждый независимый булев флаг удваивает число возможных рантайм-конфигураций вашей системы. С 3 флагами у вас 2³ = 8 конфигураций; с 10 флагами — 1024; с 20 — более миллиона. Вы тестируете горстку этих путей и выкатываете; остальные живы, но непроверены. Большинство инцидентов от долга флагов — это баги взаимодействия: флаг A в порядке, флаг B в порядке, но состояние, где оба включены (или один включён, а другой полу-разогнан), никогда не прогонялось и делает что-то не так. Это та же проблема 2^N непротестированных конфигураций, которую вызвали бы устаревшие ветки, если бы вы держали их все живыми — она просто прячется в условиях вместо git branch -a.
Вот почему таксономия времени жизни из урока 3 несущая. Release-флаги должны быть короткоживущими именно потому, что это они тихо становятся постоянными. Смягчение — дисциплина жизненного цикла: дайте каждому release-флагу владельца и срок годности, отслеживайте возраст флага так же, как возраст ветки, валите сборку или алертите, когда release-флаг переживает свою ожидаемую жизнь, и сделайте «убрать флаг и мёртвый путь» обязательным завершающим шагом каждой выкатки — а не опциональным follow-up тикетом, который никогда не приоритизируется.
| Активные release-флаги | Возможные рантайм-конфиги (2^N) | Реалистично протестировано | Поверхность багов взаимодействия |
|---|---|---|---|
| 3 | 8 | Несколько | Малая, управляемая |
| 10 | 1024 | Всё ещё несколько | Большинство конфигов никогда не бегут |
| 20 | > 1 000 000 | Исчезающая доля | Баги взаимодействия неизбежны |
Progressive delivery и честное прочтение данных
Сделанный правильно, жизненный цикл флага — это ещё и техника доставки: progressive delivery — это дисциплина релиза через контролируемые, измеряемые стадии — внутренние → 1% → canary-когорта → 100% — с автоматическими ограждениями, которые останавливают или откатывают рампу, если частота ошибок, задержка или бизнес-метрики регрессируют. Флаг — это исполнительный механизм; метрики — контроллер. Это та операционная зрелость, к которой строит trunk-based: релиз как замкнутый контур обратной связи, а не односторонний пуш.
Стоит также быть точным в том, что доказывают знаменитые числа. Исследование DORA стабильно ставит trunk-based development среди сильнейших коррелятов элитной доставки — отчёт 2024 года (39 000+ респондентов) нашёл, что лишь ~19% команд достигают элиты, а элитные исполнители деплоят порядка в 182× чаще, с в 127× более быстрым lead time и куда более низкой частотой отказов изменений, и заметно чаще практикуют trunk-based. Но корреляция идёт через предпосылки: trunk-based приносит результат, только когда быстрый зелёный гейт, флаги и дисциплина зачистки — все присутствуют. Примите только модель ветвления — и получите режимы отказа из этих пяти уроков: разрыв без слияний, если ветки залёживаются, красный trunk, если гейт отсутствует, или комбинаторное болото флагов, если зачистку пропустили. Рычаг — не модель веток; это вся дисциплинированная система вокруг неё.
Почему это работает
Чистая симметрия этого юнита: долгоживущая ветка и устаревший флаг — это один и тот же баг в двух местах. Оба — развилки, которые должны были быть временными, оба накапливают непротестированное расхождение, и оба налагают цену 2^N, если дать им накопиться — ветки в вашей истории, флаги в вашем рантайме. Trunk-based development в корне — это единая дисциплина, применяемая к обоим: держи развилки короткоживущими и удаляй их в тот момент, когда они отслужили своё. Получи эту дисциплину — и числа DORA последуют; пропусти её — и ты просто переместил бардак.
Фича на 100% выкатки и стабильна две недели. Release-флаг всё ещё в коде. Что должно произойти?
Почему release-флаг, оставленный включённым после полной выкатки, описывают как «долгоживущую ветку, спрятанную в if»?
Почему долг флагов описывают как экспоненциальный, а не линейный по числу флагов?
Упорядочьте полный жизненный цикл дисциплинированного release-флага:
- 1 Создать release-флаг с владельцем и датой истечения
- 2 Выкатить работу на trunk тёмной за выключенным флагом, интегрируясь ежедневно
- 3 Постепенно разогнать 1% → 100% с метрик-ограждениями, способными авто-остановить
- 4 Подержать на стабильных 100% коротко, чтобы подтвердить отсутствие регрессии
- 5 Удалить флаг и мёртвый старый путь как завершающий шаг выкатки
- 01Объясните утверждение, что trunk-based «меняет долг веток на долг флагов», и почему эта сделка стоит того только с дисциплиной зачистки.
- 02Что данные DORA на самом деле доказывают про trunk-based, и какова честная причинная история?
Trunk-based development не устраняет долг — он меняет его форму с долга веток на долг флагов, потому что каждый флаг — это ветка в вашем рантайме: release-флаг, оставленный включённым после полной выкатки, — это долгоживущая ветка в if, со старым путём, гниющим непротестированным, но всё ещё достижимым. Цена экспоненциальна, поскольку каждый независимый булев флаг удваивает рантайм-конфигурации — 20 флагов это более миллиона состояний, из которых вы тестируете горстку — так что баги взаимодействия (A в порядке, B в порядке, оба-включены никогда не пробовали) становятся неизбежными, та же проблема 2^N непротестированных конфигураций, которую вызвали бы устаревшие ветки, перемещённая в условия. Дисциплина, делающая всю практику net-positive, — это зачистка, выполняемая как завершающий шаг выкатки: дайте release-флагам владельцев и сроки годности, отслеживайте возраст флага как возраст ветки и удаляйте флаг и мёртвый путь в тот момент, когда фича стабильна на 100% — в идеале как конец рампы progressive delivery, где метрики защищают каждый шаг и могут авто-остановить. А числа DORA — ~19% элиты, 182× частота деплоя, 127× более быстрый lead time — коррелируют с trunk-based, только когда быстрый зелёный гейт, флаги и дисциплина зачистки — все присутствуют; рычагом никогда не была модель ветвления сама по себе, а дисциплинированная система вокруг неё, держащая каждую развилку, в истории или в рантайме, короткоживущей.