Развёрнут push-webhook от Gitea на kb-pull-webhook.service на LXC 137 + auto-reindex FTS в kb-pull.sh после нового HEAD. Грабли: gitea webhook.ALLOWED_HOST_LIST по дефолту режет private IP; flock -n теряет двойные push, заменён на -w 180. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
219 lines
8.2 KiB
Markdown
219 lines
8.2 KiB
Markdown
---
|
||
date: 2026-05-06
|
||
type: snippet
|
||
tags: [openclaw, gitea, webhook, kb-sync]
|
||
status: deployed
|
||
---
|
||
|
||
# Webhook Gitea → kb-pull на LXC 137 (Максимка)
|
||
|
||
Push с Mac → видно боту в FTS за **~11 секунд** (было 5–15 минут через cron `*/15`). Развёрнуто 2026-05-06. Подробности — [[../decisions/2026-05-06-openclaw-kb-webhook-deployment]].
|
||
|
||
Cron `*/15` оставлен safety net — спасает, если listener умер или Gitea недоступна.
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
Mac git push ──► Gitea (LXC 136, 10.0.0.189)
|
||
│ webhook POST + HMAC-SHA256
|
||
▼
|
||
LXC 137 :18790 kb-pull-webhook.service
|
||
│ subprocess.Popen
|
||
▼
|
||
/usr/local/bin/kb-pull.sh
|
||
│ git fetch + ff-only / auto-reset
|
||
│ if HEAD changed → openclaw memory index
|
||
▼
|
||
FTS реиндекс ~38 сек на 875 файлов / 1791 chunk
|
||
```
|
||
|
||
## 1. Listener `/usr/local/bin/kb-pull-webhook.py`
|
||
|
||
```python
|
||
#!/usr/bin/env python3
|
||
"""Listens for Gitea push webhooks, triggers kb-pull.sh."""
|
||
import hmac, hashlib, http.server, os, subprocess, sys
|
||
|
||
SECRET = os.environ.get("GITEA_WEBHOOK_SECRET", "").encode()
|
||
PULL = "/usr/local/bin/kb-pull.sh"
|
||
|
||
class H(http.server.BaseHTTPRequestHandler):
|
||
def do_POST(self):
|
||
n = int(self.headers.get("Content-Length", 0) or 0)
|
||
body = self.rfile.read(n)
|
||
sys.stderr.write("WH POST event=%s sig=%s len=%d\n" % (
|
||
self.headers.get("X-Gitea-Event", "-"),
|
||
(self.headers.get("X-Gitea-Signature", "-") or "-")[:12],
|
||
n)); sys.stderr.flush()
|
||
if SECRET:
|
||
sig = self.headers.get("X-Gitea-Signature", "")
|
||
mac = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
|
||
if not hmac.compare_digest(sig, mac):
|
||
sys.stderr.write("WH 401 sig-mismatch\n"); sys.stderr.flush()
|
||
self.send_error(401); return
|
||
subprocess.Popen([PULL], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||
self.send_response(202); self.end_headers()
|
||
self.wfile.write(b"queued\n")
|
||
sys.stderr.write("WH 202 kb-pull launched\n"); sys.stderr.flush()
|
||
def do_GET(self): # health-check
|
||
self.send_response(200); self.end_headers()
|
||
self.wfile.write(b"ok\n")
|
||
def log_message(self, fmt, *a): # стандартный access log в journal
|
||
sys.stderr.write("WH-base " + (fmt % a) + "\n"); sys.stderr.flush()
|
||
|
||
if __name__ == "__main__":
|
||
port = int(os.environ.get("PORT", "18790"))
|
||
sys.stderr.write("WH listener up on :%d secret=%s\n" % (port, "yes" if SECRET else "NO"))
|
||
sys.stderr.flush()
|
||
http.server.HTTPServer(("0.0.0.0", port), H).serve_forever()
|
||
```
|
||
|
||
```bash
|
||
chmod +x /usr/local/bin/kb-pull-webhook.py
|
||
```
|
||
|
||
## 2. systemd unit `/etc/systemd/system/kb-pull-webhook.service`
|
||
|
||
```ini
|
||
[Unit]
|
||
Description=KB pull webhook listener (Gitea -> kb-pull.sh)
|
||
After=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
Environment=GITEA_WEBHOOK_SECRET=<openssl rand -hex 32>
|
||
Environment=PORT=18790
|
||
ExecStart=/usr/bin/python3 /usr/local/bin/kb-pull-webhook.py
|
||
Restart=on-failure
|
||
RestartSec=5
|
||
NoNewPrivileges=yes
|
||
PrivateTmp=yes
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
```bash
|
||
SECRET=$(openssl rand -hex 32)
|
||
sed -i "s|<openssl rand -hex 32>|$SECRET|" /etc/systemd/system/kb-pull-webhook.service
|
||
systemctl daemon-reload
|
||
systemctl enable --now kb-pull-webhook.service
|
||
ss -ltnp | grep 18790 # должен слушать на 0.0.0.0:18790
|
||
```
|
||
|
||
## 3. Хук в `kb-pull.sh` — реиндекс при новом HEAD
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# read-only pull knowledge-base for openclaw context
|
||
# Auto-reset at divergence; reindex FTS on new commits.
|
||
set -u
|
||
exec 9>/tmp/kb-pull.lock
|
||
flock -w 180 9 || exit 0 # ← ждать до 3 мин, не -n (race на двойном webhook)
|
||
cd /root/knowledge-base
|
||
|
||
LOG=/var/log/kb-pull.log
|
||
{
|
||
echo "--- $(date -Iseconds) ---"
|
||
HEAD_BEFORE=$(git rev-parse HEAD 2>/dev/null)
|
||
git fetch --quiet origin main
|
||
if ! git merge-base --is-ancestor HEAD origin/main 2>/dev/null; then
|
||
echo "divergence detected, resetting to origin/main"
|
||
git reset --hard origin/main
|
||
else
|
||
git pull --ff-only --quiet origin main
|
||
fi
|
||
HEAD_AFTER=$(git rev-parse HEAD 2>/dev/null)
|
||
if [ "$HEAD_BEFORE" != "$HEAD_AFTER" ]; then
|
||
echo "HEAD: $HEAD_BEFORE -> $HEAD_AFTER, triggering FTS reindex"
|
||
timeout 120 openclaw memory index 2>&1 | tail -5
|
||
else
|
||
echo "no new commits, skip reindex"
|
||
fi
|
||
} >> $LOG 2>&1
|
||
```
|
||
|
||
`flock -w 180` — критично. Если webhook прилетает во время предыдущего pull/reindex (быстрые двойные push), `-n` молча выходит и пропускает push.
|
||
|
||
## 4. Регистрация webhook в Gitea (через API)
|
||
|
||
```bash
|
||
SECRET=... # тот же, что в systemd unit
|
||
curl -u oleg:OL260380eg -X POST http://10.0.0.189:3000/api/v1/repos/oleg/knowledge-base/hooks \
|
||
-H "Content-Type: application/json" \
|
||
-d "{\"type\":\"gitea\",\"config\":{\"url\":\"http://10.0.0.239:18790/\",\"content_type\":\"json\",\"secret\":\"$SECRET\"},\"events\":[\"push\"],\"active\":true,\"branch_filter\":\"main\"}"
|
||
```
|
||
|
||
Через UI: `git.dttb.ru` → репо → Settings → Webhooks → Add → Gitea, заполнить теми же полями.
|
||
|
||
## 5. **Обязательно:** разрешить private IP в Gitea
|
||
|
||
По умолчанию Gitea **блокирует webhooks на private адреса** (SSRF-protection). В docker-логах будет:
|
||
|
||
> `webhook can only call allowed HTTP servers (check your webhook.ALLOWED_HOST_LIST setting), deny '10.0.0.239'`
|
||
|
||
Listener при этом ничего не получит, в его journal пусто. Лечится в `app.ini` Gitea:
|
||
|
||
```ini
|
||
[webhook]
|
||
ALLOWED_HOST_LIST = 10.0.0.0/24,*.dttb.ru,private
|
||
```
|
||
|
||
Конкретно у нас Gitea в docker, конфиг — `/opt/gitea/data/gitea/conf/app.ini` на LXC 136. После правки:
|
||
```bash
|
||
docker restart gitea
|
||
```
|
||
|
||
## 6. Проверка end-to-end
|
||
|
||
```bash
|
||
# на Mac
|
||
cd ~/knowledge-base
|
||
git commit --allow-empty -m "smoke" && git push
|
||
|
||
# на 137 (через 5–10 сек)
|
||
pct exec 137 -- journalctl -u kb-pull-webhook.service --since "30 sec ago" -n 5
|
||
# WH POST event=push sig=... len=...
|
||
# WH 202 kb-pull launched
|
||
pct exec 137 -- tail -3 /var/log/kb-pull.log
|
||
# HEAD: <old> -> <new>, triggering FTS reindex
|
||
# Memory index updated (main).
|
||
```
|
||
|
||
Измеренный тайминг real push (2026-05-06): T0 git push → T+11s `git rev-parse HEAD` на 137 уже новый.
|
||
|
||
## 7. Опционально: ограничить порт по источнику
|
||
|
||
Listener слушает `0.0.0.0:18790`. Если нужно жёстко закрыть:
|
||
|
||
```bash
|
||
iptables -A INPUT -p tcp --dport 18790 -s 10.0.0.189 -j ACCEPT
|
||
iptables -A INPUT -p tcp --dport 18790 -j DROP
|
||
```
|
||
|
||
Не делал — homelab LAN trusted, secret-HMAC уже отсекает левые запросы.
|
||
|
||
## Откат
|
||
|
||
```bash
|
||
# на 137
|
||
systemctl disable --now kb-pull-webhook.service
|
||
rm /etc/systemd/system/kb-pull-webhook.service /usr/local/bin/kb-pull-webhook.py
|
||
# восстановить kb-pull.sh из бэкапа
|
||
cp /usr/local/bin/kb-pull.sh.bak.* /usr/local/bin/kb-pull.sh
|
||
|
||
# в Gitea
|
||
curl -u oleg:OL260380eg -X DELETE http://10.0.0.189:3000/api/v1/repos/oleg/knowledge-base/hooks/<id>
|
||
```
|
||
|
||
Cron `*/15` продолжит работать.
|
||
|
||
## Грабли (выписаны из реального деплоя)
|
||
|
||
| Симптом | Причина | Где смотреть |
|
||
|---|---|---|
|
||
| Test delivery 204 от Gitea, но listener не получил | `webhook.ALLOWED_HOST_LIST` не разрешает private IP | `docker logs gitea` на LXC 136 |
|
||
| Двойной push — второй пропускается | `flock -n` без ожидания | Заменить на `flock -w 180` |
|
||
| FTS не обновился, хотя git pull прошёл | `openclaw memory` не дёргается | Хук в `kb-pull.sh` после ff-only |
|
||
| Listener в restart-loop после правки | sed побил python синтаксис | `journalctl -u kb-pull-webhook --since "1 min ago"` |
|