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