Files
oto-enterprise-os-dtp/run_ci.sh
T
Claude Code DTP Worker dcb463ee00 [DTP-Worker 20260802_153601] Sprint 8 · buffer · run_ci.sh : décompte 30/23/7 DÉRIVÉ à l'exécution (retrait des comptes figés du commentaire)
Dernière surface figée dans l'outil « anti-dérive par construction » : run_ci.sh
dérive déjà la liste des checks de gate.needs (zéro liste en dur) mais son
commentaire d'en-tête re-transcrivait à la main « 30 jobs · 23 suites · 7 gates »
— un « vert trompeur » de la classe qu'il combat, logé dans sa propre doc
(ajouter un job module → le script s'adapte, le commentaire se périme en silence).

FIX (éliminer le nombre figé), pas gate N+1 : un lanceur de commodité (root ∉
ci/*.sh) ne mérite pas un gate redondant (#5) qui prendrait la mauvaise autorité.
- ventilation dérivée des mêmes ROWS que l'exécution (n_gates/n_suites/n_jobs) ;
- en-tête d'exécution + pied de --list impriment « 30 = 7 + 23 » (toujours exact) ;
- commentaire reformulé : littéraux 30/23/7 remplacés par la description
  qualitative bash ci/*.sh / python3 -m unittest + note « décompte DÉRIVÉ, jamais
  figé ici » (anti-invention CLAUDE.md #6).

Vérif : --list/--static impriment 30=7+23 · run_ci.sh complet 30 PASS/0 FAIL/0 SKIP ·
7 gates re-joués exit 0. Aucun gate ajouté (#5). Aucune commande touchant au VPS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-02 15:42:38 +00:00

142 lines
7.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
#
# 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
# --- 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)" >&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 '?')"
echo "gate.needs dérivé : $n_jobs jobs = $n_gates gates ci/*.sh + $n_suites suites de module"
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