Files
Project_Velocity/infrastructure/desineuron_ingress/gitea_velocity_webhook_receiver.py

123 lines
4.2 KiB
Python

#!/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()