[DTP-Worker] Sprint 8 · buffer L75 · Fix "vert trompeur" : les 6 gates ci/*.sh passaient exit 0 VERT hors d'un arbre git (cd "$(git rev-parse …)" → cd "" no-op succès → git ls-files vide → 0 fichier contrôlé). Helper partagé ci/lib.sh::cd_repo_root échoue bruyamment (exit 3). INV-B (check_ci_integrity) durci : un ci/*.sh est câblé s'il est lancé par un job OU sourcé par un gate (prouve que lib.sh n'est pas mort).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+17
-3
@@ -93,9 +93,11 @@ ne bloque **rien**, même rouge. Deux invariants :
|
|||||||
**oublié** du gate (MISSING → ne bloque pas), une **référence fantôme** (DANGLING →
|
**oublié** du gate (MISSING → ne bloque pas), une **référence fantôme** (DANGLING →
|
||||||
typo / job renommé-supprimé) et un job **manuel** glissé dans `needs` (gate en
|
typo / job renommé-supprimé) et un job **manuel** glissé dans `needs` (gate en
|
||||||
attente perpétuelle sur push).
|
attente perpétuelle sur push).
|
||||||
- **INV-B** — chaque script `ci/*.sh` du dépôt est lancé par exactement un job
|
- **INV-B** — chaque script `ci/*.sh` du dépôt est **câblé** : soit lancé par un job
|
||||||
(`run: bash ci/<script>`), lui-même dans `gate.needs`. Un nouveau gate statique
|
(`run: bash ci/<script>`) lui-même dans `gate.needs`, soit **sourcé** par ≥1 gate
|
||||||
**non câblé** (script mort) ou **décâblé** casse le check.
|
script (lib partagée `ci/lib.sh` — jamais un job propre). Un nouveau gate statique
|
||||||
|
**non câblé** (script mort) ou **décâblé**, ou une **lib morte** (sourcée par
|
||||||
|
personne), casse le check.
|
||||||
|
|
||||||
Ferme le trou laissé par les couvertures existantes (`audit_4big/registry`,
|
Ferme le trou laissé par les couvertures existantes (`audit_4big/registry`,
|
||||||
`qa/regression/discovery`) qui ne prouvent l'appartenance au gate que des jobs de
|
`qa/regression/discovery`) qui ne prouvent l'appartenance au gate que des jobs de
|
||||||
@@ -103,6 +105,18 @@ Ferme le trou laissé par les couvertures existantes (`audit_4big/registry`,
|
|||||||
working-directory n'étaient gardés par personne. Même classe d'anti-dérive que INV4
|
working-directory n'étaient gardés par personne. Même classe d'anti-dérive que INV4
|
||||||
(disque→CI), appliquée au **câblage** CI. stdlib pur (bash/awk/git), zéro réseau.
|
(disque→CI), appliquée au **câblage** CI. stdlib pur (bash/awk/git), zéro réseau.
|
||||||
|
|
||||||
|
### `lib.sh` — helper partagé (sourcé, jamais exécuté seul)
|
||||||
|
Les 6 gates commençaient tous par `cd "$(git rev-parse --show-toplevel)"`. **Hors**
|
||||||
|
d'un arbre de travail git (tarball, `git archive | tar -x`, `git` absent du PATH),
|
||||||
|
`git rev-parse` n'écrit rien → `cd ""` est un **no-op qui RETOURNE SUCCÈS** : le gate
|
||||||
|
poursuivait, `git ls-files` renvoyait une liste **vide**, et `validate_json` /
|
||||||
|
`guard_constraints` / `check_docs` sortaient **exit 0 VERT en n'ayant RIEN
|
||||||
|
contrôlé** — le pire « vert trompeur ». `ci/lib.sh` factorise ce préambule dans
|
||||||
|
`cd_repo_root`, qui **échoue bruyamment** (exit `3`, code distinct d'un échec de
|
||||||
|
contrôle `1`) hors d'un checkout git. `check_ci_integrity` (INV-B) prouve que
|
||||||
|
`lib.sh` est bien sourcé par des gates (pas du code mort) sans exiger qu'il figure
|
||||||
|
dans un job.
|
||||||
|
|
||||||
## 3. Exécution locale (avant push)
|
## 3. Exécution locale (avant push)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -27,7 +27,9 @@
|
|||||||
# (stdlib pur · CLAUDE.md #2 — runner Gitea sans pip).
|
# (stdlib pur · CLAUDE.md #2 — runner Gitea sans pip).
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
# 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)
|
||||||
|
|
||||||
FAIL=0
|
FAIL=0
|
||||||
report() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; }
|
report() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; }
|
||||||
|
|||||||
@@ -19,14 +19,18 @@
|
|||||||
# (un job manuel = gardé par `if: … workflow_dispatch …`, ex. e2e-baseline,
|
# (un job manuel = gardé par `if: … workflow_dispatch …`, ex. e2e-baseline,
|
||||||
# légitimement hors du gate push/PR car il exige un serveur live).
|
# 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).
|
# → aucun job oublié du gate (MISSING) ni référence fantôme (DANGLING).
|
||||||
# [INV-B] Chaque script `ci/*.sh` du dépôt est lancé par exactement un job
|
# [INV-B] Chaque script `ci/*.sh` du dépôt est CÂBLÉ : soit lancé par un job
|
||||||
# (`run: bash ci/<script>`), et ce job figure dans `gate.needs`.
|
# (`run: bash ci/<script>`) dont le nom figure dans `gate.needs`, soit
|
||||||
# → un nouveau gate statique non câblé, ou décâblé, casse le check.
|
# 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).
|
# Dépendances : bash, git, awk, grep, sort. Aucun réseau (CLAUDE.md #2).
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
# 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"
|
CI=".gitea/workflows/ci.yml"
|
||||||
FAIL=0
|
FAIL=0
|
||||||
@@ -105,14 +109,31 @@ if [[ -z "$missing$dangling$manual_in_gate" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# --- INV-B : tout ci/*.sh est câblé à un job, lui-même dans le gate ----------
|
# --- INV-B : tout ci/*.sh est câblé à un job, lui-même dans le gate ----------
|
||||||
echo "== INV-B · chaque gate statique ci/*.sh est lancé par un job du 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).
|
# Jobs référençant chaque script (via SCRIPT_REFS).
|
||||||
while IFS= read -r script; do
|
while IFS= read -r script; do
|
||||||
[[ -z "$script" ]] && continue
|
[[ -z "$script" ]] && continue
|
||||||
jobs_for="$(printf '%s\n' "$SCRIPT_REFS" | awk -v s="$script" '$2==s {print $1}' | sort -u)"
|
jobs_for="$(printf '%s\n' "$SCRIPT_REFS" | awk -v s="$script" '$2==s {print $1}' | sort -u)"
|
||||||
if [[ -z "$jobs_for" ]]; then
|
if [[ -z "$jobs_for" ]]; then
|
||||||
report "$script — présent dans le dépôt mais lancé par AUCUN job CI (gate mort)"
|
# 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
|
continue
|
||||||
fi
|
fi
|
||||||
# Chaque job qui lance ce script doit alimenter le gate.
|
# Chaque job qui lance ce script doit alimenter le gate.
|
||||||
|
|||||||
+3
-1
@@ -8,7 +8,9 @@
|
|||||||
# Dépendances : bash, git, grep, sed. Aucun réseau requis.
|
# Dépendances : bash, git, grep, sed. Aucun réseau requis.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
# 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)
|
||||||
|
|
||||||
FAIL=0
|
FAIL=0
|
||||||
report() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; }
|
report() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; }
|
||||||
|
|||||||
@@ -34,7 +34,9 @@
|
|||||||
# (stdlib pur · CLAUDE.md #2 — runner Gitea sans pip).
|
# (stdlib pur · CLAUDE.md #2 — runner Gitea sans pip).
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
# 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)
|
||||||
|
|
||||||
FAIL=0
|
FAIL=0
|
||||||
report() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; }
|
report() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; }
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
# 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)
|
||||||
|
|
||||||
# Marqueur de prohibition : si présent sur la ligne, c'est un rappel de règle.
|
# Marqueur de prohibition : si présent sur la ligne, c'est un rappel de règle.
|
||||||
PROHIBITION='jamais|JAMAIS|Jamais|❌|SEUL|Seul|only|Only|ONLY|pas de|pas d|\(pas |NON-|non-|[Ii]nterdit|INTERDIT|forbidden|proscrit|éliminer'
|
PROHIBITION='jamais|JAMAIS|Jamais|❌|SEUL|Seul|only|Only|ONLY|pas de|pas d|\(pas |NON-|non-|[Ii]nterdit|INTERDIT|forbidden|proscrit|éliminer'
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# lib.sh · OTO Enterprise OS DTP · helpers PARTAGÉS des gates ci/*.sh
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Fichier SOURCÉ (jamais exécuté en propre : aucun job `run: bash ci/lib.sh`).
|
||||||
|
# check_ci_integrity.sh (INV-B) le reconnaît comme lib câblée parce qu'≥1 gate
|
||||||
|
# le source — il n'a donc pas à figurer dans un job CI ni dans gate.needs.
|
||||||
|
#
|
||||||
|
# --- Pourquoi ce helper existe (bug « vert trompeur » corrigé) --------------
|
||||||
|
# Tous les gates commençaient par `cd "$(git rev-parse --show-toplevel)"`.
|
||||||
|
# HORS d'un arbre de travail git (tarball, `git archive | tar -x`, git absent
|
||||||
|
# du PATH, working-dir erroné), `git rev-parse --show-toplevel` n'écrit RIEN sur
|
||||||
|
# stdout → la commande devient `cd ""`, un **no-op qui RETOURNE SUCCÈS** (le
|
||||||
|
# script reste dans le cwd courant au lieu d'échouer). Les gates poursuivaient
|
||||||
|
# alors sur un `git ls-files` qui renvoie une liste VIDE → validate_json /
|
||||||
|
# guard_constraints / check_docs affichaient « rien à valider » et sortaient
|
||||||
|
# **exit 0 VERT en n'ayant RIEN vérifié**. Un gate qui passe au vert sans rien
|
||||||
|
# contrôler est la pire dérive : c'est exactement le « vert trompeur » que la
|
||||||
|
# série anti-dérive du buffer S8 traque. `cd_repo_root` le transforme en ROUGE
|
||||||
|
# honnête (exit 3, code distinct d'un échec de contrôle 1).
|
||||||
|
#
|
||||||
|
# Dépendances : bash, git. Aucun réseau (CLAUDE.md #2 · runner Gitea sans pip).
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# cd_repo_root — se positionne à la racine du dépôt git, ou ÉCHOUE bruyamment.
|
||||||
|
# À appeler tout en haut de chaque gate, à la place de `cd "$(git rev-parse …)"`.
|
||||||
|
cd_repo_root() {
|
||||||
|
local root
|
||||||
|
root="$(git rev-parse --show-toplevel 2>/dev/null)" || true
|
||||||
|
if [[ -z "$root" ]]; then
|
||||||
|
printf '\033[31m❌ ERREUR gate : hors d'\''un arbre de travail git '\
|
||||||
|
'(git rev-parse --show-toplevel n'\''a rien renvoyé).\033[0m\n' >&2
|
||||||
|
printf ' Ce gate DOIT tourner dans un checkout git (actions/checkout en '\
|
||||||
|
'CI). Un `cd ""` silencieux masquerait un VERT TROMPEUR (git ls-files vide → '\
|
||||||
|
'0 fichier contrôlé).\n' >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
cd "$root" || {
|
||||||
|
printf '\033[31m❌ ERREUR gate : cd vers la racine du dépôt (%s) a échoué.'\
|
||||||
|
'\033[0m\n' "$root" >&2
|
||||||
|
exit 3
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -9,7 +9,9 @@
|
|||||||
# Dépendances : bash, git, python3. Aucun réseau requis.
|
# Dépendances : bash, git, python3. Aucun réseau requis.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
# 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)
|
||||||
|
|
||||||
FAIL=0
|
FAIL=0
|
||||||
mapfile -t files < <(git ls-files '*.json')
|
mapfile -t files < <(git ls-files '*.json')
|
||||||
|
|||||||
Reference in New Issue
Block a user