Використання GitHub Actions для інтеграції e2e-тестів у CI/CD

💡 Усі статті, обговорення, новини про тестування — в одному місці. Приєднуйтесь до QA спільноти!

Мене звуть Євгенія Шмельова і я Automation QA Engineer у АmoMama — це мультимедійна платформа, частина міжнародної IT-компанії AMO, яка створює контент у ніші розважального сторітелінгу.

Наша технічна команда щодня проводить активний пошук та імплементацію нових рішень, спрямованих на покращення користувацького досвіду читачів та відповідність ключовим метрикам ранжування сайтів від Google (раніше ми вже розповідали на DOU про те, чого саме вже вдалося досягти).

В умовах активної розробки задачі команди автоматизації контролю якості полягають не лише у проведенні тест-дизайну, написанні автотестів та аналізі репортів, але і у налаштуванні усього процесу автоматизації: написанні пайплайнів для запуску автотестів, їх підтримці, налаштуванні моніторингу. Такий підхід дозволяє краще розуміти життєвий цикл застосунку та спрощує локалізацію можливих проблем.

В цьому тексті я хочу детальніше розповісти про особливості написання Github Actions на прикладах конкретних кроків, які можуть стати в пригоді AQA інженерам, які також прагнуть побудувати на своєму проєкті процеси CI/CD.

Перехід з Jenkins на Github Actions

CI/CD (Continuous Integration/Continuous Delivery) — це концепція розробки програмного забезпечення, що передбачає швидку доставку коду користувачу шляхом автоматизації процесів збірки, тестування та розгортання (рис. 1):

Два роки тому для автоматизації процесу збірки та розгортання, а також запуску автотестів ми використовували Jenkins, та з масштабуванням продукту наша Platform team прийняла рішення про перехід на Kubernetes та Github actions. Перед нами постала задача адаптації нашої екосистеми з автотестами.

На той момент у нас вже була автоматизована значна частина процесу, але рішення про проведення деплою залишалося за відповідальним QA. Умовно, кожна фіча проходила наступні етапи:

  • мануальне тестування на локальному середовищі;
  • автоматичний запуск Jenkins-джоби для деплою на стейдж після мержу фіча-гілки у гілку stage;
  • по факту відпрацювання деплою — автоматичний запуск джоби з автотестами, посилання на репорт відправлялися у канал Slack;
  • аналіз репорту, за потреби — мануальна перевірка на стейджі та прийняття рішення про деплой фічі на продакшен;
  • ручний запуск Jenkins-джоби для деплою на продакшен;
  • по факту відпрацювання деплою — автоматичний запуск джоби із автотестами, посилання на репорт відправлялися у канал Slack;
  • аналіз репорту та, за потреби, мануальна перевірка на продакшені і передача на верифікацію продакт-менеджеру.

На той момент ми мали понад 10 сайтів і було зрозуміло, що запускати тести вручну — не варіант, тому після перенесення деплою перших 2 сайтів на Github actions першим рішенням було додати в кожен екшен степ із викликом відповідної джоби з автотестами у Jenkins. Все, що для цього потрібно було зробити — встановити в конфігурації відповідної джоби тогл Trigger builds remotely та додати відповідний токен в Github secrets (контекст, який дозволяє зберігати чутливу інформацію вашої організації або репозиторію у зашифрованому вигляді):

В екшені ж додали степ для виклику цієї джоби:

steps:
   - name: Run autotests
     run: curl -u "${{secrets.JENKINS_USER}}:${{secrets.JENKINS_PASSWORD}}" ${{secrets.JENKINS_URL}}/job/front-prod-tests/build?token=${{secrets.JENKINS_TOKEN}}

Так нам вдалося зберегти звичне флоу та уникнути зайвих рухів для запуску тестів, але в як довготривале рішення такий підхід ми не розглядали.

Насправді Github actions та Jenkins мають багато спільного, що робить міграцію відносно простою, а основні відмінності Jenkins pipeline та Github action описані в офіційній документації.

Github actions

GitHub Actions — це CI/CD платформа, яка дозволяє вам автоматизувати збірку, тестування та розгортання вашого коду (має дуже гарну офіційну документацію). За допомогою GitHub Actions ви можете здійснити перевірку коду лінтером і тестами, виконати деплой проєкту, опублікувати нову версію пакета, налаштувати сповіщення у месенджері та багато іншого. GitHub Actions є безкоштовною для усіх публічних репозиторіїв (детальніше про білінг та ліміти), проєкти можуть виконуватися на ранерах (тимчасових серверах) Github або ваших власних з обраною операційною системою (Linux, macOS чи Windows).

