Настройка GitLab CI для загрузки java проекта в maven central
Данная статья рассчитана на java разработчиков, у которых возникла потребность быстро публиковать свои продукты в репозиториях sonatype и/или maven central с использованием GitLab. В данной статье я расскажу про настройку gitlab-runner, gitlab-ci и maven-plugin для решения данной задачи.
Предпосылки:
Безопасное хранение mvn и GPG ключей.
Безопасное выполнение публичных CI задач.
Загрузка артефактов (release/snapshot) в публичные репозитории.
Автоматическая проверка release-версий для публикации в maven central.
Общее решение по загрузке артефактов в репозиторий для нескольких проектов.
Подробное описание механизма публикации артефактов в Maven Central через Sonatype OSS Repository Hosting Service уже описано в данной статье пользователем Googolplex, поэтому в нужных местах буду ссылаться на данную статью.
Предварительно регистрируемся в Sonatype JIRA и заводим тикет на открытие репозитория (более подробно читать раздел Создаём тикет на Sonatype JIRA). После открытия репозитория пара логин/пароль от JIRA (далее учетная запись Sonatype) будет использоваться для загрузки артефактов в Sonatype nexus.
Если вы используете Linux консоль для генерации GPG ключа (gnupg/gnupg2), то необходимо установить rng-tools для генерации энтропии. В противном случае генерация ключа может проходить очень долго.
В первую очередь необходимо создать и настроить проект, в котором будет храниться pipeline, для деплоя артефактов. Свой проект я назвал просто и незамысловато — deploy
После создания репозитория, необходимо ограничить доступ на изменение репозитория.
Переходим в проект -> Settings -> Repository -> Protected Branches. Удаляем все правила и добавляем единственное правило с Wildcard * с правом на push и merge только для пользователей с ролью Maintainers. Данное правило будет работать для всех пользователей как данного проекта, так и группы в которую данный проект входит.
Если мейнтейнеров несколько, то лучшим решением будет ограничить доступ к проекту в принципе.
Переходим в проект -> Settings -> General -> Visibility, project features, permissions и выставляем Project visibility в значение Private.
У меня проект в публичном доступе, так как я использую собственный GitLab Runner и доступ на изменение репозитория есть только у меня. Ну и собственно не в моих интересах светить приватную информацию в публичных pipeline-логах.
Ужесточение правил на изменение репозитория
Переходим в проект -> Settings -> Repository -> Push Rules и устанавливаем флаги Committer restriction, Check whether author is a GitLab user. Так же рекомендую настроить подпись коммитов, и установить флаг Reject unsigned commits.
Далее требуется настроить триггер для запуска задач
Переходим в проект -> Settings -> CI / CD -> Pipeline triggers и создаем новый trigger-token
Данный токен можно сразу добавить в общую конфигурацию переменных для группы проектов.
Переходим в группу -> Settings -> CI / CD -> Variables и добавляем переменную DEPLOY_TOKEN с trigger-token в значении.
В данном разделе описана конфигурация для запуска задач на deploy с использованием собственного (Specific) и публичного (Shared) раннера.
Specific Runner
Я использую собственные раннеры, так как в первую очередь это удобно, быстро, дешево.
Для раннера рекомендую линуксовую VDS с 1 CPU, 2 GB RAM, 20 GB HDD. Цена вопроса ~3000₽ в год.
Мой раннер
Для раннера я взял VDS 4 CPU, 4 GB RAM, 50 GB SSD. Обошлась ~11000₽ и ни разу не пожалел.
У меня в общей сложности 7 машинок. 5 на aruba и 2 на ihor.
Итак, у нас есть раннер. Теперь мы его будем настраивать.
Заходим на машинку по SSH и устанавливаем java, git, maven, gnupg2.
Создаем директорию для maven кэша и навешиваем права группы runner
Этот пункт можно пропустить, если вы не планируете запускать несколько раннеров на одной машине.
Runtime platform arch=amd64 os=linux pid=17594 revision=3001a600 version=11.10.0
Running in system-mode.
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):
https://gitlab.com/
Please enter the gitlab-ci token for this runner:
REGISTRATION_TOKEN
Please enter the gitlab-ci description for this runner:
[ih1174328.vds.myihor.ru]: Deploy Runner
Please enter the gitlab-ci tags for this runner (comma separated):
deploy
Registering runner... succeeded runner=ZvKdjJhx
Please enter the executor: docker-ssh, parallels, virtualbox, docker-ssh+machine, kubernetes, docker, ssh, docker+machine, shell:
shell
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!
Проверяем, что раннер зарегистрирован. Переходим на сайт gitlab.com -> deploy-project -> Settings -> CI/CD -> Runners -> Specific Runners -> Runners activated for this project
Скрин
Добавляем отдельный сервис /etc/systemd/system/gitlab-deployer.service
Генерируем ключ отвечая на вопросы. Я использовал собственные имя и почту.
Обязательно указываем пароль для ключа. Данным ключом будут подписываться артефакты.
gpg --gen-key
Проверяем
gpg --list-keys -a
/home/gitlab-deployer/.gnupg/pubring.gpg
----------------------------------------
pub 4096R/00000000 2019-04-19
uid Petruha Petrov <[email protected]>
sub 4096R/11111111 2019-04-19
Загружаем наш публичный ключ на сервер ключей
gpg --keyserver keys.gnupg.net --send-key 00000000
gpg: sending key 00000000 to hkp server keys.gnupg.net
Создаем диреторию maven repository и линкуем с кэшем (не ошибитесь)
Этот пункт можно пропустить, если вы не планируете запуска несколько раннеров на одной машине.
Добавляем в корень deploy-проекта файл .gitlab-ci.yml
В скрипте представлено две взаимоисключающие задачи на деплой. Specific Runner или Shared Runner соответственно.
.gitlab-ci.yml
stages:
- deploy
Specific Runner:
extends: .java_deploy_template
# Задача будет выполняться на вашем shell-раннере
tags:
- deploy
Shared Runner:
extends: .java_deploy_template
# Задача будет выполняться на публичном docker-раннере
tags:
- docker
# Образ из раздела GitLab Runner -> Shared Runner -> Docker
image: registry.gitlab.com/group/deploy-project:latest
before_script:
# Импортируем GPG ключ
- printf "${GPG_SECRET_KEY}" | gpg --batch --import
# Сохраняем maven конфигурацию
- printf "${SETTINGS_SECURITY_XML}" > ~/.m2/settings-security.xml
- printf "${SETTINGS_XML}" > ~/.m2/settings.xml
.java_deploy_template:
stage: deploy
# Задача сработает по триггеру, если передана переменная DEPLOY со значением java
only:
variables:
- $DEPLOY == "java"
variables:
# отключаем клонирование текущего проекта
GIT_STRATEGY: none
script:
# Предоставляем возможность хранения пароля в незашифрованном виде
- git config --global credential.helper store
# Сохраняем временные креды пользователя gitlab-ci-token
# Токен работает для всех публичных проектов gitlab.com и для проектов группы
- echo "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com" >> ~/.git-credentials
# Полностью чистим текущую директорию
- rm -rf .* *
# Клонируем проект который, будем деплоить в Sonatype Nexus
- git clone ${DEPLOY_CI_REPOSITORY_URL} .
# Переключаемся на нужный коммит
- git checkout ${DEPLOY_CI_COMMIT_SHA} -f
# Если хоть один pom.xml содержит параметр autoReleaseAfterClose валим сборку.
# В противном случае есть риск залить сырые артефакты в maven central
- >
for pom in $(find . -name pom.xml); do
if [[ $(grep -q autoReleaseAfterClose "$pom" && echo $?) == 0 ]]; then
echo "File $pom contains prohibited setting: <autoReleaseAfterClose>";
exit 1;
fi;
done
# Если параметр DEPLOY_CI_COMMIT_TAG пустой, то принудительно ставим SNAPSHOT-версию
- >
if [[ "${DEPLOY_CI_COMMIT_TAG}" != "" ]]; then
mvn versions:set -DnewVersion=${DEPLOY_CI_COMMIT_TAG}
else
VERSION=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec)
if [[ "${VERSION}" == *-SNAPSHOT ]]; then
mvn versions:set -DnewVersion=${VERSION}
else
mvn versions:set -DnewVersion=${VERSION}-SNAPSHOT
fi
fi
# Запускаем задачу на сборку и деплой артефактов
- mvn clean deploy -DskipTests=true
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>jar</goal>
</goals>
<!-- Генерация javadoc должна быть после фазы генерации ресурсов -->
<phase>prepare-package</phase>
<configuration>
<!-- Очень помогает в публичных проектах -->
<failOnError>true</failOnError>
<failOnWarnings>true</failOnWarnings>
<!-- Убирает ошибку поиска документации в target директории -->
<detectOfflineLinks>false</detectOfflineLinks>
</configuration>
</execution>
</executions>
</plugin>
Если у вас есть модуль, который не содержит java (например только ресурсы)
Или вы не хотите в принципе генерировать javadoc, то в помощь maven-jar-plugin
Если у вас многомодульный проект, и вам нет необходимости загружать определенный модуль в репозиторий, то в pom.xml данного модуля необходимо добавить nexus-staging-maven-plugin с флагом skipNexusStagingDeployMojo
<repositories>
<repository>
<id>SonatypeNexus</id>
<url>https://oss.sonatype.org/content/groups/staging/</url>
<!-- Не надо указывать флаги snapshot/release для репозитория -->
</repository>
</repositories>
Еще плюсы
Очень богатый список целей для работы с nexus репозиторием (mvn help:describe -Dplugin=org.sonatype.plugins:nexus-staging-maven-plugin).
Автоматическая проверка релиза на возможность загрузки в maven central
При установке тега, автоматически триггерится соответствующая задача в проекте deploy на загрузку релизной версии в nexus (пример).
Самое приятное, что автоматически срабатывает close release в nexus.
[INFO] Performing remote staging...
[INFO]
[INFO] * Remote staging into staging profile ID "9043b43f77dcc9"
[INFO] * Created staging repository with ID "orgtouchbit-1037".
[INFO] * Staging repository at https://oss.sonatype.org:443/service/local/staging/deployByRepositoryId/orgtouchbit-1037
[INFO] * Uploading locally staged artifacts to profile org.touchbit
[INFO] * Upload of locally staged artifacts finished.
[INFO] * Closing staging repository with ID "orgtouchbit-1037".
Waiting for operation to complete...
.........
[INFO] Remote staged 1 repositories, finished with success.
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] Shields4J 1.0.0 .................................... SUCCESS [ 9.603 s]
[INFO] test-core .......................................... SUCCESS [ 3.419 s]
[INFO] Shields4J client ................................... SUCCESS [ 9.793 s]
[INFO] TestNG listener 1.0.0 .............................. SUCCESS [01:23 min]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:47 min
[INFO] Finished at: 2019-04-21T04:05:46+03:00
[INFO] ------------------------------------------------------------------------
И если что-то пошло не так, то задача обязательно завалится
[INFO] Performing remote staging...
[INFO]
[INFO] * Remote staging into staging profile ID "9043b43f77dcc9"
[INFO] * Created staging repository with ID "orgtouchbit-1038".
[INFO] * Staging repository at https://oss.sonatype.org:443/service/local/staging/deployByRepositoryId/orgtouchbit-1038
[INFO] * Uploading locally staged artifacts to profile org.touchbit
[INFO] * Upload of locally staged artifacts finished.
[INFO] * Closing staging repository with ID "orgtouchbit-1038".
Waiting for operation to complete...
.......
[ERROR] Rule failure while trying to close staging repository with ID "orgtouchbit-1039".
[ERROR]
[ERROR] Nexus Staging Rules Failure Report
[ERROR] ==================================
[ERROR]
[ERROR] Repository "orgtouchbit-1039" failures
[ERROR] Rule "signature-staging" failures
[ERROR] * No public key: Key with id: (1f42b618d1cbe1b5) was not able to be located on <a href=http://keys.gnupg.net:11371/>http://keys.gnupg.net:11371/</a>. Upload your public key and try the operation again.
...
[ERROR] Cleaning up local stage directory after a Rule failure during close of staging repositories: [orgtouchbit-1039]
[ERROR] * Deleting context 9043b43f77dcc9.properties
[ERROR] Cleaning up remote stage repositories after a Rule failure during close of staging repositories: [orgtouchbit-1039]
[ERROR] * Dropping failed staging repository with ID "orgtouchbit-1039" (Rule failure during close of staging repositories: [orgtouchbit-1039]).
[ERROR] Remote staging finished with a failure: Staging rules failure!
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] Shields4J 1.0.0 .................................... SUCCESS [ 4.073 s]
[INFO] test-core .......................................... SUCCESS [ 2.788 s]
[INFO] Shields4J client ................................... SUCCESS [ 3.962 s]
[INFO] TestNG listener 1.0.0 .............................. FAILURE [01:07 min]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
В резултате нам остается единственный выбор. Или удалить данную версию или опубликовать.
После релиза, через некоторое время артефакты окажутся в
офтоп
Для меня было открытием, что maven индексирует другие публичные репозитории.
Пришлось подкинуть robots.txt, так как он проиндексировал мой старый репозиторий.
Отдельный deploy-проект в котором можно реализовать несколько CI задач на загрузку артефактов в публичные репозитории для различных языков разработки.
Deploy-проект изолирован от постороннего вмешательства и может быть изменен только пользователями с ролью Owner и Maintainer.
Отдельный Specific Runner с "горячим" кэшем для запуска только deploy задач.
Публикация snapshot/release версий в публичном репозитории.
Автоматическая проверка release версии на готовность к публикации в maven central.
Защита от автоматической публикации "сырых" версий в maven central.
Сборка и публикация snapshot версий "по клику".
Единый репозиторий для получения snapshot/release версий.
Общий пайплайн на сборку/тестирование/публикацию java проекта.
Настройка GitLab CI не такая сложная тема как кажется на первый взгляд. Достаточно пару раз настроить CI "под ключ" и вот, ты уже далеко не дилетант в этом деле. Тем более GitLab документация весьма избыточна. Не бойтесь делать первый шаг. Дорога возникает под шагами идущего (не помню кто сказал 🙂 ).
Буду рад фидбэку.
В следующей статье расскажу о том, как настроить GitLab CI для конкурентного запуска задач с интеграционными тестами (с запуском тестируемых сервисов при помощи docker-compose), если у вас есть только один shell раннер.