Блог
Двенадцать прогонов публикатора подряд упали на проверке, которая каждый раз была права, а шесть смерженных pull request просидели шестнадцать часов невидимыми для читателя.
Домен t27.ai отдаёт не тот репозиторий, в котором сайт разрабатывается. Расписанный публикатор выкачивает исходники из gHashTag/trinity, собирает их, копирует результат поверх дерева apex-репозитория, прогоняет ту же проверку, что прогнал бы человек при ручной публикации, и коммитит только если проверка прошла. С 16:03 UTC 13 августа по 06:00 UTC 14 августа он запустился двенадцать раз и упал двенадцать раз — всегда на одной строке.
blog drift: the app ships [a-repair-reaches-only-the-copy-it-lands-in
a-suite-that-runs-nothing-exits-zero each-half-imported-the-other
eleven-verdicts-were-windows-not-checkpoints green-ci-does-not-mean-usable
open-gigabit-ethernet-artix7 scale-field-width-already-published]
but blog/ holds [тот же список без eleven-verdicts] — rerun build-blog.py
- **FAIL** verify-site.sh rejected the built tree — nothing pushed
Здесь верно каждое слово. Приложение действительно перечисляло семь постов, а статическое дерево действительно держало шесть, и пост, который приложение перечисляет, а дерево не содержит, — это HTTP 404 для любого читателя, который не исполняет JavaScript. Гейт нашёл настоящий дефект и отказался публиковать дерево с ним. Он для этого и стоит.
Статические страницы в blog/ генерирует build-blog.py из файла blog-posts.json, а blog-posts.json был снимком, который кто-то обновлял руками. Публикатор не вызывал ни того, ни другого. То есть в цепочке от смерженного поста до читаемой страницы было одно звено, которое мог закрыть только человек, и гейт стоял ниже этого звена по потоку.
Важно то, что из этого следует. Публикатор — одна последовательность: проверить, потом закоммитить, потом запушить. Красная проверка не пропускает блог и не публикует остальное; она не публикует ничего. Отсутствующая статическая страница одного поста задержала все мержи, которые случились после, — и ни один из них к блогу отношения не имел.
| Смержено в trinity | Что менял | Дошёл до читателя |
|---|---|---|
| #742, 15:12 UTC | тот самый пост блога, который уронил гейт | через 16 часов |
| #743, 15:17 UTC | убраны мёртвые ключи перевода и отключённые секции | через 16 часов |
| #745, 16:12 UTC | оглавление длинной страницы verification | через 15 часов |
| #747, 17:31 UTC | выровнен вертикальный ритм подстраниц | через 14 часов |
| #748, 18:15 UTC | страница автора переведена на de, es, zh | через 13 часов |
| #749, 20:08 UTC | матрица выбора формата на /#/select | через 11 часов |
Шестнадцать часов ответ на вопрос «смержено?» был «да», а ответ на вопрос «это кто-нибудь видит?» — «нет», и прибор был наведён только на первый из них. Панель зелёных мержей не врала — она отвечала на другой вопрос, не на тот, который имеет значение.
Теперь всей цепочкой владеет один скрипт, и публикатор вызывает его на каждом прогоне: транспилировать настоящий модуль постов из тех исходников, которые он только что собрал, записать blog-posts.json, пересобрать blog/ и ru/blog/ и оба фида, нарисовать карточку тому посту, у которого её нет, затем пересобрать карту сайта по тому, что лежит на диске. Гейт сравнения остался в точности каким был. Он по-прежнему — то, что поймает сломанный генератор; он больше не то, что ждёт человека.
Две детали в этом скрипте стоят там из-за того, как они ломаются, а не из-за того, как они работают. Экспортируемый publishedPosts — функция, а не массив: если сериализовать саму привязку, получится undefined, и на диск ляжет синтаксически корректный, полностью пустой файл данных без единой ошибки — то есть блог будет удалён, а не «не обновлён». И карточки были рукописным SVG, растрированным через qlmanage — утилиту macOS, которой нет ни на одном раннере, — так что беспилотная публикация никогда не смогла бы получить картинку, которую требует гейт; генератор рисует карточку напрямую, в единственном семействе шрифтов на раннере, которое покрывает кириллицу.
Обратное направление тоже подключено. Пост, отозванный в исходниках, теперь теряет и свой статический каталог, потому что тот же гейт падает и в другую сторону — когда дерево держит больше, чем отдаёт приложение, — а починка, закрывающая только случившееся направление, это половина починки.
Первый прогон после правки опубликовал текущие исходники, и apex теперь отдаёт седьмой пост по его собственному адресу на двух языках, с карточкой на диске. Это измеренный факт про один прогон. Это не доказательство, что в конвейере не осталось других ручных звеньев: в том же репозитории ещё три генератора запускаются руками, и единственная причина, по которой они не устроили простоя, — что сейчас от них не зависит ни один гейт.
Гейт полезен ровно настолько, насколько автоматично его лечение. «Верный и неавтоматизируемый» — комбинация хуже, чем «приблизительный и самовосстанавливающийся», потому что верный останавливает всё и сообщает вам, что он был прав.
Каждая цифра выше измерена, и рядом с ней названы её пределы.