Перейти к содержанию

Git-workflow для команд: квиз для самопроверки

Компаньон к мануалу 05 про Git-workflow. Сначала попробуйте ответить на все вопросы самостоятельно, потом сверьтесь с разделом «Ответы». Не подглядывайте раньше времени — так проверка честнее.


Вопросы для самопроверки

  1. Мануал выделяет четыре слоя, из которых складывается Git-workflow. Назовите их и объясните, почему нельзя, например, выбор метода слияния (squash/rebase/merge commit) назвать «стратегией ветвления».

  2. Чем стратегии ветвления принципиально различаются между собой — какой один вопрос отвечает за это различие? Расположите Trunk-Based Development, GitHub Flow и GitFlow по возрастанию числа долгоживущих веток и объясните, чем команда платит за каждую дополнительную ветку.

  3. Какие три критерия DORA использует, чтобы определить, что команда реально работает в trunk-based (а не просто называет так свой процесс)?

  4. Сценарий. Ваша команда делает мобильное приложение: ревью в сторе занимает несколько дней, и одновременно поддерживаются две последние версии для пользователей, которые ещё не обновились. Какую стратегию ветвления вы предложите и почему GitHub Flow здесь не подойдёт?

  5. Объясните разницу между понятиями «деплой» и «релиз». Какой механизм позволяет их разделить, и почему без него trunk-based development потребовал бы возврата к долгоживущим веткам?

  6. Почему для команды, которая порождает больше десятка PR в день, правило «Require branches to be up to date before merging» работает хуже, чем merge queue? Что произойдёт, если для merge queue не настроить триггер merge_group в CI?

  7. Сценарий. Ревьюер одобрил PR, после чего автор дотолкал в ветку ещё два коммита с исправлением совсем другой логики, и PR слили без повторного одобрения. Какие два правила защиты веток из мануала должны были это предотвратить и как каждое из них работает?

  8. Чем «изоляция незаконченной работы в коде» (feature flags) отличается от «изоляции в ветках» (GitFlow)? Какую цену платит команда за каждый из этих двух подходов?

  9. Опишите правильный путь hotfix для стратегии с релизными ветками (release branches). Почему исправление нельзя делать только в релизной ветке и почему порядок именно «сначала в main, потом cherry-pick», а не наоборот?

  10. Как Conventional Commits связаны с автоматическим вычислением версии по SemVer? Приведите пример коммита, который должен поднять MAJOR-версию, и объясните, через что (кроме типа коммита) можно ещё обозначить ломающее изменение.

  11. Сценарий. У вас в компании смешаны разные методы слияния PR: одни разработчики жмут «Merge commit», другие «Squash and merge», третьи вручную ребейзят и жмут «Rebase and merge». Какие две проблемы это создаёт согласно мануалу, и какое решение нужно закрепить?

  12. Что такое 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:

  1. Внести баг-фикс отдельным коммитом в main (через обычный локальный коммит, имитирующий влитый PR).
  2. Перенести именно этот коммит в release/1.0 через git cherry-pick -x <sha> (не пересобирая ветку заново и не сливая всю main).
  3. Поставить на получившийся коммит в release/1.0 тег v1.0.1.
  4. Убедиться, что не связанный с фиксом коммит, добавленный в 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 — что именно случится с этим багом в следующем плановом релизе, если так поступить.