alpinux.presentations/deploy/webhook.py
Cédrix 02cf82447b Créer le site des présentations d'Alpinux
Un dossier par présentation en Markdown, converti en diaporama HTML autonome
par Marp CLI, et une page d'accueil générée qui les liste toutes.

- scripts/build.mjs : build strict (fiche validée, images vérifiées, aucune
  ressource externe hormis le logo), page d'accueil filtrable,
  presentations.json et .htaccess pour l'en-tête CORS
- theme/alpinux.css : thème commun pour la vidéoprojection, classes titre et demo
- slides/_modele : modèle commenté, non publié
- slides/pdf-signer : « Lire, remplir et signer ses PDF », avec notes d'orateur
- scripts/deploy-presentations.sh et deploy/ : déploiement par webhook Gitea,
  sur le modèle du wiki et de www

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M713yNBL8cssBA1Zri5QLT
2026-10-10 21:44:32 +02:00

68 lines
2.5 KiB
Python

#!/usr/bin/env python3
"""Service d'écoute du webhook Gitea pour presentations.alpinux.org.
N'écoute que sur la boucle locale : c'est Apache qui lui transmet les requêtes reçues
sur https://presentations.alpinux.org/deploy. La signature HMAC envoyée par Gitea est
vérifiée avant de lancer quoi que ce soit, et seuls les push sur main déclenchent un
déploiement.
Calqué sur les services du wiki et de www (/opt/wiki-mkdocs/webhook.py,
/opt/www-alpinux/webhook.py), à trois différences près : le port, le script appelé,
et un secret qui lui est propre, lu dans un fichier d'environnement — ce dépôt est
public.
"""
import hashlib
import hmac
import http.server
import json
import os
import subprocess
import threading
SECRET = os.environ.get("PRESENTATIONS_WEBHOOK_SECRET", "")
DEPLOY_SCRIPT = "/opt/presentations-alpinux/deploy-presentations.sh"
LOG = "/var/log/presentations-deploy.log"
PORT = 9878
class Handler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
if self.path != "/deploy":
self.send_response(404); self.end_headers(); return
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
signature = self.headers.get("X-Gitea-Signature", "")
attendue = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
if not SECRET or not hmac.compare_digest(attendue, signature):
self.send_response(403); self.end_headers()
self.wfile.write(b"Forbidden"); return
try:
ref = json.loads(body).get("ref", "")
except (ValueError, AttributeError):
self.send_response(400); self.end_headers()
self.wfile.write(b"Bad request"); return
if ref != "refs/heads/main":
self.send_response(200); self.end_headers()
self.wfile.write(b"Skipped: not main"); return
# Déploiement en tâche de fond : Gitea a sa réponse tout de suite.
threading.Thread(
target=subprocess.run,
args=([DEPLOY_SCRIPT],),
kwargs={"capture_output": True},
daemon=True,
).start()
self.send_response(200); self.end_headers()
self.wfile.write(b"Deploy triggered")
def log_message(self, fmt, *args):
with open(LOG, "a") as journal:
journal.write(self.log_date_time_string() + " " + fmt % args + "\n")
if __name__ == "__main__":
http.server.HTTPServer(("127.0.0.1", PORT), Handler).serve_forever()