Git-workflow для команд: стратегии ветвления, PR-процесс и релизы¶
Доклад для DevOps-менторства
Содержание¶
- Введение
- Что такое Git-workflow
- Стратегии ветвления: сравнение
- Архитектура: путь изменения от задачи до production
- Компоненты процесса подробно
- Инструменты
- Практические примеры конфигураций
- Стратегии ветвления подробно
- Best practices
- Частые ошибки и антипаттерны
- Метрики эффективности
- Заключение
Что нужно знать заранее. Этот мануал опирается на 00-git-manual: там разобраны коммиты, ветки, теги, merge/rebase/squash как технические операции и разрешение конфликтов. Здесь не повторяем, как это работает, а договариваемся, какие правила команда из этого строит. Вторая опора — 02-cicd-manual: стратегия ветвления определяет, что и когда запускает пайплайн, поэтому эти две темы читаются вместе.
Введение¶
Пока в репозитории работает один человек, Git-workflow ему не нужен: он и
так знает, что лежит в какой ветке. С приходом второго, пятого, двадцатого
разработчика появляются вопросы, на которые Git сам не отвечает. Откуда
начинать ветку? Когда и как её вливать? Кто должен посмотреть код? Что
считается «готовым к релизу»? Как выпустить срочное исправление, если в
main уже лежит недоделанная фича? Git-workflow — это набор ответов на эти
вопросы, о котором договорилась команда и который закреплён настройками
платформы, а не устной традицией.
Для DevOps-инженера это не «дело разработчиков». Стратегия ветвления определяет, какие события запускают пайплайн, из какой ветки собирается релизный артефакт, где стоят ворота качества и насколько быстро изменение вообще может дойти до production. Слабый workflow не компенсировать самым лучшим CI/CD: если код неделями живёт в отдельных ветках, то Continuous Integration в команде нет, как бы ни назывался job в YAML.
Без согласованного workflow:
- Ветки живут неделями, а слияние превращается в многодневный «merge hell»
- Pull Request на 3000 строк «смотрят» за пять минут и одобряют не читая
- В main можно запушить напрямую, в обход тестов и review
- Непонятно, какая версия сейчас в production и что в неё вошло
- Hotfix делают прямо на сервере или в ветке, которую потом забывают влить обратно
С согласованным workflow: - Изменения маленькие и вливаются в основную ветку ежедневно - Каждое изменение прошло автоматические проверки и review нужных людей, и это гарантирует платформа, а не честное слово - Основная ветка всегда в рабочем состоянии, и из неё можно выпустить релиз - Версии и changelog вычисляются из истории коммитов автоматически - Срочное исправление идёт по заранее известному, отрепетированному пути
Что такое Git-workflow¶
Git-workflow (командный процесс работы с Git) — это соглашение о том, какие ветки существуют в репозитории, как изменения попадают из рабочей ветки в основную, какие проверки и одобрения для этого нужны и как из истории получаются релизы.
Он складывается из четырёх слоёв, и путать их не надо:
| Слой | Вопрос, на который отвечает | Пример решения |
|---|---|---|
| Стратегия ветвления | Какие ветки есть и сколько они живут | Trunk-based: только main + ветки на 1–2 дня |
| Процесс интеграции (PR/MR) | Как изменение попадает в основную ветку | PR, 1 approval от CODEOWNERS, зелёный CI, merge queue |
| Метод слияния | Какой след изменение оставляет в истории | Squash merge, заголовок PR в формате Conventional Commits |
| Релизный процесс | Как из истории получается версия | Тег vX.Y.Z + changelog, генерируемые из коммитов |
Базовые термины:
| Термин | Расшифровка | Суть |
|---|---|---|
| Trunk / main | Ствол, основная ветка | Единственная долгоживущая ветка, в которую интегрируются все изменения и из которой выпускаются релизы |
| PR / MR | Pull Request (GitHub, Bitbucket) / Merge Request (GitLab) | Запрос на вливание ветки: diff, обсуждение, результаты CI и одобрения в одном месте. Это одно и то же понятие под двумя названиями |
| TBD | Trunk-Based Development | Стратегия, в которой все интегрируются в trunk минимум раз в день |
| Branch protection / Rulesets | Защита веток | Правила платформы, запрещающие менять ветку в обход PR, проверок и одобрений |
| CODEOWNERS | Файл владельцев кода | Сопоставление путей в репозитории и людей/команд, чьё одобрение обязательно |
| SemVer | Semantic Versioning | Схема версий MAJOR.MINOR.PATCH, где номер отражает характер изменений |
Главное, что стоит усвоить: workflow — не про Git, а про договорённость людей, закреплённую в настройках платформы. Git одинаково легко позволяет и GitFlow, и trunk-based, и полный хаос. Что из этого будет у вас, решают правила защиты веток, требования к PR и то, что делает пайплайн на каждом событии.
Стратегии ветвления: сравнение¶
Все популярные стратегии отвечают на один вопрос: сколько долгоживущих веток держать и как долго изменение может жить отдельно от основной ветки. Чем больше долгоживущих веток, тем больше контроля над тем, что попадает в релиз. Платить за это приходится поздней интеграцией, а с ней растут конфликты.
flowchart LR
TBD["Trunk-Based\nтолько main"]
GHF["GitHub Flow\nmain + PR-ветки"]
GLF["GitLab Flow\nmain + ветки окружений\nили релизов"]
RB["Release branches\nmain + ветка на версию"]
GF["GitFlow\nmain + develop +\nfeature/release/hotfix"]
TBD --> GHF --> GLF --> RB --> GF
Слева направо растёт число долгоживущих веток и время до интеграции, а частота релизов, которую стратегия комфортно поддерживает, падает.
| Trunk-Based | GitHub Flow | GitLab Flow | Release branches | GitFlow | |
|---|---|---|---|---|---|
| Долгоживущие ветки | main |
main |
main + production/pre-production или X-Y-stable |
main + release/X.Y |
main + develop |
| Жизнь рабочей ветки | Часы, максимум 1–2 дня | Дни | Дни | Дни | Дни–недели |
| Откуда релиз | Из main (тег) |
Из main сразу после merge |
Из ветки окружения / стабильной ветки | Из release/X.Y |
Из release/* → main + тег |
| Незаконченная работа | В main, но за feature flag |
Не мержится, пока не готова | Не мержится, пока не готова | В main за флагом, в релиз не попадает |
Живёт в feature/* и develop |
| Hotfix | Fix в main → деплой (или cherry-pick в релизную ветку) |
Обычный PR в main |
Fix в main → cherry-pick вниз по веткам |
Fix в main → cherry-pick в release/X.Y |
hotfix/* от main → merge в main и develop |
| Требования к CI | Очень высокие: main обязан быть зелёным всегда |
Высокие | Средние–высокие | Средние | Средние |
| Подходит для | SaaS, Continuous Deployment, зрелые команды | Веб-сервисы, небольшие и средние команды | Сервисы с явными окружениями или несколькими поддерживаемыми версиями | Продукты, где нужно поддерживать N версий параллельно | Коробочный софт, мобильные приложения, релизы по расписанию |
Ключевое отличие: trunk-based и GitHub Flow изолируют незаконченную работу в коде (feature flags, маленькие шаги), а GitFlow изолирует её в ветках. Изоляция в коде требует дисциплины и хорошего CI, зато интеграция происходит постоянно. Изоляция в ветках снимает часть требований к CI, но откладывает интеграцию на потом, а чем дольше она откладывается, тем дороже обходится.
Что говорит исследование DORA¶
DORA включает trunk-based development в число технических практик, которые статистически связаны с более высокой производительностью доставки (см. DORA-метрики в 02-cicd-manual). Критерии, по которым DORA определяет, что команда действительно работает в trunk-based:
- Не больше трёх активных веток в репозитории.
- Ветки вливаются в trunk минимум раз в день.
- Нет code freeze и отдельных «фаз интеграции».
Даже сам автор GitFlow, Винсент Дриссен, в 2020 году дописал к своей статье 2010 года «заметку-рефлексию»: для непрерывно доставляемого веб-софта он советует смотреть в сторону более простых моделей вроде GitHub Flow, а GitFlow оставить для явно версионируемого ПО, где параллельно поддерживается несколько версий.
Архитектура: путь изменения от задачи до production¶
Ниже — путь одного изменения в типичном современном workflow (trunk-based / GitHub Flow с merge queue). Все остальные стратегии — вариации этой схемы с дополнительными долгоживущими ветками.
flowchart LR
T["Задача\nв трекере"] --> B["Короткая ветка\nfeat/PROJ-123-..."]
B --> PR["Pull Request"]
PR --> CI["CI на PR\nlint, тесты, commitlint"]
PR --> R["Review\nCODEOWNERS"]
CI --> G{"Правила защиты\nвыполнены?"}
R --> G
G -- Нет --> B
G -- Да --> MQ["Merge queue\nпроверка на свежем main"]
MQ --> M["main\nвсегда зелёный"]
M --> CD["Пайплайн CD\nсм. 02-cicd-manual"]
CD --> P["Production\nфича за флагом"]
M -.->|"релиз по готовности"| TAG["Тег vX.Y.Z\n+ changelog"]
Здесь видно, где проходят границы ответственности:
- Разработчик отвечает за размер и содержание ветки и PR.
- Платформа (правила защиты веток) не пропустит изменение без зелёного CI и нужных одобрений. Это не пожелание, а техническое ограничение.
- CI проверяет PR, а затем, через merge queue, ещё раз проверяет
результат слияния с актуальным
main. - CD забирает каждый коммит в
main(или каждый тег) и катит артефакт по окружениям, как описано в 02-cicd-manual. - Feature flag решает, увидят ли пользователи изменение, которое уже лежит в production.
Какие события Git запускают какие пайплайны¶
Стратегия ветвления на практике — это ещё и карта триггеров CI/CD:
flowchart TD
subgraph Events["События в репозитории"]
E1["pull_request\nоткрыт или обновлён"]
E2["merge_group\nPR в merge queue"]
E3["push в main"]
E4["push тега v*"]
end
subgraph Pipelines["Что запускается"]
P1["Быстрые проверки:\nlint, unit, commitlint"]
P2["Те же проверки\nна результате слияния"]
P3["Сборка артефакта,\nдеплой в staging"]
P4["Релиз: production,\nGitHub Release"]
end
E1 --> P1
E2 --> P2
E3 --> P3
E4 --> P4
В GitLab роль merge_group выполняют pipelines for merge trains, а роль
pull_request — merge request pipelines ($CI_PIPELINE_SOURCE ==
"merge_request_event").
Компоненты процесса подробно¶
1. Ветки: основная, рабочие, именование¶
Основная ветка (main) — единственная ветка, которая обязана быть в
рабочем состоянии всегда. «Рабочее» значит: собирается, тесты зелёные,
можно выпустить релиз прямо сейчас. Если main красный, это инцидент
уровня «все бросают дела», потому что сломанный main блокирует всех.
Рабочие ветки — короткоживущие ветки под одно изменение. Хорошее
правило: ветка живёт дни, а не недели. В строгом trunk-based — не
дольше 1–2 дней (так рекомендует trunkbaseddevelopment.com). Чем короче
жизнь ветки, тем меньше расхождение с main и тем дешевле слияние (механику
этого эффекта см. в 00-git-manual).
Именование — не косметика: по префиксу ветки CI может выбирать поведение, а по номеру задачи трекер автоматически связывает ветку, PR и тикет. Типичная схема:
<тип>/<ключ-задачи>-<краткое-описание>
feat/PROJ-123-oauth-login
fix/PROJ-456-null-in-invoice-total
chore/bump-node-24
hotfix/PROJ-789-payment-timeout # только если стратегия предусматривает hotfix-ветки
Префиксы удобно совместить с типами Conventional Commits (см. ниже) — одна система классификации вместо двух.
2. Pull Request: размер и описание¶
PR — единица review и единица интеграции. От его размера зависит почти всё остальное: качество review, время до merge, вероятность конфликтов, лёгкость отката.
Размер. Классическое исследование code review в Cisco (опубликовано SmartBear) показало, что эффективность поиска дефектов резко падает, если за раз смотреть больше 200–400 строк. Руководство Google по code review тоже настаивает на маленьких изменениях: их быстрее смотрят, внимательнее проверяют и проще откатывают. Практический ориентир для команды:
| Размер PR (изменённые строки, без сгенерированных файлов) | Что происходит на review |
|---|---|
| до ~200 | Читают внимательно, review за один подход |
| 200–400 | Ещё можно нормально проверить, но уже за время |
| 400+ | Внимание падает, review становится формальным: «LGTM» (Looks Good To Me, «выглядит нормально») не глядя |
Как держать PR маленькими:
- Разделять рефакторинг и изменение поведения. Сначала PR «переименовал и переложил», потом PR «поменял логику». Смешанный diff невозможно отревьюить.
- Вливать незаконченную фичу за feature flag, а не копить её в ветке.
- Stacked PRs — цепочка зависимых маленьких PR, где каждый следующий основан на предыдущем. Их смотрят и вливают по очереди.
- Отдельные PR для механических изменений (обновление зависимостей, автоформатирование): их проверяют по-другому, и они не должны прятать логику.
Описание PR должно отвечать на три вопроса: что изменилось, зачем
(ссылка на задачу, контекст) и как проверить. Шаблон
.github/pull_request_template.md (в GitLab —
.gitlab/merge_request_templates/*.md) превращает это в привычку:
## Что и зачем
<!-- Суть изменения и ссылка на задачу: PROJ-123 -->
## Как проверить
<!-- Шаги, команды, скриншоты — всё, что поможет ревьюеру -->
## Чек-лист
- [ ] Изменение за feature flag (если это незаконченная фича)
- [ ] Миграции БД обратно совместимы
- [ ] Документация / runbook обновлены
Draft PR (черновик) — PR, который открыт для обсуждения и прогона CI, но явно не готов к merge. Открывайте draft рано: ранний фидбек по подходу дешевле, чем переписывание готового кода.
3. Code review: ревьюеры, одобрения и CODEOWNERS¶
Цель review — не «найти опечатки» (это работа линтера), а проверить решение, понять риски и распространить знание о коде в команде. Отсюда правила:
- Всё, что можно автоматизировать, делает CI: форматирование, стиль, типы, простые баги, секреты. Человек смотрит на логику, архитектуру, безопасность и понятность.
- Количество обязательных одобрений — обычно 1, для критичных путей 2. Больше двух обычно только растягивает очередь и размывает ответственность: каждый думает, что внимательно смотрел кто-то другой.
- Скорость ответа важнее идеальности. Первая реакция на PR в течение рабочего дня — разумная командная норма; PR, ждущий review три дня, неизбежно устаревает и обрастает конфликтами.
CODEOWNERS — файл, сопоставляющий пути в репозитории с ответственными людьми или командами. Когда PR затрагивает эти пути, платформа автоматически назначает владельцев ревьюерами, а при включённом правиле «Require review from Code Owners» (GitHub) или соответствующей настройке защищённой ветки (GitLab) без их одобрения merge невозможен.
Важные детали поведения на GitHub:
- Файл ищется в
.github/, затем в корне репозитория, затем вdocs/; используется первый найденный. - Внутри файла побеждает последнее совпавшее правило, поэтому общие шаблоны пишут вверху, а специфичные ниже.
- Владельцы должны иметь права записи в репозиторий, иначе правило молча не работает.
В GitLab CODEOWNERS поддерживает разделы: [Раздел] — обязательный раздел,
^[Раздел] — опциональный. Обязательное одобрение Code Owners и approval
rules в GitLab доступны на платных тарифах (Premium и выше).
Пример файла — в разделе практических примеров.
4. Защита веток и обязательные проверки¶
Всё описанное выше — договорённости. Защита веток превращает их в техническое правило, которое нельзя обойти «один раз, очень срочно».
На GitHub есть два механизма: классические branch protection rules и более новые repository rulesets. Rulesets можно применять к нескольким веткам и тегам по шаблону, задавать на уровне организации и описывать JSON'ом (а значит, хранить как код). Оба механизма могут действовать одновременно, и тогда применяется самый строгий вариант каждого правила.
Минимальный набор правил для main (названия опций — как в интерфейсе
GitHub):
| Правило | Зачем |
|---|---|
| Require a pull request before merging | Запрещает прямой push: всё идёт через review и CI |
| Require approvals (1–2) | Хотя бы один человек кроме автора видел изменение |
| Dismiss stale pull request approvals when new commits are pushed | Одобрение относится к конкретному коду; после новых коммитов его надо получить заново |
| Require approval of the most recent reviewable push | Автор последнего push не может сам «одобрить» свои последние изменения чужим старым approve |
| Require review from Code Owners | Изменения в критичных путях видят те, кто за них отвечает |
| Require status checks to pass before merging | Красный CI блокирует merge |
| Require conversation resolution before merging | Замечания review не теряются в свёрнутых комментариях |
| Require linear history | Запрещает merge-коммиты в ветке, остаются squash и rebase |
| Запрет force push и удаления ветки | История main не переписывается, ветку не удалить случайно (в классической защите force push запрещён по умолчанию, разрешается опцией «Allow force pushes») |
В GitLab аналог — Protected branches (кто может push/merge) плюс настройки merge request: «Pipelines must succeed», approval rules и обязательные одобрения Code Owners.
Обязательные проверки (required status checks) — названия job'ов CI,
которые должны быть зелёными. Тонкость: чтобы правило работало, job
должен реально запускаться на каждом PR. Если workflow с этим job'ом отфильтрован по путям
(on.pull_request.paths) и не запустился, GitHub будет вечно ждать его статуса, и PR
застрянет. Решение — фильтровать по путям шаги внутри job'а, а не сам
запуск, или завести один «итоговый» job, который зависит от остальных
(needs:) и всегда отчитывается.
Проблема «зелёный PR + зелёный PR = красный main»¶
Два PR по отдельности проходят CI, но каждый проверялся на старом
main. После слияния обоих результат может не собраться: например,
один PR переименовал функцию, а второй добавил её новый вызов по старому
имени. Два способа защиты:
- «Require branches to be up to date before merging» — перед merge PR
обязан содержать актуальный
main. Работает, но не масштабируется: при десятке PR в день каждый merge заставляет все остальные обновляться и перезапускать CI, и разработчики бесконечно жмут «Update branch». - Merge queue (GitHub) / merge trains (GitLab Premium). Одобренные PR
встают в очередь, платформа сама создаёт временную ветку «
main+ этот PR + PR впереди в очереди», прогоняет на ней CI и вливает, только если результат зелёный. На GitHub это отдельное событие workflowmerge_group, и обязательные проверки должны запускаться и на нём, иначе очередь не продвинется. Merge queue доступна в публичных репозиториях организаций и в приватных репозиториях на GitHub Enterprise Cloud.
flowchart LR
A["PR A одобрен"] --> Q["Merge queue"]
B["PR B одобрен"] --> Q
Q --> T1["Временная ветка\nmain + A"]
Q --> T2["Временная ветка\nmain + A + B"]
T1 --> C{"CI зелёный?"}
T2 --> C
C -- Да --> M["Слияние в main\nпо очереди"]
C -- Нет --> X["PR выбывает из очереди,\nостальные перепроверяются"]
5. Метод слияния PR: merge commit, squash или rebase¶
Техническая механика трёх методов разобрана в 00-git-manual. Здесь вопрос другой: какой из них сделать командным правилом и как это настроить.
| Метод | GitHub | GitLab | Что в main |
Когда выбирать |
|---|---|---|---|---|
| Merge commit | «Merge pull request» (в настройках — Allow merge commits) | Merge commit | Все коммиты ветки + merge-коммит | Ветки, над которыми работали несколько человек и чья внутренняя история ценна; вливание release/* обратно в main |
| Squash | «Squash and merge» | Опция «Squash commits when merging» при любом методе | Один коммит на PR | Дефолт для большинства команд: «один PR = один коммит = одна запись в changelog», черновой шум ветки не попадает в историю |
| Rebase | «Rebase and merge» | Fast-forward merge (ветку надо предварительно перебазировать) | Коммиты ветки поштучно, линейно, без merge-коммита | Команды, которые приучены делать атомарные коммиты с хорошими сообщениями и хотят сохранить их в main |
GitLab дополнительно предлагает «Merge commit with semi-linear history»: merge-коммит создаётся, но только если ветка перебазирована на актуальную цель. Получается история, которая читается линейно, но с явными границами MR.
Практическая рекомендация для команды, которая только выбирает: squash merge + заголовок PR в формате Conventional Commits. Тогда:
- заголовок PR становится сообщением коммита в
main, и его можно проверять линтером в CI (см. пример 3); git revertодного коммита откатывает целый PR;- инструменты релиза считают версию и changelog по одному коммиту на PR.
Выбранный метод стоит закрепить в настройках репозитория, отключив остальные. Если оставить все три кнопки, в истории неизбежно окажется смесь стилей, и автоматика релизов начнёт ошибаться.
6. Соглашения о коммитах, версии, теги и changelog¶
Conventional Commits¶
Conventional Commits (спецификация 1.0.0) —
формат сообщения type(scope)!: описание. Для workflow важно, что формат
машиночитаем, поэтому версию можно вычислить автоматически:
| Коммит | Что означает | Влияние на версию (SemVer) |
|---|---|---|
fix(api): handle empty invoice list |
Исправление бага | PATCH: 1.4.2 → 1.4.3 |
feat(auth): add OAuth login |
Новая функциональность | MINOR: 1.4.2 → 1.5.0 |
feat(api)!: drop v1 endpoints или футер BREAKING CHANGE: ... |
Несовместимое изменение | MAJOR: 1.4.2 → 2.0.0 |
chore:, docs:, refactor:, test:, ci: |
Служебные изменения | Обычно не меняют версию |
Ломающее изменение обозначается ! после типа/scope и/или футером
BREAKING CHANGE: (синоним — BREAKING-CHANGE:). Типы кроме feat и
fix спецификация не фиксирует, их набор задаёт соглашение команды
(чаще всего — @commitlint/config-conventional).
Semantic Versioning¶
SemVer 2.0.0 — версия вида MAJOR.MINOR.PATCH:
- MAJOR — несовместимые изменения публичного API;
- MINOR — новая обратно совместимая функциональность;
- PATCH — обратно совместимые исправления.
Две детали, о которых часто забывают. Во-первых, для версий 0.y.z по
спецификации «всё может поменяться в любой момент»: пока не вышла 1.0.0,
стабильного API не обещано. Во-вторых, pre-release обозначается через дефис:
2.0.0-rc.1 считается младше 2.0.0.
Для внутреннего веб-сервиса без внешних потребителей SemVer не обязателен: многие команды версионируют такой сервис датой или SHA коммита. Для библиотек, API, Helm-чартов и всего, чем пользуются другие, SemVer — фактический стандарт.
Теги и changelog¶
Релиз фиксируется annotated-тегом vX.Y.Z на коммите в main (или
в релизной ветке). Разница annotated и lightweight-тегов и подпись тегов
описаны в 00-git-manual.
Push тега — стандартный триггер релизного пайплайна.
Changelog — человекочитаемый список изменений по версиям. Писать его руками тяжело, и он неизбежно отстаёт от кода. При Conventional Commits его генерирует инструмент релиза (release-please, semantic-release, git-cliff), группируя коммиты по типам. Правило: тег, changelog и номер версии создаёт автоматика, а не человек в конце спринта. Тогда они никогда не расходятся друг с другом.
7. Feature flags: отделить деплой от релиза¶
Feature flag — ключевой элемент trunk-based
development. Без флагов незаконченная фича должна жить в ветке, и мы
возвращаемся к долгоживущим веткам. С флагами код фичи вливается в main
маленькими PR и даже доезжает до production, но остаётся выключенным, пока
его не включат.
Так появляется важное различие:
- Деплой — техническое событие: новая версия кода запущена в production.
- Релиз — продуктовое событие: пользователи получили новую функциональность.
Флаги бывают разными, и у каждого вида свой срок жизни:
| Тип флага | Назначение | Срок жизни |
|---|---|---|
| Release toggle | Скрыть незаконченную фичу, постепенно включить готовую | Дни–недели, потом удалить |
| Experiment toggle | A/B-тест: разным пользователям разные варианты | Время эксперимента |
| Ops toggle / kill switch | Быстро отключить тяжёлую или рискованную функциональность при инциденте | Может жить долго |
| Permission toggle | Функциональность для отдельных групп (бета-тестеры, внутренние пользователи) | Долго |
Минимальный пример на OpenFeature — вендор-нейтральном стандарте API для флагов (проект CNCF со статусом incubating). Код обращается к стандартному API, а поставщика (flagd, Unleash, LaunchDarkly и др.) можно заменить без переписывания вызовов:
import { OpenFeature } from '@openfeature/server-sdk';
import { FlagdProvider } from '@openfeature/flagd-provider';
// поставщик флагов настраивается один раз при старте приложения
await OpenFeature.setProviderAndWait(new FlagdProvider());
const flags = OpenFeature.getClient();
export async function checkout(user: { id: string }) {
// второй аргумент — значение по умолчанию: если сервис флагов
// недоступен, фича остаётся ВЫКЛЮЧЕННОЙ, а не падает весь checkout
const useNewFlow = await flags.getBooleanValue('new-checkout-flow', false, {
targetingKey: user.id, // для процентного выката одному и тому же пользователю
});
return useNewFlow ? newCheckout(user) : legacyCheckout(user);
}
Флаги — это технический долг с отсроченным платежом. Каждый release toggle удваивает число путей выполнения, которые надо тестировать. Забытые флаги годами висят в коде, и никто не помнит, можно ли их удалить. Правило: задача на удаление флага создаётся одновременно с флагом, а у флага есть владелец и ожидаемая дата удаления.
Флаги подходят не для всего. Изменение схемы БД или ломающее изменение
API флагом не спрятать; для них нужны паттерны expand/contract («сначала расширить схему или API, перевести код, потом сузить», см.
раздел про откат в 02-cicd-manual) и
branch by abstraction. Во втором случае в коде сначала появляется
абстракция над старой реализацией, затем за ней постепенно пишется новая,
и в конце старая удаляется. Каждый шаг при этом отдельный маленький PR в
main.
8. Конфликты и долгоживущие ветки¶
Как разрешать отдельный конфликт, объяснено в 00-git-manual. На уровне команды задача другая: сделать так, чтобы конфликтов было мало и они были маленькими.
Откуда берутся тяжёлые конфликты:
- Долгоживущие ветки. Расхождение с
mainрастёт с каждым днём, а сложность слияния растёт быстрее, чем линейно. - Большие механические изменения (массовое переименование, автоформатирование всего проекта), сделанные параллельно с обычной работой.
- Горячие файлы: общий конфиг, реестр роутов, файл зависимостей, который правят все.
Что с этим делает команда:
- Ежедневная синхронизация с
main(rebaseсвоей ветки илиmergemainв неё — по договорённости) и ежедневная интеграция вmain. - Механические изменения делаются отдельным PR и анонсируются: «в 14:00
вливаем форматирование, после этого обновите ветки». Такие коммиты
стоит добавить в
.git-blame-ignore-revs, чтобыgit blameих пропускал. - Архитектура против горячих файлов: регистрация через каталоги и автообнаружение вместо одного общего списка, в котором правят все.
git rerere(git config --global rerere.enabled true): Git запоминает, как вы разрешили конфликт, и при повторном таком же конфликте (например, при нескольких rebase подряд) применяет решение сам.
Если долгоживущая ветка уже есть (например, крупная миграция), её нужно
регулярно, минимум раз в день, синхронизировать с main, а не ждать
финального слияния. Ещё лучше — перестроить работу: вливать в main уже
готовые части за флагом или через branch by abstraction, чтобы ветка
укорачивалась, а не росла.
9. Hotfix: срочное исправление в production¶
Hotfix — тот момент, когда workflow проверяется на прочность: всё срочно, все нервничают, хочется «просто поправить на сервере». Процесс должен быть заранее описан и отрепетирован, а не придуман во время инцидента.
Главное правило: исправление должно попасть в основную ветку. Иначе следующий обычный релиз вернёт баг обратно: это регрессия, которую команда сама себе устроила.
Путь hotfix зависит от стратегии:
- Trunk-based / GitHub Flow, релиз из
main. Hotfix — это обычный PR вmainс приоритетным review. Если вmainуже лежат неготовые к релизу изменения, они выключены флагами, и выпуститьmainцеликом безопасно. Именно поэтому в TBD отдельный hotfix-процесс почти не нужен. - Релизные ветки (release branches, GitLab Flow). Fix делается в
mainи затем cherry-pick'ается в поддерживаемую релизную ветку, не наоборот. Если сделать fix только в релизной ветке, его легко забыть перенести вmain. - GitFlow. Ветка
hotfix/*отmain, после исправления она вливается и вmain(с тегом новой PATCH-версии), и вdevelop.
flowchart LR
M1["main: fix(payments):\nretry on timeout"] -->|"git cherry-pick -x"| R1["release/1.4:\nтот же fix"]
R1 --> T1["Тег v1.4.3"]
T1 --> P1["Деплой v1.4.3\nв production"]
M1 --> N["main: попадёт в\nследующий релиз 1.5.0"]
# fix уже влит в main через обычный PR; переносим его в релизную ветку
git switch release/1.4
git pull --ff-only
# -x дописывает в сообщение "(cherry picked from commit <sha>)" —
# по нему потом видно, откуда пришло изменение
git cherry-pick -x 3f9c2ab
git push origin release/1.4 # лучше тоже через PR, если ветка защищена
git tag -a v1.4.3 -m "fix(payments): retry on timeout"
git push origin v1.4.3 # тег запускает релизный пайплайн
Hotfix не отменяет правила защиты веток. Для срочных случаев правильнее иметь быстрый путь (дежурный ревьюер, приоритетная очередь), чем выдавать кому-то право обходить защиту: такое право со временем начнут использовать и для несрочного.
Инструменты¶
| Категория | Инструменты |
|---|---|
| Git-хостинг и code review | GitHub, GitLab, Bitbucket, Gitea / Forgejo, Gerrit |
| Защита веток | GitHub rulesets и branch protection rules, GitLab protected branches и approval rules |
| Очереди слияния | GitHub merge queue, GitLab merge trains, Mergify |
| Stacked PRs | Graphite, ghstack, spr |
| Проверка сообщений коммитов | commitlint, commitizen |
| Git-хуки локально | pre-commit, husky, lefthook |
| Автоматизация релизов и changelog | release-please, semantic-release, changesets, git-cliff |
| Feature flags | OpenFeature (стандарт API), flagd, Unleash, Flagsmith, GrowthBook, LaunchDarkly, GitLab Feature Flags |
| Автоматические PR с обновлениями зависимостей | Renovate, Dependabot |
| Аналитика процесса (PR cycle time и т. п.) | GitLab Value Stream Analytics, GitHub Insights, LinearB, Swarmia |
Инструменты из этой таблицы не выбирают сами по себе. Сначала команда договаривается о процессе (стратегия, метод слияния, формат коммитов), а потом подбирает инструменты, которые этот процесс автоматизируют и закрепляют.
Практические примеры конфигураций¶
Пример 1: CODEOWNERS для сервиса с инфраструктурой¶
Файл .github/CODEOWNERS:
# Порядок важен: при нескольких совпадениях побеждает ПОСЛЕДНЕЕ правило,
# поэтому общее — сверху, специфичное — снизу.
# Владелец по умолчанию для всего репозитория
* @acme/backend
# Инфраструктура и пайплайны — только с одобрения платформенной команды
/.github/workflows/ @acme/platform
/deploy/ @acme/platform
/terraform/ @acme/platform
Dockerfile @acme/platform
# Платёжный модуль — владельцы домена плюс безопасность
/src/payments/ @acme/payments @acme/security
# Сам CODEOWNERS тоже защищаем: иначе любой может вписать себя владельцем
/.github/CODEOWNERS @acme/platform
Три вещи, на которых обычно ошибаются:
- Команды (
@acme/platform) должны иметь права записи в репозиторий, иначе строка не действует. - Файл сам по себе только назначает ревьюеров. Чтобы их одобрение стало обязательным, нужно правило «Require review from Code Owners» (см. пример 2).
- Последняя строка — защита от эскалации: без неё CODEOWNERS можно поменять в том же PR, который он должен контролировать.
Для GitLab та же идея с разделами (CODEOWNERS в корне, в docs/ или в
.gitlab/; используется первый найденный в этом порядке):
[Platform][1] @acme/platform
/deploy/
/terraform/
.gitlab-ci.yml
^[Docs] @acme/tech-writers
/docs/
[Platform][1] — обязательный раздел, требующий одного одобрения;
^[Docs] — опциональный: владельцев назначат ревьюерами, но merge без них
не заблокируется.
Пример 2: ruleset для main как код (GitHub)¶
Правила защиты, настроенные мышкой в интерфейсе, не видны на review и не воспроизводятся на другом репозитории. Ruleset можно хранить в репозитории платформенной команды как JSON и применять через API:
{
"name": "main-protection",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["~DEFAULT_BRANCH"],
"exclude": []
}
},
"rules": [
{ "type": "deletion" },
{ "type": "non_fast_forward" },
{ "type": "required_linear_history" },
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 1,
"dismiss_stale_reviews_on_push": true,
"require_code_owner_review": true,
"require_last_push_approval": true,
"required_review_thread_resolution": true,
"allowed_merge_methods": ["squash"]
}
},
{
"type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": false,
"required_status_checks": [
{ "context": "test" },
{ "context": "pr-title" }
]
}
}
]
}
# применить к репозиторию (нужны права администратора репозитория)
gh api --method POST repos/acme/checkout-service/rulesets --input main-protection.json
# посмотреть действующие правила
gh api repos/acme/checkout-service/rulesets
Что здесь происходит:
~DEFAULT_BRANCH— правило следует за веткой по умолчанию, даже если её переименуют.non_fast_forwardзапрещает force push,deletion— удаление ветки.required_linear_historyвместе сallowed_merge_methods: ["squash"]закрепляют метод слияния из раздела 5.strict_required_status_checks_policy: false— это «Require branches to be up to date» в выключенном виде. Актуальность относительноmainпроверяет merge queue (её включают отдельным правиломmerge_queue, если она доступна на вашем тарифе).- Имена в
required_status_checksдолжны точно совпадать с именами job'ов из примера 3.
Пример 3: проверки на PR и в merge queue (GitHub Actions)¶
name: PR checks
on:
pull_request:
branches: [main]
types: [opened, edited, synchronize, reopened] # edited — чтобы перепроверять смену заголовка
merge_group: # запуск внутри merge queue
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7 # актуальная major-версия на 2026-09; в проде пиньте по SHA
- uses: actions/setup-node@v7
with:
node-version: '24'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test
pr-title:
# при squash merge заголовок PR становится сообщением коммита в main,
# поэтому проверяем именно его. В merge queue заголовка нет — job
# пропускается, а пропущенный job засчитывается как успешный.
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '24'
cache: 'npm'
- run: npm ci
- name: Lint PR title (Conventional Commits)
env:
# заголовок PR — пользовательский ввод: передаём через env,
# а не подставляем ${{ }} прямо в run (иначе возможна инъекция команд)
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "$PR_TITLE" | npx commitlint --verbose
pr-size:
# мягкое предупреждение, а не блокировка: иногда большой PR оправдан
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Warn on large PR
env:
ADDITIONS: ${{ github.event.pull_request.additions }}
DELETIONS: ${{ github.event.pull_request.deletions }}
run: |
total=$((ADDITIONS + DELETIONS))
echo "Changed lines: $total"
if [ "$total" -gt 400 ]; then
echo "::warning::PR изменяет $total строк. Подумайте, можно ли разбить его на части."
fi
Конфиг commitlint в корне репозитория (commitlint.config.mjs; с расширением .js синтаксис export default работает только при "type": "module" в package.json), зависимости
ставятся через npm install --save-dev @commitlint/cli
@commitlint/config-conventional:
export default {
extends: ['@commitlint/config-conventional'],
rules: {
// заголовки PR бывают длиннее «идеальных» 50 символов — даём запас
'header-max-length': [2, 'always', 100],
},
};
Ключевые моменты:
- Событие
merge_groupобязательно, если включена merge queue: без него обязательная проверкаtestв очереди никогда не отчитается, и PR зависнет. pr-sizeне входит в required checks и только предупреждает. Жёсткий лимит на размер обычно приводит к тому, что PR режут искусственно.- Тот же commitlint стоит повесить и на локальный хук
commit-msg(husky, lefthook, pre-commit): ошибку дешевле поймать до push.
Пример 4: автоматический релиз из Conventional Commits (release-please)¶
name: Release
on:
push:
branches: [main]
permissions:
contents: write # создавать теги и GitHub Release
pull-requests: write # открывать и обновлять релизный PR
issues: write # создавать и менять метки релизного PR (как в официальном README)
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v5 # актуальная major-версия на 2026-09; в проде пиньте по SHA
with:
release-type: node # берёт версию из package.json и обновляет её
Как это работает:
- После каждого push в
mainrelease-please читает коммиты с последнего релиза и открывает (или обновляет) релизный PR: в нём новая версия, вычисленная по Conventional Commits, и обновлённыйCHANGELOG.md. - Команда вливает релизный PR, когда хочет выпустить версию. Это и есть ручные ворота Continuous Delivery, выраженные через обычный PR.
- После слияния release-please создаёт тег
vX.Y.Zи GitHub Release. Тег может запускать релизный пайплайн (но см. ловушку сGITHUB_TOKENниже) — тот же, что описан в 02-cicd-manual, только с триггеромon: push: tags: ['v*'].
Важная ловушка: теги и релизы, созданные с GITHUB_TOKEN, не запускают
другие workflow (это защита GitHub от бесконечных циклов). Если релизный
пайплайн должен стартовать от тега, созданного release-please, либо
передайте action токен GitHub App или PAT (Personal Access Token; параметр token), либо
вызовите деплой в том же workflow по выходному значению
releases_created.
Стратегии ветвления подробно¶
Trunk-Based Development¶
flowchart LR
M1["main"] --> M2["main"] --> M3["main"] --> M4["main"] --> M5["main\nтег v1.5.0"]
M1 -.-> F1["feat/a\nменьше дня"] -.-> M2
M2 -.-> F2["fix/b\nнесколько часов"] -.-> M3
M3 -.-> F3["feat/c, часть 1\nза флагом"] -.-> M4
Все интегрируются в main минимум раз в день через очень короткие ветки
(в маленьких командах иногда даже прямыми коммитами в trunk). Незаконченная
работа живёт в main за feature flags. Релиз — это тег на любом зелёном
коммите main, а при Continuous Deployment релизом становится каждый
коммит.
Требует: быстрого и надёжного CI (иначе main постоянно красный),
feature flags, культуры маленьких PR и быстрых review. Даёт: почти
полное отсутствие merge hell, минимальный Lead Time и прямой путь к
Continuous Deployment из
02-cicd-manual.
GitHub Flow¶
flowchart LR
M1["main"] --> B["feature-ветка\nот main"] --> PR["Pull Request\nreview + CI"] --> MG["Merge в main"] --> D["Деплой\nиз main"]
D -.->|"следующее изменение"| M1
Одно правило: всё, что в main, можно деплоить. Любая работа — ветка от
main → PR → review и CI → merge → деплой. По сути это trunk-based, где
ветки живут чуть дольше (дни) и незаконченную работу чаще держат в ветке,
а не за флагом. Хороший выбор по умолчанию для веб-сервиса и небольшой
команды.
GitLab Flow¶
GitLab Flow добавляет к GitHub Flow долгоживущие ветки там, где это продиктовано реальностью: деплой происходит не в момент merge, или нужно поддерживать несколько версий. Существует в двух вариантах.
С ветками окружений — изменения «продвигаются» вниз по веткам:
flowchart TD
subgraph Main["main"]
A["Все PR вливаются сюда"]
end
subgraph PreProd["pre-production"]
B["Деплой в staging"]
end
subgraph Prod["production"]
C["Деплой в production"]
end
A -->|"merge / cherry-pick\nкогда готово к staging"| B
B -->|"merge после проверки"| C
С релизными ветками (2-3-stable, 2-4-stable) — для ПО, которое
поставляется версиями. Правило то же, что у release branches ниже:
исправление сначала в main, потом cherry-pick в стабильные ветки
(upstream first).
Предостережение про ветки окружений: в мире неизменяемых артефактов из
02-cicd-manual они часто лишние.
Если по окружениям едет один и тот же образ, «продвижение» удобнее
делать сменой тега образа в конфигурации окружения (или в GitOps-репозитории),
а не слиянием веток кода. Merge из main в production пересобирает
артефакт из другой ветки, и тогда в production едет не то, что проверили
на staging.
Release branches (релизные ветки)¶
flowchart LR
M1["main"] --> M2["main"] --> M3["main"] --> M4["main"]
M2 --> R1["release/1.4\nсоздана при релизе"]
R1 --> T1["v1.4.0"]
R1 --> R2
M3 -->|"cherry-pick -x\nfix из main"| R2["release/1.4"]
R2 --> T2["v1.4.1"]
Разработка идёт в main по trunk-based. В момент релиза от main
создаётся release/X.Y, дальше в неё попадают только исправления,
перенесённые из main через cherry-pick. Новые фичи туда не попадают.
Ветка живёт, пока версия поддерживается.
Подходит, когда пользователи сидят на разных версиях: мобильные
приложения (ревью в сторе занимает дни), коробочный и on-premise софт,
библиотеки с LTS-версиями. Ветка создаётся от main, но никогда не
вливается обратно. Всё, что в ней есть, уже было в main.
GitFlow¶
flowchart LR
D1["develop"] --> F["feature/*"] --> D2["develop"]
D2 --> R["release/1.5\nстабилизация"]
R --> M["main\nтег v1.5.0"]
R -->|"merge обратно"| D3["develop"]
M --> H["hotfix/1.5.1"]
H --> M2["main\nтег v1.5.1"]
H -->|"merge обратно"| D3
Две долгоживущие ветки: main (только релизы, каждый коммит — тег) и
develop (интеграция). Фичи — в feature/* от develop. Перед релизом
создаётся release/* для стабилизации, которая вливается и в main, и в
develop. Срочные исправления идут через hotfix/* от main и тоже
вливаются в обе ветки.
Когда оправдан: явные версии с плановым циклом релизов,
сертификация или QA-фаза перед каждым релизом, поставка клиентам, которые
сами решают, когда обновляться. Когда вреден: веб-сервис с частыми
деплоями. Здесь develop — лишний слой, из-за которого интеграция
откладывается, а двойные merge (release и hotfix в две ветки)
регулярно забывают, и исправления теряются.
Как выбрать¶
flowchart TD
Q1{"Нужно поддерживать\nнесколько версий\nодновременно?"}
Q1 -- Да --> Q2{"Релизы по расписанию,\nотдельная QA-фаза?"}
Q2 -- Да --> GF["GitFlow"]
Q2 -- Нет --> RB["Trunk-based +\nrelease branches"]
Q1 -- Нет --> Q3{"CI быстрый и надёжный,\nесть feature flags?"}
Q3 -- Да --> TBD["Trunk-Based\nDevelopment"]
Q3 -- Нет --> GHF["GitHub Flow\nи курс на TBD"]
Стратегию можно менять. Обычный путь зрелости выглядит так: GitFlow → GitHub Flow → trunk-based, по мере того как CI становится быстрее и надёжнее, а команда осваивает флаги и маленькие PR.
Best Practices¶
- Выбирайте самую простую стратегию, которая покрывает ваши реальные ограничения: каждая дополнительная долгоживущая ветка — это ещё одна точка отложенной интеграции и ещё одно место, куда можно забыть влить исправление.
- Короткие ветки: дни, а не недели — сложность слияния растёт быстрее, чем линейно, от времени расхождения, а маленькое изменение ещё и дешевле отревьюить и откатить.
- Маленькие PR (ориентир — до 200–400 строк) — внимание ревьюера на большом diff падает, и review превращается в формальность.
- Правила — в настройках платформы, а не в wiki — защиту веток нельзя «забыть» или обойти в спешке, а договорённость на словах нарушается при первом дедлайне.
- Защита веток как код (rulesets, Terraform-провайдер GitHub/GitLab) — правила видны на review, одинаковы во всех репозиториях и восстанавливаются, если кто-то изменил их вручную.
- Один метод слияния на репозиторий, остальные отключены — от этого зависят читаемость истории и корректность автоматического changelog и версий.
- Conventional Commits + автоматическая версия и changelog — релиз перестаёт быть ручной работой, а тег, версия и список изменений не могут разойтись между собой.
- Merge queue вместо «Require branches to be up to date» при большом потоке PR —
mainостаётся зелёным, и разработчикам не приходится бесконечно обновлять ветки. - Feature flags для незаконченной работы, с владельцем и датой удаления — только так можно интегрироваться ежедневно, не выпуская сырую функциональность, а срок удаления не даёт флагам превратиться в долг.
- Hotfix — через
main(upstream first) — иначе следующий плановый релиз вернёт исправленный баг. - Автоматизируйте в review всё, что можно — время человека уходит на логику и риски, а не на отступы и опечатки, которые найдёт линтер.
- Ответ на PR в течение рабочего дня — ожидание review обычно самая большая часть PR cycle time, и именно оно заставляет разработчиков укрупнять PR («раз всё равно ждать»).
Частые ошибки и антипаттерны¶
| Антипаттерн | Проблема | Решение |
|---|---|---|
| GitFlow «потому что так принято» в веб-сервисе с ежедневными деплоями | Лишний слой develop, отложенная интеграция, забытые обратные merge из release/hotfix |
GitHub Flow или trunk-based; GitFlow только для реально версионируемого ПО |
| Долгоживущие feature-ветки «до готовности фичи» | Merge hell, конфликты на дни, интеграционные баги находятся в самом конце | Короткие ветки, незаконченная работа за feature flag, branch by abstraction |
| PR на тысячи строк | Review формальный («LGTM»), баги проходят, откат затрагивает всё сразу | Разбивать: рефакторинг отдельно от поведения, stacked PRs, ориентир 200–400 строк |
| Защита веток не настроена или у админов есть обход «на всякий случай» | Прямые push в main в обход CI и review — обычно именно в самый стрессовый момент |
Rulesets без исключений; для срочных случаев — быстрый путь review, а не обход |
| Одобрение сохраняется после новых коммитов | Одобрили один код, влили другой | «Dismiss stale approvals» + «Require approval of the most recent reviewable push» |
| Обязательный CI-job с фильтром по путям | Job не запускается, PR навсегда «ждёт» статуса | Фильтровать шаги внутри job'а или завести итоговый job, который всегда отчитывается |
| Разрешены все три метода слияния | Смесь стилей в истории, автоматика релизов считает версии неверно | Один метод на репозиторий, остальные отключить в настройках |
| Hotfix только в релизной ветке или прямо на сервере | Следующий релиз возвращает баг; production расходится с Git | Fix в main, затем cherry-pick -x в релизную ветку; деплой только из Git |
| Ветки окружений, из которых артефакт пересобирается | В production едет не тот артефакт, который проверили на staging | Один immutable-артефакт; продвижение — сменой версии в конфигурации окружения |
| Флаги, которые никто не удаляет | Комбинаторный взрыв путей выполнения, мёртвый код, никто не помнит, что безопасно выключать | Задача на удаление при создании флага, владелец и срок жизни, регулярный аудит |
| Метрики процесса как KPI отдельных людей | Люди оптимизируют цифру: дробят PR бессмысленно, одобряют не читая | Метрики только на уровне команды и только как повод для ретроспективы |
| Ручной changelog и ручное присвоение версий | Changelog отстаёт, версии не отражают характер изменений, релиз зависит от одного человека | Conventional Commits + release-please / semantic-release |
Метрики эффективности¶
Результат доставки измеряют DORA-метрики из
02-cicd-manual, а
Git-workflow определяет их первую часть: сколько времени изменение проводит
между первым коммитом и попаданием в main. В 00-git-manual
уже есть метрики гигиены репозитория (размер коммита, время жизни ветки).
Здесь — метрики самого командного процесса.
PR cycle time и его составляющие¶
PR cycle time — время от открытия PR (или от первого коммита в ветке)
до его слияния в main. Одно число полезно только для тренда. Чтобы
понять, где теряется время, его раскладывают на фазы:
flowchart LR
A["Первый коммит\nв ветке"] -->|"Coding time"| B["PR открыт"]
B -->|"Time to first review\nPickup time"| C["Первый комментарий\nили approve"]
C -->|"Review time\nитерации правок"| D["Approve"]
D -->|"Merge time\nочередь, CI"| E["Merge в main"]
E -->|"Deploy time"| F["Production"]
Весь путь от A до F — это Lead Time for Changes из DORA. Отрезок от B до E — PR cycle time.
| Метрика | Что показывает | Разумный ориентир |
|---|---|---|
| PR cycle time (p50 / p90 — медиана и 90-й перцентиль) | Сколько изменение ждёт от открытия PR до main |
p50 < 1 дня |
| Time to first review | Как быстро команда реагирует на PR; обычно главный источник ожидания | < 4 рабочих часов |
| Review time | Сколько длятся итерации review; большие значения — признак больших PR или неясных требований | Часы, не дни |
| Размер PR (строк, p50 / p90) | Главный управляемый фактор всех остальных метрик | p50 < 200, p90 < 400 |
| Число раундов review на PR | Сколько раз PR возвращали на доработку; много раундов — повод обсуждать подход раньше (draft PR) | 1–2 |
| Время жизни ветки | Насколько поздно происходит интеграция | < 1–2 дней для trunk-based |
| Число активных веток | Критерий TBD по DORA | ≤ 3 активных веток в репозитории (включая рабочие) |
| Доля PR, влитых без review | Работает ли защита веток на самом деле | 0 |
| Доля выбываний из merge queue | Насколько часто PR «ломают друг друга» или CI нестабилен | Низкая, тренд вниз |
| Возраст самого старого открытого PR | Забытые PR, которые постепенно становятся источником конфликтов | Дни, не недели |
Как связаны метрики процесса и DORA¶
flowchart LR
S["Размер PR"] --> RT["Time to first review\nи Review time"]
S --> CF["Риск конфликтов\nи ошибок"]
RT --> CT["PR cycle time"]
BL["Время жизни ветки"] --> CF
CT --> LT["DORA: Lead Time\nfor Changes"]
CT --> DF["DORA: Deployment\nFrequency"]
CF --> CFR["DORA: Change\nFailure Rate"]
Главный рычаг — размер PR. Уменьшение PR ускоряет review, снижает риск конфликтов и ошибок и в итоге улучшает сразу три из четырёх DORA-метрик.
Предупреждение, без которого эти метрики вредны: измеряйте команду, а не людей. Как только PR cycle time становится личным KPI, работает закон Гудхарта: цифры улучшаются, процесс нет. Разработчики дробят PR ради графика, а ревьюеры одобряют не читая. Как и с метриками пайплайна в 02-cicd-manual, смотреть нужно на тренд и обсуждать его на ретроспективе.
Заключение¶
Git-workflow — это договорённость команды о том, как изменения превращаются в релизы, закреплённая в настройках платформы. Хороший workflow делает интеграцию ежедневной и скучной, review быстрым и осмысленным, а релиз и hotfix — предсказуемыми процедурами. Он напрямую определяет, насколько быстрым может быть ваш CI/CD: с неделями жизни веток никакой пайплайн не даст короткий Lead Time.
С чего начать на практике: включите защиту main (PR обязателен, 1
approval, зелёный CI, запрет force push), выберите один метод слияния
(для начала — squash) и добавьте проверку заголовка PR по Conventional
Commits. Затем начните измерять размер PR и time to first review, и уже по
этим цифрам решайте, что улучшать дальше: внедрять merge queue,
автоматизировать релизы через release-please или двигаться к
trunk-based с feature flags.