Git Плейбук: синхронизация и стабильные релизы
В этой статье описана методология работы с Git, которую я развивал в процессе практического управления разработкой.
Я успешно применял описанные подходы в командах разного типа: кросс-функциональных, продуктовых, аутсорсинговых. Состав команд варьировался от трёх до двадцати человек.
Основные преимущества и решаемые проблемы:
Стабильные релизы и
master.
Минимизация рисков за счёт стабилизации кода, интегрируемого вmaster, непосредственно перед релизом.Синхронизация Git с планированием.
Изменения в Git явно связываются с issue в трекере задач. Это позволяет найти причину изменений в истории кода и делает релизы более предсказуемыми.Синхронизация Git с деплоем.
Явная связь истории Git с артефактами сборок и тегами production инстансов упрощает управление инцидентами.Ветвистая каноническая история Git.
Граф истории позволяет оценить текущее состояние разработки или стоимость проекта без использования таймшитирования.
Содержание
Введение
В основе процесса лежит feature branch workflow: ветка master всегда стабильна, одна задача — одна ветка.
Для релизов используются теги из ветки master. Для именования тегов используется SemVer.
Для поддержания стабильности master используются Release Candidate (RC) ветки. Все изменения, которые должны быть выпущены, содержатся в очередной RC-ветке. RC-ветки проходят стабилизацию перед интеграцией в master.
Ветка для очередного релиза, как и все остальные ветки, всегда создаётся из актуального master. Команда всегда работает с последней версией кода.
Изменения в рамках отдельной задачи интегрируются в master не напрямую, а через RC-ветку. При этом для ревью кода создаётся Merge Request (MR) из ветки задачи в master. MR для каждой отдельной задачи закрывается автоматически в момент слияния Release Candidate в master.
При необходимости можно применять практику green trunk. Если для нескольких задач требуется общий код, не влияющий на пользователей, он может быть интегрирован напрямую в master, в обход релизного цикла. Такие ситуации должны рассматриваться как исключения и требуют дополнительного контроля интегрируемых изменений.
Предусловия
Помимо описанных ниже требований, подразумевается использование автоматизации CI/CD. Однако отсутствие автоматизации не мешает внедрению процесса.
Рекомендуется наличие следующих пайплайнов в CI/CD:
- автоматическая сборка фиксированных окружений из соответствующих им веток;
- автоматическая сборка окружения для отдельной задачи по запросу;
- запуск статических анализаторов кода и автотестов в MR отдельных задач.
Трекер задач
Так как процесс подразумевает тесную связь истории изменений с планированием, необходимо использовать трекер задач, который автоматически создаёт короткий и человекочитаемый идентификатор задачи или issue.
Привычные трекеры, такие как Jira, YouTrack или Яндекс Трекер предоставляют короткий автоинкрементируемый идентификатор задачи из коробки.
Также можно использовать функционал issues в GitHub, GitLab или Gitea. В таком случае для названия ветки можно использовать как локальный, так и глобальный идентификатор issue (пример: 42 или acme/service#42 соответственно).
Такие инструменты, как Trello, Asana или Notion, скорее всего не подойдут для процесса, так как не генерируют уникальный идентификатор для каждой отдельной задачи.
Процесс тестирования
Методология предоставляет гибкость в организации процесса тестирования.
Самым важным является тестирование релизной ветки. Этот этап нельзя пропускать или исключить из процесса. При тестировании релизной ветки (или релиз-кандидата, RC) производится стабилизация кода, который окажется в master и, в последующем, в production.
Помимо тестирования релиза можно использовать тестирование задачи в изоляции или общий тестовый стенд. В таблице ниже представлены все возможные окружения для тестирования.
Процесс релиза
В первую очередь формируется список задач, которые попадают в релиз. Выбранные задачи должны удовлетворять следующим требованиям:
- Было пройдено ревью кода в Merge Request (MR), соответствующем задаче.
- В MR нет конфликтов относительно
master. - Изменения в рамках отдельной задачи были протестированы.
На основании списка задач, согласно SemVer (Semantic Versioning), определяется номер следующей версии. На основании номера следующей версии создаётся новая Release Candidate (RC) ветка с именем rc-* (например, rc-v1.5.24).
Имея список задач и номер следующей версии, можно завести сущность релиза (явно или просто обобщающую задачу). Это опциональное действие, но оно повышает связность трекера и Git.
Релизная ветка rc-* создаётся из актуальной версии master. Предпочтительно явно синхронизировать master перед созданием релизной ветки:
git checkout master
git pull
git checkout -b rc-<next_version>После создания RC-ветки перебирается список задач, каждая из них сливается поочерёдно. При возникновении конфликтов слияния они решаются на месте, в RC-ветке. После слияния веток всех необходимых задач RC-ветка синхронизируется с удалённым репозиторием.
git merge ABC-42
git merge ABC-73
git push -u origin rc-<next_version>Следующий шаг — стабилизация релиза. На фиксированном или динамическом стенде производится тестирование релиза. При обнаружении проблем, они должны быть исправлены в ветке соответствующей задачи, после чего повторно слиты в RC.
Могут быть ситуации, когда исправления невозможно отнести к какой-то конкретной задаче. В таком случае допустимо вносить изменения напрямую в RC-ветку.
После того как RC-ветка стабилизирована, изменения могут быть интегрированы в master. В момент слияния все MR задач релиза автоматически закроются.
git checkout master
git merge rc-<next_version>
git pushСразу или через некоторое время из master создаётся новый тег. Название тега используется как ключ в артефактах сборки проекта, например, в тегах Docker-образов.
git tag -a "<next_version>" -m "<next_version>"
git push && git push --tagsДеплой артефактов, ссылающихся на историю Git, производится автоматически в CI/CD или вручную.
Целевая история изменений
На диаграмме ниже изображён пример состояния графа коммитов Git между двумя релизами. Пример отражает основные идеи: асинхронную работу в feature-ветках и стабилизацию RC-ветки перед интеграцией в master.
Исходный код диаграммы
---
config:
gitGraph:
mainBranchName: 'master'
---
gitGraph LR:
commit tag: "v1.4.13"
branch ABC-42 order: 2
checkout ABC-42
commit
checkout master
branch ABC-14 order: 3
checkout ABC-14
commit
checkout master
branch rc-v1.5.0 order: 1
checkout rc-v1.5.0
merge ABC-14
checkout master
checkout ABC-42
commit
commit
checkout master
checkout rc-v1.5.0
merge ABC-42
commit id: "fix conflicts"
checkout ABC-14
commit
checkout rc-v1.5.0
merge ABC-14
commit id: "CHANGELOG"
checkout master
merge rc-v1.5.0 tag: "v1.5.0"
commit id: "HOTFIX" tag: "v1.5.1"Конфигурация Git
У каждого разработчика в файле ~/.gitconfig должны быть следующие строки:
[merge]
ff = false
[pull]
rebase = true- Отключение fast-forwarding merge используется для поддержания канонической структуры истории.
- Автоматическое использование
rebaseприpullпомогает избежать «петель» в истории при получении изменений из удалённого репозитория, которые отличаются от локальных.
Git hook для сообщений коммитов
Наличие ссылки на задачу в сообщении коммита значительно упрощает сценарии расследования причин изменений с помощью git blame. Спустя несколько релизов у отдельного коммита появляется много родительских веток и становится сложно найти исходную.
Идентификатор issue в сообщении коммита связывает каждую строчку кода с конкретной задачей.
Чтобы избежать ошибок и не забывать ссылаться на задачи, это действие должно быть автоматизировано.
Пример скрипта .git/hooks/prepare-commit-message для постановки ссылки на issue в сообщение коммита:
#!/bin/bash
COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="${2:-}"
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [[ -z "$BRANCH_NAME" || "$BRANCH_NAME" == "HEAD" ]]; then
exit 0
fi
PREFIX="$BRANCH_NAME"
if [[ "$BRANCH_NAME" =~ ^[0-9]+$ ]]; then
PREFIX="#$BRANCH_NAME"
fi
FIRST_LINE=$(head -n1 "$COMMIT_MSG_FILE")
if [[ "$FIRST_LINE" == "$PREFIX "* ]]; then
exit 0
fi
sed -i.bak -e "1s|^|$PREFIX - |" "$COMMIT_MSG_FILE"Правила работы с Git
Коммиты
- Коммиты должны быть максимально атомарными. 1
- Много мелких коммитов лучше одного большого.
- Сообщение коммита должно содержать описание изменений, которые он содержит.
- Сообщение коммита должно содержать ссылку на issue. 2
- В ветке задачи допустимо коммитить и пушить нерабочий код с пометкой
WIP(Work In Progress). - Предпочтительно оставлять
WIPкоммиты в истории. 3
Создание feature-веток
- Для каждой задачи создаётся уникальная ветка для внесения изменений.
- Ветка задачи всегда создаётся только из актуальной версии
master. - Название ветки должно полностью совпадать с идентификатором задачи в трекере. 4
- В одной ветке задачи могут работать несколько разработчиков одновременно.
- Если один и тот же новый код требуется в двух разных ветках задач, предпочтительно использовать
git cherry-pickили провести общий код через релизный цикл. - Недопустимо вливать ветки задач друг в друга.
Слияние веток
- Для слияния веток всегда используется
git merge. - При слиянии веток должен создаваться merge-коммит.
- При слиянии веток все созданные ранее коммиты остаются в истории без изменений (навсегда).
- Для слияния веток всегда используется
Merge Requests
- Перед интеграцией изменений из ветки задачи в
masterдолжен быть создан Merge Request (MR). - До того как задача попадёт в релизную сборку, должно быть пройдено ревью кода.
- Если в MR есть конфликты относительно
master, они должны быть решены через слияниеmasterв ветку задачи. 5
- Перед интеграцией изменений из ветки задачи в
Интеграция в
master- Изменения из feature-веток попадают в
masterчерез Release Candidate (RC) ветки. - Допустимо вносить изменения напрямую в RC-ветку. 6
- Изменения из feature-веток попадают в
Сноски
Подходящий момент для создания коммита — завершение работы над отдельным логическим блоком. Подразумевается, что отдельный коммит можно откатить используя
git revertили перенести в другую ветку с помощьюgit cherry-pick. ↩Подстановка ссылки на issue должна быть автоматизирована с помощью Git hook. ↩
Squash коммитов и другое изменение истории в процессе работы в ветке задачи допустимо при выполнении следующих условий: (1) в ветке задачи работает не больше одного разработчика; (2) ветка не вливалась ни в какую другую; (3) в ветку не вливались другие ветки. Чтобы избежать проблем с состоянием Git лучше не изменять историю коммитов. ↩
Предпочтительно ограничить название ветки только идентификатором задачи и не использовать префиксы
feature,fixили суффикс с описанием. В отличие от идентификатора такие префиксы и суффиксы субъективны. ↩Для решения конфликтов необходимо синхронизироваться с актуальной версией
master, после чего выполнитьgit merge masterв ветке задачи и при необходимости решить конфликты. Это создаст merge-коммит изmasterв ветку задачи. При наличии конфликтов сmasterзадача не может быть включена в релизную сборку. ↩В Release Candidate ветки допустимо вносить изменения, которые не относятся к какой-то конкретной задаче. Это может быть как изменение мета-информации (changelog, версия пакета), так и исправление неявных конфликтов интеграции двух разных веток задач. ↩