Версионирование артефактов сборки в Gradle используя git имена тегов, бранчей и коммитов

в 19:16, , рубрики: ci, Git, gradle, java, Jenkins, packaging, versioning, Программирование, системы сборки

С переездом из 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

Источник

Поделиться

* - обязательные к заполнению поля