feat: ORCH-104-lite #126
@@ -1,4 +1,4 @@
|
||||
Work item: ORCH-011
|
||||
Work item: ORCH-104
|
||||
Repo: orchestrator
|
||||
Branch: feature/ORCH-011-
|
||||
Branch: feature/ORCH-104-lite
|
||||
Stage: development
|
||||
@@ -3,6 +3,12 @@
|
||||
Формат: [Keep a Changelog](https://keepachangelog.com/). Записи — на смысловой PR/задачу.
|
||||
|
||||
## [Unreleased]
|
||||
- **Lite-тираж: интерактивный installer `scripts/setup_lite.py`** (ORCH-104, `feat`): у Lite-тиража (ORCH-102 — документ `docs/deployment/LITE_SETUP.md`) появился исполняемый инструмент — один операторский CLI, автоматизирующий маршрут LITE_SETUP §2–§12 для внешнего заказчика: скан предусловий хоста с офером доустановки → discovery docker-инсталляций Plane/Gitea → интерактивный сбор обязательных ключей с немедленной верификацией → автодетект хост-параметров и когерентность портов → сборка `.env`/`.env.watchdog` от канонов → webhook Plane → guard-ы Gitea → подъём ровно `orchestrator`+`orchestrator-watchdog` → регистрация проекта строго кирпичом `onboard_project.py` → итоговый отчёт PASS/FAIL/MANUAL. **Scripts+docs+tests** (паттерн ORCH-009/103): рантайм `src/**`, корневой `docker-compose.yml`, `Dockerfile`, `.env.example`/`.env.watchdog.example`, `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД — байт-в-байт; kill-switch не нужен (активация — только явный запуск CLI человеком на целевом хосте, в нашем контуре артефакт инертен). ADR: `docs/work-items/ORCH-104/06-adr/ADR-001-setup-lite-interactive-installer.md`, сквозной `adr-0040-lite-interactive-installer.md`.
|
||||
- **Режимы и дефолт (D1/D2):** python stdlib-only, один файл; закрытый набор режимов семейной лексики `plan`/`apply`/`verify` (`choices`), но **дефолт без аргументов — `apply`** (бизнес-цель «одна команда»). Безопасность дефолта — структурно, не режимом: фаза 0 ≡ `plan` (read-only скан + discovery + автодетект), ранний guard чужого `.env`, per-action consent на каждую мутацию, non-TTY без `--yes` → `exit 2` ДО любой мутации. Exit-контракт `EXIT_OK=0`/`EXIT_MANUAL=2`/`EXIT_ERROR=1`; resume = повторный запуск (check→ensure по реальности, без state-файла).
|
||||
- **Step-движок (D3):** 10 нормативных шагов `scan→prereqs→discovery→collect→render-env→plane-webhook→gitea-guards→up→onboard→report`; каждый шаг ссылается на свой § LITE_SETUP; инвариант `[n for n,_,_ in APPLY_STEPS] == [n for n,_ in build_plan()]` (нет «теневых» шагов).
|
||||
- **Решающая логика — чистые функции (D4–D11):** вердикты предусловий `prereq_verdicts` (не-Linux/не-x86_64 → WARN «вне контура Lite», ни один пункт не пропускается молча), офер установки per-package consent'ом по закрытому набору менеджеров (`apt-get`/`dnf`/`yum`/`zypper`; неопределимый → MANUAL), classifier discovery строго по image-префиксам (`makeplane/*`, `gitea/gitea*`), `port_overrides` (когерентная тройка `ORCH_DEPLOY_PROD_TARGET_PORT`⇄`WATCHDOG_METRICS_URL`⇄`ORCH_POST_DEPLOY_BASE_URL` одной функцией), `staging_port == prod` → fail-closed (ORCH-058/101), рендер `.env` от канона `.env.example` с маркером managed-файла (`# managed by scripts/setup_lite.py (ORCH-104)` → основа resume и guard'а чужого конфига), `WATCHDOG_TG_*` только в `.env.watchdog` (ловушка файла-носителя §4.3), webhook-секреты строго кирпичом `gen_secrets.py`, C-1 ORCH-100 машинно (токен watchdog-бота == токену орка → отказ), §6.4 branch protection на `main` → FAIL с лечением без удаления (no-delete), webhook Plane CE Path A (UI) / Path Б (SQL под пятью предусловиями), `build_onboard_args` детерминированная сборка аргументов кирпича `onboard_project.py` (собственного канона статусов/лейблов скрипт не несёт).
|
||||
- **Секрет-гигиена (NFR-3):** значения секретов вводятся скрыто (`getpass`-класс) и НИКОГДА не печатаются (только имена ключей); существующий немаркированный `.env`/`.env.watchdog` не перетирается без `--force`; delete-операций нет вообще; никаких операций с `main`/force-push; рестарт — только собственного свежеподнятого контура.
|
||||
- **Документация и анти-дрейф (D12):** `LITE_SETUP.md` получил подраздел `### 1.1. Быстрый путь: setup_lite.py` (пиннинг «13 разделов в порядке» цел байт-в-байт) + footer-норматив «меняешь шаги тиража → обнови док **и** `scripts/setup_lite.py` в том же PR»; новый `tests/test_setup_lite_script.py` (ast-скан stdlib-only/нет `src.*`, зеркала `FORBIDDEN_DELETE_NEEDLES`/`FORBIDDEN_STATUS_NEEDLES`, unit чистых функций, секрет-гигиена, import без side-effects); аддитивный тест в `tests/test_lite_setup_doc.py` (TC-27, существующий кортеж `SECTIONS` не правится); витрина `docs/overview/README.md` (маршрут «Развернуть у себя») и `docs/architecture/README.md` (блок Type A) дополнены.
|
||||
- **Витрина системы `docs/overview/`: бизнес + тех, маршруты трёх аудиторий, презентация** (ORCH-011, `docs`): единая точка входа в документацию платформы — новый docs-раздел `docs/overview/` (плоский каталог, 10 файлов, ADR-001 D1): индекс `README.md` (маршруты «Я заказчик / Я менеджер / Я разработчик» + норматив сопровождения «изменил функциональность → обнови витрину в том же PR»), бизнес-часть `business.md` (проблема → решение → что умеет фактически → ценность → 6 сценариев; без жаргона, цифры только с атрибуцией), 7 тех-блоков `tech-*.md` (архитектура со схемой потока, конвейер/гейты, агенты, модель объектов, интеграции, качество/безопасность, наблюдаемость; link-first — за деталями ссылки в golden sources, разрешённый дубль только машинно-сверяемый). **Docs+tests+dev-скрипт** (паттерн ORCH-102/103): `src/**`/`docker-compose.yml`/`Dockerfile`/`requirements*`/`STAGE_TRANSITIONS`/`QG_CHECKS`/machine-verdict/схема БД — ноль изменений. ADR: `docs/work-items/ORCH-011/06-adr/ADR-001-system-overview-canon.md`, сквозной `adr-0039-system-overview-docs-canon.md`.
|
||||
- **Презентация (D4/D5):** слайдо-источник `docs/overview/presentation.md` (16 слайдов в машинно-парсимой структуре «## Слайд N: …» + процедура сборки «команда + Проверка:») + dev-скрипт `scripts/build_presentation.py` (python-pptx, тёмный дизайн, редактируемый текст с точной кириллицей; чистый stdlib-парсер `parse_slides` + ленивый импорт pptx). Запуск только вне рантайма; `python-pptx` НЕ в прод-образе (машинный гард); собранный `.pptx` в git не коммитится — `build/` в `.gitignore`.
|
||||
- **Анти-дрейф (D6):** новый структурный `tests/test_system_docs.py` (без сети/LLM/subprocess, паттерн `test_lite_setup_doc.py`) — 10 файлов витрины; маршруты/норматив; derive-сверки с кодом: стадии импортом `src.stages.STAGE_TRANSITIONS` (вкл. `deploy-staging`/`cancelled`, порядок цепочки), exit-гейты и под-гейты именами реестра `QG_CHECKS` в нормативном порядке security → merge → coverage → image-freshness (+ маркер «не стадии»), 6 агентов glob'ом промптов, таблица эффортов class-default'ами config (ORCH-41/81); валидность относительных ссылок + обязательные golden-source ссылки; полнотекстовый FORBIDDEN-скан (импорт из `test_no_host_hardcodes.py`) + секрет-эвристика + запрет вне-репозиторных путей; слайды каноническим парсером; `pptx` отсутствует в `requirements*`/`Dockerfile`; указатели README/CLAUDE/CHANGELOG.
|
||||
|
||||
47
CLAUDE.md
47
CLAUDE.md
@@ -380,6 +380,53 @@ API → `manual-step` (fail-safe); **runbook** `docs/operations/ONBOARDING.md` (
|
||||
в том же PR. Детали — `docs/work-items/ORCH-103/06-adr/ADR-001-bundled-stack-compose-and-bootstrap.md`,
|
||||
сквозной `docs/architecture/adr/adr-0038-bundled-replication-canon.md`.
|
||||
|
||||
## Lite-installer: интерактивный `scripts/setup_lite.py` (ORCH-104)
|
||||
У Lite-тиража (ORCH-102 — документ-канон `docs/deployment/LITE_SETUP.md`) появился **исполняемый
|
||||
инструмент**: один операторский CLI `scripts/setup_lite.py` (python stdlib-only, мнемоническая пара
|
||||
`LITE_SETUP.md` ⇄ `setup_lite.py`), автоматизирующий маршрут LITE_SETUP §2–§12 для внешнего
|
||||
заказчика. **Scripts+docs+tests** (паттерн ORCH-009/103): рантайм `src/**`, корневой
|
||||
`docker-compose.yml`, `Dockerfile`, `.env.example`/`.env.watchdog.example`,
|
||||
`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД — **байт-в-байт**; kill-switch не
|
||||
нужен (активация — только явный запуск CLI человеком на целевом хосте, в нашем контуре артефакт
|
||||
инертен; на mva154 не запускается).
|
||||
- **Режимы (D1/D2):** закрытый набор семейной лексики `plan`/`apply`/`verify` (`choices`), но
|
||||
**дефолт без аргументов — `apply`** (бизнес-цель «одна команда» — суть задачи; осознанное
|
||||
отступление от plan-default семейства ORCH-009/103). Безопасность дефолта — **структурно, не
|
||||
режимом**: фаза 0 ≡ `plan` (read-only скан предусловий + discovery + автодетект; ноль мутаций),
|
||||
ранний guard чужого `.env`, per-action consent на каждую мутацию (печать точной команды → согласие
|
||||
→ исполнение; отказ → честный MANUAL с эквивалентной командой), non-TTY без `--yes` → `exit 2` ДО
|
||||
любой мутации. Exit-контракт `EXIT_OK=0`/`EXIT_MANUAL=2`/`EXIT_ERROR=1`; resume = повторный запуск
|
||||
(check→ensure по реальности, **без state-файла**).
|
||||
- **Step-движок (D3):** 10 нормативных шагов `scan→prereqs→discovery→collect→render-env→
|
||||
plane-webhook→gitea-guards→up→onboard→report`, каждый ссылается на свой § LITE_SETUP; инвариант
|
||||
`[n for n,_,_ in APPLY_STEPS] == [n for n,_ in build_plan()]` (нет «теневых» шагов).
|
||||
- **Решающая логика — чистые функции (D4–D11):** `prereq_verdicts` (не-Linux/не-x86_64 → WARN «вне
|
||||
контура Lite», ни один пункт не молча), офер установки per-package consent'ом по закрытому набору
|
||||
менеджеров `apt-get`/`dnf`/`yum`/`zypper` (неопределимый → MANUAL), `discover_installations`
|
||||
**строго по image-префиксам** (`makeplane/*`, `gitea/gitea*`; имена контейнеров/проектов как признак
|
||||
НЕ используются — анти-ложноположительность; 0/1/≥2 → ручной ввод/префилл/выбор, «ввести вручную»
|
||||
всегда), немедленная верификация каждого ключа (Plane `/api/v1/workspaces/<slug>/projects/`, Gitea
|
||||
`/api/v1/user`, Telegram `getMe`; лимит 3 попытки → MANUAL), `port_overrides` (когерентная тройка
|
||||
`ORCH_DEPLOY_PROD_TARGET_PORT`⇄`WATCHDOG_METRICS_URL`⇄`ORCH_POST_DEPLOY_BASE_URL` одной функцией),
|
||||
`staging_port == prod` → fail-closed (ORCH-058/101), рендер `.env` от канона `.env.example` с
|
||||
**маркером managed-файла** `# managed by scripts/setup_lite.py (ORCH-104)` (основа resume и guard'а
|
||||
чужого конфига: немаркированный `.env`/`.env.watchdog` → отказ `exit 2` без `--force`),
|
||||
`WATCHDOG_TG_*` только в `.env.watchdog` (ловушка файла-носителя §4.3), webhook-секреты **строго
|
||||
кирпичом** `gen_secrets.py` (свежий выпуск), **C-1 ORCH-100 машинно** (токен watchdog-бота == токену
|
||||
орка → отказ шага), **§6.4** branch protection на `main` → FAIL с лечением **без удаления**
|
||||
(no-delete), webhook Plane CE Path A (UI рекомендация) / Path Б (SQL под пятью предусловиями D8),
|
||||
`build_onboard_args` детерминированная сборка аргументов кирпича `onboard_project.py` (собственного
|
||||
канона статусов/лейблов скрипт НЕ несёт — 22 статуса остаются за `plane_sync._PLANE_NAME_TO_KEY`).
|
||||
- **Гигиена (NFR-1/3):** stdlib-only (ast-скан), `src.*` не импортируется, канон-знания — только
|
||||
субпроцессами кирпичей; значения секретов скрыты (`getpass`) и НИКОГДА не печатаются; delete-операций
|
||||
нет вообще; никаких операций с `main`/force-push; рестарт — только собственного свежеподнятого
|
||||
контура (никогда чужие/боевые контейнеры). **Норматив сопровождения (D12):** меняешь шаги тиража →
|
||||
обнови `LITE_SETUP.md` **и** `scripts/setup_lite.py` в том же PR (footer LITE_SETUP). Анти-дрейф —
|
||||
`tests/test_setup_lite_script.py` (зеркала `FORBIDDEN_DELETE_NEEDLES`/`FORBIDDEN_STATUS_NEEDLES`,
|
||||
unit чистых функций, секрет-гигиена) + аддитивный TC-27 в `tests/test_lite_setup_doc.py`. Детали —
|
||||
`docs/work-items/ORCH-104/06-adr/ADR-001-setup-lite-interactive-installer.md`, сквозной
|
||||
`docs/architecture/adr/adr-0040-lite-interactive-installer.md`.
|
||||
|
||||
## Конвенции
|
||||
- Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`)
|
||||
- Ветки: `feature/ORCH-NNN-slug`, `fix/ORCH-NNN-slug`
|
||||
|
||||
@@ -245,6 +245,28 @@ Type B). Анти-дрейф — `tests/test_bundle_compose.py` / `test_bundled_
|
||||
[adr-0038](adr/adr-0038-bundled-replication-canon.md), детально —
|
||||
`docs/work-items/ORCH-103/06-adr/ADR-001-bundled-stack-compose-and-bootstrap.md`.
|
||||
|
||||
**Интерактивный installer Lite (ORCH-104).** Lite получает инструмент, симметричный
|
||||
Bundled-bootstrap'у: **`scripts/setup_lite.py`** — python stdlib-only wizard, автоматизирующий
|
||||
маршрут LITE_SETUP §2–§12 (скан предусловий с офером доустановки per-package consent'ом;
|
||||
discovery docker-инсталляций Plane/Gitea строго по image-префиксам с выбором пользователя;
|
||||
интерактивный сбор ключей §4.2 с немедленной верификацией каждого значения и секрет-гигиеной;
|
||||
автодетект хост-параметров; сборка `.env`/`.env.watchdog` от канонов + `gen_secrets.py`;
|
||||
webhook Plane CE: UI-путь — рекомендация, SQL-путь — офер под предусловиями с обязательной
|
||||
пост-верификацией; машинная охрана C-1/§6.4/ORCH-058; подъём ровно орк+watchdog;
|
||||
onboarding строго кирпичом plan→согласие→apply→verify; итог PASS/FAIL/MANUAL, exit 0/2/1).
|
||||
Режимы — семейные `plan`/`apply`/`verify`, но **дефолт — `apply`-wizard** (бизнес-цель «одна
|
||||
команда»); безопасность дефолта — структурно, не режимом: фаза 0 ≡ plan (read-only), ранний
|
||||
guard чужого `.env` (**маркер managed-файла**: без маркера → отказ exit 2, с маркером →
|
||||
resume-ensure), per-action consent на каждую мутацию, non-TTY без явного `--yes` → exit 2
|
||||
(headless — env-prefill каноническими именами ключей, не answers-file). Step-движок
|
||||
check→ensure без state-файла (resume = повторный запуск); no-delete; канон не форкается —
|
||||
LITE_SETUP получает подраздел §1.1 «Быстрый путь» (пиннинг «13 разделов» цел), норматив
|
||||
сопровождения расширен: «меняешь шаги тиража → обнови док **и скрипт** в том же PR».
|
||||
Анти-дрейф — `tests/test_setup_lite_script.py` (зеркала delete/status-needle-наборов, unit
|
||||
чистых функций) + аддитивный тест в `test_lite_setup_doc.py`. Рантайм/конвейер — байт-в-байт;
|
||||
kill-switch не нужен. Подробнее: [adr-0040](adr/adr-0040-lite-interactive-installer.md),
|
||||
детально — `docs/work-items/ORCH-104/06-adr/ADR-001-setup-lite-interactive-installer.md`.
|
||||
|
||||
## Витрина системы `docs/overview/` (ORCH-011 — design)
|
||||
|
||||
Единая точка входа «бизнес + тех» для трёх аудиторий (заказчик / менеджер / разработчик) —
|
||||
|
||||
121
docs/architecture/adr/adr-0040-lite-interactive-installer.md
Normal file
121
docs/architecture/adr/adr-0040-lite-interactive-installer.md
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
work_item: ORCH-104
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-11
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# adr-0040: Интерактивный installer Lite-тиража — `scripts/setup_lite.py` (ORCH-104)
|
||||
|
||||
## Статус
|
||||
Proposed
|
||||
|
||||
## Контекст
|
||||
|
||||
Эпик ORCH-10 закрыт по обоим типам, но асимметрично: Type B — Bundled имеет **инструмент**
|
||||
(`scripts/bootstrap_bundle.py`, ORCH-103/adr-0038), Type A — Lite — только **документ**
|
||||
(`docs/deployment/LITE_SETUP.md`, ORCH-102/adr-0037: 13 разделов, ~30+ ручных команд, ~20
|
||||
обязательных ключей). Порог входа Lite высок: оператор вручную собирает значения, которые
|
||||
машина определяет сама; ошибки конфигурации проявляются поздно (на smoke, не в момент ввода).
|
||||
Бизнес-запрос Владельца: один установочный файл — выполняет установку, запрашивает данные в
|
||||
момент установки, сканирует систему с офером доустановки, при нескольких инсталляциях
|
||||
Plane/Gitea даёт выбрать.
|
||||
|
||||
Сквозной характер: серия канонов тиража (adr-0035 onboarding → adr-0036 10-common →
|
||||
adr-0037 Lite-док → adr-0038 Bundled) дополняется слоем «Lite-инструмент»; вводятся нормативы,
|
||||
обязательные для будущих операторских CLI платформы и любого агента, меняющего шаги тиража.
|
||||
Детальный пакет решений (D1…D12, исходы OQ-1…OQ-6 ТЗ) — work-item ADR:
|
||||
`docs/work-items/ORCH-104/06-adr/ADR-001-setup-lite-interactive-installer.md`.
|
||||
|
||||
## Решение
|
||||
|
||||
1. **Новый операторский CLI `scripts/setup_lite.py`** (имя зеркалит LITE_SETUP.md): python
|
||||
stdlib-only wizard, автоматизирующий маршрут LITE_SETUP §2–§12 — скан предусловий хоста с
|
||||
офером доустановки (per-package consent, закрытый набор менеджеров apt-get/dnf/yum/zypper,
|
||||
re-check после установки, иначе MANUAL), discovery docker-инсталляций Plane/Gitea (строго
|
||||
image-префиксы `makeplane/*`/`gitea/gitea*`; 0 → ручной ввод + подсказка про Bundled, 1 →
|
||||
префилл, ≥2 → выбор; «ввести вручную» всегда; never-block), интерактивный сбор ключей §4.2
|
||||
с немедленной верификацией каждого значения фактическим вызовом (Plane/Gitea API, Telegram
|
||||
getMe; лимит 3 попытки → MANUAL), автодетект хост-параметров, сборка `.env`/`.env.watchdog`
|
||||
от канонов-example (webhook-секреты — строго кирпич `gen_secrets.py`), подъём ровно
|
||||
орк+watchdog + health, регистрация проекта строго кирпичом `onboard_project.py`
|
||||
(plan→согласие→apply→verify), итоговый отчёт PASS/FAIL/MANUAL + exit `0/2/1`.
|
||||
2. **Wizard-контракт consent-gated мутаций (норматив для интерактивных операторских CLI):**
|
||||
режимы — семейные `plan`/`apply`/`verify`, но **дефолт — `apply`** (бизнес-цель «одна
|
||||
команда»); отступление от plan-default семейства безопасно по построению и допускается
|
||||
ТОЛЬКО при четырёх структурных гарантиях: (а) фаза 0 apply ≡ plan (read-only скан + печать
|
||||
плана + явный вопрос «продолжить?»), (б) ранний guard чужих конфигов до первого вопроса,
|
||||
(в) per-action consent на каждую мутацию с печатью точной команды (отказ → честный MANUAL),
|
||||
(г) non-TTY без явного `--yes` → немедленный exit 2 (зависание/мутации исключены).
|
||||
Инструменты без per-action consent (bootstrap_bundle) обязаны сохранять plan-default.
|
||||
3. **Маркер managed-файла** — норматив примирения идемпотентного resume с защитой чужих
|
||||
конфигов: первая строка собранного скриптом `.env*` — фиксированный комментарий
|
||||
`# managed by scripts/setup_lite.py (ORCH-104)`; существующий файл без маркера → отказ
|
||||
exit 2 (без `--force`), с маркером → resume-ensure (дозаполнение без перетирания значений).
|
||||
4. **Headless-канал — env-prefill + явный `--yes`** (не answers-file: секреты на диске вне
|
||||
`.env` запрещены): значения берутся из переменных окружения процесса с каноническими
|
||||
именами ключей `.env.example`; верификация обязательна и в headless; отсутствие
|
||||
обязательного значения → exit 2, не молчаливый дефолт.
|
||||
5. **Webhook Plane CE (Lite-инсталляция заказчика):** Path A (UI) — рекомендация
|
||||
(manual-checkpoint с верификацией либо честный MANUAL «проверка — smoke §11»); Path Б
|
||||
(SQL INSERT в Postgres Plane) — **офер** строго при предусловиях: docker-Plane +
|
||||
подтверждённый пользователем контейнер БД + согласие с показом SQL + идемпотентный INSERT +
|
||||
обязательная пост-верификация; только INSERT/SELECT; ввод валидируется (анти-инъекция);
|
||||
сбой → fail-safe в Path A. (Отличие от Bundled adr-0038, где bootstrap владеет инсталляцией
|
||||
и пишет безусловно.)
|
||||
6. **Машинная охрана нормативов тиража:** C-1 ORCH-100 (идентичные токены орка/watchdog →
|
||||
отказ шага), §6.4/D10 ORCH-009 (непустые `branch_protections` на `main` → FAIL с лечением,
|
||||
удаление — никогда), guard ORCH-058 (`ORCH_STAGING_PORT == прод-порт` → fail-closed),
|
||||
когерентная тройка прод-порта — одной чистой функцией.
|
||||
7. **Канон не форкается; норматив сопровождения расширен:** LITE_SETUP.md получает подраздел
|
||||
`### 1.1. Быстрый путь` (пиннинг «13 разделов в порядке» цел байт-в-байт; ручной маршрут —
|
||||
канон и fallback для MANUAL-шагов); footer-норматив NFR-5: «меняешь шаги тиража → обнови
|
||||
LITE_SETUP.md **и `scripts/setup_lite.py`** в том же PR». Канон-знания в скрипте запрещены
|
||||
структурно: кирпичи — только субпроцессами; имён Plane-статусов в скрипте нет вообще
|
||||
(зеркало `FORBIDDEN_STATUS_NEEDLES`); smoke — ссылкой на §11.
|
||||
8. **Анти-дрейф — постоянная CI-гарантия:** `tests/test_setup_lite_script.py` (ast stdlib-only,
|
||||
зеркала delete/status-needle-наборов, кирпичи упомянуты, unit чистых функций: вердикты
|
||||
скана, классификатор discovery, когерентность портов, рендер env с маркером, builder
|
||||
onboard-аргументов, step-движок/resume, exit-контракт, дефолт `apply`, секрет-гигиена
|
||||
транскрипта) + аддитивный тест в `tests/test_lite_setup_doc.py` (скрипт упомянут в доке;
|
||||
существующие ассерты и кортеж `SECTIONS` не правятся).
|
||||
|
||||
### Что НЕ меняется
|
||||
`src/**`, корневой `docker-compose.yml`, `Dockerfile`, `.env.example`, `.env.watchdog.example`,
|
||||
кирпичи `gen_secrets.py`/`onboard_project.py`/`bootstrap_bundle.py`, `deploy/**`,
|
||||
`onboarding/**`; `STAGE_TRANSITIONS`, состав `QG_CHECKS`, семантика `check_*`, machine-verdict
|
||||
ключи, схема БД — байт-в-байт. Kill-switch не вводится (активация — только явный запуск
|
||||
оператора; паттерн ORCH-009/102/103). Прод-контейнер в рамках задачи не рестартуется.
|
||||
|
||||
## Альтернативы
|
||||
- **plan-дефолт (строгий D5 ORCH-009/103)** — отвергнуто для wizard-инструмента: воспроизводит
|
||||
порог, который задача убирает; безопасность дана гарантиями п.2, а не выбором режима.
|
||||
- **State-файл прогресса / answers-file / файл-отчёт** — отвергнуты: новые артефакты = новые
|
||||
поверхности дрейфа и утечки; реальность как источник resume + env-prefill + stdout покрывают
|
||||
те же кейсы.
|
||||
- **Безусловная SQL-автоматизация webhook (как Bundled)** — отвергнуто: в Lite БД Plane — чужая
|
||||
прод-инсталляция (см. п.5).
|
||||
- **Новый раздел LITE_SETUP / перенумерация** — отвергнуто: ломает пиннинг «13 разделов» и
|
||||
внешние §-ссылки; подраздел §1.1 даёт то же бесплатно.
|
||||
|
||||
## Последствия
|
||||
- Порог входа Lite: «прочитай 13 разделов» → «запусти один файл»; ошибки конфигурации ловятся
|
||||
в момент ввода; число обращений «не взлетело» падает.
|
||||
- Платформа получает переиспользуемые нормативы: wizard-контракт consent-gated мутаций (п.2),
|
||||
маркер managed-файла (п.3), headless-канал env-prefill+`--yes` (п.4) — для будущих
|
||||
операторских CLI.
|
||||
- Цена: двойной источник маршрута (док+скрипт) — под нормативом same-PR и анти-дрейф тестами;
|
||||
дефолт `apply` расходится с семейством — зафиксировано явным тест-ассертом с обоснованием.
|
||||
- Откат: удалить скрипт, тест-модуль, §1.1/footer и аддитивный тест — состояние 1:1
|
||||
(scripts+docs+tests, без миграций).
|
||||
|
||||
## Связи
|
||||
adr-0037 (ORCH-102 — канон LITE_SETUP: автоматизируемый маршрут и пиннинг структуры),
|
||||
adr-0038 (ORCH-103 — bootstrap-паттерн: step-движок, manual-checkpoint, no-delete, кирпичи
|
||||
субпроцессами), adr-0036 (ORCH-101 — «дефолт=боевое», gen_secrets, guard ORCH-058),
|
||||
adr-0035 (ORCH-009 — onboarding-CLI: 22 статуса, D10 branch protection), adr-0033 (ORCH-100 —
|
||||
C-1 независимые Telegram-каналы). Детально —
|
||||
`docs/work-items/ORCH-104/06-adr/ADR-001-setup-lite-interactive-installer.md`,
|
||||
`07-infra-requirements.md`, `10-tech-risks.md`.
|
||||
@@ -6,6 +6,8 @@
|
||||
> Хост-специфика в командах — только плейсхолдеры `<...>` и `$ENV_VAR`.
|
||||
> Тираж **stateless**: данные/задачи/секреты исходного (боевого) хоста **НЕ переносятся**
|
||||
> ни на одном шаге — всё создаётся заново (§12).
|
||||
> **Быстрый путь — `scripts/setup_lite.py`** (§1.1): интерактивный installer проводит по
|
||||
> §2–§12 за один прогон; ручной маршрут ниже остаётся каноном и fallback'ом.
|
||||
|
||||
---
|
||||
|
||||
@@ -32,6 +34,35 @@ compose-файле, но строго за профилем `staging` и в ба
|
||||
- контейнерные пути (`/app/data`, `/repos`, `/opt/claude-code`) — layout контейнера,
|
||||
не хост-значения; не параметризуются.
|
||||
|
||||
### 1.1. Быстрый путь: `setup_lite.py` (рекомендуется)
|
||||
|
||||
Вместо ручного прохода §2–§12 запустите **интерактивный installer** — он сканирует
|
||||
предусловия хоста и предлагает доустановить недостающее, обнаруживает ваши инсталляции
|
||||
Plane/Gitea (при нескольких — даёт выбрать), запрашивает обязательные ключи **в момент
|
||||
установки с немедленной верификацией**, автодетектит хост-параметры, собирает
|
||||
`.env`/`.env.watchdog` от канонов (свежие webhook-секреты — кирпичом `gen_secrets.py`),
|
||||
поднимает ровно орк+watchdog и регистрирует ваш проект строго кирпичом `onboard_project.py`.
|
||||
|
||||
```bash
|
||||
cd <путь-чекаута> # корень репо orchestrator
|
||||
python3 scripts/setup_lite.py # apply (дефолт): интерактивная установка
|
||||
python3 scripts/setup_lite.py plan # read-only диагностика (ноль мутаций)
|
||||
python3 scripts/setup_lite.py verify # read-only пост-проверка контура
|
||||
```
|
||||
|
||||
**Контракт:** дефолт-режим `apply` — установка «одной командой»; безопасность дефолта
|
||||
структурна — фаза 0 любого `apply` ≡ `plan` (read-only скан), каждая мутация хоста — с
|
||||
**явного согласия** (per-action consent с печатью точной команды), существующий чужой
|
||||
`.env`/`.env.watchdog` **не перетирается** без `--force` (guard managed-маркера), в
|
||||
non-TTY без `--yes` — честный выход без зависания. Exit-коды: `0` — все шаги PASS; `2` —
|
||||
остановка на ручном шаге (повторный запуск продолжит с него — resume); `1` — ошибка.
|
||||
Секреты вводятся скрыто и **никогда не печатаются**; delete-операций скрипт не выполняет
|
||||
(лечение — всегда инструкция). Любой ручной шаг ссылается на соответствующий § ниже —
|
||||
ручной маршрут §2–§13 остаётся полным каноном и fallback'ом для MANUAL-шагов.
|
||||
|
||||
**Проверка:** `python3 scripts/setup_lite.py plan` завершается без блокеров (exit 0) —
|
||||
PASS; есть блокеры (exit 2) — устраните по выводу и повторите.
|
||||
|
||||
---
|
||||
|
||||
## 2. Предусловия хоста
|
||||
@@ -588,9 +619,10 @@ PR-merge API оркестратора, ручной merge не требуетс
|
||||
|
||||
---
|
||||
|
||||
*Golden source Lite-тиража (ORCH-102, ADR-001). **Норматив сопровождения (NFR-5):**
|
||||
меняешь шаги тиража (env-ключи, compose-сервисы, маршрут онбординга, smoke) → обнови
|
||||
этот док В ТОМ ЖЕ PR (правило агентов №2). Полноту и гигиену дока держит структурный
|
||||
*Golden source Lite-тиража (ORCH-102, ADR-001). **Норматив сопровождения (NFR-5, расширен
|
||||
ORCH-104):** меняешь шаги тиража (env-ключи, compose-сервисы, маршрут онбординга, smoke) →
|
||||
обнови этот док **И установочный скрипт `scripts/setup_lite.py`** В ТОМ ЖЕ PR (правило
|
||||
агентов №2). Полноту и гигиену дока держит структурный
|
||||
анти-дрейф тест `tests/test_lite_setup_doc.py`; кирпичи-каноны: REPLICATION.md (карта
|
||||
env §2, секреты §3, smoke §4), ONBOARDING.md (статусы §1, онбординг), SETUP_WEBHOOKS.md
|
||||
(формат вебхуков), `.env.example` / `.env.watchdog.example` (канон ключей).*
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
1. [business.md](business.md) — проблема, решение, ценность.
|
||||
2. [business.md → Сценарии использования](business.md#сценарии-использования) — как это выглядит в работе.
|
||||
3. [presentation.md](presentation.md) — слайдовая версия рассказа (собирается в PowerPoint).
|
||||
4. Развернуть у себя: [LITE_SETUP](../deployment/LITE_SETUP.md) (своя инфраструктура) или
|
||||
4. Развернуть у себя: [LITE_SETUP](../deployment/LITE_SETUP.md) (своя инфраструктура;
|
||||
быстрый путь — интерактивный installer `scripts/setup_lite.py`, ORCH-104) или
|
||||
[BUNDLED_SETUP](../deployment/BUNDLED_SETUP.md) (весь стек одним комплектом).
|
||||
|
||||
### Я менеджер проекта
|
||||
|
||||
7
docs/work-items/ORCH-104/00-business-request.md
Normal file
7
docs/work-items/ORCH-104/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: Установочный скрипт для lite
|
||||
|
||||
Work Item ID: ORCH-104
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
201
docs/work-items/ORCH-104/01-brd.md
Normal file
201
docs/work-items/ORCH-104/01-brd.md
Normal file
@@ -0,0 +1,201 @@
|
||||
---
|
||||
work_item: ORCH-104
|
||||
stage: analysis
|
||||
author_agent: analyst
|
||||
status: ready-for-review
|
||||
created_at: 2026-06-11
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 01 — BRD: ORCH-104 — Установочный скрипт Lite-тиража (интерактивный installer)
|
||||
|
||||
Work Item: **ORCH-104** · Repo: **orchestrator** (self-hosting) · Стадия: analysis
|
||||
Заказчик: Слава (поставщик платформы); конечный потребитель — внешний оператор Lite-тиража
|
||||
|
||||
---
|
||||
|
||||
## 1. Бизнес-контекст и проблема
|
||||
|
||||
### 1.1. Текущее состояние
|
||||
Type A эпика ORCH-10 («Lite»: заказчик разворачивает у себя только `orchestrator` +
|
||||
`orchestrator-watchdog`, подключая СВОИ Plane/Gitea/Telegram/LLM) закрыт задачей ORCH-102
|
||||
**документом**: `docs/deployment/LITE_SETUP.md` — golden source из 13 нормативных разделов,
|
||||
~30+ ручных fenced-команд и ~20 обязательных ключей нового хоста (§4.2 LITE_SETUP).
|
||||
Маршрут полный и честный (каждый шаг несёт «Проверка:»/PASS/FAIL), но **порог входа высокий**:
|
||||
|
||||
- оператор вручную собирает и переносит в `.env` значения, которые машина определяет сама
|
||||
(uid/gid владельца каталога репо, gid группы docker, пути node / дистрибутива claude-code,
|
||||
занятость портов);
|
||||
- ошибки конфигурации (символ в секрете, рассинхрон тройки прод-порта, ключи watchdog не в том
|
||||
файле-носителе, branch protection на `main`) проявляются **поздно** — на smoke или первой
|
||||
задаче, а не в момент ввода;
|
||||
- каверзные шаги (webhook Plane CE без внешнего API — §5.4; два независимых Telegram-бота —
|
||||
§8; статусы строго через onboarding-CLI — §5.3) требуют дисциплины чтения дока.
|
||||
|
||||
### 1.2. Бизнес-запрос (источник — описание задачи Plane)
|
||||
Нужен **один установочный файл**, который:
|
||||
1. на автомате выполняет установку Lite;
|
||||
2. информацию, которую знает только пользователь (токен Plane и т.п.), **запрашивает в момент
|
||||
установки**;
|
||||
3. **сканирует систему**, говорит чего не хватает и **предлагает установить**;
|
||||
4. если на хосте **несколько инсталляций** (например, Plane) — **показывает их и предлагает
|
||||
выбрать**.
|
||||
|
||||
Цель — максимально упростить установку для пользователей.
|
||||
|
||||
### 1.3. Прецедент в платформе (переиспользовать, не изобретать)
|
||||
- **Bundled (ORCH-103)** уже имеет установочный скрипт `scripts/bootstrap_bundle.py`:
|
||||
режимы `plan` (дефолт) / `apply` / `verify`, step-движок check→ensure (повторный запуск =
|
||||
каскад skip), exit-коды `0/2/1`, python stdlib-only, manual-checkpoint с обязательной
|
||||
верификацией, **ни одной delete-операции**, секреты никогда не печатаются. **Lite такого
|
||||
инструмента не имеет** — только док. ORCH-104 закрывает этот разрыв тем же выверенным
|
||||
паттерном, добавляя три новых способности: интерактивный сбор данных, скан предусловий с
|
||||
офером установки, discovery нескольких инсталляций с выбором.
|
||||
- **Кирпичи готовы:** `scripts/gen_secrets.py` (webhook-секреты; x-mode «существующий файл →
|
||||
отказ exit 2», перезапись только `--force`) и `scripts/onboard_project.py`
|
||||
(`plan`/`apply`/`verify`: Plane-проект + 22 статуса + лейблы, Gitea-репо + webhook, kit,
|
||||
merged-`ORCH_PROJECTS_JSON`). Канон статусов/лейблов **не форкается** — только кирпич.
|
||||
|
||||
### 1.4. Установленные факты (проверено по репо, не изобретать)
|
||||
- Маршрут Lite = LITE_SETUP.md §2–§12; кирпичи-каноны: REPLICATION.md (карта env §2, секреты
|
||||
§3, smoke §4), ONBOARDING.md (статусы §1), SETUP_WEBHOOKS.md, `.env.example` /
|
||||
`.env.watchdog.example` (канон 100% ключей).
|
||||
- Контур Lite: **Linux x86_64, Docker Engine + Compose v2, git, python3, node** (§2 LITE_SETUP);
|
||||
вне контура — вне гарантии.
|
||||
- Webhook Plane CE **не экспонирован во внешнем `/api/v1`** — настраивается через UI (если
|
||||
сборка показывает) или прямым SQL в Postgres Plane (§5.4 LITE_SETUP).
|
||||
- Branch protection на `main` **НЕ включать** (норматив §6.4, ADR D10 ORCH-009 / INV-4).
|
||||
- Telegram-канала **два и они независимы** (C-1 ORCH-100); токен орка для watchdog
|
||||
переиспользовать запрещено; sidecar читает только `.env.watchdog` (ловушка файла-носителя).
|
||||
- Тираж **stateless**: данные/БД/секреты боевого хоста не переносятся; секреты — только
|
||||
свежевыпущенные (§12 LITE_SETUP, REPLICATION §3/§5).
|
||||
- Скрипт живёт в репо → `git clone` чекаута остаётся единственным ручным предшагом
|
||||
(разрешает «курицу и яйцо» §3: к моменту запуска скрипта чекаут уже есть).
|
||||
- Структуру LITE_SETUP.md пиннит анти-дрейф тест `tests/test_lite_setup_doc.py`
|
||||
(«13 разделов в порядке», кирпичи, env-ключи ⊂ `.env.example`) — правка дока требует
|
||||
синхронной правки теста в том же PR.
|
||||
|
||||
---
|
||||
|
||||
## 2. Объём (scope)
|
||||
|
||||
### 2.1. В объёме
|
||||
- **Новый операторский CLI** в `scripts/` (один файл; кандидат имени — `setup_lite.py`,
|
||||
финализирует архитектор), автоматизирующий маршрут LITE_SETUP §2–§12.
|
||||
- **Скан предусловий** хоста (§2 + §7 LITE_SETUP) с честными вердиктами и **офером установки**
|
||||
недостающего (с явного согласия; неподдерживаемый дистрибутив → manual-step).
|
||||
- **Discovery инсталляций Plane/Gitea** на хосте (через локальный Docker): ≥2 → показать и
|
||||
предложить выбрать; 1 → подставить по умолчанию; 0 → ручной ввод + честная подсказка.
|
||||
- **Интерактивный сбор** обязательных ключей нового хоста (§4.2) с **немедленной верификацией**
|
||||
каждого введённого значения фактическим вызовом (Plane API / Gitea API / Telegram getMe).
|
||||
- **Автодетект хост-параметров** (uid/gid, docker gid, пути node/claude-code, порты) — то, что
|
||||
машина может узнать сама, у пользователя не спрашивается.
|
||||
- **Сборка `.env` / `.env.watchdog`** от канонов `.env.example` / `.env.watchdog.example`;
|
||||
webhook-секреты — кирпичом `gen_secrets.py`; существующие файлы молча не перетираются.
|
||||
- **Запуск Lite-контура** (ровно орк + watchdog) и health-проверки; stateless-проверка чистоты.
|
||||
- **Регистрация проекта заказчика** строго кирпичом `onboard_project.py` (plan→apply→verify).
|
||||
- **Итоговый отчёт** PASS/FAIL/MANUAL + exit-коды `0/2/1`; **идемпотентный повторный запуск**
|
||||
(resume после manual-step).
|
||||
- **Обновление `docs/deployment/LITE_SETUP.md`** (скрипт = рекомендованный быстрый путь; ручной
|
||||
маршрут сохраняется как канон-fallback) + синхронные анти-дрейф тесты.
|
||||
|
||||
### 2.2. Вне объёма (явно, не делать)
|
||||
- **Установка/администрирование Plane и Gitea** — Lite-рамка: это продукты заказчика
|
||||
(§1 LITE_SETUP); кейс «нет инфраструктуры вовсе» закрыт Bundled (ORCH-103). Скрипт только
|
||||
обнаруживает и подключает.
|
||||
- Любые правки **`src/**`**, корневого `docker-compose.yml`, `Dockerfile`, схемы БД,
|
||||
`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` — рантайм байт-в-байт.
|
||||
- Teardown / uninstall / миграция данных (stateless-канон; delete-операций в скрипте нет).
|
||||
- Поддержка не-Linux платформ (контур Lite не расширяется).
|
||||
- Автоматизация интерактивного OAuth-логина claude CLI (логин — по инструкции Anthropic,
|
||||
вручную; скрипт только верифицирует результат).
|
||||
- Самодостаточный «скачиватель» до чекаута (`curl | bash`): канал дистрибуции чекаута
|
||||
согласуется с поставщиком (§3 LITE_SETUP) и не фиксирован платформой.
|
||||
- Изменение `bootstrap_bundle.py` (Bundled) и `onboard_project.py` (кирпич переиспользуется
|
||||
как есть).
|
||||
|
||||
---
|
||||
|
||||
## 3. Заинтересованные стороны
|
||||
- **Внешний оператор/заказчик Lite** — главный потребитель: ставит платформу одной командой,
|
||||
отвечая на вопросы по ходу; не обязан заранее читать 13 разделов.
|
||||
- **Поставщик платформы (Слава/Owner)** — снижение стоимости каждого внедрения и числа
|
||||
обращений «не взлетело»; принимает результат.
|
||||
- **Платформа/конвейер** — не затрагиваются: операторский инструмент вне рантайма; прод и
|
||||
enduro-trails не подвержены риску (активация — только явный запуск человеком).
|
||||
|
||||
---
|
||||
|
||||
## 4. Бизнес-требования (BR)
|
||||
|
||||
| ID | Требование | Связь |
|
||||
|----|------------|-------|
|
||||
| BR-1 | **Один входной файл:** установка Lite выполняется одной командой из корня чекаута на голом python3; каждый шаг маршрута LITE_SETUP §2–§12 либо выполняется скриптом, либо явно выдаётся как ручной чекпоинт с верификацией результата. Молчаливых пропусков нет. | FR-1/FR-10, AC-1 |
|
||||
| BR-2 | **Скан системы:** скрипт определяет недостающие предусловия (docker/compose/git/python3/node/claude-code/uid-gid/docker-group/ssh/порты), честно перечисляет их и предлагает установить/исправить; установка — только с явного согласия пользователя; неподдерживаемое окружение → manual-step с готовыми командами. | FR-2, AC-2 |
|
||||
| BR-3 | **Запрос данных в момент установки:** все обязательные ключи нового хоста (§4.2 LITE_SETUP) запрашиваются интерактивно тогда, когда нужны; каждый токен/URL немедленно верифицируется фактическим вызовом; ввод секретов скрытый, значения секретов нигде не печатаются. | FR-4, AC-4 |
|
||||
| BR-4 | **Discovery с выбором:** при нескольких инсталляциях Plane/Gitea на хосте скрипт показывает кандидатов и предлагает выбрать; при одной — подставляет её по умолчанию; при нуле — ручной ввод и честная подсказка (Lite не ставит Plane/Gitea; «нет инфраструктуры» = маршрут Bundled). Вариант «ввести вручную» доступен всегда. | FR-3, AC-3 |
|
||||
| BR-5 | **Безопасность повторного запуска и чужих сред:** повторный запуск идемпотентен (каскад skip, resume после manual-step); существующие `.env`/`.env.watchdog` не перетираются молча; скрипт не содержит delete-операций, не трогает `main`, не рестартит чужие контейнеры; каждая мутация — с явного согласия. | FR-1/FR-6, AC-5/AC-11 |
|
||||
| BR-6 | **Результат — работающий Lite-контур:** ровно два контейнера (`orchestrator`, `orchestrator-watchdog`), `/health`–`/queue`–`/metrics` зелёные, stateless-чистота подтверждена, проект заказчика зарегистрирован кирпичом onboarding, smoke-инструкция выдана; итог прогона — однозначный вердикт + exit-код `0/2/1`. | FR-8/FR-9/FR-10, AC-9/AC-10 |
|
||||
| BR-7 | **Канон не форкается:** скрипт автоматизирует маршрут LITE_SETUP.md и использует существующие кирпичи (`gen_secrets.py`, `onboard_project.py`); сам канон-знание (статусы, ключи, нормативы) не дублирует; изменение маршрута → обновление LITE_SETUP.md в том же PR (норматив NFR-5 ORCH-102); соответствие закреплено анти-дрейф тестами. | FR-11, AC-14 |
|
||||
| BR-8 | **Рантайм не тронут:** `src/**`, корневой compose, `Dockerfile`, `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — байт-в-байт; kill-switch не нужен (активация — только явный запуск оператором; паттерн ORCH-009/102/103). | NFR-7, AC-13 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Нефункциональные требования (NFR)
|
||||
|
||||
| ID | Требование |
|
||||
|----|------------|
|
||||
| NFR-1 | **stdlib-only:** скрипт работает на голом python3 целевого хоста ДО любого venv/`docker compose up`; модули платформы (`src/**`) не импортируются; закрепляется ast-сканом в тестах (паттерн `test_bootstrap_script.py`). |
|
||||
| NFR-2 | **Безопасность среды заказчика:** никаких delete/необратимых операций; мутации — только локальный хост и только с явного согласия; БД Plane заказчика мутируется (webhook, Path Б §5.4) исключительно по явному согласию с обязательной верификацией; branch protection скрипт сам НЕ удаляет (только честный FAIL с лечением). |
|
||||
| NFR-3 | **Секрет-гигиена:** значения секретов не печатаются и не логируются (только имена ключей/пути файлов); ввод — скрытый; секреты — только свежевыпущенные (stateless, REPLICATION §3); боевые значения исходного хоста не используются даже как подсказки-дефолты. |
|
||||
| NFR-4 | **Честность шагов:** каждый шаг — явный PASS/FAIL/MANUAL со ссылкой на соответствующий § LITE_SETUP; «молчаливый пропуск запрещён» (паттерн ORCH-103). |
|
||||
| NFR-5 | **Тестируемость без TTY/сети/docker:** вся решающая логика (вердикты предусловий, классификация discovery, когерентность портов, рендер env, аргументы onboard, step-движок) — чистые функции; интерактив — за инжектируемым I/O; pytest-прогон детерминирован. |
|
||||
| NFR-6 | **Идемпотентность/resume:** step-движок check→ensure; повторный запуск пропускает выполненное и продолжает с первого незавершённого шага; manual-step → exit 2 → «resume» = просто повторный запуск (паттерн bootstrap_bundle). |
|
||||
| NFR-7 | **Self-hosting безопасность:** случайный запуск на хосте с живым продом не ломает ничего: ранний отказ по существующему `.env` (без force), read-only режим диагностики, никакого воздействия на уже бегущие контейнеры без согласия. |
|
||||
| NFR-8 | **Поддержка канона:** LITE_SETUP.md, CHANGELOG.md, витрина `docs/overview/` (если затронуто описание тиража) обновляются в том же PR (правило агентов №2); анти-дрейф тесты дока остаются зелёными. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Допущения и ограничения
|
||||
- Контур — как у Lite: Linux x86_64, Docker Engine + Compose v2; скрипт контур не расширяет.
|
||||
- Plane/Gitea — инсталляции заказчика, сетево доступны с хоста оркестратора; их установка —
|
||||
вне задачи.
|
||||
- Discovery гарантируется для **docker-инсталляций** (контейнеры с опознаваемыми
|
||||
образами/метками compose); native/k8s-инсталляции честно уходят в ручной ввод URL —
|
||||
это не FAIL discovery.
|
||||
- Запуск — из корня чекаута репо `orchestrator`; clone чекаута — ручной предшаг (канал
|
||||
дистрибуции — договорённость с поставщиком, §3 LITE_SETUP).
|
||||
- Язык UX скрипта — русский (паттерн `gen_secrets.py`/`bootstrap_bundle.py`); сообщения
|
||||
ссылаются на разделы LITE_SETUP.md как на полный канон.
|
||||
- Интерактивный OAuth-логин claude CLI принципиально не автоматизируем скриптом —
|
||||
верифицируется результат (читаемость кредов uid'ом контейнера).
|
||||
|
||||
---
|
||||
|
||||
## 7. Критерии успеха (резюме; детали — 03-acceptance-criteria.md)
|
||||
- AC-1 одна команда, режимы и идемпотентный повтор;
|
||||
- AC-2 скан предусловий с честными вердиктами и офером установки;
|
||||
- AC-3 discovery 0/1/много с выбором и ручным fallback;
|
||||
- AC-4 интерактивный сбор с немедленной верификацией и секрет-гигиеной;
|
||||
- AC-5/AC-6 корректная сборка `.env` (обязательные ключи, свежие секреты, когерентные порты,
|
||||
отказ перетирания);
|
||||
- AC-7/AC-8 нормативы C-1 (два бота) и §6.4 (branch protection) машинно охраняются;
|
||||
- AC-9 Lite-контур поднят и здоров; AC-10 проект зарегистрирован кирпичом;
|
||||
- AC-11/AC-12 exit-коды, resume, stdlib-only/no-delete гигиена;
|
||||
- AC-13 рантайм байт-в-байт; AC-14 документация обновлена синхронно.
|
||||
|
||||
---
|
||||
|
||||
## 8. Риски (кратко; детали — 10-tech-risks.md, заполняет архитектор)
|
||||
- **R-1 Запуск на боевом хосте по ошибке** → ранний guard существующего `.env`/живого
|
||||
инстанса; read-only диагностика дефолтно безопасна.
|
||||
- **R-2 Офер установки пакетов** — вмешательство в систему заказчика → только явное согласие,
|
||||
печать точной команды; неопределимый пакетный менеджер → manual-step.
|
||||
- **R-3 SQL-вставка webhook в Postgres Plane** (Path Б §5.4) инвазивна → только согласие +
|
||||
подтверждённый пользователем контейнер БД + верификация; UI-путь предпочтителен.
|
||||
- **R-4 Дрейф скрипта от канона LITE_SETUP** (двойной источник истины) → норматив same-PR +
|
||||
анти-дрейф тесты (ключи ⊂ `.env.example`, кирпичи, упоминание скрипта в доке).
|
||||
- **R-5 Интерактивность против автоматизации** (CI/non-TTY: риск зависания) → инжектируемый
|
||||
I/O, честный отказ/неинтерактивная альтернатива в non-TTY.
|
||||
- **R-6 Ложноположительный discovery** (чужой контейнер опознан как Plane) → выбор всегда за
|
||||
пользователем, ручной ввод всегда доступен, токен-верификация всё равно обязательна.
|
||||
271
docs/work-items/ORCH-104/02-trz.md
Normal file
271
docs/work-items/ORCH-104/02-trz.md
Normal file
@@ -0,0 +1,271 @@
|
||||
---
|
||||
work_item: ORCH-104
|
||||
stage: analysis
|
||||
author_agent: analyst
|
||||
status: ready-for-review
|
||||
created_at: 2026-06-11
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 02 — ТЗ (TRZ): ORCH-104 — Установочный скрипт Lite-тиража (интерактивный installer)
|
||||
|
||||
Work Item: **ORCH-104** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
> ТЗ описывает **что** должно измениться и **где** (модули/контракты/артефакты). **Как**
|
||||
> (имя/режимы скрипта, эвристики discovery, степень автоматизации отдельных шагов) — решает
|
||||
> архитектор в `06-adr/`. Тип изменения — **scripts + docs + tests** (паттерн ORCH-009/103):
|
||||
> рантайм `src/**` байт-в-байт.
|
||||
|
||||
---
|
||||
|
||||
## 1. Сводка изменения
|
||||
|
||||
Ввести **единый операторский установочный скрипт Lite-тиража**: один файл в `scripts/`,
|
||||
который интерактивно проводит внешнего оператора по маршруту `docs/deployment/LITE_SETUP.md`
|
||||
§2–§12 — сканирует предусловия хоста и предлагает доустановить недостающее, обнаруживает
|
||||
инсталляции Plane/Gitea (при нескольких — даёт выбрать), запрашивает обязательные ключи в
|
||||
момент установки с немедленной верификацией, автодетектит хост-параметры, собирает
|
||||
`.env`/`.env.watchdog` от канонов, поднимает Lite-контур, регистрирует проект заказчика
|
||||
кирпичом `onboard_project.py` и выдаёт итоговый вердикт PASS/MANUAL/FAIL с exit-кодами
|
||||
`0/2/1`. Паттерн — `bootstrap_bundle.py` (ORCH-103): step-движок check→ensure, stdlib-only,
|
||||
no-delete, manual-checkpoint с верификацией, идемпотентный повтор. Канон LITE_SETUP.md не
|
||||
форкается — скрипт становится в нём рекомендованным быстрым путём, ручной маршрут сохраняется.
|
||||
|
||||
---
|
||||
|
||||
## 2. Задействованные модули / пути
|
||||
|
||||
| Путь | Действие | Роль в задаче |
|
||||
|------|----------|---------------|
|
||||
| `scripts/setup_lite.py` *(имя-кандидат; финал — архитектор)* | **создать** | единый установочный CLI: step-движок, скан, discovery, интерактивный сбор, сборка конфигов, запуск, отчёт |
|
||||
| `scripts/gen_secrets.py` | переиспользовать, **не менять** | кирпич выпуска webhook-секретов (субпроцесс — паттерн AC-7 ORCH-103: канон-знания только субпроцессами кирпичей) |
|
||||
| `scripts/onboard_project.py` | переиспользовать, **не менять** | кирпич регистрации проекта: Plane-проект + 22 статуса + лейблы, Gitea-репо + webhook, merged-`ORCH_PROJECTS_JSON` |
|
||||
| `docs/deployment/LITE_SETUP.md` | **обновить** | скрипт = рекомендованный быстрый путь; ручной маршрут остаётся каноном-fallback; размещение не ломает пиннинг «13 разделов в порядке» ЛИБО тест обновляется синхронно |
|
||||
| `tests/test_setup_lite_script.py` *(имя-кандидат)* | **создать** | анти-дрейф + unit чистых функций (по образцу `tests/test_bootstrap_script.py`) |
|
||||
| `tests/test_lite_setup_doc.py` | обновить **при необходимости** | если меняется пиннингуемая структура LITE_SETUP.md (разделы/кирпичи) |
|
||||
| `CHANGELOG.md` | обновить | запись `feat:` |
|
||||
| `docs/overview/` | обновить **при необходимости** | если витрина описывает маршрут Lite-тиража (таблица соответствия — в индексе витрины, правило агентов №2/ORCH-011) |
|
||||
| `src/**`, `docker-compose.yml`, `Dockerfile`, `.env.example`, `.env.watchdog.example` | **НЕ менять** | каноны-источники: скрипт их только читает (шаблоны env, состав сервисов) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Функциональные требования
|
||||
|
||||
### FR-1 — Единая точка входа, режимы, идемпотентность (BR-1, BR-5)
|
||||
- Один файл; запуск `python3 scripts/setup_lite.py …` из корня чекаута репо `orchestrator`
|
||||
на голом python3 (до venv и до `docker compose up`).
|
||||
- Обязательны как минимум: **read-only режим диагностики** (ноль мутаций: скан предусловий +
|
||||
discovery + план шагов — аналог `plan` ORCH-103) и **установочный интерактивный режим**
|
||||
(аналог `apply`); желателен `verify` (read-only пост-проверка). Дефолтный режим и имена —
|
||||
решение архитектора (OQ-1) с учётом паттерна D5 ORCH-009/103 («plan — дефолт») и
|
||||
бизнес-цели «одна команда».
|
||||
- Step-движок **check→ensure**: каждый шаг сперва проверяет «уже сделано?» и при PASS
|
||||
пропускается → повторный запуск идемпотентен; «resume» после manual-step = просто повторный
|
||||
запуск (паттерн `bootstrap_bundle.run_apply`).
|
||||
- **Exit-коды (контракт):** `0` — все шаги PASS; `2` — остановка на manual-step /
|
||||
незавершённое предусловие; `1` — ошибка.
|
||||
- Каждая мутация хоста — с **явного согласия** пользователя (per-action consent); отказ от
|
||||
согласия → честный MANUAL-шаг с печатью эквивалентной команды, не молчаливый пропуск.
|
||||
|
||||
### FR-2 — Скан предусловий с офером установки (BR-2; LITE_SETUP §2, §7)
|
||||
- Проверяемый перечень (каждый пункт — отдельный вердикт `OK | MISSING | WARN | MANUAL`):
|
||||
1. ОС/арх: `uname -sm` = Linux x86_64 (иное → WARN «вне контура Lite»);
|
||||
2. `docker --version`, `docker compose version` (v2), `git --version`, `python3 --version`,
|
||||
`node --version`;
|
||||
3. дистрибутив claude-code (`npm root -g` → каталог `@anthropic-ai/claude-code`) и
|
||||
аутентификация CLI (читаемость `~/.claude/.credentials.json` uid'ом из п.5);
|
||||
4. группа docker (`getent group docker` → gid для `ORCH_DOCKER_GID`);
|
||||
5. uid/gid пользователя-владельца и каталога репозиториев (`ORCH_HOST_REPOS_DIR`,
|
||||
инвариант ORCH-040: владелец = `ORCH_RUN_UID:ORCH_RUN_GID`);
|
||||
6. каталог ssh-ключей (`ORCH_HOST_SSH_DIR`): существование/ключи;
|
||||
7. свободность портов (прод/staging, дефолты 8500/8501).
|
||||
- Для каждого `MISSING`: определить пакетный менеджер хоста (например apt/dnf/yum/zypper —
|
||||
точный набор фиксирует архитектор) → предложить **конкретную команду установки** → выполнить
|
||||
её ТОЛЬКО с явного согласия; для node+claude-code — офер `npm install -g
|
||||
@anthropic-ai/claude-code`; для ssh-ключей — офер `ssh-keygen` (+ печать pubkey как
|
||||
manual-step «добавить в Gitea», §2.4/§6.2).
|
||||
- Неопределимый дистрибутив/менеджер, отказ пользователя, sudo-недоступность → честный
|
||||
**MANUAL** с готовыми командами и ссылкой на § LITE_SETUP; молчаливый пропуск запрещён.
|
||||
- Интерактивный OAuth-логин claude CLI скрипт **не выполняет** — только верифицирует
|
||||
результат (§7.2) и выдаёт manual-step.
|
||||
|
||||
### FR-3 — Discovery инсталляций Plane/Gitea (BR-4)
|
||||
- Источник — **локальный Docker**: перечисление контейнеров, группировка в «инсталляции» по
|
||||
метке compose-проекта (`com.docker.compose.project`), опознание по именам образов
|
||||
(Plane: `makeplane/*`-семейство; Gitea: `gitea/*`) и published-портам; точные
|
||||
эвристики/паттерны — ADR. Из кандидата выводятся **предлагаемые** URL
|
||||
(`ORCH_PLANE_API_URL`/`ORCH_PLANE_WEB_URL`; `ORCH_GITEA_URL`/`ORCH_GITEA_PUBLIC_URL`).
|
||||
- Поведение по числу найденных: **0** → ручной ввод URL + честная подсказка «Lite не
|
||||
устанавливает Plane/Gitea; нет инфраструктуры — см. Bundled (BUNDLED_SETUP.md)»; **1** →
|
||||
префилл по умолчанию (с подтверждением); **≥2** → нумерованный список (проект, образы,
|
||||
порты) + выбор. Пункт **«ввести вручную»** доступен всегда.
|
||||
- **Best-effort, never-block:** docker недоступен / ошибка перечисления / не-docker
|
||||
инсталляция (native/k8s) → ручной ввод, не FAIL.
|
||||
- Discovery заполняет только кандидаты URL; **токены всегда вводит пользователь** (FR-4);
|
||||
выбранный кандидат всё равно проходит верификацию FR-4.
|
||||
|
||||
### FR-4 — Интерактивный сбор обязательных ключей с немедленной верификацией (BR-3)
|
||||
- Покрывается карта обязательных ключей нового хоста — **§4.2 LITE_SETUP** (группы: Plane,
|
||||
Gitea, webhook-секреты, Telegram, `ORCH_PROJECTS_JSON`, хост-параметры, порты).
|
||||
- Секретные значения вводятся **скрыто** (`getpass`-класс ввода) и **никогда не печатаются**
|
||||
(ни в stdout, ни в лог; только имена ключей) — паттерн NFR-3 ORCH-103.
|
||||
- Немедленная верификация каждого введённого значения фактическим вызовом:
|
||||
- Plane: `GET $ORCH_PLANE_API_URL/api/v1/workspaces/<slug>/projects/` c `X-API-Key` → 200;
|
||||
- Gitea: `GET $ORCH_GITEA_URL/api/v1/user` c токеном → 200 (+ владелец → `ORCH_GITEA_OWNER`);
|
||||
- Telegram: `getMe` → `"ok":true`; helper определения chat-id через `getUpdates`;
|
||||
- публичный URL оркестратора (для webhook'ов) — синтаксическая валидация + предупреждение,
|
||||
что достижимость со стороны Plane/Gitea проверится на smoke.
|
||||
- Неуспех верификации → re-prompt с диагнозом (ограниченное число попыток — паттерн
|
||||
`manual_checkpoint(max_tries=3)`), затем честный MANUAL/остановка exit 2 — не бесконечный
|
||||
цикл и не молчаливое принятие.
|
||||
- **non-TTY** (нет интерактива): честный отказ с подсказкой ЛИБО неинтерактивная альтернатива
|
||||
(флаги/answers-file — механизм решает архитектор, OQ-2); зависание недопустимо.
|
||||
|
||||
### FR-5 — Автодетект хост-параметров и когерентность портов (BR-2, BR-6)
|
||||
- Автоматически определяются и НЕ спрашиваются у пользователя (только подтверждение):
|
||||
`ORCH_RUN_UID`/`ORCH_RUN_GID` (uid/gid владельца `ORCH_HOST_REPOS_DIR` / текущего
|
||||
пользователя), `ORCH_DOCKER_GID` (`getent group docker`), `ORCH_HOST_NODE_BIN`
|
||||
(`which node`), `ORCH_HOST_CLAUDE_CODE_DIR` (`npm root -g`), `ORCH_AGENT_HOME_DIR` /
|
||||
`ORCH_HOST_CLAUDE_DIR` / `ORCH_HOST_CLAUDE_JSON` (HOME пользователя из §2.2),
|
||||
`ORCH_DEPLOY_HOST_REPO_PATH` (корень чекаута).
|
||||
- Порты: busy-check (`ss`/socket) прод- и staging-портов; занят → предложить альтернативу.
|
||||
Смена прод-порта → **синхронно** согласовать тройку `ORCH_DEPLOY_PROD_TARGET_PORT` ⇄
|
||||
`WATCHDOG_METRICS_URL` ⇄ `ORCH_POST_DEPLOY_BASE_URL` (§2.5/§4.2). `ORCH_STAGING_PORT` ==
|
||||
прод-порт → **отказ fail-closed** (инвариант ORCH-058, усилен ORCH-101).
|
||||
|
||||
### FR-6 — Сборка `.env` / `.env.watchdog` (BR-5; LITE_SETUP §4)
|
||||
- `.env` собирается **от канона `.env.example`** (принцип ORCH-101: дефолт = боевое значение;
|
||||
записываются только собранные отличия нового хоста). `.env.watchdog` — от
|
||||
`.env.watchdog.example` (ключи `WATCHDOG_TG_BOT_TOKEN`/`WATCHDOG_TG_CHAT_ID` кладутся
|
||||
**только туда** — ловушка файла-носителя §4.3).
|
||||
- Webhook-секреты — **кирпичом `gen_secrets.py`** (свежий выпуск; боевые секреты не
|
||||
используются — stateless §12 / REPLICATION §3).
|
||||
- **Существующий `.env`/`.env.watchdog` → отказ** (exit 2, внятное сообщение); перезапись —
|
||||
только явный force-флаг (паттерн `gen_secrets --force` / `--force-secrets` ORCH-103).
|
||||
Подсказки-дефолты промптов — из `.env.example`/автодетекта, **никогда** из боевых значений.
|
||||
- После сборки — резолв-проверка `docker compose config` (PASS/FAIL, §4 «Проверка»).
|
||||
|
||||
### FR-7 — Подключения и машинная охрана нормативов (BR-3, BR-6)
|
||||
- **Plane (§5):** наличие workspace/доступность API верифицируются (FR-4); статусы/лейблы
|
||||
вручную НЕ создаются (только кирпич onboarding, §5.3). **Webhook (§5.4, каверза Plane CE):**
|
||||
Path A (UI) = manual-checkpoint с инструкцией и верификацией; Path Б (SQL в Postgres Plane)
|
||||
= только с **явного согласия** + подтверждённый пользователем контейнер БД Plane +
|
||||
пост-верификация (`SELECT url, is_active FROM webhooks`); выбор степени автоматизации — ADR
|
||||
(OQ-3). Молчаливый пропуск запрещён.
|
||||
- **Gitea (§6):** токен верифицирован (`/api/v1/user`); **branch protection на `main`:**
|
||||
непустой `branch_protections` → **FAIL шага с лечением** (норматив §6.4 / ADR D10 ORCH-009 /
|
||||
INV-4); скрипт правила **сам не удаляет** (no-delete) — только инструкция. Per-repo webhook
|
||||
и репо создаёт кирпич onboarding (FR-9), не сам скрипт.
|
||||
- **Telegram (§8):** два независимых бота; **идентичные токены орка и watchdog → отказ шага**
|
||||
(C-1 ORCH-100 «ЗАПРЕЩЕНО» — машинная проверка, не примечание).
|
||||
- **LLM (§7):** верификация дистрибутива/нод-бинаря/читаемости кредов uid'ом контейнера;
|
||||
наличие `ORCH_AGENT_MODEL_DEFAULT`/`ORCH_AGENT_EFFORT_DEFAULT` в собранном `.env` (§7.3).
|
||||
|
||||
### FR-8 — Запуск Lite-контура и health (BR-6; LITE_SETUP §9, §12)
|
||||
- С согласия: `docker compose up -d --build`; проверка состава: запущены **ровно**
|
||||
`orchestrator` + `orchestrator-watchdog`, `orchestrator-staging` НЕ поднят (строго за
|
||||
`profiles: [staging]`).
|
||||
- Health-чек контрактов: `/health` → 200 `"status":"ok"`; `/queue` → штатный JSON;
|
||||
`/metrics` → `"schema_version": 1` (порт — фактический из конфига).
|
||||
- Stateless-проверка чистоты (§12): счётчики jobs нулевые, ни одной задачи чужих проектов;
|
||||
нарушение → FAIL с лечением «пересобрать `data/` с нуля».
|
||||
|
||||
### FR-9 — Регистрация проекта заказчика кирпичом onboarding (BR-6, BR-7; LITE_SETUP §10)
|
||||
- Скрипт собирает параметры проекта (имя/описание/repo/prefix/stack/test-cmd/порты/
|
||||
webhook-url из публичного URL оркестратора) и строит аргументы вызова
|
||||
`onboard_project.py` **детерминированной чистой функцией** (тестируемо без сети).
|
||||
- Последовательность: `plan` → показать пользователю → согласие → `apply` → `verify`
|
||||
(субпроцессы; exit 2 кирпича = остались ручные шаги → транслировать как MANUAL).
|
||||
Степень автоматизации vs «печать готовой команды» — ADR (OQ-6; прецедент драйва —
|
||||
`bootstrap_bundle.step_onboard`).
|
||||
- `ORCH_PROJECTS_JSON` из отчёта `apply` вписывается в `.env` (с согласия); затем управляемый
|
||||
рестарт **только собственного свежеподнятого** контура по процедуре §10: проверка тихого
|
||||
окна (`/queue` без running-job) → `docker compose up -d --force-recreate orchestrator` →
|
||||
пост-проверка `/health`+`/queue`.
|
||||
|
||||
### FR-10 — Итоговый отчёт (BR-1, BR-6)
|
||||
- Финальная сводная таблица всех шагов: PASS/FAIL/MANUAL; для каждого MANUAL — что сделать и
|
||||
ссылка на § LITE_SETUP; для FAIL — диагноз и лечение (зеркало §13 траблшутинга).
|
||||
- Smoke первой задачи (§11: issue → «To Analyse» → артефакты `01–04`) выдаётся как
|
||||
завершающая manual-инструкция (вердикт «тираж PASS» — за оператором).
|
||||
|
||||
### FR-11 — Документация: канон не форкается (BR-7)
|
||||
- `docs/deployment/LITE_SETUP.md`: скрипт вводится как **рекомендованный быстрый путь**
|
||||
(врезка/подраздел в начале маршрута); ручной маршрут §2–§13 сохраняется как канон и
|
||||
fallback для MANUAL-шагов. Размещение либо не ломает пиннинг
|
||||
`test_doc_exists_with_all_13_sections_in_order`, либо тест обновляется в том же PR.
|
||||
- Норматив сопровождения (NFR-5 ORCH-102) расширяется симметрично: «меняешь шаги тиража →
|
||||
обнови LITE_SETUP.md **и установочный скрипт** в том же PR».
|
||||
|
||||
---
|
||||
|
||||
## 4. Изменения API
|
||||
**Нет.** Эндпоинты оркестратора не добавляются и не меняются; скрипт — потребитель
|
||||
существующих read-only контрактов (`/health`, `/queue`, `/metrics`) и внешних API
|
||||
(Plane/Gitea/Telegram).
|
||||
|
||||
## 5. Изменения схемы БД
|
||||
**Нет.** БД оркестратора не затрагивается (создаётся пустой при первом старте — штатно);
|
||||
БД Plane заказчика — только опциональная webhook-вставка Path Б §5.4 (FR-7, с согласия).
|
||||
|
||||
## 6. Требования к новым/изменённым QG checks
|
||||
**Нет.** Скрипт — операторский инструмент вне рантайма и вне конвейера;
|
||||
`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключи — байт-в-байт.
|
||||
|
||||
## 7. Совместимость / регресс
|
||||
- **Kill-switch не нужен:** активация — только явный запуск CLI человеком (паттерн
|
||||
ORCH-009/102/103); рантайм не несёт ни одной новой кодовой ветки.
|
||||
- Полный существующий регресс `pytest tests/ -q` остаётся зелёным; `test_lite_setup_doc.py` —
|
||||
зелёный (с синхронным обновлением при правке структуры дока).
|
||||
- Обратимость: задача добавляет файлы `scripts/` + `tests/` + правку дока; откат = revert PR,
|
||||
поведение платформы не меняется в обе стороны.
|
||||
- Скрипт не вносит хост-литералов в `src/**` (вне скана `test_no_host_hardcodes.py` по
|
||||
определению, т.к. `src/**` не трогается).
|
||||
|
||||
## 8. Конфигурация
|
||||
Новых ключей `Settings`/`.env.example` **не вводится**. Скрипт читает каноны
|
||||
`.env.example`/`.env.watchdog.example` как шаблоны и пишет целевые `.env`/`.env.watchdog`
|
||||
(FR-6). Платформенные константы (`SELF_HOSTING_REPO="orchestrator"`, имена сервисов, layout
|
||||
контейнера) не параметризуются (норматив REPLICATION §1).
|
||||
|
||||
## 9. Наблюдаемость
|
||||
- Структурный stdout-лог прогона (шаг → вердикт → диагноз) + итоговая таблица (FR-10).
|
||||
- Exit-код = машинный итог прогона (`0/2/1`) — пригоден для обвязки/CI заказчика.
|
||||
- Опциональный файл-отчёт прогона — на усмотрение архитектора (ADR).
|
||||
|
||||
## 10. Артефакты pipeline (создать/обновить в ТОМ ЖЕ PR)
|
||||
- `docs/work-items/ORCH-104/06-adr/ADR-001-<slug>.md` — решения: имя/режимы скрипта,
|
||||
эвристики discovery, политика оферов установки, автоматизация webhook Path Б и onboarding,
|
||||
механизм non-TTY (архитектор).
|
||||
- `docs/deployment/LITE_SETUP.md` — быстрый путь (FR-11).
|
||||
- `tests/test_setup_lite_script.py` (+ правка `tests/test_lite_setup_doc.py` при
|
||||
необходимости).
|
||||
- `CHANGELOG.md` — запись `feat:`; витрина `docs/overview/` — если затронуто описание тиража.
|
||||
|
||||
## 11. Инварианты (не нарушать)
|
||||
- `src/**`, корневой `docker-compose.yml`, `Dockerfile`, `STAGE_TRANSITIONS`, `QG_CHECKS`,
|
||||
`check_*`, machine-verdict ключи, схема БД — **байт-в-байт**.
|
||||
- **stdlib-only**; модули платформы не импортируются; канон-знания — только субпроцессами
|
||||
кирпичей (`gen_secrets.py`, `onboard_project.py`).
|
||||
- **No-delete:** ни одной удаляющей операции (файлы, контейнеры, ветки, правила) — лечение
|
||||
всегда инструкцией.
|
||||
- **Никогда** не push/force-push/иные операции в `main` (INV-4); не рестартить чужие/боевые
|
||||
контейнеры; мутации — только с явного согласия.
|
||||
- Секреты: свежие, скрытый ввод, не печатаются; существующие `.env*` молча не перетираются.
|
||||
- Stateless: данные/задачи/секреты исходного хоста не переносятся ни одним шагом.
|
||||
|
||||
## 12. Открытые вопросы для архитектора (не блокируют анализ)
|
||||
- **OQ-1:** имя скрипта и набор/дефолт режимов: строгий паттерн D5 (`plan` — дефолт,
|
||||
мутации только в `apply`) vs «запуск без аргументов = wizard» (бизнес-цель «одна команда»).
|
||||
Возможный компромисс: wizard-дефолт, где фаза скана всегда read-only, а мутации — за
|
||||
per-action consent.
|
||||
- **OQ-2:** механизм неинтерактивного прогона (флаги / answers-file / env-переменные) и
|
||||
точное поведение в non-TTY.
|
||||
- **OQ-3:** webhook Plane: автоматизировать ли Path Б (SQL) или строго manual-checkpoint;
|
||||
критерий выбора/подтверждения контейнера Postgres Plane.
|
||||
- **OQ-4:** политика оферов установки: исполнять команды пакетного менеджера из скрипта
|
||||
(с согласия) vs только печатать (безопаснее); набор поддерживаемых менеджеров.
|
||||
- **OQ-5:** размещение быстрого пути в LITE_SETUP.md (врезка в §1 vs новый раздел с правкой
|
||||
пиннинга «13 разделов») и судьба нумерации.
|
||||
- **OQ-6:** степень драйва onboarding: субпроцесс plan→apply→verify изнутри скрипта
|
||||
(прецедент `bootstrap_bundle.step_onboard`) vs печать готовой команды.
|
||||
206
docs/work-items/ORCH-104/03-acceptance-criteria.md
Normal file
206
docs/work-items/ORCH-104/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
work_item: ORCH-104
|
||||
stage: analysis
|
||||
author_agent: analyst
|
||||
status: ready-for-review
|
||||
created_at: 2026-06-11
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-104 — Установочный скрипт Lite-тиража
|
||||
|
||||
Work Item: **ORCH-104** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
Формат: каждый критерий — чёткое условие **PASS/FAIL**. Поведенческие критерии проверяются
|
||||
детерминированными pytest-тестами (чистые функции / инжектируемый I/O / tmp-файлы — без
|
||||
реальных TTY/сети/docker); структурные — по файлам репозитория. Имя скрипта ниже —
|
||||
`scripts/setup_lite.py` (кандидат; если архитектор финализирует иное имя, критерии читаются
|
||||
с фактическим именем 1:1).
|
||||
|
||||
---
|
||||
|
||||
## AC-1 — Единая точка входа, режимы, идемпотентный повтор
|
||||
|
||||
**Условие:** скрипт существует и реализует контракт FR-1.
|
||||
- **PASS:** один файл в `scripts/`; запускается на голом python3 из корня чекаута; существует
|
||||
read-only режим диагностики (ноль мутаций ФС/docker/сети-мутаций); step-движок check→ensure:
|
||||
на «уже выполненном» хосте шаги дают каскад skip (повторный запуск не перевыполняет
|
||||
мутации); manual-step останавливает прогон с exit 2, повторный запуск продолжает с первого
|
||||
незавершённого шага.
|
||||
- **FAIL:** установка по-прежнему требует ручного прохода LITE_SETUP без скрипта; повторный
|
||||
запуск перевыполняет мутации или ломает состояние; read-only режима нет.
|
||||
|
||||
---
|
||||
|
||||
## AC-2 — Скан предусловий: честные вердикты + офер установки
|
||||
|
||||
**Условие:** функция вердиктов предусловий (FR-2) на фикстурах фактов хоста.
|
||||
- **PASS:** полный набор фактов (всё установлено) → все пункты `OK`, блокеров нет; факт
|
||||
«docker отсутствует» → вердикт `MISSING` с конкретной командой установки и запросом
|
||||
согласия (инжектированный отказ → шаг `MANUAL` с печатью команды, мутация не выполняется);
|
||||
неопределимый пакетный менеджер → `MANUAL` с готовыми командами и ссылкой на § LITE_SETUP;
|
||||
не-Linux/не-x86_64 → `WARN` «вне контура Lite». Ни один пункт перечня FR-2 не пропускается
|
||||
молча.
|
||||
- **FAIL:** отсутствующее предусловие даёт ложный `OK`/молчаливый пропуск; установка
|
||||
выполняется без явного согласия; неподдерживаемое окружение роняет скрипт вместо MANUAL.
|
||||
|
||||
---
|
||||
|
||||
## AC-3 — Discovery Plane/Gitea: 0 / 1 / много, ручной ввод всегда
|
||||
|
||||
**Условие:** классификатор discovery (FR-3) на фикстурах перечня docker-контейнеров.
|
||||
- **PASS:** фикстура с двумя независимыми Plane-инсталляциями (разные compose-проекты) →
|
||||
ровно 2 кандидата, пользователю показан нумерованный список и выбор применён; одна
|
||||
инсталляция → её URL предложен по умолчанию (с подтверждением); ноль → ручной ввод URL +
|
||||
подсказка про Bundled; ошибка/недоступность docker → ручной ввод без падения (never-block);
|
||||
пункт «ввести вручную» присутствует при любом числе кандидатов; посторонние образы в
|
||||
кандидаты не попадают.
|
||||
- **FAIL:** при ≥2 кандидатах выбор сделан автоматически без пользователя; при 0 кандидатах
|
||||
скрипт падает/блокируется; чужой контейнер опознан как кандидат без возможности отказаться.
|
||||
|
||||
---
|
||||
|
||||
## AC-4 — Интерактивный сбор: немедленная верификация + секрет-гигиена
|
||||
|
||||
**Условие:** цикл запроса значения (FR-4) с инжектированным I/O и замоканной верификацией.
|
||||
- **PASS:** неуспешная верификация (например, Plane-токен → 401) даёт re-prompt с диагнозом;
|
||||
после исчерпания лимита попыток — честный MANUAL/остановка exit 2 (не бесконечный цикл);
|
||||
успешная верификация → значение принято; секретные значения запрашиваются скрытым вводом и
|
||||
не появляются ни в stdout-транскрипте, ни в отчёте (только имена ключей); non-TTY-запуск
|
||||
без неинтерактивной альтернативы → честный отказ с подсказкой, не зависание.
|
||||
- **FAIL:** значение секрета напечатано/залогировано; неверный токен принят без верификации;
|
||||
бесконечный цикл re-prompt; зависание в non-TTY.
|
||||
|
||||
---
|
||||
|
||||
## AC-5 — Сборка `.env`/`.env.watchdog`: канон, свежие секреты, отказ перетирания
|
||||
|
||||
**Условие:** рендер конфигов (FR-6) на tmp-каталоге.
|
||||
- **PASS:** собранный `.env` содержит все группы обязательных ключей §4.2 LITE_SETUP
|
||||
(Plane, Gitea, webhook-секреты, Telegram, `ORCH_PROJECTS_JSON` — допустим как отложенный
|
||||
MANUAL до FR-9, хост-параметры, порты), а собранный `.env.watchdog` — оба ключа
|
||||
`WATCHDOG_TG_*` (и они НЕ требуются в `.env`); webhook-секреты — свежевыпущенные кирпичом
|
||||
`gen_secrets` (64 hex, при повторном выпуске значения различаются); существующий
|
||||
`.env`/`.env.watchdog` → отказ с exit 2 без force-флага, файл не изменён байт-в-байт;
|
||||
подсказки-дефолты промптов не содержат боевых значений исходного хоста.
|
||||
- **FAIL:** молчаливая перезапись существующего файла; обязательный ключ отсутствует без
|
||||
явной MANUAL-отметки; секреты скопированы/захардкожены вместо свежего выпуска.
|
||||
|
||||
---
|
||||
|
||||
## AC-6 — Порты: busy-check, когерентная тройка, staging ≠ prod
|
||||
|
||||
**Условие:** логика портов (FR-5).
|
||||
- **PASS:** занятый прод-порт → предложена альтернатива; смена прод-порта → в собранном
|
||||
`.env` синхронно согласованы `ORCH_DEPLOY_PROD_TARGET_PORT` ⇄ `WATCHDOG_METRICS_URL` ⇄
|
||||
`ORCH_POST_DEPLOY_BASE_URL`; ввод `ORCH_STAGING_PORT` равного прод-порту → отказ
|
||||
fail-closed (значение не принято).
|
||||
- **FAIL:** тройка рассинхронизирована; совпадение staging/prod принято молча.
|
||||
|
||||
---
|
||||
|
||||
## AC-7 — Норматив C-1: два независимых Telegram-бота
|
||||
|
||||
**Условие:** шаг Telegram (FR-7) получает одинаковые токены для орка и watchdog.
|
||||
- **PASS:** шаг отказывает с явным объяснением запрета (C-1 ORCH-100) и требует другой токен;
|
||||
различные токены (оба `getMe` ok) → PASS шага.
|
||||
- **FAIL:** одинаковые токены приняты молча.
|
||||
|
||||
---
|
||||
|
||||
## AC-8 — Норматив §6.4: branch protection на `main` — честный FAIL без удаления
|
||||
|
||||
**Условие:** шаг Gitea (FR-7) при непустом списке `branch_protections` репо (замокан).
|
||||
- **PASS:** шаг даёт FAIL с лечением (текст норматива §6.4: правила удалить, иначе ложные
|
||||
HOLD); скрипт НЕ выполняет удаление правил сам (no-delete); пустой список → PASS шага.
|
||||
- **FAIL:** непустые правила пропущены молча; скрипт сам удалил правила.
|
||||
|
||||
---
|
||||
|
||||
## AC-9 — Запуск Lite-контура: ровно два контейнера + health
|
||||
|
||||
**Условие:** шаг запуска (FR-8) на замоканных `compose`/HTTP-примитивах.
|
||||
- **PASS:** последовательность вызовов соответствует §9: `up -d --build` только после
|
||||
согласия; проверка состава констатирует ровно `orchestrator` + `orchestrator-watchdog`
|
||||
(поднятый `orchestrator-staging` или третий сервис → FAIL шага); health-чек требует
|
||||
`/health` 200 `"status":"ok"`, `/queue` JSON, `/metrics` `schema_version: 1`;
|
||||
stateless-проверка отмечает чужие задачи/ненулевые счётчики как FAIL с лечением §12.
|
||||
- **FAIL:** контур поднят без согласия; состав не проверяется; нездоровый инстанс получает
|
||||
PASS.
|
||||
|
||||
---
|
||||
|
||||
## AC-10 — Регистрация проекта: строго кирпич onboarding
|
||||
|
||||
**Условие:** шаг регистрации (FR-9).
|
||||
- **PASS:** аргументы `onboard_project.py` построены чистой функцией из собранных ответов
|
||||
(unit-проверяемо); последовательность `plan` → согласие → `apply` → `verify`; exit 2
|
||||
кирпича транслируется как MANUAL (не как успех); `ORCH_PROJECTS_JSON` из отчёта `apply`
|
||||
вписывается в `.env`; рестарт собственного контура — только после проверки тихого окна
|
||||
(`/queue` без running-job). Скрипт сам НЕ создаёт статусы/лейблы/репо/webhook мимо кирпича.
|
||||
- **FAIL:** скрипт дублирует канон-знания кирпича (свой список статусов/лейблов); `apply`
|
||||
вызван без показа плана/согласия; реестр не доведён до `.env` без MANUAL-отметки.
|
||||
|
||||
---
|
||||
|
||||
## AC-11 — Контракт exit-кодов и resume
|
||||
|
||||
**Условие:** прогоны step-движка на фикстурах (все шаги ok / есть manual / есть ошибка).
|
||||
- **PASS:** все PASS → exit 0; остановка на manual-step / незавершённое предусловие → exit 2;
|
||||
ошибка → exit 1; после exit 2 повторный запуск продолжает с первого незавершённого шага.
|
||||
- **FAIL:** коды перепутаны/неразличимы; manual-step считается успехом.
|
||||
|
||||
---
|
||||
|
||||
## AC-12 — Гигиена скрипта (структурно, по образцу test_bootstrap_script.py)
|
||||
|
||||
**Условие:** статический анализ файла скрипта.
|
||||
- **PASS:** импорты — только python stdlib (ast-скан); модули платформы (`src.*`) не
|
||||
импортируются; delete-операций нет (эвристический скан: `rm -rf`, `compose down -v`,
|
||||
`DELETE`-вызовы API, `git push --delete` и т.п.); импорт модуля не имеет side effects;
|
||||
значения секретов не печатаются (нет f-string/print с переменными секретов); канонические
|
||||
кирпичи (`gen_secrets.py`, `onboard_project.py`, `LITE_SETUP.md`) упомянуты/используются.
|
||||
- **FAIL:** любой пункт нарушен.
|
||||
|
||||
---
|
||||
|
||||
## AC-13 — Рантайм байт-в-байт
|
||||
|
||||
**Условие:** диф задачи.
|
||||
- **PASS:** `src/**`, корневой `docker-compose.yml`, `Dockerfile`, `.env.example`,
|
||||
`.env.watchdog.example`, схема БД, `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*` — не изменены;
|
||||
полный регресс `pytest tests/ -q` зелёный.
|
||||
- **FAIL:** любой из перечисленных путей/контрактов изменён.
|
||||
|
||||
---
|
||||
|
||||
## AC-14 — Документация синхронна (канон не форкается)
|
||||
|
||||
**Условие:** состояние доков в том же PR.
|
||||
- **PASS:** `docs/deployment/LITE_SETUP.md` вводит скрипт как рекомендованный быстрый путь и
|
||||
сохраняет ручной маршрут; `tests/test_lite_setup_doc.py` зелёный (при изменении
|
||||
пиннингуемой структуры — обновлён в том же PR); `CHANGELOG.md` несёт запись; витрина
|
||||
`docs/overview/` обновлена, если её описание тиража затронуто.
|
||||
- **FAIL:** скрипт добавлен, а LITE_SETUP.md не упоминает его / doc-тест красный /
|
||||
CHANGELOG пуст.
|
||||
|
||||
---
|
||||
|
||||
## Сводная матрица AC ↔ BR/FR
|
||||
|
||||
| AC | FR | BR | Тип проверки |
|
||||
|----|----|----|--------------|
|
||||
| AC-1 | FR-1 | BR-1, BR-5 | unit (step-движок, парсер режимов) + структурный |
|
||||
| AC-2 | FR-2 | BR-2 | unit (вердикты на фикстурах фактов) |
|
||||
| AC-3 | FR-3 | BR-4 | unit (классификатор discovery на фикстурах docker) |
|
||||
| AC-4 | FR-4 | BR-3 | unit (инжектируемый I/O, замоканная верификация) |
|
||||
| AC-5 | FR-6 | BR-3, BR-5 | unit + tmp-ФС |
|
||||
| AC-6 | FR-5 | BR-2, BR-6 | unit (чистая функция когерентности) |
|
||||
| AC-7 | FR-7 | BR-6 | unit |
|
||||
| AC-8 | FR-7 | BR-6, BR-7 | unit (замоканный Gitea API) |
|
||||
| AC-9 | FR-8 | BR-6 | unit (замоканные compose/HTTP) |
|
||||
| AC-10 | FR-9 | BR-6, BR-7 | unit (builder аргументов, трансляция exit-кодов) |
|
||||
| AC-11 | FR-1, FR-10 | BR-1, BR-5 | unit (контракт кодов, resume) |
|
||||
| AC-12 | — | BR-5, BR-7, NFR-1/2/3 | структурный (ast/эвристики) |
|
||||
| AC-13 | — | BR-8 | структурный + полный регресс |
|
||||
| AC-14 | FR-11 | BR-7 | структурный (doc-тесты) |
|
||||
197
docs/work-items/ORCH-104/04-test-plan.yaml
Normal file
197
docs/work-items/ORCH-104/04-test-plan.yaml
Normal file
@@ -0,0 +1,197 @@
|
||||
work_item: ORCH-104
|
||||
stage: analysis
|
||||
author_agent: analyst
|
||||
status: ready-for-review
|
||||
created_at: 2026-06-11
|
||||
model_used: claude-opus-4-8
|
||||
title: "Установочный скрипт Lite-тиража: интерактивный installer (setup_lite)"
|
||||
framework: pytest
|
||||
scope: >
|
||||
Новый операторский CLI scripts/setup_lite.py (имя-кандидат) + обновление
|
||||
docs/deployment/LITE_SETUP.md + анти-дрейф тесты. Вне покрытия: реальные
|
||||
TTY/сеть/docker/пакетные менеджеры (всё мокается/инжектируется), установка
|
||||
Plane/Gitea (вне объёма Lite), сквозной прогон на живом хосте (ручной smoke
|
||||
по LITE_SETUP §11 силами оператора/deploy-staging).
|
||||
notes: >
|
||||
Принципы (паттерн tests/test_bootstrap_script.py): вся решающая логика — чистые
|
||||
функции (вердикты предусловий, классификатор discovery, когерентность портов,
|
||||
рендер env, builder аргументов onboarding, step-движок), тестируемые без
|
||||
TTY/сети/docker; интерактив — через инжектируемый I/O (скриптованные ответы);
|
||||
файловые сценарии — на tmp_path; структурная гигиена — ast/эвристики по файлу
|
||||
скрипта. Полный существующий регресс tests/ обязан остаться зелёным; имя модуля
|
||||
скрипта в тестах — фактическое (если архитектор финализирует иное имя).
|
||||
|
||||
tests:
|
||||
# ---------- AC-1 / FR-1: точка входа, режимы, step-движок ----------
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "Парсер CLI: существует read-only режим диагностики (ноль мутаций) и установочный режим; набор режимов закрыт; контракт соответствует ADR (паттерн plan/apply/verify ORCH-103)"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: "Step-движок check→ensure: шаг с уже истинным check пропускается (skip) без вызова ensure; повторный прогон по фикстуре 'всё выполнено' — каскад skip, ни одной повторной мутации"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "Resume: прогон останавливается на manual-step (exit 2); повторный запуск продолжает с первого незавершённого шага, выполненные не перевыполняются"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-2 / FR-2: скан предусловий и офер установки ----------
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: "Вердикты предусловий: полный набор фактов хоста (docker/compose v2/git/python3/node/claude-code/креды/docker-группа/uid-gid/ssh/порты) → все OK, блокеров нет"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: "Факт 'docker отсутствует' → вердикт MISSING с конкретной командой установки под детектированный пакетный менеджер; инжектированный отказ от согласия → шаг MANUAL, команда напечатана, мутация НЕ выполнена"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: "Неопределимый пакетный менеджер → честный MANUAL с готовыми командами и ссылкой на § LITE_SETUP (не молчаливый пропуск, не падение); uname != Linux x86_64 → WARN 'вне контура Lite'"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-3 / FR-3: discovery Plane/Gitea ----------
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: "Классификатор discovery: фикстура docker-перечня с ДВУМЯ Plane-инсталляциями (разные compose-проекты) → ровно 2 кандидата с проектом/образами/портами; выбор пользователя применяется; пункт 'ввести вручную' присутствует"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: "Discovery: одна инсталляция → её URL префилл по умолчанию (с подтверждением); ноль инсталляций → ручной ввод + подсказка про Bundled; посторонние образы (не Plane/не Gitea) в кандидаты не попадают"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: unit
|
||||
description: "Discovery best-effort: ошибка/недоступность docker при перечислении → ручной ввод URL без падения и без блокировки прогона (never-block)"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-4 / FR-4: интерактивный сбор + верификация + секрет-гигиена ----------
|
||||
- id: TC-10
|
||||
type: unit
|
||||
description: "Цикл запроса с инжектированным I/O: верификация токена падает (401, мок) → re-prompt с диагнозом; после лимита попыток → MANUAL/остановка exit 2, НЕ бесконечный цикл; успешная верификация → значение принято"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: "Секрет-гигиена: секретные значения запрашиваются скрытым вводом и отсутствуют в stdout-транскрипте и итоговом отчёте (печатаются только имена ключей)"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: "non-TTY без неинтерактивной альтернативы → честный отказ с подсказкой (детерминированный exit), НЕ зависание на ожидании ввода"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-5 / FR-6: сборка .env / .env.watchdog ----------
|
||||
- id: TC-13
|
||||
type: integration
|
||||
description: "Рендер на tmp_path: собранный .env содержит все группы обязательных ключей §4.2 LITE_SETUP; .env.watchdog содержит WATCHDOG_TG_BOT_TOKEN/WATCHDOG_TG_CHAT_ID (файл-носитель §4.3); webhook-секреты свежие 64-hex и различаются между прогонами"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-14
|
||||
type: integration
|
||||
description: "Существующий .env (и отдельно .env.watchdog) на tmp_path → отказ exit 2 без force-флага, файл байт-в-байт не изменён; с явным force — перезапись выполняется"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-15
|
||||
type: unit
|
||||
description: "Подсказки-дефолты промптов берутся из .env.example/автодетекта и не содержат боевых значений исходного хоста (ни секретов, ни хост-литералов: переиспользовать FORBIDDEN-набор test_no_host_hardcodes)"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-6 / FR-5: порты ----------
|
||||
- id: TC-16
|
||||
type: unit
|
||||
description: "Когерентность портов: смена прод-порта → синхронно согласованы ORCH_DEPLOY_PROD_TARGET_PORT ⇄ WATCHDOG_METRICS_URL ⇄ ORCH_POST_DEPLOY_BASE_URL; занятый порт (мок busy-check) → предложена альтернатива"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-17
|
||||
type: unit
|
||||
description: "ORCH_STAGING_PORT == прод-порт → отказ fail-closed (значение не принято; инвариант ORCH-058/101)"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-7..AC-8 / FR-7: машинная охрана нормативов ----------
|
||||
- id: TC-18
|
||||
type: unit
|
||||
description: "Telegram C-1: одинаковые токены бота орка и watchdog-бота → отказ шага с объяснением запрета; различные валидные (getMe ok, мок) → PASS шага"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-19
|
||||
type: unit
|
||||
description: "Gitea branch protection (мок API): непустой branch_protections на main → FAIL шага с лечением §6.4, БЕЗ попытки удаления правил скриптом; пустой список → PASS"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-20
|
||||
type: unit
|
||||
description: "Webhook Plane Path Б (SQL): выполняется только при явном согласии — инжектированный отказ → MANUAL-чекпоинт с инструкцией UI-пути, мутирующий вызов НЕ произведён (мок); после согласия — обязательная пост-верификация"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-9 / FR-8: запуск и health ----------
|
||||
- id: TC-21
|
||||
type: unit
|
||||
description: "Шаг запуска (моки compose/HTTP): up только после согласия; состав 'ровно orchestrator + orchestrator-watchdog' → PASS, поднятый staging/третий сервис → FAIL шага; health требует /health 200 ok + /queue JSON + /metrics schema_version 1; чужие задачи в /queue → FAIL stateless-проверки"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-10 / FR-9: onboarding-кирпич ----------
|
||||
- id: TC-22
|
||||
type: unit
|
||||
description: "Builder аргументов onboard_project.py — чистая функция от собранных ответов (имя/repo/prefix/stack/test-cmd/порты/webhook-url); последовательность plan→согласие→apply→verify; exit 2 кирпича транслируется как MANUAL; скрипт не несёт собственного канона статусов/лейблов"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-11 / FR-1, FR-10: exit-коды ----------
|
||||
- id: TC-23
|
||||
type: unit
|
||||
description: "Контракт exit-кодов: все шаги PASS → 0; manual-step/незавершённое предусловие → 2; ошибка → 1; коды — именованные константы"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-12: структурная гигиена скрипта ----------
|
||||
- id: TC-24
|
||||
type: unit
|
||||
description: "ast-скан: импорты скрипта — только python stdlib; модули платформы (src.*) не импортируются; канонические кирпичи gen_secrets.py/onboard_project.py и LITE_SETUP.md упомянуты"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-25
|
||||
type: unit
|
||||
description: "Эвристический скан: delete-операций нет (rm -rf, compose down -v, DELETE-вызовы API, push --delete и т.п.); import модуля скрипта не имеет side effects (ничего не пишет/не запускает)"
|
||||
module: tests/test_setup_lite_script.py
|
||||
expected: PASS
|
||||
|
||||
# ---------- AC-13..AC-14: рантайм и документация ----------
|
||||
- id: TC-26
|
||||
type: integration
|
||||
description: "Рантайм байт-в-байт: полный существующий регресс pytest tests/ -q зелёный; src/**, корневой docker-compose.yml, Dockerfile, .env.example, .env.watchdog.example задачей не изменены"
|
||||
module: tests/ (полный регресс)
|
||||
expected: PASS
|
||||
|
||||
- id: TC-27
|
||||
type: unit
|
||||
description: "LITE_SETUP.md вводит установочный скрипт как рекомендованный быстрый путь (упоминание файла скрипта в доке) и сохраняет ручной маршрут; все проверки test_lite_setup_doc.py зелёные (при изменении пиннингуемой структуры тест обновлён в том же PR)"
|
||||
module: tests/test_lite_setup_doc.py
|
||||
expected: PASS
|
||||
@@ -0,0 +1,470 @@
|
||||
---
|
||||
work_item: ORCH-104
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-11
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# ADR-001: Интерактивный installer Lite-тиража — `scripts/setup_lite.py` (wizard поверх канона LITE_SETUP)
|
||||
|
||||
Work Item: **ORCH-104** — Установочный скрипт Lite-тиража (интерактивный installer)
|
||||
Стадия: **architecture**
|
||||
Сквозная регистрация: **`docs/architecture/adr/adr-0040-lite-interactive-installer.md`**
|
||||
(дополняет серию канонов тиража adr-0035…0038: Lite получает исполняемый инструмент;
|
||||
вводится норматив сопровождения «док + скрипт в одном PR» и wizard-контракт consent-gated
|
||||
мутаций, обязательный для будущих операторских CLI платформы).
|
||||
|
||||
## Статус
|
||||
Proposed
|
||||
|
||||
## Контекст
|
||||
|
||||
Эпик ORCH-10 закрыт по обоим типам: Type A — Lite **документом** (ORCH-102:
|
||||
`docs/deployment/LITE_SETUP.md`, 13 нормативных разделов, ~30+ ручных fenced-команд), Type B —
|
||||
Bundled **комплектом + скриптом** (ORCH-103: `deploy/bundled/` + `scripts/bootstrap_bundle.py`).
|
||||
Асимметрия: у Lite нет инструмента — порог входа высокий (BRD §1.1: оператор вручную собирает
|
||||
значения, которые машина определяет сама; ошибки конфигурации проявляются поздно). Бизнес-запрос
|
||||
Владельца (BRD §1.2): один установочный файл, который выполняет установку, запрашивает данные в
|
||||
момент установки, сканирует систему с офером доустановки и даёт выбор при нескольких инсталляциях.
|
||||
|
||||
Факты, сверенные с репо (не изобретать):
|
||||
|
||||
- **Паттерн step-движка готов:** `scripts/bootstrap_bundle.py` (ORCH-103 D5) — режимы
|
||||
`plan` (дефолт)/`apply`/`verify`, check→ensure (повторный запуск = каскад skip), exit `0/2/1`,
|
||||
`manual_checkpoint(title, instructions, verify, max_tries=3)` (без TTY — немедленный exit 2),
|
||||
чистые функции `parse_env`/`render_env`/`preflight_verdict`/`build_plan` под unit-тестами,
|
||||
`_ensure_venv` (host-venv для onboard-кирпича, probe `import httpx, pydantic`),
|
||||
`_psql` (SQL через stdin — секреты не в argv), инвариант `APPLY_STEPS == build_plan()`.
|
||||
- **Кирпичи:** `scripts/gen_secrets.py` (`--write PATH` x-mode: существующий файл → exit 2,
|
||||
перезапись только `--force`; `secrets.token_hex(32)` → 64 hex); `scripts/onboard_project.py`
|
||||
(`plan|apply|verify`, обязательные `--name/--repo/--prefix/--stack/--test-cmd/--prod-port/
|
||||
--staging-port/--webhook-url`, опции `--gitea-owner/--env-file/--json`, exit 0/2/1).
|
||||
- **Анти-дрейф канона:** `tests/test_lite_setup_doc.py` пиннит кортеж `SECTIONS` —
|
||||
заголовки `## 1.`…`## 13.` в порядке маршрута (TC-01), «каждый раздел §2–§13 несёт
|
||||
fenced-команду» проверяется только для `SECTIONS[1:]` (§1 — без требования команд), каждый
|
||||
упомянутый в доке `ORCH_*`/`WATCHDOG_*`-токен обязан существовать в `.env.example`
|
||||
(`_ENV_TOKEN_RE`), fenced-блоки сканируются на `FORBIDDEN`-литералы и секрет-эвристику
|
||||
(hex≥32 / alnum≥40).
|
||||
- **Структурная гигиена скриптов:** `tests/test_bootstrap_script.py` — ast-скан stdlib-allowlist,
|
||||
`FORBIDDEN_DELETE_NEEDLES` (`volume rm`, `rm -rf`, `down -v`, `compose down`, `rmtree`,
|
||||
`os.remove`, `.unlink`), `FORBIDDEN_STATUS_NEEDLES` (`Backlog`, `To Analyse`, `Confirm Deploy`,
|
||||
…) — собственный канон статусов в скрипте запрещён.
|
||||
- **Сеть нашего контура:** все три сервиса корневого `docker-compose.yml` —
|
||||
`network_mode: host` → из контейнера орка `http://127.0.0.1:<published-port>` достигает
|
||||
сервисов хоста; URL-кандидаты discovery на published-портах валидны для API-ключей.
|
||||
- **Целевой режим резолва модели/эффорта, статусов и секретов** — только кирпичи/конфиг
|
||||
(ORCH-41/74, `plane_sync._PLANE_NAME_TO_KEY`, gen_secrets): скрипт канон-знаний не дублирует.
|
||||
|
||||
ТЗ (02-trz.md §12) оставило архитектору OQ-1…OQ-6. Ниже — пакет решений D1…D12.
|
||||
Тип изменения — **scripts + docs + tests**: рантайм `src/**` байт-в-байт (BR-8/AC-13).
|
||||
|
||||
## Решение
|
||||
|
||||
### Сводка
|
||||
|
||||
Новый операторский CLI **`scripts/setup_lite.py`** (имя финализировано, зеркалит
|
||||
`docs/deployment/LITE_SETUP.md`): python stdlib-only wizard, автоматизирующий маршрут
|
||||
LITE_SETUP §2–§12. Режимы — семейные **`plan` / `apply` / `verify`**, но **дефолт — `apply`**
|
||||
(интерактивная установка; осознанное отступление от plan-default семейства — D2): бизнес-цель
|
||||
«одна команда» — суть задачи, а безопасность дефолта обеспечена структурно (фаза 0 ≡ plan,
|
||||
ранний guard `.env`, per-action consent на каждую мутацию, non-TTY → exit 2 до мутаций).
|
||||
Step-движок check→ensure, 10 нормативных шагов (D3), exit `0/2/1`, resume = повторный запуск
|
||||
без state-файла (реальность — единственный источник истины; коллизию «resume против guard
|
||||
существующего `.env`» решает маркер-строка managed-файла — D6). Скан предусловий с офером
|
||||
доустановки per-package consent'ом (D4), discovery docker-инсталляций Plane/Gitea строго по
|
||||
image-префиксам с выбором пользователя (D5), интерактивный сбор ключей §4.2 с немедленной
|
||||
верификацией и секрет-гигиеной (getpass, env-prefill + `--yes` для headless — D10), webhook
|
||||
Plane CE: Path A (UI) — рекомендация, Path Б (SQL) — офер под пятью предусловиями (D8),
|
||||
машинная охрана нормативов C-1 / §6.4 / ORCH-058 (D9), onboarding строго кирпичом
|
||||
plan→согласие→apply→verify (D11). Канон не форкается: LITE_SETUP получает подраздел
|
||||
`### 1.1. Быстрый путь` (пиннинг «13 разделов» цел байт-в-байт — D12).
|
||||
|
||||
### D1 — Имя, место, рамка: `scripts/setup_lite.py`, один файл, stdlib-only (OQ-1-часть)
|
||||
|
||||
- **`scripts/setup_lite.py`** — один файл в `scripts/` рядом с кирпичами; имя зеркалит док-канон
|
||||
(`LITE_SETUP.md` ⇄ `setup_lite.py` — мнемоническая пара «инструкция ⇄ инструмент»).
|
||||
Альтернатива `bootstrap_lite.py` отвергнута: bootstrap в платформе уже значит «доводка
|
||||
Bundled-стека» (ORCH-103); у Lite семантика иная — установка поверх инфраструктуры заказчика.
|
||||
- **python stdlib-only** (NFR-1): запуск `python3 scripts/setup_lite.py` из корня чекаута на
|
||||
голом python3 ДО venv и до `docker compose up`; импорты — только stdlib-allowlist (ast-скан,
|
||||
паттерн `test_bootstrap_script.py::STDLIB_ALLOWED`); `src.*` не импортируется; канон-знания —
|
||||
только субпроцессами кирпичей (`gen_secrets.py`, `onboard_project.py`).
|
||||
- **Язык UX — русский** (паттерн gen_secrets/bootstrap_bundle); каждое сообщение шага ссылается
|
||||
на соответствующий § LITE_SETUP как на полный канон (NFR-4).
|
||||
- Рамка Lite не расширяется: Linux x86_64 + Docker/Compose v2; установка Plane/Gitea — вне
|
||||
объёма (подсказка «нет инфраструктуры → Bundled»); teardown/uninstall отсутствуют.
|
||||
|
||||
### D2 — Режимы и дефолт: `plan`/`apply`/`verify`, дефолт — `apply`-wizard (OQ-1)
|
||||
|
||||
- **Закрытый набор режимов** — семейная лексика ORCH-009/103: `plan`, `apply`, `verify`
|
||||
(никаких новых слов `wizard`/`install`: оператор, выучивший bootstrap_bundle, понимает
|
||||
setup_lite мгновенно; TC-01 «набор режимов закрыт»).
|
||||
- **Контракты `plan` и `verify` — 1:1 с семейством:** `plan` — строгий read-only (скан
|
||||
предусловий + discovery + автодетект + печать плана шагов; ноль мутаций ФС/docker/сети;
|
||||
exit 0 — блокеров нет / 2 — есть); `verify` — read-only пост-проверка (`/health`+`/queue`+
|
||||
`/metrics`, состав «ровно орк+watchdog», stateless-чистота §12, `onboard_project.py verify`
|
||||
субпроцессом при доступном venv). Оба — полноценные non-TTY-режимы (CI-пригодны).
|
||||
- **Дефолт без аргументов — `apply`** (отступление от plan-default семейства, осознанное и
|
||||
тестируемое): бизнес-запрос Владельца — «один установочный файл, который на автомате
|
||||
выполняет установку» (BRD §1.2 п.1); plan-default воспроизвёл бы порог, который задача
|
||||
призвана убрать. **Семейный инвариант сохранён в его сути** — «запуск без аргументов не
|
||||
мутирует без явного согласия» — но другим механизмом: у bootstrap_bundle `apply` мутирует
|
||||
без per-action вопросов (его согласие = выбор режима), поэтому там безопасен только
|
||||
plan-default; у setup_lite **каждая мутация отдельно согласуется** (FR-1), поэтому
|
||||
apply-default безопасен по построению:
|
||||
1. **Фаза 0 `apply` ≡ `plan`:** read-only скан + discovery + печать плана; первый вопрос
|
||||
оператору задаётся только после неё («Продолжить установку? [y/N]» — дефолт-ответ N);
|
||||
2. **Ранний guard `.env`:** существующий немаркированный `.env`/`.env.watchdog` (D6) →
|
||||
отказ exit 2 ДО первого вопроса установки (NFR-7: живой хост защищён);
|
||||
3. **Per-action consent:** каждая мутация (установка пакета, запись файла, SQL, `up`,
|
||||
рестарт) — отдельное согласие с печатью точной команды; отказ → честный MANUAL
|
||||
с эквивалентной командой, не молчаливый пропуск;
|
||||
4. **non-TTY:** `apply` без `--yes` → немедленный честный exit 2 с подсказкой
|
||||
(никаких зависаний и мутаций — D10).
|
||||
- Тест-контракт (новый модуль, D12): `parse_args([]).mode == "apply"` — **сознательно**
|
||||
зеркальный к `test_plan_is_default_mode` bootstrap'а ассерт с комментарием-обоснованием;
|
||||
набор режимов закрыт `choices=("plan", "apply", "verify")`.
|
||||
- **Флаги:** `--force` (разрешить перезапись существующих НЕмаркированных `.env*` — см. D6;
|
||||
печать пути + согласие всё равно запрашивается), `--yes` (headless-consent, D10). Параметры
|
||||
проекта заказчика (для onboarding-шага) — опциональные `--project-*`-флаги как
|
||||
альтернатива интерактивному вводу (точный список — developer, зеркало sandbox-флагов
|
||||
bootstrap_bundle).
|
||||
|
||||
### D3 — Step-движок: 10 нормативных шагов, check→ensure, без state-файла (FR-1)
|
||||
|
||||
- **Нормативный план `apply`** (механика — паттерн `build_plan()`/`APPLY_STEPS` ORCH-103;
|
||||
инвариант «APPLY_STEPS == build_plan()» — под тестом):
|
||||
|
||||
| # | Шаг | § LITE_SETUP | Суть |
|
||||
|---|-----|--------------|------|
|
||||
| 1 | `scan` | §2, §7 | read-only скан предусловий + автодетект хост-параметров + ранний guard `.env` (D6) |
|
||||
| 2 | `prereqs` | §2, §7 | доустановка MISSING per-package consent'ом / MANUAL (D4) |
|
||||
| 3 | `discovery` | §5, §6 | обнаружение инсталляций Plane/Gitea, выбор / ручной ввод (D5) |
|
||||
| 4 | `collect` | §4.2, §5–§8 | интерактивный сбор ключей с немедленной верификацией (D9, D10) |
|
||||
| 5 | `render-env` | §4 | сборка `.env`/`.env.watchdog` от канонов + gen_secrets + `docker compose config` (D6, D7) |
|
||||
| 6 | `plane-webhook` | §5.4 | Path A manual-checkpoint / Path Б офер SQL (D8) |
|
||||
| 7 | `gitea-guards` | §6 | branch_protections == `[]` (FAIL+лечение), ssh-pubkey manual-step (D9) |
|
||||
| 8 | `up` | §9 | `docker compose up -d --build` с согласия; состав «ровно орк+watchdog»; health-чек |
|
||||
| 9 | `onboard` | §10 | кирпич plan→согласие→apply→verify; `ORCH_PROJECTS_JSON` → `.env`; управляемый рестарт (D11) |
|
||||
| 10 | `report` | §11, §12 | stateless-проверка; итоговая таблица PASS/FAIL/MANUAL; smoke-инструкция ссылкой на §11 |
|
||||
|
||||
- **check→ensure:** каждый шаг сперва проверяет «уже сделано?» по реальности (файл существует и
|
||||
маркирован; токен валиден API-пробой; контейнеры бегут; webhook существует) и при PASS
|
||||
пропускается. **State-файл НЕ вводится** (альтернатива отвергнута: новый артефакт = новая
|
||||
поверхность дрейфа/устаревания; реальность — единственный источник истины; resume после
|
||||
manual-step = просто повторный запуск — каскад skip доводит до первого незавершённого шага,
|
||||
NFR-6; паттерн bootstrap_bundle).
|
||||
- **Exit-коды — именованные константы** `EXIT_OK=0` / `EXIT_MANUAL=2` / `EXIT_ERROR=1`
|
||||
(контракт FR-1/AC-11; исключения `ManualStop`/`SetupError` — паттерн bootstrap).
|
||||
- **Smoke-инструкция шага 10 — ссылкой на LITE_SETUP §11**, без текста статусов: скрипт не несёт
|
||||
имён Plane-статусов вообще (новый тест зеркалит `FORBIDDEN_STATUS_NEEDLES` — D12); вердикт
|
||||
«тираж PASS» остаётся за оператором (FR-10).
|
||||
- **Итоговая таблица** (FR-10): шаг → PASS/FAIL/MANUAL/skip; для MANUAL — что сделать + § дока;
|
||||
для FAIL — диагноз и лечение (зеркало §13). Опциональный файл-отчёт НЕ вводится (stdout +
|
||||
exit-код достаточны; меньше артефактов — меньше утечек, NFR-3).
|
||||
|
||||
### D4 — Скан предусловий и оферы установки: per-package consent, закрытый набор менеджеров (OQ-4)
|
||||
|
||||
- **Вердикты — чистая функция** `prereq_verdicts(facts) -> [(item, OK|MISSING|WARN|MANUAL,
|
||||
detail)]` от read-only снимка фактов хоста (паттерн `collect_facts`/`preflight_verdict`):
|
||||
ОС/арх (`uname -sm`; не-Linux/не-x86_64 → **WARN «вне контура Lite»**, не FAIL), docker,
|
||||
compose v2, git, python3, node, дистрибутив claude-code (`npm root -g` →
|
||||
`@anthropic-ai/claude-code`), аутентификация CLI (читаемость `~/.claude/.credentials.json`
|
||||
uid'ом будущего контейнера — §7.2), группа docker (`getent group docker`), uid/gid владельца
|
||||
каталога репо (§2.2), ssh-каталог (§2.4), свободность портов (§2.5). Ни один пункт перечня
|
||||
FR-2 не пропускается молча (AC-2).
|
||||
- **Оферы установки — исполняются скриптом, но только с явного per-package согласия**
|
||||
(бизнес-запрос п.3 «предлагает установить»; BRD R-2): печать **точной команды** ДО исполнения
|
||||
→ согласие → исполнение → **re-check фактом** (повторная проверка версии/наличия; для compose
|
||||
обязательно `docker compose version` — нативные репо дистрибутивов могут нести v1!) →
|
||||
не сошлось → честный MANUAL со ссылкой на официальную инструкцию. Отказ от согласия →
|
||||
MANUAL с той же командой (TC-05).
|
||||
- **Закрытый набор пакетных менеджеров: `apt-get` / `dnf` / `yum` / `zypper`** (детект по
|
||||
наличию бинаря, в этом порядке; `apt-get`, не `apt` — стабильный CLI). Неопределимый
|
||||
менеджер (например, pacman/alpine) → MANUAL с готовыми generic-командами и ссылкой на
|
||||
§ LITE_SETUP — не падение (TC-06). Точные имена пакетов per-менеджер — developer (карта-
|
||||
константа в скрипте); политика зафиксирована здесь.
|
||||
- **Sudo-честность:** требуется root, а его нет (не root и sudo недоступен) → MANUAL с командой
|
||||
под sudo; скрипт не пытается эскалировать привилегии сам.
|
||||
- **Спец-случаи:** node+claude-code — офер `npm install -g @anthropic-ai/claude-code` (node —
|
||||
пакетом менеджера); ssh-ключи — офер `ssh-keygen -t ed25519` + печать pubkey как manual-step
|
||||
«добавить в Gitea» (§2.4/§6.2); **OAuth-логин claude CLI не автоматизируется** — только
|
||||
верификация результата + manual-step (§7.2, допущение BRD §6).
|
||||
- **Автодетект хост-параметров (FR-5)** — спрашивается только подтверждение, не значения:
|
||||
`ORCH_RUN_UID`/`ORCH_RUN_GID` (владелец `ORCH_HOST_REPOS_DIR` / текущий пользователь),
|
||||
`ORCH_DOCKER_GID`, `ORCH_HOST_NODE_BIN` (`which node`), `ORCH_HOST_CLAUDE_CODE_DIR`
|
||||
(`npm root -g`), `ORCH_AGENT_HOME_DIR`/`ORCH_HOST_CLAUDE_DIR`/`ORCH_HOST_CLAUDE_JSON`
|
||||
(HOME из §2.2), `ORCH_DEPLOY_HOST_REPO_PATH` (корень чекаута).
|
||||
|
||||
### D5 — Discovery Plane/Gitea: строго image-префиксы, выбор всегда за пользователем (FR-3)
|
||||
|
||||
- **Источник — только локальный Docker:** перечисление контейнеров (`docker ps` с форматом,
|
||||
дающим имя/образ/published-порты/метку `com.docker.compose.project`); группировка контейнеров
|
||||
в «инсталляции» по метке compose-проекта (без метки → группа-одиночка по имени контейнера).
|
||||
- **Опознание — строго по префиксам образов** (анти-ложноположительность R-6; имена
|
||||
контейнеров/проектов НЕ используются как признак): Plane — образы `makeplane/*`;
|
||||
Gitea — `gitea/gitea*` и `docker.gitea.com/gitea*`. Посторонние образы в кандидаты не
|
||||
попадают (AC-3). Список префиксов — константа скрипта (расширение = осознанная правка под
|
||||
тестом).
|
||||
- **URL-кандидаты из published-портов:** Plane — порт контейнера-входа (образ
|
||||
`makeplane/plane-proxy`; нет proxy → инсталляция показывается без URL-префилла);
|
||||
Gitea — published-порт на контейнерный 3000. Предлагаются: API-URL `http://127.0.0.1:<port>`
|
||||
(валидно: корневой compose — `network_mode: host`, контейнер орка видит published-порты хоста
|
||||
через loopback) и публичный URL `http://<hostname>:<port>` (hostname — ввод/подтверждение
|
||||
оператора; для webhook'ов нужен адрес, достижимый со стороны Plane/Gitea).
|
||||
- **Поведение по числу кандидатов (BR-4):** `0` → ручной ввод URL + честная подсказка «Lite не
|
||||
устанавливает Plane/Gitea; нет инфраструктуры — маршрут Bundled (BUNDLED_SETUP.md)»; `1` →
|
||||
префилл по умолчанию с подтверждением; `≥2` → нумерованный список (compose-проект, образы,
|
||||
порты) + выбор. Пункт **«ввести вручную» доступен всегда** (включая случай 1 и ≥2).
|
||||
- **Best-effort, never-block:** docker недоступен / ошибка перечисления / native- или
|
||||
k8s-инсталляция → ручной ввод URL, не FAIL и не падение (TC-09). Discovery заполняет только
|
||||
кандидаты URL; **токены всегда вводит пользователь** (FR-4), выбранный кандидат всё равно
|
||||
проходит токен-верификацию.
|
||||
- **Постгрес-кандидат для Path Б (D8):** из выбранной Plane-инсталляции — контейнеры с образом
|
||||
`postgres*` ТОГО ЖЕ compose-проекта; подтверждение пользователя обязательно.
|
||||
|
||||
### D6 — Сборка `.env`/`.env.watchdog`: рендер от канонов + маркер managed-файла (FR-6; решает «resume против guard»)
|
||||
|
||||
- **Рендер от канонов** `.env.example`/`.env.watchdog.example` чистой функцией (паттерн
|
||||
`render_env` bootstrap: строки/комментарии канона сохраняются, значения подставляются;
|
||||
принцип ORCH-101 «дефолт = боевое значение» — записываются только собранные отличия).
|
||||
`WATCHDOG_TG_BOT_TOKEN`/`WATCHDOG_TG_CHAT_ID` кладутся **только** в `.env.watchdog`
|
||||
(ловушка файла-носителя §4.3); подсказки-дефолты промптов — из `.env.example`/автодетекта,
|
||||
**никогда** из боевых значений (TC-15: FORBIDDEN-набор переиспользуется).
|
||||
- **Маркер managed-файла — ключ к resume:** первая строка каждого собранного скриптом файла —
|
||||
фиксированный комментарий-маркер `# managed by scripts/setup_lite.py (ORCH-104)`. Семантика
|
||||
guard'а на шаге `scan`:
|
||||
- файла нет → штатный путь (рендер на шаге 5);
|
||||
- файл есть, **маркера нет** → это чужой/живой конфиг → **отказ exit 2 без `--force`**,
|
||||
файл байт-в-байт не тронут (NFR-7/AC-5/TC-14; `--force` = явная перезапись с согласием);
|
||||
- файл есть, **маркер есть** → собран нами ранее → **resume-ensure**: дозаполнение
|
||||
недостающих ключей, существующие значения не перетираются (NFR-6; без маркера resume после
|
||||
manual-step на шаге 6+ был бы невозможен — guard отбивал бы собственный артефакт).
|
||||
- **Webhook-секреты — строго кирпичом:** субпроцесс `gen_secrets.py --write <tmpdir>/fragment`
|
||||
→ парс фрагмента → перенос двух ключей (паттерн `step_secrets` bootstrap); свежий выпуск,
|
||||
боевые секреты не используются (stateless §12). Запись live-файлов — права `600`, содержимое
|
||||
не печатается (NFR-3).
|
||||
- **Проверка шага:** `docker compose config` → PASS/FAIL (§4 «Проверка»; ошибка интерполяции →
|
||||
диагноз «ищите незакрытую кавычку/невалидный JSON в ORCH_PROJECTS_JSON»).
|
||||
|
||||
### D7 — Порты: busy-check, когерентная тройка одной функцией, staging ≠ prod fail-closed (FR-5)
|
||||
|
||||
- Busy-check прод/staging-портов сокетом (паттерн `_port_busy`); занят → предложить
|
||||
альтернативу (ввод с дефолтом «порт+N»).
|
||||
- **Когерентность тройки — механически, одной чистой функцией** `port_overrides(prod_port) ->
|
||||
{ORCH_DEPLOY_PROD_TARGET_PORT, WATCHDOG_METRICS_URL, ORCH_POST_DEPLOY_BASE_URL}` — рассинхрон
|
||||
структурно невозможен (TC-16; ловушка §2.5/§4.2 закрывается кодом, не дисциплиной).
|
||||
- **`ORCH_STAGING_PORT == прод-порт` → отказ fail-closed** на вводе (значение не принято,
|
||||
re-prompt; инвариант ORCH-058, усиленный ORCH-101; TC-17).
|
||||
|
||||
### D8 — Webhook Plane CE: Path A (UI) — рекомендация, Path Б (SQL) — офер под предусловиями (OQ-3)
|
||||
|
||||
Каверза Plane CE: webhook не экспонирован во внешнем `/api/v1` (§5.4). В Bundled bootstrap
|
||||
выполняет SQL-вставку безусловно — он **владеет** инсталляцией (сам её создал, знает контейнер
|
||||
и креды). В Lite Plane — **продукт заказчика**: контейнер/пароль БД скрипту неизвестны,
|
||||
инсталляция может быть не-docker, мутация чужой прод-БД инвазивна (BRD R-3, NFR-2:
|
||||
«UI-путь предпочтителен»). Решение — двухпутёвый шаг с приоритетом UI:
|
||||
|
||||
- **Path A (UI) — дефолт-рекомендация:** `manual_checkpoint`-контракт (печать точной инструкции
|
||||
§5.4 с подставленными URL приёмника и именем секрета — значение секрета НЕ печатается, только
|
||||
имя ключа) → подтверждение оператора → верификация. Верификация Path A: при доступных
|
||||
координатах БД (введены для Path Б/проверки) — механический `SELECT url, is_active FROM
|
||||
webhooks`; иначе — честная деградация: шаг фиксируется **MANUAL** («подтверждено оператором,
|
||||
сквозная проверка доставки — smoke §11»), НЕ выдаётся за механически проверенный PASS (NFR-4).
|
||||
- **Path Б (SQL INSERT) — офер автоматизации**, исполняется ТОЛЬКО при всех предусловиях:
|
||||
1. Plane-инсталляция — docker, и **Postgres-контейнер выбран и подтверждён пользователем**
|
||||
(кандидаты — D5: `postgres*`-образы того же compose-проекта);
|
||||
2. **явное согласие с показом точного SQL** (INSERT канона §5.4) ДО исполнения;
|
||||
3. пароль БД Plane — **скрытый ввод** (или env-prefill, D10); в argv не попадает —
|
||||
SQL и `PGPASSWORD` передаются через stdin/env субпроцесса (паттерн `_psql` bootstrap);
|
||||
4. **идемпотентность:** сначала `SELECT count(*)` по URL приёмника (`deleted_at IS NULL`) —
|
||||
уже зарегистрирован → skip (паттерн `_exists` bootstrap);
|
||||
5. **обязательная пост-верификация** `SELECT url, is_active` → нет строки → шаг НЕ PASS.
|
||||
- **Только INSERT/SELECT** — никаких UPDATE/DELETE (no-delete распространяется на чужую БД);
|
||||
**slug и прочие подстановки валидируются** (`^[a-z0-9-]+$` для slug; UUID — `uuid.uuid4()`
|
||||
локально; секрет — через psql-переменную/stdin, не конкатенация в argv) — анти-SQL-инъекция
|
||||
на пользовательском вводе (отличие от bundle, где значения самогенерированные).
|
||||
- Любой сбой Path Б / отказ согласия / не-docker Plane → **fail-safe в Path A** (manual-checkpoint
|
||||
c инструкцией UI-пути; мутирующий вызов не произведён — TC-20).
|
||||
|
||||
### D9 — Машинная охрана нормативов: C-1, §6.4, верификация каждого ввода (FR-4/FR-7)
|
||||
|
||||
- **Немедленная верификация каждого введённого значения фактическим вызовом** (FR-4):
|
||||
Plane-токен — `GET /api/v1/workspaces/<slug>/projects/` c `X-API-Key` → 200; Gitea-токен —
|
||||
`GET /api/v1/user` → 200 (+ логин владельца → префилл `ORCH_GITEA_OWNER`); Telegram —
|
||||
`getMe` → `"ok":true` + helper chat-id через `getUpdates`; публичный URL оркестратора —
|
||||
синтаксическая валидация + предупреждение «достижимость со стороны Plane/Gitea проверится
|
||||
на smoke». Неуспех → re-prompt с диагнозом, **лимит 3 попытки** (паттерн
|
||||
`manual_checkpoint(max_tries=3)`) → MANUAL/exit 2, не бесконечный цикл (TC-10).
|
||||
- **C-1 (ORCH-100) — машинно:** токен watchdog-бота **байт-в-байт равен** токену орка → отказ
|
||||
шага с объяснением запрета и требованием другого токена (TC-18); оба токена — скрытый ввод.
|
||||
- **§6.4 — branch protection:** `GET /api/v1/repos/<owner>/<repo>/branch_protections` непуст →
|
||||
**FAIL шага с лечением** (текст норматива: правила удалить, иначе ложные HOLD — §13.7);
|
||||
скрипт правила **сам не удаляет** (no-delete; TC-19). Проверка — для репо проекта заказчика
|
||||
после onboarding (шаг 9) и/или по введённому owner/repo.
|
||||
- **§7.3:** наличие непустых `ORCH_AGENT_MODEL_DEFAULT`/`ORCH_AGENT_EFFORT_DEFAULT` в собранном
|
||||
`.env` (дефолты канона `.env.example` достаточны — проверка, не ввод).
|
||||
|
||||
### D10 — non-TTY и headless: env-prefill + явный `--yes`; answers-file отвергнут (OQ-2)
|
||||
|
||||
- **Интерактив — за инжектируемым I/O** (NFR-5): примитивы `ask(key, secret=False)` /
|
||||
`consent(action)` принимают источники ввода/вывода параметрами → unit-тесты со
|
||||
скриптованными ответами без TTY (TC-10…TC-12).
|
||||
- **Env-prefill — неинтерактивная альтернатива без новой лексики:** `ask()` ПЕРЕД промптом
|
||||
проверяет переменную окружения процесса с **тем же каноническим именем ключа**
|
||||
(`ORCH_PLANE_API_TOKEN`, `WATCHDOG_TG_BOT_TOKEN`, …) — значение найдено → принимается без
|
||||
промпта, **но верификация выполняется как обычно** (неверный токен из env → в TTY re-prompt,
|
||||
в headless exit 2). Словарь имён один — `.env.example`; никакого нового формата.
|
||||
- **`--yes` — headless-consent:** явный флаг = заранее данное согласие на consent-вопросы
|
||||
(аналог семантики «выбор режима apply = согласие» bootstrap_bundle). non-TTY матрица:
|
||||
- `apply` без `--yes` → немедленный честный **exit 2** с перечнем того, что потребует
|
||||
интерактива, и подсказкой (`plan`/`verify` доступны; TTY или `--yes`+env-prefill) —
|
||||
зависание исключено (TC-12);
|
||||
- `apply --yes` → consent'ы подтверждены флагом; значения — только из env-prefill;
|
||||
отсутствует обязательное значение → exit 2 (не молчаливый дефолт); manual-step → exit 2
|
||||
(resume — повторный запуск);
|
||||
- `plan`/`verify` — работают в non-TTY полноценно (exit по результату).
|
||||
- **Answers-file отвергнут:** секреты в файле-вне-`.env` = новая поверхность утечки (NFR-3),
|
||||
новый формат = новый канон под сопровождение; env-prefill покрывает кейс без этих издержек.
|
||||
|
||||
### D11 — Onboarding: строго кирпич, plan→согласие→apply→verify, управляемый рестарт (OQ-6)
|
||||
|
||||
- **Драйв субпроцессом** (прецедент `step_onboard` bootstrap_bundle), НЕ «печать готовой
|
||||
команды»: печать разорвала бы «одну команду» на самом ценном шаге. Усиление против bundle —
|
||||
**сначала `plan`**: вывод плана кирпича показывается пользователю → согласие → `apply` →
|
||||
`verify` (FR-9/TC-22).
|
||||
- **Запуск кирпича — host-venv** (канон ONBOARDING; кирпичу нужны httpx/pydantic):
|
||||
идемпотентный ensure venv — паттерн `_ensure_venv` (probe импорта → `pip install -r
|
||||
requirements.txt`); сам setup_lite от venv не зависит (stdlib-only).
|
||||
- **Аргументы — детерминированная чистая функция** `build_onboard_args(answers, mode) -> list`
|
||||
от собранных ответов (`--name/--description/--repo/--gitea-owner/--prefix/--stack/--test-cmd/
|
||||
--prod-port/--staging-port/--webhook-url` + `--env-file <корневой .env>` + `--json`) —
|
||||
unit-тестируемо без сети (AC-10). Webhook-url строится от публичного URL оркестратора
|
||||
(`https://<orchestrator-public-host>/webhook/gitea`).
|
||||
- **Трансляция исходов:** exit 2 кирпича (остались ручные шаги) → **MANUAL** итогового отчёта
|
||||
(не успех); exit 1 → FAIL. `ORCH_PROJECTS_JSON` — из JSON-отчёта `apply` (строка
|
||||
`instructions` `ORCH_PROJECTS_JSON=…` — фактический контракт кирпича) → запись в `.env`
|
||||
с согласия.
|
||||
- **Управляемый рестарт — только собственного свежеподнятого контура** (§10): проверка тихого
|
||||
окна (`GET /queue` без running-job) → `docker compose up -d --force-recreate orchestrator` →
|
||||
пост-проверка `/health`+`/queue`. Чужие/боевые контейнеры не трогаются (NFR-7; рестартуется
|
||||
контур, поднятый шагом 8 этого же прогона).
|
||||
- Собственная реализация статусов/лейблов/репо/webhook в setup_lite **запрещена** (BR-7;
|
||||
22 статуса остаются за `plane_sync._PLANE_NAME_TO_KEY` через кирпич; структурный тест — D12).
|
||||
|
||||
### D12 — Документация и анти-дрейф: §1.1 в LITE_SETUP (пиннинг цел), новый тест-модуль (OQ-5, FR-11)
|
||||
|
||||
- **Размещение быстрого пути — подраздел `### 1.1. Быстрый путь: setup_lite.py` внутри
|
||||
`## 1. Рамка Lite`** (+ упоминание в шапке-цитате дока). Заголовки `## 1.`…`## 13.` не
|
||||
меняются → пиннинг `test_doc_exists_with_all_13_sections_in_order` и нумерация ссылок
|
||||
(§5.4, §13.7 и т.д. из других доков/скриптов) сохраняются **байт-в-байт**; ручной маршрут
|
||||
§2–§13 — канон и fallback для MANUAL-шагов. Fenced-блок §1.1 (команда запуска + режимы)
|
||||
обязан проходить существующие сканы дока: только плейсхолдеры, упоминаемые env-токены —
|
||||
только существующие в `.env.example` (`_ENV_TOKEN_RE`-ассерт). Альтернативы отвергнуты:
|
||||
`## 0`/14-й раздел/перенумерация — ломают пиннинг и внешние ссылки на §-номера без выгоды.
|
||||
- **Footer-норматив LITE_SETUP расширяется** (FR-11): «меняешь шаги тиража → обнови этот док
|
||||
**и `scripts/setup_lite.py`** в том же PR» (симметрично NFR-5 ORCH-102).
|
||||
- **`tests/test_setup_lite_script.py`** (имя финализировано; по образцу
|
||||
`test_bootstrap_script.py` + тест-план TC-01…TC-25): ast-скан stdlib-only и отсутствия
|
||||
`src.*`; **зеркала обоих needle-наборов** (`FORBIDDEN_DELETE_NEEDLES` — delete-операций нет;
|
||||
`FORBIDDEN_STATUS_NEEDLES` — собственного канона статусов нет); упоминание кирпичей
|
||||
`gen_secrets.py`/`onboard_project.py`/`LITE_SETUP.md`; unit чистых функций (вердикты
|
||||
предусловий, классификатор discovery, `port_overrides`, рендер env c маркером, builder
|
||||
аргументов onboard, step-движок/resume, exit-контракт, дефолт-режим `apply`, закрытость
|
||||
режимов); секрет-гигиена (инжектированный транскрипт без значений секретов); import без
|
||||
side effects.
|
||||
- **`tests/test_lite_setup_doc.py` — только аддитивно:** новый тест «LITE_SETUP.md упоминает
|
||||
`setup_lite.py` (быстрый путь) и сохраняет ручной маршрут» (TC-27); существующие ассерты,
|
||||
включая кортеж `SECTIONS`, **не правятся** (пиннинг структуры не меняется — D12 п.1).
|
||||
- **Синхронно в том же PR** (правило агентов №2 / NFR-8): `CHANGELOG.md` (`feat:`); витрина
|
||||
`docs/overview/README.md` (строка маршрута «Развернуть у себя» — упомянуть установочный
|
||||
скрипт; `business.md` — при необходимости); паспорт `CLAUDE.md` (секция ORCH-104);
|
||||
`docs/architecture/README.md` — обновлён на этой стадии (блок Type A дополнен инсталлером).
|
||||
|
||||
### Что НЕ меняется
|
||||
|
||||
`src/**`, корневой `docker-compose.yml`, `Dockerfile`, `.env.example`, `.env.watchdog.example`,
|
||||
`scripts/gen_secrets.py`, `scripts/onboard_project.py`, `scripts/bootstrap_bundle.py`,
|
||||
`deploy/**`, `onboarding/**`, промпты `.openclaw/agents/**`; `STAGE_TRANSITIONS`, состав
|
||||
`QG_CHECKS`, семантика `check_*`, machine-verdict ключи, схема БД — **байт-в-байт** (BR-8/AC-13).
|
||||
Kill-switch не вводится: активация — только явный запуск CLI человеком на целевом хосте
|
||||
(паттерн ORCH-009/102/103); в нашем контуре артефакт инертен. Прод-контейнер в рамках задачи
|
||||
не рестартуется (выкат — штатно: staging 8501 → `Confirm Deploy`).
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **`plan`-дефолт (строгое следование D5 ORCH-009/103)** — отвергнуто для этого инструмента:
|
||||
воспроизводит порог входа, который задача убирает (бизнес-запрос «одна команда» — BRD §1.2);
|
||||
безопасность дефолта достигается per-action consent + ранним guard + non-TTY exit 2 (D2), а
|
||||
не выбором режима. `plan` сохранён как полноценный строгий read-only режим.
|
||||
- **Отдельное имя режима `wizard`/`install`** — отвергнуто: новая лексика против выученной
|
||||
семейной (`plan`/`apply`/`verify`); дешевле объяснить «apply интерактивен», чем вводить слово.
|
||||
- **State-файл прогресса для resume** — отвергнуто: новый артефакт = поверхность
|
||||
дрейфа/устаревания; check→ensure по реальности уже даёт resume (паттерн bootstrap); коллизия
|
||||
с guard'ом `.env` решена маркером (D6) без состояния.
|
||||
- **Answers-file для headless** — отвергнуто: секреты на диске вне `.env` (NFR-3), новый формат
|
||||
под сопровождение; env-prefill с каноническими именами + `--yes` покрывает кейс (D10).
|
||||
- **Безусловная автоматизация SQL-вставки webhook (как в bundle)** — отвергнуто: в Lite БД
|
||||
Plane — чужая прод-инсталляция; без подтверждённого контейнера/согласия/пост-верификации
|
||||
это R-3; UI-путь — рекомендация, SQL — офер (D8).
|
||||
- **Только печать команд установки пакетов (никогда не исполнять)** — отвергнуто: ломает
|
||||
бизнес-запрос п.3 («предлагает установить»); per-package consent + re-check дают тот же
|
||||
уровень безопасности с лучшим UX (D4).
|
||||
- **Discovery по именам контейнеров/compose-проектов** — отвергнуто: ложноположительные
|
||||
срабатывания на чужих контейнерах (R-6); строго image-префиксы + выбор за пользователем (D5).
|
||||
- **Новый раздел LITE_SETUP (`## 0`/14-й/перенумерация)** — отвергнуто: ломает пиннинг
|
||||
«13 разделов в порядке» и внешние ссылки на §-номера; подраздел §1.1 даёт то же без издержек
|
||||
(D12).
|
||||
- **Опциональный файл-отчёт прогона** — отвергнуто (TRZ §9 оставлял на усмотрение): stdout +
|
||||
exit-код покрывают наблюдаемость; файл с транскриптом — лишняя поверхность для случайного
|
||||
попадания чувствительных значений.
|
||||
|
||||
## Последствия
|
||||
|
||||
- **+** Порог входа Lite падает с «прочитай 13 разделов и выполни ~30 команд» до «запусти один
|
||||
файл и отвечай на вопросы»; ошибки конфигурации ловятся в момент ввода (немедленная
|
||||
верификация), а не на smoke.
|
||||
- **+** Нулевой дрейф канонов: статусы/секреты/onboarding — только кирпичами; маршрут — только
|
||||
LITE_SETUP (ссылки из каждого шага); форма закреплена анти-дрейф тестами с зеркалами уже
|
||||
существующих needle-наборов.
|
||||
- **+** Рантайм байт-в-байт, наш прод недостижим по построению: ранний guard немаркированного
|
||||
`.env` (живой хост), no-delete, никаких операций с `main`, рестарт — только собственного
|
||||
свежеподнятого контура.
|
||||
- **−** Дефолт `apply` отличается от plan-default семейства — осознанная цена бизнес-цели;
|
||||
митигировано структурными гарантиями D2 (фаза 0 ≡ plan, consent, non-TTY exit 2) и явным
|
||||
тест-ассертом с комментарием-обоснованием.
|
||||
- **−** Двойной источник маршрута (док + скрипт) — поверхность дрейфа; митигировано нормативом
|
||||
«док + скрипт в одном PR» (footer LITE_SETUP, adr-0040) и анти-дрейф тестами (кирпичи,
|
||||
env-ключи ⊂ канона, упоминание скрипта в доке).
|
||||
- **−** Оферы установки пакетов вмешиваются в систему заказчика — митигировано per-package
|
||||
consent с печатью точной команды, закрытым набором менеджеров, re-check'ом и честным MANUAL.
|
||||
- **−** Path Б мутирует чужую Plane-БД — митигировано пятью предусловиями D8 (docker +
|
||||
подтверждённый контейнер + согласие с показом SQL + идемпотентный INSERT + пост-верификация)
|
||||
и приоритетом UI-пути.
|
||||
- **Откат:** удалить `scripts/setup_lite.py`, `tests/test_setup_lite_script.py`, вернуть §1.1 и
|
||||
footer LITE_SETUP.md, аддитивный тест в `test_lite_setup_doc.py`, записи
|
||||
CHANGELOG/CLAUDE.md/README/overview — состояние репо 1:1 (scripts+docs+tests, без миграций);
|
||||
поведение платформы не меняется в обе стороны.
|
||||
|
||||
## Ссылки
|
||||
|
||||
- BRD: `docs/work-items/ORCH-104/01-brd.md`
|
||||
- TRZ: `docs/work-items/ORCH-104/02-trz.md` (OQ-1…OQ-6 → D1…D12)
|
||||
- Acceptance: `docs/work-items/ORCH-104/03-acceptance-criteria.md`; тест-план: `04-test-plan.yaml`
|
||||
- Сквозной ADR: `docs/architecture/adr/adr-0040-lite-interactive-installer.md`
|
||||
- Риски: `docs/work-items/ORCH-104/10-tech-risks.md`; инфра: `07-infra-requirements.md`
|
||||
- Предшественники: adr-0037 (ORCH-102 — канон LITE_SETUP/13 разделов), adr-0038 (ORCH-103 —
|
||||
bootstrap-паттерн: режимы/step-движок/manual-checkpoint/no-delete), adr-0036 (ORCH-101 —
|
||||
«дефолт=боевое», gen_secrets, guard ORCH-058), adr-0035 (ORCH-009 — onboarding-CLI, D10
|
||||
branch protection)
|
||||
- Сверено по коду/репо: `scripts/bootstrap_bundle.py` (`manual_checkpoint(max_tries=3)`,
|
||||
`render_env`/`parse_env`, `_ensure_venv`, `_psql` stdin, `step_onboard` → `instructions`
|
||||
`ORCH_PROJECTS_JSON=`, `APPLY_STEPS == build_plan()`), `scripts/gen_secrets.py` (`--write`
|
||||
x-mode/exit 2/`--force`, token_hex(32)), `scripts/onboard_project.py` (CLI: `plan|apply|verify`,
|
||||
обязательные флаги, `--env-file`/`--json`), `tests/test_lite_setup_doc.py` (кортеж `SECTIONS`,
|
||||
`_ENV_TOKEN_RE`, FORBIDDEN-импорт, секрет-эвристика, «команды только в SECTIONS[1:]»),
|
||||
`tests/test_bootstrap_script.py` (`STDLIB_ALLOWED`, `FORBIDDEN_DELETE_NEEDLES`,
|
||||
`FORBIDDEN_STATUS_NEEDLES`), `docker-compose.yml` (`network_mode: host` ×3),
|
||||
`docs/deployment/LITE_SETUP.md` (§2–§13), `.env.example` (канон имён ключей)
|
||||
65
docs/work-items/ORCH-104/07-infra-requirements.md
Normal file
65
docs/work-items/ORCH-104/07-infra-requirements.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
work_item: ORCH-104
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-11
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 07 — Инфра-требования: ORCH-104 — Установочный скрипт Lite-тиража (интерактивный installer)
|
||||
|
||||
Work Item: **ORCH-104** · Repo: **orchestrator** · Стадия: architecture
|
||||
|
||||
> When-applicable: топология **нашего** прода не меняется — файл создан для
|
||||
> аудитопригодности с явными `N/A` и фиксацией предусловий **целевого хоста заказчика**
|
||||
> (на котором скрипт исполняется). Решения — `06-adr/ADR-001-setup-lite-interactive-installer.md`.
|
||||
|
||||
## I-1. Топология / окружения
|
||||
|
||||
- **Наш прод/staging (mva154):** `N/A` — задача scripts+docs+tests; контейнеры, порты, сеть,
|
||||
тома, compose-файлы не меняются. В нашем контуре скрипт инертен (активация — только явный
|
||||
запуск человеком; запуска на mva154 не предполагается вовсе).
|
||||
- **Целевой хост заказчика (где скрипт работает):** контур Lite без изменений — Linux x86_64,
|
||||
Docker Engine + Compose v2, git, python3, node (LITE_SETUP §2). Скрипт сам **сканирует** эти
|
||||
предусловия и предлагает доустановить недостающее (per-package consent, ADR D4); результат
|
||||
установки — ровно базовый Lite-контур: `orchestrator` + `orchestrator-watchdog`
|
||||
(`orchestrator-staging` НЕ поднимается — строго за `profiles: [staging]`).
|
||||
- **Discovery-предположение:** Plane/Gitea заказчика обнаруживаются только как
|
||||
docker-инсталляции локального хоста (image-префиксы `makeplane/*`, `gitea/gitea*`,
|
||||
`docker.gitea.com/gitea*` — ADR D5); native/k8s-инсталляции — честный ручной ввод URL
|
||||
(не FAIL). Сетевая достижимость Plane/Gitea с хоста оркестратора — предусловие заказчика
|
||||
(BRD §6).
|
||||
|
||||
## I-2. Переменные окружения / секреты
|
||||
|
||||
- **Новых ключей `Settings`/`.env.example`/`.env.watchdog.example` НЕТ** (TRZ §8) — каноны
|
||||
только читаются как шаблоны рендера.
|
||||
- Скрипт **пишет** целевые `.env`/`.env.watchdog` на хосте заказчика: от канонов-example,
|
||||
права `600`, первая строка — маркер managed-файла `# managed by scripts/setup_lite.py
|
||||
(ORCH-104)` (ADR D6: немаркированный существующий файл → отказ exit 2 без `--force`).
|
||||
- Секреты: webhook-секреты — только свежий выпуск кирпичом `gen_secrets.py` (субпроцесс);
|
||||
внешние токены (Plane/Gitea/Telegram ×2) — скрытый ввод оператора с немедленной верификацией;
|
||||
значения нигде не печатаются и не логируются (NFR-3). Боевые секреты исходного хоста не
|
||||
используются даже как подсказки-дефолты (stateless, LITE_SETUP §12).
|
||||
- Headless-prefill: переменные окружения процесса с каноническими именами ключей + явный
|
||||
`--yes` (ADR D10); answers-file не вводится.
|
||||
|
||||
## I-3. Деплой / рестарт
|
||||
|
||||
- **Рестарт нашего прод-контейнера НЕ требуется и не выполняется** (self-hosting инвариант):
|
||||
изменение — файлы `scripts/` + `tests/` + docs; выкат задачи — штатный конвейер
|
||||
(deploy-staging 8501 → `Confirm Deploy`).
|
||||
- На **хосте заказчика** скрипт выполняет `docker compose up -d --build` и управляемый
|
||||
`--force-recreate orchestrator` **только собственного свежеподнятого** контура, после
|
||||
проверки тихого окна `GET /queue` (ADR D11); чужие/уже бегущие контейнеры не трогаются без
|
||||
согласия (NFR-7); delete-операций нет вообще.
|
||||
|
||||
## I-4. CI/CD
|
||||
|
||||
- `.gitea/workflows/` — **без изменений**. Новый структурный/unit тест-модуль
|
||||
`tests/test_setup_lite_script.py` + аддитивный тест в `tests/test_lite_setup_doc.py`
|
||||
попадают в существующий прогон `pytest tests/ -q` (детерминированы: без TTY/сети/docker —
|
||||
инжектируемый I/O, моки, tmp_path; NFR-5).
|
||||
- Инфра-предусловий прод-образа нет (скрипт не входит в рантайм-контейнер как зависимость;
|
||||
pip-зависимости не добавляются — stdlib-only).
|
||||
47
docs/work-items/ORCH-104/10-tech-risks.md
Normal file
47
docs/work-items/ORCH-104/10-tech-risks.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
work_item: ORCH-104
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-11
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 10 — Технические риски: ORCH-104 — Установочный скрипт Lite-тиража (интерактивный installer)
|
||||
|
||||
Work Item: **ORCH-104** · Repo: **orchestrator** · Стадия: architecture
|
||||
|
||||
> Информационный (гейтом не парсится). Риски реализации и эксплуатации интерактивного
|
||||
> installer'а; решения — `06-adr/ADR-001-setup-lite-interactive-installer.md` (D1…D12).
|
||||
> Развивает реестр BRD §8 (R-1…R-6).
|
||||
|
||||
## Реестр рисков
|
||||
|
||||
| ID | Риск | Вер. | Влия. | Митигейшн |
|
||||
|----|------|------|-------|-----------|
|
||||
| TR-1 | **Случайный запуск на хосте с живым продом** (R-1): дефолт-режим — `apply`-wizard, не `plan` | Низ. | Выс. | Структурные гарантии D2: фаза 0 `apply` ≡ `plan` (read-only), ранний guard НЕмаркированного `.env` → exit 2 ДО первого вопроса (D6), per-action consent с дефолт-ответом «нет», non-TTY → exit 2 до мутаций (D10); `--force` требует явного набора + согласия; на нашем mva154 запуск не предполагается вовсе |
|
||||
| TR-2 | **Оферы установки пакетов** (R-2) — вмешательство в систему заказчика; нативные репо могут дать не то (compose v1, старый docker) | Сред. | Сред. | Per-package consent с печатью точной команды ДО исполнения; закрытый набор менеджеров apt-get/dnf/yum/zypper; **re-check фактом после установки** (в т.ч. `docker compose version` → v2), не сошлось → MANUAL с официальной инструкцией; нет менеджера/sudo/отказ → честный MANUAL (D4) |
|
||||
| TR-3 | **SQL-вставка webhook в чужую Plane-БД** (R-3): инвазивная мутация прод-инсталляции заказчика; пользовательский ввод в SQL | Сред. | Выс. | Path A (UI) — дефолт-рекомендация; Path Б — только при 5 предусловиях D8 (docker-Plane + подтверждённый пользователем postgres-контейнер + согласие с показом SQL + идемпотентный INSERT + обязательная пост-верификация SELECT); только INSERT/SELECT (no UPDATE/DELETE); секрет/пароль — stdin/env, не argv; валидация slug `^[a-z0-9-]+$` (анти-инъекция); любой сбой → fail-safe в Path A |
|
||||
| TR-4 | **Дрейф скрипта от канона LITE_SETUP** (R-4): два источника маршрута расходятся | Сред. | Сред. | Канон-знания не дублируются (кирпичи субпроцессами, статусы — за `plane_sync` через onboarding, smoke — ссылкой на §11); footer-норматив «док + скрипт в одном PR» (FR-11/adr-0040); анти-дрейф: упоминание скрипта в доке, env-ключи ⊂ `.env.example`, зеркала needle-наборов (D12) |
|
||||
| TR-5 | **Зависание/недетерминизм в non-TTY** (R-5): CI/обвязка заказчика виснет на ожидании ввода | Низ. | Сред. | `isatty`-гейт: `apply` без `--yes` в non-TTY → немедленный exit 2 с подсказкой; headless — только env-prefill + явный `--yes`; `plan`/`verify` — полноценные non-TTY режимы; весь интерактив за инжектируемым I/O (детерминированные тесты TC-12) (D10) |
|
||||
| TR-6 | **Ложноположительный discovery** (R-6): чужой контейнер опознан как Plane/Gitea; неверный URL уезжает в конфиг | Низ. | Сред. | Опознание строго по image-префиксам (не по именам); выбор всегда за пользователем + пункт «ввести вручную» всегда; токен-верификация выбранного URL обязательна (FR-4) — неверный кандидат не пройдёт `collect` (D5/D9) |
|
||||
| TR-7 | **Утечка секретов** через транскрипт wizard'а / отчёт / файлы | Низ. | Выс. | Скрытый ввод (getpass-класс); значения не печатаются и не логируются (только имена ключей); итоговая таблица без значений; live-файлы `600`; файл-отчёт прогона сознательно НЕ вводится; анти-дрейф: тест транскрипта (TC-11) + секрет-эвристика на fenced-блоках дока (D3/D6/D10) |
|
||||
| TR-8 | **Коллизия resume ⟷ guard существующего `.env`**: после manual-step повторный запуск отбивается собственным артефактом ЛИБО guard ослабляется и перетирает чужой конфиг | Сред. | Выс. | Маркер managed-файла (первая строка): без маркера → отказ exit 2 (чужой конфиг), с маркером → resume-ensure без перетирания значений (D6); семантика покрыта unit-тестами (TC-03/TC-14) |
|
||||
| TR-9 | **Разнообразие хостов**: экзотический дистрибутив/пакетный менеджер/нестандартный docker — скан даёт неверный вердикт | Сред. | Низ. | Вердикты — чистые функции от фактов (тестируемы на фикстурах); неопределимое → честный MANUAL с generic-командами и ссылкой на § LITE_SETUP (никогда не падение); не-Linux/не-x86_64 → WARN «вне контура» (рамка Lite не расширяется) (D4) |
|
||||
| TR-10 | **Дрейф интерфейсов кирпичей** (`gen_secrets.py`/`onboard_project.py`): смена флагов/формата отчёта молча ломает installer | Низ. | Сред. | Контракты кирпичей закреплены их собственными тестами (`test_secrets_gen`, onboarding-тесты); builder аргументов — чистая функция под unit-тестом (ломается громко в CI); exit-коды кирпичей транслируются честно (2 → MANUAL, 1 → FAIL) (D11); норматив same-PR симметричен |
|
||||
|
||||
## Сводный вывод
|
||||
|
||||
Доминирующий класс — **эксплуатационные риски целевого хоста заказчика** (TR-2/TR-3/TR-9):
|
||||
наш прод они не затрагивают и гасятся per-action consent'ом, контрактом manual-checkpoint,
|
||||
честными MANUAL-деградациями и обязательной верификацией каждого ввода. Специфика ORCH-104
|
||||
против предшественников — два новых внимательных места: **дефолт-режим `apply`** (TR-1; закрыт
|
||||
структурными гарантиями D2, а не дисциплиной) и **маркер managed-файла** (TR-8; единственный
|
||||
механизм, примиряющий идемпотентный resume с защитой чужого `.env`).
|
||||
|
||||
Рисков для прод-конвейера self-hosting **нет по построению** (BR-8: рантайм байт-в-байт,
|
||||
kill-switch не нужен — активация только явным запуском оператора; полный регресс и все
|
||||
существующие анти-дрейф тесты остаются зелёными, пиннинг «13 разделов» LITE_SETUP не меняется).
|
||||
Эскалация `arch:major-change` не требуется: новых стадий/компонентов рантайма/смены БД нет —
|
||||
задача целиком в слое дистрибуции (паттерн ORCH-101/102/103). Возврат в анализ не требуется:
|
||||
ТЗ выполнимо без нарушения принципов архитектуры.
|
||||
146
docs/work-items/ORCH-104/12-review.md
Normal file
146
docs/work-items/ORCH-104/12-review.md
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
verdict: REQUEST_CHANGES
|
||||
work_item: ORCH-104
|
||||
stage: review
|
||||
author_agent: reviewer
|
||||
status: changes-requested
|
||||
created_at: 2026-06-12
|
||||
model_used: claude-opus-4-8
|
||||
type: review
|
||||
work_item_id: ORCH-104
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-104 — Установочный скрипт Lite-тиража (`scripts/setup_lite.py`)
|
||||
|
||||
## Summary
|
||||
|
||||
Задача — **scripts + docs + tests**: исполняемый интерактивный installer Lite-тиража поверх
|
||||
канона `LITE_SETUP.md`. Качество **чистых функций** и **документации** — высокое: рантайм
|
||||
`src/**`/`docker-compose.yml`/`Dockerfile`/`.env*.example` байт-в-байт не тронуты (AC-13 ✓),
|
||||
оба тест-файла зелёные (74 passed), `LITE_SETUP.md` §1.1 + footer-норматив + CHANGELOG +
|
||||
витрина `docs/overview/` + `docs/architecture/README.md` + ADR (локальный и сквозной
|
||||
`adr-0040`) обновлены в том же PR (AC-14 ✓, документация — golden source соблюдён).
|
||||
|
||||
**Однако** обнаружен блокер: **решающая логика реализована как чистые функции, но НЕ
|
||||
подключена к исполняемому пути `apply`** — `step_collect` пустой, `io.ask(...)` не вызывается
|
||||
ни разу, результаты discovery и флаги `--project-*` не попадают в `answers`, машинные guard-ы
|
||||
(C-1 Telegram, §6.4 branch protection, когерентность портов, Path Б webhook) не вызываются из
|
||||
шагов. Реальный прогон `apply` НЕ собирает ключи и НЕ выполняет установку, ради которой задача
|
||||
заведена. Это «не реализовано требование ТЗ» (FR-1/FR-3/FR-4/FR-5/FR-7/FR-9) и нарушение
|
||||
собственного ADR D3/D5/D9 → `REQUEST_CHANGES`.
|
||||
|
||||
Анти-доказательство «доки точны»: §1.1 LITE_SETUP и CHANGELOG утверждают, что скрипт
|
||||
«запрашивает обязательные ключи в момент установки с немедленной верификацией» — исполняемый
|
||||
код этого не делает (документация описывает спецификацию, а не текущую реализацию).
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
|
||||
- [ ] **Интерактивный сбор ключей не подключён — `apply` не функционален end-to-end.**
|
||||
`step_collect` (`scripts/setup_lite.py:1061-1066`) — пустая заглушка: тело делает только
|
||||
`ctx.setdefault("answers", {})` и `return "ok"`, хотя docstring обещает «значения собираются
|
||||
в ctx['answers']». Метод `IO.ask` (`:181`) с верификацией/env-prefill/лимитом попыток
|
||||
реализован и протестирован (TC-10…12), но в самом скрипте `io.ask(...)` **не вызывается
|
||||
нигде** (подтверждено grep). Следствие: при реальном `apply` ни один из
|
||||
`MANDATORY_NEW_HOST_KEYS` (Plane URL/токен, Gitea, Telegram, хост-параметры, порты) не
|
||||
собирается → `step_render_env` рендерит `.env`/`.env.watchdog` только со свежими
|
||||
webhook-секретами поверх **плейсхолдеров** `.env.example`, а `build_onboard_args` получает
|
||||
пустые `--name/--repo/--prefix`. Нарушены **FR-4** (`02-trz.md` §3, «немедленная верификация
|
||||
каждого введённого значения») и **ADR D3** (шаг 4 `collect` = «интерактивный сбор ключей с
|
||||
немедленной верификацией»). Headless-путь тоже мёртв: env-prefill живёт внутри `ask`, который
|
||||
не зовётся.
|
||||
|
||||
- [ ] **Результаты discovery и `--project-*` не доходят до `answers` (FR-3/FR-9 не
|
||||
реализованы).** `step_discovery` пишет `ctx["chosen_plane"]/["chosen_gitea"]`
|
||||
(`:1056-1057`), но эти значения **нигде не читаются** — кандидаты URL вычисляются и
|
||||
отбрасываются (FR-3 «префилл кандидатов URL» не реализован). Флаги `--project-name/-repo/
|
||||
-prefix/...` (`:1264-1271`) парсятся, но **не копируются** в `ctx["answers"]`;
|
||||
`build_onboard_args` (`:722-742`) читает `answers.get("project_name")` и т.д., которые
|
||||
всегда пусты → `onboard_project.py` вызывается с пустыми параметрами проекта (FR-9 / AC-10
|
||||
в исполняемом пути не выполняется).
|
||||
|
||||
- [ ] **Машинные guard-ы ТЗ не вызываются из шагов (FR-5/FR-7 не реализованы end-to-end).**
|
||||
Подтверждено grep: `telegram_c1_verdict` (C-1 ORCH-100), `port_overrides` (когерентная
|
||||
тройка), `staging_port_ok` (staging≠prod fail-closed), `next_free_port` (busy-check
|
||||
альтернатива) — **не вызываются** нигде в скрипте. `step_gitea_guards` (`:1103-1115`) читает
|
||||
`ctx.get("gitea_bp_status")`/`ctx.get("gitea_branch_protections")`, которые **никогда не
|
||||
устанавливаются** → §6.4 branch-protection guard всегда возвращает `"ok"` (никогда не
|
||||
срабатывает). `step_plane_webhook` (`:1091-1100`) уходит в Path Б только при `ctx.get("psql")`
|
||||
и `answers.get("plane_db_container")` — оба **никогда не устанавливаются** → Path Б
|
||||
физически недостижим. Нарушены **FR-5** (`02-trz.md` §3), **FR-7** (C-1, §6.4) и **ADR D9**:
|
||||
guard-ы заявлены «машинными», но в рантайме мертвы.
|
||||
|
||||
### P1 — Must fix
|
||||
|
||||
- [ ] **Контракт идемпотентности/resume (check→ensure) не реализован на уровне движка.** Все
|
||||
`APPLY_STEPS` используют `_always_run` (`:998-999`, реестр `:1214-1225`), который всегда
|
||||
возвращает `False` → ни один продакшн-шаг никогда не даёт `"skip"`. Заявленный **AC-1**
|
||||
(«на уже выполненном хосте шаги дают каскад skip; повторный запуск не перевыполняет
|
||||
мутации») и **ADR D3** («каждый шаг сперва проверяет „уже сделано?“ … при PASS пропускается»)
|
||||
не выполнены: повторный `apply` заново гоняет `gen_secrets`, `docker compose up -d --build`
|
||||
и `onboard_project.py plan/apply/verify`. Сам `run_steps` skip поддерживает (TC-02/03 на
|
||||
фейковом check), но реальные шаги его не используют.
|
||||
|
||||
- [ ] **Нет интеграционного теста `run_apply` — зелёный suite маскирует P0.**
|
||||
`tests/test_setup_lite_script.py` тщательно покрывает чистые функции **в изоляции** (вердикты,
|
||||
discovery, `port_overrides`, `build_onboard_args`, exit-контракт), но **ни один тест не
|
||||
проверяет, что `run_apply`/`step_collect` действительно собирают ключи и протягивают их в
|
||||
`answers`/render/onboard**. Именно поэтому полностью оторванная проводка (P0) проходит при
|
||||
74 passed. Ось «тесты содержательные»: нужен сценарный тест шага collect → render → onboard
|
||||
на инжектируемом I/O (инфраструктура `IO` для этого уже есть, D10).
|
||||
|
||||
### P2 — Should fix
|
||||
|
||||
- [ ] **Порт health-чека захардкожен `DEFAULT_PROD_PORT` (8500), игнорирует override
|
||||
прод-порта.** `step_up` (`:1134` `port = DEFAULT_PROD_PORT`) и `run_verify` (`:1337`) бьют
|
||||
в 8500 независимо от выбранного прод-порта (FR-5). После починки сбора порта это даст ложный
|
||||
FAIL health на не-дефолтном порте; брать порт из собранного `ORCH_DEPLOY_PROD_TARGET_PORT`.
|
||||
|
||||
- [ ] **`url` подставляется в SQL без валидации/экранирования (расхождение с ADR D8).**
|
||||
`build_webhook_insert_sql` (`:607-619`) и `count_sql` (`:630-631`) интерполируют
|
||||
`answers["orchestrator_public_url"]` в текст SQL напрямую; ADR D8 заявляет «slug и прочие
|
||||
подстановки валидируются … анти-SQL-инъекция на пользовательском вводе». Импакт низкий (своя
|
||||
БД заказчика, путь Path Б сейчас недостижим), но при включении Path Б — валидировать `url`
|
||||
(как `valid_slug`) или передавать psql-переменной.
|
||||
|
||||
- [ ] **`run_verify` вызывает `stateless_verdict(queue)` без `own_prefixes` (`:1347`)** → после
|
||||
онбординга проекта заказчика его собственные задачи в `/queue` будут помечены «чужими» и
|
||||
`verify` даст ложный FAIL. Протянуть префикс проекта из конфигурации/реестра.
|
||||
|
||||
## Документация
|
||||
|
||||
**Статус: обновлена в том же PR (ось документации — PASS).**
|
||||
- `docs/deployment/LITE_SETUP.md` — добавлен `### 1.1. Быстрый путь: setup_lite.py` (пиннинг
|
||||
«13 разделов в порядке» цел, `test_lite_setup_doc.py` зелёный) + footer-норматив сопровождения
|
||||
расширен «обнови док **и** `scripts/setup_lite.py` в том же PR» (FR-11/D12 ✓).
|
||||
- `CHANGELOG.md` — запись `feat:` (ORCH-104) есть.
|
||||
- Витрина системы `docs/overview/README.md` (маршрут «Развернуть у себя») и
|
||||
`docs/architecture/README.md` (блок Type A) дополнены — правило агентов №2 / ORCH-011 ✓.
|
||||
- Паспорт `CLAUDE.md` — секция ORCH-104 добавлена; ADR `06-adr/ADR-001-…` + сквозной
|
||||
`adr-0040-lite-interactive-installer.md` присутствуют.
|
||||
|
||||
> Замечание (учесть при доработке, НЕ блокер само по себе): после починки P0 проверить, что
|
||||
> §1.1 / CHANGELOG не «опережают» реализацию (сейчас формулировка «запрашивает обязательные
|
||||
> ключи … с немедленной верификацией» соответствует ТЗ, но не текущему коду). Доводка кода до
|
||||
> спецификации это снимает.
|
||||
|
||||
## Что проверено и в порядке
|
||||
|
||||
- **AC-13 (рантайм байт-в-байт):** `git diff origin/main...HEAD` по `src/**`,
|
||||
`docker-compose.yml`, `Dockerfile`, `.env.example`, `.env.watchdog.example` — пусто. ✓
|
||||
- **Гигиена скрипта (AC-12):** stdlib-only, без `src.*`, no-delete, секреты через `getpass` и
|
||||
не печатаются, кирпичи `gen_secrets.py`/`onboard_project.py` — субпроцессами; структурные
|
||||
тесты TC-24/25 зелёные. ✓ (замечание: гигиена соблюдена, но функциональная проводка — нет).
|
||||
- **Тесты:** `pytest tests/test_setup_lite_script.py tests/test_lite_setup_doc.py -q` →
|
||||
74 passed. ✓ (зелёные, но без интеграционного покрытия — см. P1).
|
||||
|
||||
## Вывод
|
||||
|
||||
Чистые функции, документация и инварианты рантайма — на эталонном уровне. Но исполняемый
|
||||
`apply` не делает того, ради чего задача заведена: интерактивный сбор/верификация ключей,
|
||||
префилл из discovery, протяжка параметров проекта и срабатывание машинных guard-ов не
|
||||
подключены к step-движку. Это P0 → **REQUEST_CHANGES**. После подключения чистых функций к
|
||||
`step_collect`/`step_discovery`/`step_*-guards` (и добавления интеграционного теста `run_apply`)
|
||||
артефакт будет соответствовать ТЗ и ADR.
|
||||
1379
scripts/setup_lite.py
Normal file
1379
scripts/setup_lite.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -426,3 +426,45 @@ def test_replication_boundaries_reference_lite_setup():
|
||||
|
||||
def test_changelog_has_orch_102_entry():
|
||||
assert "ORCH-102" in CHANGELOG.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-27 (ORCH-104, FR-11 / D12): LITE_SETUP.md вводит установочный скрипт как
|
||||
# рекомендованный быстрый путь и сохраняет ручной маршрут (пиннинг «13 разделов»
|
||||
# в порядке держит test_doc_exists_with_all_13_sections_in_order — не трогается).
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_doc_introduces_setup_lite_fast_path():
|
||||
text = _doc_text()
|
||||
assert "setup_lite.py" in text, (
|
||||
"LITE_SETUP.md не вводит установочный скрипт setup_lite.py (ORCH-104 FR-11)"
|
||||
)
|
||||
# быстрый путь — подраздел §1.1 ВНУТРИ §1 (нумерация ## 1.…## 13. не меняется)
|
||||
body1 = _section_bodies()["## 1. Рамка Lite"]
|
||||
assert "1.1" in body1 and "setup_lite.py" in body1, (
|
||||
"быстрый путь обязан быть подразделом §1.1 внутри §1 (D12)"
|
||||
)
|
||||
# ручной маршрут сохранён как канон/fallback — упомянут явно
|
||||
assert "fallback" in text.lower() or "ручной маршрут" in text, (
|
||||
"ручной маршрут §2–§13 обязан остаться каноном/fallback (FR-11)"
|
||||
)
|
||||
# норматив сопровождения расширен на скрипт (D12)
|
||||
assert "scripts/setup_lite.py" in text
|
||||
|
||||
|
||||
def test_setup_lite_fast_path_block_is_clean():
|
||||
"""§1.1 fenced-блок проходит те же сканы, что весь док: без боевых литералов,
|
||||
без секретоподобных значений, без неизвестных env-токенов."""
|
||||
body1 = _section_bodies()["## 1. Рамка Lite"]
|
||||
blocks = _fenced_blocks(body1)
|
||||
assert blocks, "§1.1 обязан нести fenced-блок с командой запуска (D12)"
|
||||
for i, block in enumerate(blocks):
|
||||
for literal in FORBIDDEN:
|
||||
assert literal not in block, f"§1.1 блок #{i}: боевой литерал {literal!r}"
|
||||
for rx in (_SECRET_HEX_RE, _SECRET_ALNUM_RE):
|
||||
assert rx.search(block) is None, f"§1.1 блок #{i}: секретоподобное значение"
|
||||
# упомянутые в §1.1 env-токены (если есть) — только из канона .env.example
|
||||
canon = _env_keys(ENV_EXAMPLE)
|
||||
mentioned = set(_ENV_TOKEN_RE.findall(body1))
|
||||
assert not (mentioned - canon), (
|
||||
f"§1.1 упоминает env-токены вне .env.example: {sorted(mentioned - canon)}"
|
||||
)
|
||||
|
||||
988
tests/test_setup_lite_script.py
Normal file
988
tests/test_setup_lite_script.py
Normal file
@@ -0,0 +1,988 @@
|
||||
"""ORCH-104 (TC-01…TC-25, AC-1…AC-12): структурные и unit-проверки
|
||||
`scripts/setup_lite.py` — интерактивного installer'а Lite-тиража.
|
||||
|
||||
По образцу `tests/test_bootstrap_script.py` (ORCH-103) + ADR-001 ORCH-104 D12:
|
||||
вся решающая логика скрипта — чистые функции (вердикты предусловий,
|
||||
классификатор discovery, когерентность портов, рендер env с маркером
|
||||
managed-файла, builder аргументов onboarding, step-движок), тестируемые без
|
||||
TTY/сети/docker; интерактив — через инжектируемый I/O (`IO(input_fn=…,
|
||||
getpass_fn=…, say_fn=…, is_tty=…, env=…)`); файловые сценарии — на tmp_path;
|
||||
структурная гигиена — ast/эвристики по файлу скрипта (stdlib-only, зеркала
|
||||
delete/status-needle-наборов, кирпичи gen_secrets.py / onboard_project.py).
|
||||
|
||||
Детерминировано: без сети/docker/LLM (единственный субпроцесс — stdlib-кирпич
|
||||
gen_secrets.py в TC-13); модуль импортируется по файлу, import не имеет
|
||||
side effects.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import importlib.util
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Один источник истины запрещённых боевых литералов (TC-15, ORCH-101 AC-7).
|
||||
from tests.test_no_host_hardcodes import FORBIDDEN
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = REPO_ROOT / "scripts/setup_lite.py"
|
||||
|
||||
# Зеркало FORBIDDEN_DELETE_NEEDLES bootstrap'а (ORCH-103 D9) + API/git-удаления
|
||||
# (AC-12): delete-операций в installer'е нет ВООБЩЕ — лечение всегда инструкцией.
|
||||
FORBIDDEN_DELETE_NEEDLES = (
|
||||
"volume rm",
|
||||
"rm -rf",
|
||||
"down -v",
|
||||
"compose down",
|
||||
"rmtree",
|
||||
"os.remove",
|
||||
".unlink",
|
||||
"push --delete",
|
||||
'method="DELETE"',
|
||||
"method='DELETE'",
|
||||
)
|
||||
|
||||
# Зеркало FORBIDDEN_STATUS_NEEDLES: собственный канон Plane-статусов в скрипте
|
||||
# запрещён (статусы — только кирпич onboard_project.py / plane_sync, BR-7/D11).
|
||||
FORBIDDEN_STATUS_NEEDLES = (
|
||||
"Backlog",
|
||||
"To Analyse",
|
||||
"Confirm Deploy",
|
||||
"Code-Review",
|
||||
"Awaiting Deploy",
|
||||
"Monitoring after Deploy",
|
||||
)
|
||||
|
||||
# stdlib-allowlist top-level импортов (D1: python stdlib-only).
|
||||
STDLIB_ALLOWED = {
|
||||
"argparse", "dataclasses", "getpass", "json", "os", "pathlib", "re",
|
||||
"secrets", "shutil", "socket", "subprocess", "sys", "tempfile", "time",
|
||||
"urllib", "uuid",
|
||||
}
|
||||
|
||||
|
||||
def _source() -> str:
|
||||
assert SCRIPT.is_file(), "scripts/setup_lite.py отсутствует (AC-1/FR-1)"
|
||||
return SCRIPT.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _load_module():
|
||||
spec = importlib.util.spec_from_file_location("setup_lite", SCRIPT)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
def _io(mod, answers=(), secrets_=(), say=None, is_tty=True, env=None, yes=False):
|
||||
"""Инжектируемый I/O со скриптованными ответами (D10/NFR-5)."""
|
||||
answers_it = iter(answers)
|
||||
secrets_it = iter(secrets_)
|
||||
return mod.IO(
|
||||
input_fn=lambda prompt="": next(answers_it),
|
||||
getpass_fn=lambda prompt="": next(secrets_it),
|
||||
say_fn=(say.append if say is not None else (lambda s: None)),
|
||||
is_tty=is_tty,
|
||||
env=({} if env is None else env),
|
||||
yes=yes,
|
||||
)
|
||||
|
||||
|
||||
def _full_facts() -> dict:
|
||||
"""Фикстура «всё установлено» (перечень FR-2; без боевых литералов)."""
|
||||
return {
|
||||
"uname": "Linux x86_64",
|
||||
"docker": True,
|
||||
"compose_v2": True,
|
||||
"git": True,
|
||||
"python3": True,
|
||||
"node": True,
|
||||
"node_bin": "/usr/bin/node",
|
||||
"claude_code_dir": "/usr/lib/node_modules/@anthropic-ai/claude-code",
|
||||
"claude_creds_readable": True,
|
||||
"docker_gid": "984",
|
||||
"uid": 1001,
|
||||
"gid": 1001,
|
||||
"home": "/home/operator",
|
||||
"repos_dir": "/home/operator/repos",
|
||||
"repos_dir_owner_ok": True,
|
||||
"ssh_dir": "/home/operator/.orchestrator-ssh",
|
||||
"ssh_keys": True,
|
||||
"busy_ports": [],
|
||||
"pkg_manager": "apt-get",
|
||||
"repo_root": "/home/operator/orchestrator",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-01 / AC-1: CLI — режимы закрыты, дефолт apply (ADR-001 D2), флаги.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc01_modes_closed_and_apply_is_default():
|
||||
mod = _load_module()
|
||||
parser = mod.build_arg_parser()
|
||||
# ОСОЗНАННО зеркально к test_plan_is_default_mode bootstrap'а: у setup_lite
|
||||
# дефолт — apply-wizard (бизнес-цель «одна команда», ADR-001 D2); безопасность
|
||||
# дефолта — структурно (фаза 0 ≡ plan, ранний guard .env, per-action consent,
|
||||
# non-TTY без --yes → exit 2), а не выбором режима.
|
||||
assert parser.parse_args([]).mode == "apply"
|
||||
assert parser.parse_args(["plan"]).mode == "plan" # строгий read-only
|
||||
assert parser.parse_args(["verify"]).mode == "verify" # read-only пост-проверка
|
||||
with pytest.raises(SystemExit):
|
||||
parser.parse_args(["wizard"]) # набор режимов закрыт (D2)
|
||||
args = parser.parse_args([])
|
||||
assert args.force is False and args.yes is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-02 / AC-1: step-движок check→ensure — skip без вызова ensure.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc02_engine_skips_done_steps_without_ensure():
|
||||
mod = _load_module()
|
||||
ran: list = []
|
||||
steps = (
|
||||
("one", lambda ctx: False, lambda ctx: (ran.append("one"), "ok")[1]),
|
||||
("two", lambda ctx: False, lambda ctx: (ran.append("two"), "ok")[1]),
|
||||
)
|
||||
ctx = {"results": {}}
|
||||
mod.run_steps(steps, ctx)
|
||||
assert ran == ["one", "two"]
|
||||
assert ctx["results"] == {"one": "ok", "two": "ok"}
|
||||
|
||||
# повторный прогон по фикстуре «всё выполнено» — каскад skip, ни одной мутации
|
||||
ran.clear()
|
||||
done_steps = tuple((n, (lambda ctx: True), e) for n, _, e in steps)
|
||||
ctx2 = {"results": {}}
|
||||
mod.run_steps(done_steps, ctx2)
|
||||
assert ran == []
|
||||
assert set(ctx2["results"].values()) == {"skip"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-03 / AC-1, AC-11: resume — manual-step останавливает (exit 2), повторный
|
||||
# запуск продолжает с первого незавершённого шага.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc03_resume_continues_from_first_unfinished_step():
|
||||
mod = _load_module()
|
||||
ran: list = []
|
||||
done = {"a": False, "b": False}
|
||||
|
||||
def ensure_a(ctx):
|
||||
ran.append("a")
|
||||
return "ok"
|
||||
|
||||
def ensure_b(ctx):
|
||||
if not done["b"]:
|
||||
raise mod.ManualStop("b: выполните ручной шаг")
|
||||
ran.append("b")
|
||||
return "ok"
|
||||
|
||||
steps = (
|
||||
("a", lambda ctx: done["a"], ensure_a),
|
||||
("b", lambda ctx: done["b"], ensure_b),
|
||||
("c", lambda ctx: False, lambda ctx: (ran.append("c"), "ok")[1]),
|
||||
)
|
||||
with pytest.raises(mod.ManualStop):
|
||||
mod.run_steps(steps, {"results": {}})
|
||||
assert ran == ["a"] and "c" not in ran # остановились на manual-step
|
||||
|
||||
# «resume» = повторный запуск: выполненное скипается, продолжаем с первого
|
||||
# незавершённого (b теперь сделан руками → skip, c выполняется)
|
||||
done["a"] = done["b"] = True
|
||||
ran.clear()
|
||||
ctx = {"results": {}}
|
||||
mod.run_steps(steps, ctx)
|
||||
assert ran == ["c"]
|
||||
assert ctx["results"]["a"] == "skip" and ctx["results"]["b"] == "skip"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-04 / AC-2: вердикты предусловий — полный набор фактов → все OK.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc04_full_facts_give_all_ok_and_no_blockers():
|
||||
mod = _load_module()
|
||||
verdicts = mod.prereq_verdicts(_full_facts())
|
||||
assert verdicts, "prereq_verdicts вернул пустой перечень (FR-2)"
|
||||
assert all(v == "OK" for _, v, _ in verdicts), verdicts
|
||||
assert mod.has_blockers(verdicts) is False
|
||||
# ни один пункт перечня FR-2 не пропускается молча
|
||||
items = {item for item, _, _ in verdicts}
|
||||
for required in ("os", "docker", "compose", "git", "python3", "node",
|
||||
"claude-code", "claude-auth", "docker-group", "repos-dir",
|
||||
"ssh", "ports"):
|
||||
assert required in items, f"пункт {required!r} перечня FR-2 не покрыт"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-05 / AC-2: MISSING с конкретной командой; отказ от согласия → MANUAL,
|
||||
# команда напечатана, мутация НЕ выполнена.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc05_missing_docker_offer_declined_is_manual_without_mutation():
|
||||
mod = _load_module()
|
||||
facts = _full_facts()
|
||||
facts.update(docker=False, compose_v2=False)
|
||||
verdicts = dict((i, (v, d)) for i, v, d in mod.prereq_verdicts(facts))
|
||||
assert verdicts["docker"][0] == "MISSING"
|
||||
assert mod.has_blockers(mod.prereq_verdicts(facts)) is True
|
||||
|
||||
command = mod.install_command("apt-get", "docker")
|
||||
assert command and "apt-get" in command # конкретная команда под менеджер
|
||||
|
||||
transcript: list = []
|
||||
executed: list = []
|
||||
io = _io(mod, answers=["n"], say=transcript) # инжектированный отказ
|
||||
status = mod.offer_install("docker", command, io,
|
||||
runner=lambda cmd: executed.append(cmd))
|
||||
assert status == "manual"
|
||||
assert executed == [], "мутация выполнена несмотря на отказ (AC-2)"
|
||||
blob = "\n".join(transcript)
|
||||
assert command in blob, "точная команда не напечатана ДО запроса согласия"
|
||||
|
||||
|
||||
def test_tc05b_offer_accepted_runs_and_rechecks():
|
||||
mod = _load_module()
|
||||
executed: list = []
|
||||
|
||||
class _Proc:
|
||||
returncode = 0
|
||||
|
||||
io = _io(mod, answers=["y"])
|
||||
status = mod.offer_install(
|
||||
"git", "sudo apt-get install -y git", io,
|
||||
runner=lambda cmd: (executed.append(cmd), _Proc())[1],
|
||||
recheck=lambda: True,
|
||||
)
|
||||
assert status == "ok" and executed == ["sudo apt-get install -y git"]
|
||||
# re-check фактом не сошёлся → честный MANUAL (не ложный OK)
|
||||
io2 = _io(mod, answers=["y"])
|
||||
status2 = mod.offer_install(
|
||||
"git", "sudo apt-get install -y git", io2,
|
||||
runner=lambda cmd: _Proc(), recheck=lambda: False,
|
||||
)
|
||||
assert status2 == "manual"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-06 / AC-2: неопределимый пакетный менеджер → MANUAL со ссылкой на канон;
|
||||
# uname вне контура → WARN, не падение.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc06_unknown_pkg_manager_and_foreign_os():
|
||||
mod = _load_module()
|
||||
assert mod.detect_pkg_manager(which=lambda name: None) is None
|
||||
assert mod.detect_pkg_manager(
|
||||
which=lambda name: "/usr/bin/dnf" if name == "dnf" else None) == "dnf"
|
||||
assert mod.install_command(None, "docker") is None
|
||||
hint = mod.manual_install_hint("docker")
|
||||
assert "LITE_SETUP" in hint # ссылка на § канона, не молчаливый пропуск
|
||||
|
||||
facts = _full_facts()
|
||||
facts["uname"] = "Darwin arm64"
|
||||
verdicts = dict((i, (v, d)) for i, v, d in mod.prereq_verdicts(facts))
|
||||
assert verdicts["os"][0] == "WARN"
|
||||
assert "вне контура Lite" in verdicts["os"][1]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-07 / AC-3: discovery — две независимые Plane-инсталляции → ровно 2
|
||||
# кандидата; выбор пользователя применяется; «ввести вручную» присутствует.
|
||||
# ---------------------------------------------------------------------------
|
||||
def _two_plane_containers() -> list:
|
||||
return [
|
||||
{"name": "plane-a-proxy-1", "image": "makeplane/plane-proxy:v0.23.1",
|
||||
"ports": "0.0.0.0:8080->80/tcp", "project": "plane-a"},
|
||||
{"name": "plane-a-api-1", "image": "makeplane/plane-backend:v0.23.1",
|
||||
"ports": "", "project": "plane-a"},
|
||||
{"name": "plane-a-db-1", "image": "postgres:15.7-alpine",
|
||||
"ports": "", "project": "plane-a"},
|
||||
{"name": "plane-b-proxy-1", "image": "makeplane/plane-proxy:v0.23.1",
|
||||
"ports": "0.0.0.0:9090->80/tcp", "project": "plane-b"},
|
||||
{"name": "shop-nginx-1", "image": "nginx:1.25",
|
||||
"ports": "0.0.0.0:80->80/tcp", "project": "shop"},
|
||||
]
|
||||
|
||||
|
||||
def test_tc07_two_plane_installations_listed_and_choice_applied():
|
||||
mod = _load_module()
|
||||
installs = mod.discover_installations(_two_plane_containers())
|
||||
planes = [i for i in installs if i["kind"] == "plane"]
|
||||
assert len(planes) == 2, planes
|
||||
assert {p["project"] for p in planes} == {"plane-a", "plane-b"}
|
||||
assert {p["url_port"] for p in planes} == {8080, 9090}
|
||||
|
||||
transcript: list = []
|
||||
io = _io(mod, answers=["2"], say=transcript)
|
||||
chosen = mod.choose_installation("Plane", planes, io)
|
||||
assert chosen is not None and chosen["url_port"] in (8080, 9090)
|
||||
blob = "\n".join(transcript)
|
||||
assert "1." in blob and "2." in blob, "нумерованный список не показан"
|
||||
assert "вручную" in blob, "пункт «ввести вручную» отсутствует (AC-3)"
|
||||
|
||||
# «ввести вручную» доступен и при ≥2 кандидатах
|
||||
io0 = _io(mod, answers=["0"])
|
||||
assert mod.choose_installation("Plane", planes, io0) is None
|
||||
|
||||
|
||||
def test_tc08_single_zero_and_foreign_images():
|
||||
mod = _load_module()
|
||||
containers = _two_plane_containers()
|
||||
# одна инсталляция → префилл по умолчанию (Enter = подтверждение)
|
||||
single = [i for i in mod.discover_installations(containers[:3])
|
||||
if i["kind"] == "plane"]
|
||||
assert len(single) == 1
|
||||
io = _io(mod, answers=[""])
|
||||
chosen = mod.choose_installation("Plane", single, io)
|
||||
assert chosen is single[0]
|
||||
|
||||
# ноль инсталляций → ручной ввод + честная подсказка про Bundled
|
||||
transcript: list = []
|
||||
io0 = _io(mod, answers=[], say=transcript)
|
||||
assert mod.choose_installation("Plane", [], io0) is None
|
||||
assert "BUNDLED_SETUP" in "\n".join(transcript)
|
||||
|
||||
# посторонние образы кандидатами не становятся (строго image-префиксы, D5)
|
||||
foreign_only = [c for c in containers if c["project"] == "shop"]
|
||||
assert mod.discover_installations(foreign_only) == []
|
||||
|
||||
|
||||
def test_tc08b_gitea_discovery_by_image_prefix():
|
||||
mod = _load_module()
|
||||
containers = [
|
||||
{"name": "git-gitea-1", "image": "docker.gitea.com/gitea:1.24.9",
|
||||
"ports": "0.0.0.0:3300->3000/tcp", "project": "git"},
|
||||
]
|
||||
giteas = [i for i in mod.discover_installations(containers)
|
||||
if i["kind"] == "gitea"]
|
||||
assert len(giteas) == 1 and giteas[0]["url_port"] == 3300
|
||||
|
||||
|
||||
def test_tc09_docker_failure_is_never_block(monkeypatch):
|
||||
mod = _load_module()
|
||||
|
||||
def _boom(*args, **kwargs):
|
||||
raise OSError("docker недоступен")
|
||||
|
||||
monkeypatch.setattr(mod, "_run", _boom)
|
||||
assert mod.list_containers() is None # ошибка перечисления → None, не исключение
|
||||
# классификатор на «нет данных» отвечает пустым перечнем (ручной ввод дальше)
|
||||
assert mod.discover_installations([]) == []
|
||||
assert mod.parse_docker_ps("") == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-10 / AC-4: цикл запроса — re-prompt с диагнозом, лимит попыток, не
|
||||
# бесконечный цикл; успешная верификация → значение принято.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc10_verify_retry_limit_and_success():
|
||||
mod = _load_module()
|
||||
transcript: list = []
|
||||
attempts: list = []
|
||||
|
||||
def verify_fail(value):
|
||||
attempts.append(value)
|
||||
return False, "HTTP 401: токен отклонён"
|
||||
|
||||
io = _io(mod, secrets_=["t1", "t2", "t3", "t4"], say=transcript)
|
||||
with pytest.raises(mod.ManualStop):
|
||||
io.ask("ORCH_PLANE_API_TOKEN", "Plane API token", secret=True,
|
||||
verify=verify_fail, max_tries=3)
|
||||
assert len(attempts) == 3, "лимит попыток не соблюдён (не бесконечный цикл)"
|
||||
assert any("401" in line for line in transcript), "re-prompt без диагноза"
|
||||
|
||||
def verify_second(value):
|
||||
return (value == "good", "HTTP 401")
|
||||
|
||||
io2 = _io(mod, secrets_=["bad", "good"])
|
||||
assert io2.ask("ORCH_GITEA_TOKEN", "Gitea token", secret=True,
|
||||
verify=verify_second) == "good"
|
||||
|
||||
|
||||
def test_tc10b_env_prefill_is_used_and_verified():
|
||||
mod = _load_module()
|
||||
seen: list = []
|
||||
io = _io(mod, env={"ORCH_GITEA_TOKEN": "from-env"}, is_tty=False, yes=True)
|
||||
value = io.ask("ORCH_GITEA_TOKEN", "Gitea token", secret=True,
|
||||
verify=lambda v: (seen.append(v) or True, ""))
|
||||
assert value == "from-env" and seen == ["from-env"] # верификация выполняется
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-11 / AC-4: секрет-гигиена — значение секрета не попадает в транскрипт.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc11_secret_values_never_printed(capsys):
|
||||
mod = _load_module()
|
||||
secret = "supersecret-token-value-123"
|
||||
transcript: list = []
|
||||
io = _io(mod, secrets_=[secret], say=transcript)
|
||||
value = io.ask("ORCH_TELEGRAM_BOT_TOKEN", "токен бота", secret=True,
|
||||
verify=lambda v: (True, ""))
|
||||
assert value == secret
|
||||
blob = "\n".join(transcript) + capsys.readouterr().out
|
||||
assert secret not in blob, "значение секрета напечатано (NFR-3)"
|
||||
assert "ORCH_TELEGRAM_BOT_TOKEN" in blob or transcript == [], (
|
||||
"в транскрипте допустимы только имена ключей"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-12 / AC-4: non-TTY без альтернативы → честный отказ, не зависание.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc12_non_tty_is_deterministic_refusal():
|
||||
mod = _load_module()
|
||||
io = _io(mod, is_tty=False)
|
||||
with pytest.raises(mod.ManualStop):
|
||||
io.ask("ORCH_PLANE_API_TOKEN", "Plane API token", secret=True)
|
||||
# consent в non-TTY без --yes — тоже честный отказ
|
||||
with pytest.raises(mod.ManualStop):
|
||||
io.consent("выполнить мутацию")
|
||||
# с --yes согласие дано флагом (headless-consent, D10)
|
||||
io_yes = _io(mod, is_tty=False, yes=True)
|
||||
assert io_yes.consent("выполнить мутацию") is True
|
||||
# non-TTY + непустой дефолт (автодетект) → дефолт принимается, прогон жив
|
||||
io_def = _io(mod, is_tty=False, yes=True)
|
||||
assert io_def.ask("ORCH_RUN_UID", "uid", default="1001") == "1001"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-13 / AC-5: сборка .env/.env.watchdog — группы ключей, файл-носитель §4.3,
|
||||
# свежие 64-hex webhook-секреты кирпичом gen_secrets.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc13_split_overrides_watchdog_keys_only_in_watchdog_file():
|
||||
mod = _load_module()
|
||||
answers = {
|
||||
"ORCH_PLANE_API_URL": "http://127.0.0.1:8080",
|
||||
"ORCH_TELEGRAM_BOT_TOKEN": "tg-orch",
|
||||
"WATCHDOG_TG_BOT_TOKEN": "tg-wd",
|
||||
"WATCHDOG_TG_CHAT_ID": "42",
|
||||
"WATCHDOG_METRICS_URL": "http://127.0.0.1:8500/metrics",
|
||||
}
|
||||
root_ov, wd_ov = mod.split_overrides(answers)
|
||||
assert "WATCHDOG_TG_BOT_TOKEN" not in root_ov # ловушка файла-носителя §4.3
|
||||
assert "WATCHDOG_TG_CHAT_ID" not in root_ov
|
||||
assert wd_ov["WATCHDOG_TG_BOT_TOKEN"] == "tg-wd"
|
||||
assert wd_ov["WATCHDOG_TG_CHAT_ID"] == "42"
|
||||
assert root_ov["ORCH_PLANE_API_URL"] == "http://127.0.0.1:8080"
|
||||
# когерентный WATCHDOG_METRICS_URL уходит в файл-носитель sidecar'а
|
||||
assert wd_ov["WATCHDOG_METRICS_URL"] == "http://127.0.0.1:8500/metrics"
|
||||
|
||||
|
||||
def test_tc13b_webhook_secrets_are_fresh_64_hex_via_brick():
|
||||
mod = _load_module()
|
||||
first = mod.issue_webhook_secrets()
|
||||
second = mod.issue_webhook_secrets()
|
||||
for batch in (first, second):
|
||||
assert set(batch) == {"ORCH_PLANE_WEBHOOK_SECRET", "ORCH_GITEA_WEBHOOK_SECRET"}
|
||||
for value in batch.values():
|
||||
assert len(value) == 64 and all(c in "0123456789abcdef" for c in value)
|
||||
assert first != second, "повторный выпуск обязан давать другие значения (AC-5)"
|
||||
|
||||
|
||||
def test_tc13c_render_env_keeps_canon_and_adds_marker():
|
||||
mod = _load_module()
|
||||
example = "# шапка канона\nA=1\nB=\n"
|
||||
rendered = mod.render_env(example, {"B": "v", "NEW": "n"})
|
||||
assert rendered.startswith(mod.MANAGED_MARKER), "маркер managed-файла (D6)"
|
||||
assert "# шапка канона" in rendered and "A=1" in rendered
|
||||
assert mod.parse_env(rendered) == {"A": "1", "B": "v", "NEW": "n"}
|
||||
|
||||
|
||||
def test_tc13d_mandatory_key_groups_covered():
|
||||
"""Карта обязательных ключей §4.2 LITE_SETUP покрыта константой скрипта."""
|
||||
mod = _load_module()
|
||||
keys = set(mod.MANDATORY_NEW_HOST_KEYS)
|
||||
for required in (
|
||||
"ORCH_PLANE_API_URL", "ORCH_PLANE_WEB_URL", "ORCH_PLANE_WORKSPACE_SLUG",
|
||||
"ORCH_PLANE_API_TOKEN", "ORCH_GITEA_URL", "ORCH_GITEA_PUBLIC_URL",
|
||||
"ORCH_GITEA_OWNER", "ORCH_GITEA_TOKEN", "ORCH_PLANE_WEBHOOK_SECRET",
|
||||
"ORCH_GITEA_WEBHOOK_SECRET", "ORCH_TELEGRAM_BOT_TOKEN",
|
||||
"ORCH_TELEGRAM_CHAT_ID", "ORCH_RUN_UID", "ORCH_RUN_GID",
|
||||
"ORCH_DOCKER_GID", "ORCH_HOST_REPOS_DIR",
|
||||
"ORCH_DEPLOY_PROD_TARGET_PORT", "ORCH_STAGING_PORT",
|
||||
):
|
||||
assert required in keys, f"{required} не покрыт картой §4.2 (AC-5)"
|
||||
# все имена — канонические (существуют в .env.example)
|
||||
canon = set()
|
||||
for line in (REPO_ROOT / ".env.example").read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
canon.add(line.split("=", 1)[0].strip())
|
||||
assert keys <= canon, f"вне канона .env.example: {sorted(keys - canon)}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-14 / AC-5: существующий чужой .env → отказ без force, байт-в-байт; маркер
|
||||
# managed-файла → resume-ensure; с force — перезапись.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc14_existing_foreign_env_is_refused_byte_identical(tmp_path):
|
||||
mod = _load_module()
|
||||
env = tmp_path / ".env"
|
||||
env.write_text("MY_LIVE_KEY=1\n", encoding="utf-8")
|
||||
before = env.read_bytes()
|
||||
assert mod.env_file_state(env.read_text(encoding="utf-8")) == "foreign"
|
||||
assert mod.env_file_state(None) == "absent"
|
||||
|
||||
io = _io(mod, answers=["y"])
|
||||
with pytest.raises(mod.ManualStop):
|
||||
mod.ensure_env_file(str(env), "A=\n", {"A": "x"}, force=False, io=io)
|
||||
assert env.read_bytes() == before, "чужой .env изменён без force (NFR-7)"
|
||||
|
||||
# с явным force (+согласие) — перезапись выполняется
|
||||
io2 = _io(mod, answers=["y"])
|
||||
mod.ensure_env_file(str(env), "A=\n", {"A": "x"}, force=True, io=io2)
|
||||
text = env.read_text(encoding="utf-8")
|
||||
assert text.startswith(mod.MANAGED_MARKER) and "A=x" in text
|
||||
|
||||
|
||||
def test_tc14b_managed_file_resume_ensure_keeps_existing_values(tmp_path):
|
||||
mod = _load_module()
|
||||
env = tmp_path / ".env"
|
||||
env.write_text(mod.MANAGED_MARKER + "\nA=keep\nB=\n", encoding="utf-8")
|
||||
assert mod.env_file_state(env.read_text(encoding="utf-8")) == "managed"
|
||||
io = _io(mod, answers=[])
|
||||
mod.ensure_env_file(str(env), "A=\nB=\nC=\n", {"A": "new", "B": "vb", "C": "vc"},
|
||||
force=False, io=io)
|
||||
got = mod.parse_env(env.read_text(encoding="utf-8"))
|
||||
assert got["A"] == "keep", "resume-ensure перетёр существующее значение (D6)"
|
||||
assert got["B"] == "vb" and got["C"] == "vc", "недостающие ключи не дозаполнены"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-15 / AC-5: подсказки-дефолты — только автодетект/канон, без боевых значений.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc15_prompt_defaults_come_from_autodetect_without_forbidden():
|
||||
mod = _load_module()
|
||||
defaults = mod.prompt_defaults(_full_facts())
|
||||
blob = json.dumps(defaults, ensure_ascii=False)
|
||||
for literal in FORBIDDEN:
|
||||
assert literal not in blob, f"боевой литерал {literal!r} в дефолтах промптов"
|
||||
assert defaults["ORCH_RUN_UID"] == "1001"
|
||||
assert defaults["ORCH_DOCKER_GID"] == "984"
|
||||
assert defaults["ORCH_HOST_NODE_BIN"] == "/usr/bin/node"
|
||||
assert defaults["ORCH_AGENT_HOME_DIR"] == "/home/operator"
|
||||
assert defaults["ORCH_HOST_CLAUDE_DIR"] == "/home/operator/.claude"
|
||||
|
||||
|
||||
def test_tc15b_script_source_carries_no_forbidden_literals():
|
||||
src = _source()
|
||||
offenders = [literal for literal in FORBIDDEN if literal in src]
|
||||
assert not offenders, f"боевые литералы в setup_lite.py: {offenders}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-16 / AC-6: когерентная тройка портов одной функцией; занятый порт →
|
||||
# альтернатива.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc16_port_overrides_keep_triple_coherent():
|
||||
mod = _load_module()
|
||||
overrides = mod.port_overrides(8700)
|
||||
assert overrides["ORCH_DEPLOY_PROD_TARGET_PORT"] == "8700"
|
||||
assert overrides["WATCHDOG_METRICS_URL"] == "http://127.0.0.1:8700/metrics"
|
||||
assert overrides["ORCH_POST_DEPLOY_BASE_URL"] == "http://localhost:8700"
|
||||
# занятый порт → предложена альтернатива (мок busy-check)
|
||||
assert mod.next_free_port(8500, busy=lambda p: p in (8500, 8501)) == 8502
|
||||
|
||||
|
||||
def test_tc17_staging_port_equal_to_prod_is_fail_closed():
|
||||
mod = _load_module()
|
||||
assert mod.staging_port_ok(8500, 8500) is False # инвариант ORCH-058/101
|
||||
assert mod.staging_port_ok(8501, 8500) is True
|
||||
# в цикле ввода значение не принимается — re-prompt до различного
|
||||
io = _io(mod, answers=["8500", "8501"])
|
||||
value = io.ask("ORCH_STAGING_PORT", "staging-порт", default="8501",
|
||||
verify=lambda v: (mod.staging_port_ok(int(v), 8500),
|
||||
"staging-порт обязан отличаться от прод-порта"))
|
||||
assert value == "8501"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-18 / AC-7: C-1 — одинаковые токены орка и watchdog запрещены машинно.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc18_telegram_c1_identical_tokens_refused():
|
||||
mod = _load_module()
|
||||
ok, hint = mod.telegram_c1_verdict("same-token", "same-token")
|
||||
assert ok is False
|
||||
assert "C-1" in hint or "ЗАПРЕЩЕНО" in hint.upper(), "отказ без объяснения запрета"
|
||||
ok2, _ = mod.telegram_c1_verdict("token-a", "token-b")
|
||||
assert ok2 is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-19 / AC-8: branch protection — честный FAIL с лечением, без удаления.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc19_branch_protection_verdict():
|
||||
mod = _load_module()
|
||||
ok, hint = mod.branch_protection_verdict(200, [{"branch_name": "main"}])
|
||||
assert ok is False
|
||||
assert "6.4" in hint or "13.7" in hint, "лечение без ссылки на норматив §6.4"
|
||||
assert mod.branch_protection_verdict(200, [])[0] is True
|
||||
# репо ещё не создан (создаст onboarding) — не FAIL
|
||||
assert mod.branch_protection_verdict(404, None)[0] is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-20 / AC-8 (D8): webhook Plane Path Б — только с явного согласия; отказ →
|
||||
# UI-путь, мутирующий вызов не произведён; после согласия — пост-верификация.
|
||||
# ---------------------------------------------------------------------------
|
||||
def _webhook_answers() -> dict:
|
||||
return {
|
||||
"plane_db_container": "plane-a-db-1",
|
||||
"plane_db_user": "plane",
|
||||
"plane_db_name": "plane",
|
||||
"plane_db_password": "pw",
|
||||
"ORCH_PLANE_WORKSPACE_SLUG": "acme",
|
||||
"ORCH_PLANE_WEBHOOK_SECRET": "a" * 64,
|
||||
"orchestrator_public_url": "https://orch.example.com",
|
||||
}
|
||||
|
||||
|
||||
def test_tc20_path_b_refused_no_mutation():
|
||||
mod = _load_module()
|
||||
calls: list = []
|
||||
|
||||
def psql(sql):
|
||||
calls.append(sql)
|
||||
return 0, "0"
|
||||
|
||||
transcript: list = []
|
||||
io = _io(mod, answers=["n"], say=transcript) # отказ от согласия на SQL
|
||||
status = mod.plane_webhook_path_b(_webhook_answers(), io, psql)
|
||||
assert status == "manual"
|
||||
assert not any("INSERT" in c.upper() for c in calls), (
|
||||
"мутирующий SQL выполнен без согласия (D8)"
|
||||
)
|
||||
assert "INSERT" in "\n".join(transcript).upper(), (
|
||||
"точный SQL не показан ДО запроса согласия (D8 п.2)"
|
||||
)
|
||||
|
||||
|
||||
def test_tc20b_path_b_consent_insert_and_postverify():
|
||||
mod = _load_module()
|
||||
state = {"rows": 0}
|
||||
calls: list = []
|
||||
|
||||
def psql(sql):
|
||||
calls.append(sql)
|
||||
if "INSERT" in sql.upper():
|
||||
state["rows"] = 1
|
||||
return 0, ""
|
||||
if "count(*)" in sql:
|
||||
return 0, str(state["rows"])
|
||||
if "SELECT id FROM workspaces" in sql:
|
||||
return 0, "11111111-2222-3333-4444-555555555555"
|
||||
return 0, ("https://orch.example.com/webhook/plane|t" if state["rows"] else "")
|
||||
|
||||
io = _io(mod, answers=["y"])
|
||||
assert mod.plane_webhook_path_b(_webhook_answers(), io, psql) == "ok"
|
||||
assert any("INSERT" in c.upper() for c in calls)
|
||||
# пост-верификация обязательна: INSERT прошёл, но строки нет → НЕ PASS
|
||||
io2 = _io(mod, answers=["y"])
|
||||
|
||||
def psql_lost(sql):
|
||||
if "count(*)" in sql:
|
||||
return 0, "0"
|
||||
if "SELECT id FROM workspaces" in sql:
|
||||
return 0, "11111111-2222-3333-4444-555555555555"
|
||||
if "INSERT" in sql.upper():
|
||||
return 0, ""
|
||||
return 0, ""
|
||||
|
||||
assert mod.plane_webhook_path_b(_webhook_answers(), io2, psql_lost) == "manual"
|
||||
|
||||
|
||||
def test_tc20c_path_b_is_idempotent_skip_when_registered():
|
||||
mod = _load_module()
|
||||
calls: list = []
|
||||
|
||||
def psql(sql):
|
||||
calls.append(sql)
|
||||
return 0, "1" # webhook уже зарегистрирован
|
||||
|
||||
io = _io(mod, answers=["y"])
|
||||
assert mod.plane_webhook_path_b(_webhook_answers(), io, psql) == "skipped"
|
||||
assert not any("INSERT" in c.upper() for c in calls)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-21 / AC-9: запуск — up только после согласия; состав «ровно орк+watchdog»;
|
||||
# health-контракты; stateless-проверка.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc21_composition_verdict_is_exactly_two_services():
|
||||
mod = _load_module()
|
||||
ok, _ = mod.lite_composition_verdict(["orchestrator", "orchestrator-watchdog"])
|
||||
assert ok is True
|
||||
bad, hint = mod.lite_composition_verdict(
|
||||
["orchestrator", "orchestrator-watchdog", "orchestrator-staging"])
|
||||
assert bad is False and "staging" in hint
|
||||
assert mod.lite_composition_verdict(["orchestrator"])[0] is False
|
||||
|
||||
|
||||
def test_tc21b_health_checks_require_contract_bodies():
|
||||
mod = _load_module()
|
||||
bodies = {
|
||||
"/health": (200, '{"status":"ok"}'),
|
||||
"/queue": (200, '{"counts": {}, "recent": []}'),
|
||||
"/metrics": (200, '{"schema_version": 1}'),
|
||||
}
|
||||
|
||||
def http(url, headers=None, timeout=10):
|
||||
for path, resp in bodies.items():
|
||||
if url.endswith(path):
|
||||
return resp
|
||||
return None, ""
|
||||
|
||||
results = mod.health_checks(http, 8500)
|
||||
assert all(ok for _, ok, _ in results), results
|
||||
bodies["/metrics"] = (200, '{"schema_version": 2}')
|
||||
results2 = mod.health_checks(http, 8500)
|
||||
assert any(path == "/metrics" and not ok for path, ok, _ in results2)
|
||||
|
||||
|
||||
def test_tc21c_step_up_requires_consent_and_checks_composition(monkeypatch):
|
||||
mod = _load_module()
|
||||
compose_calls: list = []
|
||||
|
||||
class _Proc:
|
||||
returncode = 0
|
||||
stderr = ""
|
||||
stdout = "orchestrator\norchestrator-watchdog\n"
|
||||
|
||||
monkeypatch.setattr(mod, "_compose",
|
||||
lambda *args, **kw: (compose_calls.append(args), _Proc())[1])
|
||||
monkeypatch.setattr(mod, "_http", lambda url, **kw: (
|
||||
(200, '{"status":"ok"}') if url.endswith("/health")
|
||||
else (200, '{"schema_version": 1}') if url.endswith("/metrics")
|
||||
else (200, '{"counts": {}, "recent": []}')))
|
||||
ctx = {"io": _io(mod, answers=["y"]), "answers": {}, "results": {}}
|
||||
assert mod.step_up(ctx) == "ok"
|
||||
assert any("up" in c for c in compose_calls), "compose up не вызван"
|
||||
|
||||
# отказ от согласия → MANUAL, ни одного вызова compose-мутации
|
||||
compose_calls.clear()
|
||||
ctx2 = {"io": _io(mod, answers=["n"]), "answers": {}, "results": {}}
|
||||
assert mod.step_up(ctx2) == "manual"
|
||||
assert not any("up" in c for c in compose_calls), "контур поднят без согласия"
|
||||
|
||||
|
||||
def test_tc21d_step_up_fails_on_staging_in_composition(monkeypatch):
|
||||
mod = _load_module()
|
||||
|
||||
class _Proc:
|
||||
returncode = 0
|
||||
stderr = ""
|
||||
stdout = "orchestrator\norchestrator-watchdog\norchestrator-staging\n"
|
||||
|
||||
monkeypatch.setattr(mod, "_compose", lambda *args, **kw: _Proc())
|
||||
ctx = {"io": _io(mod, answers=["y"]), "answers": {}, "results": {}}
|
||||
with pytest.raises(mod.SetupError):
|
||||
mod.step_up(ctx)
|
||||
|
||||
|
||||
def test_tc21e_stateless_verdict_flags_foreign_tasks():
|
||||
mod = _load_module()
|
||||
clean = {"counts": {"queued": 0, "running": 0}, "recent": []}
|
||||
assert mod.stateless_verdict(clean, own_prefixes=("SHP",))[0] is True
|
||||
dirty = {"counts": {"done": 3},
|
||||
"recent": [{"work_item_id": "FOO-61"}]}
|
||||
ok, hint = mod.stateless_verdict(dirty, own_prefixes=("SHP",))
|
||||
assert ok is False and "FOO-61" in hint
|
||||
own = {"counts": {"done": 1}, "recent": [{"work_item_id": "SHP-1"}]}
|
||||
assert mod.stateless_verdict(own, own_prefixes=("SHP",))[0] is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-22 / AC-10: onboarding — builder аргументов чистой функцией; строго
|
||||
# последовательность plan → согласие → apply → verify; exit 2 кирпича → MANUAL.
|
||||
# ---------------------------------------------------------------------------
|
||||
def _project_answers(tmp_path=None) -> dict:
|
||||
return {
|
||||
"project_name": "Shop",
|
||||
"project_description": "магазин",
|
||||
"project_repo": "shop",
|
||||
"project_prefix": "SHP",
|
||||
"project_stack": "python",
|
||||
"project_test_cmd": "pytest -q",
|
||||
"project_prod_port": "8600",
|
||||
"project_staging_port": "8601",
|
||||
"ORCH_GITEA_OWNER": "acme",
|
||||
"orchestrator_public_url": "https://orch.example.com",
|
||||
}
|
||||
|
||||
|
||||
def test_tc22_build_onboard_args_is_pure_and_complete():
|
||||
mod = _load_module()
|
||||
args = mod.build_onboard_args(_project_answers(), "plan")
|
||||
assert args[0].endswith("onboard_project.py") and args[1] == "plan"
|
||||
pairs = dict(zip(args[2::2], args[3::2]))
|
||||
assert pairs["--name"] == "Shop" and pairs["--repo"] == "shop"
|
||||
assert pairs["--prefix"] == "SHP" and pairs["--stack"] == "python"
|
||||
assert pairs["--test-cmd"] == "pytest -q"
|
||||
assert pairs["--prod-port"] == "8600" and pairs["--staging-port"] == "8601"
|
||||
assert pairs["--webhook-url"] == "https://orch.example.com/webhook/gitea"
|
||||
assert pairs["--gitea-owner"] == "acme"
|
||||
assert "--json" in args
|
||||
# детерминизм чистой функции
|
||||
assert args == mod.build_onboard_args(_project_answers(), "plan")
|
||||
|
||||
|
||||
def test_tc22b_onboard_sequence_plan_consent_apply_verify(tmp_path, monkeypatch):
|
||||
mod = _load_module()
|
||||
modes: list = []
|
||||
report = {"steps": [], "instructions": [
|
||||
"Добавь/обнови строку в .env оркестратора: ORCH_PROJECTS_JSON="
|
||||
'[{"repo": "shop"}]',
|
||||
]}
|
||||
|
||||
class _Proc:
|
||||
def __init__(self, rc, out=""):
|
||||
self.returncode = rc
|
||||
self.stdout = out
|
||||
self.stderr = ""
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
mode = next((m for m in ("plan", "apply", "verify") if m in cmd), "?")
|
||||
modes.append(mode)
|
||||
if mode == "apply":
|
||||
return _Proc(0, json.dumps(report))
|
||||
if mode == "verify":
|
||||
return _Proc(2) # остались ручные шаги
|
||||
return _Proc(0, "план...")
|
||||
|
||||
env = tmp_path / ".env"
|
||||
env.write_text(mod.MANAGED_MARKER + "\nORCH_PROJECTS_JSON=\n", encoding="utf-8")
|
||||
monkeypatch.setattr(mod, "_run", fake_run)
|
||||
monkeypatch.setattr(mod, "_ensure_venv", lambda: "python3")
|
||||
monkeypatch.setattr(mod, "_http", lambda url, **kw: (200, '{"counts": {}, "recent": []}'))
|
||||
monkeypatch.setattr(mod, "_compose", lambda *a, **kw: _Proc(0))
|
||||
ctx = {
|
||||
"io": _io(mod, answers=["y", "y", "y"]),
|
||||
"answers": _project_answers(),
|
||||
"results": {},
|
||||
"paths": {"root_env": str(env),
|
||||
"root_env_example": str(REPO_ROOT / ".env.example")},
|
||||
"args": mod.build_arg_parser().parse_args([]),
|
||||
}
|
||||
status = mod.step_onboard(ctx)
|
||||
assert modes[:2] == ["plan", "apply"] and "verify" in modes, (
|
||||
f"последовательность кирпича нарушена: {modes}"
|
||||
)
|
||||
assert status == "manual", "exit 2 кирпича обязан транслироваться как MANUAL"
|
||||
got = mod.parse_env(env.read_text(encoding="utf-8"))
|
||||
assert got["ORCH_PROJECTS_JSON"] == '[{"repo": "shop"}]'
|
||||
|
||||
|
||||
def test_tc22c_onboard_consent_refused_no_apply(tmp_path, monkeypatch):
|
||||
mod = _load_module()
|
||||
modes: list = []
|
||||
|
||||
class _Proc:
|
||||
returncode = 0
|
||||
stdout = "план..."
|
||||
stderr = ""
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
modes.append(next((m for m in ("plan", "apply", "verify") if m in cmd), "?"))
|
||||
return _Proc()
|
||||
|
||||
monkeypatch.setattr(mod, "_run", fake_run)
|
||||
monkeypatch.setattr(mod, "_ensure_venv", lambda: "python3")
|
||||
env = tmp_path / ".env"
|
||||
env.write_text(mod.MANAGED_MARKER + "\n", encoding="utf-8")
|
||||
ctx = {
|
||||
"io": _io(mod, answers=["n"]), # план показан → отказ
|
||||
"answers": _project_answers(),
|
||||
"results": {},
|
||||
"paths": {"root_env": str(env),
|
||||
"root_env_example": str(REPO_ROOT / ".env.example")},
|
||||
"args": mod.build_arg_parser().parse_args([]),
|
||||
}
|
||||
assert mod.step_onboard(ctx) == "manual"
|
||||
assert modes == ["plan"], f"apply вызван без согласия после плана: {modes}"
|
||||
|
||||
|
||||
def test_tc22d_extract_projects_json_from_brick_instructions():
|
||||
mod = _load_module()
|
||||
instructions = [
|
||||
"что-то ещё",
|
||||
'Добавь/обнови строку: ORCH_PROJECTS_JSON=[{"repo": "shop"}]',
|
||||
]
|
||||
assert mod.extract_projects_json(instructions) == '[{"repo": "shop"}]'
|
||||
assert mod.extract_projects_json([]) == ""
|
||||
assert mod.extract_projects_json(["без реестра"]) == ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-23 / AC-11: контракт exit-кодов — именованные константы 0/2/1.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc23_exit_code_contract():
|
||||
mod = _load_module()
|
||||
assert (mod.EXIT_OK, mod.EXIT_MANUAL, mod.EXIT_ERROR) == (0, 2, 1)
|
||||
assert mod.exit_code_for({"a": "ok", "b": "skip"}) == 0
|
||||
assert mod.exit_code_for({"a": "ok", "b": "manual"}) == 2
|
||||
assert mod.exit_code_for({}) == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-24 / AC-12: ast-скан — stdlib-only, без модулей платформы; кирпичи и канон
|
||||
# упомянуты.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc24_imports_are_stdlib_only():
|
||||
tree = ast.parse(_source())
|
||||
offenders = []
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
offenders.extend(a.name.split(".")[0] for a in node.names
|
||||
if a.name.split(".")[0] not in STDLIB_ALLOWED)
|
||||
elif isinstance(node, ast.ImportFrom) and node.module:
|
||||
top = node.module.split(".")[0]
|
||||
if top not in STDLIB_ALLOWED:
|
||||
offenders.append(top)
|
||||
assert not offenders, f"не-stdlib импорты в setup_lite.py (D1): {sorted(set(offenders))}"
|
||||
|
||||
|
||||
def test_tc24b_no_platform_imports():
|
||||
src = _source()
|
||||
assert "from src" not in src and "import src" not in src, (
|
||||
"setup_lite обязан быть stdlib-only без импортов платформы (D1)"
|
||||
)
|
||||
|
||||
|
||||
def test_tc24c_canonical_bricks_and_doc_are_referenced():
|
||||
src = _source()
|
||||
assert "gen_secrets.py" in src, "webhook-секреты обязаны идти через gen_secrets.py"
|
||||
assert "onboard_project.py" in src, "онбординг обязан идти через onboard_project.py"
|
||||
assert "LITE_SETUP.md" in src, "каждый шаг обязан ссылаться на канон LITE_SETUP.md"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-25 / AC-12: эвристический скан — delete-операций нет; собственного канона
|
||||
# статусов нет; import модуля без side effects.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc25_no_delete_operations():
|
||||
src = _source()
|
||||
offenders = [n for n in FORBIDDEN_DELETE_NEEDLES if n in src]
|
||||
assert not offenders, (
|
||||
f"delete-операции в setup_lite запрещены (no-delete, D1): {offenders}"
|
||||
)
|
||||
|
||||
|
||||
def test_tc25b_no_own_status_canon():
|
||||
src = _source()
|
||||
offenders = [n for n in FORBIDDEN_STATUS_NEEDLES if n in src]
|
||||
assert not offenders, (
|
||||
f"setup_lite несёт собственный канон статусов (дрейф BR-7): {offenders}; "
|
||||
"статусы — только onboard_project.py/plane_sync"
|
||||
)
|
||||
|
||||
|
||||
def test_tc25c_module_import_has_no_side_effects():
|
||||
mod1 = _load_module()
|
||||
mod2 = _load_module()
|
||||
assert mod1.build_plan() == mod2.build_plan()
|
||||
|
||||
|
||||
def test_apply_steps_match_normative_plan():
|
||||
"""Инвариант D3: имена step-движка = нормативному плану (10 шагов)."""
|
||||
mod = _load_module()
|
||||
plan = mod.build_plan()
|
||||
assert len(plan) == 10, f"нормативный план D3 — 10 шагов, получено {len(plan)}"
|
||||
assert [name for name, _, _ in mod.APPLY_STEPS] == [name for name, _ in plan]
|
||||
assert plan[0][0] == "scan" and plan[-1][0] == "report"
|
||||
Reference in New Issue
Block a user