Блог
Наш CLI не мог поставить никто, а CI, который должен был об этом сказать, имел ноль успехов из тридцати прогонов. Каждая причина прятала следующую; удаление четвёртой породило пятую.
До вчерашнего дня наш инструмент командной строки не мог поставить никто, включая нас. Не «было неудобно собрать» — `zig build` не мог его произвести из чистого клона ни на одной машине. Задача CI, существующая ровно чтобы об этом сообщить, падала тридцать прогонов подряд, что равносильно молчанию.
Пять причин, наложенных друг на друга. Каждая прятала следующую, поэтому любое исправление выглядело как несработавшее.
Сборка компилирует четыре GUI-цели, линкующие raylib. У раннера нет raylib и нет причин ему быть. В `build.zig` давно есть `-Dci=true`, пропускающий ровно эти цели; воркфлоу его никогда не передавал. Одно слово — а ошибка «unable to find dynamic system library» указывала на отсутствующий пакет, а не на отсутствующий флаг.
Второй воркфлоу ставил Zig через `brew install zig`, то есть текущий. Текущий — 0.16, а дерево под 0.15.2, который пинят остальные 28 воркфлоу. На 0.16 `build.zig` не компилируется вовсе: `linkLibC` переехал, а вендоренный пакет зовёт удалённые `std.mem.trimLeft` и `std.process.getEnvVarOwned`.
С запиненной версией сборка всё равно умирала — теперь на libSystem: `undefined _abort`, `_bzero`, `___availability_version_check`. Задача шла на `macos-latest`, чей SDK Zig 0.15.2 линковать не умеет. Тот же отказ воспроизводится на рабочем Mac с macOS 26 — по нему его и опознали. Перевели на ubuntu, где и так работают 116 из 127 задач репозитория.
И только тогда всплыла честная ошибка:
error: failed to check cache:
'trinity-nexus/output/lang/zig/full-serve-v1.zig' file_hash FileNotFound
Этот путь в gitignore, не отслеживается ничем и отсутствует во всех чекаутах и кэшах машины. Спека, названная его источником, тоже не существует — каталог с ней пуст. Артефакт нельзя было ни получить, ни перегенерировать, и сборка была невозможна ровно столько, сколько стояла эта ссылка.
И его никто не импортировал. Единственная ссылка в дереве — закомментированная строка. Удаление не стоило ни капли функциональности.
Удаление сломало сборку немедленно:
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` не пропускает опубликованный пост без квитанций или без раздела о недоказанном. Обе команды существуют потому, что обе ошибки сперва были сделаны здесь.
Каждая цифра выше измерена, и рядом с ней названы её пределы.