Git Плейбук: синхронизация и стабильные релизы

В этой статье описана методология работы с 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:

Трекер задач

Так как процесс подразумевает тесную связь истории изменений с планированием, необходимо использовать трекер задач, который автоматически создаёт короткий и человекочитаемый идентификатор задачи или issue.

Привычные трекеры, такие как Jira, YouTrack или Яндекс Трекер предоставляют короткий автоинкрементируемый идентификатор задачи из коробки.

Также можно использовать функционал issues в GitHub, GitLab или Gitea. В таком случае для названия ветки можно использовать как локальный, так и глобальный идентификатор issue (пример: 42 или acme/service#42 соответственно).

Такие инструменты, как Trello, Asana или Notion, скорее всего не подойдут для процесса, так как не генерируют уникальный идентификатор для каждой отдельной задачи.

Процесс тестирования

Методология предоставляет гибкость в организации процесса тестирования.

Самым важным является тестирование релизной ветки. Этот этап нельзя пропускать или исключить из процесса. При тестировании релизной ветки (или релиз-кандидата, RC) производится стабилизация кода, который окажется в master и, в последующем, в production.

Помимо тестирования релиза можно использовать тестирование задачи в изоляции или общий тестовый стенд. В таблице ниже представлены все возможные окружения для тестирования.

ОкружениеОписание
<issue_id>

Стенды для тестирования отдельной задачи в изоляции.

+ Отсутствие false-positive из-за пересечений с другими задачами.

- Дополнительные требования к серверным ресурсам и инфраструктуре, которая позволяет автоматизировать создание окружений (например k8s или Coolify).

test

Общее тестовое окружение, максимально нестабильное.

+ Дёшево и просто в реализации.
+ Удобно использовать для получения быстрой обратной связи.

- Проблемные изменения могут сломать тестовый стенд для всех участников процесса разработки.
- Может значительно рассинхронизироваться относительно master — в таком случае нужно просто пересоздать окружение.

stageКод последнего релиза с тестовыми зависимостями. Может использоваться для отладки production проблем на тестовых данных.
rc-<version> или rcСтенд для тестирования изменений, которые содержатся в очередном релизе. Должен обязательно использоваться в процессе разработки.

Процесс релиза

В первую очередь формируется список задач, которые попадают в релиз. Выбранные задачи должны удовлетворять следующим требованиям:

На основании списка задач, согласно 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.

master rc-v1.5.0 ABC-42 ABC-14 0-1c4eac0 v1.4.13 1-78edbb1 2-c0b34b8 4-cd4788f 5-c8720b1 fix conflicts 8-eb5b94b CHANGELOG v1.5.0 HOTFIX v1.5.1
Исходный код диаграммы

Открыть в mermaid.live.

---
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

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. Коммиты

    1. Коммиты должны быть максимально атомарными. 1
    2. Много мелких коммитов лучше одного большого.
    3. Сообщение коммита должно содержать описание изменений, которые он содержит.
    4. Сообщение коммита должно содержать ссылку на issue. 2
    5. В ветке задачи допустимо коммитить и пушить нерабочий код с пометкой WIP (Work In Progress).
    6. Предпочтительно оставлять WIP коммиты в истории. 3
  2. Создание feature-веток

    1. Для каждой задачи создаётся уникальная ветка для внесения изменений.
    2. Ветка задачи всегда создаётся только из актуальной версии master.
    3. Название ветки должно полностью совпадать с идентификатором задачи в трекере. 4
    4. В одной ветке задачи могут работать несколько разработчиков одновременно.
    5. Если один и тот же новый код требуется в двух разных ветках задач, предпочтительно использовать git cherry-pick или провести общий код через релизный цикл.
    6. Недопустимо вливать ветки задач друг в друга.
  3. Слияние веток

    1. Для слияния веток всегда используется git merge.
    2. При слиянии веток должен создаваться merge-коммит.
    3. При слиянии веток все созданные ранее коммиты остаются в истории без изменений (навсегда).
  4. Merge Requests

    1. Перед интеграцией изменений из ветки задачи в master должен быть создан Merge Request (MR).
    2. До того как задача попадёт в релизную сборку, должно быть пройдено ревью кода.
    3. Если в MR есть конфликты относительно master, они должны быть решены через слияние master в ветку задачи. 5
  5. Интеграция в master

    1. Изменения из feature-веток попадают в master через Release Candidate (RC) ветки.
    2. Допустимо вносить изменения напрямую в RC-ветку. 6

Сноски

  1. Подходящий момент для создания коммита — завершение работы над отдельным логическим блоком. Подразумевается, что отдельный коммит можно откатить используя git revert или перенести в другую ветку с помощью git cherry-pick.

  2. Подстановка ссылки на issue должна быть автоматизирована с помощью Git hook.

  3. Squash коммитов и другое изменение истории в процессе работы в ветке задачи допустимо при выполнении следующих условий: (1) в ветке задачи работает не больше одного разработчика; (2) ветка не вливалась ни в какую другую; (3) в ветку не вливались другие ветки. Чтобы избежать проблем с состоянием Git лучше не изменять историю коммитов.

  4. Предпочтительно ограничить название ветки только идентификатором задачи и не использовать префиксы feature, fix или суффикс с описанием. В отличие от идентификатора такие префиксы и суффиксы субъективны.

  5. Для решения конфликтов необходимо синхронизироваться с актуальной версией master, после чего выполнить git merge master в ветке задачи и при необходимости решить конфликты. Это создаст merge-коммит из master в ветку задачи. При наличии конфликтов с master задача не может быть включена в релизную сборку.

  6. В Release Candidate ветки допустимо вносить изменения, которые не относятся к какой-то конкретной задаче. Это может быть как изменение мета-информации (changelog, версия пакета), так и исправление неявных конфликтов интеграции двух разных веток задач.