# 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'; ```