[DTP-Worker] Sprint 1 · CI/CD Gitea Actions (gate qualité 4Big · DevOps)

Débloque le dernier livrable DevOps Sprint 1 authorable en-repo (GAP §7).

- .gitea/workflows/ci.yml : pipeline Gitea Actions (Gitea ONLY, jamais GitHub)
  4 jobs : constraints-guard · validate-json · check-docs · gate agrégat.
- ci/guard_constraints.sh : enforce contraintes NON-NÉGOCIABLES CLAUDE.md
  (GitHub/GitLab/Bitbucket #2 · EspoCRM/HubSpot #3 · Stripe #10 ·
  /var/www/html/static · git clean · remote non-Gitea).
  Heuristique zéro faux positif (ignore lignes de prohibition) + escape ci-allow.
- ci/validate_json.sh : parse strict schémas Faisabilité↔Publiciste.
- ci/check_docs.sh : liens Markdown internes [HARD] + auto-score 4Big [SOFT].
- ci/README.md : doc pipeline + procédure enregistrement act_runner (VPS/DevOps).
- GAP_ANALYSIS §7 : critère CI/CD Gitea Actions -> done (runner VPS restant).

Validé localement : 3 scripts verts (exit 0) sans faux positif ;
test négatif (hubspot+github.com) -> guard exit 1 correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-07-30 00:30:40 +00:00
parent 0b2c97916a
commit 1cfb8191f4
7 changed files with 385 additions and 1 deletions
+95
View File
@@ -0,0 +1,95 @@
# 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`). Quatre jobs :
| 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) |
| `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).
## 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.
### `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.
## 3. Exécution locale (avant push)
```bash
bash ci/guard_constraints.sh # contraintes CLAUDE.md
bash ci/validate_json.sh # schémas JSON
bash ci/check_docs.sh # liens + score
```
Chaque script retourne `0` si conforme, `1` sinon. Reproduit exactement ce que
fait la CI (mêmes scripts, aucune logique cachée côté YAML).
## 4. Enregistrement du runner Gitea (ops · à faire sur le VPS)
> ⚠ Étape **hors périmètre de ce worker** (touche au VPS). À réaliser par
> l'agent DevOps. Documenté ici pour traçabilité.
Le workflow cible un runner avec le label `ubuntu-latest`. Sur le VPS Gitea
(`:3015`), enregistrer un `act_runner` :
```bash
# Sur le VPS, récupérer le token runner :
# Gitea → Site Administration → Actions → Runners → Create new Runner
act_runner register \
--instance http://153.75.250.214:3015 \
--token <RUNNER_TOKEN> \
--labels ubuntu-latest:docker://node:20-bookworm \
--name oto-dtp-runner
act_runner daemon # ou service systemd dédié
```
Vérifier ensuite : Gitea → repo `michel/oto-enterprise-os-dtp` → **Settings →
Actions** doit être activé, et le runner doit apparaître « Idle ».
## 5. Checklist de vérification (DevOps, sur VPS)
- `[ ]` Gitea Actions activé au niveau instance ET repo.
- `[ ]` `act_runner` enregistré, label `ubuntu-latest`, statut Idle.
- `[ ]` Push de test → les 4 jobs apparaissent et passent au vert.
- `[ ]` PR de test avec violation volontaire → `constraints-guard` bloque (rouge).
---
**Auto-score 4Big du livrable : 96/100.** _Réserve 4_ : l'enregistrement du
runner (§4) est hors périmètre repo et reste à confirmer sur le VPS par DevOps ;
tant que le runner n'est pas Idle, la CI ne s'exécute pas côté serveur bien que
les scripts soient validés localement.
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# ============================================================================
# check_docs.sh · OTO Enterprise OS DTP
# ----------------------------------------------------------------------------
# Contrôle qualité documentaire (gate 4Big, roadmap Sprint 1) :
# [HARD] Liens Markdown relatifs internes → la cible doit exister.
# [SOFT] Livrables 05_deliverables_mvp/*.md → auto-score 4Big attendu.
# Dépendances : bash, git, grep, sed. Aucun réseau requis.
# ============================================================================
set -uo pipefail
cd "$(git rev-parse --show-toplevel)"
FAIL=0
report() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; }
warn() { printf ' \033[33m⚠\033[0m %s\n' "$1"; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
echo "== 1. Liens Markdown relatifs internes (cible existe ?) =="
broken=0
while read -r md; do
dir="$(dirname "$md")"
# Extrait les cibles [texte](cible) ; ignore http(s), mailto, ancres pures,
# gabarits {{...}} et liens wiki [[...]].
grep -oE '\]\([^)]+\)' "$md" 2>/dev/null \
| sed -E 's/^\]\(//; s/\)$//' \
| while read -r link; do
link="${link%% *}" # retire un éventuel "titre"
link="${link%%#*}" # retire l'ancre
[[ -z "$link" ]] && continue
case "$link" in
http://*|https://*|mailto:*|tel:*|\#*|\{\{*|data:*) continue ;;
esac
target="$dir/$link"
if [[ ! -e "$target" ]]; then
printf ' \033[31m✗\033[0m %s → lien cassé : %s\n' "$md" "$link"
echo "BROKEN" >> /tmp/brokenlinks
fi
done
done < <(git ls-files '*.md')
if [[ -s /tmp/brokenlinks ]]; then FAIL=1; rm -f /tmp/brokenlinks; else ok "Aucun lien interne cassé"; fi
echo "== 2. Auto-score 4Big sur les livrables (SOFT · CLAUDE.md #5 · ≥95/100) =="
while read -r md; do
case "$md" in
05_deliverables_mvp/daily_reports/*) continue ;; # rapports : hors gate score
esac
if grep -qiE 'score|/100' "$md"; then
ok "$md — mention de score présente"
else
warn "$md — pas de mention d'auto-score 4Big (recommandé ≥95/100)"
fi
done < <(git ls-files '05_deliverables_mvp/*.md')
echo
if [[ "$FAIL" -eq 0 ]]; then
echo -e "\033[32m✅ Documentation conforme (liens internes OK).\033[0m"
else
echo -e "\033[31m❌ Lien(s) interne(s) cassé(s) — corriger avant merge.\033[0m"
fi
exit "$FAIL"
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# ============================================================================
# guard_constraints.sh · OTO Enterprise OS DTP
# ----------------------------------------------------------------------------
# Enforce les contraintes NON-NÉGOCIABLES de CLAUDE.md dans la CI.
# Objectif : empêcher qu'un commit introduise un outil/plateforme interdit
# ou une pratique proscrite. Gate qualité DevOps (roadmap Sprint 1).
#
# Principe de robustesse (zéro faux positif sur le texte de politique) :
# - On détecte l'USAGE d'un terme interdit, PAS sa simple mention.
# - Une ligne qui contient un MARQUEUR DE PROHIBITION (jamais, ❌, only,
# pas de, interdit, SEUL...) est un rappel de la règle → on l'ignore.
# - Escape hatch explicite : une ligne contenant « ci-allow » est ignorée.
#
# Sortie : exit 0 si conforme, exit 1 sinon (liste des violations).
# Dépendances : bash, git, grep. Aucun réseau requis.
# ============================================================================
set -uo pipefail
cd "$(git rev-parse --show-toplevel)"
# 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'
# ci-allow : échappatoire manuel documenté.
ALLOW='ci-allow'
FAIL=0
report() { printf ' \033[31m✗\033[0m %s\n' "$1"; FAIL=1; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
# Fichiers suivis (exclut ce script lui-même : il liste les termes interdits).
tracked_files() {
git ls-files -- . \
':(exclude)ci/guard_constraints.sh' \
':(exclude).gitea/workflows/*.yml'
}
# scan_forbidden <label> <regex-terme>
# Flag toute occurrence NON préfixée d'un marqueur de prohibition / ci-allow.
scan_forbidden() {
local label="$1" term="$2" hits
hits="$(tracked_files | while read -r f; do
grep -inE "$term" "$f" 2>/dev/null \
| grep -ivE "$PROHIBITION" \
| grep -ivE "$ALLOW" \
| sed "s#^#${f}:#"
done)"
if [[ -n "$hits" ]]; then
report "Interdit détecté — $label :"
echo "$hits" | sed 's/^/ /'
else
ok "Aucun usage interdit — $label"
fi
}
echo "== 1. Plateformes git interdites (CLAUDE.md #2 · Gitea SEULEMENT) =="
scan_forbidden "GitHub" 'github\.com|git@github|github\.io'
scan_forbidden "GitLab" 'gitlab\.com|gitlab\.io'
scan_forbidden "Bitbucket" 'bitbucket\.org'
echo "== 2. CRM interdits (CLAUDE.md #3 · CRM = ERPNext natif) =="
scan_forbidden "EspoCRM" 'espocrm'
scan_forbidden "HubSpot" 'hubspot'
echo "== 3. Paiement interdit (CLAUDE.md #10 · Cardnet, pas Stripe) =="
scan_forbidden "Stripe" 'stripe'
echo "== 4. Écriture directe /var/www/html/static (Interdit absolu) =="
scan_forbidden "chemin /var/www/html/static en écriture" '/var/www/html/static/'
echo "== 5. Commande git clean (Interdit absolu) =="
scan_forbidden "git clean" 'git[[:space:]]+clean'
echo "== 6. Remote git = Gitea uniquement =="
if git remote -v >/dev/null 2>&1 && [[ -n "$(git remote)" ]]; then
bad_remote="$(git remote -v | grep -iE 'github\.com|gitlab\.com|bitbucket\.org' || true)"
if [[ -n "$bad_remote" ]]; then
report "Remote git interdit configuré :"; echo "$bad_remote" | sed 's/^/ /'
else
ok "Remotes git conformes (Gitea / interne uniquement)"
fi
else
ok "Aucun remote git configuré (CI checkout) — rien à vérifier"
fi
echo
if [[ "$FAIL" -eq 0 ]]; then
echo -e "\033[32m✅ Contraintes NON-NÉGOCIABLES respectées.\033[0m"
else
echo -e "\033[31m❌ Violation(s) de contrainte détectée(s). Voir ci-dessus.\033[0m"
echo " (Faux positif légitime ? Ajouter « ci-allow » sur la ligne concernée.)"
fi
exit "$FAIL"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# ============================================================================
# validate_json.sh · OTO Enterprise OS DTP
# ----------------------------------------------------------------------------
# Valide que tous les fichiers .json suivis sont bien formés (parse strict).
# Les schémas JSON (version.schema.json, projets_master.schema.json) sont le
# contrat de données Faisabilité ↔ Publiciste : un JSON cassé bloque le
# pipeline en aval → on l'attrape ici.
# Dépendances : bash, git, python3. Aucun réseau requis.
# ============================================================================
set -uo pipefail
cd "$(git rev-parse --show-toplevel)"
FAIL=0
mapfile -t files < <(git ls-files '*.json')
if [[ "${#files[@]}" -eq 0 ]]; then
echo "Aucun fichier .json suivi — rien à valider."
exit 0
fi
for f in "${files[@]}"; do
if python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$f" 2>/tmp/jsonerr; then
printf ' \033[32m✓\033[0m %s\n' "$f"
# Si un $schema est déclaré, contrôle que 'draft' est reconnu (info seule).
else
printf ' \033[31m✗\033[0m %s\n' "$f"
sed 's/^/ /' /tmp/jsonerr
FAIL=1
fi
done
echo
if [[ "$FAIL" -eq 0 ]]; then
echo -e "\033[32m✅ Tous les JSON sont bien formés.\033[0m"
else
echo -e "\033[31m❌ JSON invalide détecté.\033[0m"
fi
exit "$FAIL"