157 lines
4.7 KiB
Python
157 lines
4.7 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
智队中枢 Webhook 服务
|
|||
|
|
接收 Gitea Webhook 请求,触发自动更新
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import hmac
|
|||
|
|
import hashlib
|
|||
|
|
import subprocess
|
|||
|
|
import logging
|
|||
|
|
from flask import Flask, request, jsonify
|
|||
|
|
from datetime import datetime
|
|||
|
|
|
|||
|
|
app = Flask(__name__)
|
|||
|
|
|
|||
|
|
# 配置
|
|||
|
|
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET', 'pit-router-webhook-secret-2026')
|
|||
|
|
UPDATE_SCRIPT = '/www/wwwroot/pit-router/auto-update.sh'
|
|||
|
|
LOG_FILE = '/var/log/pit-router-webhook.log'
|
|||
|
|
|
|||
|
|
# 日志配置
|
|||
|
|
logging.basicConfig(
|
|||
|
|
level=logging.INFO,
|
|||
|
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
|||
|
|
handlers=[
|
|||
|
|
logging.FileHandler(LOG_FILE),
|
|||
|
|
logging.StreamHandler()
|
|||
|
|
]
|
|||
|
|
)
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def verify_signature(payload: bytes, signature: str) -> bool:
|
|||
|
|
"""验证 Gitea Webhook 签名"""
|
|||
|
|
if not signature:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
expected = 'sha256=' + hmac.new(
|
|||
|
|
WEBHOOK_SECRET.encode(),
|
|||
|
|
payload,
|
|||
|
|
hashlib.sha256
|
|||
|
|
).hexdigest()
|
|||
|
|
|
|||
|
|
return hmac.compare_digest(expected, signature)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def log_request(event: str, data: dict):
|
|||
|
|
"""记录请求日志"""
|
|||
|
|
ref = data.get('ref', '')
|
|||
|
|
commits = data.get('commits', [])
|
|||
|
|
pusher = data.get('pusher', {}).get('login', 'unknown')
|
|||
|
|
|
|||
|
|
logger.info(f"Event: {event} | Ref: {ref} | Pusher: {pusher} | Commits: {len(commits)}")
|
|||
|
|
|
|||
|
|
for commit in commits[:3]: # 只记录前3个 commit
|
|||
|
|
msg = commit.get('message', '').split('\n')[0][:50]
|
|||
|
|
logger.info(f" - {commit.get('id', '')[:7]}: {msg}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.route('/health', methods=['GET'])
|
|||
|
|
def health():
|
|||
|
|
"""健康检查端点"""
|
|||
|
|
return jsonify({'status': 'ok', 'service': 'pit-router-webhook'})
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.route('/webhook/pit-router', methods=['POST'])
|
|||
|
|
def handle_webhook():
|
|||
|
|
"""处理 Gitea Webhook"""
|
|||
|
|
try:
|
|||
|
|
# 1. 验证签名
|
|||
|
|
signature = request.headers.get('X-Gitea-Signature', '')
|
|||
|
|
if not verify_signature(request.data, signature):
|
|||
|
|
logger.warning("Invalid signature")
|
|||
|
|
return jsonify({'error': 'Invalid signature'}), 401
|
|||
|
|
|
|||
|
|
# 2. 解析事件
|
|||
|
|
event = request.headers.get('X-Gitea-Event', '')
|
|||
|
|
data = request.json or {}
|
|||
|
|
|
|||
|
|
# 记录请求
|
|||
|
|
log_request(event, data)
|
|||
|
|
|
|||
|
|
# 3. 只处理 push 事件
|
|||
|
|
if event != 'push':
|
|||
|
|
logger.info(f"Event '{event}' ignored")
|
|||
|
|
return jsonify({'message': f'Event {event} ignored'}), 200
|
|||
|
|
|
|||
|
|
# 4. 只处理 main 分支
|
|||
|
|
ref = data.get('ref', '')
|
|||
|
|
if ref != 'refs/heads/main':
|
|||
|
|
logger.info(f"Branch '{ref}' ignored")
|
|||
|
|
return jsonify({'message': f'Branch {ref} ignored'}), 200
|
|||
|
|
|
|||
|
|
# 5. 执行更新脚本
|
|||
|
|
logger.info("🚀 Triggering auto-update...")
|
|||
|
|
result = subprocess.run(
|
|||
|
|
[UPDATE_SCRIPT],
|
|||
|
|
capture_output=True,
|
|||
|
|
text=True,
|
|||
|
|
timeout=600 # 10 分钟超时
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 记录输出
|
|||
|
|
if result.stdout:
|
|||
|
|
logger.info(f"Update output:\n{result.stdout[-2000:]}") # 只记录最后 2000 字符
|
|||
|
|
if result.stderr:
|
|||
|
|
logger.error(f"Update error:\n{result.stderr[-1000:]}")
|
|||
|
|
|
|||
|
|
if result.returncode == 0:
|
|||
|
|
logger.info("✅ Update completed successfully")
|
|||
|
|
return jsonify({
|
|||
|
|
'status': 'success',
|
|||
|
|
'message': 'Update completed',
|
|||
|
|
'timestamp': datetime.utcnow().isoformat()
|
|||
|
|
})
|
|||
|
|
else:
|
|||
|
|
logger.error(f"❌ Update failed with code {result.returncode}")
|
|||
|
|
return jsonify({
|
|||
|
|
'status': 'error',
|
|||
|
|
'message': 'Update failed',
|
|||
|
|
'details': result.stderr[-500:] if result.stderr else 'Unknown error'
|
|||
|
|
}), 500
|
|||
|
|
|
|||
|
|
except subprocess.TimeoutExpired:
|
|||
|
|
logger.error("Update timeout")
|
|||
|
|
return jsonify({'error': 'Update timeout'}), 500
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.exception(f"Unexpected error: {e}")
|
|||
|
|
return jsonify({'error': str(e)}), 500
|
|||
|
|
|
|||
|
|
|
|||
|
|
@app.route('/webhook/test', methods=['GET'])
|
|||
|
|
def test_webhook():
|
|||
|
|
"""测试端点(手动触发更新)"""
|
|||
|
|
try:
|
|||
|
|
logger.info("🧪 Manual test trigger")
|
|||
|
|
result = subprocess.run(
|
|||
|
|
[UPDATE_SCRIPT],
|
|||
|
|
capture_output=True,
|
|||
|
|
text=True,
|
|||
|
|
timeout=600
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return jsonify({
|
|||
|
|
'status': 'success' if result.returncode == 0 else 'error',
|
|||
|
|
'output': result.stdout[-1000:],
|
|||
|
|
'error': result.stderr[-500:] if result.stderr else None
|
|||
|
|
})
|
|||
|
|
except Exception as e:
|
|||
|
|
return jsonify({'error': str(e)}), 500
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
logger.info("🚀 Starting PIT Router Webhook Server on port 5001")
|
|||
|
|
app.run(host='0.0.0.0', port=5001, debug=False)
|