Files
mabibli_ynh/build/publier.sh
T
mathieuandClaude Opus 5 6d88ddb1d0 Restaure les vraies URL : on anonymise une copie, jamais l'original
Le manifeste de la 0.5.0, deja publiee, s'est mis a pointer une archive
introuvable — l'installation echouait sur ynh_setup_source, sans que le
symptome designe la cause.

Le remplacement global des URL avait reecrit amd64.url, c'est-a-dire
precisement le champ que la documentation ecrite le meme jour designait comme
« a ne jamais changer a la main », puisque publier.sh le regenere a chaque
publication.

⚠ L'erreur de conception est plus large : le placeholder avait sa place dans la
COPIE publique, pas dans le depot de travail, qui sert au deploiement reel et
doit rester operationnel. Anonymiser un depot qui sert encore, c'est le casser.

Ce qui reste, et qui vaut : depot_code commande toujours toutes les URL
derivees, et le garde-fou refuse toujours de publier avec une URL en
example.org — il ne se declenche simplement plus ici, puisqu'il sert a la copie
publique.

sha256 inchange : seule l'URL avait bouge, le manifeste retrouve exactement son
etat de publication.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:22:50 +02:00

531 lines
21 KiB
Bash
Executable File

#!/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 <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."
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 <version> <patch|minor|major>
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 <<FIN
=== Terminé (--archive-seule) ===
Archive prête : $archive
sha256 : $somme
Rien n'a été publié : manifest.toml est inchangé, aucun commit, aucun tag.
Pour publier réellement cette version, relancez sans --archive-seule.
FIN
exit 0
fi
#=================================================
# MANIFESTE
#=================================================
echo
echo "=== Mise à jour de manifest.toml ==="
# Le suffixe ~ynhN est conservé s'il existe déjà pour cette version, sinon
# remis à ~ynh1 : un changement de version applicative repart toujours de 1.
version_actuelle="$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' "$manifeste" | head -1)"
if [[ "$version_actuelle" == "$version~ynh"* ]]; then
version_paquet="$version_actuelle"
echo " version : $version_paquet (inchangée)"
else
version_paquet="$version~ynh1"
echo " version : $version_actuelle -> $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 <dépôt> <ref>
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 <<FIN
=== Publié : $version ===
Il ne reste qu'une chose, dans Gitea : créer la release v$version et y téléverser
l'archive.
Releases : $url_releases
Archive : $archive
Ensuite, vérifier que le manifeste pointe sur un fichier réellement téléchargeable
(sans être authentifié) :
curl -fsSLI "$url_archive" | head -1
Puis, sur le serveur :
sudo yunohost app upgrade mabibli -u ${depot_code}_ynh
FIN