Pull Request і стратегії гілкування
Зміст
Pull Request (PR) — це точка, де зміна проходить рев'ю людей і автоматичні перевірки перед тим, як потрапити у спільну гілку. Сам Git такого поняття не має: він уміє лише зливати гілки локально. PR — це надбудова платформи (GitHub, GitLab, Bitbucket), яка обгортає звичайне злиття в процес: дифф, обговорення, апрув, статуси CI і правила, за яких кнопка «злити» взагалі стає доступною.
Для AQA це тема з подвійним дном. По-перше, ваші власні автотести їдуть у репозиторій рівно тим самим шляхом — через PR і рев'ю. По-друге, QA часто сам рев'ювер продуктових PR (тестовий погляд на зміну) і власник gate, який блокує злиття, поки не зелений smoke. А стратегія гілкування визначає, скільки гілок живе, як довго і як швидко зміни доходять до продакшену — і саме звідси ростуть merge-конфлікти й «інтеграційне пекло». На middle-співбесідах це питають майже завжди в парі зі способами злиття.
Pull Request: чому його немає в Git
Git знає команди merge і rebase, але не знає, що таке «запит на злиття з обговоренням». PR — це сутність платформи, яка каже: «ось гілка feature/login, я хочу влити її в main; подивіться, обговоріть, схваліть». На GitHub і Bitbucket це Pull Request, у GitLab — Merge Request (MR). Це те саме, просто GitLab-івська назва точніша: ми справді просимо злити (merge), а не підтягнути (pull). Термін «pull» історичний — колись контриб'ютор просив мейнтейнера відкритого проєкту «підтягнути» його зміни до себе.
У PR завжди дві гілки: базова (base) — куди зливаємо (частіше main), і head-гілка — звідки беремо зміни. Платформа рахує дифф між ними, показує обговорення, прив'язує коментарі до конкретних рядків і збирає статуси перевірок CI. Механіка гілок і три-way merge — у главі Гілки та злиття.
Життєвий цикл PR і draft
PR проходить кілька станів, і плутанина в них — типова причина, чому «готова» зміна тижнями висить незлитою.
Ключова деталь — draft-режим (чернетка). Draft-PR каже команді: «ще не готово, не рев'юй усерйоз і не мерджи». GitHub фізично блокує кнопку злиття для draft; GitLab додає префікс Draft: до заголовка і теж не дає злити. Навіщо відкривати PR раніше готовності:
- рання видимість — команда бачить, що фіча в роботі, і не робить те саме;
- запустити CI на неготовому коді, щоб раніше зловити падіння;
- обговорити напрям рішення, поки не пізно переробляти;
- не смикати рев'юерів завчасним запитом на апрув.
Важливо: кожен push у head-гілку оновлює PR — переобчислюється дифф і перезапускаються перевірки CI. Тобто PR не «фотографія», а живе відображення гілки. Виправив зауваження рев'юера, запушив — той самий PR оновився, статуси пішли по новій.
Code review автотестів
Рев'ю тестового коду — той самий стандарт, що й продуктового: читабельність, відсутність дублювання, осмислені імена. Але в автотестів є свій список того, на що рев'юер дивиться першим:
- Стабільні локатори.
data-testidі ролі, а не крихкі CSS/XPath, прив'язані до верстки чи згенерованих класів (стратегія локаторів — канон розділу «Автоматизація»). - Жодних hard wait.
page.waitForTimeout(3000)— червоний прапор: тест або флакітиме, або марно гальмуватиме. Механіку правильних очікувань розібрано у web-главі Практичні сценарії AQA: флак і синхронізація. - Незалежність і атомарність. Тест не залежить від порядку запуску й перевіряє один інваріант (Arrange–Act–Assert).
- Без секретів у коді. Паролі й токени тестових акаунтів — у секретах CI, а не в репозиторії (див. Секрети та конфігурація в CI).
- Асерт перевіряє інваріант, а не факт «сторінка відкрилась».
expect(page).toHaveURL(...)без перевірки вмісту — часто фейковий тест. - Назва описує перевірку, а не дію; жодних закоментованих чи безпричинно
skip-нутих тестів.
Класичний приклад того, що рев'ю має завернути:
// погано: так рев'юер завертає
await page.click('#submit');
await page.waitForTimeout(2000);
expect(await page.locator('.toast').count()).toBe(1);
// добре: web-first assertion сам чекає появи, без магічних затримок
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByTestId('toast')).toBeVisible();
Окреме правило: нові тести мають бути реально зеленими в CI, а не лише «локально в мене працює». Тому branch protection часто вимагає, щоб job з тестами був зеленим перед злиттям — про це нижче.
Стратегії гілкування
Стратегія (branching strategy) — це домовленість про те, які гілки існують, скільки живуть і як зливаються. Трьох моделей вистачить, щоб орієнтуватися.
GitHub Flow
Найпростіша. Одна вічнозелена гілка main, яка завжди придатна до деплою. Будь-яка зміна — це коротко-живуча feature-гілка від main, PR назад у main, деплой з main. Жодних develop, release, hotfix. Модель ідеально лягає на continuous delivery: злив — задеплоїв.
Git Flow
Модель Вінсента Дрізена (Vincent Driessen, 2010) з п'ятьма типами гілок: main (реліз), develop (інтеграція), feature/*, release/*, hotfix/*.
Модель потужна для продуктів із явними версіонованими релізами — коробкового ПЗ, застосунків із довгим циклом. Але для веба, який деплоять по кілька разів на день, вона важка: довгоживучі гілки develop і feature/* розходяться і породжують merge-конфлікти («merge hell»), а release-гілки додають церемоній. Показово, що сам автор у 2020 році додав до статті примітку: git-flow створювався для епохи явних релізів, і командам на безперервній доставці він радить простіший процес — прямо називає GitHub Flow. Саме тому Git Flow і «відходить» — не тому що поганий, а тому що для багатьох команд змінилася модель доставки.
Trunk-based development
Усі працюють з однією гілкою-стовбуром (trunk, зазвичай main), а гілки живуть години, максимум день. Кожен інтегрується у trunk щонайменше раз на день. Це не про хаос — це про дисципліну частої інтеграції: чим менша й свіжіша гілка, тим менший конфлікт при злитті. Замість довгих feature-гілок незавершені фічі ховають за feature flags (прапорцями функціональності), тож недороблений код мирно лежить у main вимкненим.
Trunk-based нерозривно пов'язаний із CI: часта інтеграція має сенс лише тоді, коли кожен PR за хвилини проганяється автоматичними перевірками (див. CI/CD: інтеграція, доставка, розгортання). Для AQA звідси два наслідки: перевірки на PR мають бути швидкими (інакше вони гальмують усю команду), а feature flags треба тестувати в обох станах — і ввімкненому, і вимкненому.
Branch protection
Branch protection (захист гілки) — це правила платформи на базову гілку, які не дають злити «як попало». Типовий набір на main:
- вимагати PR і щонайменше N апрувів (жодних прямих push у
main); - вимагати зелені required status checks — конкретні job'и CI мають пройти;
- вимагати, щоб гілка була оновлена відносно бази (up-to-date) перед злиттям;
- вимагати лінійну історію (заборонити merge-коміти);
- заборонити force-push у захищену гілку;
- поширювати правила навіть на адміністраторів.
Найважливіший для QA пункт — required status checks: це прямий місток до CI. Поки job з автотестами червоний, кнопка злиття заблокована платформою. Так quality gate стає не «домовленістю», а технічним бар'єром (розкладку перевірок і що саме блокує merge розібрано в главі Стратегія якості в пайплайні).
Зворотний бік: якщо required-перевіркою зробити нестабільний e2e-набір, один флакі-тест блокує злиття всій команді. Тому політика довкола флакі (карантин, ретраї, що взагалі ставити в блокувальні перевірки) — окрема тема канону автоматизації.
Хто саме має рев'ювити, автоматизує файл CODEOWNERS: він прив'язує ділянки коду до власників, і платформа сама запитує в них рев'ю.
# CODEOWNERS: власники автоматично додаються в рев'юери
/tests/e2e/ @qa-team
/src/payments/ @payments-team @qa-team
*.spec.ts @qa-team
Способи злиття: merge, squash, rebase
Коли PR схвалено, платформа пропонує кілька способів «покласти» зміни в базову гілку. На GitHub це три різні кнопки, і різниця — у тому, який слід лишиться в історії.
| Спосіб | Що робить з історією | Коли доречно |
|---|---|---|
Merge commit (--no-ff) | Зберігає всі коміти PR плюс окремий merge-коміт із двома батьками | Релізні гілки, коли важлива повна історія інтеграції |
| Squash and merge | Стискає всі коміти PR в один новий коміт на базі | Feature-гілки: один PR = один коміт, чиста лінійна історія |
| Rebase and merge | Переграє кожен коміт PR на базу без merge-коміту | Коли потрібна лінійна історія, але окремі коміти варто зберегти |
Merge commit нічого не втрачає, але залишає в історії «діамант» з двома гілками — на активному репозиторії граф стає шумним. Squash дає найчистішу історію (один коміт на PR, легко читати git log), але «розчиняє» проміжні коміти — і разом з ними гранулярність для bisect (див. главу про bisect і blame). Rebase and merge дає лінійну історію без merge-комітів, але переписує коміти на нові SHA (нові хеші) — механіку й ризики цього розібрано в главі Rebase, cherry-pick і переписування історії, включно із золотим правилом спільних гілок і --force-with-lease.
Практичний дефолт багатьох команд: squash для feature-гілок (кожен PR — атомарний коміт), merge-коміт для злиття релізних гілок.
Тести в репо продукту чи окремо
Питання, де живуть автотести, — архітектурне, і його теж люблять на співбесідах.
| Разом із продуктом | Окремий репозиторій | |
|---|---|---|
| Версіонування | Тести і код завжди синхронні | Ризик версійного дрейфу |
| Зміна фічі + тесту | Один атомарний PR | Два PR у двох репо |
| CI | Той самий пайплайн | Власний, треба тригерити |
| Володіння | Розробники бачать червоне одразу | Чиста межа, володіє QA |
| Крос-сервісні e2e | Незручно | Природно |
Разом із кодом тести версіонуються з продуктом: PR, який ламає поведінку, у тому ж PR оновлює й тест, а CI проганяє їх на одному коміті. Це і є shift-left — розробник бачить червоне негайно. Мінуси: важкі e2e роздувають репо продукту, можуть бути іншою мовою/тулінгом, і постає питання, хто їх підтримує.
Окремий репозиторій дає власний темп релізів, чисту межу відповідальності й зручність для e2e, що ходять по кількох сервісах одразу. Головна ціна — версійний дрейф: проти якої версії продукту ганяються тести? Легко отримати ситуацію, коли зелений прогін означає лише, що тести відповідають вчорашньому продукту. Плюс крос-репо тригери й неможливість атомарно оновити код і тест в одному PR.
Загальний орієнтир: unit/integration/component-тести майже завжди живуть із кодом продукту; окремий репозиторій виправданий для крос-сервісних e2e й контрактних тестів (стратегію для мікросервісів і контрактне тестування розібрано в розділах «Автоматизація» та «API-тестування»).
Типові помилки
- Виглядає як «PR готовий», а насправді він забутий у стані draft — рев'юери його навіть не бачать у своїй черзі, і зміна тижнями висить.
- Виглядає як «зелений PR злився штатно», а насправді required status checks не налаштовані, і merge пройшов із червоним CI — захист гілки був лише на словах.
- Виглядає як «squash усе — чиста історія», а насправді ви втратили гранулярність для
bisect: коли тест упаде за місяць, винним буде один жирний коміт на весь PR замість точного. - Виглядає як «тести в окремому репо незалежні й це добре», а насправді вони ганяються проти застарілої версії продукту — версійний дрейф робить зелений прогін безглуздим.
- Виглядає як «rebase and merge безпечний», а насправді він переписує SHA; якщо хтось відгалузився від вашої гілки до злиття, ви створили йому розбіжну історію.
- Виглядає як «підправлю гілку force-push прямо в PR», а насправді ви можете «відв'язати» коментарі рев'юера від старих комітів і збити CI — під час активного рев'ю форс-пуш робіть обережно.
Підсумок
- PR/MR — це надбудова платформи над Git-злиттям: дифф, рев'ю, статуси CI і правила, за яких злиття взагалі дозволене; draft явно каже «ще не рев'юй».
- Стратегія гілкування = час життя гілок: Git Flow (довгі гілки, версійні релізи) відходить на користь GitHub Flow і trunk-based, бо веб деплоять безперервно.
- Trunk-based має сенс лише разом зі швидким CI і feature flags — часта інтеграція гасить merge-конфлікти в зародку.
- Required status checks у branch protection перетворюють quality gate з домовленості на технічний бар'єр — і роблять флакі-тест проблемою всієї команди.
- Спосіб злиття — це вибір форми історії: squash (чисто, але без гранулярності), merge-коміт (повно, але шумно), rebase (лінійно, але нові SHA).
Що питають на співбесіді
- «Чим відрізняється Pull Request від merge-команди Git?» — інтерв'юер перевіряє, чи ви розумієте, що PR не з Git, а з платформи, і додає процес поверх звичайного злиття.
- «Порівняйте Git Flow, GitHub Flow і trunk-based. Що обрали б для веб-застосунку на щоденних деплоях?» — дивиться на зв'язок стратегії з моделлю доставки й на аргумент про довжину життя гілок і merge-конфлікти.
- «Merge commit, squash чи rebase — що і коли?» — хоче почути не «улюблене», а тредоф: чистота історії проти повноти й гранулярності
bisect. - «Що перевіряєте на code review автотесту?» — очікує конкретику: локатори, відсутність hard wait, незалежність, секрети, осмислені асерти, а не «щоб працювало».
- «Як зробити, щоб не можна було злити PR із червоними тестами?» — перевіряє знання branch protection і required status checks як містка до CI.
- «Тести тримати в репо продукту чи окремо?» — сильна відповідь називає версійний дрейф як головний ризик окремого репо і shift-left як плюс спільного.
Джерела
- GitHub Docs — About pull requests та About merge methods — життєвий цикл PR і три способи злиття.
- GitHub Docs — About protected branches — required checks, апруви, CODEOWNERS.
- GitLab Docs — Merge requests — MR як аналог PR, draft-режим.
- Vincent Driessen — A successful Git branching model — першоджерело Git Flow і примітка автора 2020 року про його межі.
- Trunk Based Development — канонічний опис trunk-based і зв'язку з CI.
Чим Pull Request відрізняється від команди git merge?
git merge — це локальна операція самого Git: він механічно зливає одну гілку в іншу й нічого не знає ні про рев'ю, ні про обговорення. Pull Request (у GitLab — Merge Request) живе рівнем вище: це сутність платформи (GitHub, GitLab, Bitbucket), яка обгортає майбутнє злиття в процес — показує дифф між двома гілками, збирає коментарі до конкретних рядків, тримає апруви й статуси CI. Простими словами: merge просто зливає, а PR питає «можна?» і не дає натиснути кнопку, поки не виконані умови. Для AQA це важливо тому, що саме на рівні PR платформа вміє заблокувати злиття з червоними тестами — сам Git такого важеля не має. Тому «PR не з Git» — це не педантизм, а розуміння, де саме стоїть quality gate.
Що таке draft Pull Request і навіщо відкривати його до готовності?
Draft (чернетка) — це стан PR, який явно каже команді «ще не готово, не рев'юй усерйоз і не мерджи». GitHub фізично блокує кнопку злиття для draft, GitLab додає до заголовка префікс Draft: і теж не дає злити. Відкривають його рано з кількох причин: дати команді ранню видимість, щоб двоє не робили одну фічу; запустити CI на неготовому коді й раніше зловити падіння; обговорити напрям рішення, поки переробляти ще дешево; не смикати рев'юерів завчасним запитом на апрув. Типова пастка тут — забути перемкнути draft у Ready for review: зміна виглядає готовою, але рев'юери не бачать її у своїй черзі, і вона тижнями висить незлитою. Тобто draft економить час, але вимагає дисципліни його вчасно знімати.
Що відбувається з відкритим PR, коли ви пушите новий коміт у head-гілку?
PR не «фотографія» коду на момент відкриття, а живе відображення head-гілки. Кожен push у неї автоматично оновлює PR: платформа переобчислює дифф і перезапускає перевірки CI на новому коміті. Практично це означає, що правки за зауваженнями рев'юера не потребують нового PR — ви пушите в ту саму гілку, і той самий PR оновлюється, а статуси йдуть по-новій. Звідси й корисність draft: можна пушити багато разів, доганяючи зелений CI, поки рев'юери ще не залучені. Розуміння цього рятує від типового джуніорського «а давайте закриємо цей PR і відкриємо новий» — зазвичай цього робити не треба.
Порівняйте GitHub Flow, Git Flow і trunk-based development.
Це три моделі, що різняться передусім тривалістю життя гілок. GitHub Flow — найпростіший: одна вічнозелена main, завжди придатна до деплою, від неї коротко-живучі feature-гілки, PR назад у main, деплой з main; жодних develop чи release. Git Flow — важча модель з п'ятьма типами гілок (main, develop, feature/*, release/*, hotfix/*), заточена під продукти з явними версійними релізами. Trunk-based — усі працюють з однією гілкою-стовбуром, а гілки живуть години, максимум день, і кожен інтегрується у trunk щонайменше раз на добу. Ключова вісь порівняння — як швидко зміни доходять до спільної гілки: що довше живуть гілки, то сильніше вони розходяться і тим болючіші merge-конфлікти. Для веба на щоденних деплоях природний вибір — GitHub Flow або trunk-based, а не Git Flow.
Чому Git Flow вважають застарілим, хоча колись він був стандартом?
Git Flow не «поганий» сам по собі — просто змінилася модель доставки, під яку він створювався. Він проєктувався для епохи явних версійних релізів: коробкового ПЗ й застосунків із довгим циклом, де реліз — рідкісна подія з окремою підготовкою. Його довгоживучі гілки develop і feature/* розходяться між собою й породжують merge-конфлікти («merge hell»), а release-гілки додають церемоній, які на веб-продукті з десятком деплоїв на день просто гальмують. Показово, що сам автор моделі у 2020 році додав до оригінальної статті примітку: командам на безперервній доставці він радить простіший процес і прямо називає GitHub Flow. Тобто Git Flow не «зламався», а вийшов з-під свого контексту — для веба з continuous delivery він завеликий.
Що таке trunk-based development і як він обходиться без довгоживучих feature-гілок?
Trunk-based — це дисципліна частої інтеграції: усі комітять в один стовбур (зазвичай main), гілки живуть години й не встигають розійтися, тож конфлікти гасяться в зародку. Головне питання — куди дівати незавершені фічі, якщо не тримати їх у довгих гілках. Відповідь — feature flags (прапорці функціональності): недороблений код спокійно лежить у main, але вимкнений прапорцем, і його ніхто не бачить у продакшені, поки фічу не добудують і не ввімкнуть. Це працює лише в парі зі швидким CI — часта інтеграція має сенс тільки тоді, коли кожен PR за хвилини проходить автоматичні перевірки. Для AQA звідси два наслідки: перевірки на PR мусять бути швидкими, інакше вони гальмують усю команду, а фічі під прапорцем треба тестувати в обох станах — і ввімкненому, і вимкненому.
Merge commit, squash чи rebase — у чому різниця й коли що доречно?
Це три способи «покласти» схвалений PR у базову гілку, і різниця — у сліді, який лишається в історії. Merge commit (--no-ff) зберігає всі коміти PR плюс окремий коміт злиття з двома батьками — повна історія інтеграції, але на активному репозиторії граф стає шумним «діамантом»; доречний для релізних гілок, де важлива повнота. Squash and merge стискає всі коміти PR в один новий коміт на базі — найчистіша лінійна історія (один PR = один коміт), але проміжні коміти розчиняються, а з ними й гранулярність для bisect; типовий дефолт для feature-гілок. Rebase and merge переграє кожен коміт PR на базу без коміту злиття — лінійна історія зі збереженням окремих комітів, але коміти отримують нові SHA. Сильна відповідь на співбесіді — не «мені подобається squash», а тредоф: чистота історії проти повноти й проти гранулярності для пошуку регресій.
Чому squash-злиття може ускладнити пошук причини регресії через git bisect?
git bisect шукає коміт, який зламав поведінку, роблячи бінарний пошук по історії: що дрібніші коміти, то точніше він локалізує проблему. Squash стискає весь PR в один коміт, тож уся робота — хоч п'ять рядків, хоч п'ятсот — злипається в одну точку історії. Коли за місяць тест почне падати й bisect вкаже на цей коміт, ви дізнаєтеся лише «винен ось цей великий PR», а не конкретну зміну всередині нього. За чисту й читабельну історію squash розплачується саме цією втратою роздільної здатності для розслідувань. Тому команди, яким важлива діагностика регресій, іноді свідомо обирають rebase and merge замість squash — коміти лишаються окремими, хоч історія трохи «густіша».
Що таке branch protection і які правила зазвичай ставлять на main?
Branch protection (захист гілки) — це набір правил платформи на базову гілку, які не дають злити «як попало». Типовий набір на main: вимагати PR і щонайменше N апрувів (жодних прямих push у main); вимагати зелені required status checks — конкретні job'и CI мусять пройти; вимагати, щоб гілка була оновлена відносно бази перед злиттям; іноді — лінійну історію (заборона merge-комітів); заборону force-push у захищену гілку; і поширення правил навіть на адміністраторів. Суть у тому, що домовленості «ми не пушимо в main» ненадійні, а правило платформи — це технічний бар'єр, який не можна обійти забудькуватістю. Для QA найважливіший пункт — required status checks, бо саме він перетворює zelений CI на умову злиття.
Як технічно унеможливити злиття PR із червоними тестами?
Через required status checks у branch protection базової гілки. Ви позначаєте конкретні job'и CI (наприклад, job із автотестами) як обов'язкові — і поки такий job червоний, платформа фізично блокує кнопку злиття. Так quality gate перестає бути «домовленістю» («ну ти ж бачив, що впало») і стає технічним бар'єром: злити з червоним CI просто не вийде. Це прямий місток між гілковим захистом і CI, і саме його перевіряють питанням про «як заборонити merge з червоними тестами». Важливий нюанс і зворотний бік: якщо зробити required-перевіркою нестабільний e2e-набір, один флакі-тест почне блокувати злиття всій команді — тому в блокувальні перевірки ставлять лише швидкі й стабільні job'и, а флакі виносять у карантин.
Що таке CODEOWNERS і як він пов'язаний із рев'ю?
CODEOWNERS — це файл у репозиторії, який прив'язує ділянки коду до їхніх власників, і платформа сама автоматично запитує в них рев'ю, коли PR чіпає відповідні файли. Синтаксис простий: шаблон шляху плюс команда чи користувач, наприклад /tests/e2e/ @qa-team або *.spec.ts @qa-team. Для QA це зручний важіль: можна закріпити за командою тестування всі спеки й e2e-каталоги, і тоді жоден PR, що їх зачіпає, не пройде повз ваше рев'ю — платформа сама додасть вас у рецензенти. У поєднанні з branch protection, яке вимагає апрув від code owner, це робить рев'ю потрібних ділянок не побажанням, а обов'язковим кроком. Тобто CODEOWNERS автоматизує питання «хто саме має це подивитися».
Що ви перевіряєте на code review автотесту насамперед?
Крім загального стандарту (читабельність, відсутність дублювання, осмислені імена), в автотестів є свій список червоних прапорців. Стабільні локатори — data-testid і ролі замість крихких CSS/XPath, прив'язаних до верстки чи згенерованих класів. Жодних hard wait — waitForTimeout(3000) означає, що тест або флакітиме, або марно гальмує; правильні очікування самі чекають потрібного стану. Незалежність і атомарність — тест не залежить від порядку запуску й перевіряє один інваріант за схемою Arrange–Act–Assert. Жодних секретів у коді — паролі й токени тестових акаунтів живуть у секретах CI, а не в репозиторії. Асерт перевіряє інваріант, а не факт «сторінка відкрилась» — toHaveURL без перевірки вмісту часто фейковий тест. І назва описує перевірку, а не дію, без закоментованих чи безпричинно skip-нутих тестів. Ключова відповідь на співбесіді — конкретика, а не «щоб працювало».
Тести тримати в репо продукту чи в окремому репозиторії?
Це архітектурний компроміс без єдиної правильної відповіді, але сильна відповідь називає головний ризик кожного варіанту. Разом із кодом продукту тести версіонуються синхронно: PR, що змінює поведінку, у тому ж атомарному PR оновлює й тест, а CI проганяє їх на одному коміті — це і є shift-left, розробник бачить червоне негайно. Мінуси спільного репо: важкі e2e роздувають репозиторій, можуть бути іншою мовою чи тулінгом, і постає питання володіння. Окремий репозиторій дає власний темп релізів, чисту межу відповідальності QA і зручність для крос-сервісних e2e — але його головна ціна це версійний дрейф: проти якої версії продукту ганяються тести? Легко отримати зелений прогін, який означає лише, що тести відповідають вчорашньому продукту. Загальний орієнтир: unit/integration/component живуть із кодом, а окремий репозиторій виправданий для крос-сервісних e2e й контрактних тестів.
Що таке версійний дрейф і чому він знецінює зелений прогін?
Версійний дрейф — це розходження між версією продукту й версією тестів, коли вони живуть у різних репозиторіях і релізяться в різному темпі. Проблема в тому, що коли тести й код не прив'язані до одного коміту, зелений прогін перестає гарантувати, що тестувалася поточна версія продукту: набір міг проганятися проти вчорашнього білда, а сьогоднішні зміни лишилися непокритими. Виходить фальшива впевненість — «усе зелене», хоча насправді перевірено застарілий стан. Це головний аргумент проти окремого репо тестів і головна причина, чому unit/integration тримають поруч із кодом: там код і тест завжди на одному коміті, дрейф неможливий за побудовою. Лікують дрейф явним прив'язуванням версій (тести знають, проти якого релізу ганяються) і крос-репо тригерами, але це складніше, ніж атомарний PR у спільному репо.
Які ризики несе rebase and merge, попри чисту лінійну історію?
Rebase and merge переграє коміти PR на базу й видає їм нові SHA — тобто ті самі за змістом коміти отримують інші хеші. Це безпечно, поки гілку ніхто не використовував, але стає проблемою, якщо хтось відгалузився від вашої гілки до злиття: після rebase його база «переписалася», і в нього утворюється розбіжна історія з дублікатами комітів. Тому rebase-операції над уже поширеними гілками — класична пастка, від якої рятує золоте правило «не переписуй історію спільних гілок» і обережний --force-with-lease замість грубого --force. У контексті самого злиття PR ризик менший (платформа робить rebase контрольовано), але розуміти, що SHA змінюються, треба — на цьому будуються питання про переписування історії. Коротко: лінійна історія від rebase не безкоштовна, її ціна — нові хеші й потенційно зламані відгалуження.
Чому force-push у гілку під час активного рев'ю треба робити обережно?
Force-push переписує історію head-гілки, і це має два неприємні побічні ефекти під час рев'ю. По-перше, коментарі рев'юера прив'язані до конкретних комітів і рядків — після force-push старі коміти зникають, і платформа може «відв'язати» ці коментарі, тож обговорення губить контекст. По-друге, це збиває CI: перезапускаються перевірки, а проміжні статуси на старих комітах стають неактуальними. Тому під час активного рев'ю краще додавати нові коміти зверху (їх потім можна squash при злитті), а не переписувати гілку через rebase + --force. Якщо force-push усе ж потрібен, роблять його свідомо й попереджають рев'юера, а замість грубого --force використовують --force-with-lease, який не затре чужі коміти. Тобто сам по собі force-push не заборонений, але в момент рев'ю він коштує втрачених коментарів і збитих статусів.
Три кейси, у яких PR і гілкування прямо впливають на роботу AQA: як влаштувати технічний бар'єр проти злиття з червоними тестами, як виглядає рев'ю чужого автотесту з коментарями до рядків, і як обрати стратегію гілкування під конкретну команду. Скрізь — що дивитися й чому.
Кейс 1. Захист main: щоб не можна було злити з червоним CI
Домовленість «ми не мерджимо червоне» ненадійна — рано чи пізно хтось зіллє поспіхом. Перетворимо її на технічний бар'єр через branch protection плюс CODEOWNERS, щоб QA автоматично потрапляв у рецензенти тестів.
Файл CODEOWNERS у корені репо (або в .github/):
# кожен PR, що чіпає ці шляхи, автоматично отримує qa-team в рецензенти
/tests/e2e/ @qa-team
/tests/api/ @qa-team
*.spec.ts @qa-team
/src/payments/ @payments-team @qa-team
Правило захисту гілки main (тут — у вигляді GitHub ruleset, суть та сама і для класичного branch protection):
# require_status_checks: ці job'и мусять бути зеленими, інакше merge заблоковано
required_status_checks:
strict: true # гілка має бути up-to-date з main перед злиттям
checks:
- context: "e2e-smoke" # швидкий стабільний набір — у блокувальні
- context: "unit"
required_pull_request_reviews:
required_approving_review_count: 2
require_code_owner_reviews: true # апрув від власника з CODEOWNERS обов'язковий
enforce_admins: true # правила діють навіть на адмінів
allow_force_pushes: false
required_linear_history: true # заборонити merge-коміти в main
Що дивитися й чому:
e2e-smokeв required, а не весь e2e-набір. У блокувальні перевірки ставлять лише швидкі й стабільні job'и. Якщо зробити required повільний нестабільний e2e, один флакі-тест почне блокувати злиття всій команді — а це гірше, ніж відсутність gate, бо люди почнуть шукати способи його обійти.strict: true(up-to-date) ловить «зелене окремо, червоне разом». PR міг бути зелений проти старої бази, але після чужого злиття вmainламається інтеграція. Вимога оновитися перед merge проганяє перевірки на актуальному стані.require_code_owner_reviewsробить рев'ю QA обов'язковим, а не бажаним. Без ньогоCODEOWNERSлише пропонує рецензентів; у парі з цим прапорцем PR не зіллється, поки власник спеків не апрувне.enforce_admins: true— щоб gate не був «на словах». Класична пастка: перевірки налаштовані, але адмін має право обійти їх і одного разу зливає повз червоне. Тоді захист гілки існує лише формально.
Кейс 2. Рев'ю чужого автотесту: що завертати й якими словами
QA часто сам рецензент PR із тестами. Нижче — дифф, який приходить на рев'ю, і коментарі, які варто лишити прямо до рядків (як на GitHub). Тест наче «зелений локально», але завертати його треба.
// tests/e2e/checkout.spec.ts (як прийшло в PR)
test('checkout', async ({ page }) => {
await page.goto('https://shop.example.com');
await page.locator('.btn.btn-primary.css-1x9f').click(); // 1
await page.waitForTimeout(3000); // 2
await page.fill('#card', '4111111111111111'); // 3
await page.click('text=Pay');
await expect(page).toHaveURL(/success/); // 4
});
Коментарі рев'юера до рядків:
- Крихкий локатор.
.btn.btn-primary.css-1x9fприв'язаний до верстки й згенерованого класу — зміниться тема, і тест впаде на порожньому місці. Треба роль абоdata-testid:page.getByRole('button', { name: 'Checkout' }). - Hard wait — червоний прапор.
waitForTimeout(3000)або флакітиме (форма ще не з'явилась за 3 с), або марно гальмує (з'явилась за 200 мс). Заміни на web-first очікування конкретного елемента:await expect(page.getByTestId('card-form')).toBeVisible(). - Секрет/чутливі дані в коді. Реальний номер картки в репозиторії — погано навіть як тестовий; такі дані тримають у секретах CI або тестових фікстурах, а не хардкодять у спеку.
- Фейковий асерт.
toHaveURL(/success/)перевіряє лише адресу, а не те, що оплата справді пройшла. Додай перевірку інваріанту: номер замовлення, статус, суму —await expect(page.getByTestId('order-status')).toHaveText('Paid').
Як мала б виглядати виправлена версія після рев'ю:
test('успішна оплата показує статус Paid і номер замовлення', async ({ page }) => {
await page.goto('https://shop.example.com');
await page.getByRole('button', { name: 'Checkout' }).click();
await expect(page.getByTestId('card-form')).toBeVisible();
await page.getByTestId('card-number').fill(process.env.TEST_CARD!);
await page.getByRole('button', { name: 'Pay' }).click();
await expect(page.getByTestId('order-status')).toHaveText('Paid');
});
Окремо зверни увагу: назва тесту тепер описує перевірку («показує статус Paid»), а не дію («checkout»). І поки цей PR не зелений у CI-job e2e-smoke, branch protection з кейсу 1 не дасть його злити — навіть з двома апрувами.
Кейс 3. Яку стратегію гілкування обрати: таблиця рішень
Питання «яку модель узяли б для нашого продукту» на співбесіді перевіряє не знання назв, а вміння зв'язати стратегію з моделлю доставки. Ось орієнтир.
| Ситуація команди | Розумний вибір | Чому |
|---|---|---|
| Веб-застосунок, деплой кілька разів на день, зрілий CI | Trunk-based або GitHub Flow | Короткі гілки гасять merge-конфлікти; часта інтеграція вимагає швидкого CI |
| Невелика команда, простий реліз-флоу, CD | GitHub Flow | Мінімум церемоній: одна main, feature-гілка, PR, деплой |
| Коробкове ПЗ, кілька версій у підтримці, релізи раз на квартал | Git Flow | Явні release/* і hotfix/* під версійні релізи й патчі старих версій |
| Велика команда, хочуть частої інтеграції, але фічі великі | Trunk-based + feature flags | Незавершене лежить у main вимкненим, гілки не встигають розійтися |
Міні-кейс. Команда з 8 розробників і 2 QA пише SPA, деплоїть у прод 5–10 разів на день, e2e-smoke проходить за 4 хвилини. Хтось пропонує Git Flow «щоб було по-дорослому». Сильна відповідь QA: Git Flow тут зайвий — його develop і довгі feature/* при щоденних деплоях розійдуться й дадуть merge hell, а release-гілки нічого не додадуть, бо релізимо ми не версіями, а безперервно. Природний вибір — trunk-based: короткі гілки, feature flags для незавершених фіч (їх тестуємо у двох станах), а швидкий smoke в required status checks тримає main завжди зеленою. Аргумент не «мені так подобається», а зв'язка «модель доставки → тривалість життя гілок → ризик конфліктів».
Pull Request: суть і життєвий цикл
- Можу пояснити, що PR/MR — сутність платформи, а не Git: він обгортає звичайне злиття в дифф, рев'ю, апруви й статуси CI; PR і MR — те саме, а GitLab-івська назва точніша за історичну «pull».
- Розумію стани PR: draft → open → changes requested → approved → merged (або closed без злиття).
- Знаю, що draft фізично блокує кнопку злиття, навіщо відкривати PR до готовності (рання видимість, ранній CI, обговорення напряму) і типову пастку — «готовий» PR, забутий у draft.
- Розумію, що кожен push у head-гілку оновлює той самий PR — переобчислює дифф і перезапускає CI, а не створює новий.
Стратегії гілкування
- Знаю три моделі й головну вісь між ними — тривалість життя гілок: GitHub Flow, Git Flow, trunk-based.
- Можу описати GitHub Flow: одна вічнозелена
main, коротко-живучі feature-гілки, деплой зmain. - Можу назвати п'ять гілок Git Flow (
main,develop,feature/*,release/*,hotfix/*), для чого він створювався (версійні релізи) і чому відходить — довгі гілки дають merge hell, а сам автор у 2020 радить простіший процес. - Можу пояснити trunk-based: гілки живуть години, незавершене ховається за feature flags (тестувати у двох станах), і це має сенс лише в парі зі швидким CI.
Branch protection і required checks
- Можу перелічити типові правила захисту
main: PR + N апрувів, required checks, up-to-date, заборона force-push, правила для адмінів. - Розумію, що required status checks — це технічний бар'єр (червоний job блокує кнопку злиття), і зворотний бік: нестабільний e2e у required блокує злиття всій команді через один флакі-тест.
- Можу пояснити, що робить CODEOWNERS: прив'язує ділянки коду до власників, і платформа сама запитує в них рев'ю.
Способи злиття: merge / squash / rebase
- Знаю різницю трьох способів за слідом в історії: merge commit зберігає все + коміт злиття, squash стискає в один, rebase переграє на базу.
- Розумію тредоф squash (чиста історія ціною гранулярності для
git bisect) і merge commit (повна історія ціною шумного «діаманта»). - Знаю, що rebase and merge дає лінійну історію, але переписує коміти на нові SHA (ризик для тих, хто відгалузився); пам'ятаю дефолт: squash для feature-гілок, merge-коміт для релізних.
Code review автотестів
- Можу назвати червоні прапорці в тесті: крихкі локатори, hard wait, залежність від порядку, секрети в коді, фейкові асерти.
- Розумію, чому
waitForTimeoutзавертають, аdata-testid/ролі — ні, і правило «асерт перевіряє інваріант, а не факт відкриття сторінки». - Розумію вимогу «новий тест реально зелений у CI», а не лише «локально в мене працює».
Де живуть тести
- Можу назвати головний плюс спільного репо — shift-left і атомарний PR «код + тест» на одному коміті.
- Можу назвати головний ризик окремого репо — версійний дрейф, через який зелений прогін перевіряє вчорашній продукт; і орієнтир: unit/integration/component — з кодом, крос-сервісні e2e й контрактні — виправдано окремо.
Чому Pull Request не є частиною самого Git?
Питання
Pull Request (PR) vs Merge Request (MR) — це те саме?