Будова пайплайна: стадії, джоби, тригери, артефакти
Зміст
У попередній главі «CI/CD: інтеграція, доставка, розгортання» ми розібрали, навіщо існує CI: щоб інтеграція не перетворювалась на пекло, а фідбек приходив за хвилини, а не за тижні. Тепер — з чого CI фізично складається. Пайплайн (pipeline) — це не магія, а звичайний конвеєр: набір кроків, які автоматично виконуються на певну подію в репозиторії.
Для AQA будова пайплайна — це не факультатив, а щоденне середовище проживання. Ваші автотести не запускаються «десь у хмарі» — вони живуть усередині конкретного джоба (job), на конкретному ранері (runner), зі своїми змінними середовища. Коли о другій ночі пайплайн червоний, від вас чекають не «перезапустити й молитися», а конкретну відповідь: яка стадія впала, ваш це тест чи інфраструктура, де взяти трейс падіння і чому джоба взагалі запустилась. Усе це — про будову пайплайна. І майже кожна співбесіда на AQA/SDET має блок «розкажіть, як влаштований ваш CI»: тут відсіюють тих, хто натискав зелену кнопку, від тих, хто розуміє конвеєр.
Ієрархія: pipeline → stage → job → step
Пайплайн має чітку вкладену структуру з чотирьох рівнів. Розуміти її треба не як термінологію, а як механізм ізоляції та порядку.
- Пайплайн (pipeline) — увесь конвеєр цілком, який запускається на одну подію (наприклад, на push). Один запуск пайплайна = одна спроба провести зміни крізь усі перевірки.
- Стадія (stage) — впорядкована фаза конвеєра: спершу
build, потімtest, потімdeploy. Стадії виконуються послідовно: наступна не почнеться, доки попередня не завершилась успішно. Стадія — це ворота (gate). - Джоба (job) — одиниця роботи, яка виконується на окремому чистому ранері. Джоби всередині однієї стадії за замовчуванням біжать паралельно й незалежно. Джоба — це одиниця планування та ізоляції: у неї свій процес, своя файлова система, свій набір змінних.
- Крок (step) — одна команда всередині джоба: «викачати код», «встановити залежності», «запустити тести», «завантажити артефакти». Кроки виконуються послідовно згори вниз; перший провалений крок зазвичай зупиняє джобу.
Чому саме така ієрархія, а не плаский список команд? Три причини, кожна — про реальну потребу. Порядок: стадії гарантують, що ви не задеплоїте білд, який не зібрався й не пройшов тести. Паралелізм: джоби в одній стадії біжать одночасно, тож e2e-chromium і e2e-firefox не чекають одне одного, і прогін коротший. Ізоляція: кожна джоба стартує на чистому ранері, тож сміття з однієї не протікає в іншу — а це прямо про стабільність автотестів.
Важливе застереження: конкретні назви рівнів різняться між інструментами. У GitLab CI стадії задаються явно через stages. У GitHub Actions окремого поняття «stage» немає — там є джоби, а порядок між ними виражають через залежності (needs). У Jenkins є stage і steps усередині. Ми говоримо про концептуальну модель; мапінг термінології на конкретні інструменти — у главі «Інструменти CI: GitHub Actions, GitLab CI, Jenkins».
Тригери: коли пайплайн запускається
Тригер (trigger) — це подія, на яку CI реагує запуском пайплайна. Ключова думка: різні тригери — різний обсяг перевірок, бо ганяти повну регресію на кожен коміт і довго, і дорого.
Найпоширеніші тригери:
- push — коміт запушено в гілку. Часто налаштовують окремо для гілок фіч і для
main. - pull request / merge request — відкрито або оновлено PR. Це головний тригер для перевірок якості перед злиттям (див. «Pull Request і стратегії гілкування»).
- schedule / cron — за розкладом. Синтаксис cron — п'ять полів:
хвилина година день-місяця місяць день-тижня. Наприклад,0 2 * * *— щодня о 02:00. Тут пастка: розклад майже завжди рахується в UTC, а не у вашому часовому поясі — не дивуйтесь, що «нічний» прогін стартує в інший час. - ручний запуск (manual) — людина натискає кнопку (у GitHub Actions це
workflow_dispatch). Зручно для деплою чи разового прогону важкого набору. - тег (tag) — створення тега на кшталт
v1.4.0. Типово запускає реліз-пайплайн.
Типова стратегія розкладки: на PR — швидкий smoke, щоб не тримати розробника; на push у main — повніший набір; нічним cron — уся регресія, яку не шкода ганяти годину. Цю стратегію докладно розбирає глава «Стратегія якості в пайплайні».
Ще один нюанс саме для AQA: PR із форка (fork) зазвичай не отримує доступу до секретів — це навмисне обмеження безпеки, щоб чужий код у чужому PR не витягнув ваші токени. Тому тести, яким потрібні креденшели, у таких PR можуть падати або пропускатись — і це не зламана конфігурація, а захист (див. «Секрети та конфігурація в CI»).
Runners: hosted vs self-hosted
Ранер (runner), або агент (agent), — це машина, яка реально виконує джобу: викачує код і проганяє команди. Ранери бувають двох типів, і різниця між ними — часте джерело «а чому в CI поводиться інакше».
Hosted-ранери надає сам CI-провайдер. Ключова властивість: на кожну джобу — свіжа віртуалка, яка після завершення знищується. Плюси: чистий старт без сміття, нуль обслуговування. Мінуси: обмежені ресурси (CPU/RAM), оплата за хвилини, і ви не контролюєте оточення.
Self-hosted-ранери — ваші власні машини, які ви підключаєте до CI. Плюси: потужніше залізо, кастомне оточення (специфічні браузери, доступ до внутрішньої мережі), економія на хвилинах. Мінуси: ви їх обслуговуєте, і — головна пастка — вони персистентні: стан від попереднього прогону нікуди не дівається сам.
Чому це критично для тестів. На спільному self-hosted-ранері легко зловити флак (flakiness), якого немає локально: лишилися зомбі-процеси браузера від попередньої джоби, забився диск скриншотами, у порт уже хтось стукає. Виглядає як баг застосунку — а насправді брудний ранер. Fresh hosted-ранер від цього застрахований, але має інший бік: слабше залізо означає інші тайминги, і тест, який покладається на швидкість, раптом не встигає — класичне «локально проходить, у CI падає» (див. «Автотести в CI: стабільність і дебаг»). Ізольоване відтворюване оточення для джоба часто будують контейнером — про це глава «Docker і тестові середовища».
Змінні середовища
Джоба не має права знати, куди їй стукати й під ким логінитись, зашитим у код. Base URL стенду, ім'я тестового користувача, ознака «ми в CI» — усе це передають через змінні середовища (environment variables). Це стандартний канал конфігурації, який не залежить від мови й інструмента.
Змінні задають на кількох рівнях: усього пайплайна, окремої джоби, окремого кроку — і вужчий рівень перекриває ширший. Окремо стоять секрети (secrets): змінні з чутливими значеннями (токени, паролі), які CI маскує в логах, замінюючи на ***. Плутати звичайну змінну із секретом — пряма діра; канон гігієни секретів — у главі «Секрети та конфігурація в CI».
Автотести читають ці змінні з оточення процесу. У Playwright конфіг зазвичай бере base URL зі змінної, а поведінку перемикає за ознакою CI:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
},
// У CI забороняємо .only і додаємо ретраї — типовий дефолт
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
});
Тут process.env.CI — конвенційна змінна, яку більшість CI-систем виставляє в true автоматично. Playwright спирається на неї, щоб у локальному прогоні не було ретраїв (вони маскують флак), а в CI були (там флак дорожчий). Один і той самий код тесту так адаптується до середовища без жодного if у самому тесті.
Артефакти джоба
Ранер ефемерний: після завершення джоба знищується разом з усім, що на ній лежало. Тож усе, що ви хочете зберегти після прогону, треба явно оголосити артефактом (artifact) — інакше воно зникне.
Що зазвичай зберігають артефактами:
- зібраний білд — щоб наступна джоба взяла готовий бандл, а не збирала його вдруге;
- звіти про тести — HTML-звіт, JUnit XML (універсальний формат, який розуміє більшість CI);
- докази падіння — скриншоти, відео, трейс (trace) Playwright, логи.
Для AQA артефакти — це різниця між «тест червоний, поняття не маю чому» і «відкрив трейс, побачив клік у порожнечу». Playwright пише трейс і відео при падінні; джоба вивантажує їх артефактом; ви завантажуєте його з CI і відкриваєте у trace viewer — так дебажать фейл, який відтворюється лише в CI. Але тут — найпоширеніша пастка новачків: артефакт треба вивантажувати навіть тоді, коли тести впали. Якщо крок вивантаження стоїть без умови «виконати завжди», він просто не запуститься після провалу тестів — і ви лишитесь без доказів. Про це — у секції про умовний запуск нижче.
Артефакти виконують і другу роль — передають дані між джобами. Джоба build виробляє бандл і кладе його в артефакт; джоба test у наступній стадії його забирає. Так стадії обмінюються результатами, попри те що біжать на різних ранерах.
Кешування залежностей
Кожен прогін встановлює залежності з нуля: npm ci тягне сотні пакетів, Playwright докачує браузери. Це десятки секунд, а то й хвилин — щоразу однакової роботи. Кеш (cache) дозволяє зберегти ці залежності між прогонами й відновити їх наступного разу.
Механізм тримається на ключі кешу (cache key), який зазвичай рахують як хеш lock-файлу (package-lock.json). Логіка та сама, що у фінгерпринтингу ассетів (див. «Кешування»): доки залежності не змінились — хеш той самий, кеш підхоплюється; щойно ви оновили пакет — lock-файл змінюється, хеш інший, кеш перебудовується. Так кеш ніколи не віддасть застарілі залежності — якщо ключ підібрано правильно.
Артефакт і кеш — різні речі, і на співбесіді їх люблять розводити. Плутати їх — концептуальна помилка:
| Аспект | Артефакт | Кеш |
|---|---|---|
| Навіщо | зберегти результат / передати далі | пришвидшити прогін |
| Чи можна втратити | ні, це цінний вихід | так, це лише оптимізація |
| Коректність залежить від нього | так | ніколи не повинна |
| Приклад | трейс падіння, білд, JUnit-звіт | node_modules, кеш браузерів |
Головне правило: коректність прогону не сміє залежати від кешу. Кеш може бути порожнім (перший прогін), протухлим чи скинутим — і пайплайн зобов'язаний однаково правильно відпрацювати, просто повільніше. Стратегії, що і як кешувати на великих пайплайнах, — у главі «Паралелізація і швидкість пайплайна».
Статуси і залежності джобів
Кожна джоба проходить свій життєвий цикл: queued/pending (у черзі на ранер) → running (виконується) → фінальний статус: success, failed, cancelled або skipped. Статус усього пайплайна — агрегат: зазвичай він червоний, якщо впала хоча б одна блокувальна джоба.
Джоби рідко незалежні — вони утворюють граф залежностей (DAG). Залежність виражають директивою на кшталт needs (у GitHub Actions і GitLab CI): джоба deploy чекає, поки завершаться всі три тестові джоби. Це дає дві типові фігури: fan-out (одна джоба породжує кілька паралельних) і fan-in (кілька збігаються в одну).
За замовчуванням провалена джоба блокує все, що від неї залежить: якщо e2e-chromium червоний, deploy-staging навіть не стартує. Це і є сенс quality gate. Іноді потрібне протилежне — щоб некритична джоба не валила весь пайплайн (наприклад, експериментальний браузер): для цього є continue-on-error / allow_failure, які кажуть «дай їй впасти, але не блокуй решту». Користуватися цим на основних тестах — спосіб приховати проблему, тож обережно.
Умовний запуск
Не кожна джоба має бігти щоразу. Умовний запуск (conditional execution) — це правила «запусти цю джобу/крок, тільки якщо…». Навіщо: економія часу й грошей, і коректна логіка релізу.
Типові умови:
- за гілкою — деплой у prod лише з
main, а не з кожної фіч-гілки; - за подією — реліз-джоба лише на тег;
- за зміненими файлами (path filter) — ганяти візуальні тести, лише якщо чіпали UI, а не бекенд;
- за результатом попередніх кроків — виконати крок, тільки якщо щось упало (
if: failure()), або — навпаки — завжди (if: always(), у GitLabwhen: always).
Останнє — прямий рецепт для тієї пастки з артефактами: крок вивантаження трейсів має стояти з always(), щоб спрацювати саме тоді, коли тести червоні й докази найпотрібніші.
І ключовий нюанс для читання пайплайна: skipped — це не passed. Джоба, яку відсіяла умова, не запускалась зовсім; вона не «пройшла» — її просто не було. Зелений пайплайн, де критична тестова джоба тихо пропущена path-фільтром, дає фальшиве відчуття безпеки.
Типові помилки
- «Тест зелений локально, червоний у CI — значить, баг у моєму тесті» → часто винне оточення ранера: headless замість headed, слабше залізо й інші тайминги, відсутня змінна середовища. Логіка тесту може бути правильною (див. «Автотести в CI»).
- «Тести впали, а трейсу немає» → крок вивантаження артефактів стояв без
always()і не запустився після провалу. Виглядає як «CI загубив файли», а насправді його не попросили їх зберегти при падінні. - «Кеш пришвидшив, але тести беруть стару версію бібліотеки» → ключ кешу не включав хеш lock-файлу, тож CI підтягнув застарілі
node_modules. Кеш став залежністю коректності — цього не можна. - «Пайплайн зелений, отже, тести пройшли» → тестова джоба була skipped через умову чи path-фільтр. Skipped рахується не як провал, але й не як успіх — перевірки просто не було.
- «Флак на self-hosted-ранері — нестабільний застосунок» → на персистентному ранері лишився стан від попереднього прогону: процеси, файли, зайняті порти. Це брудне оточення, а не баг продукту.
- «Секрети зникли в PR — хтось зламав конфіг» → PR із форка навмисне не отримує секретів. Це безпека за задумом, а не поломка.
Підсумок
- Пайплайн вкладений: pipeline → stage → job → step. Стадії дають порядок, джоби — паралелізм та ізоляцію (кожна на чистому ранері), кроки — гранульованість і логи.
- Тригер визначає обсяг: PR — smoke, push у
main— повніше, cron — нічна регресія. Cron зазвичай рахується в UTC. - Ранер ефемерний: нічого не переживе джобу, крім артефакту. Кеш пришвидшує прогін, але коректність від нього залежати не сміє. Артефакт і кеш — різні сутності.
- Джоби утворюють DAG через
needs; провал блокує залежних. Skipped ≠ passed — умова могла тихо вимкнути перевірку. - Докази падіння (трейс, скриншоти) вивантажуйте з умовою
always(), інакше при червоних тестах їх не буде саме тоді, коли вони найпотрібніші.
Що питають на співбесіді
- «Чим артефакт відрізняється від кешу?» — інтерв'юер перевіряє, чи розумієте ефемерність ранера й головне правило: коректність не залежить від кешу, а артефакт — це цінний вихід, який треба зберегти.
- «Як передати зібраний білд із джоба build у джоб test?» — очікують згадку артефактів і залежності
needs; слабка відповідь — «збилдити ще раз у тестовій джобі». - «У чому різниця між stage, job і step?» — дивляться, чи бачите ви за словами механізм: порядок, паралелізм, ізоляція, а не завчену термінологію.
- «Тест проходить локально, а в CI падає — де шукатимеш?» — хочуть почути систему: оточення ранера, headless, тайминги, змінні середовища, і — обов'язково — «візьму трейс/скриншот з артефактів», а не «перезапущу джобу».
- «Які тригери й обсяг перевірок ти б поставив на PR, а які — на нічний прогін?» — перевіряють розуміння економіки CI: швидкий фідбек на PR проти повного покриття вночі (містки до глав про стратегію якості й швидкість пайплайна).
- «Hosted чи self-hosted ранери — коли що?» — чекають на компроміс: чистота й нуль-обслуговування проти потужності, кастомного оточення й ризику протікання стану.
Джерела
- ISTQB Certified Tester Foundation Level (CTFL) 4.0 Syllabus — підрозділ 2.1.4 «DevOps and Testing» (місце автоматизації тестів у конвеєрі CI/CD).
- GitHub Actions documentation — офіційна модель workflow → job → step,
needs, артефакти й кешування залежностей. - GitLab CI/CD documentation — стадії, джоби,
artifacts:іcache:, правила запуску. - Playwright — Continuous Integration — запуск тестів у CI, ознака
CI, вивантаження трейсів і звітів як артефактів.
Що таке пайплайн і з яких рівнів він складається?
Пайплайн (pipeline) — це автоматичний конвеєр кроків, який CI проганяє у відповідь на подію в репозиторії: один запуск = одна спроба провести зміни крізь усі перевірки. Усередині він вкладений на чотири рівні. Найширший — сам пайплайн, під ним ідуть стадії (stage), у стадіях сидять джоби (job), а в джобах — окремі кроки (step). Кожен рівень відповідає за свою річ: стадії задають порядок фаз, джоби — паралельні незалежні одиниці роботи, кроки — окремі команди на кшталт «викачати код» чи «запустити тести». Для AQA це не абстракція, а середовище, де живуть ваші автотести: вони виконуються всередині конкретного джоба, і читати цю ієрархію треба вміти, щоб на червоному пайплайні одразу бачити, що і де впало.
У чому різниця між stage, job і step?
За цими словами стоїть механізм, а не термінологія, і сильна відповідь називає саме механізм. Стадія — це ворота (gate): фази йдуть строго послідовно, і test не почнеться, доки build не завершився успішно. Джоба — одиниця планування та ізоляції: вона стартує на окремому чистому ранері зі своїм процесом, своєю файловою системою та своїм набором змінних, а джоби в межах однієї стадії за замовчуванням біжать паралельно. Крок — одна команда всередині джоба, кроки виконуються згори вниз, і перший провалений зазвичай валить весь джоб. Тобто стадії дають порядок, джоби — паралелізм та ізоляцію, кроки — гранульованість і окремі логи. Плутати ці рівні на співбесіді — червоний прапор.
Навіщо потрібна ізоляція джоба на окремому ранері?
Кожна джоба стартує на чистому оточенні, і це прямо про стабільність автотестів. Якби всі команди бігли в одному спільному середовищі, сміття з попередньої роботи протікало б далі: недобиті процеси браузера, зайняті порти, тимчасові файли, змінні від сусіднього кроку. Ізоляція гарантує, що джоба e2e-firefox не бачить наслідків джоби e2e-chromium і навпаки. Побічний ефект такої моделі — джоби нічим не діляться автоматично: усе, що одна джоба виробила й що потрібне іншій, доводиться явно передавати через артефакти. Тобто ізоляція коштує трохи зручності, але дає передбачуваність, без якої тести флакали б від чужого стану.
Що таке тригер і чому на різні тригери ставлять різний обсяг перевірок?
Тригер (trigger) — це подія, на яку CI відповідає запуском пайплайна: push, відкриття PR, розклад, тег, ручний старт. Обсяг перевірок прив'язують до тригера тому, що ганяти повну регресію на кожен коміт і повільно, і дорого. Розумна розкладка: на PR — швидкий smoke, щоб не тримати розробника в очікуванні; на push у main — повніший набір; нічним cron — уся важка регресія, на яку не шкода години. Так фідбек лишається швидким там, де за нього чекають наживо, а глибоке покриття їде туди, де ніхто не дивиться на годинник. Розуміння цієї економіки — те, що інтерв'юери перевіряють питанням «що б ти поставив на PR, а що на нічний прогін».
Які бувають тригери й що в них підступного?
Основні: push (коміт у гілку, часто окремо для фіч і для main), pull/merge request (головний тригер перевірок перед злиттям), schedule/cron (за розкладом), manual (людина тисне кнопку, у GitHub Actions це workflow_dispatch) і tag (створення тега на кшталт v1.4.0, зазвичай запускає реліз). Найпоширеніша пастка — у cron: його синтаксис це п'ять полів «хвилина година день місяць день-тижня», і рахується він майже завжди в UTC, а не у вашому часовому поясі. Тому «нічний» прогін, який ви поставили на другу ночі, реально стартує зі зсувом — і це не поломка, а неврахований часовий пояс. Друга тонкість саме для AQA: тег і ручний запуск часто ведуть на окремі важкі пайплайни, тому не всі перевірки видно на звичайному push.
Чому PR із форка часто не отримує секретів і як це б'є по тестах?
Це навмисне обмеження безпеки, а не зламана конфігурація. Якби чужий PR із форка (fork) мав доступ до ваших токенів, автор такого PR міг би дописати крок, який ці токени витягне й відправить назовні — тому CI за замовчуванням не віддає секрети коду з форка. Практичний наслідок: тести, яким потрібні креденшели (логін під реальним користувачем, звернення до платного API), у таких PR можуть падати або тихо пропускатись. Побачивши це, недосвідчений інженер біжить «чинити конфіг», хоча чинити нічого — це захист за задумом. Правильна реакція: розуміти, що для форк-PR частина сценаріїв недоступна, і не завалювати їх у quality gate там, де секретів принципово немає.
Чим hosted-ранер відрізняється від self-hosted і коли який брати?
Ранер (runner) — це машина, яка реально виконує джобу. Hosted надає сам CI-провайдер: на кожну джобу піднімається свіжа віртуалка, яка після завершення знищується — чистий старт і нуль обслуговування, але слабше залізо, оплата за хвилини й повна відсутність контролю над оточенням. Self-hosted — ваші власні машини: потужніше залізо, кастомне оточення (специфічні браузери, доступ до внутрішньої мережі), економія на хвилинах, але ви їх обслуговуєте, і вони персистентні. Компроміс саме такий: чистота й безтурботність проти потужності, гнучкості й ризику протікання стану. Для важких e2e з рідкісним оточенням беруть self-hosted; для звичайних перевірок, де важлива відтворюваність, — hosted.
Тест флакає на self-hosted-ранері, хоча локально стабільний. Чому?
Найімовірніше винне брудне оточення персистентного ранера, а не застосунок. На self-hosted машині стан від попереднього прогону нікуди не дівається сам: лишилися зомбі-процеси браузера, забився диск скриншотами й відео, у потрібний порт уже хтось стукає, недочищена тимчасова папка. Наступна джоба потрапляє в це сміття й падає «випадково» — виглядає як нестабільний продукт, а насправді брудний ранер. Fresh hosted-ранер від цього застрахований, бо щоразу стартує з нуля, зате має інший бік: слабше залізо дає інші тайминги, і тест, який мовчки покладався на швидкість, раптом не встигає. Тому діагностика «локально зелено, у CI червоно» починається не з коду тесту, а з питання, на якому ранері й у якому оточенні воно бігло.
Як конфігурацію передають у джобу і чим секрет відрізняється від звичайної змінної?
Джоба не має права знати base URL стенду чи ім'я тестового користувача, зашитими в код, — усе це передають через змінні середовища (environment variables). Це стандартний канал, незалежний від мови й інструмента: змінні задають на рівні всього пайплайна, окремої джоби чи окремого кроку, і вужчий рівень перекриває ширший. Окремо стоять секрети (secrets) — змінні з чутливими значеннями (токени, паролі), які CI маскує в логах, підмінюючи на ***, щоб вони не витекли у вивід. Плутати звичайну змінну із секретом — пряма діра: покласти токен у нешифровану змінну означає засвітити його в логах. Автотести читають ці значення з оточення процесу, тому один і той самий код працює на різних стендах, просто з іншими змінними.
Що таке process.env.CI і навіщо в CI вмикати ретраї, яких немає локально?
process.env.CI — конвенційна змінна, яку більшість CI-систем автоматично виставляє в true. Код тесту може на неї спиратися, щоб адаптувати поведінку до середовища без жодного if у самому тесті. Класичний приклад — ретраї у Playwright: локально їх тримають на нулі, бо ретрай маскує флак і ви хочете бачити падіння одразу, а в CI ставлять retries: 2, бо там флак дорожчий і разова мережева гикавка не повинна валити весь прогін. Так само за ознакою CI зазвичай забороняють .only, щоб випадково закомічений фокус-тест не «сховав» решту сюїта. Суть у тому, що конфіг читає оточення й сам вирішує, як поводитись, а тіло тесту лишається однаковим скрізь.
Що таке артефакт джоба і чому без нього дані зникають?
Ранер ефемерний: щойно джоба завершилась, її середовище знищується разом з усіма файлами, що на ньому лежали. Тому все, що ви хочете зберегти після прогону, треба явно оголосити артефактом (artifact) — інакше воно просто випарується. Артефактами зазвичай зберігають зібраний білд (щоб наступна джоба взяла готове, а не білдила вдруге), звіти про тести (HTML-звіт, JUnit XML — універсальний формат, який розуміє більшість CI) і докази падіння (скриншоти, відео, трейс Playwright, логи). Для AQA це різниця між «тест червоний, гадки не маю чому» і «відкрив трейс, побачив клік у порожнечу». Друга роль артефактів — передавати дані між джобами: build кладе бандл в артефакт, test у наступній стадії його забирає, попри те що вони бігли на різних ранерах.
«Тести впали, а трейсу немає». Що сталося і як цього уникнути?
Майже завжди причина одна: крок вивантаження артефактів стояв без умови «виконати завжди», тому після провалу тестів він просто не запустився. За замовчуванням провалений крок зупиняє джобу, і все, що йде після нього, пропускається — зокрема й upload trace. У результаті докази зникають рівно тоді, коли вони найпотрібніші. Лікування — позначити крок вивантаження умовою always() (у GitLab when: always), яка змушує його спрацювати незалежно від того, зелені тести чи червоні. Виглядає як «CI загубив файли», а насправді його просто не попросили зберегти їх при падінні. Це одна з найтиповіших помилок новачків, і на співбесіді її люблять перевіряти саме через сценарій «де взяти трейс фейлу, що відтворюється лише в CI».
Чим артефакт відрізняється від кешу?
Це різні сутності, і питання на співбесіді перевіряє, чи розумієте ви ефемерність ранера. Артефакт — це цінний вихід прогону: білд, JUnit-звіт, трейс падіння; його не можна втратити, бо це результат роботи, а іноді ще й вхід для наступної джоби. Кеш — це суто оптимізація швидкості: збережені між прогонами node_modules чи браузери, щоб не качати їх щоразу з нуля. Головна межа проходить по коректності: артефакт зберігають заради результату, а кеш зобов'язаний бути таким, що коректність прогону від нього ніколи не залежить. Кеш може бути порожнім на першому прогоні, протухлим чи скинутим — і пайплайн має відпрацювати однаково правильно, просто повільніше. Якщо порожній кеш ламає прогін, кеш перетворили на приховану залежність, і це помилка дизайну.
Як влаштований ключ кешу і чому коректність не сміє від нього залежати?
Кеш тримається на ключі (cache key), який зазвичай рахують як хеш lock-файлу, наприклад package-lock.json. Логіка та сама, що у фінгерпринтингу ассетів: доки залежності не змінились, хеш той самий і кеш підхоплюється; щойно ви оновили пакет, lock-файл змінюється, хеш стає інший, і кеш перебудовується з нуля. Правильно підібраний ключ гарантує, що кеш ніколи не віддасть застарілі залежності. А якщо ключ зроблено недбало — наприклад, він не включає хеш lock-файлу, — CI підтягне старі node_modules, і тести побіжать проти неактуальної версії бібліотеки. Саме тому діє жорстке правило: коректність прогону не може спиратися на кеш. Кеш лише пришвидшує, і пайплайн мусить давати той самий результат навіть при повністю холодному кеші.
Як передати зібраний білд із джоба build у джоб test?
Через артефакт плюс залежність, і слабка відповідь тут — «зібрати ще раз у тестовій джобі». Джоби ізольовані й нічим не діляться самі по собі: build і test біжать на різних ранерах, тому напряму передати файли не можна. Правильно так: джоба build виробляє бандл і оголошує його артефактом; джоба test оголошує залежність від build (директивою needs у GitHub Actions і GitLab CI) і на старті завантажує цей артефакт до себе. Залежність needs водночас гарантує порядок — test не стартує, доки build не завершився. Так стадії обмінюються результатами, не роблячи подвійну роботу, а зайвий ребілд у тестовій джобі — це і зайвий час, і ризик зібрати щось інше, ніж перевіряли.
Що таке needs, DAG і що робить провалена джоба з рештою?
Джоби рідко незалежні — вони утворюють граф залежностей (directed acyclic graph, DAG), де ребро задають директивою на кшталт needs. Граф дає дві типові фігури: fan-out, коли одна джоба породжує кілька паралельних (build → три тестові джоби), і fan-in, коли кілька збігаються в одну (три тести → deploy). За замовчуванням провалена джоба блокує все, що від неї залежить: якщо e2e-chromium червоний, deploy-staging навіть не стартує — у цьому й сенс quality gate. Статус усього пайплайна — агрегат: він червоний, якщо впала хоча б одна блокувальна джоба. Іноді потрібне протилежне — щоб некритична джоба (експериментальний браузер) не валила весь пайплайн: для цього є continue-on-error / allow_failure. Але вмикати це на основних тестах небезпечно — так можна тихо приховати реальну проблему.
Чому зелений пайплайн може брехати, або чому skipped ≠ passed?
Тому що джоба, яку відсіяла умова, не запускалась зовсім — вона не «пройшла», її просто не було. Умовний запуск (conditional execution) відсіює джоби за гілкою, за подією, за зміненими файлами (path filter) чи за результатом попередніх кроків. Корисно це для економії й коректної логіки релізу, але тут криється підступ: якщо критична тестова джоба тихо пропущена path-фільтром (наприклад, фільтр вважав, що UI не чіпали, хоча насправді чіпали), пайплайн буде зелений, а перевірки не було. Skipped рахується не як провал, але й не як успіх — це «перевірку не проводили». Тому, читаючи зелений пайплайн, треба дивитися не лише на колір, а й на те, що реально відпрацювало, а що умова вимкнула. Зелень із пропущеною ключовою джобою дає фальшиве відчуття безпеки.
Тест проходить локально, а в CI падає. Де шукатимеш?
Перше — не «перезапущу й помолюся», а систематична перевірка оточення ранера, бо логіка тесту цілком може бути правильною. Типові винуватці: headless замість headed (інший рендеринг і тайминги), слабше залізо hosted-ранера (тест не встигає там, де локально встигав), відсутня або інша змінна середовища (не той base URL, не підставився токен), брудний стан на self-hosted-ранері (недобиті процеси, зайняті порти), rate limit від паралельних воркерів у CI. Обов'язкова частина відповіді — «візьму трейс і скриншот з артефактів і подивлюся, що саме сталося», а не гадання. Тобто спершу дивишся докази падіння, потім звіряєш оточення CI з локальним, і лише впевнившись, що це не інфраструктура й не дані, заводиш баг на продукт. Сильний кандидат тут показує процес, а не одну гіпотезу.
Три кейси, які показують будову пайплайна не як термінологію, а як інструмент діагностики: таблиця рішень для читання червоного пайплайна, анотований YAML із правильним вивантаженням доказів і передачею білда між джобами, та розбір різниці між кешем і артефактом на конфізі. Скрізь — що дивитися й чому.
Кейс 1. Червоний пайплайн о другій ночі: таблиця рішень
Прийшов алерт: пайплайн червоний. Замість «перезапущу й подивлюся» корисно за симптомом одразу звужувати, де шукати — інфраструктура, дані чи справді продукт. Ось як читати найтиповіші картини.
| Симптом на дашборді | Що це найімовірніше | Перша дія |
|---|---|---|
Впала стадія build, до test не дійшло | Компіляція / залежності, не ваш тест | Дивитись лог build; тестів ще не було |
e2e-chromium червоний, e2e-firefox зелений | Специфіка одного браузера або флак | Взяти трейс саме з chromium з артефактів |
| Усі e2e червоні однаково | Оточення: не піднявся стенд, немає BASE_URL | Перевірити змінні й доступність стенду |
| Тести червоні, а трейсу в артефактах немає | Крок вивантаження без always() | Полагодити умову вивантаження, потім дебажити |
Джоба deploy-prod — skipped, пайплайн зелений | Умова за гілкою відсіяла, це не провал | Нормально для не-main; перевірити, чи очікувано |
Масові 502/503 одразу після релізу | Стенд ще прогрівається, не баг тесту | Зачекати/повторити, звірити стан середовища |
| Флак лише на self-hosted, локально зелено | Брудний ранер: процеси, порти, диск | Перевірити оточення ранера, не код тесту |
Головна дисципліна тут та сама, що й у розборі будь-якого фейлу: спершу відповісти, на чиєму боці проблема, і лише потім заводити дефект. «Впала стадія X, трейс указує на конкретний крок» коштує рівно стільки, скільки під ним доказів з артефактів.
Кейс 2. GitHub Actions: передати білд і не втратити трейс при падінні
Дві найчастіші помилки новачка в конфізі — зібрати білд повторно замість передати готовий, і залишити докази падіння без умови «завжди». Ось джоба, де обидві речі зроблено правильно.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
# ефемерний ранер знищиться — зберігаємо бандл артефактом
- uses: actions/upload-artifact@v4
with:
name: app-build
path: dist/
e2e:
needs: build # чекає build І отримує його артефакти
runs-on: ubuntu-latest
env:
BASE_URL: https://staging.example.com
steps:
- uses: actions/checkout@v4
- run: npm ci
# забираємо готовий бандл, а не білдимо вдруге
- uses: actions/download-artifact@v4
with:
name: app-build
path: dist/
- run: npx playwright test
# КЛЮЧОВЕ: if: always() — інакше після червоних тестів кроку не буде
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-trace
path: test-results/
retention-days: 7
Що дивитися й чому:
needs: buildробить дві речі одразу — задає порядок (е2е не стартує, доки білд не зелений) і дає доступ до артефактів білда. Прибратиneeds— і джоба або побіжить без бандла, або збиратиме його вдруге.download-artifactзамість повторногоnpm run build. Ребілд у тестовій джобі — це не лише зайвий час: ви ризикуєте протестувати не той артефакт, що поїде далі. Передавати треба саме те, що зібрали.if: always()на вивантаженні трейсів — не косметика. За замовчуванням проваленийplaywright testзупиняє джобу, і крок вивантаження просто пропускається. Самеalways()рятує докази тоді, коли тести червоні й доказ найпотрібніший.env: BASE_URLна рівні джоби. Playwright-конфіг прочитає його зprocess.envі піде на потрібний стенд; той самий код тесту так адаптується до будь-якого середовища без змін.
У Playwright-конфізі це замикається на ознаку CI:
// playwright.config.ts
export default defineConfig({
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry', // трейс з'явиться в test-results/ саме на ретраї
},
forbidOnly: !!process.env.CI, // випадковий .only не сховає решту сюїта
retries: process.env.CI ? 2 : 0, // локально бачимо флак одразу, у CI гасимо гикавку
});
Кейс 3. Кеш проти артефакту: одне пришвидшує, інше не можна втратити
На співбесіді ці дві сутності люблять розводити, а в конфізі їх легко переплутати. Порівняйте два блоки — обидва зберігають файли між кроками/прогонами, але з протилежною метою.
# КЕШ — лише оптимізація. Ключ прив'язаний до хешу lock-файлу.
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
# змінився lock-файл -> інший ключ -> кеш перебудується з нуля
# АРТЕФАКТ — цінний вихід, який має пережити ефемерний ранер.
- uses: actions/upload-artifact@v4
with:
name: junit-report
path: results/junit.xml
Що дивитися й чому:
- Ключ кешу мусить включати хеш lock-файлу. Доки залежності ті самі — ключ той самий, кеш підхоплюється; оновили пакет — lock змінився, ключ інший, кеш перебудувався. Без хешу в ключі CI підтягне застарілі
node_modules, і тест побіжить проти старої версії бібліотеки — класичний баг «кеш став залежністю коректності». - Коректність не сміє залежати від кешу. Видаліть увесь кеш — пайплайн зобов'язаний відпрацювати так само правильно, просто повільніше. Якщо холодний кеш ламає прогін, це помилка дизайну, а не «не пощастило».
- Артефакт — навпаки, втрачати не можна.
junit.xmlчитає CI, щоб показати впалі тести; трейс потрібен вам для дебагу. Це результат роботи, а не оптимізація. - Проста перевірка на співбесіді. «Чи можна втратити X без шкоди для коректності?» Так — це кеш. Ні — це артефакт. Одне речення, яке показує, що ви розумієте ефемерність ранера.
Ієрархія й виконання
- Можу назвати чотири рівні по спадній: pipeline → stage → job → step, і сказати, за що відповідає кожен.
- Знаю механізм за словами: стадії дають порядок (ворота, послідовні), джоби — паралелізм та ізоляцію (кожна на чистому ранері, стан не протікає), кроки — гранульованість і окремі логи.
- Пам'ятаю, що назви рівнів різняться між інструментами: у GitHub Actions немає окремого «stage», порядок там виражають через
needs.
Тригери
- Знаю основні тригери: push, pull/merge request, schedule (cron), manual (
workflow_dispatch), tag. - Розумію принцип «різний тригер — різний обсяг»: на PR smoke, на push у
mainповніше, cron — нічна регресія. - Пам'ятаю, що cron рахується в UTC, а не в локальному поясі, і знаю формат п'яти полів.
- Можу пояснити, чому PR із форка навмисне не отримує секретів і чому це не зламаний конфіг.
Ранери й оточення
- Знаю різницю hosted vs self-hosted: свіжа одноразова віртуалка проти персистентної власної машини.
- Можу назвати компроміс: чистота й нуль-обслуговування проти потужності, кастомного оточення й ризику протікання стану.
- Розумію, чому флак на self-hosted-ранері частіше означає брудне оточення (процеси, порти, диск), а на fresh hosted інші тайминги дають класику «локально проходить, у CI падає».
Змінні середовища й секрети
- Розумію, що конфіг (base URL, тестовий юзер, ознака CI) передають через змінні середовища на рівнях пайплайна / джоби / кроку (вужчий перекриває ширший), а не зашивають у код.
- Можу пояснити різницю між звичайною змінною та секретом і те, що CI маскує секрети в логах як
***. - Знаю конвенцію
process.env.CIі навіщо в CI вмикають ретраї таforbidOnly, яких немає локально.
Артефакти та кеш
- Розумію, що ранер ефемерний: без явного оголошення артефактом дані джоби зникають; зберігають білд, звіти (JUnit XML, HTML), докази падіння (трейс, скриншоти, відео).
- Можу пояснити, чому крок вивантаження доказів має стояти з
always()/when: always, інакше при червоних тестах його не буде. - Знаю різницю артефакт vs кеш (цінний вихід проти оптимізації, коректність від кешу залежати не сміє) і що ключ кешу — хеш lock-файлу, а недбалий ключ підтягує застарілі
node_modules. - Можу описати передачу білда з
buildуtestчерез артефакт +needs, а не повторний білд.
Статуси, залежності, умовний запуск
- Знаю життєвий цикл джоби: queued/pending → running → success/failed/cancelled/skipped, і що статус пайплайна — агрегат.
- Розумію DAG через
needs(fan-out і fan-in), що провал блокує залежні, і коли доречнийcontinue-on-error/allow_failure(небезпечний на основних тестах). - Тримаю в голові види умов (гілка, подія, path-фільтр,
if: failure()/always()) і головне: skipped ≠ passed — зелений пайплайн із пропущеною перевіркою бреше.
Яка вкладеність рівнів пайплайна від найширшого до найвужчого?
Питання
Які чотири рівні утворюють пайплайн і що в що вкладено?