В 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
- Хотим отличать билд из форка (или feature branch) от релизного. При этом надо добавлять кусок хеша коммита чтобы точно знать откуда он собран
- Для релизов мы решили использовать создание тегов прямо через WEB интерфейс gitlab — это удобно. Но при этом уже не нужен хеш потому что это не очень красиво выглядит для пользователей, а тег в гите нормальный, Read-only (и не требует всяких хуков как в svn) и однозначно идентифицирует коммит, удобным именем, которое мы дали. То есть в версии уже должен использоваться он
- Плюс нам нужен структурированный объект версии, для прописывания в некоторые части системы (js, html) для некоторых тасок
- Нам также необходимо экспортировать как-то эту информацию вовне, для gitlab-ci чтобы он знал какие контейнеры, каких версий нужно поднять на следующих шагах для тестирования
Предлагаемое решение: Gradle plugin net.nemerosa:versioning
Посмотрев вокруг на имеющиеся плагины для gradle, нашёл вот такой прекрасный вариант: github.com/nemerosa/versioning
Его документация сразу подкупает — всё просто, логично и понятно для чего сделано.
Плюс ко всему семантическое разделение на release, feature
Попробуем в деле
Итак подключить к проекту очень просто, следуем инструкции:
plugins {
id 'net.nemerosa.versioning' version '2.4.0'
}
Всё, в большинстве случаев уже можно использовать версию в своих билд-скриптах далее, где она нужна:
version = versioning.info.full
Ну или ближе к делу, скажем в имени war артефакта:
war {
archiveName = "portal-api##${versioning.info.full}.war"
}
После сборки из бранча feature-1 мы получим файл приблизительно следующего именования: portal-api##feature-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. Да, и всячески рекомендую ставить баги или писать реквесты автору — он очень оперативно на них отвечает. Хорошего кодинга!
Комментарии (11)
Hubbitus
13.02.2017 18:41varnav, честно говоря, тут в общем всё просто, но зависит от ваших специфических нужд. На хабре уже есть неплохие стати на эту тему, например Введение в GitLab CI.
По моим впечатлениям, он просто потрясающий. А открытость его разработки заслуживает особого восхищения. Они очень быстро обычно реагируют на баги. Помню ездил на highload++ в этом году, просто к слову пожаловался в беседе что они не правят багу с раннером для приватного репозитория docker что я им ставил… Так они не только исправили в тчении ближайших пару дней, так ещё и написали мне об этом дополнительно.
Однако мне для полного счастья всё же не хватает некоторых вещей. Например для для того чтобы удобнее работать с контейнерами вроде: #25047, #25000.
Но это ж больше хотелки, понятно что они больше сосредоточены на энтерпрайзных вещах вроде kubernetes, автоскалирования в облаке…
А с точки зрения начать использовать — действительно просто по туториалу сконфигурить.
Если у вас есть какие-то конкретные вопросы, то буду рад помочь, а так чтобы на отдельную статью, просто и не знаю что писать…varnav
13.02.2017 19:09Мне просто казалось что они не взаимозаменяемы, т.к. сильно разные по сути.
Hubbitus
13.02.2017 19:39На самом деле, с Jenkins 2.0, когда они у себя также внедрили концепцию пайплайнов (pipline), разрыв значительно уменьшился. На митапе в Яндексе в прошлом году, они очень активно рассказывали как это всем поможет. Теперь не обязательно все билды «накликивать мышкой». Раньше для меня это было просто бедой, хотя и можно было экспортировать конфигурацию в XML. Теперь можно нормально версионировать скажем с помощью того же GIT.
Однако остаётся проблема что всё-таки это отдельно от кода, от проекта. Версионируется отдельно, релиз-циклы как-то надо согласовывать. Тестировать отдельно…
Ну и опять же, gitlab и gitlab-ci имеют из коробки гораздо более плотную интеграцию. Тут вам прогресс билда, и просмотр лога, и артефакты сборки прямо на странице билда, и авторизация общая, тут же можно указать сколько хранить артефакты, чтобы место освободить, тут же и docker-registry свой предлагают (я правда не использую его, сторонним пользуемся. И к слову тут нет автоочистки образов, хотя обещают добавить по моей просьбе). Ну и много других приятных мелочей.varnav
14.02.2017 09:02Однако остаётся проблема что всё-таки это отдельно от кода, от проекта. Версионируется отдельно, релиз-циклы как-то надо согласовывать. Тестировать отдельно…
В смысле?
Ну и опять же, gitlab и gitlab-ci имеют из коробки гораздо более плотную интеграцию. Тут вам прогресс билда, и просмотр лога, и артефакты сборки прямо на странице билда… можно указать сколько хранить артефакты, чтобы место освободить
Это функции Jenkins-а из коробки.Hubbitus
14.02.2017 15:39Это не функции Jenkins никак, если у вас проект в другом месте. В лучшем случае, может быть плагин интеграции, который сможет решить часть проблем. Но в общем gitlab не так много хуков предлагает, чтобы сделать это так же удобным как с его CI. Если вам не предоставляется возможности рядом с коммитом отобразить статус билда из другой системы (исключительно для примера, с другими возможностями также) — то вы это из плагина не сделаете.
Deosis
14.02.2017 06:56Если нужен тэг, можно попробовать использовать команду git describe, которая вернет строку вида Tag-Count-gHash
Hubbitus
14.02.2017 15:42Конечно можно. И парсить вывод. А если бинарного git нет в PATH ещё поставить его или подтянуть зависимость. А потом прикритить логику парсинга имен бранчей, потом логику формирования версий. А потом добавить логику обработки dirty рабочей копии, а потом ввести чтение переменных окружения… А потом понять что этого кода в скрипте билда стало больше чем остального описания билда и вынести это в плагин градл. Ну или взять один из имеющихся.
varnav
переезд из Jenkins на Gitlab-CI — достойно отдельного поста кстати!
nulled
На самом деле нету тут материала на отдельную статью. Уже думал на эту тему.