#!/bin/bash # # Publie une version de MaBibli de bout en bout, en une commande. # # 1. choisir la version, à partir de celle déjà publiée # 2. compiler self-contained, vérifier le publish, produire l'archive # 3. réécrire manifest.toml (version, url, sha256) # 4. committer et pousser mabibli_ynh (le serveur lit Gitea, jamais le local) # 5. taguer et pousser mabibli (le tag porte le code de la release) # # La seule chose qui reste manuelle est le dépôt de l'archive dans la release Gitea : # l'URL est rappelée en fin de sortie. # # ⚠️ POURQUOI UN SEUL SCRIPT, ET PAS DEUX # # Ce fichier était jusqu'au 2026-08-21 un orchestrateur appelant `publier-release.sh`, # qui compilait et réécrivait le manifeste. Deux scripts, dont le mauvais portait le nom # le plus convaincant : lancé seul, `publier-release.sh` réécrivait le manifeste sans le # committer, et acceptait de recompiler une version dont le tag existait déjà. # # Les deux moitiés de la panne du 2026-08-21 en sont sorties : une release `v0.4.0` # contenant en réalité six commits de plus que son tag, et un `manifest.toml` corrigé # mais jamais poussé — donc un `yunohost app upgrade` qui échoue sur le sha256, et dont # le retour arrière échoue pour la même raison. # # D'où la fusion. Le mode `--archive-seule` conserve la brique réutilisable en CI # (compiler et archiver, sans toucher ni au manifeste ni à git), mais sous CE nom : il # n'y a plus deux commandes voisines entre lesquelles se tromper. # set -euo pipefail #================================================= # VALEURS PAR DÉFAUT #================================================= racine_paquet="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" manifeste="$racine_paquet/manifest.toml" # ⚠️ 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. # ⚠️ LA SEULE LIGNE À CHANGER pour héberger ce projet ailleurs. # Tout ce qui suit en dérive : URL de release, URL d'archive, message de fin. # # Une copie publique du projet porte ici `https://forge.example.org/mabibli`, injoignable # par construction (example.org est réservé à la documentation par la RFC 2606). Le # contrôle préalable plus bas REFUSE alors de publier, et refuse AVANT de compiler : # celui qui reprend le projet doit renseigner cette ligne avant sa première release, # plutôt que de découvrir le problème à l'installation, dans ynh_setup_source. depot_code="https://git.akbar.nohost.me/mathieu/mabibli" source_dir="$(cd "$racine_paquet/../mabibli" 2>/dev/null && pwd || true)" base_url="$depot_code/releases/download" url_releases="$depot_code/releases" output_dir="$racine_paquet/build/dist" version="" increment="" sans_confirmation=0 archive_seule=0 runtime="linux-x64" projet="MaBibli.Api" usage() { cat <<'FIN' Usage : publier.sh [options] --version X.Y.Z Version à publier (sinon : demandée, à partir de la précédente) --patch Incrémenter le dernier chiffre (0.3.0 -> 0.3.1) --minor Incrémenter le chiffre du milieu (0.3.0 -> 0.4.0) --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 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) --depot-code URL Dépôt public du code, d'où dérivent les URL de release --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 FIN } while [ $# -gt 0 ]; do case "$1" in --version) version="$2"; shift 2 ;; --patch|--minor|--major) increment="${1#--}"; shift ;; --oui|-y) sans_confirmation=1; shift ;; --archive-seule) archive_seule=1; shift ;; --source-dir) source_dir="$2"; shift 2 ;; --depot-code) depot_code="$2"; base_url="$2/releases/download"; url_releases="$2/releases"; shift 2 ;; --base-url) base_url="$2"; shift 2 ;; --output-dir) output_dir="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "Option inconnue : $1" >&2; usage >&2; exit 2 ;; esac 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 [...] 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:-}'" echoerr "Précisez-le avec --source-dir." exit 1 fi source_dir="$(cd "$source_dir" && pwd)" if [ ! -d "$source_dir/$projet" ]; then echoerr "'$source_dir' ne contient pas de projet $projet." exit 1 fi # ⚠️ 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 # ⚠️ Refuser AVANT de compiler, comme les autres garde-fous. Publier avec le placeholder # écrirait dans manifest.toml une `amd64.url` que personne ne peut télécharger — et # `ynh_setup_source` échouerait à l'installation, loin d'ici et sans rapport apparent. if [ "$archive_seule" -eq 0 ] && [[ "$base_url" == *example.org* ]]; then echoerr "Le dépôt public n'est pas configuré : $depot_code" echoerr "Changez « depot_code » en tête de ce script, ou passez --depot-code URL." exit 1 fi if ! command -v dotnet >/dev/null; then echoerr "Le SDK .NET est introuvable. Il n'est nécessaire QUE sur cette machine :" echoerr "le serveur YunoHost reçoit un publish self-contained et n'a pas de SDK." exit 1 fi #================================================= # LA VERSION #================================================= # 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="$(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)" precedente="$(printf '%s\n%s\n' "$version_tag" "$version_manifeste" \ | grep -E '^[0-9]+(\.[0-9]+)*$' | sort -V | tail -1 || true)" suivante() { # suivante IFS=. read -r maj min cor <<<"$1" case "$2" in major) echo "$((maj + 1)).0.0" ;; minor) echo "$maj.$((min + 1)).0" ;; patch) echo "$maj.$min.$((cor + 1))" ;; esac } # ⚠️ 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 fi echo "Dernière version publiée : $precedente" [ "$version_tag" != "$version_manifeste" ] && echo " ⚠ tag v${version_tag:-—} et manifest.toml ${version_manifeste:-—} divergent" if [ -z "$version" ] && [ -n "$increment" ]; then version="$(suivante "$precedente" "$increment")" fi if [ -z "$version" ]; then if [ "$sans_confirmation" -eq 1 ] || [ ! -t 0 ]; then echoerr "Pas de terminal pour demander la version ; utilisez --version ou --patch/--minor/--major." exit 1 fi echo echo " 1) $(suivante "$precedente" patch) (correction)" echo " 2) $(suivante "$precedente" minor) (nouveautés)" echo " 3) $(suivante "$precedente" major) (rupture)" echo " 4) autre — à saisir" echo read -rp "Version à publier [1] ? " choix case "${choix:-1}" in 1) version="$(suivante "$precedente" patch)" ;; 2) version="$(suivante "$precedente" minor)" ;; 3) version="$(suivante "$precedente" major)" ;; 4) read -rp "Numéro de version : " version ;; *) version="$choix" ;; # un numéro tapé directement passe aussi esac fi if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echoerr "Version invalide : '$version' (attendu X.Y.Z)." exit 1 fi #================================================= # CE QUI DOIT ÊTRE VRAI AVANT DE COMPILER #================================================= # ⚠️ 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 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 [ "$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 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. # # ⚠️ 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 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" branche="$(git -C "$source_dir" rev-parse --abbrev-ref HEAD)" echo " code : $source_dir ($branche, $(git -C "$source_dir" rev-parse --short HEAD))" echo " paquet : $racine_paquet" [ "$archive_seule" -eq 1 ] && echo " mode : --archive-seule (ni manifeste, ni commit, ni tag)" if [ "$sans_confirmation" -ne 1 ]; then read -rp "Continuer ? [O/n] " reponse case "${reponse:-o}" in [oO]*) ;; *) echo "Abandon."; exit 1 ;; esac fi #================================================= # COMPILATION #================================================= # Horodatage du build, injecté dans l'assembly à côté de la version. # # ⚠️ Il sert de TÉMOIN : l'application ne montre sa version que si cet horodatage est présent. # Sans lui, le numéro lu serait le « 1.0.0 » que le SDK pose par défaut, qui se lirait comme une # vraie version alors qu'il ne désigne rien — et c'est précisément la valeur qu'on va chercher # pour diagnostiquer un appareil dépareillé. Un binaire compilé à la main se déclare donc # « version de développement », ce qui est vrai. # # `SOURCE_DATE_EPOCH` est respecté, comme pour l'archive : sans cela, deux compilations du même # commit donneraient deux binaires — donc deux sha256 — pour la seule raison que l'heure a # changé, et la reproductibilité visée plus bas ne vaudrait plus rien. if [ -n "${SOURCE_DATE_EPOCH:-}" ]; then date_build="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y-%m-%dT%H:%M:%SZ)" else date_build="$(date -u +%Y-%m-%dT%H:%M:%SZ)" fi travail="$(mktemp -d)" # shellcheck disable=SC2064 trap "rm -rf '$travail'" EXIT publish="$travail/publish" echo echo "=== Compilation self-contained $runtime ===" echo " source : $source_dir" echo " version: $version" echo " build : $date_build" # `--self-contained` embarque le runtime .NET dans le dossier publié : c'est ce qui # permet au paquet YunoHost de ne dépendre d'aucun `dotnet-runtime` côté serveur. # # `MaBibli.Api` référence `MaBibli.Client` : le client Blazor WebAssembly est compilé # au passage et atterrit dans `wwwroot/`. Un seul publish suffit donc pour les trois # projets. dotnet publish "$source_dir/$projet" \ --configuration Release \ --runtime "$runtime" \ --self-contained \ --output "$publish" \ -p:Version="$version" \ -p:MaBibliDateBuild="$date_build" #================================================= # CONTRÔLES SUR LE RÉSULTAT #================================================= echo echo "=== Vérification du publish ===" if [ ! -x "$publish/$projet" ]; then echoerr "Binaire '$projet' absent ou non exécutable dans le publish." exit 1 fi # Sans `wwwroot`, le service démarrerait et répondrait — en ne servant aucune page. # C'est le défaut qui est passé inaperçu pendant trois phases du projet, faute de # regarder ailleurs que la racine. On le vérifie donc ici, à la source. if [ ! -f "$publish/wwwroot/index.html" ]; then echoerr "wwwroot/index.html absent : le client Blazor n'a pas été embarqué." exit 1 fi # `index.html` doit référencer un script réellement présent. Un placeholder non # substitué (`blazor.webassembly#[.{fingerprint}].js`) donnerait une page blanche # alors que `/` répondrait 200. if grep -q '#\[\.{fingerprint}\]' "$publish/wwwroot/index.html"; then echoerr "index.html contient des placeholders d'empreinte non substitués." echoerr "Voir CLAUDE.md, « Empreintes WASM désactivées »." exit 1 fi for ressource in $(grep -oE '(src|href)="[^"]+"' "$publish/wwwroot/index.html" \ | sed -E 's/^(src|href)="//; s/"$//' \ | grep -vE '^(https?:|data:|#|/$|\.$)'); do if [ ! -f "$publish/wwwroot/$ressource" ]; then echoerr "index.html référence '$ressource', absent du publish." exit 1 fi done echo " binaire, wwwroot et ressources d'index.html : OK" #================================================= # ARCHIVE #================================================= archive_nom="mabibli-$version-$runtime.tar.gz" mkdir -p "$output_dir" archive="$output_dir/$archive_nom" echo echo "=== Archive ===" # Fichiers à la racine de l'archive, sans dossier intermédiaire : cohérent avec # `in_subdir = false` dans manifest.toml. # # Options de reproductibilité : sans elles, deux compilations identiques donneraient # deux sha256 différents (dates, ordre de parcours, uid/gid). En CI comme en local, # on veut pouvoir vérifier qu'une archive correspond bien à un commit. tar --create --gzip \ --file="$archive" \ --directory="$publish" \ --owner=0 --group=0 --numeric-owner \ --mtime="@${SOURCE_DATE_EPOCH:-0}" \ --sort=name \ . somme="$(sha256sum "$archive" | cut -d' ' -f1)" taille="$(du -h "$archive" | cut -f1)" taille_octets="$(stat -c%s "$archive")" echo " fichier : $archive" echo " taille : $taille ($taille_octets octets)" echo " sha256 : $somme" url_archive="$base_url/v$version/$archive_nom" if [ "$archive_seule" -eq 1 ]; then cat < $version_paquet" fi python3 - "$manifeste" "$version_paquet" "$url_archive" "$somme" <<'PY' import re, sys chemin, version, url, somme = sys.argv[1:5] with open(chemin, encoding="utf-8") as f: texte = f.read() remplacements = [ (r'^version = ".*"$', f'version = "{version}"'), (r'^ amd64\.url = ".*"$', f' amd64.url = "{url}"'), (r'^ amd64\.sha256 = ".*"$', f' amd64.sha256 = "{somme}"'), ] for motif, valeur in remplacements: texte, n = re.subn(motif, valeur, texte, count=1, flags=re.MULTILINE) if n != 1: sys.exit(f"Motif introuvable dans le manifeste : {motif}") with open(chemin, "w", encoding="utf-8") as f: f.write(texte) PY echo " url : $url_archive" echo " sha256 : $somme" #================================================= # LE PAQUET (le serveur lit Gitea, jamais la copie locale) #================================================= echo echo "=== Paquet : commit et push ===" git -C "$racine_paquet" add -A if git -C "$racine_paquet" diff --cached --quiet; then echo " rien à committer (manifeste déjà à jour)" else git -C "$racine_paquet" commit -m "Publier la version $version" fi git -C "$racine_paquet" push #================================================= # LE CODE : tag et push #================================================= echo echo "=== Code : tag v$version et push ===" git -C "$source_dir" tag "v$version" git -C "$source_dir" push origin "$branche" --tags # Un push qui échoue à demi ne se voit pas dans la sortie de git : on redemande au distant. verifier_pousse() { # verifier_pousse if git -C "$1" ls-remote --exit-code origin "$2" >/dev/null 2>&1; then echo " ✓ $2 présent sur origin" else echoerr " ✗ $2 ABSENT sur origin — le push n'a pas abouti." exit 1 fi } echo echo "=== Vérification côté distant ===" verifier_pousse "$source_dir" "refs/tags/v$version" verifier_pousse "$racine_paquet" "refs/heads/$(git -C "$racine_paquet" rev-parse --abbrev-ref HEAD)" #================================================= # CE QUI RESTE À FAIRE À LA MAIN #================================================= cat <