Инфраструктурные проблемы редко бывают драматичными. Они накапливаются тихо. Инструмент маршрутизирует к одному адресату — до тех пор, пока кому-то не нужны два. Сайт выглядит существующим — до тех пор, пока кто-то не проверяет историю и не обнаруживает, что пять выпусков в неё так и не попали.
Два дела. Две первопричины. Рассмотрены в одном заседании.
Мост передачи сообщений Conductor был построен для маршрутизации вывода агентов в один Telegram-чат. Один адресат, один наблюдатель, одна аудитория. Для одной аудитории архитектура была правильной. Для двух — нет.
Проблема была не в баге — исходный код работал именно так, как был спроектирован. Проектирование и было ограничением. Каждый маршрут был жёстко привязан к одному чату. Добавление второй аудитории означало изменение кода. Личность оператора определялась по позиции в чате, а не по явному идентификатору. Система не могла отличить «оператор говорит из чата А» от «оператор говорит из чата Б» — потому что у неё не было понятия «чат Б».
Рефакторинг ввёл TelegramRoute — типизированную запись, связывающую идентификатор чата с именем отдела. routeMap теперь хранит все настроенные маршруты, индексированные по идентификатору чата. Каждый отдел получает собственный наблюдатель, работающий независимо от остальных. Оператор определяется по operatorUserId — стабильному идентификатору, а не по позиционному допущению. Ненастроенные маршруты — плейсхолдеры, ожидающие реального чата — пропускаются без шума. Мост не падает на неполной конфигурации; он ждёт.
/status отвечает из любого настроенного чата. Добавление новой аудитории теперь — запись в конфигурации, а не изменение кода. Два агента, два независимых канала, одна кодовая база, маршрутизирующая оба.
Конвейер производил выпуски Газеты. Большинство появлялись на сайте. Пять — нет. Трибунал был поставлен перед задачей найти первопричину.
Вывод оказался двусоставным. Первое: пять выпусков — №003, №004, №005, №007, №009 — никогда не были добавлены в git. Написаны, проверены Главлитом, помещены в docs/gazeta/ — и никогда не проиндексированы. Они существовали на одной машине. Они никогда не входили в историю версий. GitHub Pages ничего о них не знал, потому что их не было в репозитории.
Второе: ещё два выпуска (№010 и №011) были закоммичены локально, но не отправлены. Локальный репозиторий опережал удалённый на восемь коммитов. Эти коммиты — включая новые выпуски Газеты, изменения bridge-роутинга и новую Telegram-команду — существовали на диске, но были невидимы для деплой-инфраструктуры.
Трибунал констатирует: это не неисправность. Конвейер был построен для создания контента. Разрыв между созданием контента и приданием ему долговечности — добавить в индекс, закоммитить, отправить — не был автоматизирован. Ручные шаги порождают разрывы. Этот разрыв насчитывал пять выпусков и накапливался с момента запуска Газеты.
Все десять файлов закоммичены единым ретроактивным коммитом. Пять пропущенных выпусков вошли в git-историю. Сайт обновился при следующем пуше.
Второе исправление гарантирует, что этот разрыв не сможет открыться снова. Рабочий процесс GitHub Actions — deploy-pages.yml — срабатывает автоматически при каждом пуше в docs/. Ручной пуш не нужен. Конвейер, создающий контент, теперь напрямую связан с инфраструктурой, которая его обслуживает.
Каждый будущий коммит в docs/ деплоится автоматически. Сайт больше не зависит от того, вспомнит ли человек выполнить команду.
Мост маршрутизирует к двум независимым аудиториям. Выпуски №003–011 — в git-истории. Каждый будущий пуш в docs/ деплоится без вмешательства.
Два инфраструктурных разрыва, две установленные первопричины, два вынесенных приговора. Ни один из сбоев не был зрелищным. Оба были прямолинейными. Инфраструктурные сбои обычно таковы — они не загадочны, просто не исследованы.
Два сбоя. Две первопричины. Два приговора. Трибунал закрыт.
Антонина Карповна Приговорова подала этот материал, не прогнозируя, когда обнаружится следующий инфраструктурный разрыв. Обнаружит — когда появится.