Files
oto-enterprise-os-dtp/run_ci.sh
T
Claude Code DTP Worker 68f8c1a718 [DTP-Worker 20260802_170601] Sprint 8 · buffer · run_ci.sh : -h|--help + en-tête --static honnête (UX du lanceur, PAS un gate #5)
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>
2026-08-02 17:11:19 +00:00

168 lines
8.0 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 dun job : clé à exactement 2 espaces dindentation
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, nexé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