С переездом из SVN на GIT и gitlab (плюс переезд из Jenkins на Gitlab-CI, но его использование также упомянём), встал вопрос версионирования получаемых артефактов сборки приложения.
В SVN был всем привычный номер ревизии, монотонно увеличивающийся с каждым коммитом. Его было удобно добавлять в номер версии, и это решало большинство проблем. Но git конечно предоставляет множество плюшек, и стоило убеждать руководство и всё команду перевести проект на него…
Зато пришлось отстроить заново процесс версионирования получаемых артефактов сборки.
В итоге остановились на очень хорошем Gradle плагине github.com/nemerosa/versioning, о его использовании я и собираюсь рассказать.
Проблема
У нас в приложении Gradle используется давно, и для SVN просто использовалась наколенная функция, доставшаяся по наследству, написанная прямо в файле build.gradle. Благо, среди других достоинств Gradle можно упомянуть что это прекрасный язык Groovy, и он ничем вас не ограничивает в написании логики билда — подключил необходимые библиотеки из Java мира, и вперёд, хоть всё приложение перепиши в одном файле!
Впрочем, вы же понимаете пагубность такого подхода? Если логика получения номера версии занимает больше 5-10 строчек, а также если мы будем по любому поводу городить свой костыль, то поддерживать это будет просто невозможно очень скоро…
Подобные «решения» ручного парсинга вы можете увидеть например в статье Jenkins для Android на чистой системе и без UI или Через тернии к сборке где предлагается вызывать вручную git describe и парсить вывод регулярными выражениями…
Хотелось чего-то более простого, надёжного и изначально рабочего.
Наш workflow и хотелки
В нашем приложении собирается парочка jar файлов, 3 war артефакта, 3 RPM их включающих, и в конце Docker образ приложения, с установленными RPM, который после автоматического тестирования тут же на gitlab-ci отправляется в приватный репозиторий.
В общем, с переходом на git/gitlab мы придерживаемся логики, унаследованной от стандартного github flow с небольшими изменениями, а это значит для версионирования:
- Мы хотим отличать локальные билды, от билдов на CI
- Хотим отличать билд из форка (или future branch) от релизного. При этом надо добавлять кусок хеша коммита чтобы точно знать откуда он собран
- Для релизов мы решили использовать создание тегов прямо через WEB интерфейс gitlab — это удобно. Но при этом уже не нужен хеш потому что это не очень красиво выглядит для пользователей, а тег в гите нормальный, Read-only (и не требует всяких хуков как в svn) и однозначно идентифицирует коммит, удобным именем, которое мы дали. То есть в версии уже должен использоваться он
- Плюс нам нужен структурированный объект версии, для прописывания в некоторые части системы (js, html) для некоторых тасок
- Нам также необходимо экспортировать как-то эту информацию вовне, для gitlab-ci чтобы он знал какие контейнеры, каких версий нужно поднять на следующих шагах для тестирования
Предлагаемое решение: Gradle plugin net.nemerosa:versioning
Посмотрев вокруг на имеющиеся плагины для gradle, нашёл вот такой прекрасный вариант: github.com/nemerosa/versioning
Его документация сразу подкупает — всё просто, логично и понятно для чего сделано.
Плюс ко всему семантическое разделение на release, future
Попробуем в деле
Итак подключить к проекту очень просто, следуем инструкции:
plugins {
id 'net.nemerosa.versioning' version '2.4.0'
}
Всё, в большинстве случаев уже можно использовать версию в своих билд-скриптах далее, где она нужна:
version = versioning.info.full
Ну или ближе к делу, скажем в имени war артефакта:
war {
archiveName = "portal-api##${versioning.info.full}.war"
}
После сборки из бранча future-1 мы получим файл приблизительно следующего именования: portal-api##future-1.3e46dc.war (в примере используется именование в стиле Tomcat). Варианты настройки и парсинга значений для более интересных ситуаций разберём далее.
Сразу же доступно 2 задачи:
versionDisplay — показывающую информацию и версиях и выводит на консоль. Очень удобно в отладке и versionFile — создающая файл build/version.properties с готовыми переменными, для импорта в bash скрипты вовне:
> ./gradlew versionDisplay
:versionDisplay
[version] scm = git
[version] branch = release/0.3
[version] branchType = release
[version] branchId = release-0.3
[version] commit = da50c50567073d3d3a7756829926a9590f2644c6
[version] full = release-0.3-da50c50
[version] base = 0.3
[version] build = da50c50
[version] display = 0.3.0
> ./gradlew versionFile
> cat build/version.properties
VERSION_BUILD=da50c50
VERSION_BRANCH=release/0.3
VERSION_BASE=0.3
VERSION_BRANCHID=release-0.3
VERSION_BRANCHTYPE=release
VERSION_COMMIT=da50c50567073d3d3a7756829926a9590f2644c6
VERSION_DISPLAY=0.3.0
VERSION_FULL=release-0.3-da50c50
VERSION_SCM=git
просто отлично.
Кастомная логика парсинга версий
Сразу хочется заметить, что имеется множество опций как парсить имена, обрабатывать префиксы, суффиксы, трактовать версии. Там же есть и поддержка SVN к слову. В общем вам в раздел customisation.
Однако, тут не без ложки дёгдя. На момент когда я начинал им пользоваться, документация выглядела иначе.
Да, можно задать своё замыкание как трактовать имя бранча (например 'release/1' считать релизным, а 'qa/0.1' иначе):
versioning {
branchParser = { String branch, String separator = '/' ->
int pos = branch.indexOf(separator)
if (pos > 0) {
new BranchInfo(
type: branch.substring(0, pos),
base: branch.substring(pos + 1))
} else {
new BranchInfo(type: branch, base: '')
}
}
}
Это всё здорово, но мы-то хотим тег вместо бранча, если он есть!?
Я не хотел отказываться от этой идеи. Разумеется запилил временный воркараунд, но автору создал реквест сделать логику парсинга более общей: github.com/nemerosa/versioning/issues/32
Damien Coraboeuf, являющийся автором этого плагина, оказался очень отзывчивым, и оперативным исправив оперативно пару мелких вещей.
В общем же, как это часто бывает, предложил реализовать мне самому, то что я предлагаю.
Я последовал его совету — быстренько сварганил pull request.
Теперь, после его принятия, мы получаем объект информации о коммите SCM (SVN или GIT) и вольны в выборе способа, как нам формировать версию. Например, тот же самый, код что приведён выше, может быть реализован так:
versioning {
releaseParser = { scmInfo, separator = '/' ->
List<String> part = scmInfo.tag.split(separator) + ''
new net.nemerosa.versioning.ReleaseInfo(type: part[0], base: part[1])
}
}
То же самое в замыкании full.
Что нам это даёт? Ну например, как описано было в требованиях, мы используем это для того чтобы брать в одном случае имя бранча, а в другом имя тега, а не ограничены только строковым представлением имени бранча. У нас это сейчас выглядит примерно так:
versioning{
full = { scmInfo ->
// Tag name, or '_branch_name_' if it is not 'master'
(scmInfo.tag ?: ( 'master' == scmInfo.branch ? '' : "_${scmInfo.branch}_." ) + scmInfo.abbreviated).toLowerCase().replaceAll(/[^a-z0-9-_.]/, '_')
}
}
К нижнему регистру приводится для использования в тегах Docker образов.
Как я упоминал, опции этим не исчерпываются, мы также контролируем dirty-суффикс, а также время сборки добавляем в этот же объект, используя meta-магию Groovy…
Интеграция с CI
Ну и раз уж я взялся рассказывать про удобную интеграцию, сразу стоит обратить внимание на один подводный камень, о который я тоже споткнулся. А в этом плагине уже позаботились!
Указанный код работал прекрасно, был протестирован, закоммичен. Но первый же пуш и билд на CI принесли странный результат — именем бранча стало нечто вроде HEAD.
В самом деле причина простая, если посмотрим что делает билдер он же собирает не ветку, а конкретный коммит. На момент сборки, в этой же ветке могут будь уже другие. Поэтому, он всегда делает checkout по имени хеша коммита. Таким образом, мы получаем git репозиторий в состоянии detached head.
Как я, забегая вперёд уже сказал, эта ситуация нормальная и большинство работают так, а в данном плагине просто нужно прописать одну строчку, с указанием имени внешней переменной или переменных, из которых нужно взять настоящее имя бранча, для gitlab-ci мне нужно было просто добавить:
branchEnv = ['CI_BUILD_REF_NAME']
В Jenkins такие переменные также были добавлены достаточно давно по запросу JENKINS-30252. Так, если вы хотите поддержать сразу обе системы, вы можете просто написать:
branchEnv = ['CI_BUILD_REF_NAME', 'GIT_LOCAL_BRANCH']
Я надеюсь вам станет удобнее работать с версиями в gradle. Да, и всячески рекомендую ставить баги или писать реквесты автору — он очень оперативно на них отвечает. Хорошего кодинга!
Автор: Hubbitus