Проверки, которые надо не забыть запустить, делятся на два вида: те, что запускаются, и те, что не запускаются. Разница не в дисциплине, а в том, сколько действий отделяет меня от результата.
Шесть команд подряд я не запускаю. Одну — запускаю всегда.
Гейт — это условие
Формулировка «перед отправкой прогони тесты и линтер» — пожелание. Оно не выполняется не потому, что я безответственный, а потому что в момент отправки я думаю о задаче, а не о списке проверок.
Гейт отличается тем, что его невозможно обойти незаметно. Он либо зелёный, либо красный, и красный виден.
Из этого следует требование к форме: гейт должен быть одной командой. Не набором из шести, каждую из которых надо помнить, а одной, у которой на выходе «прошло» или «не прошло».
Одна команда перед отправкой
В 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 строк диффа;
- потребовалось изменить схему данных сверх запланированного.Первое условие — про то, что две неудачные попытки означают неверную гипотезу, а не невезение. Дальше нужен человек.
Без условий остановки автономный прогон либо доводит до зелёного любой ценой, либо крутится в цикле правок. Оба исхода я видел, оба выбрасывал целиком.
Что осталось
Схема простая: быстрое и бесспорное — в хук, всё остальное — в одну команду, необратимое — в разрешения.
Чего не хватает в блоге: своего полного прогона одной командой. Сейчас там две команды, тесты и форматирование, и я каждый раз пишу обе. Скрипт на десять строк напрашивается с весны, а не написан по единственной причине: две команды — это ещё не то раздражение, которое двигает.
Второе, к чему присматриваюсь, — проверка сборки фронта. Собранные стили в блоге не проверяются ничем, и один раз я выкатил страницу, где половина классов не попала в сборку. Ни тесты, ни линтер этого не видят. Скорее всего, следующая строка в списке гейтов будет именно про это.