Files
blog/infra/deploy/webhook_receiver.py
wait4xandGLM-5.3 bb29c8abaf fix(deploy): 构建抓取通道可配+构建日志落盘——ESA 522 事故双根治
- build.sh:BLOG_GIT_URL 环境变量(默认公网,同机构建可指内网
  http://127.0.0.1:13020);每次构建 remote set-url 后 fetch——
  2026-10-26 事故:ESA 边缘对 git.wait4x.online 522 时公网 fetch
  set -e 静默死,staging 冻结 150s+ 无痕
- webhook_receiver.py:构建输出落盘 ~/docker_apps/blog/build.log
  (capture_output 丢弃=黑洞,本次事故"构建为何没跑"零证据);
  与运行副本双同步纪律维持
- 宿主 unit 补 BLOG_GIT_URL 内网通道(配置在 unit 不在仓——主机
  特定值不污染仓库默认)

验证: 手动构建实测 stamp=19b2390+robots.txt 352B 上线;18743 测试
      床恢复 200;smoke 对 staging 软门容忍符合 T1 转硬门前设计

Spec: none (发布链通道与可观测性修复,事故驱动)

Co-authored-by: GLM-5.3
2026-11-01 09:58:47 +08:00

107 lines
4.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""gitea webhook 接收器(Phase 4 T3):127.0.0.1/桥网关 :13021。
安全模型:
- /hook 仅接受 HMAC-SHA256 通过的 push(main) 事件(密钥宿主 0600 文件);
- /healthz 无副作用探活;
- 监听 0.0.0.0 是为 gitea 容器经桥网关(172.22.0.1)回连——宿主在 NAT 后、
ESA 只代理 80/443 vhost,该端口不对公网;HMAC 是硬门槛。
触发链:gitea push(main) → 本接收器验签 → build.sh → www-staging 更新。
部署形态:systemd 运行副本 ~/docker_apps/blog/webhook_receiver.py——改本文件
必须同步两份(仓内源 + 运行副本)并 restart 单元,漂移即事故。
常驻方式:systemd --user(blog-webhook.service);reboot 自启需一次性
`sudo loginctl enable-linger wait4x`(已列入上线检查单)。
"""
import hashlib
import hmac
import json
import subprocess
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
PORT = 13021
SECRET_FILE = Path.home() / "docker_apps/gitea/webhook-secret.txt"
BUILD_REPO = Path.home() / "docker_apps/blog/build-repo"
BUILD_LOCK = threading.Lock()
# 构建脚本 = 构建仓自己的 origin/main 版本(架构修正 2026-10-25:此前指向
# 工作树副本,车道在途半成品状态会断生产链——20:31 事故实锤)。构建仓缺失时
# 首克引导(blog 仓公开,匿名 clone)。
def build_script() -> Path:
s = BUILD_REPO / "infra/deploy/build.sh"
if not s.exists():
subprocess.run(
["git", "clone", "https://git.wait4x.online/wait4x/blog.git", str(BUILD_REPO)],
check=True, capture_output=True,
)
return s
def read_secret() -> bytes:
return SECRET_FILE.read_text().strip().encode()
class Handler(BaseHTTPRequestHandler):
def _json(self, code: int, obj: dict) -> None:
body = json.dumps(obj).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self) -> None:
if self.path == "/healthz":
self._json(200, {"ok": True, "service": "blog-webhook"})
else:
self._json(404, {"error": "not found"})
def do_POST(self) -> None:
if self.path != "/hook":
self._json(404, {"error": "not found"})
return
length = int(self.headers.get("Content-Length", 0))
payload = self.rfile.read(length)
sig = self.headers.get("X-Gitea-Signature", "")
expected = hmac.new(read_secret(), payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
self._json(401, {"error": "bad signature"})
return
try:
ref = json.loads(payload).get("ref", "")
except json.JSONDecodeError:
self._json(400, {"error": "bad json"})
return
if not ref.endswith("/main"):
self._json(200, {"ignored": ref})
return
if BUILD_LOCK.locked():
self._json(202, {"queued": False, "reason": "build in progress"})
return
threading.Thread(target=self._run_build, daemon=True).start()
self._json(202, {"queued": True})
def _run_build(self) -> None:
with BUILD_LOCK:
# 输出落盘:capture_output 丢弃是调试黑洞(2026-10-26 ESA 522
# 事件中构建静默死、staging 冻结无任何可见痕迹)
log = Path.home() / "docker_apps/blog/build.log"
with open(log, "wb") as fh:
subprocess.run(
["bash", str(build_script())],
stdout=fh, stderr=subprocess.STDOUT, timeout=1200,
cwd=str(BUILD_REPO),
)
def log_message(self, fmt: str, *args: object) -> None:
# 静默常规访问日志(健康探针会刷屏);错误仍走默认 stderr
if "healthz" in (fmt % args if args else fmt):
return
super().log_message(fmt, *args)
if __name__ == "__main__":
ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()