CI/CD: webhook receiver + deploy automatico su push
- 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
This commit is contained in:
37
deploy.sh
Executable file
37
deploy.sh
Executable file
@@ -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"
|
||||||
@@ -26,6 +26,13 @@ server {
|
|||||||
alias /home/marco/olimpic_nastri/media/;
|
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 / {
|
location / {
|
||||||
proxy_pass http://unix:/run/olimpic_nastri/gunicorn.sock;
|
proxy_pass http://unix:/run/olimpic_nastri/gunicorn.sock;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
14
olimpic-nastri-webhook.service
Normal file
14
olimpic-nastri-webhook.service
Normal file
@@ -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
|
||||||
85
webhook_receiver.py
Normal file
85
webhook_receiver.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user