Інструменти CI: GitHub Actions, GitLab CI, Jenkins
Зміст
У попередній главі ми розібрали пайплайн абстрактно: стадії, джоби, тригери, артефакти, runners (детально — у главі «Будова пайплайна»). Але жоден проєкт не запускає «пайплайн взагалі» — він запускає його в конкретному інструменті. І кожен інструмент говорить власним діалектом: те, що GitHub називає workflow, GitLab називає pipeline, а Jenkins — job. Той самий «крок» в одному місці — run, в іншому — script, у третьому — sh. Через це конфіг чужого проєкту читається як шифр, поки ти не побачиш за назвами ту саму знайому анатомію.
Для AQA це щоденна реальність, а не факультатив. Ти приходиш на проєкт — а там уже лежить теку .github/workflows/, або файл .gitlab-ci.yml, або Jenkinsfile. Треба вміти його прочитати, додати туди запуск своїх автотестів, зрозуміти, чому пайплайн червоний і що саме впало. На співбесіді класика: «З яким CI працювала? Як налаштуєш прогін автотестів?» Мета глави — навчитися бачити одну модель крізь три інструменти, щоб будь-який конфіг перестав бути магією.
Pipeline as code: конфігурація живе в репозиторії
Головний принцип, який об'єднує всі сучасні CI-інструменти, — пайплайн як код (pipeline as code). Замість того щоб клікати налаштування збірки в вебінтерфейсі, ти описуєш весь пайплайн текстовим файлом і кладеш його поруч із кодом у той самий репозиторій.
Чому це важливо, стає видно, коли порівняти з тим, як робили раніше. Стара школа Jenkins — freestyle job: адміністратор заходить у вебморду, руками проставляє галочки, вписує команди в текстові поля, зберігає. Проблем із цим підходом чотири, і всі болючі:
- Немає історії. Хто і коли змінив крок збірки? Невідомо — конфіг живе в базі сервера, не в git.
- Немає рев'ю. Зміну в пайплайні ніхто не переглядає перед застосуванням, як переглядають код.
- Немає відтворюваності. Впав сервер — конфіг треба відновлювати руками по пам'яті.
- Конфіг не подорожує з гілкою. У feature-гілці не можна легко мати інший набір кроків.
Pipeline as code лікує все це одним рухом. Конфіг — це файл у репозиторії, тож він версіонований (видно кожну зміну через git blame), рев'ю́ється у звичайному pull request (див. главу «Pull Request»), відтворюваний (клон репо = повний пайплайн) і подорожує з гілкою (кожна гілка несе свою версію кроків). Формат — майже завжди декларативний YAML: ти описуєш що має статися, а не пишеш імперативний скрипт як це зробити крок за кроком. Jenkins тут виняток — його мова Groovy ближча до справжнього коду, але й він переїхав із кліків у файл Jenkinsfile.
GitHub Actions: workflow, event, job, action
GitHub Actions — вбудований у GitHub CI. Його модель має чотири рівні, і плутанина між ними — найчастіша помилка новачка.
Workflow — це один YAML-файл у теці .github/workflows/ репозиторію. Проєкт може мати кілька workflow-файлів (окремо тести, окремо деплой, окремо нічний прогін). Кожен запускається у відповідь на event (подію) — це те, що описано в секції on:: push, pull_request, schedule (cron), workflow_dispatch (ручний запуск кнопкою). Ключове: подія — це подія на сервері GitHub, а не на твоєму диску. on: push спрацьовує, коли ти зробив git push, а не git commit (межу «Git ≠ GitHub» розібрано в главі «Системи контролю версій»).
Усередині workflow — jobs (джоби). За замовчуванням усі джоби виконуються паралельно, кожен на своєму свіжому runner (машині-виконавці; hosted на кшталт ubuntu-latest або self-hosted). Якщо один джоб має чекати на інший — це задають явно через needs:. Усередині джоба — steps (кроки), і от вони вже йдуть строго послідовно. Кожен крок — це одне з двох:
run:— виконати shell-команду (npm ci,npx playwright test);uses:— підключити action (екшен): готовий переиспользуваний блок, свій чи зі спільного каталогу GitHub Marketplace. Наприклад,actions/checkoutвикачує код репозиторію,actions/setup-nodeставить Node.js. Це і є та «цеглинка», що дала назву всьому інструменту.
Матриця (strategy.matrix) множить один джоб на набір комбінацій — наприклад, прогнати ті самі тести на трьох браузерах чи двох версіях Node одразу. Секрети підставляються через вираз ${{ secrets.NAME }} і ніколи не пишуться у файл відкритим текстом (гігієна секретів — окрема тема, глава «Секрети та конфігурація в CI»).
GitLab CI: один файл .gitlab-ci.yml
GitLab пішов іншим шляхом: замість теки з багатьма файлами — один файл .gitlab-ci.yml у корені репозиторію. Він описує весь пайплайн проєкту.
Модель виконання простіша і крутиться навколо двох понять — stages і jobs. Спочатку ти оголошуєш список стадій у порядку виконання:
stages:
- build
- test
- deploy
Далі описуєш джоби, і кожен прив'язуєш до стадії через ключ stage:. Правило виконання, яке треба засвоїти твердо: стадії йдуть послідовно, а джоби всередині однієї стадії — паралельно. Тобто всі джоби стадії test стартують разом, і лише коли всі вони зелені, починається стадія deploy.
Кожен джоб обов'язково має секцію script: — список команд. Ключ image: задає Docker-образ, у якому джоб виконається (механіка контейнерів — глава «Docker і тестові середовища»). Умовний запуск задають через rules: (сучасний спосіб) або старіші only/except. Артефакти й кеш — секції artifacts: і cache:. Виконавці називаються GitLab Runner і вибираються за тегами. Якщо треба порушити суворий порядок «стадія за стадією» й побудувати граф залежностей, є ключ needs: — так само, як needs: у GitHub Actions. Готового «маркетплейсу екшенів» тут немає, натомість повторне використання роблять через include: і шаблони.
Jenkins: Jenkinsfile і спадок freestyle
Jenkins — найстаріший із трьох і роками був найпоширенішим (за останніми опитуваннями розробників GitHub Actions уже випередив його за загальним поширенням, хоча Jenkins досі широко зустрічається у великих компаніях), і в ньому фактично живуть два світи.
Legacy-світ — freestyle jobs. Це та сама «конфігурація кліками» через вебінтерфейс, з якої ми почали розмову про pipeline as code. Досі величезна кількість проєктів так і працює, тож на реальній роботі ти це зустрінеш. Але це антипатерн: не версіонується, не рев'ю́ється, крихке.
Сучасний світ — Pipeline. Пайплайн описується файлом Jenkinsfile у репозиторії, мовою Groovy. Є два стилі: declarative (структурований, з фіксованими секціями — рекомендований за замовчуванням) і scripted (повноцінний імперативний Groovy — гнучкіший, але складніший). Структура declarative-пайплайна:
pipeline {
agent { docker { image 'mcr.microsoft.com/playwright:vX.Y.Z-noble' } }
stages {
stage('Install') {
steps { sh 'npm ci' }
}
stage('Test') {
steps { sh 'npx playwright test' }
}
}
post {
always {
archiveArtifacts artifacts: 'playwright-report/**', allowEmptyArchive: true
junit 'results.xml'
}
}
}
Тут agent каже, де виконувати (машина або Docker-образ), stages містить stage-и, а кожен stage — блок steps із командами (sh для shell). Секція post виконується після пайплайна — блоки always/success/failure зручні саме для AQA: зберегти звіт і скриншоти незалежно від того, впали тести чи ні.
Архітектурно Jenkins — це controller (координатор; історична назва «master») і agents (виконавці; історична назва «slave», машину-виконавця також називають node). Уся сила й уся біль Jenkins — у плагінах: майже кожна інтеграція (git, Docker, Allure, JUnit, Slack) — це окремий плагін, який хтось має встановити й підтримувати. Чому Jenkins досі всюди, попри вік: він self-hosted і безкоштовний, працює в закритому контурі (air-gapped, де хмарний CI заборонений політиками безпеки), а екосистема плагінів закриває майже будь-яку екзотику. Ціна — його треба самому ставити, оновлювати й доглядати.
Мапінг термінології
Три інструменти — одна анатомія. Ось словник-перекладач, який знімає більшість плутанини:
| Поняття | GitHub Actions | GitLab CI | Jenkins (declarative) |
|---|---|---|---|
| Де живе конфіг | .github/workflows/*.yml | .gitlab-ci.yml | Jenkinsfile |
| Верхній контейнер | workflow | pipeline | pipeline |
| Що запускає | event (on:) | тригер / rules | trigger / SCM-хук |
| Групування джобів | (немає стадій; needs) | stage | stage |
| Одиниця роботи | job | job | stage |
| Крок | step (run / uses) | рядок у script | step (sh) |
| Виконавець | runner | GitLab Runner | agent / node |
| Переиспользуваний блок | action | include / шаблон | plugin / shared library |
| Матриця | strategy.matrix | parallel:matrix | matrix |
| Секрети | secrets.* | CI/CD variables (masked) | credentials |
Дві пастки в цій таблиці варто підкреслити окремо. По-перше, «job» у GitHub і GitLab — це одиниця, що виконується на окремій машині, а в Jenkins цю роль грає радше stage (сам «job» у Jenkins — це весь пайплайн). По-друге, у GitHub Actions немає рівня «стадія»: джоби або паралельні, або зв'язані через needs, а візуальні групи в UI — це вже похідне від needs. У GitLab стадія — явна й обов'язкова.
Мінімальний workflow автотестів
Зберемо все докупи на найчастішому для AQA завданні: прогнати Playwright-тести на кожен push і pull request і зберегти звіт. Ось повний робочий workflow для GitHub Actions:
name: e2e-tests
on:
push:
branches: [main]
pull_request:
jobs:
playwright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
Читаємо крок за кроком, і кожен крок — універсальний, незалежно від інструмента:
- checkout — викачати код репозиторію на runner. Без цього немає що тестувати.
- setup-node — поставити потрібну версію Node.js.
npm ci— детермінована установка залежностей ізpackage-lock.json(самеci, неinstall— вона відтворювана й для CI призначена).npx playwright install --with-deps— докачати браузери та їхні системні бібліотеки.npx playwright test— власне прогін. Ненульовий код виходу цієї команди робить джоб (і весь пайплайн) червоним — це і є сигнал CI.- upload-artifact з
if: always()— зберегти HTML-звіт навіть якщо тести впали (безalways()крок пропустився б після червоного прогону, і ти лишився б без доказів).
Той самий флоу в GitLab CI — це джоб зі script, image з браузерами й секцією artifacts: { when: always }; у Jenkins — stage('Test') і збереження звіту в блоці post { always { ... } }. Кроки ті самі, змінюються лише ключові слова.
Окремо про формат обміну результатами. Усі три інструменти вміють читати JUnit XML — універсальний формат звіту про тести. Playwright уміє його генерувати, GitLab показує впалі тести прямо в merge request через artifacts:reports:junit, Jenkins — через junit-плагін, GitHub — через сторонні екшени-репортери. Це той місток, що дає бачити впалі тести в інтерфейсі CI, а не рити текстовий лог. Артефакти фейлу (трейси, скриншоти, відео) і специфічні для CI причини падінь — тема глави «Автотести в CI: стабільність і дебаг».
Як обирають інструмент
Чесна відповідь, яка часто дивує кандидатів: найчастіше інструмент не обирають — його диктує хостинг коду. Код на GitHub → природний вибір GitHub Actions (нічого не треба ставити, воно вже там). Код у GitLab → GitLab CI з тієї ж причини. Ці два «вбудовані» варіанти виграють за замовчуванням завдяки нульовому порогу входу.
Jenkins обирають свідомо й зазвичай тоді, коли є одна з причин:
- Self-hosted / закритий контур. Дані не можна віддавати в хмару — потрібен CI на своєму залізі.
- Готова інфраструктура. У компанії вже роками крутиться Jenkins із десятками плагінів і скриптів — переписувати все дорожче, ніж підтримувати.
- Екзотичні інтеграції. Потрібне щось, чого немає в хмарних CI, але є плагін під Jenkins.
- Незалежність від git-хостингу. Jenkins працює однаково з будь-яким сервером — і GitHub, і GitLab, і Bitbucket, і власним.
Решта критеріїв, які згадують на співбесіді: вартість (хмарні CI тарифікуються за хвилини — деталі економіки в главі «Паралелізація і швидкість пайплайна»), багатство екосистеми готових блоків, підтримка self-hosted runners, зручність секретів і оточень, крива навчання. Для AQA практичний висновок один: інструмент — це деталь, анатомія — спільна. Хто розуміє модель «тригер → джоби → кроки → артефакти», той за годину освоює будь-який із трьох.
Типові помилки
- «Джоби виконаються в тому порядку, як я їх написав». Виглядає логічно, а насправді за замовчуванням джоби паралельні (і в GitHub, і всередині стадії GitLab). Порядок задають явно:
needs:у GitHub, стадії абоneeds:у GitLab. Тест, що залежить від збірки, без цього стартує на порожньому місці. - «Пайплайн запуститься на мій commit». Ні: тригер
push— це подія на сервері, вона спрацьовує наgit push, а не на локальнийcommit. Поки не запушив — CI про твої коміти не знає. - «Jenkins — це конфіг кліками». Так виглядає legacy-freestyle, але сучасний Jenkins — це
Jenkinsfileу репозиторії, той самий pipeline as code. Плутати ці два світи — типовий провал на співбесіді. - «Action, job і step — синоніми». Ні. У GitHub: job — окрема машина, step — крок усередині неї, action — готова цеглинка, яку крок підключає через
uses. Три різні рівні. - «Тести впали — звіту не буде». Так і станеться, якщо забути
if: always()(GitHub) чиwhen: always(GitLab) чиpost { always }(Jenkins): крок збереження артефакту пропуститься після червоного прогону — саме тоді, коли звіт найпотрібніший. - «Той самий YAML перенесу з GitHub у GitLab». Синтаксис несумісний: різні ключі, різна модель стадій, різні вирази. Спільна лише анатомія, не текст.
- «Помилка відступу — то дрібниця». У YAML відступ — це синтаксис. Зайвий пробіл ламає весь пайплайн з малозрозумілою помилкою парсера; більшість «зелений код, червоний CI на порожньому місці» — це саме биті відступи.
Підсумок
- Pipeline as code — конфіг пайплайна живе у репозиторії як версіонований файл, який рев'ю́ється в PR; це витіснило «конфігурацію кліками» (freestyle) з усіх трьох інструментів.
- Одна анатомія — три діалекти. GitHub Actions (workflow/event/job/step/action), GitLab CI (
.gitlab-ci.yml, stages/jobs), Jenkins (Jenkinsfile, agent/stages/steps) описують ту саму модель різними словами. - Порядок виконання за замовчуванням — паралельний для джобів; послідовність задають явно (
needs:або стадії). Кроки всередині джоба — навпаки, строго послідовні. - Мінімальний прогін автотестів усюди однаковий: checkout → залежності → браузери → тест → артефакт-звіт із
always. Змінюються лише ключові слова. - Інструмент диктує хостинг коду: GitHub → Actions, GitLab → GitLab CI; Jenkins — свідомий вибір під self-hosted, закритий контур чи наявну інфраструктуру.
Що питають на співбесіді
- «З якими CI-інструментами працювала?» — інтерв'юер перевіряє не список брендів, а чи розумієш ти спільну модель. Сильна відповідь: назвати інструмент, але одразу показати анатомію («у GitHub Actions workflow реагує на event, містить паралельні джоби, кожен — з кроків, крок або
run, абоusesекшен»). - «Опиши, як налаштуєш прогін автотестів у CI». — чекають послідовність checkout → setup → install → test → артефакт і згадку, що звіт зберігається навіть при падінні. Плюс — згадати запуск на PR і на push.
- «Чим GitHub Actions відрізняється від Jenkins?» — дивляться, чи розумієш вісь hosted-vs-self-hosted і pipeline-as-code-vs-freestyle, а не завчені гасла.
- «Джоби в пайплайні йдуть послідовно чи паралельно?» — пастка на розуміння моделі виконання. Правильно: паралельно за замовчуванням, послідовність — через залежності/стадії.
- «Що таке pipeline as code і навіщо він?» — чекають чотири вигоди: версіонування, рев'ю, відтворюваність, конфіг подорожує з гілкою.
Джерела
- GitHub Actions Documentation — офіційна документація: workflow, events, jobs, actions.
- GitLab CI/CD Documentation — офіційна документація:
.gitlab-ci.yml, stages, runners. - Jenkins Pipeline Documentation — офіційний посібник: Jenkinsfile, declarative vs scripted.
- Playwright: Continuous Integration — готові рецепти прогону Playwright у різних CI.
- ISTQB CTFL 4.0 (Certified Tester Foundation Level) — розділ про DevOps і роль CI/CD у тестуванні (без прив'язки до конкретного інструмента).
Що таке «пайплайн як код» (pipeline as code) і чим він кращий за налаштування збірки кліками?
Це підхід, за якого весь пайплайн описано текстовим файлом, що лежить у репозиторії поруч із кодом, а не галочками у вебінтерфейсі сервера. Стара freestyle-модель, де адміністратор руками проставляв кроки в браузері, дає чотири болючі мінуси: нема історії змін, нема рев'ю, конфіг не відновиш після падіння сервера й він не їде разом із гілкою. Файл у репо лікує все одразу: кожну правку видно через git blame, зміну переглядають у звичайному pull request, клон репозиторію відтворює пайплайн повністю, а feature-гілка може нести власну версію кроків. Формат майже завжди декларативний YAML — ти кажеш що має статися, а не пишеш покрокову інструкцію як. На співбесіді від тебе чекають саме ці чотири вигоди: версіонування, рев'ю, відтворюваність і подорож із гілкою.
З яких рівнів складається модель GitHub Actions і як вони пов'язані?
Рівнів чотири, і плутанина між ними — класична помилка новачка. Workflow — це один YAML-файл у теці .github/workflows/; проєкт може мати їх кілька (окремо тести, окремо деплой). Кожен workflow стартує у відповідь на event (подію) із секції on: — push, pull_request, schedule, workflow_dispatch. Усередині — jobs (джоби), кожен на своєму свіжому runner, а всередині джоба — steps (кроки), які йдуть строго послідовно. Крок — це або run: (shell-команда), або uses: (готовий action, наприклад actions/checkout). Сильна відповідь одразу показує цю ієрархію: подія запускає workflow, той містить паралельні джоби, кожен складається з послідовних кроків.
Тригер on: push спрацьовує на git commit чи на git push? Чому різниця критична?
На git push, бо подія відбувається на сервері GitHub, а не на твоєму диску. Локальний commit записує зміну лише в твій репозиторій — сервер про неї ще нічого не знає, отже й CI мовчить. Пайплайн прокидається тільки тоді, коли коміти доїхали на віддалений сервер через push. Це прямий наслідок межі «Git — це не GitHub»: локальна система контролю версій і хостинг — різні речі. Практичний висновок: якщо чекаєш зеленого прогону, а його нема — перше питання «а ти взагалі запушив?».
Джоби в пайплайні виконуються послідовно чи паралельно за замовчуванням?
Паралельно — і це головна пастка на розуміння моделі виконання. У GitHub Actions усі джоби workflow за замовчуванням стартують одночасно, кожен на окремій машині; у GitLab CI паралельно біжать джоби в межах однієї стадії. Послідовність — це те, що треба задати явно: у GitHub через ключ needs:, у GitLab через порядок стадій або той самий needs:. Типовий наслідок помилки: тест, який залежить від складеного артефакту, без needs: стартує на порожньому місці, бо збірка ще не завершилась. А от кроки всередині одного джоба — навпаки, завжди строго послідовні.
Як у GitHub Actions змусити один джоб дочекатися іншого?
Через ключ needs: у залежному джобі — він перелічує імена джобів, які мусять завершитися успішно перед його стартом. Наприклад, джоб deploy із needs: [build, test] не почнеться, поки обидва не стануть зеленими. Без цього обидва джоби вважаються незалежними й запустяться одночасно, що для «збери → протестуй → задеплой» зламає логіку. По суті needs: перетворює плаский набір паралельних джобів на граф залежностей. У GitLab той самий ключ needs: дозволяє порушити суворий порядок «стадія за стадією» й теж побудувати граф.
Чим структура GitLab CI відрізняється від GitHub Actions?
GitLab пішов шляхом «один файл на весь проєкт»: замість теки з багатьма workflow-файлами тут єдиний .gitlab-ci.yml у корені репозиторію. Його модель крутиться навколо двох понять — stages і jobs: спершу оголошуєш список стадій у порядку виконання, потім описуєш джоби й кожен прив'язуєш до стадії ключем stage:. Правило залізне: стадії йдуть послідовно, а джоби в межах однієї стадії — паралельно, і наступна стадія стартує лише коли всі джоби попередньої зелені. Docker-образ джоба задають ключем image:, команди — секцією script:, умовний запуск — rules:. Готового «маркетплейсу» тут немає: повторне використання роблять через include: і шаблони.
Що таке стадія (stage) у GitLab і як вона співвідноситься з джобами?
Стадія — це явна іменована фаза пайплайна (наприклад build, test, deploy), а джоб — конкретна одиниця роботи всередині неї. Спершу ти оголошуєш список стадій у порядку їх виконання, а далі кожен джоб кидаєш у потрібну стадію ключем stage:. Модель проста: усі джоби однієї стадії стартують разом і біжать паралельно, а перехід до наступної стадії відбувається тільки після того, як уся попередня стала зеленою. Важлива відмінність від GitHub Actions: там рівня «стадія» взагалі немає — є лише джоби та їхні зв'язки через needs:, а візуальні групи в UI похідні від цих зв'язків. У GitLab же стадія — явна й обов'язкова частина моделі.
Чим run: відрізняється від uses: у кроці GitHub Actions?
Це два способи задати, що робить крок. run: виконує звичайну shell-команду — npm ci, npx playwright test; ти просто пишеш те, що набрав би в терміналі. uses: підключає action — готовий переиспользуваний блок, свій або зі спільного каталогу GitHub Marketplace: actions/checkout викачує код репозиторію, actions/setup-node ставить Node.js. Тобто run: — це «зроби ось цю команду», а uses: — «використай ось цю цеглинку, яку хтось уже написав». Саме ці екшени-цеглинки й дали назву всьому інструменту. Плутати action, job і step не можна: джоб — окрема машина, крок — дія всередині неї, екшен — те, що крок підключає через uses.
Freestyle job проти Jenkinsfile — у чому різниця й чому це часто ловлять на співбесіді?
У Jenkins фактично живуть два світи. Legacy — freestyle job: та сама «конфігурація кліками» через вебморду, з галочками й командами в текстових полях; величезна кількість старих проєктів досі так працює, тож на реальній роботі ти це зустрінеш, але це антипатерн — не версіонується, не рев'ю́ється, крихке. Сучасний світ — Pipeline, описаний файлом Jenkinsfile у репозиторії мовою Groovy, той самий pipeline as code. На співбесіді провалюються ті, хто ототожнює Jenkins із «конфігом кліками»: правильна відповідь розрізняє ці два світи й знає, що сучасний Jenkins тримає пайплайн у репо, як і решта інструментів.
Declarative проти scripted пайплайна в Jenkins — коли який?
Це два стилі опису Jenkinsfile. Declarative — структурований, із фіксованими секціями (pipeline, agent, stages, stage, steps, post); він рекомендований за замовчуванням, бо читабельний і має чіткі рамки. Scripted — повноцінний імперативний Groovy: гнучкіший, дозволяє складну логіку, але й складніший та легше стріляє в ногу. На практиці більшість проєктів пише declarative, а до scripted вдаються, коли треба щось, чого декларативний формат не дозволяє. Для AQA корисна деталь declarative-стилю — секція post із блоками always/success/failure: вона виконується після пайплайна й ідеально підходить, щоб зберегти звіт і скриншоти незалежно від результату тестів.
Чому слово «job» у Jenkins означає не те саме, що в GitHub і GitLab?
Бо це різні рівні анатомії під однією назвою. У GitHub і GitLab job — це одиниця, що виконується на окремій машині-виконавці. У Jenkins ту роль «одиниці, що біжить у своєму оточенні» грає радше stage, а слово «job» позначає весь пайплайн цілком. Тому механічний переклад «job = job» через три інструменти вводить в оману: правильний мапінг — джоб GitHub/GitLab ↔ стадія Jenkins. Це одна з двох головних пасток словника-перекладача; друга — що в GitHub Actions рівня «стадія» взагалі немає, тоді як у GitLab він явний.
Опиши мінімальний workflow прогону автотестів у CI.
Послідовність універсальна, хоч у GitHub Actions, хоч у GitLab, хоч у Jenkins — змінюються лише ключові слова. Крок 1 — checkout: викачати код репозиторію на runner, інакше нема що тестувати. Крок 2 — setup: поставити потрібну версію середовища (наприклад Node.js). Крок 3 — детермінована установка залежностей (npm ci). Крок 4 — докачати браузери та системні бібліотеки (npx playwright install --with-deps). Крок 5 — сам прогін (npx playwright test), чий ненульовий код виходу робить джоб і весь пайплайн червоним — це і є сигнал CI. Крок 6 — зберегти HTML-звіт як артефакт, обов'язково з умовою «навіть якщо тести впали». Плюс варто згадати, що запуск вішають і на push, і на pull_request.
Навіщо на кроці збереження звіту ставлять if: always()?
Щоб артефакт зберігся навіть після червоного прогону — саме тоді, коли він найпотрібніший. За замовчуванням, коли попередній крок (npx playwright test) впав з ненульовим кодом, наступні кроки джоба пропускаються — і крок вивантаження звіту теж не виконається. if: always() каже «виконай цей крок незалежно від того, що було раніше», тож HTML-звіт, трейси й скриншоти доїдуть до артефактів попри падіння тестів. У GitLab той самий сенс дає when: always у секції artifacts:, у Jenkins — блок post { always { ... } }. Забути це — типова помилка: тести червоні, доказів нема, дебажити ні по чому.
npm ci проти npm install — чому в CI беруть саме ci?
Бо npm ci детермінований і призначений спеціально для автоматизованих середовищ. Він ставить залежності строго за package-lock.json, не змінюючи його, і починає з чистого node_modules — тобто дає точно відтворюваний набір версій від прогону до прогону. npm install натомість може оновити lock-файл і підтягнути новіші сумісні версії, через що збірка стає невідтворюваною: локально одне, у CI інше. Для пайплайна відтворюваність критична — червоний тест має падати через код, а не через те, що вночі приїхала інша мінорна версія залежності. Тому в CI-скриптах майже завжди саме ci.
Як на проєкті обирають CI-інструмент і коли свідомо беруть Jenkins?
Чесна відповідь, яка часто дивує кандидатів: найчастіше інструмент не обирають — його диктує хостинг коду. Код на GitHub → природно GitHub Actions, воно вже вбудоване; код у GitLab → GitLab CI з тієї ж причини. Ці два «вбудовані» варіанти виграють за замовчуванням завдяки нульовому порогу входу. Jenkins беруть свідомо, коли є вагома причина: self-hosted і закритий контур (дані не можна віддавати в хмару), уже готова роками інфраструктура з десятками плагінів, екзотична інтеграція, для якої існує лише Jenkins-плагін, або незалежність від git-хостингу. Практичний висновок для AQA: інструмент — деталь, анатомія «тригер → джоби → кроки → артефакти» спільна, і хто розуміє модель, за годину освоює будь-який із трьох.
Що таке JUnit XML і навіщо він у CI?
Це універсальний текстовий формат звіту про результати тестів, який уміють читати всі три інструменти. Playwright (як і більшість фреймворків) уміє його генерувати, і саме через нього CI показує впалі тести прямо в інтерфейсі, а не змушує рити текстовий лог. GitLab підтягує його секцією artifacts:reports:junit і підсвічує падіння прямо в merge request, Jenkins — через junit-плагін, GitHub — через сторонні екшени-репортери. По суті це місток між прогоном і UI: він перетворює «щось червоне в логах» на структурований список конкретних впалих кейсів. Для дебагу це економить час — видно, який саме тест і чому впав, без гортання простирадла stdout.
Чому не можна просто перенести YAML із GitHub Actions у GitLab CI?
Бо в них спільна лише анатомія, а не текст. Синтаксис несумісний на всіх рівнях: різні імена ключів (jobs/steps/uses проти stages/script/image), різна модель виконання (у GitLab явні стадії, у GitHub їх нема), різні вирази для секретів і умов. Скопійований файл або не розпарситься, або зробить не те. Правильний спосіб «перенести» пайплайн — узяти ту саму модель (тригер → джоби → кроки → артефакти) і переписати її ключовими словами цільового інструмента. Окремо варто пам'ятати, що в YAML відступ — це синтаксис: зайвий пробіл ламає весь пайплайн малозрозумілою помилкою парсера, і добра частина «зелений код, червоний CI на порожньому місці» — це саме биті відступи.
Три ситуації, у яких AQA щодня стикається з CI-інструментами: прочитати той самий прогін трьома діалектами, не втратити звіт після червоного прогону і розшифрувати чужий конфіг на новому проєкті. Скрізь — що дивитися і чому.
Кейс 1. Один прогін — три діалекти
Задача одна: на кожен push і pull request прогнати Playwright-тести й зберегти звіт. Нижче — той самий флоу трьома інструментами. Анатомія ідентична (тригер → джоб → кроки → артефакт), змінюються лише ключові слова.
GitHub Actions — .github/workflows/e2e.yml:
name: e2e-tests
on:
push:
branches: [main]
pull_request:
jobs:
playwright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
GitLab CI — .gitlab-ci.yml:
stages:
- test
e2e:
stage: test
image: mcr.microsoft.com/playwright:v1.44.0-jammy
script:
- npm ci
- npx playwright test
artifacts:
when: always
paths:
- playwright-report/
Jenkins — Jenkinsfile (declarative):
pipeline {
agent { docker { image 'mcr.microsoft.com/playwright:v1.44.0-jammy' } }
stages {
stage('Install') { steps { sh 'npm ci' } }
stage('Test') { steps { sh 'npx playwright test' } }
}
post {
always {
archiveArtifacts artifacts: 'playwright-report/**', allowEmptyArchive: true
}
}
}
Що дивитися і чому:
- Один рядок словника-перекладача бачиш наживо. «Крок» — це
run:у GitHub, рядок уscript:у GitLab іshу Jenkins. «Готовий блок» —uses: actions/setup-nodeу GitHub, а в GitLab та Jenkins ту саму роль граєimage:з готовими браузерами, тож окремий крок установки не потрібен. - Збереження звіту — три написання однієї ідеї.
if: always(),when: alwaysіpost { always { ... } }роблять те саме: віддають артефакт навіть після червоного прогону. Приберіть цю умову — і при падінні тестів звіт не збережеться. npm ci, а неinstall, скрізь однаково. Детермінована установка зpackage-lock.json— те, що робить прогін відтворюваним від запуску до запуску; це властивість не інструмента, а дисципліни.- Перенести текст «як є» не вийде. Ключі, модель стадій і вирази несумісні — спільна лише анатомія. Тому «переписати пайплайн під інший CI» означає перекласти модель, а не скопіювати файл.
Кейс 2. Тести червоні — а звіту немає
Найдорожча дрібниця в конфізі автотестів. Уяви workflow, де крок збереження артефакту написали без умови:
- run: npx playwright test
- uses: actions/upload-artifact@v4 # ❌ немає if: always()
with:
name: playwright-report
path: playwright-report/
Поки тести зелені — усе працює. Але щойно npx playwright test завершується ненульовим кодом (тобто щось упало), джоб позначається як провалений, і всі наступні кроки пропускаються — зокрема й вивантаження звіту. Результат парадоксальний: звіт є рівно тоді, коли він не потрібен, і зникає рівно тоді, коли потрібен найбільше. Дебажити червоний прогін ні по чому — ні HTML-звіту, ні трейсів, ні скриншотів.
Виправлення — один рядок:
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always() # ✅ зберегти незалежно від результату тестів
with:
name: playwright-report
path: playwright-report/
Що дивитися і чому:
if: always()не ховає падіння тестів. Джоб усе одно лишається червоним (це визначає код виходуplaywright test) — умова впливає тільки на те, чи виконається крок збереження. Ти отримуєш і чесний червоний статус, і докази.- У GitLab і Jenkins та сама пастка з іншими словами. Без
when: alwaysу секціїartifacts:GitLab викине звіт при падінні; безpost { always }те саме зробить Jenkins. Умова «завжди» — не косметика, а частина коректного конфігу. - Це перше, що варто перевірити в чужому пайплайні. Якщо колеги скаржаться «тест червоний, а подивитися нема на що» — дивись, чи є умова always на кроці артефакту.
Кейс 3. Зелений локально, червоний у CI на порожньому місці
Ти прийшов на новий проєкт, у репо вже лежить .gitlab-ci.yml, і твій новий джоб e2e падає ще до першого тесту. Два найчастіші винуватці — порядок джобів і биті відступи.
Порядок джобів. Припустимо, конфіг такий:
stages:
- build
- test
build-app:
stage: build
script: [ npm run build ]
e2e:
stage: build # ❌ помилково в тій самій стадії, що й збірка
script: [ npx playwright test ]
e2e і build-app опинилися в одній стадії build, а джоби всередині стадії біжать паралельно. Тести стартують одночасно зі збіркою — на порожньому місці, бо застосунок ще не зібрано. Лікування: винести e2e у стадію test (яка йде після build), або в GitHub Actions додати залежному джобу needs: [build-app]. Правило, яке рятує від цілого класу таких падінь: за замовчуванням джоби паралельні, послідовність задають явно.
Биті відступи. У YAML відступ — це синтаксис, а не форматування. Зайвий пробіл перед ключем ламає весь пайплайн малозрозумілою помилкою парсера, хоча код застосунку бездоганний:
e2e:
stage: test
script: # ❌ зайвий пробіл — script «випав» із джоба
- npx playwright test
Що дивитися і чому:
- «Червоний CI на порожньому місці» частіше про конфіг, ніж про код. Перш ніж підозрювати застосунок, перечитай сам YAML: відступи, стадію джоба, наявність
needs:. - Спершу відтвори модель у голові. Постав питання «цей джоб від чогось залежить?» і «він точно в правильній стадії?» — це знімає більшість гонок «тест стартував раніше за збірку».
- Помилку парсера читай буквально. Повідомлення на кшталт «did not find expected key» майже завжди вказує на відступ, а не на логіку — шукай зсув на пробіл, а не переписуй кроки.
Pipeline as code
- Можу назвати чотири вигоди підходу «пайплайн як код»: версіонування, рев'ю в PR, відтворюваність, конфіг подорожує з гілкою.
- Знаю, чому freestyle-модель (налаштування кліками) — антипатерн: нема історії, нема рев'ю, конфіг не відновиш і не візьмеш у гілку.
GitHub Actions
- Знаю чотири рівні моделі: workflow → event (
on:) → jobs → steps, і не плутаю їх між собою. - Можу пояснити різницю
run:(shell-команда) vsuses:(готовий action на кшталтactions/checkout). - Знаю, що workflow-файли лежать у теці
.github/workflows/і їх може бути кілька на проєкт.
GitLab CI
- Знаю, що весь пайплайн описано одним файлом
.gitlab-ci.ymlу корені репозиторію. - Можу пояснити модель stages + jobs: стадії послідовні, джоби всередині стадії паралельні.
- Знаю ключі джоба:
script:(команди),image:(Docker-образ),rules:(умовний запуск),artifacts:/cache:.
Jenkins
- Розрізняю два світи Jenkins: legacy freestyle (кліки) і сучасний
Jenkinsfile(pipeline as code). - Знаю різницю declarative (структурований, рекомендований) vs scripted (імперативний Groovy, гнучкіший) пайплайна.
- Розумію секцію
postз блокамиalways/success/failureі чому вона зручна для збереження звіту AQA.
Мапінг і модель виконання
- Знаю пастку: «job» у Jenkins — це радше
stage, а сам job у Jenkins — весь пайплайн. - Пам'ятаю, що в GitHub Actions рівня «стадія» немає (лише
needs:), а в GitLab стадія явна й обов'язкова. - Твердо знаю: джоби за замовчуванням паралельні, послідовність задають через
needs:або стадії; кроки в джобі — навпаки, послідовні.
Прогін автотестів і артефакти
- Можу з голови описати мінімальний прогін: checkout → setup → залежності → браузери → тест → артефакт-звіт.
- Розумію, чому в CI беруть
npm ci, а неnpm install(детермінована, відтворювана установка з lock-файлу). - Пам'ятаю обов'язковість
if: always()(GitHub) /when: always(GitLab) /post { always }(Jenkins) для звіту при падінні. - Розумію роль JUnit XML як універсального містка, що показує впалі тести в UI CI замість тексту логу.
Вибір інструмента
- Можу пояснити, що інструмент найчастіше диктує хостинг коду: GitHub → Actions, GitLab → GitLab CI.
- Знаю причини свідомо взяти Jenkins: self-hosted/закритий контур, готова інфраструктура, екзотичний плагін, незалежність від git-хостингу.
Які вигоди дає підхід «пайплайн як код» порівняно з freestyle-налаштуванням кліками? Оберіть усі правильні.
Питання
Що таке «пайплайн як код» (pipeline as code)?