vyvchy
    Теми розділу

    09 · Git і CI/CD

    Будова пайплайна: стадії, джоби, тригери, артефакти

    Зміст

    У попередній главі «CI/CD: інтеграція, доставка, розгортання» ми розібрали, навіщо існує CI: щоб інтеграція не перетворювалась на пекло, а фідбек приходив за хвилини, а не за тижні. Тепер — з чого CI фізично складається. Пайплайн (pipeline) — це не магія, а звичайний конвеєр: набір кроків, які автоматично виконуються на певну подію в репозиторії.

    Для AQA будова пайплайна — це не факультатив, а щоденне середовище проживання. Ваші автотести не запускаються «десь у хмарі» — вони живуть усередині конкретного джоба (job), на конкретному ранері (runner), зі своїми змінними середовища. Коли о другій ночі пайплайн червоний, від вас чекають не «перезапустити й молитися», а конкретну відповідь: яка стадія впала, ваш це тест чи інфраструктура, де взяти трейс падіння і чому джоба взагалі запустилась. Усе це — про будову пайплайна. І майже кожна співбесіда на AQA/SDET має блок «розкажіть, як влаштований ваш CI»: тут відсіюють тих, хто натискав зелену кнопку, від тих, хто розуміє конвеєр.

    Ієрархія: pipeline → stage → job → step

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

    • Пайплайн (pipeline) — увесь конвеєр цілком, який запускається на одну подію (наприклад, на push). Один запуск пайплайна = одна спроба провести зміни крізь усі перевірки.
    • Стадія (stage) — впорядкована фаза конвеєра: спершу build, потім test, потім deploy. Стадії виконуються послідовно: наступна не почнеться, доки попередня не завершилась успішно. Стадія — це ворота (gate).
    • Джоба (job) — одиниця роботи, яка виконується на окремому чистому ранері. Джоби всередині однієї стадії за замовчуванням біжать паралельно й незалежно. Джоба — це одиниця планування та ізоляції: у неї свій процес, своя файлова система, свій набір змінних.
    • Крок (step) — одна команда всередині джоба: «викачати код», «встановити залежності», «запустити тести», «завантажити артефакти». Кроки виконуються послідовно згори вниз; перший провалений крок зазвичай зупиняє джобу.

    Pipeline

    Stage build

    Stage test

    Stage deploy

    Job e2e-chromium

    Job e2e-firefox

    Step checkout

    Step npm ci

    Step run tests

    Step upload trace

    Pipeline

    Stage build

    Stage test

    Stage deploy

    Job e2e-chromium

    Job e2e-firefox

    Step checkout

    Step npm ci

    Step run tests

    Step upload trace

    Чому саме така ієрархія, а не плаский список команд? Три причини, кожна — про реальну потребу. Порядок: стадії гарантують, що ви не задеплоїте білд, який не зібрався й не пройшов тести. Паралелізм: джоби в одній стадії біжать одночасно, тож 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 (кілька збігаються в одну).

    лише гілка main

    build

    e2e-chromium

    e2e-firefox

    api-tests

    deploy-staging

    deploy-prod

    лише гілка main

    build

    e2e-chromium

    e2e-firefox

    api-tests

    deploy-staging

    deploy-prod

    За замовчуванням провалена джоба блокує все, що від неї залежить: якщо e2e-chromium червоний, deploy-staging навіть не стартує. Це і є сенс quality gate. Іноді потрібне протилежне — щоб некритична джоба не валила весь пайплайн (наприклад, експериментальний браузер): для цього є continue-on-error / allow_failure, які кажуть «дай їй впасти, але не блокуй решту». Користуватися цим на основних тестах — спосіб приховати проблему, тож обережно.

    Умовний запуск

    Не кожна джоба має бігти щоразу. Умовний запуск (conditional execution) — це правила «запусти цю джобу/крок, тільки якщо…». Навіщо: економія часу й грошей, і коректна логіка релізу.

    Типові умови:

    • за гілкою — деплой у prod лише з main, а не з кожної фіч-гілки;
    • за подією — реліз-джоба лише на тег;
    • за зміненими файлами (path filter) — ганяти візуальні тести, лише якщо чіпали UI, а не бекенд;
    • за результатом попередніх кроків — виконати крок, тільки якщо щось упало (if: failure()), або — навпаки — завжди (if: always(), у GitLab when: 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 ранери — коли що?» — чекають на компроміс: чистота й нуль-обслуговування проти потужності, кастомного оточення й ризику протікання стану.

    Джерела