1a44dfe1c0
CI / Contraintes NON-NÉGOCIABLES (CLAUDE.md) (push) Has been cancelled
CI / Validation JSON (schémas Faisabilité) (push) Has been cancelled
CI / Qualité documentaire (liens + 4Big) (push) Has been cancelled
CI / Reproductibilité des artefacts out/ (build == commité) (push) Has been cancelled
CI / Fraîcheur matrice de régression (run == commité) (push) Has been cancelled
CI / Intégrité du câblage CI (gate agrège tout · gates statiques verrouillés) (push) Has been cancelled
CI / Intégrité des chiffres du README (valeur == artefact cité · (push) Has been cancelled
CI / Intégrité mobile-build.yml (gating portable · activation différée · (push) Has been cancelled
CI / Publiciste · parser + schéma + generator (unittest) (push) Has been cancelled
CI / RBAC · 50 rôles + schéma (unittest) (push) Has been cancelled
CI / Faisabilité · générateur 4 volets + round-trip (unittest) (push) Has been cancelled
CI / RBAC · fixtures ERPNext (Role + Custom DocPerm) (push) Has been cancelled
CI / RBAC · plan User Permission (row-level) (push) Has been cancelled
CI / RBAC · Role Profile (bundles par portail) (push) Has been cancelled
CI / RBAC · run-book d'application unifié (agrégat 3 volets) (push) Has been cancelled
CI / Faisabilité · dossier bancable trilingue FR/EN/ES (push) Has been cancelled
CI / CRM · workflow vente ERPNext (lead → CONFOTUR) (push) Has been cancelled
CI / CRM · DocType porteur OTO Dossier Vente (push) Has been cancelled
CI / CRM · barème commissions vendeurs (push) Has been cancelled
CI / CRM · Financement Bancaire (gate hypothécaire RD) (push) Has been cancelled
CI / Fiscal · e-CF DGII (Compupar) (push) Has been cancelled
CI / Frontend · Workspaces 5 portails rôle (push) Has been cancelled
CI / Legal · DocType CONFOTUR Application (push) Has been cancelled
CI / QA · Audit 5D conformité (push) Has been cancelled
CI / SEO · mots-clés trilingues + schema.org + hreflang (push) Has been cancelled
CI / Chat OTOIA · montage par portail (Custom Block) (push) Has been cancelled
CI / QA · Audit 4Big (95+/100 sur 100% deliverables) (push) Has been cancelled
CI / Démo · Scénarios (run-sheet P07 banquier / P05 client) (push) Has been cancelled
CI / QA · Matrice de régression exhaustive (Sprint 8) (push) Has been cancelled
CI / DevOps · Run-book de déploiement VPS unifié (Sprint 8) (push) Has been cancelled
CI / QA · Matrice d'acceptation / traçabilité MVP (Sprint 8) (push) Has been cancelled
CI / Mobile · config app Expo/EAS (navigation par rôle) (push) Has been cancelled
CI / PIE · manifest de dépendances (Annexe 12 · V10.1) (push) Has been cancelled
CI / E2E baseline Playwright (manuel) (push) Has been cancelled
CI / Gate qualité (agrégat) (push) Has been cancelled
180 lines
9.4 KiB
Bash
Executable File
180 lines
9.4 KiB
Bash
Executable File
#!/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 CÂBLÉ : soit lancé par un job
|
||
# (`run: bash ci/<script>`) dont le nom figure dans `gate.needs`, soit
|
||
# SOURCÉ par ≥1 gate script (lib partagée, ex. ci/lib.sh — jamais un
|
||
# job propre). → un nouveau gate statique non câblé/décâblé, ou une lib
|
||
# morte (sourcée par personne), casse le check.
|
||
#
|
||
# Dépendances : bash, git, awk, grep, sort. Aucun réseau (CLAUDE.md #2).
|
||
# ============================================================================
|
||
set -uo pipefail
|
||
# shellcheck source=ci/lib.sh
|
||
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" || exit 3
|
||
cd_repo_root # racine du dépôt, ou ROUGE honnête si hors arbre git (cf. lib.sh)
|
||
|
||
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 ci/*.sh est câblé (lancé par un job du gate, ou lib sourcée) =="
|
||
|
||
# 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
|
||
# Pas lancé par un job → soit une LIB sourcée (ex. ci/lib.sh), soit un gate
|
||
# mort. Une lib est légitime SSI ≥1 gate script (lui-même lancé par un job)
|
||
# la source. On grepe les gate scripts sur disque pour une ligne
|
||
# `source …/<base>` ou `. …/<base>` (jamais un commentaire : ancrée en début
|
||
# de ligne, `#` exclu). Sinon → vraiment mort.
|
||
base="$(basename "$script")"; base_re="${base//./\\.}"
|
||
sourced_by=""
|
||
while IFS= read -r gs; do
|
||
[[ -z "$gs" || "$gs" == "$script" || ! -f "$gs" ]] && continue
|
||
if grep -Eq "^[[:space:]]*(source|\.)[[:space:]]+[^#]*${base_re}([[:space:]\"'\`]|\$)" "$gs"; then
|
||
sourced_by="$sourced_by $gs"
|
||
fi
|
||
done < <(printf '%s\n' "$SCRIPT_REFS" | awk '{print $2}' | sort -u)
|
||
if [[ -n "$sourced_by" ]]; then
|
||
ok "$script — lib sourcée par [$(echo $sourced_by | tr ' ' ',')] · non exécutée en propre (hors gate.needs, OK)"
|
||
else
|
||
report "$script — présent dans le dépôt mais lancé/sourcé par AUCUN job CI (gate mort)"
|
||
fi
|
||
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')
|
||
|
||
# --- INV-C : le workflow force le mode UTF-8 de Python (indépendance locale) --
|
||
# Chaque générateur clôt son `build` par un print(✅/accents) vers stdout ; sous
|
||
# une locale ASCII (runner minimal : LC_ALL=C, sans coercition PEP 538), ce print
|
||
# lève UnicodeEncodeError → `check-artifacts` ROUGE et les 25 suites plantent,
|
||
# EN SILENCE le jour où l'image runner change (VERT aujourd'hui car UTF-8). Le
|
||
# correctif est un `env: PYTHONUTF8: "1"` au niveau WORKFLOW (s'applique à tous
|
||
# les jobs). Comme son absence est invisible sur un runner UTF-8, on l'ancre ici :
|
||
# le retirer casse ce gate au lieu de dériver silencieusement. On lit l'`env:`
|
||
# top-level (indenté 2 espaces, AVANT `jobs:` → hors du parseur de jobs).
|
||
echo "== INV-C · le workflow force PYTHONUTF8 (I/O UTF-8 · indépendance locale runner) =="
|
||
utf8_mode="$(awk '
|
||
/^jobs:[[:space:]]*$/ { exit } # ne considérer que l’en-tête (avant jobs:)
|
||
/^env:[[:space:]]*$/ { inenv=1; next }
|
||
inenv && /^[^[:space:]]/ { inenv=0 } # fin du bloc env: (clé non indentée)
|
||
inenv && match($0, /^[[:space:]]+PYTHONUTF8:[[:space:]]*["'\'']?1["'\'']?[[:space:]]*$/) { print "1" }
|
||
' "$CI")"
|
||
if [[ "$utf8_mode" == "1" ]]; then
|
||
ok "workflow env: PYTHONUTF8=\"1\" → stdout/défaut-fichier UTF-8 sur tout runner (builds + suites locale-robustes)"
|
||
else
|
||
report "workflow SANS \`env: PYTHONUTF8: \"1\"\` top-level → gate locale-fragile (RED silencieux sur runner ASCII · cf. print ✅ en fin de build)"
|
||
fi
|
||
|
||
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"
|