diff --git a/src/cmdforge/registry/app.py b/src/cmdforge/registry/app.py index 0b358bb..abaf77b 100644 --- a/src/cmdforge/registry/app.py +++ b/src/cmdforge/registry/app.py @@ -1652,6 +1652,56 @@ def create_app() -> Flask: response.status_code = status return response + @app.route("/api/v1/webhook/deploy", methods=["POST"]) + def webhook_deploy() -> Response: + """Auto-deploy webhook triggered by Gitea on push to main.""" + import hmac + import subprocess + + secret = os.environ.get("CMDFORGE_DEPLOY_WEBHOOK_SECRET", "") + if not secret: + return error_response("UNAUTHORIZED", "Deploy webhook secret not configured", 401) + + # Verify Gitea signature (X-Gitea-Signature is HMAC-SHA256) + signature = request.headers.get("X-Gitea-Signature", "") + if not signature: + return error_response("UNAUTHORIZED", "Missing signature", 401) + + expected = hmac.new( + secret.encode(), + request.data, + hashlib.sha256 + ).hexdigest() + + if not hmac.compare_digest(signature, expected): + return error_response("UNAUTHORIZED", "Invalid signature", 401) + + # Parse payload to check branch + try: + payload = json.loads(request.data) + ref = payload.get("ref", "") + # Only deploy on push to main branch + if ref not in ("refs/heads/main", "refs/heads/master"): + return jsonify({"data": {"deployed": False, "reason": f"Ignoring ref {ref}"}}) + except (json.JSONDecodeError, KeyError): + pass # Proceed anyway if we can't parse + + # Run deploy in background (so we can respond before restart kills us) + deploy_script = """ + cd /srv/mergerfs/data_pool/home/rob/cmdforge-registry && \ + git pull origin main && \ + sleep 1 && \ + systemctl --user restart cmdforge-web + """ + subprocess.Popen( + ["bash", "-c", deploy_script], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, + ) + + return jsonify({"data": {"deployed": True, "message": "Deploy triggered"}}) + return app