feat: full pipeline fixes - CI status branch lookup, review webhook routing, auto-advance, plane sync

- handle_ci_status: fallback git branch -r --contains when branches[] empty
- webhook router: handle pull_request_approved event type
- handle_pr: map review.type to review.state for new Gitea format
- launcher: auto-advance stage after agent completion (_try_advance_stage)
- plane_sync: notify Plane on stage changes
- stages.py: stage machine with QG definitions
- notifications.py: stage change notifications
- safe.directory fix for container git operations
This commit is contained in:
Dev Agent
2026-05-22 01:57:02 +03:00
parent b428163c32
commit b545665e2d
16 changed files with 1729 additions and 102 deletions

163
docs/SETUP_WEBHOOKS.md Normal file
View File

@@ -0,0 +1,163 @@
# Webhook Setup: Plane + Gitea → Orchestrator
## Архитектура
```
Gitea (push/PR/CI) ──→ Nginx proxy ──→ Orchestrator /webhook/gitea
Plane (work_item/comment) ──→ Nginx proxy ──→ Orchestrator /webhook/plane
```
External URL: `https://openclaw.mva154.duckdns.org/orchestrator/`
Internal URL: `http://127.0.0.1:8500/`
---
## Gitea Webhook
**Создан автоматически через API.**
- URL: `https://openclaw.mva154.duckdns.org/orchestrator/webhook/gitea`
- Events: `push`, `pull_request`, `status`
- Secret: значение `ORCH_GITEA_WEBHOOK_SECRET` в `.env`
- Signature header: `X-Gitea-Signature` (HMAC-SHA256 hex digest)
### Проверка
```bash
GITEA_TOKEN=$(grep ORCH_GITEA_TOKEN /home/slin/repos/orchestrator/.env | cut -d= -f2)
curl -s "http://localhost:3000/api/v1/repos/admin/enduro-trails/hooks" \
-H "Authorization: token ${GITEA_TOKEN}" | python3 -m json.tool
```
### Пересоздание (если нужно)
```bash
GITEA_WEBHOOK_SECRET=$(openssl rand -hex 20)
# Обновить в .env: ORCH_GITEA_WEBHOOK_SECRET=<new_secret>
curl -X POST "http://localhost:3000/api/v1/repos/admin/enduro-trails/hooks" \
-H "Authorization: token ${GITEA_TOKEN}" \
-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": "*"
}'
```
---
## Plane Webhook
**Создан напрямую в PostgreSQL** (Plane CE не экспортирует webhook API через внешний /api/v1/).
- URL: `https://openclaw.mva154.duckdns.org/orchestrator/webhook/plane`
- Events: `issue` (work_item.created), `issue_comment` (comment.created)
- Secret: значение `ORCH_PLANE_WEBHOOK_SECRET` в `.env`
- Signature header: `X-Plane-Signature` (HMAC-SHA256 hex digest)
### Проверка
```bash
docker exec -e PGPASSWORD=plane plane-app-plane-db-1 psql -U plane -d plane -c \
"SELECT id, url, is_active FROM webhooks;"
```
### Ручная настройка через UI (альтернатива)
1. Открыть `https://plane.mva154.duckdns.org`
2. Workspace Settings → Webhooks → Add Webhook
3. URL: `https://openclaw.mva154.duckdns.org/orchestrator/webhook/plane`
4. Secret: значение из `ORCH_PLANE_WEBHOOK_SECRET` в `.env`
5. Events: Issue, Issue Comment
6. Save
### Пересоздание через SQL
```bash
PLANE_WEBHOOK_SECRET=$(openssl rand -hex 20)
# Обновить в .env: ORCH_PLANE_WEBHOOK_SECRET=<new_secret>
WORKSPACE_ID=$(docker exec -e PGPASSWORD=plane plane-app-plane-db-1 psql -U plane -d plane -t -A -c \
"SELECT id FROM workspaces WHERE slug='ag_proj'")
WEBHOOK_ID=$(cat /proc/sys/kernel/random/uuid)
docker exec -e PGPASSWORD=plane plane-app-plane-db-1 psql -U plane -d plane -c "
INSERT INTO webhooks (id, created_at, updated_at, deleted_at, workspace_id, url, is_active, secret_key, project, issue, module, cycle, issue_comment, is_internal, version)
VALUES ('${WEBHOOK_ID}', NOW(), NOW(), NULL, '${WORKSPACE_ID}',
'https://openclaw.mva154.duckdns.org/orchestrator/webhook/plane',
true, '${PLANE_WEBHOOK_SECRET}', true, true, false, false, true, false, 'v1');
"
```
---
## HMAC Signature Verification
Оба handler'а проверяют подпись:
- Если secret пустой в `.env` — верификация пропускается (для dev/debug)
- Если secret задан — запрос без валидной подписи получает `401 Unauthorized`
### Формат подписи
| Source | Header | Algorithm | Format |
|--------|--------|-----------|--------|
| Gitea | `X-Gitea-Signature` | HMAC-SHA256 | hex digest (без префикса) |
| Plane | `X-Plane-Signature` | HMAC-SHA256 | hex digest |
### Тест подписи вручную
```bash
SECRET=$(grep ORCH_GITEA_WEBHOOK_SECRET /home/slin/repos/orchestrator/.env | cut -d= -f2)
BODY='{"ref":"refs/heads/test","repository":{"name":"enduro-trails"},"commits":[]}'
SIG=$(echo -n "${BODY}" | openssl dgst -sha256 -hmac "${SECRET}" | awk '{print $NF}')
curl -X POST http://localhost:8500/webhook/gitea \
-H "Content-Type: application/json" \
-H "X-Gitea-Event: push" \
-H "X-Gitea-Signature: ${SIG}" \
-d "${BODY}"
# Expected: {"status":"accepted"}
```
---
## Переменные окружения (.env)
| Переменная | Описание |
|-----------|----------|
| `ORCH_GITEA_WEBHOOK_SECRET` | HMAC secret для Gitea webhook |
| `ORCH_PLANE_WEBHOOK_SECRET` | HMAC secret для Plane webhook |
| `ORCH_GITEA_TOKEN` | API token для Gitea |
| `ORCH_PLANE_API_TOKEN` | API token для Plane |
---
## Troubleshooting
```bash
# Логи Orchestrator
docker logs orchestrator --tail 50 2>&1 | grep -i "webhook\|signature\|401"
# События в БД
docker exec orchestrator python3 -c "
import sqlite3
conn = sqlite3.connect('/app/data/orchestrator.db')
for r in conn.execute('SELECT id, source, event_type, timestamp FROM events ORDER BY id DESC LIMIT 10').fetchall():
print(r)
"
# Gitea webhook delivery history
# Gitea UI → Settings → Webhooks → click webhook → Recent Deliveries
```
---
*Создано: 2026-05-21 | Автор: Dev-агент*