OSVauco/docs/CI_FIRST_RUN.md

7.6 KiB
Raw Permalink Blame History

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

cd ~/OSVauco
git status
git rev-parse --abbrev-ref HEAD
which python3
which gcloud
gcloud config get-value project

Ensure:

  • Youre on feat/osvx-mcp-full-catalog
  • gcloud project is propane-will-491900-m5

1. Create the webhook listener script

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

export WEBHOOK_SECRET="$(openssl rand -hex 16)"
echo "WEBHOOK_SECRET=$WEBHOOK_SECRET"

Save this value; youll need it for Gitea and systemd.

3. Quick manual test (optional)

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

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

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:

curl http://127.0.0.1:9999
journalctl -u osvx-mcp-webhook -n 20 --no-pager

Expected health response:

{"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:

cd ~/OSVauco
git add .
git commit -m "ci: test webhook deploy"
git push origin feat/osvx-mcp-full-catalog

On the VM, watch logs:

journalctl -u osvx-mcp-webhook -f

You should see:

  • Webhook received
  • git pull
  • ./deploy-mcp.sh staging running

Then verify in GCP:

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)

cd ~/OSVauco
./deploy-mcp.sh prod

Verify:

gcloud run services describe osvx-mcp-prod   --project=propane-will-491900-m5   --region=us-central1

9. Troubleshooting (quick)

  • Health check fails:
    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.