feat(replication): расхардкод хоста + секреты нового хоста + smoke-runbook
All checks were successful
CI / test (push) Successful in 57s
CI / test (pull_request) Successful in 55s

Фундамент тиража 10-common (эпик ORCH-10): платформа разворачивается на
новой инфре без правки кода — только env/конфиг. Каждый дефолт = боевому
значению (пустой .env => поведение 1:1, kill-switch-природа, NFR-2);
STAGE_TRANSITIONS/QG_CHECKS/check_*/machine-verdict/схема БД не тронуты.

- config: agent_home_dir / agent_git_name / git_email_domain / staging_port
  (ADR-001 D2/D4); код-блокеры A1-A4 закрыты: plane_sync ссылки из
  gitea_public_url+gitea_owner, launcher - единый agent_git_env() (x2 места),
  self_deploy/post_deploy - HOME+домен из Settings (имена системных акторов -
  платформенные литералы)
- image_freshness: staging_port из конфига + fail-closed guard
  staging_port == прод-порт -> отказ ДО ssh/build (инвариант ORCH-058 AC-9
  стал исполняемым); REPO= передаётся хуку явно обоими инвокерами (D7)
- SELF_HOSTING_REPO - нормативная платформенная константа (D3, пин-тест)
- compose: полная ${VAR:-default}-интерполяция (реестр B, карта D6); группа
  ORCH-040 uid/gid/HOME/маунты двигается согласованно (build.args APP_*);
  group_add "МИНА 1" сохранён x3; оба app-сервиса с явным command:
- Dockerfile: ARG APP_UID/APP_GID/APP_USER/APP_HOME (CMD exec-form 8500
  сознательно не тронут - D5); deploy-hook: REPO="${REPO:-...}" (D1 реестра)
- секреты: stdlib scripts/gen_secrets.py (token_hex(32); печать по умолчанию;
  --write никогда не перезаписывает существующий .env молча, exit=2;
  перезапись только --force); .env.example дополнен до полноты ключей старта
- доки: новый docs/operations/REPLICATION.md (карта env, чек-лист секретов,
  smoke-процедура с PASS/FAIL, границы 10-common/Lite/Bundled), INFRA.md,
  README, CLAUDE.md, CHANGELOG
- анти-регресс: tests/test_no_host_hardcodes.py (tokenize-сканер запрещённых
  литералов, config-модули - структурное исключение, allowlist пуст,
  негативная самопроверка) + test_host_config_keys / test_infra_parametrization
  / test_secrets_gen / test_replication_smoke; согласованные структурные
  правки test_orch040_compose (судит резолв дефолтов) и
  test_deploy_hook_rollback_sim (REPO через env-override = контракт D7)

Полный регресс: 1764 passed.

Refs: ORCH-101

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 20:50:43 +03:00
parent 26bdd783d6
commit f1635ddb39
26 changed files with 1583 additions and 86 deletions

View File

@@ -158,7 +158,7 @@ orchestrator (каноны ORCH-52b/c/d/e); enduro-trails эталоном не
`docs/work-items/ORCH-009/06-adr/ADR-001-turnkey-onboarding-kit-and-cli.md` (D1…D11),
`docs/work-items/ORCH-009/07-infra-requirements.md`.
## Тираж платформы: фундамент 10-common (ORCH-101 — design)
## Тираж платформы: фундамент 10-common (ORCH-101)
Фундамент эпика ORCH-10 (D5.3 «Масштаб»: раздача платформы заказчикам-тестерам, типы A Lite /
B Bundled, оба stateless). Платформа разворачивается на новой инфре **без правки кода** — только

View File