Hello World with Github actions

Все, що потрібно для створення першого екшену — це GitHub-репозиторій. В обраному репозиторії створюємо (якщо відсутня) директорію .github/workflows в яку додаємо hello-world.yml з наступним змістом:

1. name: Hello World
2. on: push  
3. jobs:
4.  hello-world:
5.    runs-on: ubuntu-latest
6.    steps:
7.       - uses: actions/[email protected]
8.       - name: Step name
9.         run: echo "Hello World!"

Після того як екшен буде змерджено у дефолтну гілку, він з’явиться на вкладці Actions у репозиторії:

Також звідси можемо перейти до логів рану, який нас цікавить, в цьому випадку вони виглядатимуть так:

А за допомогою кнопки у правому верхньому кутку можемо перезапустити зафейлені джоби чи увесь воркфлоу повністю.

Основні елементи воркфлоу та приклади їх використання

Кожен екшн складається з низки обов’язкових та не обов’язкових інструкцій. Розберемо детальніше воркфлоу з попереднього пункту:

1. name — назва воркфлоу, буде відображатися у переліку всіх воркфлоу на вкладці Actions (рис 2); може містити пробіли. Також саме за допомогою цього поля можна налаштувати залежність запуску одного воркфлоу від іншого (наприклад: запускати автотести після того, як закінчиться деплой); якщо не вказано — буде використано шлях від кореня проєкта;

2. on — тригер запуску, може бути як один, так і декілька:

#Запуститься при пуші у будь-яку гілку
on: push  
#Запуститься при пуші у будь-яку гілку а також мануально
on: [push, workflow_dispatch]

тригером для воркфлоу може бути одна з наступних подій:

  1. Подія, що відбулася у репозиторії, в якому розміщено воркфлоу.
  2. Зовнішня подія, що ініціює repository_dispatch івент у вашому GitHub.
  3. Запуск за розкладом.
  4. «Ручний запуск».

Додатково до тригерів можуть застосовуватися фільтри (наприклад, пуш лише у конкретну гілку);

3. jobs — може містити одну або кілька джоб, які за замовчуванням виконуються паралельно (наприклад: тести для мобайлу та десктопу); але можуть запускатися і послідовно та бути залежними одна від одної (наприклад: в одній ми білдимо аплікейшен і, якщо білд успішний, — запускаємо тести, в такому випадку використовується інструкція needs);

4. job_id — унікальний ідентифікатор завдання; має починатися з літери або нижнього підкреслювання та може містити літери, цифри, нижнє підкреслення та дефіс;

5. runs-on — задає тип машини, на якій буде виконуватися поточне завдання, як вже згадувалося вище, можуть використовуватися як GitHub-hosted, так і Self-hosted машини з операційними системами Linux, macOS та Windows:

runs-on: ubuntu-latest  
runs-on: [self-hosted, linux]

Для визначення ранеру можна використовувати лейби або групи чи їх комбінації; допустимі значення стрічка або масив стрічок, якщо вказано масив — завдання запуститься на ранері, який відповідає кожному з елементів масиву/

Для прикладу я використовую GitHub-hosted ранер, втім, для продакшену вам, швидше за все, знадобляться Self-hosted. Зокрема, якщо ви використовуєте Security groups, тому під час налаштування варто впевнитися, що обраний ранер має доступ до тестового оточення та інших допоміжних сервісів;

6. steps — група кроків, які складають ваше завдання, кожен новий крок починається із символу дефіса;

7. uses — дозволяє і використовує інший екшен як частину завдання; перевикористати можна екшен, що знаходиться у тому ж репозиторії, публічний репозиторій (наприклад з GitHub Marketplace), або опублікований Docker container image. Ключове слово with дозволяє передати в екшен мапу вхідних параметрів, якщо такі передбачено.

Більшість екшенів містять крок із Checkout-action який, фактично, виконує pull вашого репозиторію на обраному ранері; не потрібен у випадку, коли ваш екшен не працює з кодом репозиторію (наприклад, відправляє повідомлення у месенджер по факту певної події у GitHub); також дозволяє працювати не лише з репозиторієм поточного воркфлоу, але і з будь-яким іншим публічним чи приватним (за умови наявності токена), кількома репозиторіями одночасно та, за потреби, перемикатися між їх гілками;

