Непрерывная интеграция (CI, англ. Continuous Integration) — это практика разработки программного обеспечения, которая заключается в выполнении частых автоматизированных сборок проекта для скорейшего выявления и решения интеграционных проблем. Целей у непрерывной интеграции две:

  • Недопущение появления в репозитории кода, который не соответствует принятым стандартам качества. Чистый репозиторий — чистые помыслы;

  • Скорейшее выявление проблем качества и их устранение. Чем раньше выявил проблему — тем дешевле ее исправление.

На практике эти цели закрываются использованием статических анализаторов кода, тестами и их автоматизированным запуском.

Современные возможности статического анализа

На текущий момент в PHP‑мире список статических анализаторов кода довольно широк. Наиболее популярные утилиты статического анализа:

  • проверка кода на правила/стандарты его оформления. Например, ecs, phpcs;

  • исправление оформления кода в соответствии с принятыми в команде правилами/стандартами. Например, phpcbf;

  • проверка кода и зависимостей проекта на наличие известных уязвимостей. Например, security‑advisories;

  • выполнять поиск неиспользуемого или дублирующего кода. Например, phpmd, phpcpd;

  • поиск неявных ошибок в коде. Например, phpstan, phan, psalm;

  • и многое другое.

И главное, эти богатые возможности можно и использовать для улучшения качества кода своего проекта!

Проблематика

Обычно выполнение статического анализа кода выбранными инструментами происходит на сервере после обновления кода в удаленном репозитории. Особо ценны такие проверки при слиянии кода из одной ветки в другую перед выполнением командного ревью. Это централизованные проверки, позволяющие не допустить (в случае со слиянием) появления кода, не прошедшего все необходимые проверки в основной ветке. При таком подходе иногда возникают ситуации, когда при создании запроса на слияние двух веток запускаются автоматические проверки, которые выявляют какие‑то банальные недочеты в коде, которые мешают выполнения слияния, и которые необходимо устранить, например:

упавший pipeline из-за досадной ошибки.
упавший pipeline из-за досадной ошибки.

Обидно. Залил код, подождал, пока выполнится часть проверок, а в итоге получил ошибку. Конечно, выполняемые на сервере проверки перед созданием запроса на слияние хорошо бы выполнять на локальном компьютере. Но кто это делает? Единицы. Остальные надеются на собственную непогрешимость. Программисты же.

Для решения таких обидных недочетов, очевидно, необходимо чтобы проверки локально запускались автоматически. Отчасти это можно сделать настройкой собственной 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:

  1. Конфигурируем в 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"
            ]
        }
    }
}
  1. Устанавливаем composer‑пакет brainmaestro/composer‑git‑hooks. После окончания установки хуки будут прописаны в в локальном гит‑репозитории в.git/hooks/pre‑commit и.git/hooks/pre‑push;

  2. Готово.

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

некорректные изменения в коде.
некорректные изменения в коде.

То есть удалил необходимую зависимость — теперь при каждом http‑запросе вывалится эксепшн из‑за ее отсутствия. И также нарушил форматирование. Коммитим это добро:

консольный вывод команды автоматического форматирования кода.
консольный вывод команды автоматического форматирования кода.

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

коммитим исправленный код.
коммитим исправленный код.

Теперь попробуем запушить код, содержащий ошибку:

не получилось запушить код, содержащий ошибку.
не получилось запушить код, содержащий ошибку.

И штатно отработал phpstan, который говорит нам, что код следует поправить, и он не улетит в удаленный репозиторий.

Итог

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

В итоге получаем код, удовлетворяющий нашим критериям качества. Проверка качества происходит автоматизировано. Лишний раз не запускаются ложные проверки качества на удаленном сервере при внесении изменений в удаленном репозитории. А мы вносим свой посильный вклад в декарбонизацию планеты, не заставляя железо лишний раз работать.

Комментарии (8)


  1. dimas846
    03.02.2023 11:23
    +1

    Может ли, теоретически, возникнуть противоречие в проверке локально и на сервере? Когда код локально прошел проверку, а на сервере - нет?


    1. sunlynx
      03.02.2023 12:20

      Конечно может. Разработчик у себя локально может просто случайно оторвать pre‑commit и локальная проверка вообще не выполнится.


      1. dimas846
        03.02.2023 13:40

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


        1. Serganbus Автор
          03.02.2023 13:46
          +2

          На моей памяти такого не было. Если проверки и локально и на CI-сервере выполняются на основе одних конфигов, которые лежат вместе с кодом в репозитории, то такого быть не должно.


    1. olku
      04.02.2023 01:16

      Может, если у тулы есть кэш как, например, у phpstan и psalm. Кэш сильно помогает при большой кодовой базе, но вот такой побочный эффект скорости.


  1. FanatPHP
    03.02.2023 12:53
    +1

    Спасибо, очень хорошая статья, тут таких сильно не хватает.


    Можете только пояснить логику разбивки на pre-commit и pre-push? Разве не логично проверять синтаксис перед коммитом?


    1. Serganbus Автор
      03.02.2023 13:44
      +1

      Спасибо за вопрос.

      Исходил из того, что pre-commit вызывается чаще, чем pre-push, а значит и более затратные по времени инструменты должны вызываться именно в pre-push. В моем примере на pre-commit вызывается только принудительный форматтер кода по принятому стандарту. Выходит, в моей схеме на pre-commit проверка phpcs вызывается только для перестраховки.


  1. GinoPane
    05.02.2023 16:35

    С выводом не соглашусь, т.к. количество проверок только возрастает в итоге, анализаторы в репозитории надо оставлять в любом случае.

    Сам привык на пре-коммит настраивать только phpcs или аналог, чтобы явно видеть, где в чем ошибся. Когда автоматически все исправляется phpcbf, то не возникнет привычки писать сразу все правильно отформатированно.

    Насчет добавления установки хуков в команды композера - отличная идея????