Суть Читайте реальные диффы и CI-конфиг, предсказывайте поведение по trunk-based и выбирайте фикс с наибольшим рычагом — обёртка флагом, branch by abstraction и зелёный гейт.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Дисциплина trunk-based видна в диффе и CI-конфиге, а не в манифесте. Читайте каждый сниппет, предсказывайте, как он поведёт себя на общем trunk, и выбирайте фикс, который senior-инженер сделает первым.
Цель
Потренируйтесь читать артефакты, которые решают, действительно ли команда trunk-based: как незавершённая работа обёрнута, чтобы ехать dark, как большое изменение едет на trunk через абстракцию и как настроен гейт, держащий trunk зелёным.
Сниппет 1 — отправка незавершённой работы в trunk
// Новый checkout-флоу не готов, но должен влиться в trunk сегодня.export async function handleCheckout(cart: Cart, user: User) { if (flags.isEnabled("checkout-v2", { userId: user.id })) { return newCheckout(cart, user); // недостроен, ещё нет расчёта налога } return legacyCheckout(cart, user); // текущий продакшен-путь}
Викторина
Completed
Флаг 'checkout-v2' по умолчанию выключен в продакшене. Что верно, если этот код вливается в trunk ежедневно?
Heads-up Deployed — не released. Флаг выключен, поэтому пользователи попадают в legacyCheckout; новый путь присутствует, но dark. Именно так trunk-based безопасно отправляет незавершённую работу.
Heads-up Весь смысл флага в том, что newCheckout может быть незавершён. Он вливается ежедневно за выключенным флагом и включается только когда готов — release отделён от deploy.
Heads-up Флаг заменяет долгоживущую ветку. Размещение условия на trunk и позволяет работе вливаться ежедневно, не накапливая дрейф.
Сниппет 2 — шестинедельная замена ORM на trunk
interface UserStore { // абстракция find(id: string): Promise<User>; save(u: User): Promise<void>;}class LegacyOrmStore implements UserStore { /* текущий, в работе */ }class NewOrmStore implements UserStore { /* строится инкрементально, dark */ }// связано один раз, переключается на границе, когда NewOrmStore готовexport const userStore: UserStore = flags.isEnabled("orm-v2") ? new NewOrmStore() : new LegacyOrmStore();
Викторина
Completed
Почему такая форма branch by abstraction — это trunk-based ответ на миграцию, которую нельзя выпустить за один день?
Heads-up Суть обратная: NewOrmStore строится на trunk за абстракцией, а не на долгоживущей ветке. Абстракция и убирает потребность в ветке.
Heads-up Прямое переписывание вынуждает либо долгоживущую ветку, либо сломанный trunk. Абстракция держит trunk релизопригодным всё время и позволяет переключить или откатить на одной границе.
Heads-up После проверки нового пути вы удаляете старую реализацию, а затем ставшую лишней абстракцию — оставить её значит создать свою же оставленную обвязку.
Сниппет 3 — конфиг CI-гейта
# .ci/trunk-gate.yml — запускается на каждый PRon: pull_requestjobs: gate: steps: - run: make unit-tests # ~2 мин, блокирует merge - run: make lint typecheck # ~1 мин, блокирует merge e2e: steps: - run: make e2e-suite # ~40 мин if: github.event_name == 'schedule' # только ночью, никогда не блокирует
Викторина
Completed
Читая этот гейт, какая senior-оценка верна?
Heads-up Блокирующий 40-минутный набор подтолкнул бы инженеров батчить изменения, удлиняя ветки и возвращая дрейф. Многоуровневость — быстрые проверки до merge, медленные после/ночью — стандартный способ держать гейт быстрым.
Heads-up Опора на память — не гейт. Выбор намеренный: блокировать на быстрых проверках, медленные гонять ночью, а редкий ночной сбой лечить через stop-the-line плюс bisect.
Heads-up Быстрые статические проверки, блокирующие merge, — ровно то, что дёшево держит trunk зелёным. Управлять надо медлительностью, а не строгостью — а эти проверки идут секунды-минуты.
Сниппет 4 — оставленный устаревший флаг
// выпущен 14 месяцев назад, "search-v2" всё это время был на 100%function rankResults(q: Query, results: Result[]) { if (flags.isEnabled("search-v2")) { return rankV2(q, results); // живой путь } return rankV1(q, results); // не запускается, не обновлялся с запуска}
Викторина
Completed
search-v2 был на 100% уже 14 месяцев. Какое действие имеет наибольший рычаг и почему?
Heads-up Это type laundering — переименование оставленной release-обвязки в постоянный ops-флаг держит гниющий старый путь живым. Если kill switch правда нужен, спроектируйте его осознанно.
Heads-up Отложенная очистка флагов — ровно то, как команды доходят до 400 устаревших флагов. Удаление должно быть закрывающим шагом раскатки, частью «готово», а не follow-up, который деприоритизируют.
Heads-up Он не безвреден: rankV1 всё ещё достижим (опечатка в конфиге может направить туда), не поддерживается и это ещё один булев флаг в комбинаторном пространстве конфигов. Release flag на 100% нужно удалять.
Итог
Дисциплина trunk-based читаема в артефактах. Условие с флагом на trunk позволяет незавершённой работе ехать dark и вливаться ежедневно без дрейфа; интерфейс-абстракция позволяет шестинедельной миграции ехать на trunk как маленькие зелёные коммиты с одним flip-and-delete; многоуровневый CI-гейт остаётся быстрым, блокируя на unit/lint/typecheck и гоняя медленный e2e вне критического пути; а release flag, всё ещё включённый на стабильном 100%, — это долгоживущая ветка, спрятанная в if, которую вы удаляете (флаг и мёртвый путь вместе) как закрывающий шаг раскатки.