Допустим, вы разрабатываете простой веб-сайт, состоящий из веб-сервера и какой-либо БД. В таком случае, релиз для Вас - это, скорее всего, просто конкретный камит в git, который выкладывается, в нём запускаются миграции БД, и всё работает.
В таких схемах, Вы можете настроить автоматический деплой, если, конечно, проект хорошо покрыт тестами всех родов. Можно автоматически деплоить каждый git push, можно по расписанию (например, ночью), можно релизить только камиты, помеченные определенной git меткой. Как угодно.
И нет никаких проблем.
Зачем вообще нужны долгоживущие релизы?
Вопрос не праздный, учитывая, что бОльшая часть «высокоуровненого» ИТ – это веб-системы. Но всё же, мир ИТ очень богат. В каких случаях концепция «камит – это и есть наш релиз» не работает?
Например, если Вы пишете ПО для железки. Допустим, Вы выпустили следующую ве��сию устройства с ПО версии 2, но старая версия устройства с ПО версии 1 продолжает работать у клиентов и нуждается в поддержке (по крайней мере, в hotfix).
Другой вариант, Вы пишете ИТ систему, в которой используются «толстые» клиенты, то есть клиенты с состоянием. Например, Ваши пользователи работают в декстопном приложении, имеющем свою локальную БД, которое общается с сервером Вашей системы. В таком случае, обновление серверной части опережает обновление клиентского слоя, обновление которого отстает на неопределенный срок.
Еще возможен вариант, когда «клиент с состоянием» из предыдущего абзаца является «промежуточным сервером» (а у сервера обычно есть состояние), и два сервера общаются между собой.
Рассмотрим крайне абстрактный пример.
Допустим, нам надо разработать ИТ систему, в которой посетитель поликлиники может записаться у работника регистратуры на прием к врачу этой поликлиники. Архитектура такой системы может быть построена по-разному. Например, мы можем поставить сервер этой системы в каком-нибудь дата-центре, и работников регистратуры всех поликлиник страны обязать через браузер заходить в эту систему. Вполне очевидная архитектура, недорогая, но не отказоустойчивая. Отказ серверной части (из-за сбоев ПО, железа, диверсии, или экскаватора рядом с оптоволоконным кабелем) сделает невозможным функционирование системы. Конечно, мы можем решать эту проблему децентрализацией серверного слоя, тут всё стандартно.
Если же требование надежной работы крайне важно, можно придумать другую архитектуру: в каждую поликлинику (на уже существующее в ней железо) ставится небольшой веб-сервис со своей БД, а работники регистратуры через веб-браузер ходят уже в этот веб-сервис через локальную сеть. Веб-сервисы множества поликлиник синхронизируются с центральным сервером системы по расписанию. В этом случае физически невозможно вывести из строя сразу множество поликлиник, так как отказ от синхронизации не повлияет немедленно на возможность записи к врачу. Основная функция системы окажется более устойчивой к сбоям, а задержки синхронизации обычно не критичны. Также открываются новые возможности, например, деплой новой версии только на часть поликлиник для дополнительного тестирования «в полях». Правда, такой дизайн - это дорого и хлопотно. Здесь мы в полной мере сталкиваемся с тем, что данный веб-сервис - э��о программа с состоянием, которая общается с центральным сервером и в такой концепции уже неизбежно возникают долгоживущие релизы просто потому, что вы не можете обновить всё одномоментно.
Какую архитектуру выбрать, конечно, определяется требованиями. Но данная заметка не про архитектуру.
Нам нужны долгоживущие релизы, как с ними жить?
Итак, Вы начинаете разрабатывать новый проект, и его требования или архитектура указывают, что проекту подойдет модель с долгоживущими релизами. Примем это как аксиому.
Как вообще с ними работать? Как организовать процессы разработки, тестирования и доставки? Какие проблемы возникают в такой модели? Для специалиста, который годами разрабатывал веб-сервисы, всё это может быть неочевидно. Такие люди – и есть целевая аудитория этой заметки.
На наш взгляд, для модели долгоживущих релизов хорошо подходит trunk based development с «тупиковыми» ветками-релизами. Вот как это выглядит:

