import asyncio import hashlib import hmac import logging from fastapi import APIRouter, HTTPException, Request from config import settings logger = logging.getLogger("admin.deploy") router = APIRouter(prefix="/api/admin", tags=["admin"]) @router.post("/deploy") async def deploy(request: Request): """Gitea webhook endpoint — pulls latest code and rebuilds Docker containers. Gitea webhook configuration: URL: https:///api/admin/deploy Secret token: value of DEPLOY_SECRET env var Content-Type: application/json Trigger: Push events only (branch: main) Add to VPS .env: DEPLOY_SECRET= DEPLOY_PROJECT_PATH=/home/bellsystems/bellsystems-cp """ if not settings.deploy_secret: raise HTTPException(status_code=503, detail="Deploy secret not configured on server") # Gitea sends the HMAC-SHA256 of the request body in X-Gitea-Signature sig_header = request.headers.get("X-Gitea-Signature", "") body = await request.body() expected_sig = hmac.new( key=settings.deploy_secret.encode(), msg=body, digestmod=hashlib.sha256, ).hexdigest() if not hmac.compare_digest(sig_header, expected_sig): raise HTTPException(status_code=403, detail="Invalid webhook signature") logger.info("Auto-deploy triggered via Gitea webhook") project_path = settings.deploy_project_path # Write a deploy script to the host filesystem (via the mounted project path) # then execute it with nsenter into the host's PID namespace so it runs as # a host process — not a container child — and survives container restarts. script_path = f"{project_path}/deploy.sh" log_path = f"{project_path}/deploy.log" script = ( f"#!/bin/sh\n" f"exec > {log_path} 2>&1\n" f"echo \"Deploy started at $(date)\"\n" f"git config --global --add safe.directory {project_path}\n" f"cd {project_path}\n" f"git fetch origin main\n" f"git reset --hard origin/main\n" f"docker-compose up -d --build\n" f"echo \"Deploy finished at $(date)\"\n" ) with open(script_path, "w") as f: f.write(script) # nsenter into host PID namespace (PID 1 = host init) so the process # is owned by the host and survives this container restarting. trigger_cmd = f"chmod +x {script_path} && nsenter -t 1 -m -u -i -n -p -- sh -c 'nohup {script_path} &'" await asyncio.create_subprocess_shell( trigger_cmd, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL, ) logger.info("Auto-deploy triggered on host via nsenter") return {"ok": True, "message": "Deploy started"}