273 lines
7.6 KiB
Markdown
273 lines
7.6 KiB
Markdown
# 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://<VM_IP>: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://<VM_IP>: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`.*
|