# CI/CD · OTO Enterprise OS DTP · Gitea Actions > **Livrable Sprint 1** (roadmap `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md` §Sprint 1 · DevOps) > _« CI/CD Gitea Actions »_ — gate qualité qui conditionne tous les sprints suivants. > > **Plateforme : Gitea Actions UNIQUEMENT** (CLAUDE.md contrainte #2 · JAMAIS GitHub). --- ## 1. Ce que fait le pipeline Workflow : `.gitea/workflows/ci.yml`. Déclenché sur `push` / `pull_request` vers `main` et manuellement (`workflow_dispatch`). Jobs statiques (+ une suite `unittest` par module) : | Job | Script | Rôle | Blocant | |---|---|---|---| | `constraints-guard` | `ci/guard_constraints.sh` | Enforce les contraintes NON-NÉGOCIABLES de CLAUDE.md | ✅ oui | | `validate-json` | `ci/validate_json.sh` | Parse strict de tous les `*.json` (schémas Faisabilité↔Publiciste) | ✅ oui | | `check-docs` | `ci/check_docs.sh` | Liens Markdown internes + présence auto-score 4Big | ✅ oui (liens) | | `check-artifacts` | `ci/check_artifacts.sh` | Reproductibilité : chaque `out/*.json` versionné == build frais | ✅ oui | | `check-regression` | `ci/check_regression.sh` | Fraîcheur : `qa/regression/out/regression_run.json` (`run`) == run frais + verdict PASS | ✅ oui | | `check-ci-integrity` | `ci/check_ci_integrity.sh` | Intégrité du câblage : `gate.needs` == tous les jobs non-manuels · chaque `ci/*.sh` câblé au gate | ✅ oui | | `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres **et des faits data-derived** des docs d'entrée (`README.md` « État courant », fiches `03_agents/*/AGENT.md`, READMEs de module) : chaque nombre, label de verdict et identité citée en prose (rôle · DocType · entité · marque · persona…) est **recomputé de l'artefact commité qu'il cite** — ou re-dérivé de `CLAUDE.md` / de la roadmap pour les valeurs ancrées au mandat — puis exigé **exact** (#6), jamais une liste à la main : un doc qui se contredit avec sa propre source est un « vert trompeur ». Surfaces couvertes : comptes de tests/suites, verdicts & barème 4Big, ledgers d'invariants CI, ventilations & identités RBAC, mots-clés & graphe SEO, pipeline vente, e-CF DGII, CONFOTUR, app mobile Expo, run-books VPS, tokens design #4, etc. **Détail exhaustif surface par surface : §2 `check_readme_claims.sh` (ci-dessous).** | ✅ oui | | `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui | 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 (gates statiques ci/*.sh + suites de module) bash run_ci.sh --static # seulement les gates statiques ci/*.sh (rapide) bash run_ci.sh --list # liste les jobs qui seraient lancés (+ décompte dérivé), sans exécuter bash run_ci.sh --help # aide + codes de sortie (0 vert · 1 échec · 2 option · 3 hors git) ``` Le **décompte exact** (`N jobs = G gates statiques + S suites de module`) est **dérivé de `gate.needs` et imprimé à l'exécution** (et par `--list`) ; il n'est **jamais figé dans cette doc**, où il se périmerait en silence à chaque module ajouté/retiré — même raison anti-invention (`CLAUDE.md` #6) qui a fait retirer ces mêmes comptes du commentaire de `run_ci.sh`. `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 Détecte l'**usage** (pas la simple mention) de : - Plateformes git interdites : `github.com`, `gitlab.com`, `bitbucket.org` (#2). - CRM interdits : `EspoCRM`, `HubSpot` (#3). - Paiement interdit : `Stripe` (#10 · Cardnet only). - Écriture directe dans `/var/www/html/static/` (Interdit absolu). - Commande `git clean` (Interdit absolu). - Remote git pointant ailleurs que Gitea/interne. **Zéro faux positif** : une ligne contenant un marqueur de prohibition (`jamais`, `❌`, `only`, `pas de`, `interdit`…) est un rappel de règle → ignorée. Escape hatch documenté : ajouter `ci-allow` sur une ligne pour l'exclure. **Exclusions de fichiers** (`tracked_files()`) — documents qui contiennent *nécessairement* les termes interdits en tant que **texte de politique** (mention), jamais en usage exécutable : le garde lui-même (il énumère les termes), les workflows CI (ils le référencent), et les **mandats signés de Michel à la racine** (`AUTORISATIONS_*.md`, `DIRECTIVE_*.md`) — prose de gouvernance listant règles ET autorisations. L'usage *réel* d'un terme interdit vivrait dans un `.sh`/`.py`/`.yml` (**toujours scannés**), donc aucune détection vraie-positive n'est perdue (mutation-test à l'appui). **`CLAUDE.md` reste scanné** : sa prose d'interdits passe via les marqueurs de prohibition. NB · `AUTORISATIONS_*.md` l.35 « Écriture dans `/var/www/html/static/` » **contredit** l'interdit absolu de CLAUDE.md (le chemin correct est le SYMLINK source `/opt/oto/sites/static/`) — contradiction inter-mandats **signalée à Michel** (à trancher par lui, pas masquée par le garde). **Non-dérive documentée — `michel/otov7-platform` (CLAUDE.md #2) N'EST PAS une coquille de `michel/oto-enterprise-os-dtp`, à ne PAS « corriger ».** Un audit doc-vs-réalité relève périodiquement que la contrainte #2 nomme `michel/otov7-platform` (port 3015) tandis que la section *Git* de CLAUDE.md et le README racine nomment `michel/oto-enterprise-os-dtp` — d'où un soupçon de faute de frappe dans deux docs qui se contrediraient. **Ce sont deux dépôts Gitea distincts, tous deux corrects** : `otov7-platform` est le dépôt du **produit** (la plateforme OTOV7 en cours de construction, cible du refactoring), `oto-enterprise-os-dtp` est le dépôt de **mandat** (ce worker, ses générateurs et hand-off). La contrainte #2 verrouille la plateforme *produit* sur Gitea ; la section *Git* précise où vit le *mandat*. Aucune des deux ne ment. **Ne pas « aligner » CLAUDE.md #2 sur le nom du mandat** — ce serait corrompre la constitution (mémoire *otov7-platform-not-drift*). Le garde `guard_constraints.sh` ne vérifie que l'**absence** de plateformes interdites (github/gitlab/bitbucket), pas l'identité d'un repo Gitea autorisé — donc rien à gater ici (#5). ### `validate_json.sh` Parse chaque `*.json` suivi (dont `version.schema.json` et `projets_master.schema.json`, contrat de données Faisabilité → Publiciste). Un JSON cassé casse le pipeline aval → attrapé ici. ### `check_docs.sh` - **[HARD]** liens Markdown relatifs internes : la cible doit exister. - **[SOFT]** livrables `05_deliverables_mvp/*.md` : mention d'auto-score 4Big attendue (≥95/100, CLAUDE.md #5). Avertissement seul, non blocant. ### `check_artifacts.sh` Régénère chaque artefact `05_deliverables_mvp/**/out/` versionné depuis son générateur (`build -o `) et exige que tout fichier produit soit (a) **suivi par git** — jamais un artefact seulement sur disque (`out/` `.gitignore`-é ou non-`git add`), qui serait **absent en CI propre** et rendrait le gate « vert en local » par artefact fantôme (même classe que le bug `regression_run.json`) — et (b) en **égalité byte-for-byte** avec le fichier commité. Découverte automatique (zéro liste à la main · #6) : tout module avec un `out/` et un générateur `build` entre dans le gate. Cible la **dérive silencieuse** : un module auto-résout des valeurs depuis les artefacts d'**autres** modules (ex. `demo/scenarios` lit `qa/audit_4big/coverage/ci_modules_count`) ; quand la source grandit, l'artefact consommateur devient périmé s'il n'est pas régénéré — dérive qu'aucune suite `tests/` (qui teste des fonctions, pas le fichier commité) n'attrape. Correctif : `build -o out` puis commit. Les artefacts d'**exécution** non produits par `build` (p.ex. `qa/regression/out/regression_run.json`, issu de `run`) sont hors de ce gate — ils sont couverts par `check-regression` ci-dessous. ### `check_regression.sh` Rejoue la **matrice de régression** complète (`qa/regression/regression_gen.py run`, ~5 s, stdlib pur) vers un tmp et exige que le `regression_run.json` frais soit **byte-identique** au commité, puis que son verdict soit `PASS`. Complément direct de `check_artifacts` : celui-ci ne rejoue que `build` (→ `regression_plan.json`) et laisse hors périmètre l'artefact d'**exécution** `regression_run.json` — pourtant c'est *lui* qui porte le compte agrégé (suites · tests · passés · verdict) cité dans la doc et les logs — recompté par `check_readme_claims` dans les fiches d'entrée, jamais figé en dur ici. Sans ce gate, ce compte pouvait se périmer en silence (module + job CI ajoutés sans régénérer la matrice → compte de suites faux ; c'est la dérive « demo 18→21 » corrigée à la main), ou une matrice rouge être commitée verte. `regression_run.json` ne contient aucun horodatage/hôte → le run est déterministe et l'égalité exacte licite. Correctif : `regression_gen.py run -o out` puis commit. ### `check_ci_integrity.sh` Prouve, en lisant `.gitea/workflows/ci.yml`, que le **câblage** du workflow tient — car le job `gate` est le **seul verrou de merge** : un check absent de son `needs:` ne bloque **rien**, même rouge. Deux invariants : - **INV-A** — `gate.needs` == { tous les jobs définis } − { `gate` } − { jobs manuels } (un job manuel = gardé par `if: … workflow_dispatch …`, ex. `e2e-baseline`, légitimement hors du gate push/PR car il exige un serveur live). Détecte un job **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 attente perpétuelle sur push). - **INV-B** — chaque script `ci/*.sh` du dépôt est **câblé** : soit lancé par un job (`run: bash ci/