285 lines
10 KiB
Markdown
285 lines
10 KiB
Markdown
# DEV TASK: Настройка Webhooks (Plane + Gitea → Orchestrator)
|
||
|
||
**Статус:** Ready for dev
|
||
**Проект:** multi-agent
|
||
**Фаза:** 3
|
||
**Предыдущая задача:** DEV_TASK_ORCHESTRATOR_QG.md (выполнена)
|
||
|
||
---
|
||
|
||
## Цель
|
||
|
||
> Plane и Gitea автоматически отправляют webhook events в Orchestrator при действиях пользователя (создание задачи, комментарий, push, PR, CI status).
|
||
|
||
## Архитектура
|
||
|
||
Nginx proxy уже настроен: `https://openclaw.mva154.duckdns.org/orchestrator/` → `localhost:8500`.
|
||
|
||
Нужно:
|
||
1. Создать webhook в Gitea через API (events: push, pull_request, status)
|
||
2. Создать webhook в Plane через API или UI (events: work_item.created, comment.created)
|
||
3. Добавить HMAC-верификацию подписи в обоих handlers
|
||
4. Добавить webhook secret в .env Orchestrator'а
|
||
5. Проверить end-to-end: действие в UI → webhook → Orchestrator обрабатывает
|
||
|
||
## Стек / Зависимости
|
||
|
||
- httpx (уже есть)
|
||
- hmac + hashlib (stdlib)
|
||
- Gitea API v1
|
||
- Plane API v1
|
||
|
||
---
|
||
|
||
## Инфраструктура
|
||
|
||
| Параметр | Значение |
|
||
|----------|----------|
|
||
| Сервер | `slin@82.22.50.71` (mva154) |
|
||
| Orchestrator URL (external) | `https://openclaw.mva154.duckdns.org/orchestrator/` |
|
||
| Orchestrator URL (internal) | `http://127.0.0.1:8500/` |
|
||
| Gitea API | `http://localhost:3000/api/v1` |
|
||
| Gitea token | в .env `ORCH_GITEA_TOKEN` |
|
||
| Plane API | `http://localhost:8091/api/v1` |
|
||
| Plane token | в .env `ORCH_PLANE_API_TOKEN` |
|
||
| Plane workspace | `ag_proj` |
|
||
| Gitea repo owner | `admin` |
|
||
| Gitea repo | `enduro-trails` |
|
||
|
||
---
|
||
|
||
## Задачи
|
||
|
||
### Task 1: Создать webhook в Gitea через API
|
||
|
||
**Шаги:**
|
||
|
||
- [ ] **1.1** Создать webhook для репо `admin/enduro-trails`:
|
||
```bash
|
||
# Сгенерировать secret
|
||
GITEA_WEBHOOK_SECRET=$(openssl rand -hex 20)
|
||
echo "ORCH_GITEA_WEBHOOK_SECRET=${GITEA_WEBHOOK_SECRET}" >> /home/slin/repos/orchestrator/.env
|
||
|
||
# Создать webhook через API
|
||
curl -s -X POST "http://localhost:3000/api/v1/repos/admin/enduro-trails/hooks" \
|
||
-H "Authorization: token $(grep ORCH_GITEA_TOKEN /home/slin/repos/orchestrator/.env | cut -d= -f2)" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"type": "gitea",
|
||
"active": true,
|
||
"config": {
|
||
"url": "https://openclaw.mva154.duckdns.org/orchestrator/webhook/gitea",
|
||
"content_type": "json",
|
||
"secret": "'${GITEA_WEBHOOK_SECRET}'"
|
||
},
|
||
"events": ["push", "pull_request", "status"],
|
||
"branch_filter": "*"
|
||
}'
|
||
```
|
||
|
||
- [ ] **1.2** Проверить что webhook создан:
|
||
```bash
|
||
curl -s "http://localhost:3000/api/v1/repos/admin/enduro-trails/hooks" \
|
||
-H "Authorization: token $(grep ORCH_GITEA_TOKEN /home/slin/repos/orchestrator/.env | cut -d= -f2)" | python3 -m json.tool
|
||
```
|
||
|
||
**Критерий готовности:** Webhook виден в Gitea UI (Settings → Webhooks), URL правильный.
|
||
|
||
---
|
||
|
||
### Task 2: Создать webhook в Plane
|
||
|
||
**Шаги:**
|
||
|
||
- [ ] **2.1** Проверить Plane API для webhooks:
|
||
```bash
|
||
# Plane webhooks API
|
||
curl -s "http://localhost:8091/api/v1/workspaces/ag_proj/webhooks/" \
|
||
-H "X-API-Key: $(grep ORCH_PLANE_API_TOKEN /home/slin/repos/orchestrator/.env | cut -d= -f2)" | python3 -m json.tool
|
||
```
|
||
|
||
- [ ] **2.2** Создать webhook:
|
||
```bash
|
||
PLANE_WEBHOOK_SECRET=$(openssl rand -hex 20)
|
||
echo "ORCH_PLANE_WEBHOOK_SECRET=${PLANE_WEBHOOK_SECRET}" >> /home/slin/repos/orchestrator/.env
|
||
|
||
curl -s -X POST "http://localhost:8091/api/v1/workspaces/ag_proj/webhooks/" \
|
||
-H "X-API-Key: $(grep ORCH_PLANE_API_TOKEN /home/slin/repos/orchestrator/.env | cut -d= -f2)" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"url": "https://openclaw.mva154.duckdns.org/orchestrator/webhook/plane",
|
||
"is_active": true,
|
||
"secret_key": "'${PLANE_WEBHOOK_SECRET}'",
|
||
"project": null
|
||
}'
|
||
```
|
||
|
||
Примечание: Plane API для webhooks может отличаться от документации. Если API не работает — настроить через UI (`plane.mva154.duckdns.org` → Settings → Webhooks).
|
||
|
||
- [ ] **2.3** Если API не поддерживает webhooks — задокументировать и оставить TODO для ручной настройки через UI.
|
||
|
||
**Критерий готовности:** Webhook создан (через API или UI), URL указывает на Orchestrator.
|
||
|
||
---
|
||
|
||
### Task 3: HMAC-верификация подписи в webhook handlers
|
||
|
||
**Файлы:**
|
||
- Изменить: `src/webhooks/plane.py`
|
||
- Изменить: `src/webhooks/gitea.py`
|
||
- Изменить: `src/config.py`
|
||
|
||
**Шаги:**
|
||
|
||
- [ ] **3.1** Добавить верификацию в `src/webhooks/gitea.py`:
|
||
```python
|
||
import hmac
|
||
import hashlib
|
||
from ..config import settings
|
||
|
||
def verify_gitea_signature(body: bytes, signature: str) -> bool:
|
||
"""Verify Gitea webhook HMAC-SHA256 signature."""
|
||
if not settings.gitea_webhook_secret:
|
||
return True # Skip verification if no secret configured
|
||
expected = hmac.new(
|
||
settings.gitea_webhook_secret.encode(),
|
||
body,
|
||
hashlib.sha256
|
||
).hexdigest()
|
||
return hmac.compare_digest(f"sha256={expected}", signature)
|
||
|
||
@router.post("/gitea")
|
||
async def gitea_webhook(request: Request):
|
||
body = await request.body()
|
||
signature = request.headers.get("X-Gitea-Signature", "")
|
||
|
||
if not verify_gitea_signature(body, signature):
|
||
raise HTTPException(status_code=401, detail="Invalid signature")
|
||
|
||
# ... rest of handler
|
||
```
|
||
|
||
- [ ] **3.2** Добавить верификацию в `src/webhooks/plane.py`:
|
||
```python
|
||
# Plane использует свой формат подписи — проверить документацию
|
||
# Обычно: X-Plane-Signature header с HMAC-SHA256
|
||
def verify_plane_signature(body: bytes, signature: str) -> bool:
|
||
if not settings.plane_webhook_secret:
|
||
return True
|
||
expected = hmac.new(
|
||
settings.plane_webhook_secret.encode(),
|
||
body,
|
||
hashlib.sha256
|
||
).hexdigest()
|
||
return hmac.compare_digest(expected, signature)
|
||
```
|
||
|
||
- [ ] **3.3** Добавить `from fastapi import HTTPException` в оба файла
|
||
|
||
**Критерий готовности:** Запрос без правильной подписи → 401. С правильной → 200.
|
||
|
||
---
|
||
|
||
### Task 4: Пересобрать и протестировать
|
||
|
||
**Шаги:**
|
||
|
||
- [ ] **4.1** Пересобрать Orchestrator:
|
||
```bash
|
||
cd /home/slin/repos/orchestrator && docker compose up -d --build
|
||
```
|
||
|
||
- [ ] **4.2** Проверить health:
|
||
```bash
|
||
curl -s http://localhost:8500/health
|
||
```
|
||
|
||
- [ ] **4.3** Тест Gitea webhook — сделать push в enduro-trails:
|
||
```bash
|
||
cd /home/slin/repos/enduro-trails
|
||
echo "# webhook test $(date)" >> .webhook-test
|
||
git add .webhook-test && git commit -m "test: webhook delivery check" && git push origin feature/ET-002-poi-toggle
|
||
```
|
||
|
||
- [ ] **4.4** Проверить что Orchestrator получил event:
|
||
```bash
|
||
docker logs orchestrator --tail 10 2>&1 | grep -i "gitea\|push"
|
||
```
|
||
|
||
- [ ] **4.5** Проверить в БД:
|
||
```bash
|
||
docker exec orchestrator python -c "
|
||
import sqlite3
|
||
conn = sqlite3.connect('/app/data/orchestrator.db')
|
||
rows = conn.execute('SELECT id, source, event_type, timestamp FROM events ORDER BY id DESC LIMIT 3').fetchall()
|
||
for r in rows: print(r)
|
||
"
|
||
```
|
||
|
||
- [ ] **4.6** Удалить тестовый файл:
|
||
```bash
|
||
cd /home/slin/repos/enduro-trails
|
||
git rm .webhook-test && git commit -m "test: cleanup webhook test" && git push origin feature/ET-002-poi-toggle
|
||
```
|
||
|
||
**Критерий готовности:** Push в Gitea → event появляется в Orchestrator БД. Логи показывают обработку.
|
||
|
||
---
|
||
|
||
### Task 5: Настроить Plane webhook через UI (если API не сработал)
|
||
|
||
Если Task 2 не удался через API:
|
||
|
||
- [ ] **5.1** Документировать инструкцию для ручной настройки:
|
||
```
|
||
1. Открыть https://plane.mva154.duckdns.org
|
||
2. Settings → Webhooks → Add Webhook
|
||
3. URL: https://openclaw.mva154.duckdns.org/orchestrator/webhook/plane
|
||
4. Secret: <значение из ORCH_PLANE_WEBHOOK_SECRET в .env>
|
||
5. Events: All (или work_item.created, comment.created)
|
||
6. Save
|
||
```
|
||
|
||
- [ ] **5.2** Сохранить инструкцию в `/home/slin/repos/orchestrator/docs/SETUP_WEBHOOKS.md`
|
||
|
||
**Критерий готовности:** Документация есть. Webhook либо настроен, либо есть чёткая инструкция.
|
||
|
||
---
|
||
|
||
## Проверка (Acceptance)
|
||
|
||
| # | Проверка | Команда / Действие | Ожидаемый результат |
|
||
|---|----------|-------------------|---------------------|
|
||
| 1 | Gitea webhook создан | GET /api/v1/repos/admin/enduro-trails/hooks | Webhook с URL orchestrator |
|
||
| 2 | Push → event в БД | git push + check DB | Новый event source=gitea |
|
||
| 3 | Signature verification | curl без подписи → 401 | 401 Unauthorized |
|
||
| 4 | Signature verification | curl с правильной подписью → 200 | 200 accepted |
|
||
| 5 | Plane webhook | Создать issue в Plane → check DB | Event source=plane |
|
||
|
||
---
|
||
|
||
## Ограничения и контекст
|
||
|
||
- ⚠️ Plane API для webhooks может не поддерживать программное создание — тогда через UI
|
||
- ⚠️ Gitea webhook secret формат: `X-Gitea-Signature: sha256=<hex>`
|
||
- ⚠️ НЕ удалять существующие данные в БД
|
||
- ⚠️ Если signature verification ломает существующие smoke tests — сделать skip если secret пустой
|
||
- 🚫 НЕ менять Nginx конфиг (уже настроен)
|
||
- 🚫 НЕ менять порт Orchestrator'а
|
||
|
||
---
|
||
|
||
## Деплой-чеклист
|
||
|
||
- [ ] Webhook secret сгенерирован и добавлен в .env
|
||
- [ ] Gitea webhook создан через API
|
||
- [ ] Plane webhook создан (API или UI)
|
||
- [ ] HMAC verification добавлена в handlers
|
||
- [ ] `docker compose up -d --build` успешен
|
||
- [ ] Push в Gitea → event в Orchestrator
|
||
- [ ] Логи чистые, нет ошибок
|
||
|
||
---
|
||
|
||
*Создано: 2026-05-21 | Автор ТЗ: Стрим | Исполнитель: Dev-агент*
|