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.
This commit is contained in:
41
README.md
41
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/<repo>` (авто-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":"<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
|
||||
|
||||
@@ -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
|
||||
|
||||
82
docs/BUGFIXES_2026-06-03.md
Normal file
82
docs/BUGFIXES_2026-06-03.md
Normal file
@@ -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';
|
||||
```
|
||||
7
docs/INCIDENT_2026-06-02_webhook_autorun.txt
Normal file
7
docs/INCIDENT_2026-06-02_webhook_autorun.txt
Normal file
@@ -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...
|
||||
Reference in New Issue
Block a user