Двухуровневый CI-процесс PHP-проекта
Непрерывная интеграция (CI, англ. Continuous Integration) — это практика разработки программного обеспечения, которая заключается в выполнении частых автоматизированных сборок проекта для скорейшего выявления и решения интеграционных проблем. Целей у непрерывной интеграции две:
Недопущение появления в репозитории кода, который не соответствует принятым стандартам качества. Чистый репозиторий — чистые помыслы;
Скорейшее выявление проблем качества и их устранение. Чем раньше выявил проблему — тем дешевле ее исправление.
На практике эти цели закрываются использованием статических анализаторов кода, тестами и их автоматизированным запуском.
Современные возможности статического анализа
На текущий момент в PHP‑мире список статических анализаторов кода довольно широк. Наиболее популярные утилиты статического анализа:
проверка кода на правила/стандарты его оформления. Например, ecs, phpcs;
исправление оформления кода в соответствии с принятыми в команде правилами/стандартами. Например, phpcbf;
проверка кода и зависимостей проекта на наличие известных уязвимостей. Например, security‑advisories;
выполнять поиск неиспользуемого или дублирующего кода. Например, phpmd, phpcpd;
поиск неявных ошибок в коде. Например, phpstan, phan, psalm;
и многое другое.
И главное, эти богатые возможности можно и использовать для улучшения качества кода своего проекта!
Проблематика
Обычно выполнение статического анализа кода выбранными инструментами происходит на сервере после обновления кода в удаленном репозитории. Особо ценны такие проверки при слиянии кода из одной ветки в другую перед выполнением командного ревью. Это централизованные проверки, позволяющие не допустить (в случае со слиянием) появления кода, не прошедшего все необходимые проверки в основной ветке. При таком подходе иногда возникают ситуации, когда при создании запроса на слияние двух веток запускаются автоматические проверки, которые выявляют какие‑то банальные недочеты в коде, которые мешают выполнения слияния, и которые необходимо устранить, например:
Обидно. Залил код, подождал, пока выполнится часть проверок, а в итоге получил ошибку. Конечно, выполняемые на сервере проверки перед созданием запроса на слияние хорошо бы выполнять на локальном компьютере. Но кто это делает? Единицы. Остальные надеются на собственную непогрешимость. Программисты же.
Для решения таких обидных недочетов, очевидно, необходимо чтобы проверки локально запускались автоматически. Отчасти это можно сделать настройкой собственной IDE на автозапуск инструментов статического анализа. Но такой подход имеет пару минусов:
требуется ручная настройки IDE каждым разработчиком, работающим над проектом;
IDE довольно много, и у каждого разработчика своя любимая среда разработки, поэтому разбираться, как настроить свою IDE придется в каждом частном случае.
Решение
Можно пойти другим путем и использовать возможности, которые нам дают инструменты, которые есть в арсенале каждого PHP‑разработчика — git и composer. Composer умеет запускать произвольные пользовательские скрипты по окончании выполнения команд install и update, а git имеет технологию git‑hooks. Если коротко, то это механизм запуска скрипта при наступлении какого‑то события в локальном гит‑репозитории. Событий довольно много, поэтому опишу самые полезные, на мой взгляд:
pre‑commit — выполняется перед фиксацией изменений и вызывается при git commit;
commit‑msg — выполняется при вызове команды git commit и позволяется отредактировать сообщение коммита;
pre‑push — выполняется перед отправкой изменений в удаленный репозиторий и вызывается при git push.
И эти возможности можно успешно эксплуатировать в наших целях.
В триггере pre‑commit целесообразно запускать инструменты статического анализа, которые исправляют код перед коммитом, например, приводят код в соответствие принятому стилю написания. В триггере commit‑msg можно добавлять название ветки, в которой выполняются изменения, например. В триггере pre‑push целесообразно запускать самые тяжелые и долго выполняющиеся инструменты статического анализа, поскольку разработчик выполняет git push сильно реже, чем git commit.
Осталось позаботиться о том, чтобы эти триггеры появлялись автоматически в локальном репозитории разработчика. Для этих целей можно использовать готовые пакеты, которые очень легко настраиваются. Например, brainmaestro/composer‑git‑hooks или captainhook/captainhook. Их принцип установки идентичен. Первичная настройка хуков на примере использования пакета brainmaestro/composer‑git‑hooks:
Конфигурируем в composer.json. В полях /scripts/post-install-cmd и /scripts/post-update-cmd указываем команды на установку хуков при выполнении composer install и composer update. В /extra/hooks/pre-commit и /extra/hooks/pre-push описываем команды, которые будут выполняться перед коммитом и пушем. Пример:
{
...,
"scripts": {
...,
"post-install-cmd": [
"php ./vendor/bin/cghooks add --git-dir=./.git"
],
"post-update-cmd": [
"php ./vendor/bin/cghooks update --git-dir=./.git"
]
},
"extra": {
"hooks": {
"config": {
"stop-on-failure": ["pre-commit", "pre-push"]
},
"pre-commit": [
"php vendor/bin/phpcbf --standard=phpcsconf.xml app"
],
"pre-push": [
"cd src",
"php vendor/bin/phpcs --standard=phpcsconf.xml app",
"php vendor/bin/phpstan analyze -c phpstan.neon",
"php artisan test"
]
}
}
}
Устанавливаем composer‑пакет brainmaestro/composer‑git‑hooks. После окончания установки хуки будут прописаны в в локальном гит‑репозитории в.git/hooks/pre‑commit и.git/hooks/pre‑push;
Готово.
Попробуем в деле. Для наглядности, внёс ошибку в файл, а также неверное форматирование. Некорректные изменения, который я попытаюсь зафиксировать, выглядят так:
То есть удалил необходимую зависимость — теперь при каждом http‑запросе вывалится эксепшн из‑за ее отсутствия. И также нарушил форматирование. Коммитим это добро:
Хоба, запустился линтер и форматирование поправил. Правда придется отредактировать коммит и включить изменения линтера:
Теперь попробуем запушить код, содержащий ошибку:
И штатно отработал phpstan, который говорит нам, что код следует поправить, и он не улетит в удаленный репозиторий.
Итог
Получился двухуровневый CI‑процесс — на первом уровне происходит контроль кодовой базы на рабочем компьютере разработчика, а на втором уровне происходит контроль на уровне удаленного репозитория. Отмечу также, что удаление одного из уровней контроля лишь снижает эффективность CI‑процесса, но лучше наличие хоть какого‑то CI, чем его полное отсутствие.
В итоге получаем код, удовлетворяющий нашим критериям качества. Проверка качества происходит автоматизировано. Лишний раз не запускаются ложные проверки качества на удаленном сервере при внесении изменений в удаленном репозитории. А мы вносим свой посильный вклад в декарбонизацию планеты, не заставляя железо лишний раз работать.