
Често ми се налага да изграждам пайплайн за сборка на проекти на Java. Понякога това е с отворен код, понякога не. Наскоро реших да опитам да прехвърля част от репозиториите си от Travis-CI и TeamCity на GitHub Actions и ето какво се получи от това.
Какво ще автоматизираме
Първо, нуждаем се от проект, който ще автоматизираме, да направим малко приложение на Spring Boot / Java 11 / Maven. В рамките на тази статия логиката на приложението няма да ни интересува, важно е инфраструктурата около приложението, така че ще ни стигне прост REST API контролер.
Може да се разгледат изходниците тук: всички етапи на изграждане на пайплайн-а са отразени в пулл-реквестите на този проект.
JIRA и планиране
Трябва да спомена, че обикновено използваме JIRA като трекер на задачи, така че да създадем отделен борд за този проект и да добавим първите задачи:

Чак по-късно ще се върнем към това, какво интересно могат да предоставят JIRA и GitHub в комбинация.
Автоматизиране на сборката на проекта
Нашият тестов проект се изгражда чрез Maven, така че сборката му е доста проста, всичко, което ни трябва, е mvn clean package.
За да направим това чрез GitHub Actions, трябва да създадем файл с описание на нашия работен поток в репозитория, това може да се направи с обикновен 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 — указва, че този работен поток трябва да се стартира при всяко пушване в основния и създаване на пулл-реквестове.
Следва описанието на задачите (jobs) и стъпките за изпълнение (steps) за всяка задача.
runs-on — тук можем да изберем целевата операционна система, учудващо е, че можем да избера и Mac OS, но на частни репозитории това е доста скъпо удоволствие (в сравнение с Linux).
uses позволява повторна употреба на други действия, така например с помощта на действието actions/setup-java инсталираме среда за Java 11.
С помощта на with можем да зададем параметри, с които стартираме действието, по същество това са аргументите, които ще бъдат предадени на екшъна.
Остава само да стартираме Maven сборката на проекта: run: mvn -B clean package флаг -B означава, че имаме нужда от non-interactive mode, за да не пита Maven нещо.

Отлично! Сега при всеки commit в master, се стартира сборката на проекта.
Автоматизиране на старта на тестовете
Сборката е добре, но в реалността проектът може да се компилира без проблеми, но да не работи. Затова следващата стъпка е автоматизация на прогонването на тестовете. Освен това е удобно да се виждат резултатите от тестовете, когато правиш преглед на PR — точно знаеш, че тестовете минават и никой не е забравил да тества своя клон преди merge.
Стартиране на тестовете при създаване на pull request и merge в master, а заедно с това ще добавим и генериране на отчет за 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 е механизъм за съхранение на секрети в GitHub, можем да запишем пароли/token/хостове/url и други данни, които не бива да се показват в кода на репозитория.
Добавянето на променлива в secrets става в настройките на репозитория в GitHub:

Токен може да се получи на след авторизация през GitHub, за добавяне на публичен проект е нужно просто да се мине през линк с вида: /[repo name]. Приватный репозиторий тоже можно добавить, для этого надо дать права codecov приложению в гитхабе.

Добавяме jacoco плъгин в POM файла:
org.jacoco
jacoco-maven-plugin
0.8.4
prepare-agent
report
test
report
org.apache.maven.plugins
maven-surefire-plugin
2.22.2
plain
**/*Test*.java
**/*IT*.java
Сега в всеки наш пулл-реквест ще влиза бот на codecov и ще добавя график на промените в покритията:

Да добавим статичен анализатор
В повечето си open source проекти използвам sonar cloud за статичен анализ на кода, лесно е да се свърже с travis-ci. Така че това е логичната стъпка при миграция към GitHub Actions - да направим същото. Маркетингът на actions е готина работа, но този път малко ме разочарова, защото по навик намерих нужния action и го написах в workflow. Оказа се, че sonar не поддържа работа чрез action за анализ на проекти на maven или gradle. Това е написано в документацията, но кой я чете?!
Чрез action не може, затова ще го направим чрез 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 — може да се получи в и трябва да бъде записан в secrets. GITHUB_TOKEN — това е вграден токен, който генерира GitHub, с него sonarcloud[bot] ще може да се авторизира в Git, за да оставя коментари в пулл-реквестите.
Dsonar.projectKey — името на проекта в sonar, може да се види в настройките на проекта.
Dsonar.organization — името на организацията от GitHub.
Правим пулл-реквест и чакаме, когато sonarcloud[bot] дойде с коментарите:

Управление на версии
Сглобката е настроена, тестовете са преминали, може и да направим release. Нека видим как GitHub Actions помага значително да опрости управлението на версиите.
На работа имам проекти, чийто кодов баз е в bitbucket (всичко както в онази история "през деня пиша в битбакет, през нощта комитвам в GitHub"). За съжаление, в bitbucket няма вградени средства за управление на релизите. Това е проблем, защото за всеки релиз трябва ръчно да се създаде страница в confluence и да се съберат всички функции, включени в релиза, да се прегледат идеите, задачите в jira, коментарите в репозитория. Шансовете за грешка са много, може да забравиш нещо или да запишеш нещо, което вече е било релизирано предишния път, понякога просто не е ясно как да се отнесе определен pull-request — дали е функция или поправка на бъгове, или изменение на тестовете, или нещо инфраструктурно.
Как GitHub actions може да ни помогне? Има отличен екшън — release drafter, който позволява да зададеш шаблон за файла с release notes, за да организираш категориите на pull-request-ите и автоматично да ги групираш в файла с release notes:

