Range --archive-seule au lieu de le supprimer, et mesure la RAM

Le mode etait justifie dans CLAUDE.md comme « la brique reutilisable en CI ».
C'etait faux : le README du depot du code s'en sert depuis toujours comme
chemin de reprise a la main quand publier.sh echoue en cours de route. Il a
donc failli partir sur une justification erronee.

Garde et range : 8 ramifications ramenees a 5, dont une seule assouplit encore
un garde-fou, contre trois auparavant. Deux defauts reels en sont sortis — un
depot sale n'etait PAS signale dans ce mode, et la version y retombait sur
celle deja publiee, piege que le README documentait au lieu de le corriger.

refuser() separe le constat du conseil : le constat vaut dans les deux modes,
« choisissez un numero libre » est faux en reprise, ou le tag vise est
justement celui qu'on veut retrouver.

Le depot git devient une exigence inconditionnelle en tete, ce qui supprime
quatre tests -d .git disperses plus bas.

Les cinq variables d'environnement jumelles disparaissent : chacune doublait
une option qu'elle repetait, l'aide en listait dix pour cinq reglages, et rien
ne disait laquelle l'emportait.

ram.runtime passe de 200M a 256M. Mesure sur le serveur : pic reel de
208 793 600 o, soit 199,1 Mio, contre 200M declares — 0,4 % de marge, c'est-a-
dire aucune. systemctl show -p MemoryPeak ne renvoie rien sur ce serveur et
reussit en silence ; c'est le cgroup qui garde le maximum.

