#!/usr/bin/env python3 """GUARDiA CI/CD Webhook 서버 (포트 9999) — 6개 repo 지원""" import http.server, subprocess, threading, json, hmac, hashlib, logging import os, urllib.request, base64 SECRET = b"zioinfo-deploy-2026" LOG = "/var/log/zioinfo/deploy.log" JENKINS_URL = "http://127.0.0.1:9080" JENKINS_USER = "admin" JENKINS_TOKEN = "Admin@2026!" ITSM_NOTIFY = "http://127.0.0.1:9001/api/messenger/webhook" logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", handlers=[logging.FileHandler(LOG), logging.StreamHandler()]) def notify_itsm(success: bool, msg: str): try: body = json.dumps({"event": "build_result", "room": "ops", "success": success, "result_summary": msg}).encode() req = urllib.request.Request( ITSM_NOTIFY, data=body, headers={"Content-Type": "application/json"}) urllib.request.urlopen(req, timeout=5) except Exception: pass def trigger_jenkins(job: str): try: cred = base64.b64encode(f"{JENKINS_USER}:{JENKINS_TOKEN}".encode()).decode() headers = {"Authorization": f"Basic {cred}"} crumb_req = urllib.request.Request( f"{JENKINS_URL}/crumbIssuer/api/json", headers=headers) crumb_data = json.loads(urllib.request.urlopen(crumb_req, timeout=5).read()) headers[crumb_data["crumbRequestField"]] = crumb_data["crumb"] build_req = urllib.request.Request( f"{JENKINS_URL}/job/{job}/build?token=gitea-build-2026", data=b"", headers=headers) urllib.request.urlopen(build_req, timeout=5) except Exception: pass def run_steps(repo: str, steps: list) -> bool: for name, cmd in steps: logging.info(f"[{repo}:{name}] 실행 중...") try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=300) if result.stdout.strip(): logging.info(f"[{repo}:{name}] 완료") if result.returncode != 0: logging.error(f"[{repo}:{name}] 실패: {(result.stdout + result.stderr)[:200]}") return False else: logging.info(f"[{repo}:{name}] 완료") except Exception as e: logging.error(f"[{repo}:{name}] 예외: {e}") return False return True class WebhookHandler(http.server.BaseHTTPRequestHandler): def do_POST(self): try: length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length) if length else b"" sig = self.headers.get("X-Gitea-Signature", "") if sig: expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest() if not hmac.compare_digest(sig, expected): self.send_response(403); self.end_headers(); return except Exception: body = b"" self.send_response(202) self.end_headers() self.wfile.write(b"Deploy queued") try: payload = json.loads(body) if body else {} except Exception: payload = {} repo = payload.get("repository", {}).get("name", "") branch = payload.get("ref", "").replace("refs/heads/", "") logging.info(f"Webhook 수신: repo={repo} branch={branch}") if not repo: return threading.Thread(target=self._deploy, args=(repo,), daemon=True).start() def _deploy(self, repo: str): logging.info(f"=== {repo} 배포 시작 ===") if repo == "zioinfo-web": SRC = "/opt/zioinfo/src" ok = run_steps(repo, [ ("git pull", ["bash", "-c", f"[ -d {SRC}/.git ] && git -C {SRC} fetch origin main && git -C {SRC} reset --hard origin/main" f" || git clone 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/zioinfo-web.git' {SRC}"]), ("npm build", ["bash", "-c", f"cd {SRC}/frontend && npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps && npm run build"]), ("copy to www", ["bash", "-c", f"cp -r {SRC}/backend/src/main/resources/static/. /var/www/zioinfo/ && echo 'copied'"]), ("mvn package", ["bash", "-c", f"cd {SRC}/backend && /usr/bin/mvn clean package -DskipTests -q"]), ("deploy jar", ["bash", "-c", f"cp {SRC}/backend/target/zioinfo-web-*.jar /opt/zioinfo/app/app.jar"]), ("restart", ["systemctl", "restart", "zioinfo"]), ("health check", ["bash", "-c", "sleep 5 && systemctl is-active zioinfo"]), ]) if ok: notify_itsm(True, "✅ zioinfo-web 배포 완료") trigger_jenkins("zioinfo-web") else: notify_itsm(False, "❌ zioinfo-web 빌드 실패") elif repo == "guardia-itsm": SRC = "/opt/guardia/src" ok = run_steps(repo, [ ("git pull", ["bash", "-c", f"[ -d {SRC}/.git ] && git -C {SRC} fetch origin main && git -C {SRC} reset --hard origin/main" f" || git clone 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/guardia-itsm.git' {SRC}"]), ("rsync", ["bash", "-c", f"rsync -a --exclude=__pycache__ --exclude=.git " f"--exclude=rpa_rules.json --exclude='*.pyc' " f"{SRC}/ /opt/guardia/app/"]), ("pip install", ["bash", "-c", "/opt/guardia/venv/bin/pip install -r /opt/guardia/app/requirements.txt -q"]), ("restart", ["systemctl", "restart", "guardia"]), ("health check", ["bash", "-c", "sleep 4 && systemctl is-active guardia"]), ]) if ok: notify_itsm(True, "✅ guardia-itsm 배포 완료") trigger_jenkins("guardia-itsm") else: notify_itsm(False, "❌ guardia-itsm 빌드 실패") elif repo == "guardia-manager": SRC = "/opt/manager/src" ok = run_steps(repo, [ ("git pull", ["bash", "-c", f"[ -d {SRC}/.git ] && git -C {SRC} fetch origin main && git -C {SRC} reset --hard origin/main" f" || git clone 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/guardia-manager.git' {SRC}"]), ("npm build", ["bash", "-c", f"cd {SRC}/frontend && npm ci 2>/dev/null || npm install && npm run build"]), ("copy dist", ["bash", "-c", f"cp -r {SRC}/frontend/dist/. /var/www/manager/ 2>/dev/null || " f"cp -r {SRC}/dist/. /var/www/manager/"]), ("restart", ["systemctl", "restart", "guardia-manager", "2>/dev/null", "||", "true"]), ]) if ok: notify_itsm(True, "✅ guardia-manager 배포 완료") trigger_jenkins("guardia-manager") else: notify_itsm(False, "❌ guardia-manager 빌드 실패") elif repo == "guardia-docs": SRC = "/opt/guardia-docs/src" ok = run_steps(repo, [ ("git pull", ["bash", "-c", f"[ -d {SRC}/.git ] && git -C {SRC} fetch origin main && git -C {SRC} reset --hard origin/main" f" || git clone 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/guardia-docs.git' {SRC}"]), ("copy docs", ["bash", "-c", f"mkdir -p /var/www/docs && rsync -a --delete {SRC}/ /var/www/docs/"]), ]) if ok: notify_itsm(True, "✅ guardia-docs 배포 완료") else: notify_itsm(False, "❌ guardia-docs 빌드 실패") elif repo == "guardia-messenger": trigger_jenkins("guardia-messenger") elif repo == "zioinfo-mail": SRC = "/opt/mail" ok = run_steps(repo, [ ("git pull", ["bash", "-c", f"[ -d {SRC}/src/.git ] && git -C {SRC}/src fetch origin main && git -C {SRC}/src reset --hard origin/main" f" || git clone 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/zioinfo-mail.git' {SRC}/src"]), ("npm build", ["bash", "-c", f"cd {SRC}/src/frontend && npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps && npm run build"]), ("copy dist", ["bash", "-c", f"mkdir -p /var/www/mail && cp -r {SRC}/src/dist/. /var/www/mail/"]), ("pip install", ["bash", "-c", f"{SRC}/venv/bin/pip install -r {SRC}/src/backend/requirements.txt -q"]), ("rsync", ["bash", "-c", f"rsync -a --exclude=__pycache__ --exclude=.git --exclude='*.pyc' --exclude='.env' {SRC}/src/backend/ {SRC}/backend/"]), ("restart", ["systemctl", "restart", "zioinfo-mail"]), ("health check", ["bash", "-c", "sleep 4 && curl -sf http://localhost:8026/health"]), ]) if ok: notify_itsm(True, "✅ zioinfo-mail 배포 완료") trigger_jenkins("zioinfo-mail") else: notify_itsm(False, "❌ zioinfo-mail 빌드 실패") logging.info(f"=== {repo} 배포 완료 ===") def log_message(self, fmt, *args): logging.info(fmt % args) if __name__ == "__main__": os.makedirs("/var/log/zioinfo", exist_ok=True) logging.info("GUARDiA CI/CD Webhook 서버 시작 (포트 9999) — 6개 repo 지원") server = http.server.HTTPServer(("0.0.0.0", 9999), WebhookHandler) server.serve_forever()