Senior: питання на співбесіді з теми «Pull request і code review»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Захист гілки перетворює домовленості («не пушимо в main», «зливаємо лише після рев'ю й зелених тестів») на правила, які платформа примусово виконує.
Типовий набір правил для main:
- заборонити прямий push - зміни лише через PR;
- обов'язкове схвалення (1-2 рецензенти) і рев'ю власників коду (CODEOWNERS);
- скидати схвалення при нових комітах (dismiss stale approvals) - інакше після апруву можна дописати що завгодно;
- обов'язкові перевірки статусу (required status checks): тести, Pint, PHPStan мають бути зеленими;
- гілка має бути актуальною перед злиттям - або merge queue замість цього;
- заборонити force push і видалення гілки;
- розв'язані обговорення перед злиттям, за потреби - підписані коміти і лінійна історія.
Branch protection rules проти rulesets:
| Branch protection | Rulesets | |
|---|---|---|
| скільки діє на гілку | одне правило | кілька наборів одночасно, правила агрегуються |
| конфлікт налаштувань | - | діє найсуворіший варіант |
| вимкнути тимчасово | лише видалити | статус Disabled без видалення |
| хто бачить | адміністратори | усі з доступом на читання |
| винятки | обмежено | явний список bypass (ролі, команди, GitHub Apps) |
| рівень організації | ні | так (на планах Team/Enterprise) |
Обидва механізми працюють разом: застосовуються всі правила, що підходять.
Пастки:
- назва обов'язкової перевірки має збігатися з назвою job у CI. Перейменували job - PR зависає в очікуванні перевірки, яка ніколи не прийде;
- перевірка, що запускається не завжди (через
pathsу workflow), лишається «очікуваною» - PR не можна злити. Рішення - job, що завжди звітує успіхом, якщо змін немає; - адміністратори за замовчуванням можуть обходити правила - варто вирішити свідомо;
- bypass для ботів (релізний бот) - лише конкретним GitHub Apps, не всім.
Правила як код: rulesets можна експортувати й імпортувати в JSON чи керувати ними через API й Terraform - однакові правила для десятків репозиторіїв.
Проблема «зелені окремо, червоні разом». PR A і PR B пройшли CI окремо, кожен відносно старого main. Їх злили один за одним - і main зламався: A перейменував метод, а B додав новий виклик старої назви. Конфлікту в Git немає, а код не працює.
Вимога «гілка має бути актуальною» це лікує, але в активному репозиторії перетворюється на гонку: кожне злиття робить усі інші PR застарілими, їх оновлюють, CI перезапускається, і хтось інший знову зливає першим.
Merge queue:
- PR схвалений і зелений - автор додає його в чергу замість натискання Merge;
- GitHub створює тимчасову гілку
gh-readonly-queue/main/...зmain+ усіма PR, що стоять перед ним у черзі, + цим PR; - на цій комбінації запускаються обов'язкові перевірки;
- перевірки пройшли - PR вливається; впали - PR вилучається з черги, а черга за ним перебудовується без нього.
Черга може перевіряти кілька PR групою паралельно - це прискорює потік, коли PR багато.
Що треба налаштувати в CI:
on:
pull_request:
merge_group:
Подія merge_group - окрема від pull_request і push. Якщо її не додати, обов'язкові перевірки для черги не запустяться, і злиття впаде через відсутній статус. Сторонні CI мають реагувати на push у гілки з префіксом gh-readonly-queue/.
Що змінюється для команди:
- спосіб злиття визначає черга, а не автор;
mainзавжди зелений: те, що в нього потрапляє, перевірено саме в такій комбінації;- час до злиття зростає на тривалість CI - тому швидкий CI стає ще важливішим.
Коли не потрібна: невелика команда, кілька злиттів на день - достатньо вимоги актуальності гілки. Merge queue окупається там, де PR вливаються десятками на день і зламаний main блокує всіх.
Проблема: велику фічу розбили на маленькі PR, але кожен наступний залежить від попереднього. Чекати злиття першого, щоб почати другий, - повільно; складати все в один PR - втрачаємо переваги маленьких рев'ю.
Stacked PRs - ланцюжок залежних pull request-ів, де кожен спрямований не в main, а в гілку попереднього:
feat/ui → PR #3 (base: feat/api) ← верх
feat/api → PR #2 (base: feat/schema)
feat/schema → PR #1 (base: main) ← низ
main
Кожен PR показує лише свій шар змін: рецензент дивиться міграцію окремо від API і окремо від інтерфейсу. Принцип: якщо код залежить від іншого коду, залежність має бути в тій самій гілці чи нижче.
Головна складність - rebase. Після виправлення в нижній гілці всі верхні треба перебазувати. Вручну це втомливо, але Git уміє оновлювати весь ланцюжок одразу:
git switch feat/ui
git rebase --update-refs main
--update-refs під час rebase верхньої гілки пересуває й проміжні гілки (feat/schema, feat/api) на переписані коміти. Можна увімкнути за замовчуванням: git config rebase.updateRefs true.
Після злиття нижнього PR наступний треба переспрямувати на main і перебазувати. Якщо низ вливали через squash, у верхніх гілках лишаються «старі» коміти, яких у main вже немає за SHA, - тут допомагає git rebase --onto main feat/schema feat/api.
Інструменти: GitHub має нативну підтримку стеків (на момент написання - у публічному попередньому перегляді) з каскадним rebase на сервері і розширенням gh stack; є також сторонні інструменти на кшталт Graphite чи ghstack.
Коли варто: довга фіча, яку природно ділити на шари, і швидкий потік рев'ю. Коли ні: незалежні зміни - їм не потрібен стек, достатньо окремих PR від main; повільне рев'ю - стек з п'яти PR, що висять тиждень, перетворюється на постійний rebase.
Незгоди - нормальна частина рев'ю. Проблема не в них, а в тому, що вони тягнуться днями в коментарях і псують стосунки.
Як розв'язувати незгоду:
- розділити факти й смак. Баг, вразливість, порушення домовленостей команди - аргумент. «Я б написав інакше» - ні, якщо рішення автора теж коректне;
- спиратися на спільні правила, а не на авторитет: стайлгайд, архітектурні рішення (ADR), домовленості команди. Якщо правила немає - це привід його створити, а не виграти суперечку;
- перейти в розмову: після двох-трьох раундів коментарів 10 хвилин дзвінка вирішують більше, ніж ще десять повідомлень. Підсумок - записати в PR;
- ескалація до техліда чи ширшого обговорення, якщо згоди немає, - це нормальний механізм, а не поразка;
- «не блокує, але»: рецензент може погодитися злити зараз і створити задачу на покращення - якщо проблема не критична.
Принцип рецензента: схвалювати зміну, яка покращує стан кодової бази, навіть якщо вона не ідеальна. Вимога досконалості зупиняє розробку.
Як рев'ю стає вузьким місцем:
- PR чекають рев'ю по кілька днів, автори перемикаються між задачами й втрачають контекст;
- рев'ю робить одна людина;
- великі PR, які ніхто не хоче брати.
Що допомагає:
- домовленість про швидкість відгуку: перша реакція протягом робочого дня; не обов'язково повне рев'ю, але хоча б «подивлюсь після обіду»;
- рев'ю - пріоритетна робота, а не те, що роблять «коли буде час»: незлитий PR - незавершена робота;
- розподіл рев'юерів - автоматичне призначення команді за CODEOWNERS чи round-robin, щоб навантаження не падало на одних і тих самих;
- маленькі PR - їх рев'юють швидко;
- автоматика знімає з людей стиль і очевидні помилки;
- вимірювати час до першого відгуку й до злиття - без цифр проблема помітна лише відчуттям;
- парне програмування для складних змін - рев'ю відбувається під час написання.
Докладніше в документації: Google: як працювати з запереченнями в рев'ю