145 lines
4.0 KiB
Python
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 = 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()
|