@@ -171,8 +171,15 @@ watchdog'а: **watchdog сигналит, pruner убирает**.
| `ORCH_BUILD_CACHE_PRUNE_TIMEOUT_S` | таймаут ssh-команды prune, сек; дефолт `120` |
| `ORCH_BUILD_CACHE_PRUNE_NOTIFY_MIN_GB` | Telegram при освобождении ≥ N ГБ; дефолт `0` (тихо) |
| `DEPLOY_SSH_USER` / `_HOST` / `DEPLOY_HOOK_SCRIPT` | параметры деплой-хука |
| `ORCH_AGENT_HOME_DIR` | ORCH-101: HOME всех акторских subprocess-env (агенты/finalizer/monitor) **и** таргет маунтов `.claude`/`.claude.json`/`.ssh` **и** `ARG APP_HOME` Dockerfile (группа ORCH-040 двигается согласованно); дефолт `/home/slin` |
| `ORCH_AGENT_GIT_NAME` / `ORCH_GIT_EMAIL_DOMAIN` | ORCH-101: git-идентичность коммитов агентов (`claude-bot` @ `mva154.local`); системные акторы держат платформенные имена `deploy-finalizer`/`post-deploy-monitor` под тем же доменом |
| `ORCH_STAGING_PORT` | ORCH-101: порт staging-инстанса (дефолт `8501`); читается и `image_freshness`, и compose `command:` staging; guard fail-closed при совпадении с прод-портом (ORCH-058 AC-9) |
| `ORCH_HOST_CLAUDE_DIR` / `_CLAUDE_JSON` / `_SSH_DIR` | ORCH-101: host-источники bind-маунтов `~/.claude`, `~/.claude.json`, ssh-ключей (`/home/slin/.{claude,claude.json,orchestrator-ssh}`) |
| `ORCH_HOST_CLAUDE_CODE_DIR` / `_NODE_BIN` | ORCH-101: host-пути дистрибутива claude-code и бинаря node (`/usr/lib/node_modules/@anthropic-ai/claude-code`, `/usr/bin/node`) |
| `ORCH_RUN_UID` / `ORCH_RUN_GID` | ORCH-101: uid:gid контейнера (`user:`) + `ARG APP_UID/APP_GID` (дефолт `1000:1000`, ORCH-040) |
| `ORCH_DOCKER_GID` | ORCH-101: gid docker-группы хоста для `group_add` (дефолт `999`; «МИНА 1» ORCH-040 — не удалять) |
**Секреты — только в `.env` / `.env.staging` на хосте, в гит НЕ коммитятся.** Канон — `.env.example`, `.env.staging.example`.
**Секреты — только в `.env` / `.env.staging` на хосте, в гит НЕ коммитятся.** Канон — `.env.example`, `.env.staging.example`. Выпуск нового комплекта секретов для нового хоста — `scripts/gen_secrets.py` (боевые секреты не копируются). **Тираж платформы на новую инфру** (карта переменных, секреты, smoke-процедура, границы Lite/Bundled) — `docs/operations/REPLICATION.md` (ORCH-101). Когерентность портов при смене прод-порта: `ORCH_DEPLOY_PROD_TARGET_PORT``WATCHDOG_METRICS_URL``ORCH_POST_DEPLOY_BASE_URL`.
## Реестр проектов (`src/projects.py`, ORCH-6)
Связывает Plane project id → gitea repo + work-item prefix. Источник: `ORCH_PROJECTS_JSON`, fallback — встроенный дефолт. Прод видит: `enduro-trails` (ET), `orchestrator` (ORCH). Staging видит ТОЛЬКО `orchestrator-sandbox` (SANDBOX) — изоляция.

View File

