almdev Технический блог

Гейты: pint, phpstan, тесты и что стоит в хуках
Проверка, которая запускается по настроению, не запускается.

Одна команда вместо шести, форматирование в хуке за две секунды, базовая линия статического анализа с правилом «только уменьшается» и запрет на её правку через разрешения. Плюс история про хук с анализом, который я снял через неделю.

Подкатегория: Воркфлоу

Чтение
5 мин
Технологии / версии
Воркфлоу · PHP · Тесты · CI
Серия
Цикл «Воркфлоу» · часть 4 из 5
28 июл 2026 · 5 мин · 1 просмотр · AI
Одна команда проверок и её последовательные шаги
Воркфлоу 07/2026

Проверки, которые надо не забыть запустить, делятся на два вида: те, что запускаются, и те, что не запускаются. Разница не в дисциплине, а в том, сколько действий отделяет меня от результата.

Шесть команд подряд я не запускаю. Одну — запускаю всегда.

Гейт — это условие

Формулировка «перед отправкой прогони тесты и линтер» — пожелание. Оно не выполняется не потому, что я безответственный, а потому что в момент отправки я думаю о задаче, а не о списке проверок.

Гейт отличается тем, что его невозможно обойти незаметно. Он либо зелёный, либо красный, и красный виден.

Из этого следует требование к форме: гейт должен быть одной командой. Не набором из шести, каждую из которых надо помнить, а одной, у которой на выходе «прошло» или «не прошло».

Одна команда перед отправкой

В Symfony-проекте у меня это цель в Makefile, за которой стоит скрипт:

make check
# docker exec teamspace-app_webserver bash scripts/check.sh

Внутри восемь шагов, каждый со своим результатом:

── Twig lint          проверка шаблонов
── YAML lint          конфигурация и переводы
── Container lint     сервис-контейнер
── PHPStan            статический анализ
── PHPUnit            тесты
── JS bundle build    сборка фронта
── Bundle integrity   контроллеры, на которые ссылается сборка, существуют
── Translations       нет пропущенных ключей в en / ru / de

Скрипт написан так, что не останавливается на первом падении: помечает шаг как упавший и идёт дальше. В конце — общий вердикт. Это важнее, чем кажется: при остановке на первой ошибке я узнаю о проблемах по одной, и цикл «запустил — починил — запустил» растягивается на полчаса.

Два последних шага — самодельные. Проверка целостности сборки читает собранный файл и убеждается, что все контроллеры, на которые он ссылается, реально существуют. Проверка переводов ходит по трём локалям и считает пропущенные ключи. Оба появились после происшествий, оба ловят то, что тесты не ловят.

В блоге на Laravel набор скромнее: тесты и проверка форматирования. Проект меньше, фронт собирается отдельно, переводов нет.

Что стоит в хуке

Хук — это то, что запускается само, без моего участия. Требование к нему одно: он должен быть быстрым.

Мой порог — две секунды. Всё, что дольше, начинает раздражать, а раздражающий хук рано или поздно отключают.

В пределах двух секунд помещается форматирование изменённых файлов:

./vendor/bin/pint app/Models/Post.php

Именно изменённых, а не всего проекта. Прогон форматирования по всей кодовой базе занимает секунд двадцать и трогает файлы, которых я не касался.

Что в хук не ставлю: тесты и статический анализ. Оба слишком медленные, оба лучше запускать осознанно.

Хук со статическим анализом, который я снял

Историю стоит рассказать целиком, потому что она про соблазн.

Я поставил в хук статический анализ по изменённым файлам. Логика была понятная: ошибки типов ловятся сразу, а не через полчаса на полном прогоне.

Первые два дня — прекрасно. На третий начались раздражения: анализ по одному файлу даёт ложные срабатывания, потому что не видит контекста. Он ругался на вещи, которые в полном прогоне нормальны.

На пятый день выяснилось главное. Хук замедлил цикл настолько, что я начал делать более крупные правки — просто чтобы реже его ждать. То есть быстрый инструмент испортил мою собственную привычку резать работу на мелкие шаги.

Через неделю снял. Анализ вернулся в общую команду перед отправкой, где ему и место.

Базовая линия и правило «только уменьшается»

Статический анализ на живом проекте невозможно включить с нуля: на пятом уровне у меня было под две сотни замечаний в существующем коде. Разгребать их разом — отдельный проект.

Стандартный выход — базовая линия: файл с перечислением уже существующих замечаний, которые анализатор игнорирует.

includes:
    - phpstan-baseline.neon

parameters:
    level: 5
    paths:
        - src

Правило вокруг неё у меня одно: базовая линия только уменьшается. Новое замечание в неё не дописывается никогда. Если код упал в анализ — чинится код.

Правило звучит твёрдо, а держится на честном слове, потому что дописать строку в файл — секундное дело, и вывод после этого зелёный.

Запрет через разрешения

Поэтому запрет на правку базовой линии я вынес из текста задачи в конфигурацию разрешений.

Разница принципиальная. Запрет в задаче — это просьба, которая действует, пока её помнят. Запрет в конфигурации — это отказ инструмента выполнить действие.

Туда же вынесены отправка в удалённый репозиторий, жёсткий сброс и правка файлов окружения. Общее у этих четырёх: последствия наступают вне моего экрана и обнаруживаются поздно.

Пока правка базовой линии была запрещена словами, она случалась. После переноса в разрешения вопрос закрылся. Никакой мистики: возможность и намерение — разные вещи, и убирать надо возможность.

Гейт для автономного прогона

Отдельный случай — когда работа идёт без моего присмотра до самого пулл-реквеста.

Там гейт становится ещё и условием остановки. Формулировка примерно такая:

Остановись и опиши ситуацию, если:
- полный прогон проверок красный после двух попыток починить;
- задача выросла больше 600 строк диффа;
- потребовалось изменить схему данных сверх запланированного.

Первое условие — про то, что две неудачные попытки означают неверную гипотезу, а не невезение. Дальше нужен человек.

Без условий остановки автономный прогон либо доводит до зелёного любой ценой, либо крутится в цикле правок. Оба исхода я видел, оба выбрасывал целиком.

Что осталось

Схема простая: быстрое и бесспорное — в хук, всё остальное — в одну команду, необратимое — в разрешения.

Чего не хватает в блоге: своего полного прогона одной командой. Сейчас там две команды, тесты и форматирование, и я каждый раз пишу обе. Скрипт на десять строк напрашивается с весны, а не написан по единственной причине: две команды — это ещё не то раздражение, которое двигает.

Второе, к чему присматриваюсь, — проверка сборки фронта. Собранные стили в блоге не проверяются ничем, и один раз я выкатил страницу, где половина классов не попала в сборку. Ни тесты, ни линтер этого не видят. Скорее всего, следующая строка в списке гейтов будет именно про это.

Серия

Цикл «Воркфлоу»

#workflow #php #testy #ci #phpstan