#!/usr/bin/env python3 import hashlib import hmac import json import os import subprocess from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer HOST = os.getenv("WEBHOOK_BIND_HOST", "127.0.0.1") PORT = int(os.getenv("WEBHOOK_BIND_PORT", "8788")) PATH = os.getenv("WEBHOOK_PATH", "/hooks/gitea/project-velocity") SECRET = os.getenv("GITEA_WEBHOOK_SECRET", "") EXPECTED_REF = os.getenv("GITEA_EXPECTED_REF", "refs/heads/main") EXPECTED_REPO = os.getenv("GITEA_REPO_FULL_NAME", "sagnik/Project_Velocity") LOCK_FILE = os.getenv("DEPLOY_LOCK_FILE", "/tmp/desineuron-velocity-deploy.lock") DEPLOY_CMD = os.getenv("DEPLOY_COMMAND", "/usr/local/bin/deploy_velocity_site.sh") def verify_signature(secret: str, body: bytes, signature: str) -> bool: if not secret: return False digest = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest() return hmac.compare_digest(digest, signature or "") class Handler(BaseHTTPRequestHandler): server_version = "VelocityWebhook/1.0" def _respond(self, code: int, payload: dict) -> None: body = json.dumps(payload).encode("utf-8") self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, fmt: str, *args) -> None: print("%s - - [%s] %s" % (self.address_string(), self.log_date_time_string(), fmt % args)) def do_GET(self) -> None: if self.path == "/health": self._respond(200, {"ok": True, "service": "desineuron-velocity-gitea-webhook"}) return self._respond(404, {"error": "not_found"}) def do_POST(self) -> None: if self.path != PATH: self._respond(404, {"error": "not_found"}) return length = int(self.headers.get("Content-Length", "0")) body = self.rfile.read(length) signature = self.headers.get("X-Gitea-Signature", "") event = self.headers.get("X-Gitea-Event", "") if not verify_signature(SECRET, body, signature): self._respond(401, {"error": "invalid_signature"}) return try: payload = json.loads(body.decode("utf-8")) except json.JSONDecodeError: self._respond(400, {"error": "invalid_json"}) return if event == "ping": self._respond(200, {"ok": True, "message": "ping_received"}) return if event != "push": self._respond(202, {"ok": True, "ignored": True, "reason": f"unsupported_event:{event}"}) return ref = payload.get("ref") repo_full_name = ((payload.get("repository") or {}).get("full_name")) or "" if ref != EXPECTED_REF: self._respond(202, {"ok": True, "ignored": True, "reason": f"unexpected_ref:{ref}"}) return if EXPECTED_REPO and repo_full_name != EXPECTED_REPO: self._respond(202, {"ok": True, "ignored": True, "reason": f"unexpected_repo:{repo_full_name}"}) return cmd = ["/usr/bin/flock", "-n", LOCK_FILE, "/bin/bash", "-lc", DEPLOY_CMD] proc = subprocess.run(cmd, capture_output=True, text=True) if proc.returncode == 0: self._respond( 202, { "ok": True, "deployed": True, "ref": ref, "repository": repo_full_name, "stdout_tail": proc.stdout.strip().splitlines()[-10:], }, ) return self._respond( 500, { "ok": False, "deployed": False, "ref": ref, "repository": repo_full_name, "returncode": proc.returncode, "stdout_tail": proc.stdout.strip().splitlines()[-10:], "stderr_tail": proc.stderr.strip().splitlines()[-10:], }, ) def main() -> None: server = ThreadingHTTPServer((HOST, PORT), Handler) print(f"Velocity Gitea webhook listening on http://{HOST}:{PORT}{PATH}") server.serve_forever() if __name__ == "__main__": main()