Примерен шаблон за настройка на отчета (.github/release-drafter.yml):
name-template: 'v$NEXT_PATCH_VERSION'
tag-template: 'v$NEXT_PATCH_VERSION'
categories:
- title: 'Нови функции'
labels:
- 'type:features'
# в тази категория събираме всички PR с етикет type:features
- title: 'Поправки на бъгове'
labels:
- 'type:fix'
# аналогично за етикет type:fix и т.н.
- title: 'Документация'
labels:
- 'type:documentation'
- title: 'Конфигурация'
labels:
- 'type:config'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
template: |
## Промени
$CHANGES
Добавяме скрипт за генериране на черновик на релиза (.github/workflows/release-draft.yml):
name: "Създаване на чернова на релиза"
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 }}
Всички pull-request-и от този момент нататък ще се събират в release notes автоматично — магия!
Тук може да възникне въпрос: а какво ако разработчиците забравят да добавят етикети в PR? Тогава не е ясно в коя категория да го отнесем и отново ще трябва да се занимаваме ръчно с всеки PR поотделно. За да решим този проблем, можем да използваме още един екшън — label verifier — който проверява наличието на тагове на pull-request-а. Ако няма нито един задължителен таг, проверката ще бъде неуспешна и информация за това ще видим в нашия pull-request.
name: "Проверка на етикети от тип"
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.

Авто-анотиране на пулл-реквести
След като споменахме темата за ефективната работа с пулл-реквести, е важно да кажем и за едно действие, наречено labeler, което поставя етикети в PR на база на променените файлове. Например, можем да етикираме като [build] всеки пулл-реквест, в който има промени в директорията .github/workflow.
Включването му е съвсем просто:
name: "Авто-присвояване на теми към 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 изобщо не иска да вижда етикетите, поставени от бота. Изглежда, че е по-просто да напиша свое действие, което да съчетава двата етапа. Но дори и в този вид, ползването е доста удобно, нужно е да изберете етикет от списъка при създаването на пулл-реквест.
Време е за деплой

Опитах няколко варианта за деплой през GitHub Actions (чрез ssh, чрез scp, и с помощта на docker-hub), и мога да кажа, че със сигурност ще намерите начин да качите бинарен файл на сервера, какъвто и да е вашият pipeline.
На мен ми харесва вариантът да държа цялата инфраструктура на едно място, затова ще разгледаме как да направим деплой в GitHub Packages (репозиторий за бинарно съдържание, npm, jar, docker).
Скрипт за изграждане на docker образ и публикуването му в GitHub Packages:
име: Деплой на docker образ
на:
пуш:
клонове:
- 'master'
работи:
build_docker_image:
работи-на: ubuntu-18.04
стъпки:
# Създаване на JAR:
- използва: actions/checkout@v1
- име: настройка на JDK 11
използва: actions/setup-java@v1
с:
java-версия: 1.11
- име: Maven пакет
изпълни: mvn -B clean compile package -DskipTests
# Настройване на глобални променливи на средата:
- име: глобална променлива
идентификатор: global_env
изпълни: |
echo "::set-output name=IMAGE_NAME::${GITHUB_REPOSITORY#*\/}"
echo "::set-output name=DOCKERHUB_IMAGE_NAME::docker.pkg.github.com\/${GITHUB_REPOSITORY}\/${GITHUB_REPOSITORY#*\/}"
# Създаване на Docker образ:
- име: Създаване и тагване на образа
изпълни: |
docker build -t "${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}:latest" -t "${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}:${GITHUB_SHA::8}" .
- име: Docker вход
изпълни: docker login docker.pkg.github.com -u $GITHUB_ACTOR -p ${{secrets.GITHUB_TOKEN}}
# Публикуване на образа в хранилището на пакети в GitHub:
- име: Публикуване на образа
среда:
IMAGE_NAME: $GITHUB_REPOSITORY
изпълни: 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 образа:
docker build -t "docker.pkg.github.com\/antkorwin\/github-actions\/github-actions:latest"
Да се логнем в регистъра:
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. Тук също има нюанси, ако ще правите такива сборки не само при сливането в master, а и при събития за създаване на pull request, тогава SHA може да не съвпада с хеша, който виждаме в историята на git, защото действието actions/checkout създава свой уникален хеш, за да избегне взаимни блокировки на действия в PR.

