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

    09 · Git і CI/CD

    Автотести в CI: стабільність і дебаг

    Зміст

    git push, чекаєш зелену галочку — а замість неї червоний і лог на тисячу рядків. Локально той самий тест проходив десять разів поспіль. Перший рефлекс — «CI кривий, перезапущу». За п'ять хвилин джоб зелений, PR злитий, а через тиждень той самий тест валить реліз-кандидат. Ця глава — про те, як не потрапляти в цей цикл: чому CI бачить твій тест інакше, ніж твоя машина, і як дебажити прогін, за яким ти не спостерігав наживо.

    Це кут CI до великої теми (flaky tests). Повна таксономія причин нестабільності, і командна політика — канон у розділі про стратегію автоматизації; тут ми не переказуємо його, а розбираємо саме CI-дельту: чим середовище агента відрізняється від локального, як читати артефакти фейлу й де проходить межа між «ретраїти» і «знайти причину». Якщо ти вже читав канон — це його CI-продовження.

    Чому «локально зелено, у CI червоно»

    Фраза «на моїй машині працює» — не доказ, а опис різниці двох середовищ. Автотест — це програма, яка робить приховані припущення про світ навколо: що екран певного розміру, що машина швидка, що зараз твоя таймзона, що шрифт відмальовується так само. Локально всі ці припущення випадково істинні. CI-агент їх не гарантує — і кожне хибне припущення стає падінням.

    АспектЛокальноCI-агент
    Режим браузераheaded, ти бачиш вікноheadless за замовчуванням
    Ресурситвої 8+ ядер, нічого поручкілька спільних ядер, поруч інші джоби
    Таймзона / локальтвоя (наприклад, Europe/Kyiv)часто UTC і C/POSIX
    Шрифтиповний набір ОСмінімальний, у контейнері часто майже порожній
    Станнакопичений кеш, залогінена сесіячистий, з нуля щоразу
    Данітвоя локальна базаспільний стенд, паралельні тести поруч

    Жодна з цих відмінностей не є багом CI. Це чесні дефолти чистого середовища — а падає тест, який мовчки розраховував на твої зручні умови.

    headless і чому він інший

    За замовчуванням у CI браузер запускається в headless-режимі — без графічного вікна. Історично headless-Chrome був окремою реалізацією і поводився помітно інакше за headed; сучасний Chromium звів обидва режими до спільної реалізації, але відмінності лишаються: інший дефолтний розмір , відсутній апаратний GPU-рендер, іноді інша поведінка діалогів і фокуса. Тест, який клікає по кнопці поза першим екраном на твоєму великому моніторі, у headless із дефолтною областю перегляду може не доскролити до неї.

    На Linux-агентах ще й немає дисплея взагалі. Браузери Playwright у headless працюють без нього, але щойно тесту потрібен headed-режим (наприклад, розширення браузера) — потрібен віртуальний дисплей на кшталт xvfb. Це типова причина падіння «зелений локально, а в CI одразу помирає на старті браузера».

    Ресурси агента → інші таймінги

    Це головна CI-дельта . Спільний агент має менше ядер, і поруч крутяться інші джоби. Те, що на твоїй машині відпрацьовує за 50 мілісекунд, під навантаженням займає 500. Будь-яке очікування, зашите в число замість умови, стає лотереєю: waitForTimeout(1000) вистачало локально й не вистачає під навантаженням.

    Тому (hard wait) — головний ворог стабільності саме в CI. Локально повільна машина маскується запасом, у CI запасу немає. Лікування — не збільшувати паузу, а чекати на конкретну умову: web-first assertions Playwright самі ретраяться до появи стану, а не до спливання таймера.

    // Крихко: локально встигає, у CI під навантаженням — ні
    await page.waitForTimeout(1000);
    await expect(page.getByRole('alert')).toBeVisible();
    
    // Стійко: очікування прив'язане до умови, а не до годинника
    await expect(page.getByRole('alert')).toBeVisible({ timeout: 10_000 });

    Механіку очікувань браузера детально розбирає глава «Практичні сценарії AQA: флак і синхронізація», а забутий await над асинхронним викликом як окреме джерело гонок — розділ про JavaScript/TypeScript.

    Окремий клас CI-специфічних падінь — час і локаль. Тест, що відформатовану дату 18.07.2026 або суму 1 234,50, зеленіє у твоїй локалі й червоніє в UTC/C, де форматування інше. А тест, який рахує «сьогодні + 1 день», може падати лише вночі, коли на агенті в UTC уже завтра. І шрифти: у мінімальному потрібного шрифту немає, текст рендериться , займає іншу ширину — ламається верстка або -порівняння. Про шрифти в контейнерах докладніше — «Docker і тестові середовища», про різницю оточень і конфігурацію під них — «Секрети та конфігурація в CI».

    Артефакти фейлу: як побачити те, чого ти не бачив

    Локально ти дебажиш очима: бачиш вікно, ставиш , читаєш . У CI прогін уже стався й зник — лишився тільки лог. Тому єдиний спосіб не дебажити наосліп — щоб тест сам зберіг докази свого падіння. Це артефакти джоба (job artifacts): файли, які чіпляє до прогону, і які можна завантажити після нього.

    Ключові артефакти для дебагу автотестів:

    • Скріншот на момент падіння — найдешевший доказ. Часто одразу видно: модалка не закрилась, банер cookie перекрив кнопку, сторінка взагалі 500.
    • Відео прогону — показує, як тест дійшов до фейлу: що клікнув, що не встиг завантажитись.
    • (trace) — найпотужніше. Playwright Trace Viewer дає покроковий таймлайн з DOM-снапшотами до й після кожної дії, мережею, консоллю й логами. По суті це «запис перемотуваного прогону»: відкриваєш і бачиш саме той стан сторінки, на якому тест спіткнувся, без повторного запуску.

    Головне правило: артефакти треба ввімкнути до того, як стало червоно. Задавати їх на весь прогін дорого (місце, час), тому їх вмикають на падіннях і на :

    // playwright.config.ts
    export default defineConfig({
      retries: process.env.CI ? 2 : 0,
      reporter: [
        ['list'],
        ['junit', { outputFile: 'results.xml' }],
        ['html', { open: 'never' }],
      ],
      use: {
        trace: 'on-first-retry',      // трейс пишеться на першому ретраї
        screenshot: 'only-on-failure',
        video: 'retain-on-failure',
      },
    });

    Далі ці файли треба віддати пайплайну як артефакти джоба (крок upload-artifact у GitHub Actions, artifacts:paths у GitLab CI) — інакше вони помруть разом з ефемерним агентом. Механіку артефактів на рівні пайплайна розбирає «Будова пайплайна: стадії, джоби, тригери, артефакти».

    CodeceptJS дає те саме іншими словами: плагін screenshotOnFail зберігає скріншот на кожному падінні, а під Playwright-хелпером доступні трейси й відео. Ідея одна для будь-якого стека: red-джоб без артефактів — це дебаг за описом болю без знімка.

    JUnit XML: спільна мова тесту й пайплайна

    Твій знає, що впало. CI-система — ні: для неї джоб це просто процес, що завершився кодом 0 або 1. Червона галочка й лог є, а структурованого «які саме тести впали, скільки часу йшли, з якою помилкою» — нема. Щоб пайплайн це зрозумів, потрібен спільний формат обміну. Цю роль виконує JUnit XML.

    Це простий XML родом з екосистеми JUnit (сам формат виріс із репортера Ant): корінь testsuites, усередині testsuite, у ньому testcase з атрибутами name, time й вкладеними failure/error/skipped. Єдиної офіційної специфікації в нього немає — формат виріс як домовленість, тому в дрібницях інструменти розходяться. Хто саме його читає, кожна CI-система документує сама про себе: у Jenkins є крок junit від JUnit-плагіна «for aggregating test reports», GitLab підтягує звіт секцією artifacts:reports:junit, Azure Pipelines — таском PublishTestResults@2 з testResultsFormat: 'JUnit'. Наскільки широко формат читають решта систем, джерела глави не кажуть — це перевіряють у доці своєї CI.

    Цінність у тому, що це один формат для різних читачів. Ранер пише його раз — а CI дістає з нього і зручний список впалих у UI джоба, і анотації прямо в дифі PR, і тренди «цей тест мерехтить третій тиждень».

    пише

    читає

    Тест-ранер
    Playwright / CodeceptJS

    results.xml
    JUnit XML

    CI-система

    Список впалих
    у UI джоба

    Анотації в PR

    Тренди
    й історія

    пише

    читає

    Тест-ранер
    Playwright / CodeceptJS

    results.xml
    JUnit XML

    CI-система

    Список впалих
    у UI джоба

    Анотації в PR

    Тренди
    й історія

    Практичний наслідок для AQA: якщо в UI пайплайна не видно, які тести впали, а видно лише червоний лог — майже завжди причина в тому, що ранер не згенерував JUnit XML або джоб його не підхопив. Це перше, що варто налаштувати на новому проєкті: без нього кожен фейл доводиться вичитувати руками з логів.

    Перезапустити чи копати: рішення при червоному джобі

    Кнопка «Re-run job» — найспокусливіша й найнебезпечніша в CI. Ретрай — це діагностичний крок, а не полагодження. Він відповідає рівно на одне питання: тест падає стабільно чи мерехтить?

    • Впало вдруге так само — це : баг продукту або баг тесту. Копай.
    • Стало зелено — тест флакі. І тут головна пастка: «зелено після ретраю» відчувається як «полагоджено», хоча ти лише сховав нестабільність. Наступного разу вона стрельне в найгірший момент — на реліз-кандидаті.

    Стабільно щоразу

    Раз падає, раз ні

    Так

    Ні

    Джоб червоний

    Падає стабільно
    чи мерехтить?

    Регресія:
    баг продукту або тесту

    Флак: копай причину,
    не ретрай наосліп

    Відкрити trace / скріншот
    з артефактів джоба

    Відтворюється
    локально?

    Дебаж як звичайний баг

    Дельта середовища:
    headless, таймінги, час, локаль, дані

    Стабільно щоразу

    Раз падає, раз ні

    Так

    Ні

    Джоб червоний

    Падає стабільно
    чи мерехтить?

    Регресія:
    баг продукту або тесту

    Флак: копай причину,
    не ретрай наосліп

    Відкрити trace / скріншот
    з артефактів джоба

    Відтворюється
    локально?

    Дебаж як звичайний баг

    Дельта середовища:
    headless, таймінги, час, локаль, дані

    Окрема тема — автоматичні ретраї (retries у конфізі). Вони легітимні як пом'якшення: реальна мережа й реальні стенди інколи моргають, і глобальний прогін не має падати через один випадковий збій. Але ретрай, який стабільно ховає той самий тест, — це не стабілізація, а маскування. Різниця в намірі: ретрай, після якого ти йдеш дивитись трейс невдалої спроби, — інструмент; ретрай, після якого ти забуваєш про тест, — борг, що росте. Тому трейс і варто писати саме on-first-retry: якщо тест урятувався ретраєм, у тебе лишається доказ, чому перша спроба впала.

    Коли фейл не відтворюється локально навіть з тим самим кодом — повертайся до таблиці дельти середовища. А коли треба знайти конкретний , після якого тест позеленів у мерехтливий, допомагає git bisect — але лише якщо падіння монотонне; на флакі-тесті бінарний пошук збреше.

    Хто чинить червоний пайплайн

    Технічне питання «як стабілізувати» швидко впирається в організаційне: хто відповідає за червоне. Відповідь, що тримає команду живою, проста — той, чия зміна зробила пайплайн червоним, і робить це першим пріоритетом. Це принцип «зламав — лагодиш» (you break it, you fix it), він же «стоп-лінія» (stop-the-line): поки спільна червона, ніхто не будує зверху, бо новий код лягає на зламаний фундамент і невідомо, чиє падіння тепер бачиш.

    Практична розв'язка залежить від того, що саме червоне:

    • Твій PR червоний — це твоя зона. Не зливай «поверх» червоного й не проси вимкнути перевірку. branch protection з обов'язковими статусами саме для цього й існує — розбирає «Pull Request і стратегії гілкування».
    • Спільна main червона через свіжий — на trunk-based дефолт «спершу повернути зелене, розслідувати потім»: швидкий git revert знімає блок з усієї команди за хвилини, а автор спокійно шукає причину у своїй гілці.
    • Червоне через давній флакі-тест, не пов'язаний з твоєю зміною — його не можна лишати в блокувальних: він знецінює всю сигналізацію (команда звикає, що червоне — це «нормально», і пропускає справжню регресію). Такий тест ізолюють у карантин і чинять окремо. Що ганяти в блокувальних, а що ні, і як розкладати прогони — «Стратегія якості в пайплайні».

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

    Типові помилки

    Виглядає як «CI кривий», а насправді тест залежить від таймінгу. «Локально ж працює» — і винною призначають інфраструктуру. Насправді на швидкій машині приховане припущення «встигне за секунду» випадково істинне, а на завантаженому агенті — ні. Кривий не CI, .

    Виглядає як «полагодив», а насправді сховав. Перезапустив, стало зелено, закрив задачу. Нестабільність нікуди не поділась — ти лише відклав її на гірший момент. Зелено після ретраю без розбору причини — не результат, а відстрочка.

    Виглядає як регресія UI, а насправді відсутній шрифт. Скріншот-діф червоний, усі шукають зміну в стилях. А в контейнері просто немає потрібного шрифту — текст ліг фолбеком іншої ширини. Дивись не лише на діф, а й на оточення, де він знявся.

    Виглядає як «локально зелено — тест правильний», а насправді локальна машина маскує баг. Твій швидкий CPU, тепла сесія й зручна таймзона ховають гонку, яку CI чесно показує. Зелене локально доводить, що тест проходить у твоїх умовах, а не що він коректний.

    Виглядає як «джоб червоний без причини», а насправді немає артефактів. Дивишся в лог і не розумієш, що сталось, бо трейс і скріншот не ввімкнені або не завантажені як артефакт. Дебаг наосліп — не властивість CI, а наслідок неналаштованих доказів.

    Підсумок

    • CI — інше середовище, а не «та сама машина в хмарі». «У мене працює» не доказ: дельта — це ресурси й таймінги, headless, час і локаль, шрифти, чистий стан і спільні дані.
    • Головна CI-дельта флаку — таймінги: менше ядер і сусіди по агенту роблять жорсткі очікування лотереєю. Чекай на умову, а не на годинник.
    • Артефакти (трейс, скріншот, відео, JUnit XML) — твої очі в CI. Увімкни їх до падіння, інакше дебажиш за описом болю без знімка.
    • Ретрай джоба — діагностика «стабільно чи мерехтить», а не лікування. Ретрай легітимний як пом'якшення й шкідливий як маскування.
    • Червоний спільний пайплайн — стоп-лінія: чинить той, чия зміна зламала; на trunk — швидкий revert, розслідування потім. Терпіти червоне = вбивати довіру до зеленого.

    Можливі питання

    «Тест проходить локально, а в CI падає. Твої дії?» Найчастіше питання теми. Інтерв'юер перевіряє не знання команд, а метод: чи не кинешся ретраїти, чи підеш системно — відтворити (ретрай як діагностика), відкрити артефакти (трейс/скріншот), звірити дельту середовища (headless, ресурси, час, локаль, дані). Слабка відповідь — «перезапущу, зазвичай допомагає»; сильна — «спершу з'ясую, стабільно чи флакі, і подивлюсь трейс».

    «Що таке флакі-тест і як з ним боротися в CI?» Тут дивляться, чи відрізняєш маскування від лікування: ретрай і паузи ховають причину, а розв'язання — прибрати недетермінованість (очікування на умову, ізольовані дані, детермінований час). Згадка карантину й того, що флак у блокувальних убиває довіру до , — сильний сигнал.

    «Навіщо в CI трейси/відео/скріншоти?» Очікувана думка: у CI немає очей, прогін уже зник, тому тест мусить сам зберегти докази; артефакти вмикають на падіннях і ретраях, щоб не платити за них на кожному прогоні.

    «Що таке JUnit XML і навіщо він у пайплайні?» Перевіряють розуміння, що ранер і CI говорять різними мовами, а JUnit XML — спільний формат обміну: один файл дає список впалих, анотації в PR і тренди.

    «Пайплайн червоний — хто це чинить?» Питання про культуру, не про техніку. Правильний вектор: чинить автор зміни, першим пріоритетом; на спільній гілці — стоп-лінія й швидкий revert; давній флак не тримають у блокувальних.

    Джерела

    Чому «локально зелено, у CI червоно»

    • Playwright — Continuous Integration — дефолти чистого агента: браузери запускаються в headless, на Linux-агентах headed вимагає Xvfb; кількість ставиться за виявленою кількістю ядер, і перевищення дає непотрібні й падіння, тому в CI радять workers: 1 заради стабільності й відтворюваності.
    • GitHub Docs — Understanding GitHub Actions — кожен прогін іде на свіжій, щойно розгорнутій віртуальній машині, і один ранер виконує одну джобу за раз: накопиченого локального стану тут немає за побудовою.
    • Playwright — Visual comparisons — рендеринг залежить від ОС, її версії, налаштувань, заліза й headless-режиму, звідси припис ганяти тести в тому самому середовищі, де знято .

    Артефакти фейлу: як побачити те, чого ти не бачив

    • Playwright — Trace Viewer — трейс як GUI-інструмент перегляду записаного прогону після його завершення: саме він заміняє «подивитися очима».
    • GitHub Docs — Understanding GitHub Actions — чому без вивантаження доказів немає: кожна джоба біжить у власному ранері, який після завершення зникає.
    • Playwright — Continuous Integration — вивантаження трейсів і звітів як артефактів прогону, разом із DEBUG=pw:browser для помилок запуску браузера.

    JUnit XML: спільна мова тесту й пайплайна

    • Playwright — Test reporters — репортер junit «produces a JUnit-style xml report»; щоб писати у файл, потрібні PLAYWRIGHT_JUNIT_OUTPUT_NAME і --reporter=junit.
    • Playwright — Continuous Integration — приклад споживання формату CI-системою: таск Azure Pipelines PublishTestResults@2 із testResultsFormat: 'JUnit' і testResultsFiles: 'e2e-junit-results.xml'.
    • Jenkins Pipeline Documentation — крок junit від JUnit-плагіна «for aggregating test reports» у declarative-пайплайні.
    • GitLab Docs — CI/CD YAML syntax reference — ключ artifacts:reports:junit, яким GitLab підтягує звіт джоби.

    Перезапустити чи копати: рішення при червоному джобі

    • Playwright — Continuous Integration — чому «перезапущу на тому самому агенті» не працює: перевищення воркерів над кількістю ядер — документована причина таймаутів і падінь, а не випадковість.
    • Git Reference — git-bisect — бінарний пошук працює на монотонному падінні: із випадковим результатом веде пошук не в ту половину.

    Хто чинить червоний пайплайн

    • Martin Fowler — Continuous Integration — «Continuous Integration can only work if the mainline is kept in a healthy state»; пріоритет дослівно словами Кента Бека — «nobody has a higher priority task than fixing the build», а найкращий перший крок — відкотити винний коміт із магістралі, щоб решта команди працювала далі.
    • GitHub Docs — About protected branches — бік гейтів: обовʼязкова перевірка проходить у стані successful, skipped, or neutral, а без обовʼязкових перевірок злити можна будь-коли, що збільшує ймовірність несумісних змін.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.