На днях вышла прекрасная, хотя и спорная статья — Please, stop using GitFlow! (и еще десяток на эту же тему после нее).
Ее основными тезисами были:
- GitFlow противоречит тезису "ветки должны быть короткоживущими".
Это важно, потому что чем меньше живет ветка — тем меньше шанс конфликтов (не только git, но и логических) - GitFlow препятствует rebase-ам, чтобы сохранить merge-коммиты.
Да, их можно сохранять и при ребейзах, но команды Git Flow не делают этого. - GitFlow отрицает подход Contunious Delivery, считая, что каждый акт Delivery должен совершаться релиз-инженером, да и в целом можно увидеть, что он ориентирован только на долгий релизный цикл.
- GitFlow не дружит с микросервисами поверх мультирепозиториев (впрочем, тут вообще мало что подходит, это идея, которая всегда плохо заканчивается)
-
Но проблема GitFlow в том, что он и с монорепозиториями тоже не дружит.
Я сам об это споткнулся в процессе дизайна пайплайнов CI: GitFlow чудовищно мешает, когда работает поверх монорепозитория с несколькими deliverables, например, когда в одном репозитории отдельно и бэкэнд, и фронтэнд — уже из-за того, что он позволяет докоммичивать в релизные ветки, могут возникнуть конфликты при обратном мердже, если в один момент времени существует больше, чем одна релизная ветка. Да даже если одна, все равно плохо — в таких условиях надо патчить в принципе релизные механизмы GitFlow, чтобы хоть как-то заработали отдельные релизы сущностей.
Так что делать-то?
Автору оригинальной статьи помог trunk-based development + feature flags — почти гугловый green trunk, но не совсем. Но это совсем не значит, что этот подход нужен именно вам.
Во-первых нужно понять приоритеты.
Как выглядят ваши релизы?
- Они короткие или длинные? Вы хотите релизиться раз в пару часов, дней, недель, или у вас коробочные продукты, которые выкатываются раз в полгода?
- Сколько занимает время самого релиза? Это может быть от пары минут до даже дней, если у вас есть долгоживущие клиенты, которые подключены по WebSockets, и не хотят отваливаться. Всякое бывает.
- Нужно ли поддерживать старые или будущие релизы? Есть ли у вас LTS-релизы, в которые вы обязаны делать backport-ы (это когда вы чините баг в основной ветке, а потом идете, и еще раз чините его в ветке 2016 года) изменений? Есть ли у вас canary и beta-релизы?
- Есть ли у вас вообще Continuous Delivery?
Кто работает в репозитории?
- Все (или почти все) ли члены команды самодостаточны, или им нужен фидбек старших товарищей? Нужны ли вам полноценные циклы design review и merge/pull-request-ов, или среднее время на PR — это 5 минут на каждые 100 строк кода?
- Работает ли в репозитории много команд, или только одна?
Есть ли у вас автоматизированные средства для проверки качества кода? Ну, или хотите ли вы их внедрить?
- линтеры
- проверки типов
- автотесты: юнит-тесты, интеграционные тесты и так далее
Очень важно понимать, что релизный процесс и рабочий процесс — это вообще разные потоки. Тут без вариантов, любой flow поверх гита так или иначе это делает, разделяя изменения и релизы. Работа с этими двумя процессами похожа на работу с очередями вроде Apache Kafka или SQS — есть ряд источников, которые помещают обновления в очередь, и есть ряд указателей-релизов, которые смотрят на какой-то момент в прошлом, периодически поднимаясь вверх по этой очереди.
Изменения разработчиков и релизы — это абсолютно разные вещи. Релиз включает в себя пачку (от одного до бесконечности) изменений разработчиков, да, но он не обязательно должен включать в себя всю сделанную разработчиком фичу, он может забрать, допустим, пару созданных им для работы классов или функций, но не реализованную задачу, и это нормально.
Важно понимать, что если вам иногда требуется релиз, даже если изменений нет — вы что-то делаете не так. Иногда такое происходит, когда требуется обновить базовый JSON или YAML, который забирается во время сборки от внешнего поставщика — секреты, переводы, внешняя конфигурация.
Это неправильно, потому что это завязка на внешний фактор риска, и это неправильно, потому что это можно обновлять наживую во время работы с продакшном — автоматически или по кнопке, и в ситуации, когда это нужно будет сделать срочно — вы будете ждать полноценного релиза. Все, что можно вынести из релизного процесса, стоит вынести из него. Релиз — это дорогая операция, и если вы можете уменьшить их количество при той же скорости производства — стоит это делать.
Собираем свой flow
В общем случае вам будет нужна единая мастер-ветка, в которую будут попадать все изменения. Очень сложно представить ситуацию, где все вместе работают над отдельными задачами, синхронизируясь друг с другом время от времени (если это не поддержка старых версий). Есть подход, в котором заводится отдельная ветка для документации или для отдельного проекта, но по сути это отдельный репозиторий в рамках общего, и к нему надо относиться точно так же, хотя это практика, которая может привести к расхождениям между кодом и документацией к нему, а так же к невозможности одновременно держать документацию, скажем, к v1 и v2 решениям.
В целом, если вы можете жить в монорепозитории, стоит это делать. Да, нужно разово все настроить, но у вас нет никаких значимых плюсов в отдельных репозиториях, кроме потенциального риска плохо настроенного и тормозящего CI, а потенциальных минусов очень много: про преимущества монорепозиториев написана не одна и не две статьи.
Если ваши релизы частые — нужно явно внедрять Continuous Delivery.
Время релизного инженера — это издержки, которые будут расти при росте количества релизов и количества deliverables. Лучше потратьте время разработчика на что-то еще, рук не хватает всем и всегда, даже если вы Google. По личной статистике — около 10% ресурсов команды -в абсолютно разных проектах — в среднем уходят на релиз-инженера, если релизы не автоматизированы.
Для автоматизации релизов надо разово вложиться в инфраструктуру. Это разовая издержка, которая включает в себя:
-
Хорошие автотесты
-
CI-автоматику для выкладки релизов
-
Если у вас сервера и deliverables:
- prestable/staging-окружения
- механизмы быстрого отката
- мониторинги и alert-ы (в идеале интегрированные с авто-откатами)
- аварийные сценарии (если релиз не прошел или прошел частично)
-
Если у вас публикация пакетов или артефактов — все зависит от вашего flow, но я буду рад помочь с построением релизного механизма, пишите
-
Если релизы редкие, нужны backport-ы ИЛИ если в один момент времени потенциально может существовать более чем один обслуживаемый релиз — они явно будут существовать в отдельных ветках, которые не должны мерджиться обратно. После того, как вы отбранчуете эту ветку от основной рабочей (master/develop/trunk), вам может понадобиться внести в них несовместимые изменения, или просто отдельные коммиты. Для второго есть отличный механизм в git — cherry-picking. Ваши изменения должны "течь" только в одну сторону — от разработчиков в прод, из прода в основную рабочую ветку им попадать в действительности не нужно.
- Чем меньше и атомарней коммиты, тем меньше проблем при их переносе
- Используйте агрессивные правила линтинга, которые защитят вас от ненужных конфликтов. В случае с JS — есть пара хороших наборов eslint-правил. Prettier может подойти в вашем случае (но не гарантированно).
Если у вас больше, чем 5-6 разработчиков, или опытная команда — вам может подойти подход trunk-based development для повседневной работы разработчиков (но не обязательно, что для релизов). Если счет разработчиков в одном репозитории идет на сотни — или вы хотите объединить сотни разработчиков в одном репозитории, у вас почти нет альтернативных вариантов, разве что сменить git на что-то еще. Google рапортовал в разных статьях о значительном приросте производительности после перехода на этот подход, но, к сожалению, цифры разнятся, хотя и значимы во всех источниках. Facebook не сообщает о цифрах, но известно, что они тоже пользуются этим подходом.
Несмотря на то, что Trunk-based — это просто "вливайте минимальные изменения сразу в рабочую ветку", он, в отличии от других подходов, требует вложений в инфраструктуру и настройку ее под себя. В целом, кроме обычных CI/CD, вам потребуется еще:
-
механика feature flags — это разовая издержка.
Для однопакетного репозитория стоимость внедрения сильно непредсказуема, но в целом должна быть не очень большой.
Для монорепозитория же она примерно пропорциональна количеству пакетов в монорепозитории. Переведите 2-3 разных и экстраполируйте, будет относительно точная, но слегка завышенная (потому что чем дальше, тем проще) оценка
-
В случае работы с короткоживущими ветками — merge queue, а в случае работы прямо в транке — механизм отката коммитов, не проходящих тесты.
второй вариант сделать сложнее, и он теоретически может провоцировать конфликты, так что лучше короткоживущие ветки.
В целом, эволюционный механизм для любого типичного проекта сейчас выглядит как-то так:
- github-flow
- осознание потребности контроля за качеством кода
- внедрение CI, автотестов
- осознание потребности релизных механизмов
- внедрение чистого git-flow или его вариации (любое решение с долгими ветками + релизными ветками)
- осознание потребности в CD
- настройка git-flow под свои нужды, внедрение CD, staging-релизов, интеграция с внешними инструментами
- осознание проблемы с синхронизацией работы разработчиков
- долгая работа над возможностью внедрения trunk-based и разгребание технического долга
- переход на trunk-based-подобный подход
Но забегать вперед — не всегда лучшая идея. Иногда стоит посидеть на github-flow или отложить разгребание техдолга (или на время нанять отдельного GitOps-а — да, такая роль и профессия теперь тоже есть). Более того, ваша реализация может и скорее всего должна начать отличаться от "эталонной". Это нормально, и много разных команд работают в своих собственных парадигмах, если описывать тут их все — это вполне может выйти в отдельный цикл статей.
Поймите, на каком этапе вы, что у вас болит, и что можно сделать, чтобы не болело.
Ваш Git — это часть вашей инфраструктуры и кодовой базы, в нем точно так же копится технический долг, и его точно так же надо обслуживать. К сожалению, это не очевидно для большинства, и это зачастую спотыкается о непонимание команд и руководителей.
Если вы ощущаете, что ваш Git делает вам больно — напишите мне, я буду рад помочь.
Автор: Сева Родионов