Кола пекла з 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 потрібно запускати при кожному пуші в майстер та створення пулл-реквестів.
Далі йде опис завдань (роботи) та кроки виконання (кроки) для кожного завдання.
runs-on - Тут ми можемо вибрати цільову ОС, напрочуд можна вибрати навіть Mac OS, але на приватних репозиторіях це досить дороге задоволення (порівняно з linux).
використовує дозволяє перевикористовувати інші екшени, наприклад за допомогою екшену actions/setup-java ми встановлюємо оточення для Java 11.
За допомогою з ми можемо вказати параметри з якими запускаємо дію, по суті, це аргументи, які будуть передаватися в екшен.
Залишається тільки запустити мавеном складання проекту: 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 бот і додаватиме графік зміни покриття:
Додамо статичний аналізатор
У більшості своїх 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] прийде у коментарі:
Управління випуском
Білд налаштували, тести прогнали, можна й реліз зробити. Давайте подивимося, як GitHub Actions допомагає значно спростити release management.
На роботі я маю проекти, кодова база яких лежить у 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 і якщо це не так, то виходимо з помилкою. Якщо потрібно руками «завалити» дію, то вихід 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-додатках!