# OSVx MCP — CI First Run (Gitea Webhook) This is a minimal, commands-only runbook to set up the Gitea webhook CI/CD for staging deploys. Assumes you are on the VM as `chris_christiansen` with the repo at `~/OSVauco`. ### 0. Prerequisites ```bash cd ~/OSVauco git status git rev-parse --abbrev-ref HEAD which python3 which gcloud gcloud config get-value project ``` **Ensure:** - You’re on `feat/osvx-mcp-full-catalog` - `gcloud` project is `propane-will-491900-m5` ### 1. Create the webhook listener script ```bash mkdir -p ci cat > ci/deploy-webhook-listener.py << 'EOF' #!/usr/bin/env python3 import hashlib import hmac import json import os import subprocess from http.server import BaseHTTPRequestHandler, HTTPServer PORT = int(os.getenv("WEBHOOK_PORT", "9999")) WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "") REPO_PATH = os.getenv("REPO_PATH", "/home/chris_christiansen/OSVauco") CI_BRANCH = os.getenv("CI_BRANCH", "feat/osvx-mcp-full-catalog") DEPLOY_SCRIPT = os.getenv("DEPLOY_SCRIPT", "./deploy-mcp.sh") DEPLOY_ENV = os.getenv("DEPLOY_ENV", "staging") def verify_signature(payload_bytes, signature_header): if not signature_header or not signature_header.startswith("sha256="): return False expected_sig = signature_header.split("=", 1)[1] computed = hmac.new( WEBHOOK_SECRET.encode("utf-8"), payload_bytes, hashlib.sha256, ).hexdigest() return hmac.compare_digest(computed, expected_sig) class WebhookHandler(BaseHTTPRequestHandler): def log_message(self, format, *args): print(f"[webhook] {args[0]}", flush=True) def do_POST(self): content_length = int(self.headers.get("Content-Length", 0)) payload_bytes = self.rfile.read(content_length) signature = self.headers.get("X-Gitea-Delivery", "") if WEBHOOK_SECRET and not verify_signature(payload_bytes, signature): self.send_response(401) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps({"error": "invalid signature"}).encode()) return try: payload = json.loads(payload_bytes.decode("utf-8")) except Exception: self.send_response(400) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps({"error": "invalid json"}).encode()) return ref = payload.get("ref", "") after = payload.get("after", "") if after == "0000000000000000000000000000000000000000": self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps({"status": "ignored deletion"}).encode()) return expected_ref = f"refs/heads/{CI_BRANCH}" if ref != expected_ref: self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write( json.dumps({"status": "ignored", "reason": "wrong branch", "ref": ref}).encode() ) return self.send_response(202) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write( json.dumps({ "status": "deploying", "branch": CI_BRANCH, "env": DEPLOY_ENV, }).encode() ) try: subprocess.run(["git", "-C", REPO_PATH, "pull"], check=True, capture_output=True, text=True) subprocess.run([DEPLOY_SCRIPT, DEPLOY_ENV], cwd=REPO_PATH, check=True) except Exception as e: print(f"[webhook] deploy failed: {e}", flush=True) def do_GET(self): self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps({"status": "ok", "service": "osvx-mcp-webhook"}).encode()) def main(): if not WEBHOOK_SECRET: print("WARNING: WEBHOOK_SECRET not set; webhook signature verification disabled.", flush=True) server = HTTPServer(("0.0.0.0", PORT), WebhookHandler) print(f"[webhook] listening on port {PORT}", flush=True) server.serve_forever() if __name__ == "__main__": main() EOF chmod +x ci/deploy-webhook-listener.py ``` ### 2. Generate webhook secret ```bash export WEBHOOK_SECRET="$(openssl rand -hex 16)" echo "WEBHOOK_SECRET=$WEBHOOK_SECRET" ``` Save this value; you’ll need it for Gitea and systemd. ### 3. Quick manual test (optional) ```bash export WEBHOOK_PORT=9999 export REPO_PATH="/home/chris_christiansen/OSVauco" export CI_BRANCH="feat/osvx-mcp-full-catalog" export DEPLOY_SCRIPT="./deploy-mcp.sh" export DEPLOY_ENV="staging" python3 ci/deploy-webhook-listener.py & sleep 1 curl http://127.0.0.1:9999 # Expect: {"status":"ok","service":"osvx-mcp-webhook"} kill %1 2>/dev/null || true ``` ### 4. Create systemd service ```bash sudo tee /etc/systemd/system/osvx-mcp-webhook.service > /dev/null << EOF [Unit] Description=OSVx MCP Gitea Webhook Listener After=network.target [Service] Type=simple User=chris_christiansen Group=chris_christiansen WorkingDirectory=/home/chris_christiansen/OSVauco Environment="WEBHOOK_SECRET=$WEBHOOK_SECRET" Environment="WEBHOOK_PORT=9999" Environment="REPO_PATH=/home/chris_christiansen/OSVauco" Environment="CI_BRANCH=feat/osvx-mcp-full-catalog" Environment="DEPLOY_SCRIPT=./deploy-mcp.sh" Environment="DEPLOY_ENV=staging" ExecStart=/usr/bin/python3 /home/chris_christiansen/OSVauco/ci/deploy-webhook-listener.py Restart=always StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target EOF ``` ### 5. Enable and start the service ```bash sudo systemctl daemon-reload sudo systemctl enable osvx-mcp-webhook sudo systemctl start osvx-mcp-webhook sudo systemctl status osvx-mcp-webhook --no-pager ``` Verify: ```bash curl http://127.0.0.1:9999 journalctl -u osvx-mcp-webhook -n 20 --no-pager ``` Expected health response: ```json {"status":"ok","service":"osvx-mcp-webhook"} ``` ### 6. Configure Gitea webhook In Gitea UI for `chris/osvauco`: - Settings → Webhooks → Add Webhook - Payload URL: `http://:9999/` - Secret: `$WEBHOOK_SECRET` (the value from step 2) - Events: “Push events” - Save Optionally restrict to branch `feat/osvx-mcp-full-catalog` if supported. ### 7. Test with a real push From your dev machine: ```bash cd ~/OSVauco git add . git commit -m "ci: test webhook deploy" git push origin feat/osvx-mcp-full-catalog ``` On the VM, watch logs: ```bash journalctl -u osvx-mcp-webhook -f ``` You should see: - Webhook received - `git pull` - `./deploy-mcp.sh staging` running Then verify in GCP: ```bash gcloud run services describe osvx-mcp-staging --project=propane-will-491900-m5 --region=us-central1 ``` Check the last deployment timestamp and revision. ### 8. Manual prod deploy (after staging is validated) ```bash cd ~/OSVauco ./deploy-mcp.sh prod ``` Verify: ```bash gcloud run services describe osvx-mcp-prod --project=propane-will-491900-m5 --region=us-central1 ``` ### 9. Troubleshooting (quick) - **Health check fails:** ```bash systemctl status osvx-mcp-webhook --no-pager journalctl -u osvx-mcp-webhook -n 50 --no-pager ``` - **Webhook not received:** - Check Gitea webhook “Recent Deliveries”. - Ensure `http://:9999` is reachable from the Gitea server. - **Deploy fails:** - Run `./deploy-mcp.sh staging` manually to confirm it works. - Check GCP credentials and permissions. *This runbook implements the CI/CD strategy described in `CI_GITEA_WEBHOOK.md` and `PLATFORM_MAP.md`.*