1. Intro
Мне частенько приходится строить pipe-line для сборки проектов на java. Иногда это Open Source, иногда нет. Недавно я решил попробовать перенести часть своих репозиториев с travis-ci и teamcity на github actions и сегодня хочу рассказать о том что из этого получилось.
2. Что будем автоматизировать
Для начала нам нужен проект, который мы будем автоматизировать, давайте сделаем небольшое приложение на Spring boot / Java 11 / Maven
. В рамках этой статьи, логика приложения нас интересовать не будет совсем, нам важна инфраструктура вокруг приложения, так что нам хватит простенького REST API контроллера.
Посмотреть исходники можно тут: https://github.com/antkorwin/github-actions все этапы построения pipeline-конвейера отражены в пулл-реквестах этого проекта.
3. JIRA и планирование
Стоит сказать, что мы обычно используем JIRA в качестве трекера задач, так что давайте заведем отдельную борду под этот проект и накидаем туда первые задачи:
Чуть позже мы еще вернемся к тому что интересного могут дать в связке JIRA и github.
4. Автоматизируем сборку проекта
Наш тестовый проект собирается через maven, так что сборка его довольно простая, все что нам нужно это mvn clean package
Чтобы сделать это при помощи Github Actions, нам нужно будет создать в репозитории файл с описанием нашего workflow, это можно сделать обычным yml файлом, не могу сказать что мне нравится "программирование на yml", но что поделать - делаем в директории .github/workflow/
файл build.yml
в котором будем описывать действия при сборке мастер ветки:
name: Build
on:
pull_request:
branches:
- '*'
push:
branches:
- 'master'
jobs:
build:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- name: set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.11
- name: Maven Package
run: mvn -B clean package -DskipTests
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,
чтобы мавен вдруг не захотел что-то у нас спросить
Отлично теперь при каждом коммите в мастер, запускается сборка проекта.
5. Автоматизируем запуск тестов
Сборка это хорошо но, в реальности проект может благополучно собираться но не работать. Поэтому следующим шагом нужно заняться автоматизацией прогона тестов. К тому же, довольно удобно смотреть результат прохода тестов, когда делаешь ревью PR - ты точно знаешь что тесты проходят и никто не забыл, перед тем как делать merge, прогнать свою ветку.
Делаем запуск тестов при создании пулл-реквеста и merge в мастер, а заодно добавим построение отчета о code-coverage.
name: Build
on:
pull_request:
branches:
- '*'
push:
branches:
- 'master'
jobs:
build:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- name: set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.11
- name: Maven Verify
run: mvn -B clean verify
- name: Test Coverage
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
Для покрытия тестов я использую codecov в связке с jacoco плагином. У codecov есть свой экшен, но ему для работы с нашим pull-request-ом нужен токен:
${{ secrets.CODECOV_TOKEN }}
- такую конструкцию мы будем встречать еще не один раз,
secrets это механизм хранения секретов в гитхабе, мы можем там прописать пароли/токены/хосты/url-ы
и прочие данные, которыми не стоит светить в кодовой базе репозитория.
Добавить переменную в secrets, можно в настройках репозитория на github:
Получить токен можно на https://codecov.io после авторизации через github, для добавления public проекта нужно просто пройти по ссылке вида: github user name/[repo name]
. Приватный репозиторий тоже можно добавить, для этого надо дать права codecov приложению в гитхабе.
Добавляем jacoco плагин в POM-файл:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.4</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<!-- attached to Maven test phase -->
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<reportFormat>plain</reportFormat>
<includes>
<include>**/*Test*.java</include>
<include>**/*IT*.java</include>
</includes>
</configuration>
</plugin>
Теперь в каждый наш пулл-реквест будет заходить codecov бот и добавлять график изменения покрытия:
6. Добавим статический анализатор
В большинестве своих open source проектов я использую sonar cloud для статического анализа кода, его довольно легко подключить к travis-ci. Так что это логичный шаг при миграции на Github Actions, сделать тоже самое. Маркет экшенов - клевая штука, но в этот раз он немного подвел, потому что я по привычке нашел нужный экшен и прописал его в workflow. А оказалось что sonar не поддерживает работу через действие для анализа проектов на maven или gradle. Об этом конечно написано в документации, но кто же ее читает?!
Через действие нельзя, поэтому будем делать через mvn плагин:
name: SonarCloud
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize, reopened]
jobs:
sonarcloud:
runs-on: ubuntu-16.04
steps:
- uses: actions/checkout@v1
- name: Set up JDK
uses: actions/setup-java@v1
with:
java-version: 1.11
- name: Analyze with SonarCloud
# set environment variables:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# run sonar maven plugin:
run: mvn -B verify sonar:sonar -Dsonar.projectKey=antkorwin_github-actions -Dsonar.organization=antkorwin-github -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=$SONAR_TOKEN -Dsonar.coverage.jacoco.xmlReportPaths=./target/site/jacoco/jacoco.xml
SONAR_TOKEN
- можно получить в https://sonarcloud.io/ и нужно прописать его в secrets.
GITHUB_TOKEN
- это встроенный токен который генерит гитхаб, с помощью него sonarcloud[bot] сможет авторизоваться в гите, чтобы оставлять нам сообщения в пулл-реквестах.
Dsonar.projectKey
- название проекта в сонаре, посмотреть можно в настройках проекта.
Dsonar.organization
- название организации из github.
Делаем пулл-реквест и ждем когда sonarcloud[bot] придет в комментарии:
7. Release management
Билд настроили, тесты прогнали, можно и релиз сделать. Давайте посмотрим как Github Actions помогает существенно упростить release managment.
На работе у меня есть проекты, кодовая база которых лежит в 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):
name: "Create draft release"
on:
push:
branches:
- master
jobs:
update_draft_release:
runs-on: ubuntu-18.04
steps:
- uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Все пулл-реквесты с этого момента будут собираться в release notes автоматически - magic!
Тут может возникнуть вопрос - "А что если разработчики забудут проставить метки в PR?"
тогда непонятно в какую категорию его отнести и опять придется разбираться в ручную, с каждым ПР-ом отдельно.
Чтобы исправить эту проблему, мы можем воспользоваться еще одним экшеном: label verifier
он проверяет наличие тэгов на пул-реквесте.
Если нет ниодного обязательного тэга, то проверка будет завалена и сообщение об этом мы увидим в нашем пулл-реквесте.
name: "Verify type labels"
on:
pull_request:
types: [opened, labeled, unlabeled, synchronize]
jobs:
triage:
runs-on: ubuntu-18.04
steps:
- uses: zwaldowski/match-label-action@v2
with:
allowed: 'type:fix, type:features, type:documentation, type:tests, type:config'
Теперь любой pull-request нужно пометить одним из тэгов: type:fix, type:features, type:documentation, type:tests, type:config
.
8. Авто-аннотирование пулл-реквестов
Рас уж мы коснулись такой темы как эффективная работа с пулл-реквестами, то стоит сказать еще о таком экшене, как labeler, он проставляет метки в PR, на основании того какие файлы были изменены. Например мы можем пометить как [build] любой пул-реквест в котором есть изменения в каталоге .github/workflow
подключить его довольно просто:
name: "Auto-assign themes to PR"
on:
- pull_request
jobs:
triage:
runs-on: ubuntu-18.04
steps:
- uses: actions/labeler@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
еще нам понадобится файл с описанием соответствия каталогов проекта с тематиками пулл-реквестов:
theme:build:
- ".github/**"
- "pom.xml"
- ".travis.yml"
- ".gitignore"
- "Dockerfile"
theme:code:
- "src/main/*"
theme:tests:
- "src/test/*"
theme:documentation:
- "docs/**"
theme:TRASH:
- ".idea/**"
- "target/**"
Подружить действие автоматически проставляющее метки в пулл-реквесты и действие проверяющее наличие обязательных меток - у меня не вышло, match-label
на отрез не хочет видеть проставленные ботом метки. Похоже проще написать свое действие совмещающее оба этапа.Но даже в таком виде пользоваться довольно удобно, нужно выбрать метку из списка при создании пулл-реквеста.
9. Пора деплоить
Я попробовал несколько вариантов деплоя через github actions (через ssh, через scp, и при помощи docker-hub), и могу сказать что скорее всего вы найдете способ залить бинарку на сервер, каким бы извращенным не был ваш pipeline.
Мне понравился вариант держать всю инфраструктуру в одном месте, поэтому рассмотрим как сделать деплой в github packages (это репозиторий для бинарного контента, npm, jar, docker)
скприпт сборки docker образа и публикации его в github packages:
name: Deploy docker image
on:
push:
branches:
- 'master'
jobs:
build_docker_image:
runs-on: ubuntu-18.04
steps:
# Build JAR:
- uses: actions/checkout@v1
- name: set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.11
- name: Maven Package
run: mvn -B clean compile package -DskipTests
# Set global environment variables:
- name: set global env
id: global_env
run: |
echo "::set-output name=IMAGE_NAME::${GITHUB_REPOSITORY#*/}"
echo "::set-output name=DOCKERHUB_IMAGE_NAME::docker.pkg.github.com/${GITHUB_REPOSITORY}/${GITHUB_REPOSITORY#*/}"
# Build Docker image:
- name: Build and tag image
run: |
docker build -t "${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}:latest" -t "${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}:${GITHUB_SHA::8}" .
- name: Docker login
run: docker login docker.pkg.github.com -u $GITHUB_ACTOR -p ${{secrets.GITHUB_TOKEN}}
# Publish image to github package repository:
- name: Publish image
env:
IMAGE_NAME: $GITHUB_REPOSITORY
run: docker push "docker.pkg.github.com/$GITHUB_REPOSITORY/${{ steps.global_env.outputs.IMAGE_NAME }}"
Для начала нам надо собрать 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#*/}
Далее нам нужно собрать докер образ
docker build -t "docker.pkg.github.com/antkorwin/github-actions/github-actions:latest"
Авторизоваться в registry:
docker login docker.pkg.github.com -u $GITHUB_ACTOR -p ${{secrets.GITHUB_TOKEN}}
И опубликовать образ в github packages repository:
docker push "docker.pkg.github.com/antkorwin/github-actions/github-actions"
Для того чтобы указать версию образа, мы используем первые цифры из SHA-хэша коммита - GITHUB_SHA
тут тоже есть нюансы, если вы будете делать такие сборки не только при merge в master, а еще и по событию создания пулл-реквеста, то SHA может не совпадать с хэшем, который мы видим в истории гита, потому что действие actions/checkout делает свой уникальный хэш, чтобы избежать взаимных блокировок действий в PR.
Если все получилось благополучно, то открыв раздел packages (https://github.com/antkorwin/github-actions/packages) в репозитории, вы увидите новый докер образ:
Там же можно посмотреть список версий докер-образа.
Остается только настроить наш сервер на работу с этим registry и запустить перезапуск сервиса. О том как это сделать через systemd, я пожалуй расскажу в другой раз.
10. Мониторинг
Давайте посмотрим несложный вариант как делать health check нашего приложения при помощи github actions.
В нашем бутовом приложении есть actuator, так что API для проверки его состояния даже и писать не надо, для денивых уже все сделали.
Нужно только дернуть хост: SERVER-URL:PORT/actuator/health
$ curl -v 127.0.0.1:8080/actuator/health
> GET /actuator/health HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.61.1
> Accept: */*
< HTTP/1.1 200
< Content-Type: application/vnd.spring-boot.actuator.v3+json
< Transfer-Encoding: chunked
< Date: Thu, 04 Jun 2020 12:33:37 GMT
{"status":"UP"}
Все что нам нужно написать таск проверки сервера по крону, ну а если вдруг он нам не ответит, то будем слать уведомление в телеграм:
Для начала разберемся как запустить workflow по крону:
on:
schedule:
- cron: '*/5 * * * *'
Все просто, даже не верится что в гитхабе можно сделать такие ивенты, которые совсем не укладываются в webhook-и. Детали есть в документации: https://help.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule
Проверку статуса сервера сделаем руками через 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 чата можно почитать в документации:
https://github.com/appleboy/telegram-action
Не забудьте прописать в секретах на гитхабе: URL для сервера и токены для телеграм бота.
11. Бонус трек - 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
jobs:
build:
runs-on: ubuntu-latest
name: Jira Workflow
steps:
- name: Login
uses: atlassian/gajira-login@master
env:
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
Для этого надо получить токен в JIRA, как это сделать расписано тут: https://confluence.atlassian.com/cloud/api-tokens-938839638.html
Вычленяем идентификатор задачи из названия ветки:
- 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 по названию ветки потому что это действие от atlasian ни в какую не захотело работать на моем проекте, разбираться что же там не так дольше чем сделать руками тоже самое.
Осталось только переместить задачу в колонку "Code review" при создании пулл-реквеста:
- name: Transition issue
if: ${{ success() }}
uses: atlassian/gajira-transition@master
with:
issue: ${{ steps.find_issue.outputs.ISSUE_ID }}
transition: "Code review"
Для этого есть специальное действие на github, все что ему нужно это идентификатор задачи полученный на предыдущем шаге и авторизация в JIRA, которую мы делали выше.
таким же образом можно перетягивать задачи при merge в мастер, и других событиях из github workflow. В общем все зависит от вашей фантазии и желания автоматизировать рутинные процессы.
12. Выводы
если посмотреть на классическую диаграмму 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.