Ветка master (trunk) – это ветка, которой придается смысл «хранилище сделанных задач», и от которой в любой момент можно ответвить следующий релиз (здесь же полезна концепция «Merge Request как транзакция» по ссылке Хабр). Релизная ветка – это тупиковая ветка, которая никогда не вливается обратно в master. В релизной ветке проходит окончательное тестирование и из нее происходит деплой.
В случае, если в ответвленном релизе обнаруживается ошибка (тестировщиками до деплоя или после деплоя - пользователями), то от релизной ветки создается hotfix-ветка:

И эта hotfix-ветка вливается обратно в релизную, после чего релиз надо передеплоить.
В этой схеме существует один недостаток. Представим себе ситуацию: у нас есть релиз 1, который выложен и давно работает в проде; есть релиз 2, который протестирован, и уже начато обновление на него, но всё еще бОльшая часть нашей системы работает на устаревающем релизе 1; есть релиз 3, который мы только что ответвили и начинаем тестировать, когда-нибудь он заменит релиз 2; есть master в котором уже копятся фичи для релиза 4, но ветки релиза 4 еще не сделано, время еще не пришло. Такая ситуация похожа на первую иллюстрацию, с некоторыми поправками:

Обратите внимание, что релиз 1 – уже тупиковый, в него ничего не добавляется. Релиз 2 еще не тупиковый, так как мы на него обновляемся, и в любой момент могут быть обнаружены ошибки и потребуется hotfix. Релиз 3 только что ответвлен и в нем только 1 камит, который тестируется, камиты еще могут добавиться (обязательно добавятся).
Представим себе, что после начала деплоя релиза 2, с прода пришло сообщение о критическом баге, который надо немедленно править. Вы делаете hotfix ветку от релиза 2, багу исправляете, вливаете hotfix ветку в ветку релиза 2, деплоите новый камит из ветки релиза 2. Ошибка устранена, можно выдохнуть? Проблема заключается в том, что master и релиз 3 содержат ту же ошибку. Если мы забудем исправить ее и там, то произойдет страшное: после выкладки релиза 3, уже исправленная ошибка вернется, и для пользователей это будет выглядеть ужасно, они начнут думать, что вы настолько плохой разработчик, что у вас уже исправленные ошибки возвращаются из мира мертвых. Пользователям не объяснить, как такое может получить по обычной для всех человеческой невнимательности.
Таким образом, необходимо перенести hotfix во все перспективные ветки, в нашем случае в релиз 3 и master. Это удобно делать через команду git cherry pick:

