almdev Разработка с ИИ

Локальный CI: одна команда вместо надежды на облако
Быстрые ошибки видны локально за минуту, полный прогон занимает 2 минуты 40 секунд.

Цель check, повторяющая пайплайн: порядок проверок, жизнь с базовой линией статического анализа, гейт на переводы и что имеет смысл оставить облачному CI.

Подкатегория: Инфраструктура

Чтение
5 мин
Технологии / версии
Тесты · CI · PHPStan · Качество
18 сен 2026 · 5 мин · 11 просмотров · Архив
Локальная проверка проекта перед отправкой изменений
Инфраструктура 09/2026

Пайплайн в облаке идёт восемь минут. За это время я успеваю переключиться на другую задачу, и возврат к упавшей сборке стоит дороже самой ошибки. Быстрые локальные шаги укладываются примерно в минуту, полный прогон — в 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. Три остальных — это чистая установка зависимостей и сборка образа, то есть ровно те дорогие сценарии, которые я не запускаю локально регулярно.

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

#testy #ci #phpstan #kachestvo