68f8c1a718
Deux vraies aspérités d'UX sur le lanceur run_ci.sh (miroir local du gate), reproduites avant fix : - `--help`/`-h` échouait « option inconnue » exit 2 alors qu'un bloc d'usage riche dormait dans l'en-tête → ajout usage() branché AVANT résolution git (aide marche hors checkout : exit 0 ; vrai run hors git reste exit 3), codes de sortie documentés (0/1/2/3). - en mode --static l'en-tête annonçait « 30 jobs = 7 gates + 23 suites » alors que seuls 7 gates tournent → récap « 7 PASS » contredisait l'en-tête ; en-tête rendu mode-conscient (« 7 gates seuls, 23 suites ignorées »). - message d'option inconnue + ci/README.md alignés (ajout --help). Toutes les valeurs restent DÉRIVÉES de gate.needs (#6, zéro compte figé). Zéro nouveau module · zéro gate ajouté (#5) : run_ci.sh reste un lanceur (racine ∉ ci/*.sh, hors gate.needs, INV-B). Vérif : --help exit 0 in+hors git · --bogus exit 2 · check_docs/check_readme_claims/guard_constraints verts · run_ci.sh plein → 30 PASS · 0 FAIL · 0 SKIP. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
168 lines
8.0 KiB
Bash
Executable File
168 lines
8.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ============================================================================
|
||
# run_ci.sh · OTO Enterprise OS DTP · miroir LOCAL du gate CI
|
||
# ----------------------------------------------------------------------------
|
||
# Reproduit en une commande, EN LOCAL, exactement l'ensemble de checks que le
|
||
# gate de merge Gitea Actions agrège — sans runner, sans réseau (Sprint 8 ·
|
||
# DevOps « monitoring / regression exhaustifs »).
|
||
#
|
||
# POURQUOI ce script existe : chaque session rejouait à la main une boucle
|
||
# ad-hoc « for g in guard_constraints validate_json … ; do bash ci/$g.sh ». Une
|
||
# telle liste écrite à la main DÉRIVE en silence de ce que la CI enforce
|
||
# réellement (un gate ajouté/retiré dans `.gitea/workflows/ci.yml` n'y était
|
||
# jamais répercuté) — la même classe de « vert trompeur » que les gates du
|
||
# dépôt traquent. Ce script n'a AUCUNE liste en dur : il DÉRIVE la liste des
|
||
# checks depuis `gate.needs` de ci.yml (la source de vérité que
|
||
# `ci/check_ci_integrity.sh` verrouille déjà) et exécute, pour chaque job, la
|
||
# commande `run:` que ci.yml déclare, dans son `working-directory:`. Il ne peut
|
||
# donc ni oublier ni inventer un check : ajouter un gate à la CI le fait
|
||
# apparaître ici automatiquement, zéro édition.
|
||
#
|
||
# PÉRIMÈTRE : uniquement les jobs de `gate.needs` — ce qui exclut, par
|
||
# construction et à dessein, les jobs manuels `workflow_dispatch` hors gate
|
||
# (ex. `e2e-baseline`/`npm test`, qui exige Node) et le job `gate` lui-même.
|
||
# Les jobs restants sont tous soit `bash ci/*.sh` (gates statiques), soit
|
||
# `python3 -m unittest` (suites de module) → stdlib pur, aucune install pip,
|
||
# aucune commande touchant le VPS. Le décompte exact (jobs / gates / suites)
|
||
# est DÉRIVÉ de gate.needs et imprimé à l'exécution — jamais figé dans ce
|
||
# commentaire, où il se périmerait en silence (anti-invention CLAUDE.md #6).
|
||
#
|
||
# CE N'EST PAS un gate (aucun nouveau `ci/*.sh`, aucune nouvelle assertion) :
|
||
# c'est un LANCEUR de commodité. Son exactitude est STRUCTURELLE (il exécute ce
|
||
# que ci.yml déclare), pas une re-transcription à protéger — inutile donc d'y
|
||
# adjoindre un gate N+1 (CLAUDE.md #5).
|
||
#
|
||
# Usage :
|
||
# bash run_ci.sh # exécute tout gate.needs (miroir CI complet)
|
||
# bash run_ci.sh --static # seulement les gates statiques ci/*.sh (rapide)
|
||
# bash run_ci.sh --list # liste les jobs qui SERAIENT lancés, sans exécuter
|
||
# bash run_ci.sh --help # affiche cette aide et sort
|
||
#
|
||
# Sortie : une ligne PASS/FAIL par job + un récapitulatif. Code de sortie 0 ssi
|
||
# tous les jobs passent (identique à la sémantique du gate de merge).
|
||
# ============================================================================
|
||
set -uo pipefail
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
run_ci.sh · miroir LOCAL du gate CI (dérivé de gate.needs, sans liste en dur).
|
||
Rejoue en une commande, sans runner ni réseau, l'ensemble de checks que le gate
|
||
de merge Gitea Actions agrège.
|
||
|
||
Usage :
|
||
bash run_ci.sh exécute tout gate.needs (miroir CI complet)
|
||
bash run_ci.sh --static seulement les gates statiques ci/*.sh (rapide)
|
||
bash run_ci.sh --list liste les jobs qui SERAIENT lancés, sans exécuter
|
||
bash run_ci.sh --help affiche cette aide et sort
|
||
|
||
Sortie : une ligne PASS/FAIL par job + un récapitulatif.
|
||
Code de sortie : 0 ssi tous les jobs passent · 1 si un job échoue ·
|
||
2 option inconnue · 3 hors checkout git / ci.yml illisible.
|
||
EOF
|
||
}
|
||
|
||
# --- aide : disponible AVANT toute résolution git (n'exige aucun checkout) ---
|
||
case "${1:-}" in -h|--help) usage; exit 0 ;; esac
|
||
|
||
# --- racine du dépôt (marche depuis n'importe quel sous-dossier) -------------
|
||
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || {
|
||
echo "run_ci.sh : hors d'un checkout git — impossible de localiser ci.yml." >&2
|
||
exit 3
|
||
}
|
||
cd "$ROOT" || exit 3
|
||
CI_YML=".gitea/workflows/ci.yml"
|
||
[ -f "$CI_YML" ] || { echo "run_ci.sh : $CI_YML introuvable." >&2; exit 3; }
|
||
|
||
MODE="all"
|
||
case "${1:-}" in
|
||
--static) MODE="static" ;;
|
||
--list) MODE="list" ;;
|
||
"") MODE="all" ;;
|
||
*) echo "run_ci.sh : option inconnue « $1 » (attendu : --static | --list | --help)" >&2; exit 2 ;;
|
||
esac
|
||
|
||
# --- parse ci.yml : gate.needs (ordre) + par job {working-directory, run} ----
|
||
# Les commentaires de ligne sont retirés d'abord (un « working-directory: » ou
|
||
# « bash ci/… » cité en commentaire ne doit JAMAIS être capturé). Émet, sur
|
||
# stdout, une ligne par job de gate.needs : "<job>\t<wd|->\t<commande run>".
|
||
parse_jobs() {
|
||
awk '
|
||
{ line=$0; sub(/^[[:space:]]*#.*$/, "", line) } # drop full-line comments
|
||
/^jobs:[[:space:]]*$/ { inj=1 }
|
||
# début d’un job : clé à exactement 2 espaces d’indentation
|
||
inj && match(line, /^ ([a-z0-9_-]+):[[:space:]]*$/, m) { job=m[1]; next }
|
||
# gate.needs : la liste des jobs bloquants (source de vérité)
|
||
inj && job=="gate" && match(line, /needs:[[:space:]]*\[(.*)\]/, g) {
|
||
n=split(g[1], a, ","); for (i=1;i<=n;i++){ gsub(/[[:space:]]/,"",a[i]); if(a[i]!="") needs[++nn]=a[i] }
|
||
}
|
||
inj && job!="" && match(line, /^[[:space:]]+working-directory:[[:space:]]*(.+)$/, w) { wd[job]=w[1] }
|
||
# commande run: (avec un argument — écarte la clé « run: » nue de defaults)
|
||
inj && job!="" && match(line, /[[:space:]]run:[[:space:]]+(.+)$/, r) { cmd[job]=r[1] }
|
||
END {
|
||
for (i=1;i<=nn;i++){ j=needs[i]; printf "%s\t%s\t%s\n", j, (j in wd ? wd[j] : "-"), (j in cmd ? cmd[j] : "") }
|
||
}
|
||
' "$CI_YML"
|
||
}
|
||
|
||
mapfile -t ROWS < <(parse_jobs)
|
||
[ "${#ROWS[@]}" -gt 0 ] || { echo "run_ci.sh : gate.needs vide ou illisible dans $CI_YML." >&2; exit 3; }
|
||
|
||
is_static() { case "$1" in bash\ ci/*) return 0 ;; *) return 1 ;; esac; }
|
||
|
||
# --- décompte DÉRIVÉ (jamais figé) : gates statiques ci/*.sh vs suites module -
|
||
# Ventilé depuis les mêmes ROWS que l'exécution, donc toujours exact : ajouter
|
||
# un job à gate.needs le reclasse ici automatiquement, zéro édition (#6).
|
||
n_gates=0; n_suites=0
|
||
for row in "${ROWS[@]}"; do
|
||
IFS=$'\t' read -r _j _w _c <<<"$row"
|
||
[ -n "$_c" ] || continue
|
||
if is_static "$_c"; then n_gates=$((n_gates+1)); else n_suites=$((n_suites+1)); fi
|
||
done
|
||
n_jobs=$((n_gates + n_suites))
|
||
|
||
# --- --list : montre le plan, n’exécute rien --------------------------------
|
||
if [ "$MODE" = "list" ]; then
|
||
printf '%-28s %-42s %s\n' "JOB" "WORKING-DIR" "RUN"
|
||
for row in "${ROWS[@]}"; do
|
||
IFS=$'\t' read -r job wd cmd <<<"$row"
|
||
printf '%-28s %-42s %s\n' "$job" "$wd" "$cmd"
|
||
done
|
||
printf -- '-- %d jobs dérivés de gate.needs : %d gates statiques ci/*.sh + %d suites de module.\n' \
|
||
"$n_jobs" "$n_gates" "$n_suites"
|
||
exit 0
|
||
fi
|
||
|
||
# --- exécution ---------------------------------------------------------------
|
||
LOGDIR="$(mktemp -d)"
|
||
trap 'rm -rf "$LOGDIR"' EXIT
|
||
pass=0; fail=0; skip=0; failed_jobs=()
|
||
echo "run_ci.sh · miroir du gate CI (mode=$MODE) · $(git rev-parse --short HEAD 2>/dev/null || echo '?')"
|
||
if [ "$MODE" = "static" ]; then
|
||
echo "gate.needs dérivé : $n_jobs jobs · mode --static → $n_gates gates ci/*.sh seuls ($n_suites suites de module ignorées)"
|
||
else
|
||
echo "gate.needs dérivé : $n_jobs jobs = $n_gates gates ci/*.sh + $n_suites suites de module"
|
||
fi
|
||
echo "------------------------------------------------------------------------"
|
||
for row in "${ROWS[@]}"; do
|
||
IFS=$'\t' read -r job wd cmd <<<"$row"
|
||
[ -n "$cmd" ] || { printf ' SKIP %-28s (aucune commande run: dans ci.yml)\n' "$job"; skip=$((skip+1)); continue; }
|
||
if [ "$MODE" = "static" ] && ! is_static "$cmd"; then continue; fi
|
||
log="$LOGDIR/$job.log"
|
||
if [ "$wd" = "-" ]; then dir="$ROOT"; else dir="$ROOT/$wd"; fi
|
||
if ( cd "$dir" && eval "$cmd" ) >"$log" 2>&1; then
|
||
printf ' PASS %-28s %s\n' "$job" "$cmd"; pass=$((pass+1))
|
||
else
|
||
printf ' FAIL %-28s %s\n' "$job" "$cmd"; fail=$((fail+1)); failed_jobs+=("$job")
|
||
sed 's/^/ | /' "$log" | tail -n 12
|
||
fi
|
||
done
|
||
|
||
echo "------------------------------------------------------------------------"
|
||
printf 'Récapitulatif : %d PASS · %d FAIL · %d SKIP\n' "$pass" "$fail" "$skip"
|
||
if [ "$fail" -ne 0 ]; then
|
||
printf 'Jobs en échec : %s\n' "$(IFS=', '; echo "${failed_jobs[*]}")"
|
||
exit 1
|
||
fi
|
||
echo "✅ Tous les checks du gate franchis en local."
|
||
exit 0
|