Ако всичко е минало успешно, отворете раздела пакети (https://github.com/antkorwin/github-actions/packages) в репозитория, и ще видите новия docker образ:

Там можете да видите списък с версии на Docker образа.
Остава само да конфигурираме нашия сървър да работи с този registry и да рестартираме услугата. Как да го направим чрез systemd, ще разкажа друг път.
Мониторинг
Нека разгледаме един прост вариант как да направим 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"}
Всичко, което ни трябва, е да напишем таск за проверка на сървъра по крон, а ако той не отговори, да изпратим уведомление в Telegram.
Първо, нека разберем как да пуснем workflow по крон:
on:
schedule:
- cron: '*\/5 * * * *'
Всичко е просто, дори не може да се повярва, че в GitHub може да се създадат такива събития, които не се вместят в webhook-ите. Подробности има в документацията:
Проверката на статуса на сървъра ще направим ръчно чрез 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 failed"
exit 1
fi
echo "It's OK"
Първо запазваме в променлива това, което отговори сървърът на запитването, а на следващата стъпка проверяваме дали статусът е UP и, ако не е, излизаме с грешка. Ако трябва ръчно да „съборим“ действието, exit 1 е подходящото средство.
- name: изпрати предупреждение в Telegram
if: ${{ failure() }}
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_TO }}
token: ${{ secrets.TELEGRAM_TOKEN }}
message: |
Проверката на състоянието на:
${{secrets.SERVER_HOST}}/api/actuator/health
е неуспешна с резултат:
${{ steps.ping.outputs.status }}
Изпращането в Telegram ще се направи, само ако действието е неуспешно на предишната стъпка. За изпращане на съобщението използваме appleboy/telegram-action. Как да получите токен на бота и ID на чата, можете да прочетете в документацията:

Не забравяйте да добавите в секретите на GitHub: URL за сървъра и токените за бота в Telegram.
Бонус трак — 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, как да го направим е описано тук:
Извличаме идентификатора на задачата от името на клона:
- 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 "Моля, именувайте клона си в съответствие с JIRA задачата: [project_key]-[task_number]-branch_name"
exit 1
fi
echo успешно намерена JIRA задача: ${{steps.find_issue.outputs.ISSUE_ID}}
Ако се потърси в GitHub marketplace, може да се намери действие за тази задача, но ми се наложи да напиша същото и чрез grep по името на клона, тъй като това действие от Atlassian не пожела да работи на моя проект, а да разучавам какво не е наред - отнема повече време, отколкото да направя същото на ръка.
Остало е само да преместите задачата в колоната "Code review" при създаването на пулл-реквест:
- име: Преход на задача
ако: ${{ success() }}
използва: atlassian/gajira-transition@master
с:
задача: ${{ steps.find_issue.outputs.ISSUE_ID }}
преход: "Code review"
За целта има специално действие в GitHub, всичко, което му е нужно – е идентификатора на задачата, получен на предишната стъпка, и авторизация в JIRA, която направихме по-горе.

По същия начин можете да пренасочвате задачите при сливане в master и при други събития от GitHub workflow. В общи линии, всичко зависи от вашето въображение и желание да автоматизирате рутинни процеси.
Изводи
Ако погледнете класическата диаграма DEVOPS, виждаме, че сме обхнали всички етапи, освен operate. Мисля, че с усилия можем да намерим някакво действие на пазара за интеграция с help-desk система, така че можем да приемем, че pipeline е основателен и на базата на неговото използване можем да направим изводи.

Предимства:
- Marketplace с готови действия за всевъзможни ситуации, това е наистина страхотно. В повечето от тях можете също да видите изходния код, за да разберете как да решите подобна задача или да публикувате искане за функция директно на автора в GitHub репозитория.
- Изборът на целева платформа за компилация: Linux, macOS, Windows е доста интересна функция.
- GitHub Packages е чудесна идея, да държите цялата инфраструктура на едно място е удобно, не се налага да сервирате по различни прозорци, всичко е в радиус от едно-две кликвания на мишката и е прекрасно интегрирано с GitHub Actions. Поддръжката на docker registry в безплатната версия е също добро предимство.
- GitHub скрива секретите в логовете на компилацията, така че използването му за съхранение на пароли и токени не е чак толкова страшно. През всичките ми експерименти не успях нито веднъж да видя секрет в чист вид в конзолата.
- Безплатно за Open Source проекти
Минуси:
- YML, не го харесвам. При работа с такъв флоу най-честото ми съобщение при комит е „поправи yml формата“, веднъж забравяйки да сложа таб, веднъж не на правилния ред. В общи линии, да седиш пред екрана с транспортир и линия не е особено приятно занимание.
- DEBUG, дебъгването на флоу с комити, стартиране на нова компилация и извеждане в конзолата не винаги е удобно, но това е повече от типа „вие се разглезихте“, свикнали сте да работите с удобни IDE, когато можете да дебъгвате всичко, което пожелаете.
- Можете да напишете своя екшън на каквото и да е, ако го опаковате в Docker, но само JavaScript се поддържа нативно. Разбира се, това е въпрос на вкус, но аз бих предпочел нещо различно вместо js.
Напомням, че репозиторият с всички скриптове е тук:
Следващата седмица ще участвам в конференцията Heisenbug 2020 Piter. Ще разкажа не само как да избягваме грешки при подготовка на тестови данни, но и ще споделя моите тайни за работа с набори от данни в Java приложения!
Източник: habr.com
