Перейти к основному содержимому

ADR 0150 — Установщик одной строкой, с выбранным источником и маркером

Статус​

Proposed. Зависит от ADR 0149: без подписи этот документ предлагает ровно то, что отвергла ФАЗА 28.

Контекст​

Установка Brabo сегодня требует checkout, пяти секретов, экспортированных вручную, и знаний, которых нет нигде, — три жеста, которые описывает документ фазы. Три факта на диске ограничивают любой дизайн установщика:

Compose продакшена — не манифест установки. Все сервисы docker/docker-compose.prod.yml указывают на brabo-*:prod с локальным блоком build: (:102-105, :144-147, :253-256, :381-384); в docker/ нет ни одной ссылки на ghcr.io. Заголовок файла говорит о себе, в :1 и :8, что это "compose de VALIDAÇÃO das imagens de produção" и что "o que este arquivo NÃO é: um manifesto de produção endurecido". Единственный потребитель .release/images.json в репозитории — scripts/ci/aplicar-imagens.ts, который пишет в overlay kustomize (Makefile:35-36).

Образец согласия уже существует, и он жёсткий в нужном месте. scripts/dev/consentir-base.mjs:195-208: без TTY скрипт сообщает и возвращает 0 — не соглашается, не записывает, не создаёт папку. И его порядок — закон: validarBase выполняется до любого mkdir (:228-242), запись в .env — последний шаг (:272), а проваленная проверка отказывает, не записывая (:246-258).

Пять секретов уже где-то генерируются. docker/smoke.sh:31-57 генерирует все пять через openssl rand, уважая уже экспортированное значение (${VAR:-$(gerar)}), включая деталь, что NEO4J_PASSWORD должен быть -hex, потому что / ломает разбор NEO4J_AUTH (:53-56). Эту часть не нужно изобретать, лишь сделать её постоянной.

Решение​

1. Вызов сохраняет stdin​

sh -c "$(curl -fsSL https://github.com/daneiel/brabo/releases/latest/download/install.sh)"

А не curl … | sh. Причина механическая, а не эстетическая: когда скрипт приходит через pipe, stdin процесса является загрузкой, и read, чтобы что-то спросить, читает байты самого скрипта или встречает EOF. Установщик, который не может спросить, должен был бы сам выбирать, где создавать папки на чужом компьютере, — а мерка репозитория обратная (consentir-base.mjs:195-208).

Без TTY установщик сообщает и выходит, в том же формате, что образец: печатает, что сделал бы, что нашёл, и команду для запуска в терминале.

2. Скрипт проверяет собственное происхождение до действия​

install.sh версионируется в репозитории и публикуется как asset Release. Он несёт собственную версию и первым делом скачивает подписанный checksums.txt этой Release, проверяет подпись (ADR 0149) и сверяет свой хеш с ней. Расходится — останавливается.

Это расширяет — а не цитирует как уже сказанное — пункт 5 ADR 0146:195-212, который версионирует consentir-base.mjs с доводом, что «скрипт, который настраивает хост пользователя, создаёт папку и пишет в .env, — это именно тот артефакт, чьё происхождение должно быть аудируемым», и чьё следствие — api не получает маршрута, выдающего скрипт хоста. install.sh — артефакт того же вида, с большим охватом; следствие действует так же.

3. Обнаружение называет найденное и только потом спрашивает​

Маркер живёт в $XDG_STATE_HOME/brabo/install-state.json, с запасным ~/.local/state/brabo/ — та же идиома, которую servico.ts:250-261 уже использует для XDG_CONFIG_HOME. Не в ~/.brabo/, где runner хранит вещи по проекту: состояние установки и конфигурация проекта — разные жизни, и их объединение заставило бы удаление одного стирать другое.

Маркер версионирован (schemaVersion) и хранит: установленную версию, источник (ghcr или local), применённые digests, пути (.env, база проектов, каталог compose) и дату. Именно он делает второй запуск diff, а не догадкой.

Без маркера — каждая установка до этой версии — скрипт ищет признаки: проект compose с сервисами brabo-*, units brabo-runner-*.service, .env с пятью ключами. И называет найденное до любого вопроса. Пути «что-то нашёл, решаю сам» не существует.

4. Удаление говорит, что стирает и чего никогда не стирает​

стирает (с подтверждением, перечислено заранее)никогда не стирает
именованные тома compose, включая pgdataбазу проектов пользователя
сгенерированный .envпапку зеркала (RN-516: зеркало никогда не стирает, и установщик тоже)
units сервиса runnerлюбую папку вне путей маркера
ключ устройства на дискето, что ключ защищает на сервере, — отзыв это другое действие

Вторая колонка — суть. Папка пользователя — накопление, а не состояние продукта; стирать её значило бы, что установщик принимает решение, которое ему никто не делегировал.

