Files
Claude Code DTP Worker 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
[DTP-Worker 20260805_181325] Auto exec · session 20260805_181325
2026-08-05 18:28:26 +00:00

180 lines
9.4 KiB
Bash
Executable File
Raw Permalink 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 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 len-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"