Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-143 — Couverture CI par couche : ne mesurer/tester que les couches touchées ; couverture complète rare

Statut : Accepté (2026-07-30, décision BDFL). RFC de mécanisme (ADR-015). Complète ADR-031 (couverture en root sur runners self-hosted), ADR-036 (filtrage FULL), ADR-035 (exceptions). S’appuie sur [package.metadata.air] layer = N (couche de chaque crate) et ADR-052/check-layers.

Catégorie : outillage CI (ci.yml + xtask couvrable-vide).

Contexte

Le job test-coverage prend 15‑20 min par PR. Décomposé :

  • Il exécute toujours cargo test --workspace (tous les tests : lib + intégration + doc, dont les gros interop réels — SFTP ≥ 100 Mio, ssh, conformité ABI C). C’est le vrai coût par PR.
  • La couverture lourde (couvrable-vide --strict, llvm-cov instrumenté en root, sccache désactivé ⇒ recompile complète) ne tourne déjà que sur FULL = couche 0 touchée OU push main OU nightly (ADR-036). Elle n’est donc pas le coût du cas courant (PR couche 1/2).

Directive BDFL (2026-07-30). « On ne peut pas passer 15‑20 min pour un test de couverture. Couverture PAR COUCHE : ne relancer que les couches touchées, vérifier la non-dégradation. Réduire drastiquement ce job. Garder la couverture complète pour des occasions rares. »

Décision

1. Le job changes sort les couches touchées : couche0 / couche1 / couche2

Étendre la détection existante (qui produit déjà couche0 et docs_only) : mapper chaque fichier modifié vers la couche de son crate ([package.metadata.air] layer) → trois sorties booléennes. Un fichier hors crate structurant la CI/le build (ci.yml, xtask/, Cargo.toml racine, rt/targets/, toolchain) force le mode complet (prudence : impact potentiellement transverse).

2. TESTS cargo test scopés par couche (couche touchée + couches supérieures)

Le pass/fail des tests ne s’exécute que sur les crates nécessaires :

  • couche 0 touchée → tout le workspace (tout en dépend) = mode complet ;
  • couche 1 touchée → crates couche 1 + couche 2 (la 2 dépend de la 1, peut casser) ;
  • couche 2 touchée → crates couche 2 seuls.

Sûreté : une couche basse non touchée a un code identique ⇒ ses tests passent inchangés, inutile de les rejouer. On sur-teste au plus la couche touchée entière (pas de calcul de dépendants-inverses fin — [Alternatives]). La liste des crates par couche est dérivée du metadata layer (une commande xtask la fournit).

3. COUVERTURE couvrable-vide --layer N — mesurer la couche touchée seule

xtask couvrable-vide gagne un mode --layer N : il lance llvm-cov uniquement sur les crates de la couche N, puis :

  • couche 0 / 1 → réconciliation ligne-à-ligne contre docs/COVERAGE-EXCEPTIONS.md (100 % modulo exceptions, --strict) — la vraie garantie, inchangée mais scopée ;
  • couche 2 → plancher lignes (> 90 %) + branches, scopé couche 2.

Sur une PR couche N, la CI lance couvrable-vide --layer N (pour chaque couche touchée). La non-dégradation = la couche touchée tient son seuil. Les couches non touchées ne sont pas re-mesurées (code + tests identiques ⇒ couverture stable).

4. Couverture COMPLÈTE (toutes couches réconciliées) — occasions rares seulement

Le mode complet (couvrable-vide --strict sur tout) tourne uniquement sur :

  • tag de release (garantie forte avant livraison),
  • workflow_dispatch manuel (avant audit, à la demande),
  • nightly/hebdomadaire (schedule, attrape la dérive lente).

Retiré : le déclencheur FULL sur push main (décision BDFL) — la PR a déjà vérifié sa couche en pré-merge ; inutile de refaire une couverture complète à chaque merge. (Le filet reste : nightly + release.)

Conséquences

Positives. Une PR couche 2 (cas courant — air-agent en aura beaucoup) ne compile/teste/mesure que la couche 2 ⇒ de ~15‑20 min à quelques minutes. La garantie de couverture reste exacte sur la couche touchée (réconciliation 100 % couches 0/1). La couverture complète, coûteuse, ne tourne qu’aux jalons rares.

