diff --git a/.gitea/workflows/check-pr.yml b/.gitea/workflows/check-pr.yml
new file mode 100644
index 0000000..2a3f99e
--- /dev/null
+++ b/.gitea/workflows/check-pr.yml
@@ -0,0 +1,55 @@
+name: Vérification PR
+run-name: Vérification PR de ${{ gitea.actor }}
+on:
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ extensions: curl, intl, mbstring, xml
+
+ - name: Lint PHP (tous les fichiers, samples inclus)
+ run: |
+ fail=0
+ while IFS= read -r f; do
+ php -l "$f" > /dev/null || { echo "::error file=$f::Erreur de syntaxe PHP"; fail=1; }
+ done < <(find . -path ./.git -prune -o \( -name '*.php' -o -name '*.php.sample' \) -print)
+ [ "$fail" -eq 0 ] && echo "PHP lint OK" || exit 1
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Lint JS (sw.js et js/*.js)
+ run: |
+ for f in sw.js js/*.js; do
+ node --check "$f" || exit 1
+ done
+ echo "JS lint OK"
+
+ - name: Installer Asciidoctor
+ run: sudo gem install asciidoctor
+
+ - name: Valider README.adoc
+ run: asciidoctor -o /tmp/readme.html README.adoc
+
+ - name: Valider JSON et XML
+ run: |
+ sudo apt-get update && sudo apt-get install -y libxml2-utils
+ python3 -m json.tool site.webmanifest.sample > /dev/null
+ xmllint --noout sitemap.xml.sample browserconfig.xml
+
+ - name: Shellcheck (scripts shell)
+ run: |
+ sudo apt-get install -y shellcheck
+ shellcheck docs/generate-readme-pdf.sh scripts/check.sh
diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml
new file mode 100644
index 0000000..8314b76
--- /dev/null
+++ b/.gitea/workflows/deploy-prod.yml
@@ -0,0 +1,82 @@
+name: Déploiement PROD
+run-name: ${{ gitea.actor }} déploie en PROD
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+ extensions: curl, intl, mbstring, xml
+
+ - name: Lint PHP (tous les fichiers, samples inclus)
+ run: |
+ fail=0
+ while IFS= read -r f; do
+ php -l "$f" > /dev/null || { echo "::error file=$f::Erreur de syntaxe PHP"; fail=1; }
+ done < <(find . -path ./.git -prune -o \( -name '*.php' -o -name '*.php.sample' \) -print)
+ [ "$fail" -eq 0 ] && echo "PHP lint OK" || exit 1
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Lint JS (sw.js et js/*.js)
+ run: |
+ for f in sw.js js/*.js; do
+ node --check "$f" || exit 1
+ done
+ echo "JS lint OK"
+
+ - name: Installer Asciidoctor
+ run: sudo gem install asciidoctor
+
+ - name: Valider README.adoc
+ run: asciidoctor -o /tmp/readme.html README.adoc
+
+ - name: Valider JSON et XML
+ run: |
+ sudo apt-get update && sudo apt-get install -y libxml2-utils
+ python3 -m json.tool site.webmanifest.sample > /dev/null
+ xmllint --noout sitemap.xml.sample browserconfig.xml
+
+ - name: Shellcheck (scripts shell)
+ run: |
+ sudo apt-get install -y shellcheck
+ shellcheck docs/generate-readme-pdf.sh scripts/check.sh
+
+ deploy:
+ needs: check
+ runs-on: ubuntu-latest
+ steps:
+ - name: Déployer sur le serveur
+ uses: appleboy/ssh-action@v1
+ with:
+ host: ${{ secrets.SSH_HOST }}
+ username: ${{ secrets.SSH_USER }}
+ key: ${{ secrets.SSH_KEY }}
+ script: |
+ set -e
+ cd ${{ secrets.PROD_DEPLOY_PATH }}
+
+ # Annuler le bump de version du déploiement précédent pour
+ # garantir le fast-forward, puis récupérer la dernière version.
+ git checkout -- sw.js
+ git pull --ff-only origin main
+
+ # Bumper la version des caches du Service Worker : chaque
+ # déploiement déclenche le modal de mise à jour PWA chez les
+ # visiteurs (voir js/pwa-update.js). Ce bump n'est pas commité.
+ VERSION=$(date +%d%m%Y-%H%M)
+ sed -i "s/annu-kute-ced-static-[0-9]\{8\}-[0-9]\{4\}/annu-kute-ced-static-$VERSION/" sw.js
+ sed -i "s/annu-kute-ced-dynamic-[0-9]\{8\}-[0-9]\{4\}/annu-kute-ced-dynamic-$VERSION/" sw.js
+ echo "Version des caches bumpée : $VERSION"
diff --git a/DEPLOY.adoc b/DEPLOY.adoc
new file mode 100644
index 0000000..b4dba7f
--- /dev/null
+++ b/DEPLOY.adoc
@@ -0,0 +1,278 @@
+= 🚀 DEPLOY — Mise en production et CI/CD
+:toc: left
+:toc-title: Sommaire
+:toclevels: 3
+
+Ce document décrit la mise en production complète d'*ANNU KUTE CED* sur un serveur fraîchement installé, puis l'activation du déploiement continu via *Gitea Actions*.
+
+Il est complémentaire au link:README.adoc[README] (installation manuelle, configuration de l'application).
+
+== 🧭 Vue d'ensemble
+
+L'architecture retenue (identique à celle de pawol.nu) :
+
+[source]
+----
+┌─────────────┐ push main ┌──────────────────┐ SSH (git pull) ┌─────────────┐
+│ Dépôt git │ ─────────────▶ │ Gitea Actions │ ─────────────────▶ │ Serveur │
+│ (LaBola) │ │ check → deploy │ │ production │
+└─────────────┘ └──────────────────┘ └─────────────┘
+----
+
+. *Vérification* (`check-pr.yml` + job `check` de `deploy-prod.yml`) : lint PHP/JS, validation AsciiDoc, JSON, XML, shellcheck. Les mêmes vérifications sont exécutables en local avec `scripts/check.sh`.
+. *Déploiement* (`deploy-prod.yml`) : le runner se connecte en SSH au serveur, qui tient *un clone du dépôt*, fait `git pull --ff-only`, puis *bumpe la version des caches* du Service Worker (`sw.js`) pour déclencher le modal de mise à jour PWA chez les visiteurs.
+
+Pourquoi un `git pull` sur le serveur plutôt qu'un rsync : tous les fichiers propres à l'instance (`config.local.php`, `.htaccess`, `sitemap.xml`, `robots.txt`, `site.webmanifest`, `mentions-legales.php`, `dons.php`, `uploads/`, `cache/`) sont ignorés par Git — un pull ne les écrase jamais. Le déploiement est ainsi sans risque pour la configuration de production.
+
+== 📋 Prérequis
+
+- Un serveur *Debian 12* ou *Ubuntu 24.04* fraîchement installé, avec accès `root` (ou `sudo`)
+- Un nom de domaine dont le *DNS pointe vers le serveur* (enregistrement A/AAAA)
+- Le dépôt Gitea : `git@labola.o-k-i.net:cedric/annu-kute-ced.git`
+- Un *runner Gitea Actions* opérationnel sur l'instance LaBola (déjà le cas pour pawol.nu)
+
+NOTE: Sur un hébergement mutualisé (pas d'accès root), adaptez : les fichiers sont déployés dans le docroot fourni par l'hébergeur, et la clé SSH s'ajoute via le panneau de contrôle (o2switch : *Clés SSH* dans cPanel).
+
+== 1️⃣ Installation des paquets
+
+[source,bash]
+----
+# Apache + PHP (choix retenu pour ce guide ; Nginx possible, voir §5)
+apt update && apt upgrade -y
+apt install -y apache2 php libapache2-mod-php \
+ php-curl php-intl php-mbstring php-xml \
+ git curl unzip
+
+# Vérifier les extensions requises
+php -m | grep -E 'curl|intl|mbstring|SimpleXML|json'
+----
+
+== 2️⃣ Utilisateur de déploiement
+
+Le runner CI se connectera en SSH avec cet utilisateur. Ne *pas* utiliser root.
+
+[source,bash]
+----
+adduser --disabled-password --gecos "Deploy annu-kute-ced" deploy
+usermod -aG www-data deploy
+
+# Répertoire de l'application
+mkdir -p /var/www/annu-kute-ced
+chown -R deploy:www-data /var/www/annu-kute-ced
+----
+
+== 3️⃣ Clone du dépôt
+
+Le serveur de production tient un clone du dépôt. Le runner y exécutera `git pull` à chaque déploiement.
+
+[source,bash]
+----
+# Clé SSH du serveur pour lire le dépôt (deploy key Gitea, lecture seule)
+sudo -u deploy ssh-keygen -t ed25519 -f /home/deploy/.ssh/id_ed25519 -N ""
+cat /home/deploy/.ssh/id_ed25519.pub
+----
+
+. Sur LaBola : *Settings du dépôt → Deploy Keys → Add deploy key* (coller la clé publique, lecture seule suffit).
+. Puis :
++
+[source,bash]
+----
+sudo -u deploy git clone git@labola.o-k-i.net:cedric/annu-kute-ced.git /var/www/annu-kute-ced
+cd /var/www/annu-kute-ced
+sudo -u deploy git config pull.ff only # sécurité : refuser tout pull non fast-forward
+----
+
+== 4️⃣ Fichiers d'instance
+
+Ces fichiers sont ignorés par Git : ils vivent *uniquement sur le serveur* et ne seront jamais écrasés par les déploiements.
+
+[source,bash]
+----
+cd /var/www/annu-kute-ced
+sudo -u deploy cp includes/config.local.php.sample includes/config.local.php
+sudo -u deploy cp conf/.htaccess.sample .htaccess
+sudo -u deploy cp site.webmanifest.sample site.webmanifest
+sudo -u deploy cp robots.txt.sample robots.txt
+sudo -u deploy cp sitemap.xml.sample sitemap.xml
+sudo -u deploy cp mentions-legales.php.sample mentions-legales.php
+# Facultatif (page de dons) :
+# sudo -u deploy cp dons.php.sample dons.php
+----
+
+Éditer ensuite les fichiers copiés :
+
+- `includes/config.local.php` : `APP_HOST_NAME`, sources (PeerTube/Castopod/Mastodon), `CACHE_ENABLED=true` (recommandé pour Castopod), etc. — voir la link:README.adoc#fr-configuration[référence de configuration]
+- `sitemap.xml`, `robots.txt`, `site.webmanifest` : remplacer `example.com` par le domaine réel
+- `mentions-legales.php` : remplacer le placeholder `VOTRE-DATE-MAJ`
+
+[source,bash]
+----
+# Le cache de l'API doit être accessible en écriture par PHP
+mkdir -p /var/www/annu-kute-ced/cache
+chown -R www-data:www-data /var/www/annu-kute-ced/cache
+----
+
+== 5️⃣ VirtualHost
+
+=== Option A — Apache (recommandée, `.htaccess` fourni)
+
+[source,bash]
+----
+a2enmod rewrite headers
+cat > /etc/apache2/sites-available/annu-kute-ced.conf <<'EOF'
+
+ ServerName example.com
+ ServerAlias www.example.com
+ DocumentRoot /var/www/annu-kute-ced
+
+
+ AllowOverride All
+ Require all granted
+
+
+EOF
+a2ensite annu-kute-ced
+systemctl reload apache2
+----
+
+Le `.htaccess` copié à l'étape 4 applique les règles de sécurité, le HTTPS forcé (actif après l'étape 6) et le `no-cache` de `sw.js`.
+
+=== Option B — Nginx + PHP-FPM
+
+[source,bash]
+----
+apt install -y nginx php-fpm
+# Adapter conf/nginx.conf.sample : server_name, root, socket php-fpm,
+# chemins des certificats. ⚠️ Le bloc "location ~* \.(php|inc|...)$ { deny all; }"
+# de l'exemple correspond à TOUS les .php : le restreindre à ^/includes/.
+cp conf/nginx.conf.sample /etc/nginx/sites-available/annu-kute-ced
+ln -s /etc/nginx/sites-available/annu-kute-ced /etc/nginx/sites-enabled/
+nginx -t && systemctl reload nginx
+----
+
+== 6️⃣ HTTPS (obligatoire pour la PWA)
+
+[source,bash]
+----
+apt install -y certbot python3-certbot-apache # ou python3-certbot-nginx
+certbot --apache -d example.com -d www.example.com
+# ou : certbot --nginx -d example.com -d www.example.com
+----
+
+== 7️⃣ Vérification du site
+
+[source,bash]
+----
+curl -I https://example.com
+# Attendu : HTTP/2 200, en-têtes de sécurité (CSP, X-Frame-Options…)
+curl -I https://example.com/sw.js
+# Attendu : Cache-Control: no-cache
+----
+
+Ouvrir le site dans un navigateur : vidéos, podcasts, timeline et le bouton d'installation PWA doivent fonctionner.
+
+== 8️⃣ Clé SSH du CI
+
+C'est la clé utilisée par le runner Gitea Actions pour déployer. Elle est *différente* de la deploy key de lecture (étape 3) : celle-ci doit pouvoir écrire dans le clone.
+
+[source,bash]
+----
+# Générer la paire SUR VOTRE POSTE (pas sur le serveur)
+ssh-keygen -t ed25519 -f annu-kute-ced-deploy -C "ci-gitea-annu-kute-ced"
+
+# Autoriser la clé publique pour l'utilisateur deploy
+sudo -u deploy tee -a /home/deploy/.ssh/authorized_keys < annu-kute-ced-deploy.pub
+----
+
+La clé privée (`annu-kute-ced-deploy`, sans passphrase) ira dans les secrets Gitea à l'étape suivante. *Conservez-la en lieu sûr et ne la commitez jamais.*
+
+== 9️⃣ Secrets Gitea
+
+Dans LaBola : *Settings du dépôt → Actions → Secrets*, créer :
+
+[cols="1,3",options="header"]
+|===
+| Secret | Valeur
+
+| `SSH_HOST`
+| Adresse du serveur (IP ou FQDN), port 22 par défaut (sinon `host:port`)
+
+| `SSH_USER`
+| `deploy`
+
+| `SSH_KEY`
+| Contenu *intégral* de la clé privée générée à l'étape 8
+
+| `PROD_DEPLOY_PATH`
+| `/var/www/annu-kute-ced`
+|===
+
+WARNING: `SSH_KEY` donne un accès shell au serveur avec les droits de `deploy`. Ne jamais la coller ailleurs que dans les secrets Gitea, et révoquer la clé publique (`authorized_keys`) en cas de doute.
+
+== 🔟 Test du pipeline
+
+. Pousser un commit sur `main` (par exemple une correction de coquille dans le README).
+. Dans LaBola, onglet *Actions* : le workflow *Déploiement PROD* doit exécuter `check` puis `deploy` au vert.
+. Côté serveur :
++
+[source,bash]
+----
+cd /var/www/annu-kute-ced
+git log -1 --oneline # doit correspondre au commit poussé
+grep STATIC_CACHE_NAME sw.js # le suffixe de version a été bumpé à l'heure du déploiement
+----
+. Côté visiteur : au prochain chargement de page, le modal « Nouvelle version disponible » apparaît (Service Worker déjà installé lors d'une visite précédente).
+
+== 🔄 Fonctionnement courant
+
+[cols="1,3",options="header"]
+|===
+| Événement | Résultat
+
+| Pull request vers `main`
+| Workflow *Vérification PR* : lints et validations bloquants en cas d'erreur
+
+| Push sur `main`
+| Workflow *Déploiement PROD* : mêmes vérifications, puis SSH → `git pull --ff-only` → bump de la version des caches `sw.js` → modal de mise à jour chez les visiteurs
+
+| En local, avant de pousser
+| `scripts/check.sh` exécute les mêmes vérifications que le CI
+|===
+
+Le bump de version dans `sw.js` est fait *sur le serveur uniquement* : le dépôt garde sa valeur de référence, le working tree du serveur est nettoyé (`git checkout -- sw.js`) avant chaque pull pour garantir le fast-forward.
+
+== 🧰 Dépannage
+
+[cols="1,3",options="header"]
+|===
+| Symptôme | Piste
+
+| `git pull --ff-only` échoue sur le serveur
+| Une modification locale existe : `git status` dans le docroot, puis `git checkout -- ` (le workflow le fait déjà pour `sw.js`)
+
+| L'action ne se déclenche pas
+| Vérifier que le runner act est en ligne (LaBola → *Site Administration → Actions → Runners*) et que Actions est activé pour le dépôt (*Settings → Units*)
+
+| `Permission denied (publickey)` dans le job deploy
+| Clé publique absente de `/home/deploy/.ssh/authorized_keys`, ou mauvais `SSH_USER`/`SSH_HOST`
+
+| Les visiteurs ne reçoivent pas la mise à jour
+| Vérifier `curl -I https://example.com/sw.js` → `Cache-Control: no-cache` requis (règles fournies dans `conf/`)
+
+| Erreurs 500 sur les vidéos/podcasts
+| `cache/` non accessible en écriture : `chown -R www-data:www-data cache/`
+
+| `php-intl` manquant
+| `apt install php-intl && systemctl restart apache2`
+|===
+
+== 🔒 Notes de sécurité
+
+- La clé privée du CI est *dédiée* à ce dépôt : une clé compromise ne donne accès qu'au compte `deploy`, sans sudo.
+- La deploy key Gitea (étape 3) est en *lecture seule*.
+- Pour restreindre davantage la clé du CI, on peut limiter les commandes dans `authorized_keys` (`command="..."`, `no-pty`) — facultatif, hors scope de ce guide.
+- Les fichiers sensibles (`includes/`, `.htaccess`, `config.local.php`) sont bloqués en accès web par les configurations fournies dans `conf/`.
+
+== 📞 Support
+
+En cas de blocage : mailto:kontak@o-k-i.net[kontak@o-k-i.net]
diff --git a/README.adoc b/README.adoc
index a1479f6..abadcf1 100644
--- a/README.adoc
+++ b/README.adoc
@@ -151,6 +151,8 @@ Toutes ces sources sont modifiables dans `includes/config.local.php` (voir < : vérifie la présence d'un outil
+need() {
+ if ! command -v "$1" >/dev/null 2>&1; then
+ echo "⚠️ '$1' non installé — check ignoré (paquet : $2)"
+ warn=1
+ return 1
+ fi
+}
+
+step "PHP lint (*.php, *.php.sample)"
+if need php php-cli; then
+ php_fail=0
+ while IFS= read -r f; do
+ if ! php -l "$f" > /dev/null; then
+ echo "❌ $f"
+ php_fail=1
+ fi
+ done < <(find . -path ./.git -prune -o \( -name '*.php' -o -name '*.php.sample' \) -print)
+ if [ "$php_fail" -eq 0 ]; then
+ echo "✅ PHP OK"
+ else
+ fail=1
+ fi
+fi
+
+step "JS lint (sw.js, js/*.js)"
+if need node nodejs; then
+ js_fail=0
+ for f in sw.js js/*.js; do
+ node --check "$f" || js_fail=1
+ done
+ if [ "$js_fail" -eq 0 ]; then
+ echo "✅ JS OK"
+ else
+ fail=1
+ fi
+fi
+
+step "AsciiDoc (README.adoc, DEPLOY.adoc)"
+if need asciidoctor asciidoctor; then
+ adoc_fail=0
+ for f in README.adoc DEPLOY.adoc; do
+ [ -f "$f" ] || continue
+ asciidoctor -o "/tmp/check-$$.html" "$f" || adoc_fail=1
+ done
+ rm -f "/tmp/check-$$.html"
+ if [ "$adoc_fail" -eq 0 ]; then
+ echo "✅ AsciiDoc OK"
+ else
+ fail=1
+ fi
+fi
+
+step "JSON (site.webmanifest.sample)"
+if need python3 python3; then
+ if python3 -m json.tool site.webmanifest.sample > /dev/null; then
+ echo "✅ JSON OK"
+ else
+ fail=1
+ fi
+fi
+
+step "XML (sitemap.xml.sample, browserconfig.xml)"
+if need xmllint libxml2-utils; then
+ if xmllint --noout sitemap.xml.sample browserconfig.xml; then
+ echo "✅ XML OK"
+ else
+ fail=1
+ fi
+fi
+
+step "Shellcheck (scripts shell)"
+if need shellcheck shellcheck; then
+ if shellcheck docs/generate-readme-pdf.sh scripts/check.sh; then
+ echo "✅ Shellcheck OK"
+ else
+ fail=1
+ fi
+fi
+
+echo
+if [ "$fail" -ne 0 ]; then
+ echo "❌ Des vérifications ont échoué — corrigez avant de pousser."
+ exit 1
+fi
+if [ "$warn" -ne 0 ]; then
+ echo "⚠️ Checks disponibles OK, mais certains outils manquent (le CI exécutera tout)."
+else
+ echo "✅ Tous les checks passent."
+fi