8. name — опціональне поле, буде відображатися у логах екшену (рис. 3);

9. run — виконує інструкцію командного рядка, використовуючи оболонку операційної системи. Може складатися як з поодинокої команди, так і з набору Інструкцій:

run: docker-compose up
run: |
  npm ci
  npm run build

Якщо в цього кроку не задано поле name — в логах як ім’я відобразиться повний текст команди. Ключове слово working-directory дозволяє задати директорію для виконання команди (наприклад, якщо ви працюєте з двома репозиторіями side-by-side і спочатку запускаєте команду в одному, а потім — в іншому);

Кожен крок може містити одну і тільки одну інструкцію uses або run.

Тригери запуску воркфлоу

Реальний проєкт вимагає більш точних налаштувань для тригерів ніж у нашому прикладі, тому можуть бути використані наступні комбінації фільтрів та умовних виразів:

1. Запустити воркфлоу при пуші в main:

on:
 push:
   branches:
     - main

2. Запустити на пул-реквесті не в гілку main:

on:
 pull_request:
   branches:
     - '!main'

3. Запустити після успішного відпрацювання іншого воркфлоу:

on:
 workflow_run:
   workflows:
     - deploy
   types:
     - completed

4. Якщо змінено файли в заданій директорії:

on:
 push:
   paths:
     - src

5. У будні о 18:00:

on:
 schedule:
   - cron: '0 18 * * 0,1,2,3,4'

6. При створенні пул-реквесту:

on:
 pull_request:
   types: opened

7. При додаванні ревьюверів:

on:
 pull_request:
   types: review_requested

8. При додаванні лейби:

on:
 pull_request:
   types: labeled
  • при такій конфігурації воркфлоу буде запускатися при додаванні будь-якої лейби;

8.1. Якщо потрібно запускати воркфлоу тільки для конкретної лейби, то можна написати наступним чином:

on:
 pull_request:
   types: labeled

jobs:
 run-autotests:
   if: ${{github.event.label.name == 'Run tests'}}
  • в такому випадку воркфлоу запуститься і відразу проскіпається, але потрапить на вкладку Actions у наступному вигляді:

  • при такій конфігурації під час додавання лейб важливо зберігати порядок, в іншому, якщо додавати одразу кілька — умовний вираз може повернути false і воркфлоу не відпрацює.

Використання умов у кроках воркфлоу

За замовчуванням усі кроки, вказані у воркфлоу відпрацьовують послідовно і якщо на якомусь з них станеться помилка — усі наступні будуть пропущені. Та в контексті запуску автотестів важливо отримати адекватний фідбек, згенерований репорт навіть у випадку падіння тестів або навпаки не отримувати його, якщо всі тести пройшли. Для досягнення такої поведінки можна використати умовний вираз If у поєднанні з функціями перевірки статусу:

1. Якщо потрібно виконувати крок завжди, навіть, якщо воркфлоу скасували:

if: ${{always()}}

2. Виконувати тільки при падінні:

if: ${{failure()}}

3. Виконувати завжди, крім випадків, коли воркфлоу скасували:

if: ${{failure() || success()}}

Також умовні вирази можна поєднувати з іншими літералами, операторами та функціями:

1. Тільки якщо воркфлоу запущено вручну:

if: ${{github.event_name == 'workflow_dispatch'}}

2. Якщо додано задану лейбу:

if: ${{github.event.label.name == 'Run Autotests'}}

3. Якщо передано вхідні дані:

if: ${{inputs.add_comment}}

Та ці умови будуть проігноровані, якщо один з попередніх кроків впаде, тому, якщо ми хочемо відписувати коментар незалежно від статусу, останню умову потрібно змінити наступним чином:

if: ${{inputs.add_comment && always()}}

Використання результатів одного кроку в іншому

Часто буває, що output одного кроку потрібно передати в input іншого (наприклад, ми згенерували репорт, завантажили на сервер і хочемо відправити у месенджер повідомлення з посиланням на цей репорт). В такому випадку задачу можна вирішити наступним чином:

1. Якщо output генеруємо самостійно — можемо одразу записати його в env (контекст зі змінними, які були задані у воркфлоу, завданні або певному кроці. Оновлене значення змінної недоступне на поточному кроці, але всі подальші будуть мати до нього доступ):

