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

    09 · Git і CI/CD

    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 проходить кілька станів, і плутанина в них — типова причина, чому «готова» зміна тижнями висить незлитою.

    відкрито як чернетку

    позначено Ready for review

    рецензент просить правки

    нові коміти і повторне рев'ю

    апрув плюс зелені перевірки

    злито в базову гілку

    закрито без злиття

    Draft

    Open

    ChangesRequested

    Approved

    Merged

    Closed

    відкрито як чернетку

    позначено Ready for review

    рецензент просить правки

    нові коміти і повторне рев'ю

    апрув плюс зелені перевірки

    злито в базову гілку

    закрито без злиття

    Draft

    Open

    ChangesRequested

    Approved

    Merged

    Closed

    Ключова деталь — 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/*.

    maindevelopfeaturerelease0-e0ee83d1-0ce9ae82-961257d3-cbb33235-79e7385v1.0maindevelopfeaturerelease0-0b5a3881-f326f2f2-d7ddaad3-8dfd3ea5-d63129ev1.0

    Модель потужна для продуктів із явними версіонованими релізами — коробкового ПЗ, застосунків із довгим циклом. Але для веба, який деплоять по кілька разів на день, вона важка: довгоживучі гілки 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 як плюс спільного.

    Джерела