Files
mabibli_ynh/build/publier.sh
T
mathieuandClaude Opus 5 7c7e686c7a Fonds les deux scripts de publication en un seul, qui refuse de mentir
Il y en avait deux, et le mauvais portait le nom le plus convaincant :
`publier-release.sh` compilait, archivait et réécrivait le manifeste, à
charge pour l'appelant de committer. Lancé seul, il a produit les deux
moitiés de la panne du 2026-08-21 — un manifeste corrigé mais non poussé,
et une release v0.4.0 contenant six commits de plus que son tag.

`publier.sh` absorbe l'étape et refuse désormais de publier, avant de
compiler, si le tag de la version existe déjà (en nommant le commit du tag
et celui de HEAD) ou si le dépôt du code n'est pas propre. `--archive-seule`
garde la brique réutilisable en CI, sous le même nom : ni manifeste, ni
commit, ni tag, donc rien à moitié publié.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 23:12:41 +02:00

482 lines
18 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"
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}"
version="${MABIBLI_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 une CI ou un essai ; ne publie rien et ne modifie aucun dépôt.
--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
}
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 ;;
--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; }
#=================================================
# 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)."
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
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."
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=""
[ -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)"
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
}
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
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
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 [ -d "$source_dir/.git" ] && 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."
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
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 :"
git -C "$source_dir" status --short >&2
exit 1
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)"
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 https://git.akbar.nohost.me/mathieu/mabibli_ynh
FIN