Les deux doc/*_fr.md, identiques octet pour octet a leurs jumeaux, sont
supprimes : YunoHost retombe sur les fichiers par defaut, deja en francais.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-22 12:15:24 +02:00
co-authored by Claude Opus 5
parent fac694da03
commit 1f115790ee
4 changed files with 132 additions and 170 deletions
+69 -41
View File
@@ -36,12 +36,18 @@ set -euo pipefail
racine_paquet="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
manifeste="$racine_paquet/manifest.toml"
source_dir="${MABIBLI_SOURCE_DIR:-$(cd "$racine_paquet/../mabibli" 2>/dev/null && pwd || true)}"
base_url="${MABIBLI_BASE_URL:-https://git.akbar.nohost.me/mathieu/mabibli/releases/download}"
url_releases="${MABIBLI_URL_RELEASES:-https://git.akbar.nohost.me/mathieu/mabibli/releases}"
output_dir="${MABIBLI_OUTPUT_DIR:-$racine_paquet/build/dist}"
# ⚠️ Chaque réglage a UNE seule façon de se donner : son option en ligne de commande.
# Cinq variables d'environnement jumelles (MABIBLI_SOURCE_DIR, MABIBLI_BASE_URL…) ont
# existé jusqu'au 2026-08-22, chacune doublant une option qu'elle ne faisait que
# répéter : l'aide en listait dix pour cinq réglages, et rien ne disait laquelle
# l'emportait sur l'autre. Ne pas les réintroduire pour une CI hypothétique — une CI
# passe des options aussi bien que des variables.
source_dir="$(cd "$racine_paquet/../mabibli" 2>/dev/null && pwd || true)"
base_url="https://git.akbar.nohost.me/mathieu/mabibli/releases/download"
url_releases="https://git.akbar.nohost.me/mathieu/mabibli/releases"
output_dir="$racine_paquet/build/dist"
version="${MABIBLI_VERSION:-}"
version=""
increment=""
sans_confirmation=0
archive_seule=0
@@ -58,14 +64,13 @@ Usage : publier.sh [options]
--major Incrémenter le premier chiffre (0.3.0 -> 1.0.0)
--oui, -y Ne rien demander (pour une CI)
--archive-seule Compiler et archiver SEULEMENT : ni manifeste, ni commit, ni tag.
Pour une CI ou un essai ; ne publie rien et ne modifie aucun dépôt.
Pour REPRENDRE une publication à la main après un échec en cours
de route ; ne publie rien et ne modifie aucun dépôt. Les contrôles
restent les mêmes, mais avertissent au lieu de refuser.
--source-dir CHEMIN Dépôt du code C# (défaut : ../mabibli)
--base-url URL Racine des URL de release, sans le tag ni le fichier
--output-dir CHEMIN Où déposer l'archive (défaut : build/dist)
-h, --help Cette aide
Variables d'environnement équivalentes : MABIBLI_SOURCE_DIR, MABIBLI_VERSION,
MABIBLI_BASE_URL, MABIBLI_OUTPUT_DIR, MABIBLI_URL_RELEASES.
FIN
}
@@ -85,13 +90,38 @@ done
echoerr() { echo "$@" >&2; }
# ⚠️ LE SEUL ENDROIT où `--archive-seule` assouplit quoi que ce soit.
#
# Les garde-fous ci-dessous protègent la *publication* : ils empêchent qu'une release
# contienne autre chose que le code que son tag désigne. En `--archive-seule`, rien n'est
# publié — ni manifeste, ni commit, ni tag — donc rien ne peut mentir, et un refus n'a
# plus lieu d'être. Il reste un avertissement, car ce qu'il signale est toujours vrai.
#
# Chaque garde-fou s'écrit donc UNE fois, sans savoir dans quel mode il tourne. C'est ce
# qui empêche d'assouplir par inadvertance un contrôle qui devait rester dur.
# ⚠️ Le CONSTAT et le CONSEIL sont séparés, et ce n'est pas de la cosmétique : le constat
# reste vrai dans les deux modes, le conseil non. « Choisissez un numéro libre » est le
# bon conseil pour une publication, et un mauvais conseil en reprise — où le tag visé est
# justement celui qu'on veut retrouver.
refuser() { # refuser <conseil> <constat> [<constat>...]
local conseil="$1"; shift
if [ "$archive_seule" -eq 1 ]; then
printf ' ⚠ %s\n' "$@"
echo " (poursuite : --archive-seule ne publie rien)"
else
printf '%s\n' "$@" >&2
[ -n "$conseil" ] && echoerr "$conseil"
exit 1
fi
}
#=================================================
# CONTRÔLES PRÉALABLES
#=================================================
if [ -z "$source_dir" ] || [ ! -d "$source_dir" ]; then
echoerr "Dépôt du code introuvable : '${source_dir:-<vide>}'"
echoerr "Précisez-le avec --source-dir (ou MABIBLI_SOURCE_DIR)."
echoerr "Précisez-le avec --source-dir."
exit 1
fi
source_dir="$(cd "$source_dir" && pwd)"
@@ -101,9 +131,13 @@ if [ ! -d "$source_dir/$projet" ]; then
exit 1
fi
if [ "$archive_seule" -eq 0 ] && [ ! -d "$source_dir/.git" ]; then
echoerr "'$source_dir' n'est pas un dépôt git : impossible de taguer la release."
echoerr "Pour compiler sans rien publier : --archive-seule."
# ⚠️ Exigé dans les DEUX modes. `--archive-seule` sert à reprendre une publication à la
# main, jamais à compiler un arbre quelconque : sans dépôt, il ne saurait ni proposer une
# version, ni dire si l'archive correspond au code d'un tag. L'exiger ici plutôt qu'à
# quatre endroits plus bas est ce qui rend le reste du script inconditionnel.
if [ ! -d "$source_dir/.git" ]; then
echoerr "'$source_dir' n'est pas un dépôt git."
echoerr "publier.sh compile ce qu'un commit désigne : sans dépôt, rien ne le désigne."
exit 1
fi
@@ -120,8 +154,6 @@ fi
# Le tag fait foi : c'est lui qui désigne le code publié. Le manifeste ne porte que la
# dernière version *empaquetée*, qui peut être en retard si une publication a échoué en
# cours de route. On prend le plus élevé des deux pour ne jamais proposer un numéro déjà pris.
version_tag=""
[ -d "$source_dir/.git" ] &&
version_tag="$(git -C "$source_dir" tag --list 'v[0-9]*' --sort=-v:refname | head -1 | sed 's/^v//')"
version_manifeste="$(sed -n 's/^version *= *"\([^"~]*\).*"/\1/p' "$manifeste" | head -1)"
@@ -137,15 +169,12 @@ suivante() { # suivante <version> <patch|minor|major>
esac
}
if [ "$archive_seule" -eq 1 ]; then
# Rien n'est publié : la version ne sert qu'à nommer l'archive et à alimenter
# `-p:Version`. Celle du manifeste convient, et on ne propose aucun incrément.
[ -z "$version" ] && version="$version_manifeste"
if [ -z "$version" ]; then
echoerr "Impossible de déduire la version depuis $manifeste ; utilisez --version."
exit 1
fi
else
# ⚠️ Le calcul de la version est le MÊME dans les deux modes, et c'est délibéré.
# `--archive-seule` retombait auparavant sur la version du manifeste, c'est-à-dire sur
# celle **déjà publiée** : reprendre une publication à la main reproduisait donc le
# numéro précédent tant qu'on ne passait pas `--version`. Le README devait avertir de ce
# piège ; il n'existe plus. Une reprise après échec veut la version qu'on était en train
# de publier, et c'est exactement ce que l'incrément propose.
if [ -z "$precedente" ]; then
echoerr "Aucune version précédente trouvée (ni tag, ni manifest.toml) ; utilisez --version."
exit 1
@@ -184,7 +213,6 @@ else
echoerr "Version invalide : '$version' (attendu X.Y.Z)."
exit 1
fi
fi
#=================================================
# CE QUI DOIT ÊTRE VRAI AVANT DE COMPILER
@@ -193,38 +221,38 @@ fi
# ⚠️ Le garde-fou qui manquait le 2026-08-21. Recompiler une version dont le tag existe
# déjà produit une archive qui ne correspond PAS au code que ce tag désigne : la release
# se met alors à mentir, et l'écran « À propos » avec elle, sans que rien ne le signale.
if [ -d "$source_dir/.git" ] && git -C "$source_dir" rev-parse "v$version" >/dev/null 2>&1; then
if git -C "$source_dir" rev-parse "v$version" >/dev/null 2>&1; then
commit_tag="$(git -C "$source_dir" rev-list -n1 "v$version")"
commit_head="$(git -C "$source_dir" rev-parse HEAD)"
if [ "$archive_seule" -eq 1 ]; then
[ "$commit_tag" != "$commit_head" ] &&
echo " v$version désigne ${commit_tag:0:7}, HEAD est ${commit_head:0:7} : l'archive ne sera PAS le code du tag."
if [ "$commit_tag" != "$commit_head" ]; then
refuser "Choisissez un numéro libre (--patch)." \
"Le tag v$version existe et désigne ${commit_tag:0:7} ; HEAD est ${commit_head:0:7}." \
"L'archive ne serait donc PAS le code que ce tag désigne."
else
echoerr "Le tag v$version existe déjà dans $source_dir (${commit_tag:0:7})."
[ "$commit_tag" != "$commit_head" ] &&
echoerr "HEAD est ${commit_head:0:7} : republier sous ce numéro mettrait un autre code dans la release v$version."
echoerr "Choisissez un numéro libre (--patch), ou compilez sans publier (--archive-seule)."
exit 1
refuser "Choisissez un numéro libre (--patch)." \
"Le tag v$version existe déjà (${commit_tag:0:7}) : ce numéro est pris."
fi
fi
# Le tag va désigner le HEAD du dépôt du code : un travail non committé ne serait donc
# pas dans la release, alors même que l'archive, elle, le contiendrait.
if [ "$archive_seule" -eq 0 ] && [ -n "$(git -C "$source_dir" status --porcelain)" ]; then
echoerr "Le dépôt du code a des modifications non committées :"
#
# ⚠️ Ce contrôle était auparavant SAUTÉ en `--archive-seule`, qui produisait donc en
# silence une archive contenant du travail non committé. Il s'exécute désormais toujours,
# et se contente d'avertir quand rien n'est publié : c'est précisément l'écart qu'on veut
# voir quand on reprend une publication à la main.
if [ -n "$(git -C "$source_dir" status --porcelain)" ]; then
git -C "$source_dir" status --short >&2
exit 1
refuser "" \
"Le dépôt du code a des modifications non committées (ci-dessus)." \
"Le tag désignera HEAD : ce travail ne serait pas dans la release."
fi
echo
echo "=== À publier ==="
echo " version : ${precedente:-} -> $version"
if [ -d "$source_dir/.git" ]; then
branche="$(git -C "$source_dir" rev-parse --abbrev-ref HEAD)"
echo " code : $source_dir ($branche, $(git -C "$source_dir" rev-parse --short HEAD))"
else
echo " code : $source_dir"
fi
echo " paquet : $racine_paquet"
[ "$archive_seule" -eq 1 ] &&
echo " mode : --archive-seule (ni manifeste, ni commit, ni tag)"
-82
View File
@@ -1,82 +0,0 @@
## Comment l'authentification fonctionne
MaBibli n'a **pas de connexion propre**. L'identité vient entièrement du portail YunoHost :
nginx authentifie le visiteur, puis SSOwat injecte dans la requête les en-têtes `YNH_USER`,
`YNH_USER_EMAIL` et `YNH_USER_FULLNAME`, que l'application se contente de lire.
Conséquences pratiques :
- **Qui peut voir la bibliothèque se règle dans les permissions YunoHost** (`mabibli.main`),
pas dans l'application.
- La collection est **commune** à toutes les personnes autorisées. Chaque livre garde une trace
de qui l'a saisi, mais personne n'est cloisonné : c'est une bibliothèque de foyer.
- Les **statuts de lecture sont personnels** (chacun sa progression), les **prêts sont communs**
(un livre absent l'est pour tout le monde).
- Se déconnecter du portail YunoHost ne déconnecte pas nécessairement des applications :
chacune garde sa propre session. C'est une limite connue de YunoHost, pas de MaBibli.
⚠️ **Ne pas exposer le port interne.** Le service écoute volontairement sur `127.0.0.1`
uniquement. L'application fait confiance à `YNH_USER` parce que SSOwat écrase cet en-tête à
chaque requête ; un service joignable directement permettrait à quiconque de forger
`YNH_USER` et de contourner le portail. Le port n'est pas ouvert au pare-feu, et
`ASPNETCORE_URLS` dans l'unité systemd ne doit jamais être élargi à `0.0.0.0` ou `*`.
## Domaine entier obligatoire
MaBibli s'installe **sur un domaine entier**, pas sous un sous-chemin (`/mabibli`).
Le client est une application Blazor WebAssembly : son chemin de base et les empreintes
d'intégrité de son service worker sont figés **à la compilation**. Comme le paquet installe une
archive déjà compilée — et ne compile jamais rien sur le serveur — il n'existe pas de moyen
propre de les réécrire à l'installation. YunoHost refusera donc un changement d'URL vers un
sous-chemin.
## Où vivent les données
| Quoi | Où |
|---|---|
| Binaires et client web | `/var/www/mabibli` (appartient à `root`, l'application ne peut pas s'y écrire) |
| Base SQLite | `/home/yunohost.app/mabibli/mabibli.db` |
| Journaux | `journalctl -u mabibli` |
La base est **délibérément séparée des binaires** : une mise à jour remplace intégralement
`/var/www/mabibli` sans jamais toucher aux données. Le schéma est migré automatiquement au
démarrage, il n'y a aucune commande à lancer après une mise à jour.
## Sauvegarde
La base tourne en mode **WAL**. C'est important pour qui voudrait bricoler une sauvegarde à la
main : à un instant donné, l'essentiel des données peut se trouver dans `mabibli.db-wal` et
**pas** dans `mabibli.db`. Mesuré sur une base fraîchement migrée, `mabibli.db` faisait 4 Ko —
et ne contenait **aucune table** — pendant que le fichier `-wal` en portait 205 Ko.
Le script de sauvegarde du paquet ne copie donc pas les fichiers tels quels : il demande à
SQLite un instantané cohérent (`.backup`, l'API de sauvegarde en ligne), déposé à côté de la
base sous le nom `mabibli-instantane.db`. C'est ce fichier que la restauration remet en place,
en écartant au passage les `-wal` / `-shm` de l'archive, qui décrivaient l'état d'une autre
copie de la base.
Le service **n'est pas arrêté** pendant la sauvegarde : l'API de sauvegarde en ligne garantit la
cohérence du fichier produit sans bloquer les lectures, et couper l'application à chaque
sauvegarde nocturne coûterait une indisponibilité pour rien.
Pour une sauvegarde manuelle, la bonne commande est donc :
```bash
sqlite3 /home/yunohost.app/mabibli/mabibli.db ".backup '/quelque/part/mabibli.db'"
```
et surtout pas un `cp` du seul fichier `.db`.
## Le scan du code-barres exige HTTPS
L'accès à la caméra n'est autorisé par les navigateurs que dans un contexte sécurisé. En
production, le certificat Let's Encrypt de YunoHost suffit. En revanche, joindre le serveur par
son IP locale (`http://192.168.x.x`) fera **toujours** échouer le scan : ce n'est pas un
contexte sécurisé. La saisie manuelle de l'ISBN reste disponible dans tous les cas.
## Accès sortant nécessaire
Le serveur doit pouvoir joindre `catalogue.bnf.fr` et `openlibrary.org` en HTTPS pour
pré-remplir les fiches à partir d'un ISBN. Sans accès sortant, l'application fonctionne, mais
toute saisie devient manuelle.
-9
View File
@@ -1,9 +0,0 @@
MaBibli est une application de gestion de bibliothèque personnelle, pensée pour un foyer.
- **Catalogue** des livres physiques et des ebooks (fiches uniquement, aucun fichier n'est hébergé).
- **Prêts** : à qui le livre a été confié, depuis quand, et l'historique complet des prêts passés.
- **Scan ISBN** au code-barres depuis le téléphone, ou saisie manuelle, avec pré-remplissage automatique du titre, de l'auteur, de l'éditeur et de la couverture.
- **Statuts de lecture** personnels : chaque membre du foyer suit sa propre progression sur une collection commune.
- **Consultation hors-ligne** : l'application est une PWA installable, et la bibliothèque reste consultable sans réseau.
Les métadonnées viennent du catalogue de la **BnF** puis d'**OpenLibrary**, deux sources libres et sans clé d'API. Aucune dépendance à Google Books.
+26 -1
View File
@@ -37,8 +37,33 @@ sso = true
# ~170 Mo de publish self-contained (runtime .NET embarqué), plus la base et la marge
# de décompression de l'archive.
disk = "500M"
# ⚠️ `ram.build` ne désigne AUCUNE compilation : rien n'est compilé sur le serveur, c'est
# tout l'intérêt du self-contained. Il couvre le téléchargement et la décompression de
# l'archive de 69 Mo. Ne pas aller chercher la valeur du côté d'un build .NET.
ram.build = "50M"
ram.runtime = "200M"
# Mesuré sur le serveur le 2026-08-22, service en marche :
# cat /sys/fs/cgroup/system.slice/mabibli.service/memory.peak -> 208 793 600 o
# soit 199,1 Mio de pic, contre 200M déclarés jusqu'ici : 0,4 % de marge, c'est-à-dire
# aucune. Porté à 256M.
#
# ⚠️ `systemctl show -p MemoryPeak` ne renvoie RIEN sur ce serveur — la commande réussit
# en silence et l'on croit lire un pic là où l'on ne lit rien. Passer par le cgroup.
#
# ⚠️ Le chiffre MAJORE le besoin : `memory.peak` compte le cache de fichiers, donc une
# part des 69 Mo de binaire mappé, récupérable sous pression — et le GC de .NET se serre
# sur une machine étroite. Mais YunoHost compare une déclaration statique à la RAM
# disponible, sans faire cette nuance. L'asymétrie tranche : un refus d'installation est
# visible et se contourne, un OOM survient plus tard, sous une opération lourde, sans que
# rien ne désigne la cause.
#
# ⚠️ Le pic n'est PAS le lookup ISBN, contrairement à l'intuition — une notice. C'est
# « Nouveautés » sur un auteur très réédité : jusqu'à dix pages de 100 notices SRU en
# parallèle, plus le catalogue entier chargé pour le rapprochement. Ce cas-là n'a pas
# encore été mesuré, et il croît avec la taille de la bibliothèque : à reprendre le jour
# où le fonds aura beaucoup grossi.
ram.runtime = "256M"
[install]