Files
oto-enterprise-os-dtp/run_ci.sh
T

126 lines
6.1 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 30 jobs restants sont tous `bash ci/*.sh` (7 gates statiques) ou
# `python3 -m unittest` (23 suites de module) → stdlib pur, aucune install pip,
# aucune commande touchant le VPS.
#
# 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 7 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; }
# --- --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
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 "------------------------------------------------------------------------"
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