ADR 0160 — Compose установщика едет вместе с ним, как подписанный asset
Статус
Accepted. Решено сопровождающим 2026-09-13 (AT-026). Расширяет
ADR 0149 — подписанный
checksums.txt начинает покрывать не только бинарники — и закрывает пробел
ADR 0150, который спроектировал установщик
одной строкой, не сказав, как compose установки попадёт на машину того, кто
устанавливает. Ни один из них не редактируется; этот документ на них ссылается.
Контекст
E2E установщика на чистой машине впервые прошёл в сессии 8 ФАЗЫ 30
(RN-549) и ответил нет на вопрос, ради
ответа на который существовал. install.sh поднимал стек через
docker compose -f docker/docker-compose.install.yml — путь, относительный
каталогу, откуда он запускался, — использованный в семи местах, включая
up -d --wait. Этот файл:
- не был asset Release;
- не входил в подписанный
checksums.txt; - нигде не скачивался — единственными загрузками скрипта были
cosign, манифест и его подпись,images.jsonи бинарник runner.
А compose bind-монтирует два файла относительно себя: ./postgres/init.sql
(postgres, создающий расширение pgvector) и ./ollama/pull-models.sh
(ollama-model-loader, под profile llm). Обходной шаг в install-e2e.yml
приносил ровно эти три из тега, через raw.githubusercontent.com.
Симптом был поздним, и именно это делало его дорогим. Тот, кто следовал
однострочнику из runbook в пустой папке, умирал на "no such file or
directory" после того, как скрипт проверил собственную подпись, определил
машину, спросил базу и записал .env с пятью секретами. Половина
установки и сообщение, которое не говорило, что делать.
Измерено при реализации: их четыре, а не три. Путь МИГРАЦИИ
(migrar_instalacao_anterior, RN-530)
запускает bash docker/backup/test-restore-compose.sh — ещё один
относительный путь и скрипт, исполняемый на хосте. E2E никогда до него не
доходил, потому что на чистой машине мигрировать нечего, и поэтому обход его
не приносил. В папке без checkout миграция умерла бы там — до того, как
что-либо удалить, по построению RN-530, но умерла бы.
Ни один слой ниже E2E не мог этого увидеть: каждая деталь работала и имела свой набор тестов. Дефект был между опубликованным артефактом и машиной того, кто устанавливает, — это граница, которую пересекает только E2E, а он запускается только на теге.
Решение
1. Файлы становятся assets Release и входят в ТОТ ЖЕ подписанный
checksums.txt. Четыре, с именами, которые не сталкиваются:
| asset в Release | путь в репозитории и в папке установки | почему |
|---|---|---|
brabo-install-compose.yml | docker/docker-compose.install.yml | compose, поднимающий установку |
brabo-install-postgres-init.sql | docker/postgres/init.sql | bind-монтируется postgres |
brabo-install-ollama-pull-models.sh | docker/ollama/pull-models.sh | bind-монтируется ollama-model-loader |
brabo-install-backup-test-restore.sh | docker/backup/test-restore-compose.sh | доказательство восстановления при миграции |
Release плоская — asset это имя, без папки, — а манифест индексирован по
имени. init.sql и pull-models.sh достаточно общие, чтобы столкнуться со
следующим asset, который кто-нибудь приложит, а два файла с одинаковым именем
были бы одной строкой, и проверка одного проходила бы против хеша другого.
Поэтому префикс brabo-install-, который также не совпадает с
--pattern 'brabo-runner-*', которым job checksums скачивает бинарники.
Таблица живёт в scripts/ci/assets-do-instalador.ts (кто ПУБЛИКУЕТ) и в
case функции destino_do_asset_do_instalador в install.sh (кто
СКАЧИВАЕТ). Их две, потому что установщик работает с bash 3.2 на macOS и без
Node; расхождение проваливает assets-do-instalador.spec.ts, который читает
case из shell. Тот же spec ВЫВОДИТ из compose каждый bind-mount ./… и
проваливает то, чего нет в таблице, — ровно так дефект и родился: кто-то
добавил mount, и никто не спросил, как он поедет.
2. Публикует job checksums из build-runner-binaries.yml, тот же,
что уже публикует install.sh: файлы берутся из checkout тега (тот же
коммит, что произвёл всё остальное, что там подписывается), копируются под
именем asset скриптом, входят в sha256sum, генерирующий манифест, и
прикладываются вместе. Ничего не меняется в ожидании assets из
RN-565: эти приходят не из матрицы, поэтому
ждать их нечего. Job начинает также проверять каждую строку манифеста
против файла (sha256sum -c --strict) после проверки подписи и перед
приложением — подпись доказывает, кто написал манифест; проверка доказывает,
что прикладываемое — это то, что он говорит.
3. install.sh скачивает и проверяет все четыре ДО того, как что-либо
спросить или записать. Сразу после verificar_a_si_mesmo и против ТОГО ЖЕ
checksums.txt, который эта функция уже проверила (никогда второй загрузки, по
причине, которую она записывает). Сравнение — по точному равенству поля
(awk), а не по шаблону: в имени есть точки. Три отказа, названных и
различных, потому что требуют разных действий — Release не публикует asset
(она старше этого ADR или вышла наполовину), манифест его не покрывает, хеш не
совпадает (инцидент). Все говорят "Nada foi gravado", и это правда: до этого
момента писали только во временный каталог.
4. Никогда не откатывается к относительному пути. COMPOSE_DE_INSTALACAO
перестаёт быть константой 'docker/docker-compose.install.yml' и рождается
пустой; она получает значение — абсолютный путь — только когда
materializar_os_arquivos_da_instalacao копирует проверенные копии в
<pasta de onde o script roda>/docker/, ту же папку, что .env. Это
происходит в первый же момент, когда файл нужен (миграция или подъём сразу
после .env), и никогда раньше: путь без TTY по-прежнему ничего не
записывает. То, что уже есть в месте назначения, заменяется, никогда не
читается — при обновлении это compose предыдущей версии, — с rm перед
копированием, чтобы symlink не увёл запись в другое место. Раскладка под
папкой намеренно та же, что и раньше: относительные bind-mounts compose
продолжают разрешаться, маркер продолжает записывать compose, а команда,
которую runbook даёт оператору, не меняется.
5. E2E инвертирует то, что проверяет. Обходной шаг уходит; на его место
приходит утверждение, что папка job начинается без docker/ (у него нет
checkout), что без TTY установщик проверил файлы и не записал ни одного, и — в
отдельном шаге, после установки, чтобы токена не было в окружении установщика,
— что записанные файлы байт в байт те, что покрывает манифест Release.
install-e2e.spec.ts перестаёт проверять «обе половины» обхода и начинает
проверять, что он не вернётся.
Что этот ADR явно отвергает
- Клонировать репозиторий на теге. Это был бы один файл для поддержки,
всегда согласованный с тегом, — и это заменило бы цепочку подписи
доверием к транспорту
git, добавив зависимость, которой может не быть на чистой машине. То, что поднимает установку, стало бы единственным из используемого установщиком, что не проверяется так, как проверяется бинарник. - Tarball со всеми четырьмя. Лишний файл для распаковки на чужой машине и строка манифеста, покрывающая четыре файла, не называя их, — та же причина, по которой ADR 0149 предпочёл манифест пяти подписям, применённая наоборот.
- Встроить файлы в
install.sh(heredoc). В compose сотни строк, и он меняется вместе с топологией; установщик начал бы меняться с каждым сервисом, аcomposes-em-conformidade.spec.tsпришлось бы читать shell. - Переменная или флаг, чтобы указать другой URL Release, для теста. Это
кратчайшая форма двери для пропуска проверки (ADR 0150). Тесты shell
передают URL как АРГУМЕНТ функции, что делает и
main. - Запускать
install-e2e.ymlнаpull_request. Остаётся вне, по причине из ADR 0150.
Последствия
- Закрыто в коде, доказано только на следующем финальном теге. Конвейер
публикует только на теге, а E2E запускается только после него; в PR
выполняется статическая часть (
assets-do-instalador.spec.ts,install-e2e.spec.ts) и функциональная часть shell против поддельной Release (install-arquivos-da-instalacao.spec.ts). До первого тега после этого merge ни одна опубликованная Release не имеет этих assets. - Никаких действий оператора, и ни одна работавшая установка не перестаёт
работать. Новый
install.shдоходит до кого-либо только внутри Release, и это ТОТ ЖЕ шаг ТОГО ЖЕ job, который прикладывает его и прикладывает четыре assets, — так что Release с новым установщиком и без файлов существует, только если этот шаг упадёт посередине, и тогда установщик отказывает рано, с "a Release não publica …", а не умирает после.env. Запуск новогоinstall.shиз checkout против старой Release уже отклонялся раньше, проверкой происхождения (RN-526): его хеша нет в старом манифесте. Уже опубликованные Releases остаются со старым установщиком и обходом — запускать его изнутри checkout на теге. - Release растёт на четыре assets (~29 KB), а манифест — на четыре строки, и четыре файла начинают версионироваться вместе с установщиком: изменение одного из них меняет то, что подпишет следующий тег. Изменение ТАБЛИЦЫ требует изменения обеих сторон, и spec проваливается, если изменится только одна.
- У checkout, чья папка является папкой установки,
docker/перезаписывается копиями из Release. На устанавливаемом теге они идентичны; вне егоgit statusстановится грязным, и скрипт ГОВОРИТ, какие заменил. Это цена неизменной раскладки, и она существует только для тех, кто устанавливает изнутри checkout. - Прокси
GET /runner-releases/binaryне меняется: он читает манифест как отображение имя → хеш (RN-525), и лишние строки на него не влияют. - Смежное, измеренное и НЕ исправленное. Доказательство восстановления при
миграции (
test-restore-compose.sh) вызываетdocker compose -f <compose>без--env-file, а Compose ищет.envв папке COMPOSE, а не в той, откуда запускается (измерено:docker compose -f docker/c.yml configс.envтолько в текущем каталоге падает на интерполяции). Поскольку compose живёт вdocker/, а.envв папке выше, переменнаяBRABO_BACKUP_IMAGEне доходит, и доказательство склонно проваливаться — БЕЗОПАСНЫЙ исход по RN-530 (ничего не удаляется), но миграция через compose не завершается. Это предсуществующий дефект пути миграции, который не проверяет ни один E2E; он объявлен здесь для того направления, которое его проверит.