[Перевод] GitFlow и Semantic Versioning на каждый день
Cколько времени я использую GitFlow и Semantic Versioning, меня все не покидает чувство, что чего-то в них не хватает. Обе концепции хороши, но так как они предлагают решения для проблем из разных областей, их совместное использование выглядит сложнее, чем должно быть.
Возможно причина в том, что я выбрал не самый оптимальный путь, и это может стать хорошей темой для будущего поста. В этом же я хочу описать простой подход к управлению релизами приложений и библиотек.
Проблема
Как убедиться, что релизная сборка не попадет на продакшн без надлежащего интеграционного тестирования? Как убедиться, что NuGet пакет не будет опубликован, пока не пройдет проверку статическим анализатором?
Большинство инструментов, которые могли бы решить эти проблемы, уже существуют, и мы зачастую используем их “в ручном режиме”. По сути, нужно лишь слегка все это автоматизировать, но для этого требуются ввести соответствующие соглашения и паттерны.
Инструменты
Опишем текущее положение дел.
Окружения
До тех пор, пока у нас нет возможности или инфраструктуры для тестирования на продакшне, классическим подходом будет использование различных окружений (environments) для разных этапов жизни релиза, таких как интеграционные или приемочные тесты. Было бы хорошо всегда быть уверенным в том, что конкретная сборка приложения сможет попасть только на соответствующее ей окружение. Для этого нам нужна возможность однозначной идентификации каждой версии.
Semantic Versioning
Перед тем, как углубляться в детали того, чему именно мы должны присваивать версии, нужно подумать о том, как в целом версионировать софт. Эта тема не нова, и к счастью существует Semantic Versioning, правда в версиях которого еще пойди разберись.
Приятным бонусом для .NET разработчиков является практически полная поддержка данного подхода NuGet-ом и большинством публичных пакетов.
GitFlow
Для большинства проектов, с которыми мне доводилось работать, GitFlow кажется мне достаточно естественным выбором. Если же он оказывается слишком сложным из-за частоты или количества изменений, то всегда можно “откатиться” к GitHub Flow, где каждое изменение является так называемым “хот фиксом”.
Хочу заметить, что для этой модели Git вовсе не обязателен. О чем собственно и пишут ALM Rangers. Хотя, с Git такая модель работает проще, чем с Subversion.
Не буду очень подробно рассматривать различные стратегии работы с ветками, которые и так отлично задокументированны.
О чем стоит забыть
В старые добрые времена, когда большинство из упомянутых выше инструментов отсутствовали или не так широко применялись, обычным делом был поэтапный деплой одной версии через все окружения. Одним из таких подходов был коммит бинарных файлов в специальный репозиторий, и дальнейшие продвижение через ветки, соответствующие разным окружениям. Иногда это приводило к бессмысленным мерджам и даже конфликтам в конфигурационных файлах. Нередко это было “приправленно” кастомными деплоймент-скриптами для Robocopy.
Большое количество ручных операций подвержено ошибкам. Как правило, если существует возможность допустить ошибку, то скорее всего она будет допущена. Поэтому, пожалуйста, забудьте этот подход, как и все что вы прочитали в предыдущем параграфе.
Следующий подход
Давайте рассмотрим, как мы можем применить вышеупомянутые концепции вместе с соответствующими инструментами.
Артефакты
Для удобной индентификации мы версионируем артефакты, производимые билд сервером: билды и библиотеки. Базовым требованием является возможность связать артефакт с источником изменения (хэш коммита в Git-е) и процессом породившим артефакт (идентификатор билда). У большинства CI инструментов имеются внутренние счетчики сборок, что делает эту информацию доступной.
Для работы с переиспользуемыми компонентами (библиотеками) стандартом в .NET мире является пакетный менеджер (NuGet). Использование деплоймент менеджера, например Octopus Deploy, позволяет применить тот же концепт для всех поставляемых артефактов. Но как именно собрать и связать эту мета-информацию и артефакт?
Ветки и релизы
Вместо использования веток, соответствующих окружениям, давайте создавать артефакты, которые уже сами содержат в себе информацию о том, на какое именно окружение они должны быть задеплоины. Иными словами, мы свяжем конкретный артефакт с конкретным окружением. Зная для чего используется каждое из окружений, и имея эту мета-информацию, мы можем автоматически принимать решения о следующем этапе деплоймента.
Для начала нам нужно определить, на сколько “стабилен” артефакт, построенный из конкретной ветки:
Ветка | Тип релиза | Формат версии |
---|---|---|
feature | alpha | #major.#minor.#revision-a#build#feature |
develop | beta | #major.#minor.#revision-b#build |
release | release candidate | #major.#minor.#revision-r#build |
hotfix | release candidate | #major.#minor.#revision-r#build |
master | stable | #major.#minor.#revision |
Используя приведенный выше формат мы можем описать assembly metadata, например в общем для всех сборок (assebmly) файле в проекте.
[assembly: AssemblyVersion("#major.#minor")]
[assembly: AssemblyFileVersion("#major.#minor.#revision.#build")]
[assembly: AssemblyInformationalVersion("#major.#minor.#revision[-prerelease]")]
Для AssmeblyInformationalVersion
мы используем формат, определенный в предыдущей таблице.
Таким образом версии пронумерованны в соответствии с Semanic Versioning как для рантайма, так и для идентификации CI билда и для версии пакета с результатами.
На примере GitFlow-based шагов, версии NuGet пакетов, производимых CI системой после коммита в соответствующую ветку, будут выглядить следующим образом:
Задача | Ветка | Номер CI билда | Версия NuGet пакета |
---|---|---|---|
Implement feature #1 | feature-1 | 1 | 1.2.0-a1feature1 |
Implement feature #2 | feature-2 | 2 | 1.2.0-a2feature2 |
Implement feature #1 | feature-1 | 3 | 1.2.0-a3feature1 |
Complete feature #1 | develop | 4 | 1.2.0-b4 |
Complete feature #2 | develop | 5 | 1.2.0-b5 |
Stabilize release | release-1.2.0 | 6 | 1.2.0-r6 |
Release to production | master | 7 | 1.2.0 |
Fix production issue | hotfix-1.2.1 | 8 | 1.2.1-r8 |
Release to production | master | 9 | 1.2.1 |
Данный паттерн хорошо отражает естественный порядок возрастания готовности релиза при сортировке пакетов по версии.
При создании релиза или хотфикса, я предпочитаю увеличивать номер версии вручную в общем файле с метаданными и коммитить его в репозиторий. Таким образом номер версии будет актуален и для локальных копий проекта.
Deployments
Важным моментом является защита от случайного деплоя не на то окружение. В качестве первой линии обороны может выступать CI сервер, аварийно завершающий билд, который не проходит юнит-тесты. Кроме того, при использовании GitHub можно обратить внимание на так называемые protected branches.
В Octopus Deploy существует концепция lifecycles, которая позволяет нам поэтапно проводить релиз через разные окружения. Однако, при подходе, рассматриваемом в данной статье, мы получаем различные артефакты для каждого из этапов деплоя. Это значит, что нам нужно отслеживать, чтобы каждый артефакт деплоился только на разрешенные ему окружения.
Тип релиза | Разрешенные окружения |
---|---|
alpha | D |
beta | T,D |
release candidate | A,T,D |
stable | P,A,T,D |
Если наш Deployment manager не поддерживает подобную настройку разных жизненных циклов в зависимости от версии билда на нативном уровне, то как правило можно реализовать ее самостоятельно в виде несложного скрипта. Кодируя информацию о типе билда в версию сборки, мы получаем простой и надежный инструмент для “безопасного” и автоматизированного создания релизов и их деплоя. Такая автоматизация возможна, как и при использовании Automatic Release Creation в Octopus Deploy, так и при подхоте c так называемым Release Train, запускаемым по расписанию.
Альтернативы
Одинаковые номера у релиза и у версии пакета выглядит подозрительно. По сути здесь отсутствует разделение концепций релиза и артефакта. Особенно это заметно в случае NuGet пакетов для билиотек, где хорошо бы иметь отдельную схему нумерации версий. Такая схема может опираться на тэги в VCS (вы же отмечаете релизы тегами?).
Относительно недавно я наткнулся на GitVersion, который поддерживает подобные концепции нативно, и при этом предоставляет возможность гибкой настройки. Очевидно, есть смысл обратить внимание и на него.
Заключение
В данной статье мы рассмотрели простой подход к управлению релизными сборками приложений и отдельных компонент (библиотек). Надеюсь эти идеи пригодятся вам в том или ином виде.
[Примечание переводчика] Вот такой получился неоднозначный подход к управлению релизами. Коллеги, предлагаю вам рассказать, как же устроены релизы и версионирование у вас?
Ссылки