From a8221f01c89c11767a9062fbb461a1fe287bf1c8 Mon Sep 17 00:00:00 2001 From: Slava Date: Sat, 6 Jun 2026 10:30:50 +0300 Subject: [PATCH] =?UTF-8?q?docs(history):=20LESSONS=5FORCH-048=20=E2=80=94?= =?UTF-8?q?=20staging=20B6=20isolation,=20variant=20(v),=20chicken-egg=20l?= =?UTF-8?q?esson?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/history/LESSONS_ORCH-048.md | 119 +++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 docs/history/LESSONS_ORCH-048.md diff --git a/docs/history/LESSONS_ORCH-048.md b/docs/history/LESSONS_ORCH-048.md new file mode 100644 index 0000000..b5abdda --- /dev/null +++ b/docs/history/LESSONS_ORCH-048.md @@ -0,0 +1,119 @@ +# LESSONS — ORCH-048 (B6 staging registry isolation, вариант «в») + +**Дата:** 2026-06-06 +**Work item:** ORCH-048 — «staging B6 check reads registry from host worktree, not staging container» +**Статус:** ✅ Done. Merge PR #45 (`2a36ed80`), Plane → Done, task 38 → done. Прод не тронут. + +--- + +## TL;DR + +B6-чек staging-suite давал **ложный FAIL** (`prod-ET=YES, prod-ORCH=YES`), блокируя `deploy-staging` у **всех** ORCH-задач, хотя изоляция реестра в staging работала корректно. Починили, выбрав архитектурный вариант, который **не порождает новых ловушек автономности**. По дороге словили три урока, которые стоят дороже самой фичи. + +--- + +## 1. Root cause (для истории) + +`scripts/staging_check.py` блок **B6** был единственным чеком suite, который не ходил по HTTP к живому инстансу, а **импортировал Python-код локально**: + +```python +sys.path.insert(0, "/repos/orchestrator") # host-worktree +importlib.reload(sys.modules["src.projects"]) # подхватывает env ТЕКУЩЕГО процесса +known = known_plane_project_ids() +``` + +Деплоер запускал suite **с хоста**, где `ORCH_PROJECTS_JSON` не задан → `src.projects` грузил встроенный `_DEFAULT_PROJECTS` (ET+ORCH) → `known_plane_project_ids()` возвращал боевые id → **ложный FAIL**. То есть B6 проверял реестр НЕ того окружения, реестр которого реально использует staging-инстанс. + +Изоляция при этом была исправна: внутри `orchestrator-staging` `known_plane_project_ids()` корректно отдавал только sandbox (`.env.staging`). + +--- + +## 2. ГЛАВНЫЙ УРОК: «курица-яйцо» в staging-гейте + +Архитектор на первом прогоне выбрал **вариант (а): новый HTTP-эндпоинт `GET /projects`**, и B6 стал ходить на него. Решение красивое (единый HTTP-стиль с остальными чеками), **но оно само себя заблокировало**: + +- B6 проверяет **работающий** staging-инстанс (порт 8501). +- Эндпоинт `/projects` **запечён в Docker-образ** (`src/main.py`). +- В текущем (ещё не пересобранном) образе эндпоинта НЕТ → `GET /projects` → **404** → B6 FAIL → откат на development. +- Чтобы чек прошёл, нужен **ручной bootstrap-деплой** образа. А деплой не происходит, потому что чек красный. **Тупик by design.** + +Подтверждено на проде: `GET /projects` на 8501 и 8500 → 404 → `deploy-staging FAILED`. + +**Вывод-правило:** +> Staging-чек НЕ должен проверять то, что появляется в работающем инстансе только ПОСЛЕ деплоя проверяемой ветки. Иначе первый прогон всегда падает и требует ручного bootstrap — это прямая поломка автономности. + +**Решение — вариант (в):** запускать suite **ВНУТРИ** staging-контейнера (`docker exec orchestrator-staging`), читать реестр из собственного process-env контейнера, убрать host-path хак. Преимущество принципиальное: +- B6 не зависит от того, что отдаёт инстанс по HTTP. +- `staging_check.py` берётся из bind-mount → свежий код подхватывается **без ребилда образа**. +- **Курицы-яйца нет ни на первом прогоне, ни в будущем.** + +Вариант (б) (`docker exec ... python3 -c "..."` + парсинг stdout) отклонён: хрупкое экранирование (см. `LESSONS_2026-06-05.md`). + +**Как это попало в реализацию:** после FAIL под (а) — откатили ветку к analyst-артефактам (`git reset --hard `), стёрли ADR(а)+код(а), зашили в `02-trz.md §4` блок «РЕШЕНИЕ ПРИНЯТО ВЛАДЕЛЬЦЕМ: вариант (в)» с обоснованием и чек-листом, откатили задачу на `architecture` + поставили job архитектору заново. Второй прогон: arch→dev→review→tester→deploy-staging — без петель, **B6 ✓ PASS, 10/10**. + +--- + +## 3. УРОК: орк мержит в main ТОЛЬКО логи, а не фикс-код + +После прохождения staging орк сам: +- закрыл задачу в `done`, +- смержил в `main` PR с **логами** (`15-staging-log.md`, `14-deploy-log.md`), +- но **сам фикс-код остался в feature-ветке** — `main` всё ещё содержал старый сломанный B6. + +Это by design: фичу в main вливает **владелец**. Поймали проверкой: + +```bash +git fetch origin -q +git log --oneline origin/main..origin/feature/ # покажет невлитые коммиты фикса +git show origin/main:scripts/staging_check.py | grep -c '_evaluate_b6' # 0 = фикс НЕ в main +``` + +**Правило:** +> Прежде чем считать задачу реально доставленной — проверить `git log origin/main..feature` и наличие ключевой функции/строки фикса в `origin/main`. `done` в Plane + смерженные логи ≠ код в main. + +Финальный шаг: смерджить feature-PR в main (Gitea API, `Do: merge`), затем синхронизировать host-репо. + +--- + +## 4. УРОК: rollout bind-mount-фикса = host `git pull`, без ребилда/рестарта прода + +ORCH-048 менял только **bind-mounted / non-runtime** артефакты: + +| Файл | Как доходит до прода | +|------|----------------------| +| `scripts/staging_check.py` | bind-mount (`/home/slin/repos` → `/repos`); **не** в образе (`scripts/` нет в `/app`) → host `git pull` → live сразу | +| `.openclaw/agents/deployer.md` | bind-mounted промпт, читается при запуске агента → live на следующем запуске | +| `tests/`, `docs/` | не деплоятся | + +`src/` и `Dockerfile` НЕ менялись → **рестарт/ребилд прод-контейнера 8500 не нужен и не делался** (zero group-risk для ET). + +**Грабли host-репо:** `git pull` в `/home/slin/repos/orchestrator` сначала упёрся в `sudo: a password is required` — ложная тревога. Репо принадлежит `slin`, sudo не нужен; прямой `git pull --ff-only origin main` прошёл. **Сначала проверь `ls -ld` / `stat -c %U` репо — не лезь в sudo вслепую.** + +**Верификация rollout в живом bind-mount (обязательна):** +```bash +grep -c '_evaluate_b6' scripts/staging_check.py # >=1 +grep -c 'sys.path.insert(0, "/repos/orchestrator")' scripts/staging_check.py # 0 +grep -c 'docker exec orchestrator-staging' .openclaw/agents/deployer.md # >=1 +curl -s -o /dev/null -w '%{http_code}' http://localhost:8500/health # 200 +``` + +--- + +## 5. Технические заметки (gotchas) + +- **В контейнере orchestrator НЕТ `curl`** — для Gitea/Plane API использовать `urllib` через python (script-file → base64 → `docker cp` → `docker exec`). +- **Plane state-id зависят от проекта.** Approved для проекта orchestrator = `63f2c8fe-dcda-4ace-952f-dd88bd0118ff` (НЕ дефолтный `a519a341...` из кода — тот для sandbox/ET). Брать реальные state-id через `GET .../states/`. +- **BRD-апрув = перевод Plane-issue в статус Approved** → webhook ловит смену статуса → путь `agent=None` → `approved-via-status` → гейт пропускает, БЕЗ повторного запуска `check_analysis_approved`. +- **Dockerfile НЕ копирует `scripts/`** в образ — `staging_check.py` доступен в контейнере только через mount. Путь запуска внутри контейнера учитывать (не `/app/scripts`). +- **Перезапуск стадии вручную:** `update_task_stage(task_id, "")` + `enqueue_job(agent, repo, task_content, task_id)`. Guard перед этим: `agent_running IS NULL` И нет jobs со `status IN ('queued','running')` для task_id. + +--- + +## 6. Итог по гейтам/ядру после серии ORCH-45/46/47/48 + +- ✅ `check_ci_green` — поллинг (ORCH-45) +- ✅ `check_tests_passed` — читает `result:` (ORCH-47) +- ✅ `stage_engine` — передаёт деву **текст** findings, не только ссылку (ORCH-46) +- ✅ B6 staging — читает реестр ВНУТРИ staging-контейнера, больше не ложный FAIL (ORCH-48) → **deploy-staging разблокирован для всех ORCH-задач** + +Конвейер стал по-настоящему автономным: задача проходит analyst→deploy без ручного пинания стадий. -- 2.49.1