- name: Extract report
  run: echo "report=$(head target/results.txt)" >> $GITHUB_ENV

Тепер можемо використати отримані дані в іншому кроці:

- name: Add Comment With Report
  uses: peter-evans/[email protected]
  with:
   issue-number: ${{github.event.pull_request.number}}
   body: |
     **Autotests result:**
     ${{env.report}}

2. Якщо використовується екшен, результатом виконання якого є певний output — можемо задати цьому кроку id:

- name: Upload Allure Report
  id: report
  uses: Xotabu4/send-to-allure-server-action@1
  with:
   allure-server-url: 'https://allure.server/'

Тут використовуємо екшен, який генерує посилання на репорт на сервері, далі можемо відправити це посилання у Slack:

- name: Slack Notification
 uses: 8398a7/[email protected]
 with:
   status: ${{job.status}}
   custom_payload: |
     {
       "attachments": [
         {
           "title": "Build - ${{job.status}}",
           "text": "${{steps.report.outputs.report_url}}"
         }
       ]
     }
 env:
   SLACK_WEBHOOK_URL: ${{secrets.slack-token}}

Послідовний запуск джоб

Для налаштування повноцінного процесу CI/CD виникає потреба у послідовному запуску кількох джоб (наприклад спочатку збілдити аплікейшен, потім — задеплоїти його, після чого — запустити тести). Але за замовчуванням, усі джоби одного воркфлоу запускаються паралельно. Щоб змінити цю поведінку достатньо додати інструкцію needs:

push:
 branches:
   - master

jobs:
 build:
   runs-on: ubuntu-latest
   steps:
     ...

 deploy:
   runs-on: ubuntu-latest
   needs: build
   steps:
     ...

 tests:
   runs-on: ubuntu-latest
   needs: [build, deploy]
   steps:
     ...

Використання strategy та паралельний запуск джоб

Інший варіант — коли ви маєте кілька ідентичних джоб, у яких відрізняється лише кілька параметрів (наприклад, тести мають запуститися на macOs, Linux, Windows):

jobs:
 run_on_mac:
  runs-on: macos-latest
   ...
 run_on_linux:
  runs-on: ubuntu-latest
   ...
 run_on_windows:
  runs-on: windows-latest
   ...

Щоб уникнути дублювання коду в такому випадку можна використати strategy.matrix:

jobs:
 run_tests:
   strategy:
     matrix:
       os: [macos-latest, ubuntu-latest, windows-latest]
   runs-on: ${{matrix.os}}

Але в такій ситуації якщо хоча б одна джоба впаде — інші миттєво будуть скасовані, якщо ж наші білди незалежні і ми у будь-якому випадку хочемо дочекатися проходження кожного з них, потрібно задати стратегію при падінні (continue-on-error або fail-fast):

jobs:
 run_tests:
   fail-fast: false
   strategy:
     matrix:
       os: [macos-latest, ubuntu-latest, windows-latest]
   runs-on: ${{matrix.os}}

Запуск UI-тестів за допомогою Github Actions:

Для запуску UI-автотестів ми використовуємо Selenoid, а самі тести запускаємо у Docker-контейнері, для зручності розгортання необхідні сервіси описані у docker-compose:

1. Завантажуємо потрібні браузери:

а) сервіс, описаний у docker-compose:

pull-chrome:
 profiles:
   - chrome
 image: selenoid/vnc_chrome:105.0

б) крок, у екшені:

- name: Pull browser
  run: docker-compose --profile chrome pull

в) при потребі, можемо запустити паралельно кілька різних збірок для декількох версій браузерів, в такому випадку матимемо щось подібне:

strategy:
  fail-fast: false
  matrix:
    browser: [ chrome, firefox, edge ]
steps:
  - uses: actions/[email protected]
  - run: docker-compose --profile ${{matrix.browser}} pull

2. Далі підіймаємо сам Selenoid:

- run: docker-compose --profile selenoid up -d

3. І запускаємо тести:

- run: docker-compose --profile tests up

4. Отримані результати стискаємо та відправляємо на allure-server для генерації звіту за допомогою екшену (так можна обмежитися і прикріпленням архіву з артефактами білда, але для спрощення доступу ми обрали такий варіант):

- uses: Xotabu4/send-to-allure-server-action@1
  id: allure
  with:
   allure-server-url: ${{ALLURE_SERVER_URL}}
  if: failure() || success()

