Senior: питання на співбесіді з теми «Хуки, CI й автоматизація»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Ручний git bisect вимагає на кожному кроці перевірити код і сказати good чи bad. git bisect run робить це сам: на кожному кроці запускає скрипт і читає його код завершення.
git bisect start HEAD v2.3.0 # погана ревізія, потім добра
git bisect run php artisan test --compact --filter=VacancyExportTest
git bisect reset
На 1000 комітах це близько 10 запусків тесту замість ручного пошуку.
Як Git трактує код завершення:
| Код | Значення |
|---|---|
0 |
коміт добрий |
1-127, крім 125 |
коміт поганий |
125 |
коміт неможливо перевірити - пропустити (skip) |
інше (наприклад, 128 і більше) |
перервати bisect |
Скрипт для реальних умов - стара версія коду може потребувати іншої підготовки:
#!/bin/sh
# bisect.sh
composer install --no-interaction --quiet || exit 125 # не збирається - пропустити
php artisan migrate:fresh --env=testing --force --quiet || exit 125
php artisan test --compact --filter=VacancyExportTest
exit 125- коміт, на якому проєкт не збирається з не пов'язаних причин, не вважається «поганим» і не збиває пошук;- база - тестова (
--env=testing), щоб не зачепити робочу.
Тест, якого ще не було в історії: часто баг виявили сьогодні, а тест написали теж сьогодні - у старих комітах його немає. Рішення - тримати тест поза робочим деревом і копіювати його на кожному кроці:
cp /tmp/RegressionTest.php tests/Feature/RegressionTest.php
php artisan test --compact tests/Feature/RegressionTest.php
status=$?
rm tests/Feature/RegressionTest.php
exit $status
Корисні деталі:
git bisect start --first-parent- іти лише по комітах злиття основної гілки: знайти, який PR зламав, без заходу в проміжні коміти гілок;git bisect logіgit bisect replay- зберегти й повторити сесію;- терміни
good/badможна замінити (--term-old=fast --term-new=slow) - bisect підходить і для пошуку регресії продуктивності, не лише помилки.
Серверні хуки виконуються на сервері, що приймає push, і можуть відхилити його. На відміну від клієнтських, їх не можна пропустити через --no-verify - розробник їх не контролює.
Основні серверні хуки:
| Хук | Коли | Що може |
|---|---|---|
pre-receive |
один раз на весь push, до оновлення посилань | відхилити весь push |
update |
для кожного оновлюваного посилання окремо | відхилити окрему гілку чи тег |
post-receive |
після успішного оновлення | сповіщення, запуск деплою; відхилити вже не може |
pre-receive отримує на стандартний ввід рядки <старий SHA> <новий SHA> <посилання>:
#!/bin/sh
zero=0000000000000000000000000000000000000000
while read old new ref; do
# заборонити force push у main: старий коміт має бути предком нового
if [ "$ref" = "refs/heads/main" ] && [ "$old" != "$zero" ]; then
if ! git merge-base --is-ancestor "$old" "$new"; then
echo "Force push у main заборонено"
exit 1
fi
fi
done
Вивід хука повертається клієнту з префіксом remote:.
Де це доступно:
- власний сервер Git, GitLab self-managed, GitHub Enterprise Server - так;
- github.com - власні серверні хуки встановити не можна. Їхню роль виконують rulesets (заборона force push, обов'язкові підписи, обмеження на шляхи й розмір файлів) і push protection для секретів.
Серверні хуки проти CI:
| Серверний хук | CI | |
|---|---|---|
| коли | синхронно, під час push | асинхронно, після push |
| результат | push відхилено, коду на сервері немає | код уже на сервері, PR позначено червоним |
| час | секунди - інакше push «зависає» | хвилини, без обмежень |
| що перевіряти | політики: формат, заборонені файли, права, підписи | збирання, тести, аналіз |
Правило: у хуки - швидкі й безумовні політики, порушення яких не має з'явитися на сервері навіть у гілці (секрети, величезні бінарні файли, заборонені операції). Усе, що довге чи потребує середовища, - в CI. Хук, що запускає тести, робить кожен push хвилинним і ламає роботу всієї команди при збоях.
Якщо повідомлення комітів дотримуються Conventional Commits, з історії можна автоматично визначити, яким має бути наступний номер версії і що писати в змінлог.
Правила семантичного версіонування з комітів:
| Коміти з останнього релізу | Нова версія |
|---|---|
лише fix: |
патч: 1.4.2 → 1.4.3 |
є feat: |
мінорна: 1.4.2 → 1.5.0 |
feat!: чи футер BREAKING CHANGE: |
мажорна: 1.4.2 → 2.0.0 |
chore:, docs:, test: |
релізу не потребують |
release-please (від Google) працює через релізний pull request:
- після кожного злиття в
mainдія аналізує нові коміти; - відкриває чи оновлює PR «chore(main): release 1.5.0» зі зміненим
CHANGELOG.mdі номером версії; - команда зливає цей PR, коли хоче випустити реліз;
- дія створює тег
v1.5.0і GitHub Release з нотатками.
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v5
with:
release-type: php
semantic-release - альтернатива без проміжного PR: реліз публікується одразу після злиття. Більше автоматизації, менше контролю над моментом релізу.
Що потрібно для роботи:
- дисципліна повідомлень - перевірка в
commit-msgі в CI; при злитті через squash - перевірка заголовка PR, бо він стає повідомленням коміту; - осмислені коміти:
fix: typoв історіїmainпотрапить у змінлог; - права боту на запис і створення PR; якщо релізний PR має запускати CI, потрібен токен GitHub App чи PAT - події від стандартного
GITHUB_TOKENне запускають інші workflow.
Обмеження: змінлог з комітів - для розробників. Для користувачів продукту часто потрібні людські нотатки до релізу, і автоматичний текст - лише чернетка для них.
У великому проєкті з десятками залежностей бот, що відкриває окремий PR на кожне оновлення, швидко стає шумом, який команда ігнорує. Мета - щоб рутинні оновлення проходили без людей, а увагу отримували лише ризиковані.
Renovate - гнучкіша альтернатива Dependabot: підтримує більше екосистем, складні правила, групування, автозлиття й «панель залежностей» (Dependency Dashboard) - задачу зі списком усіх очікуваних оновлень.
{
"extends": ["config:recommended"],
"schedule": ["before 6am on monday"],
"minimumReleaseAge": "3 days",
"lockFileMaintenance": { "enabled": true },
"packageRules": [
{
"matchUpdateTypes": ["patch", "minor"],
"matchCurrentVersion": "!/^0/",
"automerge": true
},
{
"matchPackageNames": ["laravel/**", "livewire/**", "filament/**"],
"groupName": "Laravel ecosystem"
},
{
"matchDepTypes": ["require-dev"],
"groupName": "dev dependencies",
"automerge": true
},
{
"matchUpdateTypes": ["major"],
"dependencyDashboardApproval": true
}
]
}
Що тут відбувається:
- автозлиття патчів і мінорних версій стабільних пакетів - якщо CI зелений, PR вливається сам;
- групи - пов'язані пакети оновлюються разом (екосистема Laravel), а не десятком окремих PR;
- мажорні версії - лише після схвалення на панелі, бо вони потребують міграції коду;
minimumReleaseAge- не брати версію, опубліковану щойно: зламані й скомпрометовані релізи зазвичай відкликають за перші дні;lockFileMaintenance- періодично оновлює транзитивні залежності вcomposer.lock, які інакше застарівають непомітно;- розклад - PR з'являються в певний час, а не протягом усього тижня.
Передумови для автозлиття:
- тести, яким можна довіряти - інакше автозлиття постачає баги в
main; - обов'язкові перевірки в захисті гілки - бот не повинен мати змоги злити червоний PR;
- моніторинг після деплою - швидко помітити регресію й відкотити.
Dependabot чи Renovate: Dependabot вбудований у GitHub і простіший у налаштуванні (групи, cooldown, ігнорування є й у ньому); Renovate виграє на складних правилах, монорепозиторіях і поза GitHub (GitLab, Bitbucket).