Пайплайн в облаке идёт восемь минут. За это время я успеваю переключиться на другую задачу, и возврат к упавшей сборке стоит дороже самой ошибки. Быстрые локальные шаги укладываются примерно в минуту, полный прогон — в 2 минуты 40 секунд.
Одна цель в Makefile закрыла для меня этот разрыв.
Состав проверки
.NOTPARALLEL: check
check: lint-container lint-twig phpstan test build-assets translations ## Полный прогон проверок
@echo "Все проверки пройдены"Шесть шагов, и порядок в них важнее содержания. .NOTPARALLEL не даёт запуску с make -j перемешать их.
Линт контейнера — проверка синтаксиса compose-конфигурации и разрешения переменных, без сборки образов. Занимает три секунды и ловит опечатку до того, как её найдёт первый тест.
Линт шаблонов — синтаксис Twig. Секунда, ловит незакрытый блок.
Статический анализ — самый долгий из быстрых шагов, около сорока секунд.
Тесты — полторы минуты.
Сборка ассетов — двадцать секунд. Нужна, потому что ошибка сборки не видна ни одному из предыдущих шагов, а на проде она означает страницу без стилей.
Переводы — проверка того, что нет ключей без перевода.
Дешёвое идёт первым. Смысл в том, чтобы ошибка в конфигурации уронила проверку через три секунды, а не через две минуты после прогона тестов.
Жизнь с базовой линией
Статический анализ на существующем проекте выдаёт тысячи замечаний. Исправить их все — работа на недели, которая никому не нужна.
Базовая линия — это снимок существующих проблем, которые игнорируются:
php vendor/bin/phpstan analyse --generate-baselineПоявляется файл с перечислением всех текущих ошибок. Дальше анализ проходит чисто, но только для нового кода.
У меня в одном проекте базовая линия на 2100 записей. Это не долг, который я собираюсь закрыть — это граница между «унаследованное» и «моё».
Ключевое правило: базовая линия только уменьшается. Никогда не перегенерируется целиком.
Соблазн перегенерировать возникает регулярно: анализ ругается на что-то в коде, который я трогаю, и проще добавить это в исключения. Это ровно тот момент, когда механизм превращается в способ не чинить ошибки.
В обычном прогоне изменение базовой линии запрещено целиком:
phpstan-baseline-check: ## Baseline меняется только отдельным коммитом
@git diff --quiet HEAD -- phpstan-baseline.neon || \
(echo "phpstan-baseline.neon изменён"; exit 1)Это не пытается сравнивать количество строк: такая проверка пропустит новую ошибку, если одновременно исчезла старая. Уменьшение baseline я делаю отдельным коммитом, где изменение видно в диффе.
Уровень анализа я поднимал постепенно. Начал с четвёртого, сейчас шестой. Каждое повышение — это новая порция замечаний, часть из которых чинится, часть уходит в базовую линию, которая на время растёт. Это единственный случай, когда рост допустим, и он делается отдельным коммитом с понятным сообщением.
Гейт на переводы
Проект на трёх языках. Ключ, добавленный в русский файл и забытый в немецком, на странице выглядит как сам ключ: issue.status.in_review посреди интерфейса.
Проверка сравнивает наборы ключей:
$reference = $this->keysOf('messages+intl-icu.ru.yaml');
foreach (['en', 'de'] as $locale) {
$missing = array_diff($reference, $this->keysOf("messages+intl-icu.{$locale}.yaml"));
if ($missing !== []) {
$io->error(sprintf('%s: нет перевода для %d ключей', $locale, count($missing)));
$io->listing(array_slice($missing, 0, 20));
return Command::FAILURE;
}
}Русский — эталонный набор, остальные сверяются с ним. Обратная проверка тоже нужна: ключ, оставшийся в английском после удаления из русского, — это мусор.
Гейт жёсткий: непереведённый ключ роняет проверку. Мягкое предупреждение не работает, я пробовал — предупреждения накапливаются и перестают читаться.
Есть исключение, о котором стоит сказать честно: для нового ключа приходится сразу писать три перевода, включая тот язык, которым не владеешь. Немецкий у меня машинный с последующей вычиткой раз в релиз. Плохо, но лучше, чем ключ вместо текста.
Что оставить облаку
Дублировать пайплайн полностью бессмысленно. Локальная проверка нужна для быстрого цикла, облачная — для дорогих сценариев, которые я не запускаю локально регулярно.
В облаке у меня остаётся:
- прогон на чистой установке зависимостей — локально они закешированы и могут быть старее;
- матрица версий PHP, если проект поддерживает больше одной;
- тесты, требующие сервисов, которые локально я не поднимаю;
- сборка образа и проверка, что он вообще собирается;
- проверка того, что миграции применяются на пустой базе с нуля.
Последний пункт самый ценный. Локально база живая, миграции на неё накатывались постепенно. Развёртывание с нуля ломается по-другому и ловится только чистым прогоном.
Хук перед коммитом
Отношение у меня к нему двойственное.
Что там работает хорошо: линтер стиля кода с автоисправлением. Быстро, не мешает, убирает шум в диффах.
#!/bin/sh
vendor/bin/pint --dirty || exit 1Флаг --dirty обрабатывает только изменённые файлы. В этом проекте первый запуск без него переформатировал бы около четырёхсот файлов.
Что там работает плохо: всё, что дольше двух секунд. Хук со статическим анализом я поставил и снял через неделю. Он превращает коммит в ожидание, и результат предсказуем — начинаешь коммитить с флагом обхода проверок, а потом делаешь это всегда.
Правило простое: в хуке живёт только то, что укладывается в две секунды. Остальное — в make check, который я запускаю перед push осознанно.
Цифры
Локальный прогон — 2 минуты 40 секунд, из них половина на тесты. Облачный — 8 минут.
За полгода облачный пайплайн падал 14 раз. Из них 11 падений локальная проверка поймала бы — просто я её не запустил перед push. Три остальных — это чистая установка зависимостей и сборка образа, то есть ровно те дорогие сценарии, которые я не запускаю локально регулярно.
Соотношение хорошо описывает пользу: почти всё, что ломается, ломается предсказуемо и ловится за минуты до отправки.