Files
startover/deploy/webhook-receiver.py
T
Johngreen bd48cafcc9 ci: IDC 서버 자동배포 파이프라인 구축
- Dockerfile: Turborepo 멀티스테이지 빌드 (Next.js standalone)
- docker-compose.prod.yml: PostgreSQL/Redis/Nginx/Web/Admin 프로덕션 스택
- deploy/poll-deploy.sh: cron 기반 자동배포 (매분 Gitea 폴링)
- deploy/nginx/default.conf: 리버스 프록시 설정
- next.config.ts: output standalone 추가
- .env.production.example: 환경변수 템플릿

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 22:28:09 +09:00

116 lines
3.5 KiB
Python

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