Стратегія якості в пайплайні
Зміст
У тебе вже є автотести й є пайплайн (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, тільки всередині одного пайплайна: посунь дешеву перевірку якомога раніше.
Важливий нюанс, який відрізняє того, хто читав, від того, хто розуміє: 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 вгорі. Проєкція на пайплайн пряма — що нижчий рівень, то частіше він бігає й то більше блокує; що вищий — то рідше він у ролі блокувального гейта.
Звідси — три різні розклади прогонів під три різні цілі:
| Тригер | Що ганяємо | Блокує? | Ціль |
|---|---|---|---|
| Кожен PR | lint, 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-стратегії — з чого почнеш?» Сильна відповідь — не список інструментів, а порядок міркування. Візьмімо інтернет-магазин.
- Інвентаризація. Які тести вже є, на яких рівнях, скільки бігають, який рівень флаку. Яка модель гілкування — trunk-based чи довгі гілки (див. «Pull Request і стратегії гілкування»). Як часто деплоїмо. Де ризик найдорожчий — тут це checkout і оплата.
- Бюджет фідбеку PR. Фіксуємо ціль, наприклад «не довше 10–12 хв», і саме під неї підбираємо, що влазить: lint, typecheck, unit, integration/API і smoke критичного шляху. Паралелізуємо й кешуємо, щоб влізти (механіка — глава про паралелізацію).
- Гейти на merge. Required checks: швидкі перевірки + smoke + білд + рев'ю. Кожен — швидкий, детермінований, з власником.
- Розкладка важкого. Повна регресія й крос-браузерна матриця e2e — у nightly з триажером. Візуальні й перф-смоук — туди ж.
- Політика coverage. Diff-coverage з храповиком, без вимоги «100% на весь репозиторій».
- Реліз-контур. Повна регресія на реліз-гілці + smoke на staging після деплою (середовища й деплой — «Середовища, деплой і релізи»).
- Власність і метрики. Хто чинить червоний
main, хто розбирає nightly, який SLA на флак. Вішаємо приладову панель: green rate, час фідбеку, флак (спостережуваність — «Логи, моніторинг і спостережуваність для QA»). - Ітерація. Гейти не висічені в камені: у міру стабілізації 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 — плюс, але з розумінням, що це рівень доставки, а не тестів.
Джерела
- ISTQB Certified Tester Foundation Level (CTFL) 4.0 Syllabus — підрозділ 2.1.4 «DevOps and Testing» (місце автоматизованих перевірок у CI/CD-конвеєрі) і розділ 5 (entry/exit-критерії як основа quality gates).
- Martin Fowler — Continuous Integration — канонічне пояснення практики CI, self-testing build і швидкого фідбеку.
- Martin Fowler — The Practical Test Pyramid — рівні тестів, їх вартість і швидкість; основа для мапінгу на стадії пайплайна.
- Google Testing Blog — Code Coverage Best Practices — чому покриття корисне як сигнал і шкідливе як ціль.
- GitHub Docs — About protected branches — required status checks і механіка того, що фізично блокує merge.
У якому порядку мають виконуватись перевірки в CI і чому саме так?
Порядок майже завжди сталий — спершу lint, далі typecheck, тоді unit, потім integration і аж наприкінці e2e. Керує цим fail fast: що перевірка дешевша й швидша, то раніше вона в конвеєрі. Лінтер відповідає за частки секунди й обходиться без збірки; перевірка типів через tsc --noEmit знаходить розбіжності, не виконуючи коду; юніти малі та ізольовані; інтеграційні вимагають підняти БД або суміжний сервіс і тривають хвилини; наскрізні тягнуть застосунок у браузері й додають десятки хвилин та найбільше нестабільності. Виходить воронка: кожен ранній етап відкидає очевидний брак, поки важкий етап навіть не почався. Запускати браузерну матрицю, коли лінтер спіткнувся на неімпортованому символі, — це марно спалені хвилини. Фактично це той самий shift-left, згорнутий у межі одного конвеєра.
Що таке quality gate і як він фізично блокує merge?
Це формальний критерій pass/fail, без якого зміна не їде далі — ні в головну гілку, ні на середовище. По суті — та сама ідея entry/exit-критеріїв із тест-менеджменту, застосована до конвеєра: доступ у гілку відкривається лише після виконання списку умов. Механіка на GitHub — захист гілки (branch protection) разом із обовʼязковими перевірками (required status checks): поки перелічені джоби не позеленіють, кнопка злиття неактивна. У блокувальний перелік зазвичай входять лінт, перевірка типів, юніти, інтеграційні, smoke-частина наскрізних, успішна збірка, часом аудит залежностей (SCA), а поверх — обовʼязкове код-рев'ю та вимога актуальної гілки.
Чому дешеві перевірки часто запускають паралельно, а не строгим ланцюжком?
Лінт, перевірка типів і юніти коштують так мало, що вишиковувати їх у ланцюг немає рації: результат приходить за секунди хоч так, хоч так, зате при паралельному запуску інженер отримує весь список зауважень відразу, а не по краплині. Ланцюг же принципово потрібен лише для важкого хвоста — інтеграційні й наскрізні не мають стартувати, доки дешеві етапи не зелені. Тому фраза «порядок перевірок» насправді про одне: дороге ховається за гейтом дешевого, а не про буквальну чергу з кожного кроку.
Які властивості відрізняють хороший quality gate від поганого?
Ознак чотири, і кожну має сенс перевіряти прямо на рев'ю CI-конфіга. Перша — швидкість: гейт має вкладатися в бюджет фідбеку на PR, це одиниці, ну максимум близько півтора десятка хвилин. Друга — детермінізм: однаковий код дає однаковий вердикт; плавучий гейт підриває довіру ще дужче, ніж повна його відсутність. Третя — інформативність: по червоному має бути зрозуміло, що конкретно впало і де лежить артефакт падіння, а не глухе «job failed». Четверта — відповідальність: у кожного червоного є той, хто його ремонтує; безхазяйний гейт розкладається. Провал хоч однієї ознаки поступово перетворює гейт на ритуал, який навчаються оминати.
Чому e2e зазвичай не ганяють на кожен PR?
Наскрізні тести водночас найповільніші, найтендітніші й найсильніше зав'язані на стан середовища. Якщо повісити блокувальний гейт на хиткий e2e-набір, довіра вмирає найшвидше: коли червоне у половині випадків означає флак, а не регресію, люди звикають перезапускати прогін або продавлювати злиття повз гейт. А гейт, який раз у раз оминають, захищає рівно нічого. Тому важкий набір відводять убік, а на PR тримають вузький і стабільний smoke основного сценарію — його блокувальність окупається, бо він короткий і майже не бреше.
Як розкласти прогони за трьома тригерами — PR, nightly, реліз?
Три конвеєрні розклади під три задачі. На кожен PR ганяють лінт, типи, юніти, інтеграційні/API, smoke основного шляху, збірку та SCA — це блокує злиття, бо мета тут швидкий і повторюваний вердикт як перепустка до merge. Уночі за розкладом cron ганяють увесь e2e, матрицю браузерів, повну регресію, візуальні й перф-смоук — merge це не блокує, зате в набору є господар; мета — виловити те, що для PR задовге або зашироке. Перед релізом — повна регресія на реліз-гілці, smoke на staging після викочування, скан безпеки і ручний exploratory-підпис; це блокує вже саме рішення про випуск. Основа розкладки — піраміда: чим нижче рівень тесту, тим частіше він біжить і тим більше блокує.
Як один і той самий код тестів обслуговує і smoke на PR, і повний набір у nightly?
Піднабір критичного шляху мітять тегом (скажімо, @smoke) і оформлюють окремим проєктом ранера — не копіюють у другу теку. У playwright.config.ts описують два проєкти: smoke фільтрує grep: /@smoke/, а full бере все, крім незавершеного, через grepInvert: /@wip/. Далі PR запускає --project=smoke, а нічний прогін — --project=full. Спільний код обслуговує обидва розклади, і правка будь-якого тесту сама доїжджає в них обох.
Чому зелений білд іноді нічого не гарантує?
«Зелено» мусить читатися як «перевірили й пройшло», але нерідко значить лише «не впало». Найтиповіше — тихо викинутий етап тестів: || true зʼїдає код виходу, continue-on-error: true не дає червоному завалити джоб, а порожній збіг тегів проганяє нуль тестів. Сюди ж покриття без асертів, коли тест проходить код, нічого не стверджуючи. На рев'ю конфіга ці конструкції варто вишукувати навмисне: зелений білд, під яким перевірки не бігали, шкідливіший за відверто червоний, бо сіє оманливу впевненість.
У чому небезпека coverage як гейта?
Покриття фіксує, що рядок або гілку виконали, а не що результат хтось перевірив асертом — прогнати код і нічого не стверджувати цілком реально, і воно дасть свої 100% ні за що. Тільки-но покриття робиться порогом-ціллю, воно псується як показник — це закон Гудгарта: команда починає догрібати відсоток тестами на геттери й крихкими перевірками заради цифри. Плюс саме покриття нерівне: сто відсотків рядків — ще не сто відсотків гілок, бо умову можна виконати, ні разу не звернувши в else. Підсумок: мала цифра — надійний сигнал біди, велика цифра якості не обіцяє.
Яка зріла альтернатива вимозі «90% покриття на весь репозиторій»?
Замість тотального порога працюють дві речі. Diff-coverage (він же patch coverage) вимагає покриття лише на рядках, доданих у цьому PR: свіжа логіка їде разом із тестами, а давній непокритий код не валить кожне злиття; так уміють Codecov, Coveralls тощо. Храповик (ratchet) фіксує планку «не нижче нинішньої»: цифрі не обовʼязково дертися до магічного числа, зате падати їй не дозволено. І окреме табу — не чіпляти coverage-гейт на e2e, де відсоток бреше вже за самою природою. Тоді гейт лишається індикатором непокритої новизни, а не приводом накручувати відсоток.
Коли e2e пора виносити з блокувальних і як саме?
Тривожних сигналів три: багато флаку (тести падають, а на повторі без правок коду зеленіють), регулярні ручні перезапуски й продавлювання гейта, тривалість, що вилазить за бюджет фідбеку PR. Лікування — не «прибрати e2e», а рознести. На PR лишають короткий надійний smoke ключового флоу — вхід, оплата, головний сценарій — і його блокувальність тут доречна. Решту, важку й хитку, переносять у нічний прогін: merge він не блокує, але має власника, SLA на розбір і карантин для нестабільних. Захист нікуди не дівається — він просто перерозподіляється: швидкий надійний контур на PR і широкий контур під наглядом уночі.
Що означає застереження «неблокувальний ≠ нічий»?
Перенести e2e в ніч і не приставити нікого — це не «побороли флак», а «вимкнули сигнал»: нічне червоне ніхто не гортає, а справжні регресії тим часом протікають у прод. Чесний неблокувальний індикатор кращий за фальшивий блокувальний, але «неблокувальний» не має рими з «нічий»: потрібні тріажер, домовлений SLA на розбір і карантин для хитких. І весь e2e-щит із блокувальних тихцем не знімають — куценький smoke на PR лишається за будь-яких обставин, а будь-яке послаблення відкрите й супроводжене планом стабілізації.
Чому флакі-гейт гірший за відсутність гейта?
Уся цінність гейта — у передбачуваності. Хиткий гейт на тому самому коді видає то одне, то інше, тож команда перестає вірити червоному, машинально тисне перезапуск і врешті вчиться його оминати. Довіру до всього конвеєра це руйнує швидше, ніж якби гейта не було зовсім, — бо чесна відсутність принаймні нікого не заколисує. Отож хиткий блокувальний гейт треба або стабілізувати, або чесно перевести в неблокувальні з власником.
Які метрики здоровʼя пайплайна ти дивишся?
Конвеєр — теж продукт, і показників у нього дві сімʼї. Про сам конвеєр: green rate головної гілки (яку частку часу main зелений — постійно червоний main викриває, що гейт не тримає), час фідбеку PR у зрізах p50 і p95 (той самий «час до сигналу»: росте він — росте й бажання оминути конвеєр), time to green чи MTTR червоного main, частка флаку, як часто вручну перезапускають і продавлюють гейт (пряма міра недовіри), а ще тривалість сьюта та рахунок за ранери. Друга сімʼя — доставкові метрики DORA: як часто деплоять, lead time від коміту до прода, частка релізів зі збоєм, час на відновлення. DORA — про доставку й команду, не про тести, проте саме на неї зрештою лягає якість конвеєра; дивитися варто пучком, бо будь-яку метрику-ціль починають накручувати.
Приходиш на проєкт без CI-стратегії — з чого почнеш?
Переконлива відповідь — це послідовність думки, а не перелік тулзів. Стартують з інвентаризації: що за тести вже є, на яких рівнях, скільки тривають, наскільки флакують; яка модель гілкування; як часто викочують; де ризик найдорожчий (для магазину — кошик і оплата). Далі назначають бюджет фідбеку PR (умовно «до 10–12 хвилин») і під нього набивають етапи, паралелячи й кешуючи. Тоді гейти злиття: обовʼязкові перевірки — швидкі етапи, smoke, збірка, рев'ю, і кожне швидке та з власником. Важке — повну регресію, матрицю браузерів, візуальні — відводять у ніч із тріажером. Покриття кладуть на diff-coverage з храповиком, без «сто відсотків на репозиторій». Реліз-контур — повна регресія на реліз-гілці плюс smoke на staging. І під кінець — власність, метрики та ітерація, бо гейти не в граніті. Хто одразу сипле інструментами, програє тому, хто спершу питає про ризик і швидкість.
Три кейси, де стратегія вирішує, чи зелений білд щось означає: таблиця рішень «яку перевірку на який тригер», розведення флакуватого e2e-набору без втрати захисту й coverage-гейт, який не бреше. Скрізь — що робити й чому саме так.
Кейс 1. Таблиця рішень: яку перевірку на який тригер
Питання не «які перевірки маємо», а «коли кожна бігає і що блокує». Розкладаємо той самий набір по трьох тригерах — і одразу видно, що коштовне стоїть за гейтом дешевого.
| Перевірка | PR (блокує merge) | Nightly (з власником) | Реліз |
|---|---|---|---|
lint, typecheck, unit | так | — | так |
integration / API | так | — | так |
| smoke e2e критичного шляху | так | — | так |
| повний e2e + крос-браузер | ні | так | так |
| візуальні, перф-смоук | ні | так | — |
SCA (npm audit) | так | так | глибокий security-скан |
| exploratory-підпис | ні | ні | так |
Читається просто: лівий стовпчик — швидкий детермінований контур під бюджет фідбеку PR; середній — усе задовге чи зашироке під наглядом уночі; правий — повнота перед виходом у прод. Один код тестів обслуговує PR і nightly через теги й окремі проєкти ранера:
// playwright.config.ts — один код, два розклади
export default defineConfig({
projects: [
{ name: 'smoke', grep: /@smoke/ }, // критичний шлях, блокує merge
{ name: 'full', grepInvert: /@wip/ }, // повний набір, у nightly
],
});
І два воркфлоу, де дорогий крок фізично стартує лише за зеленими дешевими:
# .github/workflows/pr.yml — блокувальний контур на кожен PR
on: pull_request
jobs:
fast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test -- --coverage
smoke:
needs: fast # fail fast: браузер не піднімається, поки дешеве червоне
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright test --project=smoke
# .github/workflows/nightly.yml — важке, не блокує merge, але має власника
on:
schedule:
- cron: '0 2 * * *'
jobs:
full-e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright test --project=full
Що дивитися і чому:
needs: fast— це і є fail fast у конфізі. Джобsmokeне стартує, покиlint/typecheck/unitне зелені; браузерну ферму не піднімають заради коду, що не компілюється.- Required checks задають на боці репозиторію, не в YAML. У branch protection блокувальними роблять саме
fastіsmoke;nightlyтуди не потрапляє — інакше вранці ніхто не змерджить через нічний флак. --project=fullу nightly має власника, а не «нехай висить». Cron без триажера перетворює нічний прогін на кладовище червоного.
Кейс 2. Флакуватий e2e-набір: розвести, не викинути
Ситуація з життя: 60 e2e блокують кожен PR, флак ~20%, розробники звикли тиснути re-run по два-три рази. «Зелене» вже нічого не гарантує, бо його домагаються повтором. Спокуса — зняти e2e з блокувальних цілком. Це помилка з іншого боку: тоді реальні регресії критичного шляху течуть у прод.
Правильний хід — розвести набір, а не рубати:
// playwright.config.ts — карантин нестабільних без втрати smoke
export default defineConfig({
projects: [
// блокує merge: лише критичний шлях і БЕЗ карантинних
{ name: 'smoke', grep: /@smoke/, grepInvert: /@flaky/ },
// nightly: усе, крім незавершеного; @flaky тут під наглядом, неблокувально
{ name: 'full', grepInvert: /@wip/ },
],
});
// checkout.spec.ts
test('@smoke оплата картою проходить', async ({ page }) => { /* стабільний */ });
// нестабільний кейс — у карантині, поки стабілізують; не блокує PR
test('@flaky банер знижки зникає після скролу', async ({ page }) => { /* ... */ });
Що дивитися і чому:
- На PR лишається малий стабільний smoke критичного шляху — логін, оплата, ключовий сценарій. Його блокувальність виправдана, бо він швидкий і рідко бреше.
@flakyвиходить із блокувальногоsmoke, але лишається вfull. Це карантин, а не смітник: кейс далі бігає вночі, є SLA на його стабілізацію й повернення.- Пониження прозоре. У nightly має бути триажер із чек-листом «баг чи флак»; червоне без розбору гниє так само, як флакі-гейт. Неблокувальний не означає нічий.
- Не маскуй флак
sleep-ами й ретраями наосліп. Ретрай ховає симптом; карантин + власник лікують причину.
Кейс 3. Coverage-гейт, який не бреше
Наївний гейт «90% рядків на весь репозиторій» ловиться на двох речах: покриття міряє виконання, а не перевірку, і рядки не дорівнюють гілкам. Ось функція, де один тест дає 100% line coverage і пропускає цілу гілку:
export function discount(price: number, isVip: boolean): number {
let d = 0;
if (isVip) {
d = price * 0.2; // ця гілка виконається лише за isVip === true
}
return price - d;
}
// тест, який дає високе покриття і майже нуль цінності
test('discount рахує знижку VIP', () => {
discount(100, true); // усі рядки виконано, але жодного expect — і гілка isVip=false не перевірена
});
Цей тест проходить кожен рядок функції, тож line coverage близький до 100% — але гілку isVip === false не перевірено (branch coverage ~50%), та ще й немає жодного асерту. Метрика зелена, захисту нуль.
Що ставити замість сакрального відсотка:
# codecov.yml — вимога до доданих рядків + храповик на весь проєкт
coverage:
status:
patch: # diff-coverage: тільки нові рядки цього PR
default:
target: 80%
project: # храповик: не нижче поточного, без гонитви до 100%
default:
target: auto
threshold: 0%
Що дивитися і чому:
patch(diff-coverage) вимагає тести до доданих рядків, а не до всього репо: нова логіка приходить покритою, старий непокритий код не блокує кожен PR.project: autoзthreshold: 0%— це храповик. Покриття не мусить рости до магічного числа, але й не має падати; регрес видно одразу.- Coverage-гейт не вішають на e2e — там цифра оманлива за побудовою: браузер проходить купу коду без жодного цільового асерту.
- Число — індикатор, а не бос. Низьке покриття — достовірний сигнал проблеми; високе саме по собі якості не гарантує, тому рев'ю самих тестів чесніше за будь-який поріг.
Порядок перевірок і fail fast
- Знаю принцип fail fast: дешеве й детерміноване спереду, дороге e2e — за гейтом дешевого.
- Розумію, чому
lintдешевший заtypecheck, а той — заunit/integration/e2e (компіляція, підняття середовища, браузер). - Можу пояснити, чому дешеві перевірки часто паралеляться, а строгий порядок критичний саме для дорогого хвоста.
Quality gate і блокування merge
- Знаю, що quality gate — це умова pass/fail для злиття, прикладна форма entry/exit-критеріїв.
- Можу пояснити, що технічно merge блокують branch protection + required status checks.
- Назву чотири властивості хорошого гейта: швидкий, детермінований, дієвий, з власником.
- Розумію, чому флакі-гейт гірший за відсутність гейта, а гейт, який регулярно оверрайдять, — уже не гейт.
Розклад прогонів: PR / nightly / реліз
- Знаю мапінг піраміди на пайплайн: що нижчий рівень тестів, то частіше він бігає й то більше блокує.
- Розрізняю три розклади під три цілі: PR (блокує, швидкий фідбек), nightly (важке, з власником, не блокує merge), реліз (повний контур, рішення про реліз).
- Можу пояснити, як теги (
@smoke) плюс окремі проєкти ранера обслуговують два розклади одним кодом.
Coverage як гейт
- Можу пояснити, чому покриття міряє виконання рядка, а не наявність асерту.
- Знаю різницю line coverage vs branch coverage і чому 100% рядків не дорівнює 100% гілок.
- Розумію закон Гудгарта в контексті coverage-порога: метрика-ціль накручується дешевими тестами.
- Знаю зрілу альтернативу: diff-coverage з храповиком замість «90% на весь репозиторій».
E2e і довіра до гейта
- Знаю сигнали, що e2e пора виносити з блокувальних: високий флак, часті ручні re-run/оверрайди, тривалість понад бюджет.
- Розумію правило «розвести, а не викинути»: стабільний smoke на PR + повний набір у nightly.
- Можу пояснити «неблокувальний ≠ нічий»: триажер, SLA на розбір, карантин для нестабільних.
Метрики й «зелений білд»
- Знаю ключові метрики пайплайна: green rate
main, час фідбеку (p50/p95), MTTR червоногоmain, рівень флаку, частота оверрайдів. - Можу відрізнити метрики пайплайна від DORA (частота деплоїв, lead time, change failure rate, час відновлення — рівень доставки, не тестів).
- Упізнаю «зелене, що нічого не перевірило»:
|| true,continue-on-error, порожній матч тегів.
Який принцип диктує порядок lint → typecheck → unit → integration → e2e?
Питання
Принцип fail fast у CI — що це?