# 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 (per-repo)

Gitea-webhook — **per-repo**: создаётся для КАЖДОГО подключаемого к оркестратору репозитория
(`<repo>` ниже). Для новых проектов его создаёт onboarding-CLI
(`scripts/onboard_project.py apply`) — полный процесс см. `docs/operations/ONBOARDING.md`;
команды ниже — для ручной проверки/пересоздания на любом репо.

- URL: `https://openclaw.mva154.duckdns.org/orchestrator/webhook/gitea`
- Events: `push`, `pull_request`, `status`
- Secret: значение `ORCH_GITEA_WEBHOOK_SECRET` в `.env` — **ОДИН глобальный секрет на все
  репо** (приёмник валидирует только его; новый секрет на одном репо сломал бы HMAC остальных —
  при ротации меняется на всех репо разом)
- 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/<owner>/<repo>/hooks" \
  -H "Authorization: token ${GITEA_TOKEN}" | python3 -m json.tool
```

### Пересоздание (если нужно)

```bash
# Секрет переиспользуй из .env (ORCH_GITEA_WEBHOOK_SECRET); генерируй новый ТОЛЬКО при
# первичной настройке/осознанной ротации (и обнови вебхуки ВСЕХ репо):
GITEA_WEBHOOK_SECRET=$(grep ORCH_GITEA_WEBHOOK_SECRET /home/slin/repos/orchestrator/.env | cut -d= -f2)

curl -X POST "http://localhost:3000/api/v1/repos/<owner>/<repo>/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-агент*