@@ -0,0 +1,155 @@
# REPLICATION — тираж платформы на новую инфраструктуру (ORCH-101)
> RUNBOOK фундамента тиража (эпик ORCH-10, слой **10-common**). Как развернуть
> оркестратор на чужом хосте **без правки кода**: переменные → секреты → smoke.
> Тираж **stateless**: данные/БД/секреты боевого хоста НЕ переносятся ни на одном
> шаге — на целевой инфре всё создаётся заново.
---
## 1. Границы: 10-common vs Lite vs Bundled
| Слой | Что это | Статус |
|------|---------|--------|
| **10-common** (этот док) | фундамент: все хост-значения параметризованы (env/конфиг), секреты выпускаются заново, smoke-процедура с PASS/FAIL | ✅ ORCH-101 |
| **Type A — Lite** | инструкция «поставь Plane+Gitea сам, подключи оркестратор» поверх 10-common | отдельная задача эпика |
| **Type B — Bundled** | комплект «всё в одном» (Plane+Gitea+оркестратор) поверх 10-common | отдельная задача эпика |
Этот док НЕ описывает установку Plane/Gitea — только параметризацию, секреты и
smoke самого оркестратора (анти-скоуп-крип Р-5).
### Платформенные конвенции тиража (нормативно, ADR-001 D3/D4)
- **Репо платформы обязан называться `orchestrator`.** Имя — узел безопасности
(`SELF_HOSTING_REPO`, на него завязаны все `*_repos`-leaf'ы «empty CSV →
self-hosting only»); оно сознательно НЕ конфигурируется.
- Имена compose-сервисов/профиля (`orchestrator`, `orchestrator-staging`,
`orchestrator-watchdog`, профиль `staging`) — константы платформы.
- Контейнерные пути (`/app/data`, `/repos`, `/opt/claude-code`) — layout
контейнера, не хост-значения; не параметризуются.
---
## 2. Карта переменных нового хоста
Принцип (ADR-001 D1): **дефолт каждой переменной = боевое значение текущего
хоста** — пустой `.env` ⇒ поведение байт-в-байт; на новом хосте задаёшь только
то, что отличается. Одно env-имя = один факт: pydantic `Settings` читает имя из
`env_file`, compose-интерполяция `${VAR:-default}` — из **`.env` проекта/shell**
(⚠️ НЕ из `env_file` сервиса: `.env.staging` на интерполяцию не влияет —
значения для маунтов/uid/портов живут в `.env`).
### 2.1. Хост-параметризация (новое в ORCH-101)
| Переменная | Дефолт | Назначение |
|-----------|--------|------------|
| `ORCH_AGENT_HOME_DIR` | `/home/slin` | HOME всех акторов (агенты, finalizer, monitor) + таргет маунтов `.claude`/`.claude.json`/`.ssh` + `ARG APP_HOME` (группа ORCH-040 двигается вместе) |
| `ORCH_AGENT_GIT_NAME` | `claude-bot` | git-имя коммитов агентов |
| `ORCH_GIT_EMAIL_DOMAIN` | `mva154.local` | домен git-email всех акторов (`claude-bot@…`, `deploy-finalizer@…`, `post-deploy-monitor@…`) |
| `ORCH_STAGING_PORT` | `8501` | порт staging; читают `image_freshness` И compose `command:`; guard: совпадение с прод-портом → отказ fail-closed (ORCH-058 AC-9) |
| `ORCH_HOST_REPOS_DIR` | `/home/slin/repos` | каталог репозиториев на хосте (источник маунта `/repos`) |
| `ORCH_HOST_CLAUDE_DIR` | `/home/slin/.claude` | источник маунта `~/.claude` |
| `ORCH_HOST_CLAUDE_JSON` | `/home/slin/.claude.json` | источник маунта `~/.claude.json` |
| `ORCH_HOST_SSH_DIR` | `/home/slin/.orchestrator-ssh` | источник маунта ssh-ключей (`→ $HOME/.ssh:ro`) |
| `ORCH_HOST_CLAUDE_CODE_DIR` | `/usr/lib/node_modules/@anthropic-ai/claude-code` | дистрибутив claude-code на хосте |
| `ORCH_HOST_NODE_BIN` | `/usr/bin/node` | бинарь node на хосте |
| `ORCH_RUN_UID` / `ORCH_RUN_GID` | `1000` / `1000` | uid:gid контейнера (`user:` + `ARG APP_UID/APP_GID`); = uid владельца `ORCH_HOST_REPOS_DIR` (ORCH-040) |
| `ORCH_DOCKER_GID` | `999` | gid группы docker хоста (`group_add`, «МИНА 1» — обязателен для docker.sock); узнать: `getent group docker` |
| `ORCH_DEPLOY_PROD_TARGET_PORT` | `8500` | (реюз) прод-порт; интерполируется в `command:` прод-сервиса |
| `ORCH_DEPLOY_SSH_USER` / `ORCH_DEPLOY_HOST_REPO_PATH` | `slin` / `/home/slin/repos/orchestrator` | (реюз) ssh-юзер хука и чекаут платформы на хосте; `REPO=` передаётся хуку явно из конфига |
### 2.2. Обязательные ключи идентичности нового хоста
| Переменная | Где взять |
|-----------|-----------|
| `ORCH_PLANE_API_URL` / `ORCH_PLANE_WEB_URL` / `ORCH_PLANE_WORKSPACE_SLUG` | инсталляция Plane целевого хоста |
| `ORCH_GITEA_URL` / `ORCH_GITEA_PUBLIC_URL` / `ORCH_GITEA_OWNER` | инсталляция Gitea целевого хоста |
| `ORCH_PROJECTS_JSON` | **обязателен на новом хосте**: встроенный fallback (`src/projects.py`) несёт Plane-UUID *исходного* хоста — чужие UUID безвредны (не сматчатся), но без своего реестра конвейер не увидит проекты. Сгенерировать: `scripts/onboard_project.py apply` печатает merged-значение |
| Когерентность портов | сменил прод-порт → синхронно `ORCH_DEPLOY_PROD_TARGET_PORT``WATCHDOG_METRICS_URL``ORCH_POST_DEPLOY_BASE_URL` |
Полный справочник всех остальных флагов — `.env.example` (канон) и
`docs/operations/INFRA.md` (карта env).
---
## 3. Секреты нового хоста (FR-4 / AC-5)
**Нормативно: боевые секреты текущего хоста НЕ копируются ни на одном шаге.**
Для нового хоста всегда выпускается новый комплект.
### 3.1. Генерация локальных webhook-секретов
```bash
python3 scripts/gen_secrets.py # печать .env-фрагмента в stdout
python3 scripts/gen_secrets.py --write # создать .env (существующий → отказ exit=2)
python3 scripts/gen_secrets.py --write --force # перезапись только явно
```
Скрипт stdlib-only (`secrets.token_hex(32)` — 32 байта энтропии); повторный
запуск даёт другие значения; существующий `.env` **никогда не перезаписывается
молча**.
| Секрет | Генерируется | Куда вписать |
|--------|--------------|--------------|
| `ORCH_PLANE_WEBHOOK_SECRET` | локально (gen_secrets) | `.env` + настройка webhook в Plane (см. `SETUP_WEBHOOKS.md`) |
| `ORCH_GITEA_WEBHOOK_SECRET` | локально (gen_secrets) | `.env` + webhook Gitea (создаёт `onboard_project.py apply`) |
### 3.2. Чек-лист внешних токенов
| Секрет | Где выпустить | Куда вписать | Как проверить |
|--------|---------------|--------------|---------------|
| `ORCH_PLANE_API_TOKEN` | Plane UI → Workspace Settings → API tokens | `.env` | `curl -H "X-API-Key: $TOKEN" $ORCH_PLANE_API_URL/api/v1/workspaces/<slug>/projects/` → 200 |
| `ORCH_PLANE_BOT_*` (7, опциональны) | Plane UI: bot-аккаунты per-агент; пусто → fallback на `ORCH_PLANE_API_TOKEN` | `.env` | комментарий в Plane от имени бота |
| `ORCH_GITEA_TOKEN` | Gitea UI → Settings → Applications → Generate Token (scope: repo, admin:repo_hook) | `.env` | `curl -H "Authorization: token $TOKEN" $ORCH_GITEA_URL/api/v1/user` → 200 |
| `ORCH_TELEGRAM_BOT_TOKEN` | BotFather (`/newbot`) | `.env` | `curl https://api.telegram.org/bot$TOKEN/getMe` → ok |
| `ORCH_TELEGRAM_CHAT_ID` (несекретный) | id чата оператора | `.env` | тестовое сообщение |
| `WATCHDOG_TG_BOT_TOKEN` / `WATCHDOG_TG_CHAT_ID` | отдельный бот sidecar-watchdog (ORCH-100, независимый канал) | `.env.watchdog` | алерт от sidecar |
### 3.3. Полнота
`.env.example` — канон 100% ключей старта (секретные значения — только
плейсхолдеры). Состав вывода `gen_secrets.py` сверяется с `.env.example`
тестом (`tests/test_secrets_gen.py`).
---
## 4. Smoke-верификация тиража (FR-5 / AC-3)
Процедура «инстанс → тестовый проект → тестовая задача → конвейер доехал» из
существующих кирпичей; **каждый шаг имеет явный PASS/FAIL**. Итог — однозначный
вердикт: все шаги PASS ⇒ тираж PASS; любой шаг FAIL ⇒ тираж FAIL (собери логи
контейнера `docker logs orchestrator` и снапшот `GET /queue` и разбирайся).
Воспроизводимость без нового железа: процедура прогоняется на текущей инфре —
staging-песочница (порт `ORCH_STAGING_PORT`, дефолт 8501) + sandbox-проект.
Stateless: ни один шаг не предполагает перенос данных/БД/секретов.
| # | Шаг | Команда | PASS | FAIL |
|---|-----|---------|------|------|
| 0 | Конфиг резолвится | `docker compose config` | резолв без ошибок; при пустом env значения = боевым дефолтам | ошибка интерполяции / неожиданные значения |
| 1 | Инстанс жив | `curl -fsS http://127.0.0.1:<port>/health` | HTTP 200, `"status":"ok"` | не-200 / таймаут |
| 2 | Контракты отвечают | `curl -fsS …/queue` и `…/metrics` | JSON со штатными блоками; `/metrics``schema_version: 1` | не-JSON / 5xx |
| 3 | Тестовый проект | `python3 scripts/onboard_project.py plan``apply``verify` (sandbox) | `verify` зелёный (статусы/лейблы/repo/webhook на месте) | `verify` красный / manual-step не выполнен |
| 4 | Тестовая задача | создать issue в Plane → статус «To Analyse» | задача в БД, analyst-job виден в `GET /queue` | задача не появилась (webhook/секрет/реестр) |
| 5 | **Минимальный сигнал «конвейер доехал»** | дождаться окончания `analysis` | артефакты `01``04` в ветке задачи: `git ls-tree origin/<branch> docs/work-items/<id>/` | стадия не завершилась / артефактов нет |
| 6 | Расширенный режим (опционально) | Approved → … → Confirm Deploy | задача дошла до `done` | застряла (разбор по `GET /queue` + логам) |
> Ручной запуск deploy-хука на нестандартных портах — всегда с явными
> `REPO=`/`TARGET_PORT=` (wired-путь оркестратора передаёт их сам из конфига;
> дефолты хука — staging текущего хоста).
---
## 5. Что НЕ переносится (stateless, решение 10.06)
- БД (`data/orchestrator.db`) — создаётся пустой при первом старте;
- `.env` / `.env.staging` / `.env.watchdog` — собираются заново (§2§3);
- worktree'ы/runs/логи — рабочее состояние, не данные;
- боевые секреты — никогда (§3).
---
*RUNBOOK ORCH-101. Поддерживать при добавлении хост-переменных (карта §2 +
`.env.example` + INFRA.md в том же PR). Анти-регресс возврата хардкодов —
`tests/test_no_host_hardcodes.py`; параметризация инфра-файлов —
`tests/test_infra_parametrization.py`.*