From dbc32fc10671352e44a23377a87530d0479bd577 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sun, 7 Jun 2026 07:27:38 +0000 Subject: [PATCH] architect(ET): auto-commit from architect run_id=263 --- docs/architecture/README.md | 31 ++- .../adr/adr-0008-staging-image-provenance.md | 77 +++++++ .../ADR-001-staging-image-provenance.md | 209 ++++++++++++++++++ .../ORCH-058/07-infra-requirements.md | 71 ++++++ docs/work-items/ORCH-058/10-tech-risks.md | 16 ++ 5 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 docs/architecture/adr/adr-0008-staging-image-provenance.md create mode 100644 docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md create mode 100644 docs/work-items/ORCH-058/07-infra-requirements.md create mode 100644 docs/work-items/ORCH-058/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 8e5e7e4..657481f 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -35,7 +35,7 @@ created → analysis → architecture → development → review → testing → | deploy | — | `check_deploy_status` | 14-deploy-log.md (`deploy_status:`) | | done | — | — | — | -**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043). +**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043), check_staging_image_fresh (ORCH-058). **Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`. @@ -80,6 +80,34 @@ terminal-sync, merge-gate, exit-code-контракт хука. Restart-safe с sentinel-файлы (`/.deploy-state-//`), без миграции БД. Подробнее: [adr-0007](adr/adr-0007-executable-self-deploy.md), детально — `docs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md`. + +### Свежесть артефакта BUILD-ONCE: провенанс staging-образа (ORCH-058 — design) +BUILD-ONCE retag (ORCH-36) промоутит `SOURCE_IMAGE=orchestrator-orchestrator-staging` в прод +**без rebuild**, полагаясь на «staging-образ свеж и провалидирован». Этой гарантии нет: +конвейер нигде не пересобирает staging-образ из провалидированного коммита → retag мог тихо +промоутнуть УСТАРЕВШИЙ образ (инцидент LESSONS_ORCH-036 п.4 — зелёный деплой молча +откатывал прод). ORCH-058 обеспечивает инвариант `INV-FRESH` **двумя слоями** (defense in +depth), только для self-hosting: +- **A — пересборка (liveness):** детерминированный QG-под-чек `check_staging_image_fresh` на + ребре `deploy-staging → deploy` ПОСЛЕ merge-gate и ДО Phase A пересобирает + `orchestrator-orchestrator-staging` из worktree валидированного коммита + (`--build-arg GIT_SHA=`, OCI-лейбл `org.opencontainers.image.revision`), пересоздаёт + 8501 и прогоняет `staging_check` против свежего образа → валидируем и промоутим один + артефакт. FAIL → откат на `development` (как merge-gate). Сборки/recreate — ТОЛЬКО staging. +- **B — fail-closed guard (safety):** хук шагом 2b ПЕРЕД `docker tag` сверяет лейбл `revision` + у `SOURCE_IMAGE` с `EXPECTED_REVISION` (пробрасывает `build_deploy_command`). Несовпадение + / пустой лейбл / пустой ожидаемый SHA / ошибка inspect → `exit 1` → FAILED (БАГ-8 откат), + прод не трогается. Делает тихий промоут устаревшего образа структурно невозможным даже при + отключённой/проигравшей гонку A. + +Якорь «провалидированного коммита» — `git rev-parse HEAD` worktree ПОСЛЕ merge-gate (один +helper `validated_revision` питает и штамп A, и `EXPECTED_REVISION` B). Единый kill-switch +`image_freshness_enabled` включает A+B **как целое** (нет «B без A» = вечного fail-fast); +`image_freshness_repos` (пусто → self-hosting). `STAGE_TRANSITIONS`, exit-code хука (0/1/2), +`check_deploy_status`, БАГ-8, merge-gate, схема БД — НЕ меняются (под-гейт ребра + лейбл +образа, без миграций). Подробнее: [adr-0008](adr/adr-0008-staging-image-provenance.md), +детально — `docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md`. + ### Reconciler: реконсиляция потерянных webhook (ORCH-053 — реализовано) Конвейер продвигается только входящими webhook; потерянное событие (502 на ребилде, нет ретраев у Plane/Gitea, неразрезолвленный `sha→branch`) → задача застревает молча @@ -168,3 +196,4 @@ never-raise на единицу работы; тишина при синхрон --- *Актуально на 2026-06-06. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. ORCH-043: merge-gate — design (см. adr-0006), реализация в ветке feature/ORCH-043. ORCH-036: исполняемый самодеплой стадии `deploy` — design (см. adr-0007), реализация в ветке feature/ORCH-036.* *Актуально на 2026-06-06. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. ORCH-043: merge-gate — design (см. adr-0006), реализация в ветке feature/ORCH-043. ORCH-053: reconciler — реализовано (см. adr-0007, src/reconciler.py).* +*ORCH-058: провенанс staging-образа перед BUILD-ONCE retag (check_staging_image_fresh + хук-guard) — design (см. adr-0008), реализация в ветке feature/ORCH-058. Обновлять также при изменении src/self_deploy.py, scripts/orchestrator-deploy-hook.sh, Dockerfile.* diff --git a/docs/architecture/adr/adr-0008-staging-image-provenance.md b/docs/architecture/adr/adr-0008-staging-image-provenance.md new file mode 100644 index 0000000..eaddbf6 --- /dev/null +++ b/docs/architecture/adr/adr-0008-staging-image-provenance.md @@ -0,0 +1,77 @@ +# ADR-0008: Провенанс staging-образа перед BUILD-ONCE retag в прод (ORCH-058) + +## Статус +Accepted (design) — реализация в ветке `feature/ORCH-058-self-deploy-retag-staging`. +Метка: `arch:major-change`. + +> Примечание о нумерации: в `adr/` исторически два файла `adr-0007-*` +> (`executable-self-deploy`, `reconciler`) — пред-существующая коллизия. Этот ADR берёт +> следующий свободный номер **0008**; коллизию 0007 не трогаем (вне объёма ORCH-058). + +## Контекст + +ORCH-36 (`adr-0007-executable-self-deploy`) сделал стадию `deploy` исполняемой для +self-hosting: Phase B запускает host-хук, который шагом **2b** (BUILD-ONCE) делает +`docker tag $SOURCE_IMAGE → $TARGET_IMAGE` **без rebuild** — «прод = ровно тот артефакт, +что прошёл staging». Предпосылка: staging-образ свеж и собран из провалидированного кода. + +**Этой гарантии нет.** Конвейер нигде не пересобирает `orchestrator-orchestrator-staging` +из провалидированного коммита; `deploy-staging` лишь гоняет `staging_check.py` против уже +работающего 8501. Инцидент (LESSONS_ORCH-036 п.4): staging-образ не пересобрали → проверка +прошла против старого кода → retag промоутнул СТАРЫЙ образ → прод **молча** откатился на +2-дневный код. Зелёный гейт = ложный позитив. Самый опасный из 4 багов: не падает, а тихо +откатывает инструмент, обслуживающий все проекты. + +## Решение + +Гарантировать `INV-FRESH`: в прод промоутится только образ, собранный из коммита, +провалидированного `deploy-staging` для данной задачи; иначе fail-fast (`FAILED` → откат на +`development`, БАГ-8), прод не трогается. Достигается **двумя взаимодополняющими слоями** +(defense in depth), только для self-hosting (условность как ORCH-35/36/43): + +- **A — пересборка (liveness).** На ребре `deploy-staging → deploy`, ПОСЛЕ merge-gate и ДО + Phase A, детерминированный QG-под-чек `check_staging_image_fresh` пересобирает + `orchestrator-orchestrator-staging` из worktree валидированного коммита + (`--build-arg GIT_SHA=`, лейбл `org.opencontainers.image.revision`), пересоздаёт 8501 + и прогоняет `staging_check`. FAIL → откат на `development`. Так валидируемый и промоутимый + артефакт — один и тот же; гарантирует наличие зелёного пути (нет вечного fail-fast). +- **B — fail-closed guard (safety).** Хук шагом 2b ПЕРЕД `docker tag` сверяет лейбл + `revision` образа `SOURCE_IMAGE` с `EXPECTED_REVISION` (пробрасывает `build_deploy_command`). + Несовпадение / пустой лейбл / пустой ожидаемый SHA / ошибка inspect → `exit 1` → FAILED. + Делает тихий промоут устаревшего образа структурно невозможным даже при отключённой/ + проигравшей гонку A. + +**Якорь провалидированного коммита** — `git rev-parse HEAD` в worktree ПОСЛЕ merge-gate +(post-rebase tree, который ре-тестирован и сольётся в `main`). Один helper +`validated_revision(repo, branch)` питает и штамп сборки (A), и `EXPECTED_REVISION` (B). + +**Условность и kill-switch:** единый `image_freshness_enabled` (вкл/выкл A+B как целое, +чтобы не было «B без A» = вечный fail-fast), `image_freshness_repos` (CSV; пусто → +self-hosting). Все настройки с префиксом `ORCH_`. + +### Что НЕ меняется +`STAGE_TRANSITIONS` (набор стадий — под-гейт ребра, не стадия), exit-code хука (0/1/2), +`map_exit_code_to_status`, `check_deploy_status`/`_parse_deploy_status`, БАГ-8, terminal-sync, +merge-gate, Phase A/B/C. Схема БД — без миграций (провенанс в лейбле образа, не в БД). + +### Что добавляется (сквозное) +- QG `check_staging_image_fresh` в реестре `QG_CHECKS` (+ snapshot-тест), wired через + `_handle_image_freshness` в `stage_engine` (рядом с merge-gate). +- Режим хука `--build-staging` (build из worktree + recreate 8501; STAGING-safe дефолты). +- OCI-лейбл `org.opencontainers.image.revision` в `Dockerfile` (`ARG GIT_SHA`). +- Helpers `validated_revision` / `rebuild_staging_image` в `self_deploy.py` (never-raise). + +## Последствия + +- Класс «тихого регресса прод» закрыт структурно (B); валидный деплой всегда доходит до + зелёного (A) — устранён ручной bootstrap-разрыв пересборки staging. +- Латентность ребра растёт (build + recreate + повторный staging_check); `staging_check` + гоняется дважды (soft pre-check агента + авторитетный код) — плата за «валидируем = + промоутим». +- Все сборки/recreate — ТОЛЬКО staging (8501); прод (8500) не трогается; `main` не пушится. + Новая под-компонента → `arch:major-change`. + +## Связанные ADR +`adr-0007-executable-self-deploy` (BUILD-ONCE, Phase A/B/C), `adr-0006-merge-gate` (образец +edge-под-гейта), `adr-0003-staging-gate` (условность self-hosting), `adr-0005` +(run-as-host-uid). Детальный per-work-item: `docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md`. diff --git a/docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md b/docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md new file mode 100644 index 0000000..4b164b0 --- /dev/null +++ b/docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md @@ -0,0 +1,209 @@ +# ADR-001 (ORCH-058): Провенанс staging-образа перед BUILD-ONCE retag в прод + +## Статус +Accepted (design) — реализация в ветке `feature/ORCH-058-self-deploy-retag-staging`. +Метка: `arch:major-change` (новая deploy-safety модель + новый QG + новый режим хука). + +## Контекст + +ORCH-36 сделал стадию `deploy` исполняемой для self-hosting (`orchestrator`): Phase B +(`self_deploy.build_deploy_command`) запускает детачед host-хук, который шагом **2b** +(BUILD-ONCE) делает `docker tag $SOURCE_IMAGE → $TARGET_IMAGE` **без `docker build`** — +«прод получает ровно тот артефакт, что прошёл staging». + +Дизайн-предпосылка BUILD-ONCE: **staging-образ свеж и собран из провалидированного кода**. +На практике этой гарантии НЕТ (BRD §2): + +- Стадия `deploy-staging` запускает только `scripts/staging_check.py` против **уже + работающего** контейнера 8501 — что бы в нём ни крутилось. Пересборка staging-образа — + ручная операция (STAGING.md / ORCH-34), вне конвейера. +- Между «образ собран» и «retag в прод» нет провенанс-связи с провалидированным коммитом. + +Инцидент (LESSONS_ORCH-036 п.4 — **самый опасный** из 4 багов bootstrap): staging-образ +не пересобрали из нового `main` → `staging_check` прошёл против СТАРОГО кода → BUILD-ONCE +retag промоутнул СТАРЫЙ образ в прод. Деплой «зелёный» (`result=0`, health ok), но прод +**молча откатился** на код 2-дневной давности. Орк обслуживает все проекты из одного +прод-инстанса → тихий регресс инструмента = групповой инцидент. + +Текущая защита (staging-гейт, merge-gate, health-check хука) этот класс НЕ ловит: все +гейты зелёные, потому что проверяют **не тот артефакт**, что уезжает в прод. + +## Инвариант, который нужно обеспечить + +`INV-FRESH` (ТЗ §1): образ, передаваемый хуку как `SOURCE_IMAGE` для BUILD-ONCE retag в +прод, собран из ТОГО ЖЕ git-коммита, что прошёл `deploy-staging` для этой задачи. Если +это недоказуемо — деплой fail-fast (`deploy_status: FAILED` → откат на `development`, +БАГ-8), прод не трогается. + +### Якорь «провалидированного коммита» + +**SHA = `git rev-parse HEAD` в worktree ветки задачи ПОСЛЕ merge-gate** (т.е. после +возможного `auto_rebase_onto_main` + `push --force-with-lease`). Это ровно тот tree, +который merge-gate ре-тестировал зелёным и который сольётся в `main`. Один helper +`validated_revision(repo, branch)` (never-raise) вычисляет SHA и служит ЕДИНСТВЕННЫМ +источником и для штампа сборки (Стратегия A), и для ожидаемого ревижна (Стратегия B) — +два потребителя одного якоря не могут разойтись. + +## Решение: A + B (defense in depth) + +Ни одна стратегия по отдельности не закрывает задачу: + +- **B в одиночку** (fail-fast по провенансу) делает тихий промоут структурно невозможным, + НО если staging-образ устарел — fail-fast'ит **навсегда** (нет пути к зелёному без + ручной пересборки) → нарушает BR-5 / AC-6 (deadlock), воспроизводит ровно тот + bootstrap-разрыв, который мы устраняем. +- **A в одиночку** (пересборка из провалидированного коммита) закрывает петлю «валидируем = + промоутим», НО не имеет утверждения В МОМЕНТ retag: гонка/отключение/сбой пересборки + снова даст тихий промоут. + +Поэтому берём **обе**, как взаимодополняющие слои: + +### Стратегия A — пересборка staging-образа из провалидированного коммита (liveness, AC-4/AC-6) + +Для self-hosting на ребре `deploy-staging → deploy`, **после merge-gate** (когда +валидированный HEAD финализирован) и **до Phase A**, детерминированный код: + +1. Вычисляет `sha = validated_revision(repo, branch)`. +2. Пересобирает `orchestrator-orchestrator-staging` из **worktree ветки** (build-context = + валидированный tree) с `--build-arg GIT_SHA=` и пересоздаёт контейнер 8501 на + свежем образе (`--no-build`). +3. Прогоняет `staging_check.py --mode stub` против свежего 8501. + +Результат: ровно ЭТОТ образ (с лейблом `revision=`) становится `SOURCE_IMAGE` для +прод-retag → петля замкнута, валидируем и промоутим один артефакт (AC-4). Пересборка/ +recreate трогают **ТОЛЬКО staging (8501)**, НИКОГДА прод (8500) (AC-9). + +Исполнение — через host (ssh, синхронно): docker CLI / compose доступны на ХОСТЕ, не в +контейнере (Dockerfile ставит только `openssh-client git`; staging_check уже гоняется +`docker exec`-ом на хосте). Новый режим хука `--build-staging` (см. ниже) выполняет сборку +и recreate. Синхронный ssh достаточен — рестарт staging не убивает прод-worker (в отличие +от Phase B, где нужен detached + finalizer). + +Реализуется как **детерминированный QG-под-чек `check_staging_image_fresh`** (по образцу +`check_branch_mergeable`, ORCH-043): pure-условность + never-raise; для прочих репо → +`(True, "N/A")`. Регистрируется в `QG_CHECKS` и в `tests/test_qg_registry_snapshot.py`. +Вызов — на ребре через `_handle_image_freshness(...)` в `stage_engine` (рядом с +`_handle_merge_gate`, ПОСЛЕ него, ДО Phase A). FAIL → откат на `development` + release +merge-lease (как merge-gate). **`STAGE_TRANSITIONS` (набор стадий) НЕ меняется** — это +под-гейт ребра. + +### Стратегия B — fail-closed провенанс-guard в хуке (safety, AC-1/AC-2/AC-3) + +1. **`Dockerfile`**: `ARG GIT_SHA` + `LABEL org.opencontainers.image.revision=$GIT_SHA`. + Без build-arg лейбл пустой → fail-closed на стороне B (см. ниже). +2. **`build_deploy_command`**: вычисляет `EXPECTED_REVISION = validated_revision(repo, + branch)` и пробрасывает в env команды хука. +3. **`orchestrator-deploy-hook.sh` шаг 2b** — ПЕРЕД `docker tag`: + - читает лейбл `SOURCE_IMAGE`: + `docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$SOURCE_IMAGE"`; + - сравнивает с `$EXPECTED_REVISION`; + - несовпадение / пустой лейбл / пустой `EXPECTED_REVISION` / ошибка inspect → + `log` + `exit 1` (**fail-closed**, никогда не промоутить «на авось»). + - **Обратная совместимость:** при НЕзаданном `EXPECTED_REVISION` — текущее поведение + (проверка пропускается), чтобы не сломать не-self репо и legacy-вызовы. +4. `exit 1` уже маппится `map_exit_code_to_status → FAILED` (контракт не меняется), Phase C + пишет `deploy_status: FAILED` → откат на `development` (БАГ-8). Прод не рестартуется на + устаревший образ — guard срабатывает ДО `docker tag`/restart. + +### Новый режим хука `--build-staging` (для Стратегии A) + +`orchestrator-deploy-hook.sh --build-staging` (env: `GIT_SHA`, `BUILD_CONTEXT` = host-путь +worktree, `TARGET_IMAGE=orchestrator-orchestrator-staging`, `TARGET_SERVICE`, +`COMPOSE_PROFILE=staging`, `TARGET_PORT=8501`): +`docker build --build-arg GIT_SHA= -t ` → +`docker compose --profile staging up -d --no-build orchestrator-staging` → health 8501. +Тот же exit-code-контракт (0=ok). Дефолты режима — STAGING-safe (как у `--deploy`). + +Host-путь build-context выводится из container-пути worktree заменой +`repos_dir → host_repos_dir` (как `host_state_dir` в `self_deploy.py`); требуется +производный helper host-worktree-пути (или новая настройка `ORCH_HOST_WORKTREES_DIR`). + +## Конфигурация (`src/config.py`, все с префиксом `ORCH_` — урок ORCH-36 п.2) + +- `image_freshness_enabled: bool = True` — **единый** kill-switch ВСЕЙ фичи (A и B вместе). + `False` → ни пересборки, ни проброса `EXPECTED_REVISION` → поведение ровно как ORCH-36 + (BUILD-ONCE без guard). A и B включаются/выключаются **как одно целое**, чтобы не было + опасной полу-конфигурации «B без A» (вечный fail-fast). +- `image_freshness_repos: str = ""` — CSV; пусто → только self-hosting (как + `self_deploy_repos` / `merge_gate_repos`). + +> **Инвариант конфигурации (AC-6):** B активен ТОЛЬКО когда активен A. По умолчанию +> (`image_freshness_enabled=True`) валидный деплой всегда доходит до зелёного (A пересобирает +> → лейбл == EXPECTED → B пропускает). Полное выключение → legacy ORCH-36 поведение. + +## Порядок на ребре `deploy-staging → deploy` (self-hosting) + +1. `check_staging_status` (существующий) — первичный staging-вердикт агента (smoke, + что staging-инфра жива). +2. merge-gate `check_branch_mergeable` (существующий) — финализирует валидированный HEAD + (rebase если позади, ре-тест зелёный, lease HELD). DEFER на busy-lock → возврат без + пересборки. +3. **`check_staging_image_fresh` (НОВЫЙ, Стратегия A)** — пересборка из валидированного + HEAD + recreate 8501 + `staging_check`. FAIL → откат на `development` + release lease. +4. Phase A (существующий) → запрос approve. +5. Phase B (human Approved) → `build_deploy_command` с `EXPECTED_REVISION` → хук-guard (B) + → BUILD-ONCE retag только при совпадении → restart прод → Phase C finalizer. + +> Двойной прогон `staging_check` (агент на стадии + код на шаге 3) — **намеренный**: первый +> валидирует УЖЕ работающий (потенциально устаревший) 8501 как soft pre-check; авторитетный +> — шаг 3 против СВЕЖЕГО образа, который и уедет в прод. `--mode stub` быстр и без LLM-трат. + +## Контракты, которые НЕ меняются (AC-7) + +`STAGE_TRANSITIONS` (набор стадий), exit-code-контракт хука (0/1/2), +`map_exit_code_to_status`, `check_deploy_status` / `_parse_deploy_status` (frontmatter-only), +БАГ-8 rollback, terminal-sync `deploy → done`, merge-gate (ORCH-43), Phase A/B/C ORCH-36. +**Схема БД — без миграций** (состояние свежести не персистится в БД; провенанс живёт в +лейбле образа). Добавление `check_staging_image_fresh` в `QG_CHECKS` — ожидаемое расширение +реестра (ТЗ §10), не входит в замороженный список AC-7. + +## Last-line-of-defence / fail-closed (AC-2/AC-3) + +Даже если A отключена/проиграла гонку/сбойнула — **B (хук-guard) делает тихий промоут +устаревшего образа структурно невозможным**: рассинхрон лейбла и `EXPECTED_REVISION` → +`exit 1` ДО retag → FAILED → откат. На любом сомнении (нет лейбла, пустой ожидаемый SHA, +ошибка inspect) — трактуется как несоответствие. Прод никогда не трогается «на авось». + +## never-raise (AC-8) + +`validated_revision`, `rebuild_staging_image`, `check_staging_image_fresh`, +`build_deploy_command` (проброс EXPECTED) — все защищены try/except, любая ошибка → безопасный +вердикт (для A-под-чека: `(False, reason)` с release lease; пустой `EXPECTED_REVISION` на +сомнении → B fail-closed). Исключение никогда не всплывает в `stage_engine`. + +## Последствия + +**Плюсы** +- Класс «тихого регресса прод» закрыт структурно (B), а валидный деплой всегда доходит до + зелёного (A) — bootstrap-разрыв «ручная пересборка staging» устранён. +- Валидируем и промоутим один и тот же артефакт (AC-4); провенанс машиночитаем (лейбл). +- Единый kill-switch, поэтапный раскат, условность только для self-hosting — без регрессий + для не-self репо. + +**Минусы / ограничения** +- Латентность ребра растёт: +`docker build` staging + recreate 8501 + повторный + `staging_check` перед Phase A. Приемлемо (выполняется в monitor-треде, как merge-gate + re-test; bounded timeouts). +- `staging_check` гоняется дважды (soft pre-check агента + авторитетный код) — осознанная + плата за AC-4. Возможная будущая оптимизация: облегчить шаг 3 до health+revision-smoke, + если merge-gate re-test признать достаточным для кода. +- Требуется host-доступ к `docker build`/`compose` под slin (как для `--deploy`) и writable + build-context (worktree) — заложено инфра-требованиями (07). +- Новая под-компонента (QG `check_staging_image_fresh` + режим хука `--build-staging`) → + `arch:major-change`. + +## Альтернативы (отклонены) + +- **Только B.** Deadlock без авто-пересборки (BR-5/AC-6). ❌ +- **Только A.** Нет утверждения в момент retag → гонка/отключение снова даёт тихий промоут + (AC-2/AC-3). ❌ +- **Rebuild в хуке на Phase B (прод-сторона).** Уничтожает BUILD-ONCE (прод-rebuild) и + промоутит образ, который staging-e2e никогда не валидировал. ❌ +- **Rebuild напрямую из контейнера через docker.sock.** В образе нет docker CLI/compose; + staging-операции и так host-side (ssh). ❌ + +## Связанные ADR +Глобальный: `docs/architecture/adr/adr-0008-staging-image-provenance.md`. +`adr-0007-executable-self-deploy` (ORCH-36, BUILD-ONCE), `adr-0006-merge-gate` (ORCH-43, +образец edge-под-гейта), `adr-0003-staging-gate` (ORCH-35, условность), `adr-0005` +(run-as-host-uid). diff --git a/docs/work-items/ORCH-058/07-infra-requirements.md b/docs/work-items/ORCH-058/07-infra-requirements.md new file mode 100644 index 0000000..6b182a1 --- /dev/null +++ b/docs/work-items/ORCH-058/07-infra-requirements.md @@ -0,0 +1,71 @@ +# Инфра-требования — ORCH-058 + +Work Item ID: ORCH-058 + +Топология не меняется (тот же сервер mva154, те же контейнеры 8500/8501, общая БД). Меняется +**что делает self-deploy на ребре `deploy-staging → deploy`** для self-hosting. Полная +топология/риски — `docs/operations/INFRA.md` (обновить в том же PR). + +## IR-1. Host-сборка staging-образа (Стратегия A) + +Шаг свежести пересобирает `orchestrator-orchestrator-staging` на ХОСТЕ (docker CLI/compose +есть на хосте, НЕ в контейнере — образ ставит только `openssh-client git`). Требуется: + +- Рабочий ssh `slin@127.0.0.1` (уже есть, ORCH-36 / LESSONS п.1–2: passwd-запись uid 1000, + ключ смонтирован, `ORCH_DEPLOY_*` префиксы). +- На хосте под `slin` доступны `docker build` и `docker compose --profile staging` + (recreate 8501). Группа docker (`group_add: "999"` / host-доступ к `docker.sock`) — уже + настроено. +- **Build-context = host-путь worktree** валидированной ветки + (`/home/slin/repos/_wt//`), читаемый под `slin`. Worktree уже + создаётся launcher'ом/merge-gate под slin (ADR-0005 run-as-host-uid) — права ок. +- Лог-директория хука writable под slin (`/var/log/orchestrator`, LESSONS п.3) — уже. + +## IR-2. Вывод host-пути worktree + +В контейнере worktree виден как `ORCH_WORKTREES_DIR=/repos/_wt/...`; на хосте — как +`/home/slin/repos/_wt/...`. Маппинг = замена `repos_dir → host_repos_dir` (как +`self_deploy.host_state_dir`). Реализация: производный helper host-worktree-пути, либо новая +настройка `ORCH_HOST_WORKTREES_DIR` (дефолт `/home/slin/repos/_wt`). Без неё — деривация из +`host_repos_dir`. + +## IR-3. OCI-лейбл происхождения (Стратегия B) + +`Dockerfile`: `ARG GIT_SHA` + `LABEL org.opencontainers.image.revision=$GIT_SHA`. Сборки БЕЗ +build-arg (ручные/legacy) дают пустой лейбл → B fail-closed (это by design, не регрессия: +прод-retag без доказуемого провенанса должен падать). Любой существующий способ сборки прод/ +staging-образа (CI, ручной) при включённой фиче ОБЯЗАН передавать `--build-arg GIT_SHA=`, +иначе деплой задачи fail-fast'нется на guard. Шаг A это делает автоматически. + +## IR-4. ssh-режим хука `--build-staging` + +Новый режим `orchestrator-deploy-hook.sh --build-staging` запускается синхронно (рестарт +staging безопасен, detached/finalizer не нужны — в отличие от Phase B прод). Дефолты режима — +STAGING-safe (`TARGET_PORT=8501`, `--profile staging`). Прод (8500) этим режимом НЕ +затрагивается. + +## IR-5. Конфигурация (env, префикс `ORCH_`) + +- `ORCH_IMAGE_FRESHNESS_ENABLED` (дефолт true) — единый kill-switch A+B. +- `ORCH_IMAGE_FRESHNESS_REPOS` (дефолт пусто → self-hosting). +- (опц.) `ORCH_HOST_WORKTREES_DIR` (дефолт `/home/slin/repos/_wt`). + +`EXPECTED_REVISION` для хука строится в `build_deploy_command` — отдельной настройки не +требует. `deploy_prod_source_image` (= `orchestrator-orchestrator-staging`) переиспользуется. + +## IR-6. Безопасность self-hosting (инварианты) + +- Любые `docker build` / `compose up` / recreate — ТОЛЬКО staging (8501); прод (8500) не + рестартуется в рамках шага свежести. +- `main` не пушится; force-only — `--force-with-lease` на ветку задачи (merge-gate, без + изменений). Шаг A не пушит ничего (только локальный `docker build`). +- B-guard срабатывает ДО `docker tag`/restart — прод не трогается на сомнении. + +## IR-7. Bootstrap-чеклист (урок ORCH-36 «сквозной») + +Перед мержем ORCH-058 — **реальный** прогон в staging-петле (не только бумажные гейты): +сборка staging из worktree с GIT_SHA → лейбл присутствует +(`docker image inspect ... revision`) → recreate 8501 → `staging_check` зелёный → +`build_deploy_command` отдаёт непустой `EXPECTED_REVISION` → хук-guard пропускает при +совпадении и `exit 1` при подмене `SOURCE_IMAGE` на устаревший. Зафиксировать в bootstrap- +заметке (как LESSONS_ORCH-036). diff --git a/docs/work-items/ORCH-058/10-tech-risks.md b/docs/work-items/ORCH-058/10-tech-risks.md new file mode 100644 index 0000000..b488ef9 --- /dev/null +++ b/docs/work-items/ORCH-058/10-tech-risks.md @@ -0,0 +1,16 @@ +# Технические риски — ORCH-058 + +Work Item ID: ORCH-058 + +| ID | Риск | Вероятность / Влияние | Митигация | +|----|------|----------------------|-----------| +| R-1 | **Полу-конфигурация «B без A»** → вечный fail-fast деплоя (B падает, никто не пересобирает) | Низк. / Высок. (deadlock, BR-5) | Единый kill-switch `image_freshness_enabled` включает/выключает A и B **как целое**; раздельных флагов A/B нет. Дефолт — оба включены. AC-6. | +| R-2 | **Рассинхрон якоря**: merge-gate делает rebase ПОСЛЕ того, как агент прогнал staging_check → HEAD изменился | Сред. / Сред. | Якорь берётся ПОСЛЕ merge-gate; шаг A пересобирает из post-rebase HEAD; авторитетный staging_check — против свежего образа. Pre-check агента — soft. | +| R-3 | **Гонка**: между пересборкой A и Phase B human-approve worktree HEAD сместился | Низк. / Высок. | B сверяет лейбл образа с `EXPECTED_REVISION`=validated_revision на момент Phase B; рассинхрон → fail-closed `exit 1`, прод не трогается. AC-2/AC-3. | +| R-4 | **Пустой лейбл** (ручная/legacy/CI-сборка без `--build-arg GIT_SHA`) | Сред. / Высок. | Fail-closed: пустой лейбл → несоответствие → `exit 1`. By design. Шаг A всегда передаёт GIT_SHA. IR-3 фиксирует требование к любым сборкам. | +| R-5 | **Латентность ребра**: +docker build staging +recreate +повторный staging_check перед approve | Высок. / Низк. | Bounded timeouts; выполняется в monitor-треде (как merge-gate re-test). `staging_check --mode stub` без LLM-трат. Приемлемо. | +| R-6 | **Сборка/recreate случайно затронет прод (8500)** | Низк. / Критич. | Режим хука `--build-staging` со STAGING-safe дефолтами (8501, `--profile staging`); код шага A никогда не передаёт прод-параметры. AC-9. Тест-инвариант: цель != прод. | +| R-7 | **docker build на хосте падает** (нет места, недоступен daemon, битый worktree) | Низк. / Сред. | never-raise: `check_staging_image_fresh` → `(False, reason)` + release lease → откат на `development` (не зависание, не тихий промоут). AC-8. | +| R-8 | **Двойной staging_check** воспринят как баг/лишняя трата | Сред. / Низк. | Документировано как намеренное (soft pre-check агента vs авторитетный код против промоутимого образа). Будущая оптимизация — облегчить шаг A. | +| R-9 | **Самохостинг-bootstrap**: фича не действует, пока сама не в проде (старый прод-образ без лейбла) | Высок. (однократно) / Сред. | Bootstrap-чеклист (IR-7): первый реальный staging-прогон + ручной разрыв; B обратносовместим (без `EXPECTED_REVISION` — старое поведение), раскат поэтапный через флаг. | +| R-10 | **Деградация не-self репо** | Низк. / Высок. | Условность (`image_freshness_repos` пусто → только orchestrator); для прочих — `(True, "N/A")` + хук без `EXPECTED_REVISION` = прежний путь. AC-5. |