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

    09 · Git і CI/CD

    Стратегія якості в пайплайні

    Зміст

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

    На співбесіді це тема сеньйорного рівня й високої ваги: junior пише тест, а senior/SDET/lead проєктує весь захисний контур — пояснює, чому e2e не бігають на кожен PR, що саме блокує merge, як не перетворити coverage на фікцію, і як за пів години накидати CI-стратегію для нового проєкту. Ця глава — канонічний виклад теми на сайті: інші глави («shift-left», «паралелізація») посилаються сюди. Водночас це глава-поглиблення: якщо проходиш розділ уперше й ще не тримав руками власний пайплайн, її можна пропустити й повернутися, коли з'явиться конкретна CI, яку треба впорядкувати.

    Порядок перевірок: від найдешевшого до найдорожчого

    Канонічна послідовність стадій — lint → typecheck → unit → integration → e2e. Це не випадковий ритуал, а один принцип: fail fast, тобто спершу найдешевші, найшвидші й найдетермінованіші перевірки, а найдорожчі — в кінці, за ними.

    Логіка проста, як воронка. lint (статичний аналіз стилю й очевидних помилок) відпрацьовує за секунди й не потребує навіть компіляції. typecheck (tsc --noEmit) ловить помилки типів статично, без запуску коду. Unit-тести швидкі й ізольовані. Інтеграційні вже піднімають базу чи сусідній сервіс — це хвилини. E2e піднімають увесь застосунок у браузері — це десятки хвилин і найбільша схильність до флаку (flakiness). Немає сенсу піднімати браузерну ферму на десять хвилин, якщо eslint упав на першій секунді через незакритий імпорт. Кожна дешева стадія — це фільтр, що відсіює сміття до того, як почнеться дорога стадія. По суті це той самий shift-left, тільки всередині одного пайплайна: посунь дешеву перевірку якомога раніше.

    fail

    pass

    fail

    pass

    fail

    pass

    fail

    pass

    fail

    pass

    Push у PR

    lint

    Стоп: червоний PR

    typecheck

    unit

    integration

    smoke e2e

    Гейт зелений: merge дозволено

    fail

    pass

    fail

    pass

    fail

    pass

    fail

    pass

    fail

    pass

    Push у PR

    lint

    Стоп: червоний PR

    typecheck

    unit

    integration

    smoke e2e

    Гейт зелений: merge дозволено

    Важливий нюанс, який відрізняє того, хто читав, від того, хто розуміє: lint, typecheck і unit настільки дешеві, що на практиці їх часто запускають паралельно, а не строго ланцюжком — фідбек однаково приходить за секунди, а розробник бачить одразу всі дрібні проблеми, а не по одній. Строгий порядок критичний саме для дорогого хвоста: не пускати integration і e2e, поки швидкі перевірки не зелені. Тобто «порядок» — це передусім про те, що коштовне стоїть за гейтом дешевого, а не про буквальну послідовність кожного кроку.

    Quality gate: що блокує merge

    Quality gate — це умова pass/fail, яку код мусить пройти, перш ніж рушити далі: злитися в основну гілку або поїхати на середовище. Це прикладна форма entry/exit-критеріїв, про які говорить теорія тест-менеджменту (розділ «Основи тестування»): нічого не входить у main, поки не виконано перелік умов.

    Технічно на злитті гейт реалізують через захист гілки (branch protection) і required status checks — набір джобів, які мусять бути зеленими, щоб кнопка merge стала активною (механіка PR і захисту гілок — «Pull Request і стратегії гілкування»). Типовий блокувальний набір: lint, typecheck, unit, integration, smoke-піднабір e2e, успішний білд, іноді скан залежностей (SCA — software composition analysis, напр. npm audit) плюс обов'язкове рев'ю й відсутність неактуального стану гілки.

    Хороший гейт має чотири властивості, і кожну варто перевіряти на рев'ю CI-конфіга:

    • Швидкий — вкладається в бюджет фідбеку PR (орієнтир — одиниці, максимум півтора десятка хвилин).
    • Детермінований — на тому самому коді дає той самий результат; флакі-гейт руйнує довіру швидше, ніж відсутність гейта.
    • Дієвий — з червоного видно, що саме зламалося й де взяти артефакт падіння, а не просто «job failed».
    • З власником — за кожен червоний є хтось, хто його чинить; гейт без відповідального гниє.

    Піраміда, стадії й розклад прогонів

    Піраміда тестування (канон — розділ «Автоматизація: стратегія») дає ключ до розкладки: багато дешевих unit унизу, менше інтеграційних усередині, жменя повільних крихких e2e вгорі. Проєкція на пайплайн пряма — що нижчий рівень, то частіше він бігає й то більше блокує; що вищий — то рідше він у ролі блокувального гейта.

    unit — багато, швидкі

    PR: блокує merge

    integration і API — середні

    e2e — мало, повільні, крихкі

    PR: тільки smoke критичного шляху

    Nightly і реліз: повний набір, не блокує merge

    unit — багато, швидкі

    PR: блокує merge

    integration і API — середні

    e2e — мало, повільні, крихкі

    PR: тільки smoke критичного шляху

    Nightly і реліз: повний набір, не блокує merge

    Звідси — три різні розклади прогонів під три різні цілі:

    ТригерЩо ганяємоБлокує?Ціль
    Кожен PRlint, typecheck, unit, integration/API, smoke-e2e критичного шляху, білд, SCAтакШвидкий детермінований фідбек як умова merge
    Nightly (cron)повний e2e-набір, крос-браузерна матриця, повна регресія, візуальні, перф-смоукні (але з власником)Спіймати те, що задовге чи широке для PR
    Перед релізомповна регресія на реліз-гілці, smoke на staging після деплою, security-скан, ручний exploratory-підпистак (рішення про реліз)Впевненість перед виходом у прод

    Піднабір e2e для PR зазвичай позначають тегом і виносять в окремий проєкт ранера:

    // playwright.config.ts — smoke як окремий проєкт для PR
    export default defineConfig({
      projects: [
        { name: 'smoke', grep: /@smoke/ },   // критичний шлях, блокує merge
        { name: 'full',  grepInvert: /@wip/ } // повний набір, у nightly
      ]
    });

    На PR тоді бігає --project=smoke, вночі — --project=full. Так один і той самий код тестів обслуговує два розклади без дублювання.

    Coverage як гейт: користь і пастки

    Покриття коду (code coverage) як gate — найспокусливіша й найнебезпечніша метрика в пайплайні. Користь реальна: гейт «не даємо покриттю падати» не пускає код без тестів і робить прогалину видимою на рев'ю, а не через пів року в проді.

    Пастки серйозніші за користь, і senior їх називає одразу. Покриття міряє виконання рядка, а не наявність асерту: тест, який проходить код і не перевіряє нічого, дає 100% покриття й нуль цінності. Щойно ти ставиш поріг метрикою — вона перестає бути доброю метрикою (закон Гудгарта): команда починає гнати цифру дешевими тестами на геттери й тендітними перевірками заради відсотка. До того ж «покриття» неоднорідне: 100% рядків (line coverage) ≠ 100% гілок (branch coverage) — умовний перехід можна виконати, жодного разу не зайшовши в його else (механіка — білоскринькові техніки в розділі «Тест-дизайн»).

    Що працює замість наївного «90% на весь проєкт»:

    • Покриття diff'а (patch/diff coverage) — вимога до доданих рядків, а не до всього репозиторію. Нова логіка приходить з тестами, а старий непокритий код не блокує кожен PR. Це вміють Codecov, Coveralls та подібні.
    • Храповик (ratchet) — поріг «не нижче поточного»: покриття не мусить рости до сакрального числа, але й не має падати.
    • Ніколи не вішай coverage-гейт на e2e — там цифра оманлива за побудовою.

    Коротко: низьке покриття — це достовірний сигнал проблеми; високе покриття не гарантує якості. Тому coverage — добрий індикатор і поганий бос.

    Коли e2e виносити з блокувальних

    E2e одночасно найповільніші, найкрихкіші й найзалежніші від середовища. Саме блокувальний гейт на нестабільному e2e-наборі найчастіше вбиває довіру до всього пайплайна: якщо «червоне» половину разів означає флак, а не баг, команда швидко навчається тиснути re-run або обходити гейт — і він перетворюється на театр. Гейт, який регулярно оверрайдять, — це вже не гейт.

    Сигнали, що пора виносити: високий рівень флаку (падіння, які проходять на повторі без зміни коду), часті ручні перезапуски й оверрайди, тривалість, що рве бюджет фідбеку PR. Рішення — не «викинути e2e», а розвести:

    • На PR лишити малий стабільний smoke критичного шляху (логін, оплата, ключовий сценарій) — його блокувальність виправдана.
    • Повний і флакуватий набір перенести в nightly як неблокувальний на merge, але з власником: є триажер, є SLA на розбір, є карантин для нестабільних (таксономія й політика флаку — канон у розділі «Автоматизація»).

    Ключове застереження від двох крайнощів. Чесний неблокувальний сигнал кращий за фейковий блокувальний — але «неблокувальний» не сміє означати «нічий»: червоний nightly без триажера гниє так само, як флакі-гейт. І не можна тихо зняти весь e2e-захист із блокувальних: мінімальний smoke на PR лишається завжди, а пониження прозоре й з планом стабілізації.

    Метрики здоров'я пайплайна

    Пайплайн — теж продукт, і в нього є показники здоров'я. Дивляться на дві групи.

    Метрики самого пайплайна:

    • Green rate основної гілки — яку частку часу main зелений. Хронічно червоний main означає, що гейт не тримає.
    • Час фідбеку — тривалість PR-пайплайна, p50 і p95. Це «час до сигналу»; коли він росте, зростає й спокуса обходити.
    • Time to green / MTTR червоного main — скільки в середньому основна гілка лишається зламаною.
    • Рівень флаку — частка падінь, що проходять на повторі без зміни коду.
    • Частота ручних re-run і оверрайдів гейта — прямий індикатор недовіри.
    • Тривалість сьюта й вартість — хвилини CI, ліміти, ціна ранерів (економіка — «Паралелізація і швидкість пайплайна»).

    Метрики доставки (оглядово, рівень команди) — індустріальний набір DORA: частота деплоїв (deployment frequency), час від коміту до прода (lead time for changes), частка релізів, що спричинили збій (change failure rate), і час відновлення після невдалого деплою. Вони не про тести напряму, але саме на них зрештою впливає якість пайплайна. Застереження те саме: метрику, поставлену ціллю, починають накручувати — тож дивись на них у зв'язці, а не поодинці.

    Кейс: CI-стратегія з нуля

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

    1. Інвентаризація. Які тести вже є, на яких рівнях, скільки бігають, який рівень флаку. Яка модель гілкування — trunk-based чи довгі гілки (див. «Pull Request і стратегії гілкування»). Як часто деплоїмо. Де ризик найдорожчий — тут це checkout і оплата.
    2. Бюджет фідбеку PR. Фіксуємо ціль, наприклад «не довше 10–12 хв», і саме під неї підбираємо, що влазить: lint, typecheck, unit, integration/API і smoke критичного шляху. Паралелізуємо й кешуємо, щоб влізти (механіка — глава про паралелізацію).
    3. Гейти на merge. Required checks: швидкі перевірки + smoke + білд + рев'ю. Кожен — швидкий, детермінований, з власником.
    4. Розкладка важкого. Повна регресія й крос-браузерна матриця e2e — у nightly з триажером. Візуальні й перф-смоук — туди ж.
    5. Політика coverage. Diff-coverage з храповиком, без вимоги «100% на весь репозиторій».
    6. Реліз-контур. Повна регресія на реліз-гілці + smoke на staging після деплою (середовища й деплой — «Середовища, деплой і релізи»).
    7. Власність і метрики. Хто чинить червоний main, хто розбирає nightly, який SLA на флак. Вішаємо приладову панель: green rate, час фідбеку, флак (спостережуваність — «Логи, моніторинг і спостережуваність для QA»).
    8. Ітерація. Гейти не висічені в камені: у міру стабілізації e2e частину переносимо з nightly в блокувальні на PR і навпаки.

    Головна думка кейсу: стратегія — це компроміс між швидкістю фідбеку й повнотою захисту, розкладений у часі (PR / nightly / реліз) і в довірі (блокує / сигналить).

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

    • Виглядає як надійний гейт, а насправді театр. Блокувальний e2e-набір з 20% флаку: команда навчилася тиснути re-run, і «зелене» вже нічого не гарантує. Гейт, який обходять, не захищає — його треба або стабілізувати, або зробити чесно неблокувальним з власником.
    • Виглядає як турбота про якість, а насправді гальмо. Повна регресія й уся e2e-матриця на кожен PR: фідбек 40 хвилин, розробники накопичують зміни й обходять процес. Місце важкого — nightly, а не PR.
    • Виглядає як 90% покриття, а насправді 0% перевірок. Тести виконують код без асертів заради відсотка. Покриття міряє виконання, не перевірку; diff-coverage і рев'ю тестів чесніші за поріг на весь проєкт.
    • Виглядає як «прибрали флак», а насправді прибрали сигнал. E2e винесли в nightly й нікому не віддали — червоне вночі ніхто не дивиться, реальні регресії течуть у прод. Неблокувальний ≠ нічий.
    • Виглядає як зелений білд, а насправді нічого не перевірено. Пайплайн зелений, бо стадія тестів мовчки пропускається (|| true, continue-on-error, порожній матч тегів). Зелене має означати «перевірено й пройшло», а не «не впало».

    Підсумок

    • Порядок стадій lint → typecheck → unit → integration → e2e — це fail fast: дешеве й детерміноване спереду, дороге e2e за гейтом дешевого.
    • Quality gate — умова pass/fail для merge; хороший гейт швидкий, детермінований, дієвий і з власником. Флакі-гейт гірший за відсутність гейта.
    • Розкладай прогони за пірамідою й ціллю: unit/integration і smoke блокують кожен PR; повна регресія й e2e-матриця — nightly з власником; повний контур — перед релізом.
    • Coverage — індикатор, а не бос: diff-coverage з храповиком замість сакральних 100%; покриття без асертів — фікція.
    • Гейт, який регулярно оверрайдять, — не гейт: нестабільний e2e виноси в неблокувальний nightly з триажером, лишаючи на PR стабільний smoke.

    Що питають на співбесіді

    • «У якому порядку мають іти перевірки в CI і чому?» Дивляться, чи назвеш принцип fail fast (дешеве й швидке спереду), а не лише перелік стадій; бонус — що дрібні перевірки паралеляться, а порядок критичний для дорогого хвоста.
    • «Що у вас блокує merge?» Перевіряють, чи розумієш branch protection і required checks і чи відрізняєш блокувальний гейт від інформативного сигналу.
    • «Чому e2e не ганяють на кожен PR? Коли ви їх виносите з блокувальних?» Хочуть почути про швидкість, флак і довіру до гейта, а не «бо повільно»; сильна відповідь — про smoke на PR + повний набір у nightly з власником.
    • «Ставити coverage як gate — добре чи погано?» Слухають, чи назвеш пастки (виконання ≠ перевірка, Гудгарт, line vs branch) і зрілу альтернативу — diff-coverage з храповиком.
    • «Спроєктуй CI-стратегію з нуля для сервісу X.» Оцінюють структуру міркування: інвентаризація → бюджет фідбеку → гейти → розкладка важкого → coverage → реліз → власність і метрики → ітерація. Кандидат, який одразу сипле інструментами, програє тому, хто ставить питання про ризик і швидкість.
    • «Які метрики здоров'я пайплайна дивитесь?» Чекають green rate, час фідбеку, рівень флаку, MTTR червоного main; згадка DORA — плюс, але з розумінням, що це рівень доставки, а не тестів.

    Джерела