Подобным образом можно удобно организовать про��ессы управления исходным кодом для проектов с долгоживущими релизами.
Но это лишь часть хлопот. Всё становится сложнее и интереснее, если углубиться в детали.
Три merge request на один баг?
В предложенной методологии сразу очевидна одна неприятная особенность: если каждый релиз – отдельная ветка и в один момент времени живет несколько релизов (+ master), то горячие правки надо переносить между ветками.
Это порождает следующие проблемы на практике:
просто хлопотно, много дел, затраты, которых хотелось бы избежать.
обычная человеческая забывчивость может привести к потере hotfix и возврату бага в следующем релизе, что катастрофично для репутации компании-разработчика.
по-хорошему, каждую hotfix-ветку (а их у нас три) надо код-ревьювить и перетестировать заново.
Посмотрим, что с этим можно сделать.
Итак, вы боитесь риска потери hotfix и возврата бага в следующем релизе. Этот риск можно снять, если внести в свод правил разработки правило «master first». Это правило означает, что сначала ошибка правится в master, потом через cherry pick происходит бекпорт правки в релиз 3, а потом из релиза 3 в релиз 2. То же самое, что на иллюстрации выше, только зеленые стрелки направлены в другую сторону. Данный подход гарантирует, что риск потери правки исчезнет. Но есть и недостаток: этот подход увеличивает затраты времени для доставки правки в актуальный, работающий на проде релиз. Для по-настоящему «горячих» правок такие задержки обычно нежелательны или невозможны.
Есть и другой, гораздо более предпочтительный способ решить проблему потери правок. Нужна соответствующая автоматизация. На самом деле, правильная автоматизация - это прекрасный способ решения очень многих проблем в разработке ПО, но основная трудность здесь заключается в создании такой автоматизации, которая упрощает работу и снижает когнитивную нагрузку на инженеров, это удается не так часто.
В данном случае можно предложить следующий способ. Можно создать внешний сервис, который будет контролировать правильность оформления задачи на hotfix: в задаче должна быть указана целевая ветка (release 2), например, через добавление соответствующего label или иным машино-читаемым способом. В этом случае, сервис может связать задачу и все МРы для неё и увидеть, что в задаче заявлен release 2, а репозитории уже есть ветка release 3, но МРа на неё нет. Это будет означать, что а) задачу нельзя позволять закрывать (или переоткрывать, если закрыли), если нет МРов во все необходимые ветки б) время от времени уведомлять ответственных лиц (например, через email) о том, что есть риск потери правки. Такой защитный подход проверен нами на практике и жизнеспособен. Недостаток: сервис надо написать, это затраты, но тут надо решать, что важнее: риск потери правок или затраты на такой сервис.
Можно пойти дальше. Мы знаем, что надо сделать черрипик (из релиза 2 в релиз 3). Зачем его делать руками? Доработайте написанный сервис, чтобы он сам создавал merge request в релиз 3 на основе камитов и описания исходного МРа, если на исходный МР навесили, например, метку «CherryPick To Release 3» (или иным способом уведомили, куда надо его черрипикать). Черрипик в другую ветку может породить конфликт слияния. В этом случае, черрипик стоит сделать вручную. Это не большая проблема, так как риск конфликта невелик, пока вы черрипикаете только hotfixы, которые, обычно, малы по размерам.
С помощью автоматизации, описанный выше, вы можете успешно снять трудности, связанные с риском потери правок и трудозатратами на черрипики и создание их МРов. Что делать с трудностями повторных код-ревью и перетестирования?
С код-ревью проще: если вы черрипикаете hotfix и сборка прошла успешно, вероятно, нет смысла проводить код-ревью для таких МРов, ведь hotfix обычно маленькие и их достаточно посмотреть один раз.
С тестированием чуть сложнее. Если требуется обеспечивать высокую надежность, очень желательно тестировать все исправления, которые попадают в релиз. Тут выход простой: в исходном merge request должна со��ержаться не только правка ошибки, но и автоматический тест, что ошибка исправлена (юнит-тест, или интеграционный тест – не имеет значения). Здесь же удобно возникает возможность для команды попробовать на практике TDD (test first подход).
Таким образом, мы предлагаем не проводить повторные код-ревью, но тестировать эти правки через автоматизацию тестирования.
Таким образом, все нежелательные последствия необходимости черрипикать hotfixы будут решены. Это действительно работает и помогает, проверно на практике.
Больше автоматизации богу автоматизации!
Итак, вы автоматизировали проверку на потерю правок, вы автоматизировали создание МРов для черрипиков, давайте пойдем дальше.
Вернемся к примеру в начале заметки: вы создали систему из одного центрального сервера, отдельного инстанса сервиса в каждой поликлинике, и интеграции между ними и успешно всё запустили. Теперь у вас десятки тысяч сервисов (серверов) поликлиник по всей России.
Вся система работает на релизе 1. И вы подготовили релиз 2. Вы обновили центральный сервер на релиз 2. Вы молодцы, не забыли про обратную совместимость и теперь, сервисы поликлиник, работающие на релизе 1, успешно общаются с центральным сервером. Надо обновлять слой сервисов поликлиник.
Это невозможно сделать одномоментно, как будет показано ниже, даже нежелательно делать одномоментно (потеряем преимущества).
Итак, обновление поликлиник происходит со своим темпом. В этот момент ответвляется релиз 3, чтобы его протестировать и начать выкладывать, когда релиз 2 распространится на все поликлиники. Типичный конвейер производства.
В этот момент разъяренный менеджер заходит к вам и предъявляет претензию, что процесс деплоя совершенно непрозрачный. Менеджер не может ответить директору, какой процент поликлиник обновлен до релиза 2, когда планируется завершить обновление и приступить к выкладке релиза 3. Одним словом, сложный процесс обновления с релиза на релиз нуждается в инструментах по визуализации, оценке прогресса, статуса и поставки метрик.
Всё становится еще сложнее, если когда вы начнете практиковать частичные релизы (см. ниже).
Проблему непрозрачности процесса обновления надо решать, и у вас как раз есть уже сервис, в который это можно добавить. Таким образом, сервис защиты от потери правок и автоматизации черрипиков плавно превращается в инструмент, который обеспечивает весь производственный цикл (разработку, тестирование, деплой).
Девопсы через этот сервис выдают команду «обновить поликлиники в городе Энск» и сервис отправляет пакеты, одновременно обновляя метаданные в своей БД и отправляя события или емейлы куда следует.
Картина получается целостная. По итогу, ваш проект будет иметь систему обеспечения производства, которая заточена именно под ваш проект. Думается, что это обосновано, ведь долгоживущие релизы – это сложный жизненный цикл.
Выглядит сложновато, лучше обойтись без долгоживущих релизов
Да, может создаться такое впечатление. Однако, оно скорее ложное. Долгоживущие релизы определяются предметной областью, требованиями заказчика и архитектурой системы. Не надо идти против «природы проекта». А все трудности, что описаны выше не возникают одновременно, с ними не надо бороться сразу, процесс оптимизации и автоматизации производства ПО может быть растянут на месяцы, даже годы. Автоматизируйте, когда почувствуете реальную боль.
Однако у модели долгоживущих релизов есть и существенные преимущества.
Частичный деплой
Если вы выбрали модель долгоживущих релизов из-за того, что приходится обновлять множество компонентов, имеющих состояние (например, сервисы множества поликлиник), то вы имеете преимущество частичного деплоя.
Допустим, вы подготовили к деплою новую версию из существующего релиза, или даже новый релиз. Несмотря на любое тестирование всегда остается риск внести ошибку. Можно снизить риск, организовав выкладку сначала на часть поликлиник.
Поликлиники отличаются по масштабу, в небольших населенных пунктах в поликлинику идет небольшой поток пациентов. Представим себе худшее – что новая версия ПО полностью выводит из строя сервис поликлиники. Разумно в таком случае, первый делой организовать буквально на несколько поликлиник в небольших населенных пунктах и пронаблюдать. После этого – расширить полигон деплоя, а потом – выложить на все поликлиники.
Процесс обновления замедляется, но риск критических ошибок сильно снижается.
Обратная совместимость
Если ваша система – это набор микросервисов с API, то при необходимости breaking changes обычно устанавливают дату во времени, до которой все потребители должны перейти на актуальную версию API, и старая версия API в эту дату будет выключена. Это работает, но легко забыть про такие дедлайны, и неожиданно с утра увидеть, что в системе что-то сломалось.
Долгоживущие релизы – это естественная граница для обеспечения обратной совместимости внутри системы. Вы можете принять на проекте правило, что обратная совместимость обеспечивается только для -N релизов назад. Например, вы можете написать, что обратная совместимость обеспечивается только для предыдущего релиза. Если вдруг из небытия где-то включился клиент с релизом N-2, то он не сможет работать (или вы не даете гарантии на это).
Звучит как невеликое преимущество, однако, в ежедневной горячке и текучке это очень хорошо помогает в решении проблемы «вдруг с утра отпало взаимодействие». Вы точно знаете, что пока не началась выкладка нового релиза, ожидать проблем с обратной совместимостью не надо. Соответственно, при тестировании нового релиза очень трудно забыть протестировать обратную совместимость с предыдущим релизом. Если же проблема обратной совместимости возникла- таки внутри одного релиза, вероятно, это ошибка программистов, так как они нарушили правило совместимости –N и понятно к кому бежать за исправлением.
Можно пойти дальше. Клиенты (в нашем примере, сервисы поликлиник) могут опрашивать версию центрального сервиса и, при обнаружении, что сервер ушел вперед дальше, чем позволено правилом -N, то просить пользователя (например, работника регистратуры) обратиться в техподдержку для срочного обновления.
Если в вашем проекте используются интеграционное тестирование, вы также можете создать целый пул интеграционных тестов, который разворачивает систему предыдущего релиза и обновляет ее, проверяя, как проходит обновление.
Что делать, если один компонент системы устарел сильнее, чем разрешено правилом -N? Есть два подхода: «гарантия не-работы», и «не-гарантия работы». В первом случае, устаревший компонент перестает функционировать и сообщает о том, что необходимо обновление. Во втором случае, он пытается работать (и это часто удается), но есть риск всяких неприятных ошибок. Первый способ лучше в том, что вы никак не забудете обновить всё, что требуется, но страдает доступность. Второй случай повышает доступность, но создает риски труднообнаруживаемых ошибок. Минздрав Минцифры предупреждает: принимайте решения взвешенно!
Срочные фичи
Долгоживующие релизы – это хорошая почва для удобной реализации срочных кровь-из-носу фич.
Допустим, у вас выложен в прод релиз 1, а релиз 2 ответвлен, тестируется, но еще не выложен. Прибегает менеджер с горящими глазами и говорит «бросайте всё, надо делать новую фичу!».
В модели «релиз – это тупиковая ветка», вы можете без всякой сделки с совестью, сделать фичу в релизе 1 в стиле «тяп-ляп и в продакшен». Лишь бы работало, а качество кода не имеет значения. Можете даже не проводить код-ревью, ведь ветка – тупиковая. Когда фича с плохим качеством кода в релизе 1 будет выложена (а релиз 2 задержан), жара спадет и вы можете не черрипикать плохой код в релиз 2 (и master), а сделать в релизе 2 уже нормально, и потом нормальную реализацию черрипикнуть в master.
Таким образом, возникает хороший tradeoff: фича делается быстро, тяп-ляп, и быстро выкладывается, но это никак не влияет на качество системы, ведь эта же фича потом делается повторно (но уже быстрее из-за опыта реализации) и именно качественная реализация в итоге оказывается в master заменит собою «грязную» реализацию.
Это обеспечивает быструю доставку ценности клиентам, ценой 150% затрат (где 100% - затраты на качественную разработку). Кажется, что это недорого для срочных фич.
Жизнеспособность модели
Ну и в конце концов, постоянные вносы фич в «старые» релизы, черрипики фич «вперед», много грязных реализаций и потом переписывания на чистую – это ведь тоже отличная метрика, по которой можно судить о проблемах в процессах производства.
Если на вашем проекте, в каждый «старый» релиз вносятся по нескольку фич, которые потом переписываются в свежих релизах, то это означает, что или модель с долгоживущими релизами вам не подходит, или у вас серьезнейшие проблемы в процессах производства, которые надо решать.
Будьте бдительны, не привыкайте к бардаку.
Итог
Всё, описанное здесь, реализовано на крупном проекте со сложным жизненным циклом и всё это успешно работает. Написанное может пугать сложностью и затратами, но всё не так плохо, как кажется. Затраты размазаны по времени, а полученные преимущества – перманентны. Дорогу осилит идущий, не бойтесь менять реальность вокруг себя.