[DTP-Worker 20260802_150601] Sprint 8 · buffer · DevOps outillage : run_ci.sh — miroir LOCAL du gate CI dérivé de gate.needs (zéro liste en dur), reproduit les 30 jobs (7 gates ci/*.sh + 23 suites module) en une commande · anti-dérive par construction (suit ci.yml) · PAS un gate (#5), root ∉ ci/*.sh donc hors INV-B · parseur awk anti-commentaire · chemin d'échec prouvé (EXIT=1 + dump) · doc README + ci/README · 7/7 gates statiques verts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,58 @@
|
|||||||
# Activity log · 2026-08-02 · Claude Code DTP Worker
|
# 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)
|
## 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).**
|
**Nature — réconciliation de reporting, PAS un gate de plus (CLAUDE.md #5).**
|
||||||
|
|||||||
@@ -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_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é |
|
| [`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 |
|
| [`.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 |
|
| [`tests/`](tests) | Baseline E2E Playwright |
|
||||||
|
|
||||||
## Les 13 agents
|
## Les 13 agents
|
||||||
|
|||||||
@@ -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
|
Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec
|
||||||
`bash` + `git` + `python3` (déjà présents sur un runner standard).
|
`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
|
## 2. Détail des contrôles
|
||||||
|
|
||||||
### `guard_constraints.sh` — contraintes NON-NÉGOCIABLES
|
### `guard_constraints.sh` — contraintes NON-NÉGOCIABLES
|
||||||
|
|||||||
@@ -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 : "<job>\t<wd|->\t<commande run>".
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user