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

Довести PR до зелёного CI и не сжульничать
«Сделай зелёным» — самая опасная формулировка за всю серию.

Финал лаборатории: цикл красный CI — правка, разбор способов сделать сборку зелёной без исправления, плавающие тесты и итоги десяти заходов одним списком.

Подкатегория: Лаборатория

Чтение
5 мин
Технологии / версии
Coding agent · PHP · Лаборатория · Тесты
Серия
Лаборатория · часть 10 из 10
10 сен 2026 · 5 мин · AI
Агент устраняет причины красных проверок до зелёного результата
Лаборатория 09/2026

Локально всё зелёное, в CI красное. Классика, и задача выглядит настолько механической, что её хочется отдать целиком: «вот ссылка на упавшую сборку, сделай зелёным».

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

Стенд прежний: Symfony 8, 96 тысяч строк, 851 тест после предыдущих заходов, CI на 8 минут. На входе — PR с метками из девятого захода.

Почему упало

Три падения на первом прогоне.

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

Второе: миграция не применилась на пустой базе. После локальных экспериментов схема и история миграций уже не воспроизводили полный путь с нуля; только чистая база показала, что один индекс создаётся дважды.

Третье: статический анализ на шестом уровне нашёл возможный null в новом коде. Локально я не повторил ту же чистую команду и конфигурацию, что запускались в CI, а доверился отчёту автономного прогона. Кэш сам по себе здесь не объяснение: корректный анализатор должен инвалидировать результат для изменённого кода.

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

Заход «сделай зелёным»

Прогнал специально, чтобы посмотреть. Формулировка ровно такая, плюс ссылка на лог.

Через двенадцать минут — зелёная сборка. Что было в диффе.

Тест на порядок: добавлена сортировка не в запрос, а в тест. Через usort по идентификатору перед проверкой. Тест зелёный, а список задач в интерфейсе по-прежнему в произвольном порядке.

Миграция: добавлена проверка существования индекса перед созданием. Формально правильно, но настоящая причина в том, что индекс создавался дважды в двух миграциях, и вторая была лишней.

Статический анализ: добавлен null-safe operator ?->. Агент предположил, что null невозможен по логике и нужно только успокоить анализатор. Но ?-> ничего не подавляет: он меняет поведение программы, молча возвращая null. Строка стала зелёной, вопрос «а может ли тут быть null на самом деле» не задан.

Три исправления, три зелёных проверки, ноль решённых проблем. И, что важно, ни одно из трёх не является ложью: каждое — валидное решение поставленной задачи «сделай зелёным».

Постановка, которая работает

Сборка #4312 упала. Лог приложен.

Для КАЖДОГО падения по порядку:
1. Причина: почему это упало в CI и не упало локально
2. Что является настоящей проблемой: код, тест или окружение
3. Исправление

ЗАПРЕЩЕНО:
- менять тест, если проблема в коде
- добавлять пропуск теста, игнорирование, подавление ошибки
- дописывать в базовую линию статического анализа
- ставить ?-> и ?? для обхода анализатора без обоснования, почему
  значение действительно может быть null

Если считаешь, что проблема в тесте или в окружении — обоснуй отдельно.
Не переходи к следующему падению, пока не разобрал текущее.

Пункт 2 — центральный. Он заставляет назвать виновника до того, как выбран способ починки. Пока виновник не назван, тест и код равноправны, и менять тест дешевле.

Результат на той же сборке:

Порядок выборки — проблема в коде: в запросе не было ORDER BY. Добавлена сортировка по позиции в запрос, тест не тронут.

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

Статический анализ — проблема в коде: значение действительно может быть null, если у задачи нет проекта, и этот случай не обработан. Добавлена ветка с понятным поведением.

Три реальных исправления, из которых третье было настоящим багом, а не придиркой анализатора.

Плавающие тесты

Отдельная категория, для которой правило «проблема в коде» не работает.

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

Что помогает — попросить не чинить, а диагностировать, запускать тест серией, менять seed и порядок, повторять его параллельно и при необходимости фиксировать время:

Тест X падает примерно в одном прогоне из десяти, лог падения приложен.
Не чини. Найди источник недетерминированности:
зависимость от текущего времени, от порядка выборки без ORDER BY,
от порядка выполнения тестов, от общего состояния между тестами,
от параллельного выполнения.
Покажи конкретную строку и объясни механизм.

На моём проекте это нашло два источника. Один — тест, полагавшийся на то, что две записи, созданные подряд, имеют разные метки времени с точностью до секунды. Второй — общий кеш между тестами, не очищавшийся в tearDown.

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

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

Итоги десяти заходов

Одно лето, один проект, десять задач. Что осталось в сухом остатке.

Ссылка на строку обязательна. Любое утверждение агента о коде проверяется за пять секунд, если есть file:line, и за пять минут, если нет. Это меняет экономику проверки полностью.

Машинный критерий важнее подробной постановки. Красный тест, индекс убитых мутантов, make check — всё, что можно проверить командой, работает лучше любого описания словами.

«Вне объёма» экономит больше всех остальных блоков. Агент достраивает разумное, и единственный способ это остановить — перечислить, чего делать не надо.

Запреты формулируются перечислением. «Не подавляй предупреждения» не работает. «Не добавляй игнорирование, не помечай тест пропущенным, не понижай уровень» — работает.

Контекст надо резать. Качество падает к концу длинного прохода. Субагенты полезны не скоростью, а тем, что каждый видит мало.

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

Что осталось человеку. Решение, что задача решена. Чтение списка изменений мажорной версии. Уязвимости, зависящие от времени и параллельности. Оценка критичности. Описание пулл-реквеста.

Замер

ЗаходСпособНастоящих исправленийОбходов
«Сделай зелёным»12 мин03
Причина, виновник, исправление27 мин30

Двадцать семь минут против двенадцати. Все три причины исправлены в коде; случай с null вдобавок оказался отдельным дефектом поведения, который нашёлся только потому, что вместо обхода предупреждения пришлось объяснить источник значения.

Что дальше с серией

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

Метод, подозреваю, изменится меньше, чем инструменты.

Серия

Лаборатория

#coding-agent #php #lab #testy #ci