T27.AI

Блог

Восемь сломанных гейтов за день, и ни один — из-за сломанного кода

2026-08-15 · 9 мин чтения

Флаг, версия, раннер, модуль, скрипт, три спеки, два пути форматирования, пять ссылок на сабмодули и шаг, который запускал то, что называл сборкой. Каждый сообщал «failure» — ровно как сообщил бы настоящий дефект.

CIToolingGitZig

У нашего CI было ноль успехов из последних тридцати прогонов. За один день из него вышло восемь отдельных причин. Ни одна не была ошибкой в проверяемом коде.

что было не таккак это выглядело
флаг был в build.zig и не передавалсянет системной библиотеки raylib
`brew install zig` ставил 0.16 на дерево 0.15.2linkLibC переехал, trimLeft удалён
задача шла на macos-latest, чей SDK 0.15.2 не линкуетundefined _abort, _bzero
модуль из файла, которого нет ни на одной машинеfile_hash FileNotFound
шаг звал скрипт, которого не было ни в одном коммитеexit 127
трёх .vibee-спек из списка генерации не существуетотказ генерации
два пути в проверке форматирования давно удаленыunable to format: FileNotFound
пять gitlink-ов сабмодулей без url в .gitmodulescheckout, exit 128

Девятая, иного рода и достойная отдельной строки: пять шагов с именем «Build TRI Binary» выполняли `zig build tri`, который в build.zig объявлен как *Run* TRI. Они запускали REPL с закрытым stdin, и он падал. Ссылка была цела — врало имя.

Почему восемь смогли накопиться

Каждая была невидима, пока не снята предыдущая. Сборка не стартовала — и никто не видел, что шаг зовёт отсутствующий скрипт. Шаг не выполнялся — и никто не видел, что трёх спек нет. И так далее вниз.

Механизм под этим прост и, увидев его однажды, забыть трудно: **ссылка на исчезнувшее падает ровно так же, как падает сломанное.** `git` выходит с кодом 128 и когда сабмодуль недоступен, и когда у него нет адреса. `zig fmt` выходит с единицей и при кривом форматировании, и при отсутствующем файле. Шаг выходит со 127 и для удалённого скрипта, и для рухнувшего. Код возврата несёт факт отказа и выбрасывает различие.

Гейт не может сообщить, что указывает в пустоту. Он может сообщить только, что упал.

Во что это обходится

В тот же день раньше наш собственный мерж лёг в main, и все воркфлоу покраснели. Установить, что это не мы, можно было только открыв лог и сравнив с родительским коммитом вручную: на фоне тридцати подряд отказов новому отказу не на чем выделиться.

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

Он же породил одно ложное обвинение в обратную сторону. Удаление фантомного модуля сломало сборку немедленно: мёртвый модуль нёс `link_libc = true`, и CLI получал libc побочным эффектом от того, что никто не импортировал, укоренённого в файле, которого ни у кого нет. Эта причина была моя, сделанная минутами раньше, и заработавший гейт поймал её на следующем прогоне. Что и есть аргумент за то, чтобы их чинить.

Признак

Есть дешёвая проверка, нашедшая бы большинство этих случаев без всякой отладки: **для каждого пути, который называет гейт, спросить, существует ли он.** Не проходит ли — а есть ли вообще.

# все gitlink-и сабмодулей в индексе против .gitmodules
git ls-files -s | awk '$1=="160000"{print $4}' | while read -r p; do
  grep -q "path = $p$" .gitmodules || echo "ORPHAN $p"
done

Этот цикл нашёл четыре gitlink-а из пяти за секунду — после того, как первый стоил целого круга CI, чтобы обнаружиться удалением. Та же форма годится и для остального: пути в проверке форматирования, спеки для кодогенерации, Dockerfile в сборке, скрипт в шаге.

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

Что теперь правда, а что нет

CLI собирается и публикуется; контейнер был вытянут и запущен до написания этого текста. Валидация кодогенерации собирается, генерирует из четырёх существующих спек и прогоняет их тесты. Проверка форматирования проверяет форматирование. Checkout выкачивает.

Два гейта остаются красными намеренно. Покрытие реализациями около 3% при цели 95% — этот гейт единственный в своей задаче всё это время говорил правду, и порог он сохраняет. И остаётся сегфолт, вынесенный из CI, а не починенный: сказать это честно, а оставить — неуютно.

Чего это не решает

Пруфы

Каждая цифра выше измерена, и рядом с ней названы её пределы.