#!/usr/bin/env python3 """ Re:Link Gitea Webhook Receiver Listens for push events and triggers deployment. """ import hashlib import hmac import json import os import subprocess import sys from http.server import HTTPServer, BaseHTTPRequestHandler from datetime import datetime WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "relink-deploy-secret") DEPLOY_SCRIPT = os.path.expanduser("~/relink/deploy.sh") DEPLOY_BRANCH = "main" PORT = int(os.environ.get("WEBHOOK_PORT", "9000")) LOG_FILE = os.path.expanduser("~/relink/webhook.log") def log(msg: str): line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}" print(line, flush=True) with open(LOG_FILE, "a") as f: f.write(line + "\n") def verify_signature(payload: bytes, signature: str) -> bool: if not signature: return False expected = hmac.new( WEBHOOK_SECRET.encode(), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(f"sha256={expected}", signature) class WebhookHandler(BaseHTTPRequestHandler): def do_POST(self): if self.path != "/deploy": self.send_response(404) self.end_headers() return content_length = int(self.headers.get("Content-Length", 0)) payload = self.rfile.read(content_length) # Verify signature signature = self.headers.get("X-Gitea-Signature", "") if WEBHOOK_SECRET and not verify_signature(payload, signature): log("REJECTED: Invalid signature") self.send_response(403) self.end_headers() self.wfile.write(b"Invalid signature") return try: data = json.loads(payload) except json.JSONDecodeError: self.send_response(400) self.end_headers() return ref = data.get("ref", "") if ref != f"refs/heads/{DEPLOY_BRANCH}": log(f"SKIPPED: Push to {ref} (not {DEPLOY_BRANCH})") self.send_response(200) self.end_headers() self.wfile.write(b"Skipped: not target branch") return pusher = data.get("pusher", {}).get("login", "unknown") commits = len(data.get("commits", [])) log(f"DEPLOY TRIGGERED: {pusher} pushed {commits} commit(s) to {DEPLOY_BRANCH}") # Run deploy script asynchronously try: subprocess.Popen( ["bash", DEPLOY_SCRIPT], stdout=open(LOG_FILE, "a"), stderr=subprocess.STDOUT, start_new_session=True, ) self.send_response(200) self.end_headers() self.wfile.write(b"Deploy started") log("Deploy script launched") except Exception as e: log(f"ERROR: Failed to start deploy: {e}") self.send_response(500) self.end_headers() self.wfile.write(f"Deploy failed: {e}".encode()) def do_GET(self): if self.path == "/health": self.send_response(200) self.end_headers() self.wfile.write(b"ok") else: self.send_response(404) self.end_headers() def log_message(self, format, *args): pass # Suppress default logging if __name__ == "__main__": log(f"Webhook receiver starting on port {PORT}") server = HTTPServer(("0.0.0.0", PORT), WebhookHandler) try: server.serve_forever() except KeyboardInterrupt: log("Webhook receiver stopped") server.server_close()