Coûts / risques. (a) Une régression inter-couches non couverte par le scope (théorique : un changement couche 2 qui ferait chuter la couverture d’une couche basse — impossible puisque le code bas est inchangé). (b) Le mode complet post-merge disparaît ⇒ une dérive ne serait attrapée qu’au nightly/release, pas au merge — assumé (la PR par-couche est le gate pré-merge). (c) Un faux classement de couche (metadata layer erroné) mal-scoperait — mitigé par check-layers (déjà en CI) et le repli complet sur tout fichier CI/build.

Alternatives rejetées

  • Tout recompiler/tester à chaque PR (statu quo) : 15‑20 min systématiques. Rejeté (la directive).
  • Scope par crate + dépendants-inverses (cargo tree --invert) : plus fin (une PR d’un crate feuille couche 2 ne teste que lui), mais plus de tooling et plus fragile. Rejeté au profit du par-couche (BDFL) — simple, sûr ; affinage par-crate possible plus tard si besoin.
  • Couverture complète sur push main : redondant avec le gate par-couche pré-merge. Rejeté (BDFL).

Mise en œuvre (référence)

  • ci.yml : changescouche1/couche2 (+ couche0 existant) ; step tests scopé ; step couverture couvrable-vide --layer N conditionné aux couches touchées ; mode complet sur schedule / workflow_dispatch / tag de release.
  • xtask/src/couvrable_vide.rs : mode --layer N (llvm-cov scopé aux crates de la couche + réconciliation couche 0/1 ou plancher couche 2). + une commande listant les crates d’une couche (pour le scope des tests).
  • La PR qui introduit ceci touche ci.yml/xtaskmode complet (elle se valide en couverture complète).

Suite

  • Enregistrement : SUMMARY.md, registre-adrs-fr.md.
  • Mesure : comparer le temps du job avant/après sur une PR couche 2 typique.

Amendement — critère sans_code : un diff sans code n’exécute aucun job de code (2026-08-01, RFC ADR-015)

Statut : Accepté (2026-08-01, décision BDFL). RFC de mécanisme (ADR-015, « Licence MPL 2.0, contribution DCO, gouvernance évolutive », qui institue le mécanisme d’amendement par RFC). Amende la Décision 1 ci-dessus (la sortie docs_only y est renommée et son critère élargi) et rend caduque la troisième puce de « Mise en œuvre (référence) » (« la PR qui introduit ceci touche ci.yml/xtask ⇒ mode complet »). N’amende ni les Décisions 2-4 (scope par couche), ni ADR-031 (« Mesure de couverture en root sur les runners self-hosted »), ni ADR-036 (« Filtrage par chemin de la re-vérification d’une couche scellée ») — qui décident respectivement comment la couverture est mesurée et quand elle est relancée, deux questions que le présent amendement ne touche pas.

Contexte

Directive BDFL (2026-08-01). « Quand une PR passe, on doit être en mesure de détecter si on a des ajouts/modifs/suppressions de code Rust, C, Swift, C++. Si c’est le cas, on lance tout. Si ce n’est pas le cas, on s’oriente vers un commit “docs-only” et il n’y a pas de risque de régression du code. »

