From 006bb24215e8fcab92ef7ae42f1f0a5a6847e145 Mon Sep 17 00:00:00 2001 From: automationkriz Date: Sun, 5 Apr 2026 15:02:25 +0000 Subject: [PATCH] CI/CD: webhook receiver + deploy automatico su push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deploy.sh: git pull, pip install, migrate, collectstatic, restart gunicorn - webhook_receiver.py: HTTP server con verifica HMAC-SHA256 Gitea - olimpic-nastri-webhook.service: systemd unit per il receiver - Nginx: aggiunto proxy /webhook/deploy → porta 9000 - sudoers: restart gunicorn senza password per deploy automatico --- deploy.sh | 37 +++++++++++++++ diario.olimpic.click.nginx | 7 +++ olimpic-nastri-webhook.service | 14 ++++++ webhook_receiver.py | 85 ++++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100755 deploy.sh create mode 100644 olimpic-nastri-webhook.service create mode 100644 webhook_receiver.py diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..fdf5d72 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Deploy automatico — Diario Conversazioni Olimpic Nastri +# Chiamato dal webhook receiver dopo ogni push su main + +set -e + +PROJECT_DIR="/home/marco/olimpic_nastri" +VENV="$PROJECT_DIR/nastrivenv/bin" +LOG="/home/marco/olimpic_nastri/deploy.log" + +echo "========================================" >> "$LOG" +echo "Deploy avviato: $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG" + +cd "$PROJECT_DIR" + +# Pull ultime modifiche +echo "[1/5] Git pull..." >> "$LOG" +git pull origin main >> "$LOG" 2>&1 + +# Installa eventuali nuove dipendenze +echo "[2/5] Pip install..." >> "$LOG" +"$VENV/pip" install -r requirements.txt --quiet >> "$LOG" 2>&1 + +# Applica migrazioni database +echo "[3/5] Migrazioni..." >> "$LOG" +"$VENV/python" manage.py migrate --noinput >> "$LOG" 2>&1 + +# Raccoglie file statici +echo "[4/5] Collectstatic..." >> "$LOG" +"$VENV/python" manage.py collectstatic --noinput >> "$LOG" 2>&1 + +# Riavvia Gunicorn +echo "[5/5] Restart Gunicorn..." >> "$LOG" +sudo systemctl restart olimpic-nastri-gunicorn.service >> "$LOG" 2>&1 + +echo "Deploy completato: $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG" +echo "========================================" >> "$LOG" diff --git a/diario.olimpic.click.nginx b/diario.olimpic.click.nginx index a46de34..949bce5 100644 --- a/diario.olimpic.click.nginx +++ b/diario.olimpic.click.nginx @@ -26,6 +26,13 @@ server { alias /home/marco/olimpic_nastri/media/; } + location /webhook/deploy { + proxy_pass http://127.0.0.1:9000/deploy; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + location / { proxy_pass http://unix:/run/olimpic_nastri/gunicorn.sock; proxy_set_header Host $host; diff --git a/olimpic-nastri-webhook.service b/olimpic-nastri-webhook.service new file mode 100644 index 0000000..fedb321 --- /dev/null +++ b/olimpic-nastri-webhook.service @@ -0,0 +1,14 @@ +[Unit] +Description=Webhook receiver per deploy automatico Diario Olimpic Nastri +After=network.target + +[Service] +User=marco +Group=www-data +WorkingDirectory=/home/marco/olimpic_nastri +ExecStart=/usr/bin/python3 /home/marco/olimpic_nastri/webhook_receiver.py +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/webhook_receiver.py b/webhook_receiver.py new file mode 100644 index 0000000..0e91883 --- /dev/null +++ b/webhook_receiver.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +""" +Webhook receiver per Gitea — Diario Conversazioni Olimpic Nastri. +Ascolta su porta 9000, verifica la firma HMAC del payload Gitea, +e lancia deploy.sh quando riceve un push sul branch main. +""" + +import hashlib +import hmac +import json +import subprocess +import sys +from http.server import HTTPServer, BaseHTTPRequestHandler + +WEBHOOK_SECRET = "c91dca86b9a87d9f25e07a63354da9f2469998f9" +DEPLOY_SCRIPT = "/home/marco/olimpic_nastri/deploy.sh" +LISTEN_PORT = 9000 + + +class WebhookHandler(BaseHTTPRequestHandler): + + def do_POST(self): + if self.path != "/deploy": + self.send_response(404) + self.end_headers() + return + + content_length = int(self.headers.get("Content-Length", 0)) + if content_length > 1_000_000: # max 1 MB payload + self.send_response(413) + self.end_headers() + return + + body = self.rfile.read(content_length) + + # Verifica firma HMAC-SHA256 di Gitea + signature = self.headers.get("X-Gitea-Signature", "") + expected = hmac.new( + WEBHOOK_SECRET.encode(), body, hashlib.sha256 + ).hexdigest() + + if not hmac.compare_digest(signature, expected): + self.send_response(403) + self.end_headers() + self.wfile.write(b"Firma non valida") + return + + # Controlla che sia un push su main + try: + payload = json.loads(body) + except json.JSONDecodeError: + self.send_response(400) + self.end_headers() + return + + ref = payload.get("ref", "") + if ref != "refs/heads/main": + self.send_response(200) + self.end_headers() + self.wfile.write(b"Push ignorato (non main)") + return + + # Lancia il deploy in background + subprocess.Popen([DEPLOY_SCRIPT], close_fds=True) + + self.send_response(200) + self.end_headers() + self.wfile.write(b"Deploy avviato") + + def log_message(self, format, *args): + print(f"[webhook] {args[0]}", flush=True) + + +def main(): + server = HTTPServer(("127.0.0.1", LISTEN_PORT), WebhookHandler) + print(f"Webhook receiver in ascolto su 127.0.0.1:{LISTEN_PORT}", flush=True) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + server.server_close() + + +if __name__ == "__main__": + main()