#!/usr/bin/env python3 """ RuoYi Gitee Webhook 接收服务 接收 Gitee 推送事件,触发自动部署。 启动: python3 webhook.py """ import hashlib import hmac import json import subprocess import time from http.server import HTTPServer, BaseHTTPRequestHandler from datetime import datetime # ============ 配置 ============ LISTEN_HOST = "0.0.0.0" LISTEN_PORT = 9999 # Gitee Webhook 密码(请在 Gitee 仓库设置中配置相同的密钥) WEBHOOK_SECRET = "ruoyi-auto-deploy" # 部署脚本路径 DEPLOY_SCRIPT = "/opt/ruoyi/deploy.sh" # 部署锁文件,防止并发部署 LOCK_FILE = "/tmp/ruoyi-deploy.lock" # ============ 日志 ============ def log(msg): timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") line = f"[{timestamp}] [WEBHOOK] {msg}" print(line, flush=True) class WebhookHandler(BaseHTTPRequestHandler): def do_POST(self): if self.path != "/webhook": self.send_response(404) self.end_headers() return # 读取请求体 content_length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(content_length) # 验证签名 signature = self.headers.get("X-Gitee-Token", "") if signature != WEBHOOK_SECRET: log(f"签名验证失败: {signature[:20]}...") self.send_response(403) self.end_headers() self.wfile.write(b"Forbidden") return # 解析事件 try: data = json.loads(body) except json.JSONDecodeError: log("JSON 解析失败") self.send_response(400) self.end_headers() return event = self.headers.get("X-Gitee-Event", "unknown") ref = data.get("ref", "") pusher = data.get("pusher", {}).get("name", "unknown") log(f"收到事件: {event} | 分支: {ref} | 推送者: {pusher}") # 只处理 master 分支的 push 事件 if event != "push_hooks" or ref != "refs/heads/master": log(f"忽略事件: {event} / {ref}") self.send_response(200) self.end_headers() self.wfile.write(b"Ignored") return # 检查锁 if self._is_locked(): log("部署正在进行中,忽略重复触发") self.send_response(200) self.end_headers() self.wfile.write(b"Deploy already in progress") return # 异步执行部署 self._lock() subprocess.Popen( ["/bin/bash", DEPLOY_SCRIPT], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) self.send_response(200) self.end_headers() self.wfile.write(b"Deploy started") def do_GET(self): """健康检查""" if self.path == "/health": self.send_response(200) self.end_headers() self.wfile.write(b"OK") else: self.send_response(404) self.end_headers() def _is_locked(self): try: with open(LOCK_FILE, "r") as f: lock_time = float(f.read().strip()) # 锁超过 30 分钟自动过期 if time.time() - lock_time > 1800: return False return True except FileNotFoundError: return False def _lock(self): with open(LOCK_FILE, "w") as f: f.write(str(time.time())) def log_message(self, format, *args): """重写默认日志,改为简洁格式""" log(f"{self.client_address[0]} - {format % args}") def main(): log(f"Webhook 服务启动在 http://{LISTEN_HOST}:{LISTEN_PORT}/webhook") log(f"密钥: {WEBHOOK_SECRET[:4]}****") server = HTTPServer((LISTEN_HOST, LISTEN_PORT), WebhookHandler) try: server.serve_forever() except KeyboardInterrupt: log("服务已停止") server.server_close() if __name__ == "__main__": main()