Git-workflow для команд: квиз для самопроверки¶
Компаньон к мануалу 05 про Git-workflow. Сначала попробуйте ответить на все вопросы самостоятельно, потом сверьтесь с разделом «Ответы». Не подглядывайте раньше времени — так проверка честнее.
Вопросы для самопроверки¶
-
Мануал выделяет четыре слоя, из которых складывается Git-workflow. Назовите их и объясните, почему нельзя, например, выбор метода слияния (squash/rebase/merge commit) назвать «стратегией ветвления».
-
Чем стратегии ветвления принципиально различаются между собой — какой один вопрос отвечает за это различие? Расположите Trunk-Based Development, GitHub Flow и GitFlow по возрастанию числа долгоживущих веток и объясните, чем команда платит за каждую дополнительную ветку.
-
Какие три критерия DORA использует, чтобы определить, что команда реально работает в trunk-based (а не просто называет так свой процесс)?
-
Сценарий. Ваша команда делает мобильное приложение: ревью в сторе занимает несколько дней, и одновременно поддерживаются две последние версии для пользователей, которые ещё не обновились. Какую стратегию ветвления вы предложите и почему GitHub Flow здесь не подойдёт?
-
Объясните разницу между понятиями «деплой» и «релиз». Какой механизм позволяет их разделить, и почему без него trunk-based development потребовал бы возврата к долгоживущим веткам?
-
Почему для команды, которая порождает больше десятка PR в день, правило «Require branches to be up to date before merging» работает хуже, чем merge queue? Что произойдёт, если для merge queue не настроить триггер
merge_groupв CI? -
Сценарий. Ревьюер одобрил PR, после чего автор дотолкал в ветку ещё два коммита с исправлением совсем другой логики, и PR слили без повторного одобрения. Какие два правила защиты веток из мануала должны были это предотвратить и как каждое из них работает?
-
Чем «изоляция незаконченной работы в коде» (feature flags) отличается от «изоляции в ветках» (GitFlow)? Какую цену платит команда за каждый из этих двух подходов?
-
Опишите правильный путь hotfix для стратегии с релизными ветками (release branches). Почему исправление нельзя делать только в релизной ветке и почему порядок именно «сначала в
main, потом cherry-pick», а не наоборот? -
Как Conventional Commits связаны с автоматическим вычислением версии по SemVer? Приведите пример коммита, который должен поднять MAJOR-версию, и объясните, через что (кроме типа коммита) можно ещё обозначить ломающее изменение.
-
Сценарий. У вас в компании смешаны разные методы слияния PR: одни разработчики жмут «Merge commit», другие «Squash and merge», третьи вручную ребейзят и жмут «Rebase and merge». Какие две проблемы это создаёт согласно мануалу, и какое решение нужно закрепить?
-
Что такое PR cycle time и из каких фаз он складывается? Какая метрика считается «главным управляемым рычагом», одновременно влияющим сразу на несколько DORA-метрик — и почему её нельзя превращать в личный KPI конкретного разработчика?
Ответы (открыть после самопроверки)
1. Четыре слоя: **стратегия ветвления** (какие ветки есть и сколько живут), **процесс интеграции / PR-MR** (как изменение попадает в основную ветку — ревью, проверки, одобрения), **метод слияния** (какой след изменение оставляет в истории: squash/rebase/merge commit) и **релизный процесс** (как из истории получается версия — теги, changelog). Метод слияния — это отдельный, самостоятельный слой: команда может работать по trunk-based (стратегия ветвления) и при этом выбрать squash merge (метод слияния) — эти решения независимы друг от друга, хотя и влияют друг на друга (например, squash упрощает автоматический changelog). 2. Различие — в том, **сколько долгоживущих веток команда держит и как долго изменение может жить отдельно от основной ветки**. По возрастанию: Trunk-Based Development (только `main`) → GitHub Flow (`main` + недолгие PR-ветки) → GitFlow (`main` + `develop` + `feature`/`release`/`hotfix`). За каждую дополнительную долгоживущую ветку команда платит более поздней интеграцией, а значит — более тяжёлыми конфликтами (сложность слияния растёт быстрее, чем линейно от времени расхождения) и дополнительными точками, где легко забыть перенести исправление (например, двойной merge `release`/`hotfix` в GitFlow). 3. Критерии DORA: (1) не больше трёх активных веток в репозитории, (2) ветки вливаются в trunk минимум раз в день, (3) нет code freeze и отдельных «фаз интеграции». 4. Подойдёт стратегия **Release branches** (или GitLab Flow с релизными ветками): разработка идёт в `main` по trunk-based, а в момент релиза от `main` создаётся `release/X.Y`, в которую потом попадают только исправления через cherry-pick — так можно параллельно поддерживать несколько выпущенных версий, пока пользователи их не обновили. GitHub Flow не подходит, потому что в нём правило «всё, что в `main`, можно деплоить» и релиз всегда идёт из `main` сразу после merge — там нет механизма параллельно поддерживать несколько версий одновременно, которые расходятся друг от друга исправлениями. 5. **Деплой** — техническое событие: новая версия кода уже запущена в production. **Релиз** — продуктовое событие: пользователи реально получили новую функциональность. Их разделяет **feature flag**: код фичи может доехать до production за флагом, оставаясь выключенным, пока его явно не включат. Без флагов у команды нет способа влить в `main` незаконченную работу, не показав её пользователям, — а значит, незаконченная работа вынуждена жить в отдельной ветке до полной готовности, и trunk-based по факту превращается в GitFlow с долгоживущими feature-ветками. 6. «Require branches to be up to date before merging» не масштабируется: при десятке PR в день каждый merge заставляет все остальные открытые PR обновляться и перезапускать CI заново, и разработчики бесконечно жмут «Update branch». Merge queue решает это иначе: одобренные PR встают в очередь, платформа сама создаёт временную ветку «`main` + этот PR + PR впереди в очереди», один раз прогоняет CI и вливает, если он зелёный. Если не настроить триггер `merge_group` (событие CI при постановке PR в очередь), обязательная проверка никогда не отчитается на временной ветке очереди, и очередь просто зависнет, не продвигаясь. 7. Два правила: «Dismiss stale pull request approvals when new commits are pushed» — сбрасывает уже полученное одобрение при появлении новых коммитов, потому что одобрение относится к конкретному коду, а не к PR вообще; и «Require approval of the most recent reviewable push» — требует, чтобы именно *последний* пуш был кем-то одобрен (а не только более ранняя версия ветки). Вместе они гарантируют, что одобренный код и вливаемый код — это одно и то же. 8. При изоляции в коде (feature flags) незаконченная работа вливается в `main` маленькими PR каждый день и даже доезжает до production, но остаётся выключенной — интеграция происходит постоянно, но это требует дисциплины, культуры маленьких PR и очень надёжного быстрого CI. При изоляции в ветках (GitFlow) незаконченная работа живёт в `feature/*` и `develop`, снимая часть требований к CI, но интеграция откладывается на потом — а чем дольше она откладывается, тем дороже обходится слияние (растущие конфликты, поздно найденные интеграционные баги). 9. Fix делается сначала обычным PR в `main`, и только потом `cherry-pick -x` переносится в поддерживаемую релизную ветку (upstream first), а не наоборот. Если сделать исправление только в релизной ветке, его легко забыть перенести обратно в `main` — тогда следующий плановый релиз, собранный из `main`, вернёт уже исправленный баг, потому что в `main` его фактически никогда не было. Порядок «сначала `main`» устраняет саму возможность такой регрессии. 10. Conventional Commits задаёт машиночитаемый формат сообщения `type(scope)!: описание`, по которому инструмент релиза (например, release-please) автоматически вычисляет SemVer-версию: `fix:` даёт PATCH, `feat:` даёт MINOR, а ломающее изменение — MAJOR. Пример: `feat(api)!: drop v1 endpoints` — восклицательный знак после типа/scope сразу поднимает MAJOR. Кроме `!` то же самое можно указать футером в теле коммита: `BREAKING CHANGE: ...` (синоним `BREAKING-CHANGE:`). 11. Две проблемы: в истории `main` появляется смесь стилей (где-то merge-коммиты, где-то один squash-коммит на PR, где-то поштучные ребейзнутые коммиты), что усложняет чтение истории; и автоматика релизов (вычисление версии и changelog по коммитам) начинает ошибаться, потому что не может однозначно сопоставить «один PR = одна запись в changelog». Решение — закрепить **один метод слияния на репозиторий** и отключить остальные кнопки в настройках. 12. PR cycle time — время от открытия PR (или от первого коммита в ветке) до его слияния в `main`. Складывается из фаз: coding time (до открытия PR), time to first review / pickup time (до первой реакции ревьюера), review time (итерации правок до approve) и merge time (очередь, финальный CI). Главный управляемый рычаг — **размер PR**: его уменьшение одновременно ускоряет review (Time to first review и Review time), снижает риск конфликтов и ошибок, а через это улучшает сразу Lead Time for Changes, Deployment Frequency и Change Failure Rate из DORA. Превращать её в личный KPI нельзя из-за закона Гудхарта: разработчики начнут дробить PR ради цифры, а ревьюеры — одобрять не читая, и сам процесс от этого не улучшится, хотя графики станут красивее.Практическое задание¶
Задание 1. Настройте защиту main и CODEOWNERS для учебного репозитория¶
Стартовая точка. У вас есть (или вы создаёте) публичный тестовый
репозиторий на GitHub с любым небольшим проектом (даже пустым, с одним
файлом README.md) и правами администратора в нём.
Цель. По образцу примеров 1 и 2 из мануала настроить:
- файл
.github/CODEOWNERSминимум с двумя правилами — общий владелец «по умолчанию» для всего репозитория и более специфичное правило для отдельного пути (например,/docs/или/infra/), с учётом того, что побеждает последнее совпавшее правило; - защиту ветки
main(branch protection rule или ruleset): обязателен PR перед merge, минимум 1 approval, сброс устаревших approval при новых коммитах, запрет force push и удаления ветки, требование зелёных статус-чеков; - один разрешённый метод слияния (например, только squash), остальные явно отключить в настройках репозитория.
Что значит «готово». Прямой git push в main из локального клона
(без PR) отклоняется платформой; PR, затрагивающий путь из CODEOWNERS,
автоматически получает нужного ревьюера в назначенных; в настройках
репозитория включена только одна из трёх опций слияния PR; вы можете
показать (скриншотом или gh api repos/<org>/<repo>/rulesets), что правила
применяются, а не просто описаны в документации проекта.
Задание 2. Соберите пайплайн проверки PR с commitlint и merge queue-совместимым триггером¶
Стартовая точка. Тот же (или новый) репозиторий с включённым GitHub Actions.
Цель. По образцу примера 3 из мануала написать workflow PR checks,
который:
- запускается на события
pull_request(opened, edited, synchronize, reopened) иmerge_group; - содержит job с базовой проверкой (например,
npm test/npm run lint, или для проекта без Node — любой скрипт-заглушку, имитирующий тесты); - содержит отдельный job
pr-title, который проверяет заголовок PR по Conventional Commits черезcommitlint(используйтеcommitlint.config.mjsс@commitlint/config-conventional), и который корректно пропускается (не падает) на событииmerge_group, где заголовка PR нет; - передаёт заголовок PR в шаг через
env, а не через прямую подстановку${{ }}вrun:.
Что значит «готово». Открытый вами тестовый PR с «плохим» заголовком
(не по Conventional Commits, например fix stuff) проваливает job
pr-title в статусах PR; PR с заголовком вида fix(api): handle empty
list проходит проверку; вы можете объяснить, почему в run: заголовок PR
передаётся через переменную окружения, а не напрямую в строку команды.
Задание 3. Смоделируйте и пройдите hotfix-процесс для release branches (по желанию)¶
Стартовая точка. Локальный тестовый репозиторий с веткой main, от
которой вы создаёте release/1.0 (представьте, что от неё уже был выпущен
тег v1.0.0). В main за это время добавляется новый, не связанный с
исправлением коммит (имитация того, что разработка продолжается).
Цель. Повторить последовательность из раздела мануала про hotfix:
- Внести баг-фикс отдельным коммитом в
main(через обычный локальный коммит, имитирующий влитый PR). - Перенести именно этот коммит в
release/1.0черезgit cherry-pick -x <sha>(не пересобирая ветку заново и не сливая всюmain). - Поставить на получившийся коммит в
release/1.0тегv1.0.1. - Убедиться, что не связанный с фиксом коммит, добавленный в
mainранее, вrelease/1.0не попал.
Что значит «готово». git log --oneline release/1.0 показывает исходные
коммиты релиза плюс ровно один перенесённый коммит с фиксом (сообщение
которого содержит строку (cherry picked from commit ...) благодаря
флагу -x); git tag показывает оба тега v1.0.0 и v1.0.1; вы можете
объяснить, почему нельзя было сделать этот же фикс сразу в
release/1.0 и забыть о main — что именно случится с этим багом в
следующем плановом релизе, если так поступить.