From 20f9398bd4700c965c031d19a899132174805315 Mon Sep 17 00:00:00 2001 From: william Date: Tue, 15 Sep 2026 19:39:39 +0200 Subject: [PATCH 1/4] Phase 4 (CI) : securise le job deploy (token registre, cle SSH) - docker login sur le serveur distant : passe le token via --password-stdin (echo | docker login ...) plutot qu'en argument -p, meme methode que build-and-push - un -p en argument reste visible via ps sur le serveur tant que le process tourne. - Nouvelle etape "Nettoyage de la cle SSH" (if: always()) : supprime ~/.ssh/deploy_key en fin de job, meme si une etape precedente a echoue. rm -f (jamais rm nu) : sortie 0 que le fichier ou meme le dossier ~/.ssh parent soit deja absent, verifie empiriquement - ne peut jamais faire echouer ce nettoyage ni masquer un echec anterieur. Le runner semble deja jetable (docker volume rm observe dans les logs d'un autre job de ce meme workflow), mais jamais verifie directement pour deploy (jamais execute sur dev, reserve a main) - nettoyage explicite plutot qu'une inference par analogie pour une cle privee. djLint (H021, styles inline) volontairement saute pour ce commit - meme backlog assume que les commits Phase 3/4 precedents, aucun rapport avec ce changement. Co-Authored-By: Claude Sonnet 5 --- .gitea/workflows/deploy.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 642f4120..1e7fec1c 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -205,8 +205,19 @@ jobs: cd "${{ secrets.DEPLOY_PATH }}" echo "REGISTRY_IMAGE=${{ secrets.REGISTRY_IMAGE }}" > .env echo "IMAGE_TAG=${{ gitea.sha }}" >> .env - docker login "${{ secrets.REGISTRY_HOST }}" -u "${{ secrets.REGISTRY_USER }}" -p "${{ secrets.REGISTRY_TOKEN }}" + echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${{ secrets.REGISTRY_HOST }}" -u "${{ secrets.REGISTRY_USER }}" --password-stdin docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml up -d docker image prune -f ' + + - name: Nettoyage de la cle SSH + # if: always() - meme si les etapes precedentes ont echoue, la cle + # privee posee ci-dessus (~/.ssh/deploy_key) ne doit jamais rester + # sur le disque du job. Le runner semble deja jetable (voir + # "docker volume rm .../JOB--..." dans les logs des autres + # jobs de ce meme workflow), mais jamais verifie directement pour + # CE job (jamais execute sur dev, reserve a main) - ne pas se fier + # uniquement a une inference par analogie pour une cle privee. + if: always() + run: rm -f ~/.ssh/deploy_key -- 2.54.0 From 55e81c0fbb9e99323a426380a0b87d2d8d3cc5b1 Mon Sep 17 00:00:00 2001 From: william Date: Wed, 16 Sep 2026 12:41:02 +0200 Subject: [PATCH 2/4] Ajoute CODE_QUALITY.md et corrige S7632 a la racine (helper assert_not_none) - Nouveau db/assert_not_none.py : centralise l'unique suppression Bandit/Ruff (# nosec B101 / # noqa: S101) de narrowing de type dans tout le moteur. Remplace 11 sites disperses (ai/chat.py, ai/tools.py, routes/scenes/scene_object_{add,collision,geometry,personnage_data, quiz_config}.py, screens/payload/full_game_payload.py, scripts/build_demo_dialogues.py) qui repetaient chacun le meme commentaire empile - Sonar (python:S7632) ne parse pas deux commentaires # sur une ligne, meme si Ruff et Bandit les acceptent chacun tres bien (contrainte structurelle documentee dans CODE_QUALITY.md : chaque outil exige son propre mot-cle immediatement apres un #, aucun format a un seul # ne peut satisfaire les deux a la fois). Les 6 sites # nosec B608 (SQL dynamique) restent inchanges, nature differente, hors perimetre de ce refactor. Verifie : mypy --strict propre (389 fichiers), ruff/bandit/import-linter clean, suite complete verte (591 tests), scan SonarQube local relance confirmant S7632 a 7 (1 seul site restant dans le helper lui-meme + les 6 B608), 0 bug (une regression S8371 trouvee et corrigee en route). - 4 sites |safe repositionnes sur leur ligne exacte (scene_edit.html, register_2fa.html, onboarding_new.html, play.html) - le marqueur NOSONAR etait sur la ligne precedente par erreur (meme piege que celui documente pour S8371 ci-dessus). Confirme par scan que meme corrige, l'analyseur Web de Sonar ne supporte aucune syntaxe de suppression inline testee pour la regle Web:S5247 - documente comme limitation technique connue dans CODE_QUALITY.md plutot que force. - CODE_QUALITY.md (nouveau) : reference complete du dispositif qualite - vue d'ensemble par outil, configuration de chacun, commandes de lancement local, procedure de justification d'une exception (avec le format exact attendu par Bandit/Ruff/Sonar, verifie empiriquement), table des 10 exceptions documentees, backlog (djLint H021, code smells Sonar). djLint (H021, styles inline) volontairement saute pour ce commit - meme backlog assume que les commits precedents, aucun rapport avec ce changement. Co-Authored-By: Claude Sonnet 5 --- CODE_QUALITY.md | 143 ++++++++++++++++++ ai/chat.py | 4 +- ai/tools.py | 4 +- db/__init__.py | 2 + db/assert_not_none.py | 15 ++ routes/scenes/scene_object_add.py | 13 +- routes/scenes/scene_object_collision.py | 5 +- routes/scenes/scene_object_geometry.py | 5 +- routes/scenes/scene_object_personnage_data.py | 5 +- routes/scenes/scene_object_quiz_config.py | 5 +- screens/payload/full_game_payload.py | 4 +- scripts/build_demo_dialogues.py | 6 +- templates/auth/register_2fa.html | 3 +- templates/onboarding/onboarding_new.html | 3 +- templates/play.html | 2 +- templates/scene_edit.html | 3 +- 16 files changed, 187 insertions(+), 35 deletions(-) create mode 100644 CODE_QUALITY.md create mode 100644 db/assert_not_none.py diff --git a/CODE_QUALITY.md b/CODE_QUALITY.md new file mode 100644 index 00000000..e4db0e35 --- /dev/null +++ b/CODE_QUALITY.md @@ -0,0 +1,143 @@ +# Qualité de code — Forge Engine + +Référence de fonctionnement du dispositif qualité mis en place (Phases 1 +à 4). Pour le compte-rendu chronologique de ce qui a été fait/trouvé/ +décidé pendant la mise en place, voir `docs/SESSION_RECAP.md` — ce +fichier-ci documente uniquement **l'état actuel** et **comment +l'utiliser au quotidien**. + +Règle de base, non négociable : **aucune règle de lint/typage n'est +désactivée globalement dans un fichier de config sans validation +explicite.** Une exception ponctuelle est toujours une ligne de code +(`# noqa`, `# nosec`, `// NOSONAR`) avec une raison précise, jamais un +ignore de fichier entier ou de règle globale — voir section 4. + +## 1. Vue d'ensemble + +| Outil | Vérifie | Portée | Obligatoire / Avertissement | +|---|---|---|---| +| **Ruff** (lint) | Erreurs Python, imports inutilisés, style | `*.py` | Obligatoire (bloquant CI + pre-commit) | +| **Ruff** (format) | Formatage Python | `*.py` | Obligatoire | +| **Mypy** `--strict` | Typage statique Python | `*.py` | Obligatoire | +| **Bandit** | Sécurité Python (injections, primitives faibles) | `ai`, `auth`, `core`, `db`, `filters`, `publish`, `routes`, `screens`, `scripts`, `app.py`, `build_css.py` | Obligatoire | +| **Vulture** | Code mort Python | mêmes dossiers que Bandit | Obligatoire | +| **import-linter** | Contrat de couches applicatives | tout le code Python | Obligatoire | +| **ESLint** (`airbnb-base`) | Lint JavaScript | `static/js/**/*.js` | Obligatoire | +| **Stylelint** (`stylelint-config-standard`) | Lint CSS | `styles/**/*.css` | Obligatoire | +| **djLint** | Lint des templates Jinja | `templates/**/*.html` | **Avertissement** — H021 (styles inline) en backlog assumé, voir section 6 | +| **SonarQube** (local + CI) | Bugs/vulnérabilités/code smells/hotspots agrégés | tout le dépôt | **Avertissement** — non-bloquant en CI pour l'instant, voir section 3 | + +## 2. Configuration de chaque outil + +### Ruff (`pyproject.toml`, `[tool.ruff]`) +- `line-length = 120`, `target-version = "py313"`. +- Règles activées : `F` (pyflakes), `E`/`W` (pycodestyle), `B` (bugbear), `C4` (comprehensions), `SIM` (simplification), `S` (bandit-équivalent Ruff), `I` (imports). +- `per-file-ignores` : `S101`/`S106` désactivées pour `tests/` uniquement (assert et mots de passe en dur sont la norme dans les tests, jamais en dehors). +- Exclusions : `projects/`, `user_assets/`, `data/`, `Bug/`, `regles/` (données runtime, jamais du code). + +### Mypy (`pyproject.toml`, `[tool.mypy]`) +- `strict = true`, `python_version = "3.13"`. +- `explicit_package_bases = true` + `mypy_path = "."` : nécessaire car `tests/` n'a pas de `__init__.py` — sans ça mypy refuse de démarrer ("Source file found twice under different module names"). **Ce n'est pas un assouplissement de `strict`**, juste la résolution des chemins de module. +- Mêmes exclusions que Ruff. + +### Bandit (`pyproject.toml`, `[tool.bandit]`) +- `exclude_dirs` : `projects`, `user_assets`, `data`, `Bug`, `regles`, `tests` (les tests contiennent des mots de passe/tokens de test en dur, jamais un vrai risque). +- Portée explicite (`-r ai auth core db filters publish routes screens scripts app.py build_css.py`) plutôt que tout le dépôt. + +### Vulture (`pyproject.toml`, `[tool.vulture]`) +- `min_confidence = 80`, `paths` = mêmes dossiers que Bandit + `vulture_whitelist.py`. +- `exclude = ["*/tests/*"]`. +- Faux positifs structurels (routes Flask enregistrées par décorateur, hooks appelés par convention de nom) : whitelist dédiée dans `vulture_whitelist.py`, **jamais** un `min_confidence` abaissé globalement. + +### import-linter (`pyproject.toml`, `[tool.importlinter]`) +- Un seul contrat de type `layers`, du haut vers le bas : `routes` → `ai | publish | core` → `screens` → `auth | filters` → `db`. +- Une couche ne peut importer qu'une couche strictement en dessous d'elle, jamais au-dessus, jamais une couche sœur du même niveau. +- `app.py` (point d'entrée, pas un paquet) reste hors contrat — c'est lui qui importe `routes`/`core`, jamais l'inverse. + +### ESLint (`.eslintrc.json`) +- Base `airbnb-base` + plugin `unused-imports`. +- `max-len` porté à 120 (aligné sur Ruff) au lieu du 80 par défaut d'Airbnb. +- Plusieurs règles Airbnb désactivées **après vérification individuelle que le code existant les respecte déjà différemment** (voir section 5 pour le détail et le raisonnement de chacune) : `no-param-reassign`, `no-use-before-define` (assoupli pour les fonctions, gardé strict pour variables/classes), `func-names`, `no-underscore-dangle`, `no-console`, `no-plusplus`, `no-continue`, `no-bitwise`, `guard-for-in`, `no-restricted-syntax`, `prefer-destructuring`, `consistent-return`, `no-return-assign`, `no-nested-ternary`, `no-void`, `implicit-arrow-linebreak`, `no-unused-expressions`, `no-useless-concat`. +- `unused-imports/no-unused-vars` avec une liste blanche (`varsIgnorePattern`) d'environ 95 noms : fonctions invoquées uniquement depuis des attributs `onclick`/`onchange` inline dans les templates Jinja, invisibles pour l'analyse statique d'ESLint. +- Bloc `globals` documentant les variables cross-fichiers volontaires (ex. `gameData`, `screensData` — voir section 5). + +### Stylelint (`.stylelintrc.json`) +- Base `stylelint-config-standard`. +- `selector-class-pattern`/`selector-id-pattern` désactivées : le projet utilise du camelCase pour ses classes/id CSS depuis le début (`.canvasElement`, `#scormProgress`...) — imposer le kebab-case du preset aurait demandé de renommer des milliers de sélecteurs et leurs usages JS/HTML pour un gain nul. +- `no-descending-specificity` désactivée : le CSS existant est organisé par composant/fonctionnalité, pas par ordre strict de spécificité. +- `ignoreFiles` : `static/vendor/**`, `static/style.css` (bundle généré, contient du Bulma vendored). + +### djLint (`pyproject.toml`, `[tool.djlint]`) +- `profile = "jinja"`, `max_line_length = 160`, `indent = 2`. +- H021 (styles inline) : **backlog assumé, pas une exception corrigée** — voir section 6, ne pas confondre avec les vraies exceptions de la section 5. + +### SonarQube +- **Local (développement continu)** : Docker Community Build + PostgreSQL (jamais la base H2 embarquée), tourne en permanence sur cette machine via `~/sonarqube-stack/docker-compose.yml` (WSL2/Ubuntu). Dashboard : **http://localhost:9000** — identifiants personnels, jamais consignés ici. +- **CI (`sonar.forgebase.fr`)** : instance séparée, self-hébergée, scan à chaque push via `.gitea/workflows/deploy.yml` (job `sonarqube`), **non-bloquant** (`continue-on-error: true`) le temps que le rapport soit entièrement trié. + +## 3. Lancer les checks en local + +```bash +# Un outil a la fois (memes commandes que .pre-commit-config.yaml) +ruff check . +ruff format --check . +mypy . +vulture +bandit -c pyproject.toml -r ai auth core db filters publish routes screens scripts app.py build_css.py +lint-imports +djlint templates +npx eslint "static/js/**/*.js" +npx stylelint "styles/**/*.css" + +# Tout d'un coup (ce que fait un commit) +pre-commit run --all-files + +# Suite de tests +python -m pytest tests/ -q +node --test static/js/play/__tests__/*.test.js static/js/scenes/__tests__/*.test.js + +# Scan SonarQube local (rapport seul - jamais de correction automatique +# a partir de son seul resultat sans triage prealable, voir section 4) +# Necessite le stack Docker local demarre (voir section 2) et un token +# genere sur http://localhost:9000 (Mon compte > Security > Generate Token). +docker run --rm --network sonarqube-stack_default \ + -v :/usr/src \ + sonarsource/sonar-scanner-cli \ + -Dsonar.host.url=http://sonarqube:9000 \ + -Dsonar.token= +``` + +## 4. Procédure de justification d'une exception + +1. **Jamais de correction mécanique sans comprendre la cause.** Avant de corriger un finding, déterminer s'il s'agit d'un vrai problème ou d'un faux positif dû au contexte du projet (fixture pytest, dispatch dynamique JS/Jinja, whitelist codée en dur, architecture volontaire). +2. **Une exception ponctuelle, jamais globale.** Le commentaire de suppression va **sur la ligne exacte** signalée par l'outil — pas la ligne au-dessus, pas la ligne en dessous (piège vécu à plusieurs reprises cette session : un commentaire mal placé ne supprime rien du tout, silencieusement). +3. **La raison est précise, jamais vague.** "Faux positif" seul ne suffit pas — expliquer *pourquoi* (ex. "table_name vient de slugify()+prefixe obj_, jamais d'une entrée brute"). +4. **Format attendu par outil** (vérifié empiriquement cette session, chaque outil a ses propres exigences de position du mot-clé) : + - Bandit : `# nosec ` — le mot `nosec` doit être immédiatement précédé d'un `#` sur la ligne. + - Ruff : `# noqa: ` — le mot `noqa` doit être immédiatement précédé d'un `#` sur la ligne. **Bandit et Ruff peuvent coexister sur une même ligne physique** en utilisant deux `#` distincts (`# nosec B101 # noqa: S101 - raison`) — c'est structurel, pas un choix de style : chaque outil cherche son propre mot-clé juste après un `#`, et aucun format à un seul `#` ne peut satisfaire les deux à la fois (testé empiriquement). + - SonarQube (Python) : `# NOSONAR - raison`, sur la ligne exacte. + - SonarQube (JS) : `// NOSONAR - raison`, sur la ligne exacte. + - SonarQube (templates Jinja/HTML, règle `Web:*`) : **aucune syntaxe trouvée qui fonctionne** malgré plusieurs tentatives (commentaire Jinja `{# #}`, commentaire JS natif dans un `` (voir Phase 3). Plusieurs syntaxes de suppression testées (commentaire Jinja `{# #}`, commentaire JS natif dans un `