Files
oto-enterprise-os-dtp/ci/check_ci_integrity.sh
T

137 lines
6.6 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
# ============================================================================
# check_ci_integrity.sh · OTO Enterprise OS DTP
# ----------------------------------------------------------------------------
# Intégrité du câblage du workflow CI Gitea (gate anti-dérive · Sprint 8 buffer).
#
# Le job d'agrégat `gate` est le SEUL verrou de merge : un check qui n'y figure
# pas dans `needs:` ne bloque RIEN, même rouge. Les couvertures existantes
# (audit_4big/registry, qa/regression/discovery) ne prouvent l'appartenance au
# gate QUE pour les jobs de MODULE (ceux qui portent un `working-directory:` sous
# 05_deliverables_mvp). Les gates STATIQUES (`constraints-guard`, `validate-json`,
# `check-docs`, `check-artifacts`, `check-regression` → scripts `ci/*.sh`) n'ont
# pas de working-directory : rien ne garantissait qu'ils alimentent le gate.
# Retirer l'un d'eux de `needs:` rendait le gate vert en le court-circuitant —
# même classe de dérive silencieuse que INV4 (orphan_tests_dirs), côté câblage CI.
#
# Ce gate prouve, par des FAITS lus dans ci.yml (jamais une liste à la main) :
# [INV-A] `gate.needs` == { tous les jobs définis } { gate } { jobs manuels }
# (un job manuel = gardé par `if: … workflow_dispatch …`, ex. e2e-baseline,
# légitimement hors du gate push/PR car il exige un serveur live).
# → aucun job oublié du gate (MISSING) ni référence fantôme (DANGLING).
# [INV-B] Chaque script `ci/*.sh` du dépôt est lancé par exactement un job
# (`run: bash ci/<script>`), et ce job figure dans `gate.needs`.
# → un nouveau gate statique non câblé, ou décâblé, casse le check.
#
# Dépendances : bash, git, awk, grep, sort. Aucun réseau (CLAUDE.md #2).
# ============================================================================
set -uo pipefail
cd "$(git rev-parse --show-toplevel)"
CI=".gitea/workflows/ci.yml"
FAIL=0
report() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
if [[ ! -f "$CI" ]]; then
report "workflow introuvable : $CI"
echo -e "\n\033[31m❌ CI absent.\033[0m"; exit 1
fi
# --- Extraction (awk · parseur YAML minimal, cohérent avec q4lib/registry) ---
# On ne considère que ce qui suit la ligne `jobs:` (le bloc `on:` a lui aussi des
# clés à 2 espaces : push/pull_request/workflow_dispatch — à ne pas confondre
# avec des jobs). Un job = clé `^ <nom>:` à 2 espaces exactement. Pour chaque
# job on note s'il est MANUEL (son bloc contient `workflow_dispatch` dans un
# garde `if:`), le job du bloc `gate` et sa ligne `needs: [...]`, et les scripts
# `ci/*.sh` lancés par un `run:` (avec le job appelant).
ALL_JOBS="$(awk '
/^jobs:[[:space:]]*$/ { injobs=1; next }
injobs && match($0, /^ ([A-Za-z0-9_-]+):[[:space:]]*$/, m) { print m[1] }
' "$CI" | sort)"
MANUAL_JOBS="$(awk '
/^jobs:[[:space:]]*$/ { injobs=1; next }
injobs && match($0, /^ ([A-Za-z0-9_-]+):[[:space:]]*$/, m) { job=m[1]; next }
injobs && job != "" && /if:/ && /workflow_dispatch/ { print job }
' "$CI" | sort -u)"
GATE_NEEDS="$(awk '
/^jobs:[[:space:]]*$/ { injobs=1; next }
injobs && match($0, /^ ([A-Za-z0-9_-]+):[[:space:]]*$/, m) { job=m[1]; next }
injobs && job=="gate" && match($0, /needs:[[:space:]]*\[(.*)\]/, m) {
n=split(m[1], a, ",")
for (i=1;i<=n;i++) { gsub(/[[:space:]]/,"",a[i]); if (a[i]!="") print a[i] }
}
' "$CI" | sort -u)"
# scripts référencés : "<job> <chemin ci/xxx.sh>"
SCRIPT_REFS="$(awk '
/^jobs:[[:space:]]*$/ { injobs=1; next }
injobs && match($0, /^ ([A-Za-z0-9_-]+):[[:space:]]*$/, m) { job=m[1]; next }
injobs && job != "" && match($0, /bash (ci\/[A-Za-z0-9_./-]+\.sh)/, m) { print job, m[1] }
' "$CI" | sort -u)"
# --- INV-A : gate.needs == jobs gate manuels -----------------------------
echo "== INV-A · gate.needs couvre tout job non-manuel (aucun oubli / fantôme) =="
if [[ -z "$GATE_NEEDS" ]]; then
report "job 'gate' sans liste needs: [...] exploitable"
fi
# Ensemble attendu = tous les jobs sauf 'gate' lui-même et les jobs manuels.
EXPECTED="$(comm -23 \
<(printf '%s\n' "$ALL_JOBS" | grep -vx 'gate') \
<(printf '%s\n' "$MANUAL_JOBS") )"
# MISSING : attendus mais absents de needs → checks qui ne bloquent pas.
missing="$(comm -23 <(printf '%s\n' "$EXPECTED") <(printf '%s\n' "$GATE_NEEDS"))"
# DANGLING : cités dans needs mais non définis (typo / job renommé/supprimé).
dangling="$(comm -23 <(printf '%s\n' "$GATE_NEEDS") <(printf '%s\n' "$ALL_JOBS"))"
# MANUAL-IN-GATE : un job manuel dans needs bloquerait le gate sur push (jamais
# déclenché) — le gate resterait en attente. Anomalie à signaler.
manual_in_gate="$(comm -12 <(printf '%s\n' "$MANUAL_JOBS") <(printf '%s\n' "$GATE_NEEDS"))"
for j in $missing; do report "job '$j' défini mais ABSENT de gate.needs → il ne bloque pas le merge"; done
for j in $dangling; do report "gate.needs référence '$j' qui n'est PAS un job défini (typo / job supprimé)"; done
for j in $manual_in_gate; do report "job manuel '$j' présent dans gate.needs → gate en attente sur push/PR"; done
if [[ -z "$missing$dangling$manual_in_gate" ]]; then
njobs="$(printf '%s\n' "$ALL_JOBS" | grep -c .)"
nneeds="$(printf '%s\n' "$GATE_NEEDS" | grep -c .)"
nman="$(printf '%s\n' "$MANUAL_JOBS" | grep -c . || true)"
ok "gate.needs = $nneeds jobs = $njobs définis 1 (gate) $nman manuel(s) [$(printf '%s' "$MANUAL_JOBS" | paste -sd, -)]"
fi
# --- INV-B : tout ci/*.sh est câblé à un job, lui-même dans le gate ----------
echo "== INV-B · chaque gate statique ci/*.sh est lancé par un job du gate =="
# Jobs référençant chaque script (via SCRIPT_REFS).
while IFS= read -r script; do
[[ -z "$script" ]] && continue
jobs_for="$(printf '%s\n' "$SCRIPT_REFS" | awk -v s="$script" '$2==s {print $1}' | sort -u)"
if [[ -z "$jobs_for" ]]; then
report "$script — présent dans le dépôt mais lancé par AUCUN job CI (gate mort)"
continue
fi
# Chaque job qui lance ce script doit alimenter le gate.
notgated=""
for j in $jobs_for; do
printf '%s\n' "$GATE_NEEDS" | grep -qx "$j" || notgated="$notgated $j"
done
if [[ -n "$notgated" ]]; then
report "$script — lancé par [$(echo $jobs_for | tr ' ' ',')] mais job(s) hors gate.needs :$notgated"
else
ok "$script — lancé par [$(echo $jobs_for | tr ' ' ',')] · dans le gate"
fi
done < <(git ls-files 'ci/*.sh' | grep -vx 'ci/check_ci_integrity.sh')
echo
if [[ "$FAIL" -eq 0 ]]; then
echo -e "\033[32m✅ Câblage CI intègre (gate agrège tout job non-manuel · gates statiques verrouillés).\033[0m"
else
echo -e "\033[31m❌ Dérive de câblage CI — corriger gate.needs / jobs avant merge.\033[0m"
fi
exit "$FAIL"