Files
oto-enterprise-os-dtp/ci/check_artifacts.sh
T
Claude Code DTP Worker a30ff937f0 [DTP-Worker] Sprint 8 · buffer L75 · Reproductibilité checkout propre : hand-off out/ des 4 générateurs RBAC était .gitignore-é (gate rouge en CI, vert local seulement) + durcissement check_artifacts (build produit ⇒ DOIT être suivi par git)
Défaut réel (même classe que le bug regression_run.json corrigé plus tôt) :
les 4 générateurs RBAC (fixtures_gen/userperm_gen/roleprofile_gen/apply_plan)
.gitignore-aient leur out/, alors qu'ils sont audités en archétype `generator`
(critère HANDOFF). En checkout PROPRE (git archive HEAD = ce que voit le runner
Gitea) leur out/ est absent → qa/audit_4big (dont le build relit le out/ de
CHAQUE module) les note 80<95 ⇒ INV7 ⇒ build refusé ⇒ check-artifacts ET
check-regression ROUGES. Ça ne passait qu'en local via les out/ non suivis
laissés par des build manuels.

Fix : committer le out/ des 4 générateurs (build byte-déterministe prouvé ;
apply_plan lit ses frères en process, pas via out/) → alignement sur les 15
autres générateurs ; audit 100/100 en checkout propre.

Durcissement : check_artifacts.sh exige désormais `git ls-files --error-unmatch`
sur chaque fichier produit → un out/ ignoré/non commité devient une erreur
LOCALE honnête au lieu d'une surprise en CI. Bite-proof : git rm --cached d'un
artefact (laissé sur disque) ⇒ exit 1 ; re-add ⇒ exit 0.

Régénéré : quality_report.json (dérive DOC = taille des 4 README édités).
Vérifs : 6 gates verts sur git archive propre ; 534/21 inchangé ; suites OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 01:42:14 +00:00

108 lines
4.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# ============================================================================
# check_artifacts.sh · OTO Enterprise OS DTP
# ----------------------------------------------------------------------------
# Gate de REPRODUCTIBILITÉ des artefacts (roadmap Sprint 8 · « Regression tests
# exhaustifs » · DevOps CI/CD). Prouve, en une commande, que CHAQUE artefact
# `out/` versionné se régénère À L'IDENTIQUE (byte-for-byte) depuis son
# générateur — donc qu'aucun `out/*.json` n'a dérivé de sa source.
#
# Pourquoi ce gate : plusieurs modules auto-résolvent des valeurs depuis les
# artefacts d'AUTRES modules (ex. demo/scenarios lit qa/audit_4big). Quand la
# source grandit (un compteur passe de 18 à 21…), l'artefact consommateur
# devient périmé s'il n'est pas régénéré — dérive silencieuse qu'aucune suite
# `tests/` (qui teste des FONCTIONS, pas le fichier commité) n'attrape. Ce gate
# la bloque : il compare le build frais au fichier commité.
#
# Contrat vérifié : pour chaque `05_deliverables_mvp/**/out/`, le générateur
# racine (le .py portant une sous-commande `build`) est ré-exécuté vers un
# répertoire temporaire ; tout fichier produit DOIT (a) être SUIVI par git —
# jamais un simple artefact sur disque `.gitignore`-é qui serait absent en CI
# propre — et (b) être identique à son jumeau commité. Les artefacts d'EXÉCUTION
# (non produits par `build`,
# p.ex. qa/regression/out/regression_run.json issu de `run`) ne sont pas
# comparés — le gate n'assert que ce que `build` (déterministe) produit.
#
# Dépendances : bash, git, python3, diff, mktemp. Aucun réseau requis
# (stdlib pur · CLAUDE.md #2 — runner Gitea sans pip).
# ============================================================================
set -uo pipefail
cd "$(git rev-parse --show-toplevel)"
FAIL=0
report() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
ROOT="05_deliverables_mvp"
# Découverte : tout répertoire `out/` sous les livrables. Zéro liste à la main
# (#6) — un module ajouté entre automatiquement dans le gate.
mapfile -t outdirs < <(find "$ROOT" -type d -name out | sort)
if [[ "${#outdirs[@]}" -eq 0 ]]; then
echo "Aucun répertoire out/ — rien à vérifier."
exit 0
fi
echo "== Reproductibilité des artefacts out/ (build frais == commité ?) =="
for outdir in "${outdirs[@]}"; do
mod="$(dirname "$outdir")"
rel="${mod#"$ROOT"/}"
# Générateur = l'unique .py à la racine du module portant `add_parser("build")`.
gen="$(grep -rln 'add_parser("build"\|add_parser('\''build'\''' "$mod"/*.py 2>/dev/null | head -1)"
if [[ -z "$gen" ]]; then
report "$rel — répertoire out/ sans générateur 'build' (config invalide)"
continue
fi
tmp="$(mktemp -d)"
if ! ( cd "$mod" && python3 "$(basename "$gen")" build -o "$tmp" ) >/dev/null 2>"$tmp/.err"; then
report "$rel — 'build' a échoué :"
sed 's/^/ /' "$tmp/.err"
rm -rf "$tmp"
continue
fi
drift=0
produced=0
for f in "$tmp"/*; do
[[ -e "$f" ]] || continue
bn="$(basename "$f")"
produced=$((produced + 1))
committed="$outdir/$bn"
if [[ ! -f "$committed" ]]; then
report "$rel/$bn — produit par 'build' mais ABSENT du dépôt (non commité)"
drift=1
elif ! git ls-files --error-unmatch "$committed" >/dev/null 2>&1; then
# Présent sur disque mais NON SUIVI par git (out/ .gitignore-é, ou jamais
# `git add`). En checkout CI propre le fichier serait ABSENT — donc tout
# consommateur à build reproductible qui lit ce out/ (ex. l'audit 4Big,
# critère HANDOFF, lit le out/ de CHAQUE module) casserait, tandis que le
# gate resterait « vert en local » grâce à un artefact fantôme laissé par
# un build manuel. C'est la classe de bug regression_run.json : on la
# refuse. Un hand-off audité DOIT être commité, jamais seulement sur disque.
report "$rel/$bn — produit par 'build' mais NON SUIVI par git (out/ ignoré ? → absent en CI propre, vert local trompeur)"
drift=1
elif ! diff -q "$f" "$committed" >/dev/null 2>&1; then
report "$rel/$bn — DÉRIVE : le fichier commité diffère du build frais (régénérer)"
drift=1
fi
done
if [[ "$produced" -eq 0 ]]; then
report "$rel — 'build' n'a produit aucun fichier (générateur cassé ?)"
elif [[ "$drift" -eq 0 ]]; then
ok "$rel$produced artefact(s) reproductible(s) à l'identique"
fi
rm -rf "$tmp"
done
echo
if [[ "$FAIL" -eq 0 ]]; then
echo -e "\033[32m✅ Tous les artefacts out/ sont reproductibles (aucune dérive).\033[0m"
else
echo -e "\033[31m❌ Artefact périmé/dérivé détecté — régénérer avec 'build -o out'.\033[0m"
fi
exit "$FAIL"