From 49123ecba368dc0d62f7117c4a323f5bbb5013e6 Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Sun, 2 Aug 2026 15:16:29 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker=2020260802=5F150601]=20Sprint=208?= =?UTF-8?q?=20=C2=B7=20buffer=20=C2=B7=20DevOps=20outillage=20:=20run=5Fci?= =?UTF-8?q?.sh=20=E2=80=94=20miroir=20LOCAL=20du=20gate=20CI=20d=C3=A9riv?= =?UTF-8?q?=C3=A9=20de=20gate.needs=20(z=C3=A9ro=20liste=20en=20dur),=20re?= =?UTF-8?q?produit=20les=2030=20jobs=20(7=20gates=20ci/*.sh=20+=2023=20sui?= =?UTF-8?q?tes=20module)=20en=20une=20commande=20=C2=B7=20anti-d=C3=A9rive?= =?UTF-8?q?=20par=20construction=20(suit=20ci.yml)=20=C2=B7=20PAS=20un=20g?= =?UTF-8?q?ate=20(#5),=20root=20=E2=88=89=20ci/*.sh=20donc=20hors=20INV-B?= =?UTF-8?q?=20=C2=B7=20parseur=20awk=20anti-commentaire=20=C2=B7=20chemin?= =?UTF-8?q?=20d'=C3=A9chec=20prouv=C3=A9=20(EXIT=3D1=20+=20dump)=20=C2=B7?= =?UTF-8?q?=20doc=20README=20+=20ci/README=20=C2=B7=207/7=20gates=20statiq?= =?UTF-8?q?ues=20verts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- 05_activity_log/2026-08-02.md | 53 ++++++++++++++ README.md | 1 + ci/README.md | 26 +++++++ run_ci.sh | 125 ++++++++++++++++++++++++++++++++++ 4 files changed, 205 insertions(+) create mode 100755 run_ci.sh diff --git a/05_activity_log/2026-08-02.md b/05_activity_log/2026-08-02.md index a74ac6c..39d6642 100644 --- a/05_activity_log/2026-08-02.md +++ b/05_activity_log/2026-08-02.md @@ -1,5 +1,58 @@ # Activity log · 2026-08-02 · Claude Code DTP Worker +## Sprint 8 · buffer · DevOps outillage — `run_ci.sh` : miroir LOCAL du gate CI, dérivé de `gate.needs` (zéro liste en dur · PAS un gate #5) + +**Contexte — friction récurrente, pas une surface à gater.** Ouverture : arbre +propre, **7 gates statiques verts**, **30/30 jobs `gate.needs` verts** (7 gates +`ci/*.sh` + 23 suites de module · 564/564 tests), campagne de gates **saturée** +(mémoires *verify-uncovered* / *guard-flags-usage-not-mention* : les surfaces +restantes sont mono-sourcées ou déjà mordues). Ajouter un gate N+1 serait +redondant (#5). La vraie friction non-adressée est **opérationnelle** : chaque +session rejouait à la main une boucle ad-hoc `for g in guard_constraints … ; do +bash ci/$g.sh` (cf. logs des sessions précédentes « 7 gates re-joués »). Une +telle liste écrite à la main **dérive en silence** de ce que la CI enforce (un +gate ajouté/retiré dans `ci.yml` n'y est jamais répercuté) — même classe de +« vert trompeur » que les gates traquent, appliquée au **lanceur** lui-même. + +**Livraison — `run_ci.sh` à la racine du dépôt.** Un lanceur de commodité qui +reproduit EN LOCAL, en une commande, l'intégralité du gate de merge : +- `bash run_ci.sh` → tout `gate.needs` (30 jobs) · `--static` → les 7 gates + `ci/*.sh` seuls (rapide) · `--list` → plan sans exécution. +- **Aucune liste en dur** : le script PARSE `gate.needs` de + `.gitea/workflows/ci.yml` (la source de vérité que `check_ci_integrity` + verrouille déjà) et exécute, par job, la commande `run:` déclarée dans son + `working-directory:`. Ajouter/retirer un gate en CI se répercute + **automatiquement**, zéro édition — anti-dérive par construction. +- Par périmètre `gate.needs`, exclut à dessein les jobs manuels + `workflow_dispatch` (`e2e-baseline`/`npm test`, qui exige Node) et le job + `gate`. → stdlib pur (bash/awk/git/python3), zéro pip, **zéro commande VPS**. +- Code de sortie **0 ssi tous les jobs passent** (même sémantique que le gate) ; + sur échec, dump des 12 dernières lignes du log fautif + liste des jobs rouges. + +**Ce n'est PAS un gate** (aucun nouveau `ci/*.sh`, aucune nouvelle assertion) : +lanceur hors du glob `ci/*.sh`, donc **hors [INV-B]** de `check_ci_integrity` ; +son exactitude est **structurelle** (exécute ce que ci.yml déclare) et n'a pas à +être re-gatée (#5). Placé à la racine (pas dans `ci/`) précisément pour ne pas +déclencher INV-B qui exigerait de le câbler comme job. + +**Robustesse prouvée.** Parseur awk **anti-commentaire** (un `working-directory:` +ou `bash ci/…` cité en COMMENTAIRE de ci.yml était capturé par une 1ʳᵉ version → +corrigé par `sub(/^[[:space:]]*#.*$/,"")`, cas réel sur le bloc `check-regression`). +Tests : `--list` = 30 jobs dans l'ordre de `gate.needs` (e2e-baseline/gate +correctement absents) ; `run_ci.sh` = **30 PASS / 0 FAIL** ; **chemin d'échec +vérifié** sur un dépôt-jouet synthétique (`bad-job` → FAIL rendu + dump `boom +detail` + **EXIT=1**) ; hors checkout git → EXIT 3 honnête. + +**Doc.** Ligne de nav `README.md` + section « Rejouer le gate EN LOCAL » dans +`ci/README.md` (usage + garantie anti-dérive + rappel « pas un gate #5 »). +Chemins en backticks/liens Markdown vers cibles existantes (`check_docs` vert). + +**Vérif finale.** `bash run_ci.sh --static` avec les nouveaux fichiers stagés → +**7 PASS / 0 FAIL** (guard_constraints · validate_json · check_docs · +check_artifacts · check_regression · check_ci_integrity · check_readme_claims). +Aucun gate perturbé par l'ajout (root ∉ `ci/*.sh` ; nav sans chiffre ; liens +valides). Aucune commande touchant le VPS. + ## Sprint 8 · buffer · Reporting stakeholder (canal 2) — rattrapage du snapshot `daily_reports/2026-08-02` sur les 4 sessions buffer intercalées depuis la clôture `120544` (2ᵉ addendum sourcé · ZÉRO gate ajouté #5) **Nature — réconciliation de reporting, PAS un gate de plus (CLAUDE.md #5).** diff --git a/README.md b/README.md index 780c390..c917469 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ NON-NÉGOCIABLES : [`CLAUDE.md`](CLAUDE.md). | [`05_deliverables_mvp/GAP_ANALYSIS_SPRINT1.md`](05_deliverables_mvp/GAP_ANALYSIS_SPRINT1.md) | Audit d'assets + gap analysis (Sprint 1) | | [`05_activity_log/`](05_activity_log) | Journal de session horodaté | | [`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml) · [`ci/`](ci/README.md) | Gate CI Gitea Actions + guards locaux | +| [`run_ci.sh`](run_ci.sh) | Miroir **local** du gate : rejoue tout `gate.needs` (dérivé de ci.yml, sans liste en dur) | | [`tests/`](tests) | Baseline E2E Playwright | ## Les 13 agents diff --git a/ci/README.md b/ci/README.md index c2c3e16..f2cb56c 100644 --- a/ci/README.md +++ b/ci/README.md @@ -26,6 +26,32 @@ et manuellement (`workflow_dispatch`). Jobs statiques (+ une suite `unittest` pa Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec `bash` + `git` + `python3` (déjà présents sur un runner standard). +### Rejouer le gate EN LOCAL — `run_ci.sh` (racine du dépôt) + +Avant de committer, reproduire l'intégralité du gate de merge en une commande : + +``` +bash run_ci.sh # tout gate.needs (7 gates statiques + 23 suites de module) +bash run_ci.sh --static # seulement les 7 gates ci/*.sh (rapide) +bash run_ci.sh --list # liste les jobs qui seraient lancés, sans exécuter +``` + +`run_ci.sh` n'a **aucune liste en dur** : il DÉRIVE l'ensemble des checks depuis +`gate.needs` de `.gitea/workflows/ci.yml` (la source de vérité que +`check_ci_integrity.sh` verrouille déjà) et exécute, pour chaque job, la +commande `run:` déclarée dans son `working-directory:`. Ajouter/retirer un gate +dans ci.yml se répercute donc **automatiquement**, sans éditer le lanceur — ce +qui élimine la dérive silencieuse des boucles `for g in … ; do bash ci/$g.sh` +écrites à la main de session en session. Par construction il ne balaie que +`gate.needs`, ce qui **exclut** les jobs manuels `workflow_dispatch` +(ex. `e2e-baseline`/`npm test`) et le job `gate` lui-même. Code de sortie 0 ssi +tous les jobs passent (même sémantique que le gate). + +Ce n'est **pas un gate** (aucun nouveau `ci/*.sh`, aucune nouvelle assertion) : +c'est un lanceur de commodité, hors du glob `ci/*.sh` (donc hors périmètre +[INV-B] de `check_ci_integrity`). Son exactitude est **structurelle** (il exécute +ce que ci.yml déclare) et n'a pas à être re-gatée (CLAUDE.md #5). + ## 2. Détail des contrôles ### `guard_constraints.sh` — contraintes NON-NÉGOCIABLES diff --git a/run_ci.sh b/run_ci.sh new file mode 100755 index 0000000..8d4e420 --- /dev/null +++ b/run_ci.sh @@ -0,0 +1,125 @@ +#!/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 : "\t\t". +parse_jobs() { + awk ' + { line=$0; sub(/^[[:space:]]*#.*$/, "", line) } # drop full-line comments + /^jobs:[[:space:]]*$/ { inj=1 } + # début d’un job : clé à exactement 2 espaces d’indentation + 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, n’exé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