14 KiB
ADR-001: Исполняемый самодеплой — стадия deploy дёргает хост-хук (Вариант B)
Work Item: ORCH-036 Stage: architecture Автор: architect Дата: 2026-06-06
Статус
Accepted
Контекст
Стадия deploy сейчас «бумажная»: deployer-агент (LLM) пишет в 14-deploy-log.md
deploy_status: SUCCESS|FAILED, а гейт check_deploy_status (src/qg/checks.py:464)
парсит этот вердикт и двигает deploy → done. Реального docker-деплоя нет (прод
катается руками). BRD ORCH-36 требует превратить стадию в РЕАЛЬНЫЙ самодеплой с
обязательным ручным approve, build-once и авто-rollback (BR-1…BR-10).
Три твёрдых ограничения, разведанных в коде, определяют дизайн:
- Self-restart (BR-2). Прод-контейнер
orchestrator(8500) — ОДИН на все проекты, и в нём же исполняется deployer.docker compose up -d orchestratorиз контейнера убьёт процесс агента/воркера на середине. Реальный рестарт обязан делать ВНЕШНИЙ процесс на хосте, переживающий гибель контейнера. - Status-only verdict model. Комментарии Plane НЕ управляют конвейером —
механизм
:approved:/:rejected:был удалён (src/webhooks/plane.py:544, bug-3 «echo self-hit»). Единственный человеческий гейт — смена статуса Plane наApproved(handle_verdict→_try_advance_stage→advance_stage). - Гонка чтения гейта. Так как реальный рестарт асинхронный и убивает контейнер,
check_deploy_statusнельзя выполнять на выходе агента — вердикта ещё нет; его преждевременное чтение → ложный FAILED → ложный откат.
Контракты, которые НЕ меняются (BR-9, AC-10): STAGE_TRANSITIONS,
check_deploy_status / _parse_deploy_status (frontmatter only), откат БАГ-8
(deploy → development), terminal-sync deploy → done, merge-gate (ORCH-43),
exit-code-контракт хука (0/1/2).
Решение
Деплой стадии deploy для self-hosting (orchestrator) разбивается на три фазы,
оркеструемые детерминированным кодом (без LLM в критическом пути self-restart). Для
НЕ-self репо (enduro-trails и пр.) поведение НЕ меняется — прежний синхронный
ssh-деплой агентом.
Условность по репо
Вся новая логика гейтится is_self_hosting_repo(repo) (как ORCH-35). Не-self репо
идут существующим путём: deployer-агент на стадии deploy делает ssh-деплой
синхронно, пишет 14-deploy-log.md, гейт срабатывает на выходе агента.
Фаза A — запрос approve (вход в deploy)
В advance_stage на ребре deploy-staging → deploy (ПОСЛЕ зелёного
check_staging_status и merge-gate ORCH-43), для self-hosting + deploy_require_ manual_approve=true:
- НЕ ставить в очередь прод-deployer (перехватить штатный
enqueue_job(get_agent_for_stage("deploy-staging"))); - выставить issue в approval-pending статус (паттерн
set_issue_in_review), написать Plane-коммент «approve для прод-деплоя» + Telegram (BR-5); - записать restart-safe маркер
approve-requested(sentinel-файл, см. ниже).
Задача остаётся на стадии deploy и ждёт человека. STAGE_TRANSITIONS не меняется.
При deploy_require_manual_approve=false (вне объёма, флаг НЕ выключается в ORCH-36 —
AC-12) Фаза A сразу переходит к Фазе B без человеческого гейта. Структурная ветка
закладывается, но дефолт true.
Фаза B — инициация деплоя (смена статуса Plane → Approved)
Человек ставит issue в Approved. handle_verdict(approved=True) →
_try_advance_stage → advance_stage(current_stage="deploy", finished_agent=None).
Новая ветка-перехват в advance_stage:
- условие:
current_stage=="deploy"Иfinished_agent is None(человеческий путь) И self-hosting И approve-флаг И маркерinitiatedОТСУТСТВУЕТ; - действие: запустить внешний detached host-процесс (см. ниже) и поставить в
очередь детерминированный finalizer-job с задержкой; записать маркер
initiated(идемпотентность: повторный Approved не запускает деплой дважды); Plane-коммент «прод-деплой стартовал» + Telegram (BR-5); - вернуться БЕЗ advance (НЕ запускать
check_deploy_status— вердикта ещё нет).
Дискриминатор finished_agent разводит Фазу B (человек, None) и Фазу C
(finalizer, "deployer"), поэтому повторное использование advance_stage безопасно.
Фаза C — фиксация вердикта (детерминированный finalizer)
Finalizer-job (claim'ится воркером уже в НОВОМ контейнере после рестарта):
- читает sentinel
result(exit-code хука, записан host-процессом); - если
resultещё нет и бюджет попыток не исчерпан → defer (повторный finalizer-job сavailable_at_delay_s, как merge-gate defer); бюджет считается изjobs(LIKE '%deploy-finalize%', restart-safe); - если
resultесть → маппинг exit-code → deploy_status (детерминированный, unit-тестируемый):0 → SUCCESS,1|2|иное → FAILED; записать14-deploy-log.md(frontmatterdeploy_status:), смержить вmain(паттерн лога), затем вызватьadvance_stage(current_stage="deploy", finished_agent="deployer"); - далее срабатывают СУЩЕСТВУЮЩИЕ контракты:
SUCCESS→ terminal-syncdeploy → done- release merge-lease;
FAILED→ откат БАГ-8deploy → development+set_issue_blocked+ Plane/Telegram (BR-3, AC-4)._parse_deploy_statusНЕ меняется.
- release merge-lease;
Механизм detached-запуска: ssh + setsid
Выбор: ssh на хост (slin@DEPLOY_SSH_HOST) с setsid-detached исполнением хука.
Обоснование: ssh-ключи уже смонтированы (INFRA P-2), не-self репо уже деплоятся по
ssh (единый путь), хук живёт на хосте и под slin имеет полный доступ к docker вне
контейнера → переживает рестарт 8500 (BR-2). setsid/nohup + redirect отвязывает
удалённый процесс от ssh-канала, чтобы он пережил гибель ssh-клиента при рестарте
контейнера. Отвергнуто: вызов через docker.sock изнутри контейнера = ровно мина
«убей себя на середине вызова».
Эскиз (точная сборка — за разработчиком):
ssh -o StrictHostKeyChecking=no slin@$DEPLOY_SSH_HOST \
"setsid bash -c 'cd /home/slin/repos/orchestrator && \
SOURCE_IMAGE=orchestrator-orchestrator-staging \
TARGET_SERVICE=orchestrator TARGET_PORT=8500 \
TARGET_IMAGE=orchestrator-orchestrator COMPOSE_PROFILE= \
PREV_IMAGE_FILE=.deploy-prev-image-prod \
bash scripts/orchestrator-deploy-hook.sh --deploy; \
echo \$? > <result-sentinel>' >> <hook.log> 2>&1 </dev/null &"
ssh-команда возвращается сразу; remote-процесс detached. Запись sentinel result
делает обёртка (echo $? > result), а НЕ хук — контракт хука нетронут.
Build-once (BR-6, AC-7)
Прод обязан подняться на ОБРАЗЕ, прошедшем staging (а не на пересборке). Решение:
расширить хук опциональным SOURCE_IMAGE (обратно совместимо: не задан →
текущее поведение). При заданном SOURCE_IMAGE хук ПЕРЕД up -d --no-build
делает docker tag $SOURCE_IMAGE $TARGET_IMAGE. Для прод-self:
SOURCE_IMAGE=orchestrator-orchestrator-staging → TARGET_IMAGE=orchestrator-orchestrator.
Это единственное допустимое изменение хука; exit-code-контракт и дефолтное
staging-поведение не меняются. git pull хука обновляет рабочее дерево хоста для
будущих сборок, но РАЗВЁРНУТЫЙ артефакт = перетегированный staging-образ.
Restart-safe состояние: sentinel-файлы (без миграции БД)
По образцу merge-lease (<repos_dir>/.merge-lease-<repo>.json) состояние деплоя
хранится в файлах под <repos_dir>/.deploy-state-<repo>/<work_item_id>/ (вне git,
видны и хосту, и контейнеру через mount /home/slin/repos ↔ /repos):
approve-requested— Фаза A выполнена;initiated— Фаза B запущена (idempotency-guard);result— exit-code хука (пишет host-обёртка). Бюджет finalize-defer считается изjobs(restart-safe), новых таблиц/колонок НЕТ (TRZ §4).
Последствия
Плюсы
deploy_status: SUCCESSстановится ДОКАЗАННЫМ (реальный health-ok хука), не декларацией LLM (BR-1).- Self-restart безопасен: рестарт 8500 делает внешний host-процесс; орк себя не убивает (BR-2). Вердикт фиксирует НОВЫЙ контейнер после рестарта.
- Критический путь self-restart детерминирован (без LLM) — главный выигрыш по безопасности self-hosting; зеркалит детерминизм merge-gate ORCH-43.
- Approve вписан в существующую status-only модель — restart-safe, аудируемо в Plane,
идемпотентно (маркер
initiated). - Гонка чтения гейта закрыта: гейт читает РЕАЛЬНЫЙ итог через finalizer-defer.
- Build-once гарантирует «что тестировали — то в проде».
- Нетронуты:
STAGE_TRANSITIONS, реестр QG,_parse_deploy_status, БАГ-8, terminal-sync, merge-gate, контракт хука (exit-code).
Минусы / ограничения
- Вводится новый детерминированный job-handler в очереди (reserved-agent
deploy-finalizer, не-LLM) — расширение dispatch воркера/лаунчера. Контейнированное, но это новая под-компонента → задача помечаетсяarch:major-change. - Перехваты в
advance_stageусложняют стадиюdeploy(три ветки поfinished_agent/маркерам). Требуется аккуратное покрытие тестами (TC-04…TC-09). - Build-once зависит от того, что deploy-staging оставил валидный образ
orchestrator-orchestrator-staging; при rebase merge-gate возможен дрейф образ↔main (см. 10-tech-risks R-3). - Approve = смена статуса Plane на
Approved; человек должен понимать, что на стадииdeployApprovedозначает «деплой в прод» (документируется в deployer.md и INFRA.md).
Что обязан сделать developer
src/config.py:deploy_require_manual_approve: bool = True+ прод-параметры хука/ssh +deploy_finalize_delay_s/deploy_finalize_max_attempts.src/stage_engine.py: перехваты Фазы A/B + ветка finalizer (Фаза C черезadvance_stage(..., finished_agent="deployer")).- Очередь: reserved-agent
deploy-finalizer(детерминированный handler: read-result | defer | map+write+advance). Маппинг exit→status — отдельная чистая функция (unit TC-01/02/03). scripts/orchestrator-deploy-hook.sh: опциональныйSOURCE_IMAGEretag (обратно совместимо) + продPREV_IMAGE_FILE.- Уведомления (Plane+Telegram) на initiate/success/rollback (BR-5).
- Документация:
deployer.md,INFRA.md,DEPLOY_HOOK.md,CHANGELOG.md. - Отладка — только на staging-цели хука; прод 8500 в разработке не трогать.
Связанные решения
- Глобальный ADR:
docs/architecture/adr/adr-0007-executable-self-deploy.md. - ORCH-35 staging-gate (
adr-0003), ORCH-43 merge-gate (adr-0006), ORCH-21 auto-rollback, ORCH-34 хук, ORCH-40 run-as-host-uid (adr-0005).