docs(runbook): tg-digest VDS deploy runbook + stack artifacts (task:1311)
Ранбук первого деплоя паттерна sched+worker (модель для yt-digest): стек tg-digest (воркер internal, mem_limit 256m), runtime-регистрация задачи POST /tasks, env-контракт, smoke/verify/rollback, gotchas (сессия Telethon, TZ UTC, timeoutMs). Артефакты: compose source-of-truth, Dockerfile + http_worker.py (черновики для коммита в victor/tg-digest). task:1311 разблокирован (1305 done) -> ready.
This commit is contained in:
@@ -35,6 +35,7 @@ updated: 2026-08-26
|
|||||||
| oCIS / owncloud | [`ocis-on-vds-deploy-recipe.md`](ocis-on-vds-deploy-recipe.md) | deploy recipe + gotchas |
|
| oCIS / owncloud | [`ocis-on-vds-deploy-recipe.md`](ocis-on-vds-deploy-recipe.md) | deploy recipe + gotchas |
|
||||||
| Локальный стенд sched-pipelines (sched + воркеры @apilki, мок-тест) | [`sched-pipelines-local-stack-runbook.md`](sched-pipelines-local-stack-runbook.md) | команды стенда, таймаут-сценарий (deadline-stop), gotchas 1-7 |
|
| Локальный стенд sched-pipelines (sched + воркеры @apilki, мок-тест) | [`sched-pipelines-local-stack-runbook.md`](sched-pipelines-local-stack-runbook.md) | команды стенда, таймаут-сценарий (deadline-stop), gotchas 1-7 |
|
||||||
| sched → VDS (ядро daemon + admin API, MariaDB) | [`sched-vds-deploy-runbook.md`](sched-vds-deploy-runbook.md) | стек sched Id 28, storage-mysql + TLS к mariadb, admin API, verify, gotchas |
|
| sched → VDS (ядро daemon + admin API, MariaDB) | [`sched-vds-deploy-runbook.md`](sched-vds-deploy-runbook.md) | стек sched Id 28, storage-mysql + TLS к mariadb, admin API, verify, gotchas |
|
||||||
|
| tg-digest (sched cron + HTTP-воркер, паттерн sched+worker) | [`tg-digest-vds-deploy-runbook.md`](tg-digest-vds-deploy-runbook.md) | стек tg-digest (воркер internal, mem_limit 256m), runtime-регистрация задачи POST /tasks, env-контракт, smoke, gotchas (сессия Telethon, TZ UTC); первый ранбук паттерна sched+worker (модель для yt-digest) |
|
||||||
|
|
||||||
## Публикация на GitHub (OSS)
|
## Публикация на GitHub (OSS)
|
||||||
|
|
||||||
|
|||||||
168
.wiki/concepts/tg-digest-vds-deploy-runbook.md
Normal file
168
.wiki/concepts/tg-digest-vds-deploy-runbook.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
---
|
||||||
|
title: "tg-digest — VDS deploy runbook (sched cron + HTTP-воркер)"
|
||||||
|
type: concept
|
||||||
|
tags: [tg-digest, sched, vds, deploy, docker, portainer, runbook, worker, telegram]
|
||||||
|
related: [concepts/runbooks-index.md, concepts/sched-vds-deploy-runbook.md, concepts/portainer-stack-management-vds.md]
|
||||||
|
updated: 2026-08-28
|
||||||
|
---
|
||||||
|
|
||||||
|
# tg-digest — VDS deploy runbook (sched cron + HTTP-воркер)
|
||||||
|
|
||||||
|
> **⚙️ СЛУЖЕБНЫЙ РАНБУК — только для администратора проекта `.admin`.**
|
||||||
|
> Применять/выполнять шаги может только **`.admin`** (оператор проекта admin). Другим проектам/агентам — читать по запросу, не выполнять.
|
||||||
|
> Из проекта не выносить: не копировать в другие вики, не пересказывать, не публиковать.
|
||||||
|
> Нужен деплой или прод-операция по этому ранбуку — **поставить задачу админу (`.admin`) и написать письмо**. Админ выполняет, остальные верифицируют.
|
||||||
|
|
||||||
|
Развёртывание **воркера tg-digest** (дневной дайджест телеграм-каналов) на инфра-VDS:
|
||||||
|
контейнер-воркер (internal, сеть `proxy`, без traefik-роута) + задача в **sched** (ядро уже
|
||||||
|
задеплоено, стек 28, см. `sched-vds-deploy-runbook`): cron `0 5 * * *`, runner http, simple mode.
|
||||||
|
Опер-таска: `task:1311` (tg-digest-ops-stack). Аналог: yt-digest `task:1190` (не задеплоен —
|
||||||
|
этот ранбук первый для паттерна sched+worker).
|
||||||
|
|
||||||
|
## Параметры
|
||||||
|
|
||||||
|
| Что | Значение |
|
||||||
|
|---|---|
|
||||||
|
| Хост | VDS kzntsv `89.253.255.94` |
|
||||||
|
| Стек Portainer | **`tg-digest`** (новый, создаётся первым деплоем), endpointId **1**; после создания id записать сюда |
|
||||||
|
| Образ | `registry.kzntsv.site/tg-digest-worker:<tag>` (python:3.13-alpine, telethon+requests) |
|
||||||
|
| Compose source-of-truth | `admin/host-stacks/vds-kzntsv/tg-digest.compose.yml` |
|
||||||
|
| Воркер | контейнер `tg-digest`, HTTP `:8080`: `GET /healthz`, `POST /run` (simple mode: 2xx = ok) |
|
||||||
|
| Задача sched | runtime-регистрация `POST /tasks` (fileManaged:false, без рестарта ядра); cron `0 5 * * *` (**tz UTC** = 08:00 MSK) |
|
||||||
|
| Sched admin key | `pass sched/admin-key` (Bearer) |
|
||||||
|
| Per-task auth | `x-sched-api-key` = `WORKER_API_KEY` воркера = `pass sched/tg-digest-api-key` (завести при деплое) |
|
||||||
|
| mem_limit | `256m` |
|
||||||
|
| Сети | `proxy` (external, internal — НЕТ traefik-labels, наружу не публикуется) |
|
||||||
|
| Registry | `REGISTRY_URL/REGISTRY_USER/REGISTRY_PASS` = `pass vds-kzntsv/full-env` |
|
||||||
|
|
||||||
|
## Артефакты
|
||||||
|
|
||||||
|
- **Код:** репо victor/tg-digest, ветка main, минимум `d3aefb2` (raw-ингест, task:1475 —
|
||||||
|
старый `ingest_wiki` удалён, per-post ингест не работает). Воркер: `python -m src.worker`.
|
||||||
|
- **Dockerfile + HTTP-обёртка** (`http_worker.py`): коммитятся в **корень репо** victor/tg-digest
|
||||||
|
(dev-сторона); черновики — `admin/host-stacks/vds-kzntsv/tg-digest/`. Без них image не собрать.
|
||||||
|
- **Env-контракт воркера** (см. таблицу ниже) — все значения env (pass в контейнере НЕТ).
|
||||||
|
|
||||||
|
### Env-контракт (воркер)
|
||||||
|
|
||||||
|
| Переменная | Источник (pass) | Обязательна | Назначение |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `TG_API_ID` | `telegram/api-id` — **НЕТ в pass, оператор** | да | MTProto app id (my.telegram.org) |
|
||||||
|
| `TG_API_HASH` | `telegram/api-hash` — **НЕТ в pass, оператор** | да | MTProto app hash |
|
||||||
|
| `TG_SESSION` | `/data/session.session` (volume; создаётся интерактивным логином) | да | Telethon-сессия |
|
||||||
|
| `TG_PHONE` | оператор (только при первом логине) | первый запуск | телефон аккаунта |
|
||||||
|
| `TG_CACHE` | — (дефолт `tg-cache`; на деплое `/data/tg-cache`) | опц | кэш коллектора + state.json (персист) |
|
||||||
|
| `TG_WINDOW_HOURS` | — (дефолт 24; бэкфилл 26-27 → 48-72) | опц | окно сбора |
|
||||||
|
| `TELEGRAM_BOT_TOKEN` | `telegram/full-env` | да | доставка дайджеста |
|
||||||
|
| `TELEGRAM_CHAT_ID` | `telegram/full-env` | да | чат доставки |
|
||||||
|
| `LLM_API_KEY` | pass-запись **отсутствует — уточнить у оператора** (deepseek; локально в gitignored `src/config/llm.json`) | да | стадия-2 (LLM-находки) |
|
||||||
|
| `LLM_BASE_URL` | — (дефолт `https://api.deepseek.com`) | опц | |
|
||||||
|
| `LLM_MODEL` | — (дефолт `deepseek-chat`) | опц | |
|
||||||
|
| `MAPPA_API_TOKEN` | `mappa/full-env` | да | ингест raw (wiki:3312) |
|
||||||
|
| `MAPPA_URL` | — (дефолт `https://mappa.vds.kzntsv.site`) | опц | |
|
||||||
|
| `MAPPA_PROJECT` | — (дефолт `tg-digest`) | опц | |
|
||||||
|
| `WORKER_API_KEY` | `sched/tg-digest-api-key` (завести) | да | проверка `x-sched-api-key` в /run |
|
||||||
|
| `TG_SEND` | — (`0` — dry-run smoke без отправки) | опц | доставка |
|
||||||
|
|
||||||
|
### Задача sched (POST /tasks, runtime)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "tg-digest",
|
||||||
|
"schedules": [{ "cron": "0 5 * * *" }],
|
||||||
|
"config": {
|
||||||
|
"url": "http://tg-digest:8080/run",
|
||||||
|
"method": "POST",
|
||||||
|
"timeoutMs": 900000,
|
||||||
|
"auth": { "apiKey": "<из pass sched/tg-digest-api-key>" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Простой режим (без `envelope`): sched шлёт POST, ждёт ответ до `config.timeoutMs`, 2xx = succeeded.
|
||||||
|
**`task.timeoutMs` НЕ ставить** (ceiling шлёт воркеру cancel — воркер его не реализует).
|
||||||
|
|
||||||
|
## Первый деплой
|
||||||
|
|
||||||
|
**0. Предусловия (оператор):**
|
||||||
|
- `pass telegram/api-id` + `pass telegram/api-hash` (my.telegram.org → App `tg-digest`);
|
||||||
|
- **сессия Telethon**: первый `python -m src.collector --live` интерактивен (телефон + код из СМС) —
|
||||||
|
создать session-файл заранее, положить в volume (см. gotcha 1);
|
||||||
|
- `LLM_API_KEY` (deepseek) — pass-запись или env;
|
||||||
|
- `pass sched/tg-digest-api-key` (новая запись, ключ для `x-sched-api-key`).
|
||||||
|
|
||||||
|
**1. Dockerfile + http_worker.py в репо** (корень victor/tg-digest), main. Билд строго из main.
|
||||||
|
|
||||||
|
**2. Build + push образа:**
|
||||||
|
```bash
|
||||||
|
docker login $REGISTRY_URL -u $REGISTRY_USER -p "$(pass show vds-kzntsv/full-env | awk -F= '/^REGISTRY_PASS=/{print $2}')"
|
||||||
|
docker build -t registry.kzntsv.site/tg-digest-worker:<tag> <чекаут victor/tg-digest>
|
||||||
|
docker push registry.kzntsv.site/tg-digest-worker:<tag>
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Стек Portainer** (канон `portainer-stack-management-vds`): создать `tg-digest`
|
||||||
|
(`POST /api/stacks/create/standalone/string?endpointId=1`, stackFileContent = compose из
|
||||||
|
source-of-truth, env-массив = секреты из таблицы; `pullImage: true`). **Без traefik-labels**
|
||||||
|
(воркер internal, доступа снаружи нет).
|
||||||
|
|
||||||
|
**4. Регистрация задачи (без рестарта sched):**
|
||||||
|
```bash
|
||||||
|
AK=$(pass show sched/admin-key)
|
||||||
|
curl -ksS -X POST https://sched.vds.kzntsv.site/api/tasks \
|
||||||
|
-H "Authorization: Bearer $AK" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"name":"tg-digest","schedules":[{"cron":"0 5 * * *"}],
|
||||||
|
"config":{"url":"http://tg-digest:8080/run","method":"POST","timeoutMs":900000,
|
||||||
|
"auth":{"apiKey":"<из pass sched/tg-digest-api-key>"}}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**5. Smoke (ручной триггер):**
|
||||||
|
```bash
|
||||||
|
curl -ksS -X POST https://sched.vds.kzntsv.site/api/tasks/tg-digest/run \
|
||||||
|
-H "Authorization: Bearer $AK" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"temporary": true, "triggeredBy": "admin-smoke"}'
|
||||||
|
```
|
||||||
|
Блокируется до конца рана (sync runner) → ответ = терминальный run.
|
||||||
|
Сухой деплой без ключей: контейнер Up, `/healthz` 200, ран падает `die 2 creds missing` — ок как
|
||||||
|
проверка плумбинга, не как acceptance.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
- Контейнер: `Up (healthy)`; `docker exec tg-digest python -c "import urllib.request;print(urllib.request.urlopen('http://127.0.0.1:8080/healthz').read())"` → `{"ok": true}`.
|
||||||
|
- sched: `GET /api/tasks` (Bearer) → `tg-digest` в списке, `nextRunAt` завтра 05:00 UTC.
|
||||||
|
- Smoke run: `GET /api/runs?task=tg-digest` → последний run `succeeded` (с ключами) с полем `result`.
|
||||||
|
- Артефакт дня: raw-запись в mappa `raw/research/<дата>-<topic>` (конвенция wiki:3312) + доставка в TG.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
- Задача: `DELETE /api/tasks/tg-digest` (runtime-таска, 204) — расписание снято.
|
||||||
|
- Стек: удалить `tg-digest` в Portainer.
|
||||||
|
- Образ: старый тег в registry (повторный деплой с предыдущим тегом).
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
1. **Сессия Telethon обязательна, api_id/api_hash недостаточны** (2026-08-28): первый `--live`
|
||||||
|
интерактивен — телефон + СМС-код; без session-файла в контейнере коллектор не стартует.
|
||||||
|
Сессию создать заранее (локально или TTY контейнера), смонтировать в `/data/session.session`.
|
||||||
|
2. **pass в контейнере НЕТ** (2026-08-28): коллектор умеет читать pass через bash (`_pass`), но в
|
||||||
|
образе его нет → только env. Все секреты — env при деплое (chmod 600 / Portainer env, вне git).
|
||||||
|
3. **TZ cron = UTC** (дефолт sched): `0 5 * * *` = 08:00 MSK (утренний дайджест, как yt-digest).
|
||||||
|
4. **`config.timeoutMs` (транспортный) — 900000** (пайплайн collect→stage2 LLM→ingest→tg занимает
|
||||||
|
минуты); `task.timeoutMs` (ceiling) НЕ ставить — при срабатывании sched шлёт `POST /cancel`,
|
||||||
|
воркер (не паттерн C, не @apilki) его не реализует → ран пометится failed при живом воркере.
|
||||||
|
5. **mem_limit 256m**: python+telethon на старте ~150-200m — достаточно; не поднимать без нужды.
|
||||||
|
6. **Образ строго из main с `d3aefb2`** (raw-ингест): старый `ingest_wiki` удалён, per-post
|
||||||
|
ингест не работает; билд из старого коммита → падение на ингесте.
|
||||||
|
7. **Бэкфилл за 26-27 августа**: `TG_WINDOW_HOURS` 48-72 на один прогон (или два прогона);
|
||||||
|
`ingest_raw` идемпотентен по дню (`raw/<дата>-<topic>`).
|
||||||
|
8. **Runtime-регистрация = fileManaged:false**: `POST /tasks` не трогает `sched.tasks.json`
|
||||||
|
(остаётся `{"tasks": []}`); таска живёт до `DELETE /tasks/:name`. tasks.json — только
|
||||||
|
декларативный источник (рестарт ядра НЕ нужен).
|
||||||
|
9. **Portainer env не env_file** — секреты в env-массив стека (gotcha 4b портайнер-канона:
|
||||||
|
PUT сбрасывает env, если не передать массив целиком).
|
||||||
|
|
||||||
|
## Связи
|
||||||
|
|
||||||
|
- [[concepts/runbooks-index.md]] — индекс ранбуков
|
||||||
|
- [[concepts/sched-vds-deploy-runbook.md]] — ядро sched (стек 28, admin API, MariaDB)
|
||||||
|
- [[concepts/portainer-stack-management-vds.md]] — общий Portainer-канон (JWT, gotchas)
|
||||||
|
- Репо victor/tg-digest: `src/worker.py` (оркестратор), спека wiki:3260, конвенция raw wiki:3312
|
||||||
52
host-stacks/vds-kzntsv/tg-digest.compose.yml
Normal file
52
host-stacks/vds-kzntsv/tg-digest.compose.yml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# tg-digest — HTTP-воркер дневного дайджеста телеграм-каналов.
|
||||||
|
# sched cron 0 5 * * * (tz UTC = 08:00 MSK), runner http, simple mode.
|
||||||
|
# Internal: сеть proxy, БЕЗ traefik-labels — наружу не публикуется (см. ранбук tg-digest-vds-deploy-runbook).
|
||||||
|
# Deploy: Portainer-managed (см. portainer-stack-management-vds). Env через Portainer (не env_file).
|
||||||
|
# Image: registry.kzntsv.site/tg-digest-worker:<tag> (python:3.13-alpine, telethon+requests, из main victor/tg-digest).
|
||||||
|
|
||||||
|
services:
|
||||||
|
tg-digest:
|
||||||
|
image: registry.kzntsv.site/tg-digest-worker:<tag>
|
||||||
|
container_name: tg-digest
|
||||||
|
restart: unless-stopped
|
||||||
|
mem_limit: 256m
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
environment:
|
||||||
|
# MTProto (оператор; pass telegram/api-id + api-hash)
|
||||||
|
TG_API_ID: ${TG_API_ID}
|
||||||
|
TG_API_HASH: ${TG_API_HASH}
|
||||||
|
TG_SESSION: /data/session.session
|
||||||
|
TG_PHONE: ${TG_PHONE}
|
||||||
|
TG_CACHE: /data/tg-cache
|
||||||
|
TG_WINDOW_HOURS: ${TG_WINDOW_HOURS:-24}
|
||||||
|
# Доставка (pass telegram/full-env)
|
||||||
|
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
|
||||||
|
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID}
|
||||||
|
# Стадия-2 LLM (deepseek; ключ — у оператора)
|
||||||
|
LLM_API_KEY: ${LLM_API_KEY}
|
||||||
|
LLM_BASE_URL: ${LLM_BASE_URL:-https://api.deepseek.com}
|
||||||
|
LLM_MODEL: ${LLM_MODEL:-deepseek-chat}
|
||||||
|
# Ингест raw в mappa (pass mappa/full-env)
|
||||||
|
MAPPA_URL: ${MAPPA_URL:-https://mappa.vds.kzntsv.site}
|
||||||
|
MAPPA_API_TOKEN: ${MAPPA_API_TOKEN}
|
||||||
|
MAPPA_PROJECT: ${MAPPA_PROJECT:-tg-digest}
|
||||||
|
# Auth sched → воркер (pass sched/tg-digest-api-key)
|
||||||
|
WORKER_API_KEY: ${WORKER_API_KEY}
|
||||||
|
# 0 — dry-run smoke без отправки в TG
|
||||||
|
TG_SEND: ${TG_SEND:-1}
|
||||||
|
volumes:
|
||||||
|
- tg-digest-data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request;urllib.request.urlopen('http://127.0.0.1:8080/healthz')"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
tg-digest-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
21
host-stacks/vds-kzntsv/tg-digest/Dockerfile
Normal file
21
host-stacks/vds-kzntsv/tg-digest/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# tg-digest worker — python (telethon + requests) + HTTP-обёртка для sched http-runner.
|
||||||
|
# Файл коммитится в КОРЕНЬ репо victor/tg-digest (рядом http_worker.py).
|
||||||
|
# Build (из чекаута victor/tg-digest, main с d3aefb2):
|
||||||
|
# docker build -t registry.kzntsv.site/tg-digest-worker:<tag> .
|
||||||
|
# Env-контракт — см. ранбук tg-digest-vds-deploy-runbook (все секреты env, pass в контейнере нет).
|
||||||
|
FROM python:3.13-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY src/ src/
|
||||||
|
COPY http_worker.py ./
|
||||||
|
|
||||||
|
RUN mkdir -p /data
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
EXPOSE 8080
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
CMD ["python", "http_worker.py"]
|
||||||
63
host-stacks/vds-kzntsv/tg-digest/http_worker.py
Normal file
63
host-stacks/vds-kzntsv/tg-digest/http_worker.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""tg-digest HTTP-воркер для sched (http runner, simple mode).
|
||||||
|
|
||||||
|
GET /healthz -> 200 {"ok": true} (healthcheck стека)
|
||||||
|
POST /run -> проверка x-sched-api-key (WORKER_API_KEY); запуск
|
||||||
|
`python -m src.worker`; 200 {ok, runId, summary} | 5xx {error}.
|
||||||
|
|
||||||
|
Simple mode: sched шлёт POST и ждёт ответ до config.timeoutMs; любой 2xx = succeeded.
|
||||||
|
runId приходит в заголовке x-sched-run-id (пишется в ответ, в лог).
|
||||||
|
Заголовки x-sched-api-key от sched — auth per-task (task config.auth.apiKey).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
|
WORKER_API_KEY = os.environ.get("WORKER_API_KEY", "")
|
||||||
|
WORKER_CMD = [sys.executable, "-m", "src.worker"]
|
||||||
|
WORKER_TIMEOUT = float(os.environ.get("WORKER_TIMEOUT_S") or 3600)
|
||||||
|
PORT = int(os.environ.get("PORT") or 8080)
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def log_message(self, *args): # тишина в stdout (логи — по runId)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _send(self, code, obj):
|
||||||
|
body = json.dumps(obj, ensure_ascii=False).encode("utf-8")
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == "/healthz":
|
||||||
|
self._send(200, {"ok": True})
|
||||||
|
else:
|
||||||
|
self._send(404, {"error": "not found"})
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
if self.path != "/run":
|
||||||
|
return self._send(404, {"error": "not found"})
|
||||||
|
if WORKER_API_KEY and self.headers.get("x-sched-api-key") != WORKER_API_KEY:
|
||||||
|
return self._send(401, {"error": "unauthorized"})
|
||||||
|
run_id = self.headers.get("x-sched-run-id", "?")
|
||||||
|
try:
|
||||||
|
r = subprocess.run(WORKER_CMD, capture_output=True, text=True,
|
||||||
|
env=os.environ, timeout=WORKER_TIMEOUT)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return self._send(504, {"error": "worker timeout", "runId": run_id})
|
||||||
|
if r.returncode != 0:
|
||||||
|
return self._send(500, {"error": (r.stderr or r.stdout)[-500:], "runId": run_id})
|
||||||
|
try:
|
||||||
|
summary = json.loads(r.stdout)
|
||||||
|
except ValueError:
|
||||||
|
summary = {"stdout": r.stdout[-500:]}
|
||||||
|
self._send(200, {"ok": True, "runId": run_id, "summary": summary})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
|
||||||
Reference in New Issue
Block a user