T27.AI

Блог

Пять причин, по которым сборка была красной, и пятая — моя

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

Наш CLI не мог поставить никто, а CI, который должен был об этом сказать, имел ноль успехов из тридцати прогонов. Каждая причина прятала следующую; удаление четвёртой породило пятую.

CIZigBuild systemsTooling

До вчерашнего дня наш инструмент командной строки не мог поставить никто, включая нас. Не «было неудобно собрать» — `zig build` не мог его произвести из чистого клона ни на одной машине. Задача CI, существующая ровно чтобы об этом сообщить, падала тридцать прогонов подряд, что равносильно молчанию.

Пять причин, наложенных друг на друга. Каждая прятала следующую, поэтому любое исправление выглядело как несработавшее.

1. Флаг, который был и который не передавали

Сборка компилирует четыре GUI-цели, линкующие raylib. У раннера нет raylib и нет причин ему быть. В `build.zig` давно есть `-Dci=true`, пропускающий ровно эти цели; воркфлоу его никогда не передавал. Одно слово — а ошибка «unable to find dynamic system library» указывала на отсутствующий пакет, а не на отсутствующий флаг.

2. Версия, которую не пинили

Второй воркфлоу ставил Zig через `brew install zig`, то есть текущий. Текущий — 0.16, а дерево под 0.15.2, который пинят остальные 28 воркфлоу. На 0.16 `build.zig` не компилируется вовсе: `linkLibC` переехал, а вендоренный пакет зовёт удалённые `std.mem.trimLeft` и `std.process.getEnvVarOwned`.

3. Раннер, чей SDK слишком новый

С запиненной версией сборка всё равно умирала — теперь на libSystem: `undefined _abort`, `_bzero`, `___availability_version_check`. Задача шла на `macos-latest`, чей SDK Zig 0.15.2 линковать не умеет. Тот же отказ воспроизводится на рабочем Mac с macOS 26 — по нему его и опознали. Перевели на ubuntu, где и так работают 116 из 127 задач репозитория.

4. Модуль из файла, которого нет нигде

И только тогда всплыла честная ошибка:

error: failed to check cache:
  'trinity-nexus/output/lang/zig/full-serve-v1.zig' file_hash FileNotFound

Этот путь в gitignore, не отслеживается ничем и отсутствует во всех чекаутах и кэшах машины. Спека, названная его источником, тоже не существует — каталог с ней пуст. Артефакт нельзя было ни получить, ни перегенерировать, и сборка была невозможна ровно столько, сколько стояла эта ссылка.

И его никто не импортировал. Единственная ссылка в дереве — закомментированная строка. Удаление не стоило ни капли функциональности.

5. Та, что устроил я

Удаление сломало сборку немедленно:

src/tri/env_loader.zig:8:12: error: dependency on libc must be explicitly specified in the build command

Мёртвый модуль нёс `.link_libc = true`. CLI действительно нужен libc, и он получал его побочным эффектом от модуля, который никто не использовал, укоренённого в файле, которого ни у кого нет. Объявить зависимость там, где ей место, — одна строка; и она стала видна только потому, что маскировавшее её исчезло.

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

Чего стоит вечно красная проверка

Проверка, которая всегда красная, несёт ровно столько же информации, сколько всегда зелёная.

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

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

Теперь он ставится

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

docker pull --platform linux/amd64 \
  ghcr.io/ghashtag/trinity:c7689530274d706fb0876b41e3ec0671ae16960d

docker run --rm --platform linux/amd64 \
  ghcr.io/ghashtag/trinity:c7689530274d706fb0876b41e3ec0671ae16960d blog

Две детали, которые открываются только исполнением. Образ только amd64, поэтому обычный pull на Apple Silicon падает с «no matching manifest for linux/arm64/v8». И закрепляйте sha, а не `:latest`: два пуша с разницей в пятнадцать секунд оба опубликовались, тег достался более раннему, и в `:latest` сейчас нет свежей подкоманды, а в sha-теге она есть.

Запускается там `tri` — инструмент, которым написан этот блог. `tri blog check` перепроверяет по API GitHub каждое состояние pull request, названное в посте, потому что состояние, записанное прозой, устаревает после публикации; `tri blog lint` не пропускает опубликованный пост без квитанций или без раздела о недоказанном. Обе команды существуют потому, что обе ошибки сперва были сделаны здесь.

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

Пруфы

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