Le critère docs_only d’origine était par chemin et par répertoire : seuls docs/** et les points d’entrée racine échappaient aux jobs de code. Deux conséquences pratiques :

  • toucher .github/workflows/ci.yml — un fichier qui décide de ce que la CI exécute, jamais de ce que cargo produit — forçait ~25 min de recompilation sur deux machines ;
  • le répertoire scripts/ (outillage de documentation : build mdBook, générateurs de registre et d’index, contrôle du journal) tombait dans la clause fourre-tout « hors crate et hors doc » et forçait, lui aussi, le mode complet.

Le nom docs_only décrivait par ailleurs mal la chose : le critère utile n’est pas « ce diff est 100 % de la documentation » mais « aucun fichier de ce diff ne peut changer le résultat d’un cargo build / cargo test ».

Décision

1. docs_only devient sans_code. Le nom dit ce que la sortie signifie. Les cinq jobs de code (test-coverage, cross-check-aarch64, loom, supply-chain, aarch64-native) restent gouvernés par elle.

2. Le critère a DEUX étages, le premier primant.

  • Étage 1 — SOURCES. Tout ajout, modification ou suppression d’un fichier .rs .c .h .cc .cpp .cxx .hpp .hh .swift .s .asm .capnp, où qu’il soit dans l’arbre : on lance tout. Ce contrôle porte sur le diff brut et court-circuite délibérément l’allowlist de l’étage 2, qui raisonne par répertoire — un .rs d’exemple posé sous docs/, un helper .c sous .github/, ne doivent pas passer pour de la documentation. git diff --name-only listant les fichiers ajoutés, modifiés et supprimés, les trois cas de la directive sont couverts par construction.
  • Étage 2 — ALLOWLIST STRICTE de chemins : docs/**, .github/**, points d’entrée racine (README*, CLAUDE.md, AGENTS.md, LICENSE), plus les fichiers inertes écartés en amont (.gitignore, book.toml, .claude/, scripts/, .githooks/README.md, docs.yml). Tout le reste compte comme du code, dont Cargo.lock — une bascule de dépendance régresse le code sans qu’aucun .rs ne bouge —, rust-toolchain.toml, deny.toml, .gitattributes (peut changer le contenu au checkout) et .githooks/commit-msg. docs/COVERAGE-EXCEPTIONS.md reste l’exception dans docs/ : il pilote le gate de couverture.

3. scripts/ rejoint les fichiers inertes. Vérifié avant de le déclarer : aucun Cargo.toml, build.rs, .cargo/config.toml, hook git ni sous-commande xtask ne lit ce répertoire. Le workflow docs exécute ces scripts sur chaque PR — c’est lui qui les valide.

4. CONTREPARTIE, indissociable de la décision 2. scripts/test-ci-perimetre.sh devient une étape OBLIGATOIRE du job changes. Ce test existait mais aucun workflow ne le lançait : la garde n’était gardée par rien. Il extrait la logique de décision de ci.yml et la rejoue sur une trentaine de diffs simulés — donc c’est bien cette logique qu’il valide, pas une copie à tenir en phase. Quelques secondes, sans compilation. Étant une étape de changes, dont tous les autres jobs dépendent, son échec bloque la PR sans exiger de réglage de protection de branche.

5. La boucle de mappage fichier → couche examine l’extension AVANT le répertoire. Un fichier source hors crates/ n’est rattachable à aucune couche, donc non scopable : il force le mode complet. Sans cette précédence, un .rs sous docs/ était excusé comme documentation.

Conséquences

Positives. Une PR de documentation ou d’outillage documentaire ne mobilise plus ni speedy ni le Pi : changes en ~10 s, docs en ~2 min, contre ~25 min auparavant. Le critère est en outre plus sûr que l’ancien sur un point précis : un fichier source égaré hors des répertoires de code est désormais détecté, ce que le critère par chemin ne faisait pas.

Coûts / risques. Limite assumée : le gate de périmètre valide la décision de périmètre, pas le contenu des jobs de code. Un seuil de couverture abaissé à l’intérieur de test-coverage passerait sous son radar ; cela reste couvert par la relecture humaine et par la première PR de code qui suit. C’est le prix de ne plus recompiler aveuglément à chaque changement de workflow.

Alternatives rejetées

  • Garder la règle « ci.yml touché ⇒ CI complète ». Défendable : changer un garde-fou sans le faire tourner est imprudent. Rejeté (BDFL) au profit d’une vérification ciblée — le gate de périmètre — qui couvre le risque réel en quelques secondes au lieu de 25 min de recompilation aveugle.
  • Sauter les jobs de code sans contrepartie. Le plus rapide, mais une PR affaiblissant la CI mergerait au vert sans que le pipeline modifié n’ait jamais été vérifié. Rejeté.
  • Détecter les sources par extension SEULEMENT (denylist, sans allowlist de chemins). Rejeté : Cargo.lock, rust-toolchain.toml et deny.toml ne portent aucune extension de source et régressent pourtant le code. Les deux étages sont complémentaires, pas redondants.

Mise en œuvre

ci.yml (job changes : étape test-ci-perimetre.sh + étape detect) et scripts/test-ci-perimetre.sh (32 cas). Mergé en #587. Documentation courante : CI.md, section « Périmètre : ce qui déclenche quoi ».