From ca81f383302d300503355b7900f828e7a16d32e6 Mon Sep 17 00:00:00 2001 From: Dev Agent Date: Tue, 2 Jun 2026 22:30:51 +0300 Subject: [PATCH] docs: document multi-repo registry + ORCH-6 bugfix and incident ORCH-6: ARCHITECTURE.md gets a project-registry section; README explains how to add a project via ORCH_PROJECTS_JSON; BUGFIXES_2026-06-03.md records the fix and links the 2026-06-02 webhook autorun incident. --- README.md | 41 +++++++++- docs/ARCHITECTURE.md | 32 +++++++- docs/BUGFIXES_2026-06-03.md | 82 ++++++++++++++++++++ docs/INCIDENT_2026-06-02_webhook_autorun.txt | 7 ++ 4 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 docs/BUGFIXES_2026-06-03.md create mode 100644 docs/INCIDENT_2026-06-02_webhook_autorun.txt diff --git a/README.md b/README.md index fd3a97e..c516245 100644 --- a/README.md +++ b/README.md @@ -101,12 +101,51 @@ uvicorn src.main:app --reload --port 8500 | `ORCH_GITEA_TOKEN` | Gitea API token | — | | `ORCH_GITEA_WEBHOOK_SECRET` | Gitea webhook secret | — | | `ORCH_GITEA_OWNER` | Gitea repo owner | `admin` | -| `ORCH_DEFAULT_REPO` | Default repository | `enduro-trails` | +| `ORCH_DEFAULT_REPO` | Default repository (fallback) | `enduro-trails` | +| `ORCH_PROJECTS_JSON` | Multi-repo реестр (JSON-массив, ORCH-6) | `""` → дефолт в `src/projects.py` | | `ORCH_CLAUDE_BIN` | Путь к Claude CLI | `/opt/claude-code/bin/claude.exe` | | `ORCH_REPOS_DIR` | Repos dir (container) | `/repos` | | `ORCH_HOST_REPOS_DIR` | Repos dir (host) | `/home/slin/repos` | | `ORCH_DB_PATH` | SQLite path | `/app/data/orchestrator.db` | +## Multi-repo: реестр проектов (ORCH-6) + +Оркестратор обслуживает несколько репозиториев через реестр проектов +(`src/projects.py`), ключ = **Plane project id**. Plane-webhook фильтрует события +по проекту (неизвестный проект → `ignored`) и резолвит `repo` / `work_item_prefix` / +Plane-проект из маппинга. + +По умолчанию (если `ORCH_PROJECTS_JSON` пуст) зарегистрированы два проекта: + +| Проект | Plane project id | repo | prefix | +|--------|------------------|------|--------| +| enduro-trails | `7a79f0a9-5278-49cd-9007-9a338f238f9c` | `enduro-trails` | `ET` | +| orchestrator | `8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a` | `orchestrator` | `ORCH` | + +### Как добавить новый проект + +1. Убедись, что gitea-репо уже клонировано в `/repos/` (авто-clone — отдельно). +2. Узнай Plane project uuid (из URL проекта в Plane или через Plane API). +3. Добавь запись в `ORCH_PROJECTS_JSON` в `.env` (JSON-массив). **Важно:** если + задаёшь `ORCH_PROJECTS_JSON`, он полностью заменяет дефолт — перечисли **все** + нужные проекты (включая enduro-trails и orchestrator): + + ```bash + ORCH_PROJECTS_JSON='[ + {"plane_project_id":"7a79f0a9-5278-49cd-9007-9a338f238f9c","repo":"enduro-trails","work_item_prefix":"ET","name":"enduro-trails"}, + {"plane_project_id":"8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a","repo":"orchestrator","work_item_prefix":"ORCH","name":"orchestrator"}, + {"plane_project_id":"<новый-uuid>","repo":"<новый-repo>","work_item_prefix":"","name":"<имя>"} + ]' + ``` + +4. Пересобери: `docker compose up -d --build`. +5. Проверь резолв: + ```bash + docker exec orchestrator python3 -c "from src.projects import get_project_by_plane_id as g; print(g('<новый-uuid>'))" + ``` + +Поля `name` опционально (по умолчанию = `repo`). Подробности — `docs/ARCHITECTURE.md`. + ## Ключевые механизмы ### Auto-advance diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7aec12f..73593b9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -9,9 +9,39 @@ Orchestrator — event-driven FastAPI сервис, который управл ### 1. Webhook Receivers #### Plane Webhook (`src/webhooks/plane.py`) -- Принимает `work_item.created` — создаёт задачу в DB, запускает analyst +- **Фильтр по проекту (ORCH-6):** извлекает `data.project` (Plane project uuid) и игнорирует событие, если проект не в реестре (`known_plane_project_ids()`) → ответ `{"status":"ignored","reason":"unknown project"}`. Это предотвращает инцидент 2026-06-02 (webhook на весь workspace без фильтра). +- Принимает `work_item.created` — резолвит repo/prefix/Plane-проект из реестра по `project`, создаёт задачу в DB, запускает analyst - Принимает `work_item.updated` — синхронизация статусов +#### Реестр проектов (`src/projects.py`, multi-repo, ORCH-6) +Маппинг **Plane project id → (repo, work_item_prefix, name)**. Позволяет одному +оркестратору обслуживать несколько репозиториев, не путая их. + +```python +@dataclass(frozen=True) +class ProjectConfig: + plane_project_id: str # uuid Plane-проекта (ключ реестра) + repo: str # имя gitea-репо (= папка в /repos) + work_item_prefix: str # ET / ORCH + name: str # человекочитаемое +``` + +Резолверы: +- `get_project_by_plane_id(uuid) -> ProjectConfig | None` — для фильтра/резолва в plane-webhook. +- `get_project_by_repo(repo) -> ProjectConfig | None` — когда известен только repo (gitea-webhook, plane_sync). +- `known_plane_project_ids() -> set[str]` — множество разрешённых проектов (фильтр). + +**Источник конфигурации:** env `ORCH_PROJECTS_JSON` (JSON-массив `ProjectConfig`). +Если пусто/битый JSON — используется встроенный дефолт-реестр (enduro-trails + orchestrator), +чтобы система работала из коробки. Парсинг устойчив: битые записи пропускаются, +полностью невалидный JSON → fallback на дефолт. + +Следствия multi-repo: +- **repo per project:** `repo = get_project_by_plane_id(project_id).repo` вместо хардкода `default_repo`. +- **prefix per project:** `get_next_work_item_id(repo, prefix)` нумерует независимо — `ORCH-001` vs `ET-010` (`src/db.py`). +- **plane_sync в правильный проект:** state/comment пишутся в Plane-проект самой задачи (резолв по repo через `get_project_by_repo`), а не в единственный хардкоженный `PROJECT_ID` (обратная совместимость сохранена дефолтом на enduro). +- **gitea-webhook:** push в repo вне реестра → `ignored` (не триггерит конвейер). + #### Gitea Webhook (`src/webhooks/gitea.py`) - **push** — проверяет наличие артефактов (docs/, src/), продвигает стадию - **pull_request\*** (wildcard) — обрабатывает review approved/rejected, PR merge diff --git a/docs/BUGFIXES_2026-06-03.md b/docs/BUGFIXES_2026-06-03.md new file mode 100644 index 0000000..87d227d --- /dev/null +++ b/docs/BUGFIXES_2026-06-03.md @@ -0,0 +1,82 @@ +# BUGFIXES / CHANGES — 2026-06-03 + +## ORCH-6 — Multi-repo: фильтр проекта + маппинг repo per project + +**Тип:** root-fix инцидента + новая возможность (multi-repo) +**Ветка:** `feature/ORCH-6-multirepo` +**Plane:** ORCH-6 (project `8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a`) +**Связанный инцидент:** [`INCIDENT_2026-06-02_webhook_autorun.txt`](./INCIDENT_2026-06-02_webhook_autorun.txt) + +### Контекст инцидента + +При создании задач ORCH-1..7 в Plane (проект `orchestrator`) Plane-webhook +(id `93f0c342-a614-4248-9d0f-c107276f5620`) сработал на каждую задачу и запустил +конвейер — но **всё ушло в репо `enduro-trails`**, потому что `plane.py:91` +хардкодил `repo = settings.default_repo`. Webhook слушал **весь workspace без +фильтра по проекту**, наплодив мусорные ET-010..016. + +Митигация на время фикса: Plane-webhook **деактивирован** (`is_active=false`). + +### Root cause + +1. Нет фильтра по Plane-проекту — любая issue из любого проекта попадала в конвейер. +2. `repo` хардкожен на единственный `default_repo` (enduro-trails). +3. `work_item_prefix` всегда `ET` (db.py). +4. `plane_sync` ходил в единственный хардкоженный `PROJECT_ID` (enduro). + +### Что сделано + +| Файл | Изменение | +|------|-----------| +| `src/projects.py` (новый) | Реестр проектов: `ProjectConfig` + дефолт-список (enduro-trails + orchestrator) + резолверы `get_project_by_plane_id` / `get_project_by_repo` / `known_plane_project_ids`. Источник переопределения — `ORCH_PROJECTS_JSON`; устойчивый парсинг (битый JSON / битые записи → fallback на дефолт). | +| `src/config.py` | Добавлен `projects_json: str = ""` (env `ORCH_PROJECTS_JSON`). | +| `src/webhooks/plane.py` | **Фильтр по проекту**: `data.project` не в реестре → `{"status":"ignored","reason":"unknown project"}`. Резолв `repo`/`prefix`/Plane-проекта из реестра. Plane-sync для задачи идёт в её собственный проект. | +| `src/db.py` | `get_next_work_item_id(repo, prefix="ET")` — нумерация per (repo, prefix); `ORCH-001` независимо от `ET-010`. Дефолт `ET` сохранён для обратной совместимости. | +| `src/plane_sync.py` | `_resolve_project_id` + параметризация `project_id` (дефолт на enduro → обратная совместимость существующих вызовов). | +| `src/webhooks/gitea.py` | Неизвестный repo (`get_project_by_repo` → None) → `ignored` в 3 хэндлерах. | + +### Тесты + +- `tests/test_projects.py` (16 тестов): резолверы (by plane_id, by repo, unknown→None, + known_plane_project_ids), парсинг `ORCH_PROJECTS_JSON` (валидный / битый JSON / не массив / + битые записи → skip / all-bad → fallback), reload с кастомным JSON. +- `tests/test_plane_webhook.py` (4 теста, FastAPI TestClient, `launcher.launch` замокан): + unknown project → `ignored` + нет task/branch/agent; orchestrator-проект → `repo=orchestrator`, + `ORCH-*`; enduro-проект → `repo=enduro-trails`, `ET-*`; независимые префиксы (`ORCH-001`/`ORCH-002` + параллельно с `ET-001`). + +**Прогон (в контейнере, образ `orchestrator-orchestrator`):** `57 passed`. 9 падений в +`tests/test_webhooks.py` — **pre-existing** (webhook signature 401 / TypeError, не связаны с ORCH-6, +не трогались). + +```bash +IMG=$(docker inspect orchestrator --format '{{.Config.Image}}') +docker run --rm -v /home/slin/repos/orchestrator:/code -w /code --entrypoint python3 $IMG -m pytest tests/ -q +``` + +### Проверка резолва (offline, в работающем контейнере) + +```bash +docker exec orchestrator python3 -c " +from src.projects import get_project_by_plane_id, known_plane_project_ids +o = get_project_by_plane_id('8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a') +e = get_project_by_plane_id('7a79f0a9-5278-49cd-9007-9a338f238f9c') +assert o.repo=='orchestrator' and o.work_item_prefix=='ORCH' +assert e.repo=='enduro-trails' and e.work_item_prefix=='ET' +assert get_project_by_plane_id('00000000-0000-0000-0000-000000000000') is None +print('RESOLVE OK:', o.repo, e.repo, '| known:', len(known_plane_project_ids())) +" +``` + +### ⚠️ Важно + +- Plane-webhook **остаётся выключенным** (`is_active=false`). Включение — отдельный + шаг Стрим после ревью PR. +- `ORCH_PROJECTS_JSON` (если задан) **полностью заменяет** дефолт — перечислять все нужные проекты. +- Обратная совместимость `plane_sync` сохранена (дефолт project_id = enduro), ET-задачи не сломаны. + +### Re-enable webhook (после ревью, делает Стрим) + +```sql +UPDATE webhooks SET is_active=true WHERE id='93f0c342-a614-4248-9d0f-c107276f5620'; +``` diff --git a/docs/INCIDENT_2026-06-02_webhook_autorun.txt b/docs/INCIDENT_2026-06-02_webhook_autorun.txt new file mode 100644 index 0000000..659d6ab --- /dev/null +++ b/docs/INCIDENT_2026-06-02_webhook_autorun.txt @@ -0,0 +1,7 @@ +INCIDENT 2026-06-02: Plane webhook auto-triggered pipeline for ALL ORCH-1..7 tasks +- Plane webhook (id 93f0c342) fires on ANY issue creation in workspace, no project filter +- plane.py:91 hardcodes repo=settings.default_repo (enduro-trails) +- Result: ORCH-x tasks ran analyst/architect in WRONG repo (enduro-trails), created junk ET-010..016 +- MITIGATION: Plane webhook DEACTIVATED (is_active=false) until ORCH-6 adds project filter +- ROOT FIX = ORCH-6 (multi-repo): filter by plane_project_id + repo mapping per project +- To re-enable webhook after ORCH-6: UPDATE webhooks SET is_active=true WHERE id=93f0c342...