← назад к разделу

Допустим, в команде есть автоматический конвейер: тесты гоняются при каждом коммите, сборка автоматическая. Но разработчики работают в ветках, которые живут по несколько недель. И когда приходит время слить — начинается катастрофа: конфликты на несколько часов, баги на стыке двух больших изменений, которые никто не тестировал вместе. Никакой конвейер от этого не спасёт. Проблема — не в инструментах, а в том, как организованы ветки и релизный цикл.

Почему длинные ветки ломают интеграцию

Конфликты при слиянии — это симптом. Настоящая проблема в том, что пока ветка живёт отдельно, она расходится с тем, что есть в главной ветке. Расхождение растёт с каждым днём. Через две недели две ветки разошлись так, что даже автоматическое слияние ничего не гарантирует: код технически объединился, но логически — нет. Баги появляются уже в продакшене.

Есть важная закономерность: сложность интеграции растёт квадратично от времени жизни ветки. Ветка на день — тривиальное слияние. Ветка на месяц — это месяц расхождений и полдня конфликтов.

Continuous integration в исходном смысле значит именно это: разработчики интегрируют свои изменения в общую ветку часто, каждый день. Не «сервер гоняет тесты», а именно частая интеграция кода.

Trunk-based development

Это подход, при котором существует одна главная ветка (обычно main), и все изменения попадают в неё быстро — через маленькие, короткоживущие ветки. «Короткоживущие» — это день-два, максимум несколько дней, не недели.

Большие задачи при этом не исчезают. Они режутся на последовательность маленьких шагов, каждый из которых можно безопасно слить в main. Если функция ещё не готова показывать пользователям, код всё равно попадает в main, но за выключенным флагом — подробнее об этом дальше.

Старый gitflow — develop-ветка, release-ветки, hotfix-ветки — имел смысл для коробочного ПО, которое выпускалось несколько раз в год. Для сервиса с непрерывной доставкой он добавляет накладные расходы без реальной защиты. Исключение — поддержка нескольких версий одновременно: когда нужно патчить версию 1.x и 2.x параллельно, долгоживущие ветки оправданы. Но это не типичный случай для обычного веб-сервиса.

Как работает PR на практике

Trunk-based — это не просто «ветки покороче». Он держится на нескольких командных нормах.

Маленький PR — одна логическая единица. Это может быть один вертикальный срез функции, одна правка, один шаг рефакторинга. Большой PR на тысячи строк никто по-настоящему не ревьюирует — его пролистывают и одобряют. Баги остаются.

Ревью — в течение дня. Если PR висит неделю, автор заблокирован, в его ветке копятся конфликты, мотивация падает. В командах с trunk-based ревью чужого кода стоит выше, чем написание своего нового.

Зелёный конвейер — до ревью. Ревьюер смотрит на дизайн и смысл, а не ловит синтаксические ошибки. То, что ловит машина, пусть ловит машина.

Squash при слиянии. История в main — это лента осмысленных изменений, по одной записи на PR. Не «fix», «fix2», «попробую иначе» — а одна запись с понятным описанием того, что было сделано.

Feature flags: как прятать незавершённое

Допустим, новую функцию нельзя показывать пользователям, пока она не готова полностью. Классический ответ — держать ветку открытой до готовности. Это и приводит к многонедельным веткам.

Альтернатива — feature flag (флаг функции): код уходит в main и деплоится, но функция выключена. Когда продукт решает «всё, готово» — флаг включается, функция становится видимой. Это разделяет деплой кода и релиз функции: деплой происходит часто и безопасно, а включение функции — это отдельное решение.

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

Как назначать версии

Для сервиса, который деплоится непрерывно, версия — это SHA коммита. Каждый коммит в main — потенциальный релиз. SHA однозначно идентифицирует, что именно сейчас работает в продакшене. Его видно в /actuator/info и в логах. Человеческих номеров типа «1.4.2» здесь не нужно.

SemVer (major.minor.patch) нужен там, где есть внешние потребители контракта: библиотеки, публичные API, события с версиями. Цифры передают смысл изменения:

  • patch — исправление без изменения контракта;
  • minor — новая функциональность, обратная совместимость сохранена;
  • major — ломающее изменение, потребители должны адаптироваться.

Присваивать эти цифры вручную — плохая идея: люди ошибаются и путаются. SemVer автоматически выводится из conventional commits — стандартного формата сообщений коммитов:

feat: добавить метод поиска по тегам
fix: исправить сортировку результатов
feat!: изменить формат ответа API (ломающее изменение)

По такому журналу инструменты сами вычисляют, какую цифру нужно поднять, и генерируют changelog.

GitOps: что в git — то в продакшене

Традиционный деплой: кто-то запускает скрипт, пишет в чате «деплою», нажимает кнопку. Что сейчас в продакшене — можно узнать только спросив у того, кто деплоил последним. Откат — ручная операция.

GitOps переворачивает это: желаемое состояние продакшена хранится в git-репозитории. Инструмент (обычно Argo CD) смотрит на этот репозиторий и добивается того, чтобы в кластере было именно то, что там описано. Никаких ручных деплоев — только изменения в git.

Практические следствия:

  • история деплоев = git log — понятно, что, когда и кто поменял;
  • откат = revert — обычная git-операция, воспроизводимая и понятная;
  • аудит бесплатный — всё задокументировано в коммитах.

Петля от коммита до продакшена

Вот как выглядит полный цикл в команде с trunk-based + GitOps:

PR → конвейер (тесты, линтер, сборка) → слияние в main
→ образ с SHA → автоматический деплой на staging
→ проверка → PR с новым тегом в GitOps-репозиторий
→ Argo CD обновляет продакшен
→ включение флага, когда функция готова к пользователям

Человеческих решений в этой петле два: одобрить PR и включить флаг. Остальное — автоматика.

Частые ошибки

ПроблемаЧто происходитКак исправить
Ветка фичи живёт месяцКонфликтное слияние, непротестированные стыкиРезать на маленькие PR + feature flags
Develop + release + hotfix для обычного сервисаЛишние церемонии, непонятно где правдаTrunk-based: main — единственная правда
PR висит неделюБлокировки, конфликты, демотивацияРевью в течение дня — командная норма
Версия «1.0-final-new2» вручнуюНепонятно что в продакшенеSHA для сервисов, авто-SemVer для библиотек
Деплой «по договорённости в чате»Неповторяемо, неотслеживаемоGitOps: PR в репозиторий конфигурации — единственный путь
Огромный PR «вся функция сразу»Ревью по диагонали, баги в продакшенеВертикальные срезы; флаги для неготового

Коротко

  • Длинные ветки создают конфликты и непротестированные стыки — сложность растёт квадратично от времени жизни ветки.
  • Trunk-based development: одна главная ветка, изменения попадают в неё быстро через маленькие короткоживущие ветки.
  • Незавершённый код прячется за feature flags — это разделяет деплой кода и включение функции.
  • Для сервисов — SHA как версия; для библиотек и публичных API — SemVer, выводимый автоматически из conventional commits.
  • GitOps: желаемое состояние продакшена описано в git, Argo CD следит за соответствием. Откат = revert, история деплоев = git log.
  • Squash при слиянии держит историю main читаемой.

Что почитать дальше

  • Принципы конвейера — зелёный main и деплой ≠ релиз.
  • CI: сборка, тесты, quality gates — gates, через которые проходит каждый PR.
  • Стратегии релиза — rolling, blue-green, canary и feature flags подробно.
  • Деплой в Kubernetes — как GitOps работает на уровне кластера.