Files
blog/infra/deploy/webhook_receiver.py
T
wait4xandGLM-5.3 810cf44d3d fix(deploy): receiver 改调构建仓自己的 build.sh——发布链执行源归一
- 架构债(20:31 事故根因层):receiver 此前执行工作树
  infra/deploy/build.sh,车道在途半成品状态可断生产链——改为
  build-repo/infra/deploy/build.sh(origin/main 硬跟版本),
  构建仓缺失时首克引导(匿名 clone)
- 双副本纪律入档头注:仓内源与 ~/docker_apps/blog/ 运行副本改一
  须同步二并 restart(systemd 跑的是运行副本,漂移即事故)
- 深意:构建脚本演化自此走 git 评审流,main=可部署铁律闭环

验证: 签名 POST 端到端实测(HMAC 构造无 git 污染):202 queued →
      90s 后 stamp=1fdd0cc;healthz 200;单元 active

Spec: none (发布链架构修正:单执行源切换+端到端实测)

Co-authored-by: GLM-5.3
2026-10-25 21:12:48 +08:00

103 lines
3.9 KiB
Python
Raw 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:
subprocess.run(
["bash", str(build_script())],
capture_output=True, 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()