|
@@ -0,0 +1,138 @@
|
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
|
+"""
|
|
|
|
|
+Webhook 部署脚本
|
|
|
|
|
+接收 Gogs/GitHub webhook 请求,自动执行部署
|
|
|
|
|
+
|
|
|
|
|
+用法:
|
|
|
|
|
+1. 访问 https://book.rrbrr.com/webhook-deploy.py?secret=YOUR_SECRET
|
|
|
|
|
+2. 或使用 curl: curl -X POST https://book.rrbrr.com/webhook-deploy.py -d "secret=YOUR_SECRET"
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+import os
|
|
|
|
|
+import sys
|
|
|
|
|
+import hashlib
|
|
|
|
|
+import hmac
|
|
|
|
|
+import subprocess
|
|
|
|
|
+from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
|
|
|
+import json
|
|
|
|
|
+
|
|
|
|
|
+# 配置
|
|
|
|
|
+WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET', 'your-secret-key-change-me')
|
|
|
|
|
+DEPLOY_SCRIPT = '/data/ai/deploy.sh'
|
|
|
|
|
+LOG_FILE = '/tmp/webhook-deploy.log'
|
|
|
|
|
+
|
|
|
|
|
+def log(msg):
|
|
|
|
|
+ """写入日志"""
|
|
|
|
|
+ timestamp = subprocess.check_output(['date', '+%Y-%m-%d %H:%M:%S']).decode().strip()
|
|
|
|
|
+ with open(LOG_FILE, 'a') as f:
|
|
|
|
|
+ f.write(f"[{timestamp}] {msg}\n")
|
|
|
|
|
+ print(f"[{timestamp}] {msg}")
|
|
|
|
|
+
|
|
|
|
|
+def verify_signature(secret, payload, signature):
|
|
|
|
|
+ """验证请求签名"""
|
|
|
|
|
+ if not signature:
|
|
|
|
|
+ return False
|
|
|
|
|
+ mac = hmac.new(secret.encode(), payload, hashlib.sha256)
|
|
|
|
|
+ return mac.hexdigest() == signature.replace('sha256=', '')
|
|
|
|
|
+
|
|
|
|
|
+def execute_deploy():
|
|
|
|
|
+ """执行部署脚本"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ log("开始执行部署...")
|
|
|
|
|
+
|
|
|
|
|
+ # 切换到项目目录
|
|
|
|
|
+ os.chdir('/data/ai/audio_codebuddy')
|
|
|
|
|
+
|
|
|
|
|
+ # Git pull
|
|
|
|
|
+ log("执行 git pull...")
|
|
|
|
|
+ result = subprocess.run(['git', 'pull', 'origin', 'master'],
|
|
|
|
|
+ capture_output=True, text=True, timeout=60)
|
|
|
|
|
+ log(f"Git pull 结果: {result.returncode}")
|
|
|
|
|
+ if result.stdout:
|
|
|
|
|
+ log(f"stdout: {result.stdout}")
|
|
|
|
|
+ if result.stderr:
|
|
|
|
|
+ log(f"stderr: {result.stderr}")
|
|
|
|
|
+
|
|
|
|
|
+ # 执行部署脚本
|
|
|
|
|
+ log("执行部署脚本...")
|
|
|
|
|
+ result = subprocess.run(['bash', DEPLOY_SCRIPT],
|
|
|
|
|
+ capture_output=True, text=True, timeout=600)
|
|
|
|
|
+ log(f"部署脚本返回: {result.returncode}")
|
|
|
|
|
+ if result.stdout:
|
|
|
|
|
+ log(f"stdout: {result.stdout[-2000:]}") # 只保留最后2000字符
|
|
|
|
|
+ if result.stderr:
|
|
|
|
|
+ log(f"stderr: {result.stderr[-2000:]}")
|
|
|
|
|
+
|
|
|
|
|
+ if result.returncode == 0:
|
|
|
|
|
+ log("部署成功!")
|
|
|
|
|
+ return True
|
|
|
|
|
+ else:
|
|
|
|
|
+ log(f"部署失败! 返回码: {result.returncode}")
|
|
|
|
|
+ return False
|
|
|
|
|
+ except subprocess.TimeoutExpired:
|
|
|
|
|
+ log("部署超时!")
|
|
|
|
|
+ return False
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ log(f"部署异常: {e}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+class WebhookHandler(BaseHTTPRequestHandler):
|
|
|
|
|
+ def do_GET(self):
|
|
|
|
|
+ """处理 GET 请求(健康检查)"""
|
|
|
|
|
+ self.send_response(200)
|
|
|
|
|
+ self.send_header('Content-type', 'text/plain')
|
|
|
|
|
+ self.end_headers()
|
|
|
|
|
+ self.wfile.write(b'Webhook is running!')
|
|
|
|
|
+
|
|
|
|
|
+ def do_POST(self):
|
|
|
|
|
+ """处理 POST 请求(webhook)"""
|
|
|
|
|
+ # 读取请求体
|
|
|
|
|
+ content_length = int(self.headers.get('Content-Length', 0))
|
|
|
|
|
+ body = self.rfile.read(content_length)
|
|
|
|
|
+
|
|
|
|
|
+ # 获取签名
|
|
|
|
|
+ signature = self.headers.get('X-Hub-Signature-256') or \
|
|
|
|
|
+ self.headers.get('X-Gogs-Signature') or \
|
|
|
|
|
+ self.headers.get('X-Gitea-Signature')
|
|
|
|
|
+
|
|
|
|
|
+ # 验证签名(如果有 secret)
|
|
|
|
|
+ if WEBHOOK_SECRET != 'your-secret-key-change-me':
|
|
|
|
|
+ if not verify_signature(WEBHOOK_SECRET, body, signature):
|
|
|
|
|
+ log("签名验证失败!")
|
|
|
|
|
+ self.send_response(401)
|
|
|
|
|
+ self.send_header('Content-type', 'application/json')
|
|
|
|
|
+ self.end_headers()
|
|
|
|
|
+ self.wfile.write(json.dumps({'error': 'Invalid signature'}).encode())
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # 解析 payload
|
|
|
|
|
+ try:
|
|
|
|
|
+ payload = json.loads(body)
|
|
|
|
|
+ log(f"收到 webhook: {payload.get('ref', 'unknown')}")
|
|
|
|
|
+ except:
|
|
|
|
|
+ log(f"无法解析 payload: {body[:200]}")
|
|
|
|
|
+ payload = {}
|
|
|
|
|
+
|
|
|
|
|
+ # 执行部署
|
|
|
|
|
+ success = execute_deploy()
|
|
|
|
|
+
|
|
|
|
|
+ # 返回结果
|
|
|
|
|
+ self.send_response(200 if success else 500)
|
|
|
|
|
+ self.send_header('Content-type', 'application/json')
|
|
|
|
|
+ self.end_headers()
|
|
|
|
|
+ result = {'success': success, 'message': 'Deployment completed'}
|
|
|
|
|
+ self.wfile.write(json.dumps(result).encode())
|
|
|
|
|
+
|
|
|
|
|
+ def log_message(self, format, *args):
|
|
|
|
|
+ """禁用默认日志"""
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+def main():
|
|
|
|
|
+ port = int(os.environ.get('WEBHOOK_PORT', 8080))
|
|
|
|
|
+ server = HTTPServer(('0.0.0.0', port), WebhookHandler)
|
|
|
|
|
+ log(f"Webhook 服务启动在端口 {port}")
|
|
|
|
|
+ print(f"Webhook 服务启动在端口 {port}")
|
|
|
|
|
+ server.serve_forever()
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == '__main__':
|
|
|
|
|
+ main()
|