- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
165 lines
4.8 KiB
Python
Executable File
165 lines
4.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
GitHub Webhook处理器
|
||
监听GitHub推送事件,自动触发部署
|
||
"""
|
||
|
||
import os
|
||
import subprocess
|
||
import json
|
||
import hmac
|
||
import hashlib
|
||
import logging
|
||
from flask import Flask, request, jsonify
|
||
import threading
|
||
import time
|
||
|
||
app = Flask(__name__)
|
||
|
||
# 配置
|
||
WEBHOOK_SECRET = "kaopeilian-webhook-secret-2025" # GitHub中配置的密钥
|
||
PROJECT_DIR = "/root/aiedu"
|
||
UPDATE_SCRIPT = "/root/aiedu/scripts/auto_update.sh"
|
||
|
||
# 配置日志
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||
handlers=[
|
||
logging.FileHandler('/var/log/kaopeilian_webhook.log'),
|
||
logging.StreamHandler()
|
||
]
|
||
)
|
||
|
||
def verify_signature(payload_body, signature_header):
|
||
"""验证GitHub Webhook签名"""
|
||
if not signature_header:
|
||
return False
|
||
|
||
hash_object = hmac.new(
|
||
WEBHOOK_SECRET.encode('utf-8'),
|
||
payload_body,
|
||
hashlib.sha256
|
||
)
|
||
expected_signature = "sha256=" + hash_object.hexdigest()
|
||
|
||
return hmac.compare_digest(expected_signature, signature_header)
|
||
|
||
def run_update_async():
|
||
"""异步执行更新脚本"""
|
||
try:
|
||
# 等待5秒再执行,避免GitHub推送过程中的竞争条件
|
||
time.sleep(5)
|
||
|
||
result = subprocess.run(
|
||
[UPDATE_SCRIPT],
|
||
cwd=PROJECT_DIR,
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=600 # 10分钟超时
|
||
)
|
||
|
||
if result.returncode == 0:
|
||
logging.info(f"Update completed successfully: {result.stdout}")
|
||
else:
|
||
logging.error(f"Update failed: {result.stderr}")
|
||
|
||
except subprocess.TimeoutExpired:
|
||
logging.error("Update script timed out")
|
||
except Exception as e:
|
||
logging.error(f"Error running update script: {e}")
|
||
|
||
@app.route('/webhook', methods=['POST'])
|
||
def github_webhook():
|
||
"""处理GitHub Webhook请求"""
|
||
|
||
# 验证签名
|
||
signature = request.headers.get('X-Hub-Signature-256')
|
||
if not verify_signature(request.data, signature):
|
||
logging.warning("Invalid webhook signature")
|
||
return jsonify({"error": "Invalid signature"}), 403
|
||
|
||
# 解析请求
|
||
try:
|
||
payload = request.get_json()
|
||
except Exception as e:
|
||
logging.error(f"Failed to parse JSON: {e}")
|
||
return jsonify({"error": "Invalid JSON"}), 400
|
||
|
||
# 检查事件类型
|
||
event_type = request.headers.get('X-GitHub-Event')
|
||
if event_type != 'push':
|
||
logging.info(f"Ignoring event type: {event_type}")
|
||
return jsonify({"message": "Event ignored"}), 200
|
||
|
||
# 检查分支
|
||
ref = payload.get('ref', '')
|
||
if ref != 'refs/heads/production':
|
||
logging.info(f"Ignoring push to branch: {ref}")
|
||
return jsonify({"message": "Branch ignored"}), 200
|
||
|
||
# 获取提交信息
|
||
commit_info = {
|
||
'id': payload.get('after', 'unknown'),
|
||
'message': payload.get('head_commit', {}).get('message', 'No message'),
|
||
'author': payload.get('head_commit', {}).get('author', {}).get('name', 'Unknown'),
|
||
'timestamp': payload.get('head_commit', {}).get('timestamp', 'Unknown')
|
||
}
|
||
|
||
logging.info(f"Received push event: {commit_info}")
|
||
|
||
# 异步触发更新
|
||
update_thread = threading.Thread(target=run_update_async)
|
||
update_thread.daemon = True
|
||
update_thread.start()
|
||
|
||
return jsonify({
|
||
"message": "Update triggered successfully",
|
||
"commit": commit_info
|
||
}), 200
|
||
|
||
@app.route('/health', methods=['GET'])
|
||
def health_check():
|
||
"""健康检查端点"""
|
||
return jsonify({
|
||
"status": "healthy",
|
||
"service": "kaopeilian-webhook",
|
||
"timestamp": time.time()
|
||
}), 200
|
||
|
||
@app.route('/status', methods=['GET'])
|
||
def status():
|
||
"""状态检查端点"""
|
||
try:
|
||
# 检查项目目录
|
||
project_exists = os.path.exists(PROJECT_DIR)
|
||
|
||
# 检查更新脚本
|
||
script_exists = os.path.exists(UPDATE_SCRIPT)
|
||
script_executable = os.access(UPDATE_SCRIPT, os.X_OK) if script_exists else False
|
||
|
||
# 检查Docker服务
|
||
docker_result = subprocess.run(['docker', 'compose', 'ps'],
|
||
cwd=PROJECT_DIR, capture_output=True)
|
||
docker_running = docker_result.returncode == 0
|
||
|
||
return jsonify({
|
||
"status": "ok",
|
||
"checks": {
|
||
"project_directory": project_exists,
|
||
"update_script_exists": script_exists,
|
||
"update_script_executable": script_executable,
|
||
"docker_compose_running": docker_running
|
||
}
|
||
}), 200
|
||
|
||
except Exception as e:
|
||
return jsonify({
|
||
"status": "error",
|
||
"error": str(e)
|
||
}), 500
|
||
|
||
if __name__ == '__main__':
|
||
logging.info("Starting GitHub Webhook handler...")
|
||
app.run(host='0.0.0.0', port=9000, debug=False)
|