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
68 lines
2.5 KiB
Python
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()
|