Détacher www.alpinux.org du monorepo, avec son déploiement
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
This commit is contained in:
commit
32b7cd1a0e
10 changed files with 1551 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Jetons de la forge, jamais versionnés
|
||||||
|
.gitea-token
|
||||||
|
|
||||||
|
# Rendu local
|
||||||
|
__pycache__/
|
||||||
85
README.md
Normal file
85
README.md
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
# www.alpinux.org
|
||||||
|
|
||||||
|
La page d'accueil de l'association — du HTML statique, une seule page, servie sur
|
||||||
|
`https://alpinux.org` et `https://www.alpinux.org`.
|
||||||
|
|
||||||
|
Elle affiche deux choses qu'elle ne contient pas : l'**agenda**, lu dans le calendrier
|
||||||
|
public Nextcloud, et les **dernières pages du wiki**, lues dans
|
||||||
|
[`derniers-articles.json`](https://wiki.alpinux.org/derniers-articles.json) que le wiki
|
||||||
|
publie à chaque build. Si l'une ou l'autre source est injoignable, la section
|
||||||
|
correspondante se masque et le reste de la page tient debout.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Je veux…
|
||||||
|
|
||||||
|
| Ce que vous voulez faire | Ce que vous faites |
|
||||||
|
|---|---|
|
||||||
|
| Modifier la page | Éditer `site/index.html`, pousser sur `main` — la mise en ligne suit |
|
||||||
|
| Voir le rendu avant de pousser | `python3 -m http.server -d site` puis http://localhost:8000 |
|
||||||
|
| Comprendre pourquoi ça n'est pas en ligne | `tail -20 /var/log/www-deploy.log` sur le serveur |
|
||||||
|
| Republier sans nouveau commit | `sudo -u abonnelc /opt/www-alpinux/deploy-www.sh` |
|
||||||
|
| Réinstaller la chaîne de déploiement | `sudo bash deploiement/installer.sh` |
|
||||||
|
|
||||||
|
En local, l'agenda reste vide : `/public-calendars/…` est relayé vers Nextcloud par le
|
||||||
|
serveur, pas par `http.server`. Les articles du wiki, eux, s'affichent normalement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
alpinux-www/
|
||||||
|
├── site/ ce qui part en ligne, tel quel
|
||||||
|
│ ├── index.html
|
||||||
|
│ ├── robots.txt
|
||||||
|
│ └── sitemap.xml
|
||||||
|
└── deploiement/ la chaîne, versionnée avec le site
|
||||||
|
├── installer.sh pose tout sur le serveur (root, une fois)
|
||||||
|
├── deploy-www.sh git pull + rsync vers le DocumentRoot
|
||||||
|
├── webhook.py service d'écoute, 127.0.0.1:9877
|
||||||
|
├── www-webhook.service unité systemd
|
||||||
|
└── vhost-deploy.conf le relais Apache, pour trace
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ce qui se passe après un push
|
||||||
|
|
||||||
|
```
|
||||||
|
git push (sur main)
|
||||||
|
│
|
||||||
|
Gitea (origin/main)
|
||||||
|
│ webhook ──▶ https://alpinux.org/deploy ──▶ service d'écoute (signature vérifiée)
|
||||||
|
▼
|
||||||
|
deploy-www.sh : git pull → index.html complet ? → rsync vers le DocumentRoot
|
||||||
|
▼
|
||||||
|
https://alpinux.org
|
||||||
|
```
|
||||||
|
|
||||||
|
Compter moins d'une minute. Le garde-fou est modeste mais réel : un `index.html` tronqué
|
||||||
|
— transfert coupé, conflit mal résolu — ne part pas en ligne.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Les deux pièges à connaître
|
||||||
|
|
||||||
|
**Supprimer un fichier ici ne le retire pas du serveur.** Le `rsync` de déploiement est
|
||||||
|
volontairement sans `--delete` : le DocumentRoot contient aussi des fichiers qui ne sont
|
||||||
|
pas dans ce dépôt — le `.htaccess` qui relaie `/public-calendars` vers Nextcloud, les
|
||||||
|
pages d'erreur et les statistiques d'ISPConfig. Un `--delete` les emporterait. Retirer un
|
||||||
|
fichier en ligne se fait donc à la main, sur le serveur.
|
||||||
|
|
||||||
|
**Le serveur n'écrit dans le DocumentRoot que par une ACL.** `web11/web` appartient à
|
||||||
|
l'utilisateur `web11` ; `installer.sh` pose un `setfacl` pour l'utilisateur du service.
|
||||||
|
Si ISPConfig réinitialise les droits du site un jour, le déploiement échouera avec une
|
||||||
|
erreur de permission — relancer `installer.sh` suffit à la remettre.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Historique
|
||||||
|
|
||||||
|
Ce dépôt est né le 2026-09-20, par détachement du monorepo `alpinux.site.2026` où la page
|
||||||
|
vivait sous `home/`. Elle y était **déployée à la main**, et le dépôt avait fini par
|
||||||
|
diverger : la version en ligne portait quatre mois de modifications faites directement sur
|
||||||
|
le serveur et jamais revenues dans git. C'est ce qui a motivé le webhook.
|
||||||
29
deploiement/deploy-www.sh
Executable file
29
deploiement/deploy-www.sh
Executable file
|
|
@ -0,0 +1,29 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Met en ligne www.alpinux.org depuis le dépôt alpinux-www.
|
||||||
|
# Appelé par le service webhook après chaque push sur main, ou à la main en secours.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
REPO_DIR="/opt/www-alpinux/repo"
|
||||||
|
SOURCE="$REPO_DIR/site"
|
||||||
|
TARGET="/var/www/clients/client1/web11/web"
|
||||||
|
LOG="/var/log/www-deploy.log"
|
||||||
|
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deploy started" >> "$LOG"
|
||||||
|
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
git pull origin main >> "$LOG" 2>&1
|
||||||
|
|
||||||
|
# Garde-fou : un index.html tronqué ne part pas en ligne. C'est l'équivalent, pour un
|
||||||
|
# site sans build, du « mkdocs build --strict » du wiki.
|
||||||
|
if ! grep -q "</html>" "$SOURCE/index.html"; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ABANDON : index.html incomplet" >> "$LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pas de --delete, volontairement : le DocumentRoot contient aussi des fichiers qui ne
|
||||||
|
# sont pas dans ce dépôt — le .htaccess qui relaie /public-calendars vers Nextcloud, les
|
||||||
|
# pages d'erreur et les statistiques d'ISPConfig. Supprimer un fichier ici ne le retire
|
||||||
|
# donc pas du serveur : c'est à faire à la main, et c'est écrit dans le README.
|
||||||
|
rsync -rltD --no-perms --omit-dir-times "$SOURCE/" "$TARGET/" >> "$LOG" 2>&1
|
||||||
|
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deploy done" >> "$LOG"
|
||||||
102
deploiement/installer.sh
Executable file
102
deploiement/installer.sh
Executable file
|
|
@ -0,0 +1,102 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Installe la chaîne de déploiement de www.alpinux.org sur owni. À lancer en root,
|
||||||
|
# une seule fois — relancer ne casse rien, chaque étape vérifie d'abord ce qui existe.
|
||||||
|
#
|
||||||
|
# sudo bash /chemin/vers/alpinux-www/deploiement/installer.sh
|
||||||
|
#
|
||||||
|
# Ce que ça met en place : un clone du dépôt dans /opt/www-alpinux, le service d'écoute
|
||||||
|
# du webhook sur 127.0.0.1:9877, le relais Apache /deploy, et le droit d'écriture de
|
||||||
|
# l'utilisateur du service sur le DocumentRoot.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DEPOT_URL="https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-www.git"
|
||||||
|
BASE="/opt/www-alpinux"
|
||||||
|
REPO_DIR="$BASE/repo"
|
||||||
|
UTILISATEUR="abonnelc"
|
||||||
|
CIBLE="/var/www/clients/client1/web11/web"
|
||||||
|
SECRET_FICHIER="/etc/default/www-webhook"
|
||||||
|
JOURNAL="/var/log/www-deploy.log"
|
||||||
|
UNITE="/etc/systemd/system/www-webhook.service"
|
||||||
|
|
||||||
|
[ "$(id -u)" -eq 0 ] || { echo "À lancer en root : sudo bash $0" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "==> 1/6 Dépôt dans $REPO_DIR"
|
||||||
|
install -d -o "$UTILISATEUR" -g "$UTILISATEUR" "$BASE"
|
||||||
|
if [ -d "$REPO_DIR/.git" ]; then
|
||||||
|
sudo -u "$UTILISATEUR" git -C "$REPO_DIR" pull --ff-only origin main
|
||||||
|
else
|
||||||
|
sudo -u "$UTILISATEUR" git clone "$DEPOT_URL" "$REPO_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> 2/6 Scripts et journal"
|
||||||
|
install -o "$UTILISATEUR" -g "$UTILISATEUR" -m 755 "$REPO_DIR/deploiement/deploy-www.sh" "$BASE/deploy-www.sh"
|
||||||
|
install -o "$UTILISATEUR" -g "$UTILISATEUR" -m 755 "$REPO_DIR/deploiement/webhook.py" "$BASE/webhook.py"
|
||||||
|
touch "$JOURNAL" && chown "$UTILISATEUR":"$UTILISATEUR" "$JOURNAL"
|
||||||
|
|
||||||
|
echo "==> 3/6 Droit d'écriture sur $CIBLE"
|
||||||
|
# Le DocumentRoot appartient à web11 (suexec) ; une ACL suffit à y laisser écrire le
|
||||||
|
# service, sans toucher au propriétaire ni aux droits qu'attend ISPConfig.
|
||||||
|
setfacl -m "u:$UTILISATEUR:rwx" "$CIBLE"
|
||||||
|
|
||||||
|
echo "==> 4/6 Secret du webhook"
|
||||||
|
if [ -f "$SECRET_FICHIER" ]; then
|
||||||
|
echo " (déjà là, inchangé)"
|
||||||
|
else
|
||||||
|
printf 'WWW_WEBHOOK_SECRET=%s\n' "$(openssl rand -hex 24)" > "$SECRET_FICHIER"
|
||||||
|
chmod 600 "$SECRET_FICHIER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> 5/6 Service systemd"
|
||||||
|
install -m 644 "$REPO_DIR/deploiement/www-webhook.service" "$UNITE"
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now www-webhook
|
||||||
|
systemctl is-active --quiet www-webhook && echo " service actif"
|
||||||
|
|
||||||
|
echo "==> 6/6 Relais Apache /deploy"
|
||||||
|
patcher_vhost() {
|
||||||
|
local fichier="$1"
|
||||||
|
[ -f "$fichier" ] || return 0
|
||||||
|
if grep -q "127.0.0.1:9877" "$fichier"; then
|
||||||
|
echo " $fichier : déjà relayé"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
cp -a "$fichier" "$fichier.bak-$(date +%Y%m%d%H%M)"
|
||||||
|
# Inséré juste avant le premier Include letsencrypt, donc dans le VirtualHost *:443.
|
||||||
|
awk '
|
||||||
|
!fait && /Include \/etc\/letsencrypt\/options-ssl-apache\.conf/ {
|
||||||
|
print "\t\tProxyPass /deploy http://127.0.0.1:9877/deploy"
|
||||||
|
print "\t\tProxyPassReverse /deploy http://127.0.0.1:9877/deploy"
|
||||||
|
fait = 1
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
' "$fichier" > "$fichier.tmp" && mv "$fichier.tmp" "$fichier"
|
||||||
|
grep -q "127.0.0.1:9877" "$fichier" || { echo " ÉCHEC sur $fichier" >&2; return 1; }
|
||||||
|
echo " $fichier : relais ajouté"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sur owni, sites-enabled contient des copies et non des liens : les deux sont à traiter.
|
||||||
|
for vhost in /etc/apache2/sites-enabled/100-alpinux.org.vhost \
|
||||||
|
/etc/apache2/sites-available/alpinux.org.vhost; do
|
||||||
|
patcher_vhost "$vhost"
|
||||||
|
done
|
||||||
|
|
||||||
|
if apache2ctl configtest; then
|
||||||
|
systemctl reload apache2
|
||||||
|
else
|
||||||
|
echo "Configuration Apache invalide : rien n'a été rechargé." >&2
|
||||||
|
echo "Les sauvegardes .bak-* sont à côté des vhosts." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "────────────────────────────────────────────────────────────"
|
||||||
|
echo "Reste à déclarer le webhook dans Gitea :"
|
||||||
|
echo " Paramètres du dépôt alpinux-www → Webhooks → Ajouter → Gitea"
|
||||||
|
echo " URL : https://alpinux.org/deploy"
|
||||||
|
echo " Méthode : POST, type de contenu : application/json"
|
||||||
|
echo " Secret : $(sed 's/^WWW_WEBHOOK_SECRET=//' "$SECRET_FICHIER")"
|
||||||
|
echo " Événement : Push seulement"
|
||||||
|
echo
|
||||||
|
echo "Puis, pour une première mise en ligne sans attendre un push :"
|
||||||
|
echo " sudo -u $UTILISATEUR $BASE/deploy-www.sh && tail -5 $JOURNAL"
|
||||||
|
echo "────────────────────────────────────────────────────────────"
|
||||||
9
deploiement/vhost-deploy.conf
Normal file
9
deploiement/vhost-deploy.conf
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# À insérer dans le VirtualHost *:443 de /etc/apache2/sites-enabled/100-alpinux.org.vhost,
|
||||||
|
# juste avant « Include /etc/letsencrypt/options-ssl-apache.conf ».
|
||||||
|
# installer.sh le fait tout seul ; ce fichier sert de trace et de secours.
|
||||||
|
#
|
||||||
|
# Attention : sur owni, les fichiers de sites-enabled sont des copies, pas des liens
|
||||||
|
# symboliques vers sites-available. Une modification faite dans ISPConfig ne les atteint
|
||||||
|
# pas — il faut éditer la copie, et tant qu'à faire l'original aussi.
|
||||||
|
ProxyPass /deploy http://127.0.0.1:9877/deploy
|
||||||
|
ProxyPassReverse /deploy http://127.0.0.1:9877/deploy
|
||||||
62
deploiement/webhook.py
Normal file
62
deploiement/webhook.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
#!/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()
|
||||||
17
deploiement/www-webhook.service
Normal file
17
deploiement/www-webhook.service
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[Unit]
|
||||||
|
Description=www.alpinux.org webhook deploy server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=abonnelc
|
||||||
|
Group=abonnelc
|
||||||
|
WorkingDirectory=/opt/www-alpinux
|
||||||
|
ExecStart=/usr/bin/python3 /opt/www-alpinux/webhook.py
|
||||||
|
# Le secret vit hors du dépôt : ce fichier est écrit par installer.sh, en 0600.
|
||||||
|
EnvironmentFile=/etc/default/www-webhook
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
1229
site/index.html
Normal file
1229
site/index.html
Normal file
File diff suppressed because it is too large
Load diff
4
site/robots.txt
Normal file
4
site/robots.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://alpinux.org/sitemap.xml
|
||||||
9
site/sitemap.xml
Normal file
9
site/sitemap.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://alpinux.org/</loc>
|
||||||
|
<lastmod>2026-05-03</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
Loading…
Reference in a new issue