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>
482 lines
18 KiB
Bash
Executable File
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
|