5. Два источника образов, и «официальный» — результат проверки​

  • --source=ghcr (по умолчанию): читает .release/images.json из Release, разрешает каждый образ по digest и проверяет подпись (ADR 0149).
  • --source=local: docker buildx bake с docker-bake.hcl:81-83, что производит ровно четыре имени, которых ожидает compose. Требует чистого рабочего дерева на теге; иначе отказывает, потому что образ, собранный из грязного дерева, не является той версией, которой себя называет.

Слово «официальный» появляется в выводе только тогда, когда проверка прошла. Смена источника потом — это смена образа поверх тех же томов — данные не перемещаются.

6. Собственный compose установки​

Рождается docker/docker-compose.install.yml, который потребляет digests из .release/images.json и не имеет блока build:. docker-compose.prod.yml остаётся нетронутым: это compose проверки, его использует docker/smoke.sh (:19-20, намеренно собирая локально), и возведение его в манифест установки сломало бы уже имеющееся использование и противоречило бы его собственному заголовку.

Цена объявлена: два файла с одной топологией сервисов. Разделяет их то, на что каждый отвечает, — «собирается ли этот тег?» против «запускает ли эта машина опубликованное?».

7. Broker в этой фазе не поднимается, и последствие названо​

У docker-bake.hcl:81-83 четыре цели, а scripts/ci/images-manifest.ts:54 принимает четыре; brabo-broker:prod (docker-compose.prod.yml:333) существует только собранным локально, и поэтому profile container-broker недостижим из артефактов Release.

Установщик не предлагает этот profile и говорит, что это значит: с --source=ghcr проект в режиме mounted — который ADR 0146 сделал локальным значением по умолчанию — не поднимает контейнер, потому что поднимает его broker (ADR 0144). Разговор и backlog работают; исполнение dev agent в этом режиме — нет.

Публикация broker пятым образом — исправление, и оно маленькое (цель в bake, id в images-manifest.ts, ещё одна подпись). Оно вне этого ADR по рамкам, а не из сомнения: входит отдельным пунктом, и пока не вошло, --source=local — путь, дающий полный mounted.

8. Windows — названный отказ​

Как уже в RN-518 (servico.ts:517-532): установщик называет платформу, говорит, что она вне рамок по объявленному решению, и указывает путь переднего плана. Это не общий отказ и не молчание.

9. Чистый bash, с тестируемым решением​

install.sh — это bash, потому что он должен работать до того, как на машине появится какая-либо зависимость. Логика решения (обнаружить, сравнить маркер, выбрать источник, собрать план удаления) живёт в чистых функциях, а скрипт выставляет --print-plan / --print-state, чтобы spec на vitest проверял их, ничего не исполняя, — прецедент реален и из того же репозитория: у scripts/dev/bootstrap.sh есть --print-commands, и его тестирует scripts/dev/bootstrap.spec.ts.

10. Проверка после подъёма собственная, и smoke.sh не переиспользуется​

docker/smoke.sh генерирует пять секретов (:31-57) и является прецедентом решения 5 — но вызывать его не годится: он поднимает с --build (собирает то, что эта установка только что скачала по digest) и сносит стек в конце, если только не SMOKE_KEEP_UP=1. Это тест CI, а установка — не тест.

Установщик вместо этого делает честный минимум: после up --wait обращается к /health api и engine и только тогда говорит, что установил. --wait уже ждёт healthcheck — но кто объявляет состояние, должен его спросить, а это ровно урок, которого стоил reset-total.sh (он объявлял «reset completo» при api в Exited (1)).

По тому же рассуждению нет шага migrate: compose сцепляет api → migrate-api с service_completed_successfully, и второе место, приказывающее мигрировать, было бы вторым источником одной и той же истины.

Что этот ADR явно отвергает​

  • curl | sh. По решению 1: закрывает stdin, а с ним и согласие.
  • Возведение docker-compose.prod.yml в манифест установки. Ломает smoke.sh, который намеренно собирает локально, и противоречит заголовку файла.
  • Маршрут api, генерирующий скрипт. Явное следствие пункта 5 ADR 0146, и здесь оно действует с ещё большей силой.
  • Угаданная миграция. Предыдущая установка без маркера — не «вероятно, такая же»: она называется, и путь — путь ADR 0152.
  • Стирание чего-либо вне путей маркера.

Последствия​

  • Установка перестаёт требовать checkout, а пять секретов начинают генерироваться один раз и сохраняться с режимом 600 — сегодня smoke.sh генерирует их и выбрасывает вместе со стеком.
  • Два compose с одной топологией, и расхождение между ними становится тем, за чем надо следить. Потолок: compose установки не получает сервиса, которого нет у compose проверки.
  • С --source=ghcr mounted остаётся без контейнера, пока broker не опубликован (решение 7). Объявлено в выводе установщика, а не обнаружено потом.
  • Второй запуск установщика становится читаемым diff. Первый запуск на машине, где уже был Brabo, остаётся трудным случаем, и он единственный зависит от другого ADR.