7f59b94dcf
Rebrand repository from "Re:Link" to "Startover" across the codebase. Updates include package names and scopes (@relink/* -> @startover/*), import paths, Next.js transpile settings, vitest name, UI text and docs, Dockerfile and CI/workflow names, deploy scripts and repo paths, and example/production env values. Also add auth-related env vars, an apps/web .env symlink, and small formatting/typing cleanups in several TSX/TS files and tests to accommodate the rename.
116 lines
3.5 KiB
Python
116 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Startover 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", "startover-deploy-secret")
|
|
DEPLOY_SCRIPT = os.path.expanduser("~/startover/deploy.sh")
|
|
DEPLOY_BRANCH = "main"
|
|
PORT = int(os.environ.get("WEBHOOK_PORT", "9000"))
|
|
LOG_FILE = os.path.expanduser("~/startover/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()
|