#!/usr/bin/env python3 """接收 GitHub push Webhook,并触发服务器上的固定部署脚本。""" import hmac import os import subprocess import threading from hashlib import sha256 from flask import Flask, jsonify, request DEFAULT_PROJECT_DIR = "/opt/weread-board/WeReadBoard" DEFAULT_DEPLOY_SCRIPT = "deploy.sh" DEFAULT_REPOSITORY = "KingHWJ/WeReadBoard" DEFAULT_BRANCH = "master" def create_app(config=None): app = Flask(__name__) app.config.from_mapping( WEBHOOK_SECRET=os.environ.get("GITHUB_WEBHOOK_SECRET", ""), REPOSITORY=os.environ.get("GITHUB_REPOSITORY", DEFAULT_REPOSITORY), BRANCH=DEFAULT_BRANCH, PROJECT_DIR=os.environ.get("DEPLOY_PROJECT_DIR", DEFAULT_PROJECT_DIR), DEPLOY_SCRIPT=os.environ.get("DEPLOY_SCRIPT", DEFAULT_DEPLOY_SCRIPT), ) if config: app.config.update(config) if not app.config["WEBHOOK_SECRET"]: raise RuntimeError("GITHUB_WEBHOOK_SECRET 未配置") if app.config["BRANCH"] != DEFAULT_BRANCH: raise RuntimeError("Webhook 只允许部署 master 分支") deployment_lock = threading.Lock() deployment_process = None def signature_valid(body, signature): if not signature or not signature.startswith("sha256="): return False expected = "sha256=" + hmac.new( app.config["WEBHOOK_SECRET"].encode("utf-8"), body, sha256 ).hexdigest() return hmac.compare_digest(expected, signature) def start_deployment(): nonlocal deployment_process with deployment_lock: if deployment_process is not None and deployment_process.poll() is None: return False env = os.environ.copy() env["DEPLOY_BRANCH"] = DEFAULT_BRANCH script = app.config["DEPLOY_SCRIPT"] if not os.path.isabs(script): script = os.path.join(app.config["PROJECT_DIR"], script) deployment_process = subprocess.Popen( [script], cwd=app.config["PROJECT_DIR"], env=env, start_new_session=True, ) return True @app.get("/health") def health(): return jsonify(ok=True) @app.post("/github/webhook") def github_webhook(): body = request.get_data(cache=True) if not signature_valid(body, request.headers.get("X-Hub-Signature-256", "")): return jsonify(error="签名无效"), 401 if request.headers.get("X-GitHub-Event") != "push": return jsonify(error="只接受 push 事件"), 202 payload = request.get_json(silent=True) or {} repository = (payload.get("repository") or {}).get("full_name") if repository != app.config["REPOSITORY"]: return jsonify(error="仓库不匹配"), 403 if payload.get("ref") != f"refs/heads/{app.config['BRANCH']}" or payload.get("deleted"): return jsonify(error="分支不匹配"), 202 if not start_deployment(): return jsonify(status="already_deploying"), 202 return jsonify(status="accepted"), 202 return app if __name__ == "__main__": application = create_app() application.run( host=os.environ.get("WEBHOOK_HOST", "127.0.0.1"), port=int(os.environ.get("WEBHOOK_PORT", "9001")), )