5. Посилання на отриманий репорт, разом з посиланням на поточний білд відправляємо у Слак, для зручності підсвічуємо повідомлення кольором, відповідно до статусу проходження тестів:

- uses: 8398a7/[email protected]
 with:
   status: custom
   custom_payload: |
     {
       "attachments": [
         {
           "title": "Build <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|#${{github.run_number}}>",
           "color": "${{job.status}}" === "success" ? "good" : "${{job.status}}" === "failure" ? "danger" : "warning",
           "text": "<${{steps.allure.outputs.report-url}}|View Allure Report>"
         }
       ]
     }
 env:
   SLACK_WEBHOOK_URL: ${{secrets.slack-token}}
 if: failure() || success()

Подальша оптимізація

Після того, як ми перенесли усі існуючі пайплайни в Github Actions виникла ідея автоматизувати крок Test інтеграційного етапу, для цього ми реалізували запуск тестів на пул-реквесті.

1. З метою оптимізації використання ресурсів (як інфраструктурних, так і людських) було прийнято рішення, що запуск буде виконуватися не на кожен пуш, а по додаванню відповідної лейби на пул-реквесті, після того, як пройшли код-ревью, оскільки час збірки та проходження автотестів може перевищувати 20 хвилин:

name: Run autotests on pull request
on:
 pull_request:
   types: [labeled]
jobs:
 run-autotests:
   if: github.event.label.name == 'Run Autotests'

2. В нашому випадку бізнесова логіка та е2е тести знаходяться у різних репозиторіях тож в цьому екшені потрібно працювати одразу з обома: кожен пулимо в окрему директорію, і оскільки сам екшен лежить у репозиторії з аплікейшеном, то щоб отримати доступ до приватного репозиторію з тестами потрібно вказати GitHub-токен:

- name: Pull Repository With Tests
  uses: actions/[email protected]
  with:
    repository: ${{AUTOTESTS_REPO}}
    token: ${{GH_TOKEN}}
    path: tests
- name: Pull App Repository
  uses: actions/[email protected]
  with:
    path: app

3. Наступний крок — виконати збірку аплікейшена — відповідний yml лежить в корені проєкту, тобто в директорії app нашого ранеру:

- name: Build App
  run: docker-compose up -d --build amomama.com app
  working-directory: front

4. Тепер можемо розгортати Selenoid та запускати тести, як ми б це робили і для продакшену. Після того як тести пройшли, можемо відписати в пул-реквесті коментар, щоб розробник або manual QA, який буде вести подальшу роботу над фічею та супроводжувати її до релізу, одразу міг побачити, чи є якісь проблеми з регресією (як і при написанні коментаря руками ви можете використати Markdown, щоб оформити посилання, виділити важливі дані та ін.):

- name: Add Comment With Report
  uses: peter-evans/[email protected]
  with:
    issue-number: ${{github.event.pull_request.number}}
    body: |
      **Autotests status: ${{job.status}}:**
      [Allure report](${{steps.report.outputs.url}})
  if: always()

Далі, в залежності від статусу тестів можна виконати деплой фічі на стейдж, але поки ми залишаємо це рішення за відповідальним інженером. Також, у зв’язку з особливістю продукту, за QA залишається рішення, коли здійснювати деплой на продакшен, оскільки під час проведення деяких змін може знадобитися призупинити роботу редакції у 17 країнах світу.

Після перенесення пайплайнів з Jenkins до Github Actions стало зрозуміло, що більшість воркфлоу у нас дуже подібна і використовує одні і ті самі екшени та команди як кроки, і тут виникла нова проблема: якщо нам потрібно оновити версії цих екшенів, змінити текст повідомлення або внести інші зміни — потрібно змінювати понад 20 воркфлоу.

Щоб уникнути дублювання коду у Jenkins Pipeline використовується Shared Libraries, Github actions мають два аналоги: композитні екшени та багаторазові екшени, але про їх відмінність та особливості використання поговоримо вже наступного разу.

👍ПодобаєтьсяСподобалось18
До обраногоВ обраному17
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Дякую за статтю,
Як альтернативу Allure server можна використовувати github.com/...​llure-report-with-history
з мінусів лише те, що рапорти публікуються на публічні gh-pages

Дякую за статтю, дуже інформативно!

Люблю Github Actions.
Так мало гарного та змiстовного матерiалу на цю тему.
Але тепер — його трохи бiльше)

Дуже цікаво. Дякую!

Підписатись на коментарі