Phase 2 webhook: snippet и decision (Mac→openclaw FTS лаг 15м → 11с)
Развёрнут 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>
This commit is contained in:
78
decisions/2026-05-06-openclaw-kb-webhook-deployment.md
Normal file
78
decisions/2026-05-06-openclaw-kb-webhook-deployment.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
date: 2026-05-06
|
||||||
|
type: decision
|
||||||
|
tags: [openclaw, gitea, webhook, kb-sync, performance]
|
||||||
|
status: applied
|
||||||
|
---
|
||||||
|
|
||||||
|
# Webhook Gitea → openclaw kb-pull (Фаза 2 плана улучшения KB-поиска)
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
После публикации `projects/lipki/README.md` стало видно, что openclaw (Максимка) видит новые правки vault только через cron `*/15` — лаг до 15 минут. Это первое, что бьёт по UX («только что записал → бот всё ещё не знает»).
|
||||||
|
|
||||||
|
План улучшения KB-поиска расписан в чате с Claude Code 2026-05-06 утром, Фаза 2 — webhook + автоматический FTS-реиндекс.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Push-канал Gitea → listener на LXC 137 → `kb-pull.sh` → `openclaw memory index`. Cron `*/15` оставлен как safety net.
|
||||||
|
|
||||||
|
Подробная реализация — [[../snippets/openclaw-kb-webhook]].
|
||||||
|
|
||||||
|
## Что развёрнуто
|
||||||
|
|
||||||
|
| Компонент | Где |
|
||||||
|
|---|---|
|
||||||
|
| `/usr/local/bin/kb-pull-webhook.py` | LXC 137, Python http.server, HMAC-SHA256 проверка, journal-лог |
|
||||||
|
| `/etc/systemd/system/kb-pull-webhook.service` | LXC 137, system unit, Restart=on-failure, secret в env |
|
||||||
|
| Обновлённый `/usr/local/bin/kb-pull.sh` | `flock -w 180`, hash-diff, авто `openclaw memory index` при новом HEAD |
|
||||||
|
| Webhook в Gitea (id=1, репо `oleg/knowledge-base`) | events: push, branch: main, secret в HMAC |
|
||||||
|
| `webhook.ALLOWED_HOST_LIST = 10.0.0.0/24,*.dttb.ru,private` в `app.ini` | LXC 136 (Gitea в docker), `/opt/gitea/data/gitea/conf/app.ini` |
|
||||||
|
|
||||||
|
## Метрика
|
||||||
|
|
||||||
|
End-to-end push → видно боту:
|
||||||
|
|
||||||
|
| Этап | Время |
|
||||||
|
|---|---|
|
||||||
|
| `git push` с Mac → Gitea ack | ~2 c |
|
||||||
|
| Gitea → webhook listener | ~3 c |
|
||||||
|
| listener → kb-pull.sh → git pull → новый HEAD | ~6 c |
|
||||||
|
| **Итого до видимости HEAD на 137** | **~11 c** |
|
||||||
|
| FTS reindex (875 файлов / 1791 chunk) | +38 c |
|
||||||
|
| **Итого до видимости в `openclaw memory search`** | **~50 c** |
|
||||||
|
|
||||||
|
Было: до 15 минут (cron */15) + до 38 c reindex (если бы он раньше был) = до **~16 минут**.
|
||||||
|
|
||||||
|
Улучшение: **×20 латентности до видимости HEAD, ×16 до полного FTS**.
|
||||||
|
|
||||||
|
## Грабли (для будущей памяти)
|
||||||
|
|
||||||
|
1. **Gitea SSRF-protection.** Без `ALLOWED_HOST_LIST` Gitea silently отказывает доставлять webhook на private IP. Test delivery возвращает `204`, но в Gitea log: `webhook can only call allowed HTTP servers, deny '10.0.0.239'`. Listener при этом не видит ничего — пустой journal. Это была первая причина почему ничего не работало.
|
||||||
|
|
||||||
|
2. **`flock -n` теряет вторую серию push'ов.** Если webhook прилетел во время уже идущего pull/reindex (быстрый двойной push) — non-blocking flock молча выходит. Надо `-w 180` (ждать в очереди до 3 мин).
|
||||||
|
|
||||||
|
3. **sed по строке с `||` ломается, если разделитель `|`.** `sed -i 's|flock -n|flock -w 180|'` падает с `unknown option to s`. Использовать `#` или другой разделитель — `sed -i 's#flock -n#flock -w 180#'`.
|
||||||
|
|
||||||
|
4. **Listener `log_message: pass` гасит **все** логи http.server.** Если хочется видеть приём POST — переопределить как `sys.stderr.write(...)`. Иначе debug невозможен.
|
||||||
|
|
||||||
|
## Альтернативы, которые отвергнуты
|
||||||
|
|
||||||
|
- **Полностью убрать cron**: рискованно — если listener умер и не заметили, vault зависнет. Cron `*/15` ничего не стоит и страхует.
|
||||||
|
- **Webhook напрямую через NPM/публичный URL**: лишний хоп, нужен HTTPS, всё в LAN — не нужно.
|
||||||
|
- **iptables на 18790**: HMAC-secret уже отсекает левые запросы. Homelab trusted. Не делал.
|
||||||
|
|
||||||
|
## Откат
|
||||||
|
|
||||||
|
См. секцию «Откат» в [[../snippets/openclaw-kb-webhook]]. Один rm-рецепт + удаление webhook через Gitea API.
|
||||||
|
|
||||||
|
## Что дальше
|
||||||
|
|
||||||
|
Фаза 1 плана (embeddings ollama × bge-m3) — пока **отложена**. При попытке развёртывания openclaw 2026.5.2 со всеми правильными config-полями (`provider=ollama`, `remote.baseUrl`, `model=bge-m3`, `chunking.tokens=512`) **игнорирует chunking** и шлёт в `/api/embed` файлы целиком — output buffer ollama растёт до 700+ MB, обработка >5 мин, undici fetch падает по дефолтному 5-min Headers Timeout. Конфигурация откачена, состояние FTS-only сохранено.
|
||||||
|
|
||||||
|
Возможные пути возврата к векторному поиску:
|
||||||
|
- `provider: local` (transformers.js встроенный) — попробовать без сетевого хопа
|
||||||
|
- Ждать openclaw 2026.5.3+ с фиксом chunking
|
||||||
|
- GitHub Copilot embeddings (требует токен)
|
||||||
|
|
||||||
|
Следующая фаза по плану — **Фаза 3** (`audit/objects-map.json` + `projects/_index.md`) или **Фаза 5.1** (слить дубли видеонаблюдения).
|
||||||
@@ -2,58 +2,70 @@
|
|||||||
date: 2026-05-06
|
date: 2026-05-06
|
||||||
type: snippet
|
type: snippet
|
||||||
tags: [openclaw, gitea, webhook, kb-sync]
|
tags: [openclaw, gitea, webhook, kb-sync]
|
||||||
|
status: deployed
|
||||||
---
|
---
|
||||||
|
|
||||||
# Webhook Gitea → kb-pull на LXC 137 (Максимка)
|
# Webhook Gitea → kb-pull на LXC 137 (Максимка)
|
||||||
|
|
||||||
Сократить лаг Mac → openclaw FTS с **до 15 мин** (cron `*/15`) до **<5 секунд** через push-webhook от Gitea.
|
Push с Mac → видно боту в FTS за **~11 секунд** (было 5–15 минут через cron `*/15`). Развёрнуто 2026-05-06. Подробности — [[../decisions/2026-05-06-openclaw-kb-webhook-deployment]].
|
||||||
|
|
||||||
Cron оставляем как safety net — он почти ничего не стоит и спасает, если listener умер.
|
Cron `*/15` оставлен safety net — спасает, если listener умер или Gitea недоступна.
|
||||||
|
|
||||||
## Архитектура
|
## Архитектура
|
||||||
|
|
||||||
```
|
```
|
||||||
Mac git push ──► Gitea (LXC 136, 10.0.0.189)
|
Mac git push ──► Gitea (LXC 136, 10.0.0.189)
|
||||||
│
|
│ webhook POST + HMAC-SHA256
|
||||||
│ HTTP POST (push event)
|
|
||||||
▼
|
▼
|
||||||
LXC 137 :18790 kb-pull-webhook (systemd)
|
LXC 137 :18790 kb-pull-webhook.service
|
||||||
│
|
│ subprocess.Popen
|
||||||
└─► /usr/local/bin/kb-pull.sh (тот же что в cron)
|
▼
|
||||||
|
/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`
|
## 1. Listener `/usr/local/bin/kb-pull-webhook.py`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Слушает Gitea webhook, дёргает kb-pull.sh."""
|
"""Listens for Gitea push webhooks, triggers kb-pull.sh."""
|
||||||
import hmac, hashlib, http.server, os, subprocess
|
import hmac, hashlib, http.server, os, subprocess, sys
|
||||||
|
|
||||||
SECRET = os.environ.get('GITEA_WEBHOOK_SECRET', '').encode()
|
SECRET = os.environ.get("GITEA_WEBHOOK_SECRET", "").encode()
|
||||||
PULL = '/usr/local/bin/kb-pull.sh'
|
PULL = "/usr/local/bin/kb-pull.sh"
|
||||||
|
|
||||||
class H(http.server.BaseHTTPRequestHandler):
|
class H(http.server.BaseHTTPRequestHandler):
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
n = int(self.headers.get('Content-Length', 0) or 0)
|
n = int(self.headers.get("Content-Length", 0) or 0)
|
||||||
body = self.rfile.read(n)
|
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:
|
if SECRET:
|
||||||
sig = self.headers.get('X-Gitea-Signature', '')
|
sig = self.headers.get("X-Gitea-Signature", "")
|
||||||
mac = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
|
mac = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
|
||||||
if not hmac.compare_digest(sig, mac):
|
if not hmac.compare_digest(sig, mac):
|
||||||
|
sys.stderr.write("WH 401 sig-mismatch\n"); sys.stderr.flush()
|
||||||
self.send_error(401); return
|
self.send_error(401); return
|
||||||
subprocess.Popen([PULL],
|
subprocess.Popen([PULL], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
||||||
self.send_response(202); self.end_headers()
|
self.send_response(202); self.end_headers()
|
||||||
self.wfile.write(b'queued\n')
|
self.wfile.write(b"queued\n")
|
||||||
|
sys.stderr.write("WH 202 kb-pull launched\n"); sys.stderr.flush()
|
||||||
def do_GET(self): # health-check
|
def do_GET(self): # health-check
|
||||||
self.send_response(200); self.end_headers()
|
self.send_response(200); self.end_headers()
|
||||||
self.wfile.write(b'ok\n')
|
self.wfile.write(b"ok\n")
|
||||||
def log_message(self, fmt, *a): # без спама в journal
|
def log_message(self, fmt, *a): # стандартный access log в journal
|
||||||
pass
|
sys.stderr.write("WH-base " + (fmt % a) + "\n"); sys.stderr.flush()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
port = int(os.environ.get('PORT', '18790'))
|
port = int(os.environ.get("PORT", "18790"))
|
||||||
http.server.HTTPServer(('0.0.0.0', port), H).serve_forever()
|
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
|
```bash
|
||||||
@@ -69,12 +81,11 @@ After=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
Environment=GITEA_WEBHOOK_SECRET=CHANGE_ME_LONG_RANDOM
|
Environment=GITEA_WEBHOOK_SECRET=<openssl rand -hex 32>
|
||||||
Environment=PORT=18790
|
Environment=PORT=18790
|
||||||
ExecStart=/usr/bin/python3 /usr/local/bin/kb-pull-webhook.py
|
ExecStart=/usr/bin/python3 /usr/local/bin/kb-pull-webhook.py
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
# Минимальные права — listener не пишет в /root, только запускает kb-pull.sh
|
|
||||||
NoNewPrivileges=yes
|
NoNewPrivileges=yes
|
||||||
PrivateTmp=yes
|
PrivateTmp=yes
|
||||||
|
|
||||||
@@ -83,66 +94,125 @@ WantedBy=multi-user.target
|
|||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```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 daemon-reload
|
||||||
systemctl enable --now kb-pull-webhook.service
|
systemctl enable --now kb-pull-webhook.service
|
||||||
systemctl status kb-pull-webhook.service
|
ss -ltnp | grep 18790 # должен слушать на 0.0.0.0:18790
|
||||||
ss -ltnp | grep 18790
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Сгенерировать секрет:
|
## 3. Хук в `kb-pull.sh` — реиндекс при новом HEAD
|
||||||
```bash
|
|
||||||
openssl rand -hex 32
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Webhook в Gitea UI
|
|
||||||
|
|
||||||
`git.dttb.ru` → репо `oleg/knowledge-base` → **Settings → Webhooks → Add Webhook → Gitea**
|
|
||||||
|
|
||||||
| Поле | Значение |
|
|
||||||
|---|---|
|
|
||||||
| Target URL | `http://10.0.0.239:18790/` |
|
|
||||||
| HTTP Method | POST |
|
|
||||||
| Content Type | `application/json` |
|
|
||||||
| Secret | тот же, что в systemd unit |
|
|
||||||
| Trigger On | Push Events |
|
|
||||||
| Branch filter | `main` |
|
|
||||||
| Active | ✓ |
|
|
||||||
|
|
||||||
Жмём **Test Delivery** — должно прилететь `202 queued`.
|
|
||||||
|
|
||||||
## 4. Опционально: ограничить порт по источнику
|
|
||||||
|
|
||||||
Listener слушает на `0.0.0.0`. Если хочется — закрыть всё кроме Gitea LXC 136:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
iptables -A INPUT -p tcp --dport 18790 -s 10.0.0.189 -j ACCEPT
|
#!/bin/bash
|
||||||
iptables -A INPUT -p tcp --dport 18790 -j DROP
|
# read-only pull knowledge-base for openclaw context
|
||||||
# или через nftables / netbird ACL — на вкус
|
# 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
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. Проверка end-to-end
|
`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
|
```bash
|
||||||
# на Mac
|
# на Mac
|
||||||
cd ~/knowledge-base
|
cd ~/knowledge-base
|
||||||
date >> /tmp/test && git add -A && git commit -m "webhook test" && git push
|
git commit --allow-empty -m "smoke" && git push
|
||||||
|
|
||||||
# на LXC 137 — должна появиться свежая строка
|
# на 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
|
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
|
```bash
|
||||||
|
# на 137
|
||||||
systemctl disable --now kb-pull-webhook.service
|
systemctl disable --now kb-pull-webhook.service
|
||||||
rm /etc/systemd/system/kb-pull-webhook.service
|
rm /etc/systemd/system/kb-pull-webhook.service /usr/local/bin/kb-pull-webhook.py
|
||||||
rm /usr/local/bin/kb-pull-webhook.py
|
# восстановить kb-pull.sh из бэкапа
|
||||||
# в Gitea — деактивировать webhook
|
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` остаётся живым — KB продолжит синкаться по таймеру.
|
Cron `*/15` продолжит работать.
|
||||||
|
|
||||||
## Замечания
|
## Грабли (выписаны из реального деплоя)
|
||||||
|
|
||||||
- **`openclaw memory` не переиндексируется автоматически** после `kb-pull`. Бот видит markdown сразу (он читает из FS), но FTS-индекс в `~/.openclaw/memory/main.sqlite` обновляется при следующем reindex-цикле. Если нужно ускорить — после pull можно дёргать `openclaw memory rebuild --incremental` (проверить наличие команды в текущей версии). Это уже отдельный шаг — добавить в `kb-pull.sh` после успешного `git pull`.
|
| Симптом | Причина | Где смотреть |
|
||||||
- На LXC 137 `kb-pull.sh` — простой ff-only с авто-reset при divergence. Webhook на нём ничего не ломает: тот же скрипт, та же блокировка `/tmp/kb-pull.lock`.
|
|---|---|---|
|
||||||
|
| 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"` |
|
||||||
|
|||||||
Reference in New Issue
Block a user