La page d'accueil vivait sous home/ dans alpinux.site.2026, sans webhook : elle était déposée à la main sur le serveur. Le dépôt avait fini par diverger — la version en ligne portait quatre mois de modifications jamais revenues dans git. Ce dépôt suit la règle des autres domaines : un domaine, un dépôt. Le site est dans site/, la chaîne de déploiement dans deploiement/ — clone sur le serveur, service d'écoute du webhook, relais Apache, et l'ACL qui donne le droit d'écrire dans le DocumentRoot. installer.sh pose l'ensemble en une fois, et se relance sans rien casser. Le rsync est sans --delete, volontairement : le DocumentRoot contient aussi le .htaccess qui relaie /public-calendars vers Nextcloud et les pages d'erreur d'ISPConfig. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ae3qPEPRrNkEY5LwcTTKc1
62 lines
2.2 KiB
Python
62 lines
2.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Service d'écoute du webhook Gitea pour www.alpinux.org.
|
|
|
|
N'écoute que sur la boucle locale : c'est Apache qui lui transmet les requêtes reçues
|
|
sur https://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 le service du wiki (/opt/wiki-mkdocs/webhook.py), à trois différences près :
|
|
le port, le script appelé, et le secret qui vient d'un fichier d'environnement plutôt
|
|
que de l'unité systemd — 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("WWW_WEBHOOK_SECRET", "")
|
|
DEPLOY_SCRIPT = "/opt/www-alpinux/deploy-www.sh"
|
|
LOG = "/var/log/www-deploy.log"
|
|
PORT = 9877
|
|
|
|
|
|
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
|
|
|
|
ref = json.loads(body).get("ref", "")
|
|
if ref not in ("refs/heads/main", "refs/heads/master"):
|
|
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()
|