RuoYi/deploy/webhook.py

145 lines
4.0 KiB
Python

#!/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 = 9000
# Gitee Webhook 密码(请在 Gitee 仓库设置中配置相同的密钥)
WEBHOOK_SECRET = "ruoyi-deploy-2024"
# 部署脚本路径
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()