Допустим, в команде есть автоматический конвейер: тесты гоняются при каждом коммите, сборка автоматическая. Но разработчики работают в ветках, которые живут по несколько недель. И когда приходит время слить — начинается катастрофа: конфликты на несколько часов, баги на стыке двух больших изменений, которые никто не тестировал вместе. Никакой конвейер от этого не спасёт. Проблема — не в инструментах, а в том, как организованы ветки и релизный цикл.
Почему длинные ветки ломают интеграцию
Конфликты при слиянии — это симптом. Настоящая проблема в том, что пока ветка живёт отдельно, она расходится с тем, что есть в главной ветке. Расхождение растёт с каждым днём. Через две недели две ветки разошлись так, что даже автоматическое слияние ничего не гарантирует: код технически объединился, но логически — нет. Баги появляются уже в продакшене.
Есть важная закономерность: сложность интеграции растёт квадратично от времени жизни ветки. Ветка на день — тривиальное слияние. Ветка на месяц — это месяц расхождений и полдня конфликтов.
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 работает на уровне кластера.