Круги ада с GitHub Actions (строим CI/CD pipeline для Java-проекта)
Мне частенько приходится строить пайплайн для сборки проектов на Java. Иногда это опенсорс, иногда нет. Недавно я решил попробовать перенести часть своих репозиториев с Travis-CI и TeamCity на GitHub Actions, и вот что из этого получилось.
Что будем автоматизировать
Для начала нам нужен проект, который мы будем автоматизировать, давайте сделаем небольшое приложение на Spring boot / Java 11 / Maven. В рамках этой статьи логика приложения нас интересовать не будет совсем, нам важна инфраструктура вокруг приложения, так что нам хватит простенького REST API контроллера.
Посмотреть исходники можно тут: github.com/antkorwin/github-actions все этапы построения pipeline-конвейера отражены в пулл-реквестах этого проекта.
JIRA и планирование
Стоит сказать, что мы обычно используем JIRA в качестве трекера задач, так что давайте заведем отдельную борду под этот проект и накидаем туда первые задачи:
Чуть позже мы еще вернемся к тому, что интересного могут дать в связке JIRA и GitHub.
Автоматизируем сборку проекта
Наш тестовый проект собирается через maven, так что сборка его довольно простая, все, что нам нужно, это mvn clean package.
Чтобы сделать это при помощи Github Actions, нам нужно будет создать в репозитории файл с описанием нашего workflow, это можно сделать обычным yml-файлом, не могу сказать что мне нравится «программирование на yml», но что поделать — делаем в директории .github/workflow/ файл build.yml в котором будем описывать действия при сборке мастер ветки:
on — это описание события, по которому будет запускаться наш скрипт.
on: pull_request / push — говорит о том, что этот workflow нужно запускать при каждом пуше в мастер и создании пулл-реквестов.
Дальше идет описание заданий (jobs) и шаги выполнения (steps) для каждой задачи.
runs-on — тут мы можем выбрать целевую ОС, на удивление можно выбрать даже Mac OS, но на приватных репозиториях это довольно дорогое удовольствие (в сравнении с linux).
uses позволяет переиспользовать другие экшены, так например при помощи экшена actions/setup-java мы устанавливаем окружение для Java 11.
При помощи with мы можем указать параметры с которыми запускаем действие, по сути это аргументы, которые будут передаваться в экшен.
Остается только запустить мавеном сборку проекта: run: mvn -B clean package флаг -B говорит о том, что нам нужен non-interactive mode, чтобы мавен вдруг не захотел что-то у нас спросить
Отлично! Теперь при каждом коммите в мастер, запускается сборка проекта.
Автоматизируем запуск тестов
Сборка это хорошо, но в реальности проект может благополучно собираться, но не работать. Поэтому следующим шагом нужно заняться автоматизацией прогона тестов. К тому же, довольно удобно смотреть результат прохода тестов, когда делаешь ревью PR — ты точно знаешь, что тесты проходят и никто не забыл, перед тем как делать merge, прогнать свою ветку.
Делаем запуск тестов при создании пулл-реквеста и merge в мастер, а заодно добавим построение отчета о code-coverage.
Для покрытия тестов я использую codecov в связке с jacoco плагином. У codecov есть свой экшен, но ему для работы с нашим pull-request-ом нужен токен:
${{ secrets.CODECOV_TOKEN }} — такую конструкцию мы будем встречать еще не один раз, secrets это механизм хранения секретов в гитхабе, мы можем там прописать пароли/токены/хосты/url-ы и прочие данные, которыми не стоит светить в кодовой базе репозитория.
Добавить переменную в secrets, можно в настройках репозитория на GitHub:
Получить токен можно на codecov.io после авторизации через GitHub, для добавления public проекта нужно просто пройти по ссылке вида: GitHub user name/[repo name]. Приватный репозиторий тоже можно добавить, для этого надо дать права codecov приложению в гитхабе.
Теперь в каждый наш пулл-реквест будет заходить codecov бот и добавлять график изменения покрытия:
Добавим статический анализатор
В большинстве своих oпенсорс-проектов я использую sonar cloud для статического анализа кода, его довольно легко подключить к travis-ci. Так что это логичный шаг при миграции на GitHub Actions, сделать тоже самое. Маркет экшенов — клевая штука, но в этот раз он немного подвел, потому что я по привычке нашел нужный экшен и прописал его в workflow. А оказалось, что sonar не поддерживает работу через действие для анализа проектов на maven или gradle. Об этом конечно написано в документации, но кто же ее читает?!
Через действие нельзя, поэтому будем делать через mvn плагин:
SONAR_TOKEN — можно получить в sonarcloud.io и нужно прописать его в secrets. GITHUB_TOKEN — это встроенный токен, который генерит гитхаб, с помощью него sonarcloud[bot] сможет авторизоваться в гите, чтобы оставлять нам сообщения в пулл-реквестах.
Dsonar.projectKey — название проекта в сонаре, посмотреть можно в настройках проекта.
Dsonar.organization — название организации из GitHub.
Делаем пулл-реквест и ждем, когда sonarcloud[bot] придет в комментарии:
Release management
Билд настроили, тесты прогнали, можно и релиз сделать. Давайте посмотрим, как GitHub Actions помогает существенно упростить release managеment.
На работе у меня есть проекты, кодовая база которых лежит в bitbucket(все как в той истории «днем пишу в битбакет, ночью коммичу в GitHub»). К сожалению, в bitbucket нет встроенных средств для управления релизами. Это проблема, потому что под каждый релиз приходится руками заводить страничку в confluence и скидывать туда все фичи вошедшие в релиз, шерстить чертоги разума, таски в jira, коммиты в репозитории. Шансов ошибиться много, можно что-то забыть или вписать то, что уже релизили в прошлый раз, иногда просто не понятно, к чему отнести какой-то пулл-реквест — это фича или фикс багов, или правка тестов, или что-то инфраструктурное.
Как нам может помочь GitHub actions? Есть отличный экшен — release drafter, он позволяет задать шаблон файла release notes, чтобы настроить категории пулл-реквестов и автоматически группировать их в release notes файле:
Пример шаблона для настройки отчета(.github/release-drafter.yml):
name-template: 'v$NEXT_PATCH_VERSION'
tag-template: 'v$NEXT_PATCH_VERSION'
categories:
- title: ' New Features'
labels:
- 'type:features'
# в эту категорию собираем все PR с меткой type:features
- title: ' Bugs Fixes'
labels:
- 'type:fix'
# аналогично для метки type:fix и т.д.
- title: ' Documentation'
labels:
- 'type:documentation'
- title: ' Configuration'
labels:
- 'type:config'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
template: |
## Changes
$CHANGES
Добавляем скрипт для генерации черновика релиза (.github/workflows/release-draft.yml):
Все пулл-реквесты с этого момента будут собираться в release notes автоматически — magic!
Тут может возникнуть вопрос: а что если разработчики забудут проставить метки в PR? Тогда непонятно, в какую категорию его отнести, и опять придется разбираться вручную, с каждым PR-ом отдельно. Чтобы исправить эту проблему, мы можем воспользоваться еще одним экшеном — label verifier — он проверяет наличие тэгов на пулл-реквесте. Если нет ни одного обязательного тэга, то проверка будет завалена и сообщение об этом мы увидим в нашем пулл-реквесте.
Теперь любой pull-request нужно пометить одним из тэгов: type:fix, type:features, type:documentation, type:tests, type:config.
Авто-аннотирование пулл-реквестов
Раз уж мы коснулись такой темы как эффективная работа с пулл-реквестами, то стоит сказать еще о таком экшене, как labeler, он проставляет метки в PR на основании того, какие файлы были изменены. Например, мы можем пометить как [build] любой пул-реквест в котором есть изменения в каталоге .github/workflow.
Подружить действие автоматически проставляющее метки в пулл-реквесты и действие, проверяющее наличие обязательных меток, у меня не вышло, match-label на отрез не хочет видеть проставленные ботом метки. Похоже проще написать свое действие, совмещающее оба этапа. Но даже в таком виде пользоваться довольно удобно, нужно выбрать метку из списка при создании пулл-реквеста.
Пора деплоить
Я попробовал несколько вариантов деплоя через GitHub Actions (через ssh, через scp, и при помощи docker-hub), и могу сказать, что, скорее всего, вы найдете способ залить бинарку на сервер, каким бы извращенным не был ваш pipeline.
Мне понравился вариант держать всю инфраструктуру в одном месте, поэтому рассмотрим, как сделать деплой в GitHub Packages (это репозиторий для бинарного контента, npm, jar, docker).
Cкприпт сборки docker образа и публикации его в GitHub Packages:
Для начала нам надо собрать JAR-файл нашего приложения, после чего мы вычисляем путь к GitHub docker registry и название нашего образа. Тут есть несколько хитростей, с которыми мы еще не сталкивались:
конструкция вида: echo «::set-output name=NAME::VALUE» позволяет задать значение переменной в текущем шаге, так чтобы его потом можно было прочитать во всех остальных шагах.
получить значение переменной установленой на предыдущем шаге можно через идентификатор этого шага: ${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}
В стандартной переменной GITHUB_REPOSITORY хранится название репозитория и его владелец («owner/repo-name»). Для того чтобы вырезать из этой строки все кроме названия репозитория, воспользуемся bash синтаксисом: ${GITHUB_REPOSITORY#*/}
Для того чтобы указать версию образа, мы используем первые цифры из SHA-хэша коммита — GITHUB_SHA тут тоже есть нюансы, если вы будете делать такие сборки не только при merge в master, а еще и по событию создания пулл-реквеста, то SHA может не совпадать с хэшем, который мы видим в истории гита, потому что действие actions/checkout делает свой уникальный хэш, чтобы избежать взаимных блокировок действий в PR.
Если все получилось благополучно, то открыв раздел packages (https://github.com/antkorwin/github-actions/packages) в репозитории, вы увидите новый докер образ:
Там же можно посмотреть список версий докер-образа.
Остается только настроить наш сервер на работу с этим registry и запустить перезапуск сервиса. О том как это сделать через systemd, я, пожалуй, расскажу в другой раз.
Мониторинг
Давайте посмотрим несложный вариант, как делать health check нашего приложения при помощи GitHub Actions. В нашем бутовом приложении есть actuator, так что API для проверки его состояния даже и писать не надо, для ленивых уже все сделали. Нужно только дернуть хост: SERVER-URL:PORT/actuator/health
Проверку статуса сервера сделаем руками через curl:
jobs:
ping:
runs-on: ubuntu-18.04
steps:
- name: curl actuator
id: ping
run: |
echo "::set-output name=status::$(curl ${{secrets.SERVER_HOST}}/api/actuator/health)"
- name: health check
run: |
if [[ ${{ steps.ping.outputs.status }} != *"UP"* ]]; then
echo "health check is failed"
exit 1
fi
echo "It's OK"
Сначала сохраняем в переменную то, что ответил сервер на запрос, на следующем шаге проверяем что статус UP и, если это не так, то выходим с ошибкой. Если нужно руками «завалить» действие, то exit 1 — подходящее оружие.
- name: send alert in telegram
if: ${{ failure() }}
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_TO }}
token: ${{ secrets.TELEGRAM_TOKEN }}
message: |
Health check of the:
${{secrets.SERVER_HOST}}/api/actuator/health
failed with the result:
${{ steps.ping.outputs.status }}
Отправку в телеграм делаем, только если действие завалилось на предыдущем шаге. Для отправки сообщения используем appleboy/telegram-action, о том, как получить токен бота и id чата можно почитать в документации: github.com/appleboy/telegram-action
Не забудьте прописать в секретах на гитхабе: URL для сервера и токены для телеграм бота.
Бонус трек — JIRA для ленивых
Я обещал что мы вернемся к JIRA, и мы вернулись. Сотни раз наблюдал на стендапах ситуацию, когда разработчики сделали фичу, слили ветку, но забыли перетянуть задачу в JIRA. Конечно, если бы все это делалось в одном месте, то было бы проще, но фактически мы пишем код в IDE, сливаем ветки в bitbucket или GitHub, а задачи потом таскаем в Jira, для этого надо открывать новые окна, иногда логиниться еще раз и т.д. Когда ты прекрасно помнишь, что надо делать дальше, то открывать борду лишний раз нет смысла. В итоге, утром на стендапе надо тратить время на актуализацию доски задач.
GitHub поможет нам и в этом рутинном занятии, для начала мы можем перетягивать задачи автоматом, в колонку code_review, когда закинули пулл-реквест. Все, что нужно — это придерживаться соглашения в наименовании веток:
[имя проекта]-[номер таска]-название
например, если ключ проекта «GitHub Actions» будет GA, то GA-8-jira-bot может быть веткой для реализации задачи GA-8.
Интеграция с JIRA работает через экшены от Atlassian, они не идеальны, надо сказать, что некоторые из них у меня вообще не заработали. Но мы обсудим только те, что точно работают и активно используются.
Для начала нужно пройти авторизацию в JIRA при помощи действия: atlassian/gajira-login
- name: Find Issue
id: find_issue
shell: bash
run: |
echo "::set-output name=ISSUE_ID::$(echo ${GITHUB_HEAD_REF} | egrep -o 'GA-[0-9]{1,4}')"
echo brach name: $GITHUB_HEAD_REF
echo extracted issue: ${GITHUB_HEAD_REF} | egrep -o 'GA-[0-9]{1,4}'
- name: Check Issue
shell: bash
run: |
if [[ "${{steps.find_issue.outputs.ISSUE_ID}}" == "" ]]; then
echo "Please name your branch according to the JIRA issue: [project_key]-[task_number]-branch_name"
exit 1
fi
echo succcessfully found JIRA issue: ${{steps.find_issue.outputs.ISSUE_ID}}
Если поискать в GitHub marketplace, то можно найти действие для этой задачи, но мне пришлось написать тоже самое через grep по названию ветки, потому что это действие от Atlassian ни в какую не захотело работать на моем проекте, разбираться, что же там не так — дольше, чем сделать руками тоже самое.
Осталось только переместить задачу в колонку «Code review» при создании пулл-реквеста:
Для этого есть специальное действие на GitHub, все, что ему нужно — это идентификатор задачи, полученный на предыдущем шаге и авторизация в JIRA, которую мы делали выше.
Таким же образом можно перетягивать задачи при merge в мастер, и других событиях из GitHub workflow. В общем, все зависит от вашей фантазии и желания автоматизировать рутинные процессы.
Выводы
Если посмотреть на классическую диаграмму DEVOPS, то мы покрыли все этапы, разве что кроме operate, думаю, если постараться, то можно найти какой-нибудь экшен в маркете для интеграции с help-desk системой, так что будем считать что pipeline получился основательный и на основании его использования можно сделать выводы.
Плюсы:
Marketplace с готовыми действиями на все случаи жизни, это очень круто. В большинстве из них еще и исходники можно посмотреть, чтобы понять как решить похожую задачу либо запостить feature request автору прямо в гитхаб репозитории.
Выбор целевой платформы для сборки: Linux, mac os, windows довольно интересная фича.
Github Packages отличная вещь, держать всю инфраструктуру в одном месте удобно, не надо серфить по разным окошкам, все в радиусе одного-двух кликов мыши и прекрасно интегрировано с GitHub Actions. Поддержка docker registry в бесплатной версии — это тоже хорошее преимущество.
GitHub прячет секреты в логах сборки, поэтому пользоваться им для хранения паролей и токенов не так уж и страшно. За все время экспериментов мне не удалось ни разу увидеть секрет в чистом виде в консоли.
Бесплатен для Open Source проектов
Минусы:
YML, ну не люблю я его. При работе с таким флоу у меня самый частый commit message это «fix yml format», то забудешь где-то таб поставить, то не на той строке напишешь. В общем, сидеть перед экраном с транспортиром и линейкой не самое приятное занятие.
DEBUG, отлаживать флоу коммитами, запуском пересборки и выводом в консоль не всегда удобно, но это больше из разряда «вы зажрались», привыкли работать с удобными IDEA, когда можно отлаживать все, что угодно.
Свой экшен можно написать на чем угодно если завернуть его в докер, но нативно поддерживается только javascript, конечно это дело вкуса, но я бы предпочел что-то другое заместо js.
На следующей неделе я буду выступать с докладом на конференции Heisenbug 2020 Piter. Расскажу не только, как избежать ошибок при подготовке тестовых данных, но и поделюсь своими секретами работы с наборами данных в Java-приложениях!