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.
83 lines
5.4 KiB
Markdown
83 lines
5.4 KiB
Markdown
# 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';
|
||
```
|