diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
index 00fd3d70..39610926 100644
--- a/.gitea/workflows/deploy.yml
+++ b/.gitea/workflows/deploy.yml
@@ -4,12 +4,13 @@ on:
push:
branches: [main, dev]
-# Les jobs "test-*"/"lint-*" tournent sur CHAQUE push (main et dev) :
-# jusqu'ici aucune étape de CI n'exécutait la suite de tests ni les outils
-# qualité, rien n'empêchait un commit cassé d'atteindre la production (voir
-# l'audit qualité de la Phase 0 du plan). "build-and-push"/"deploy", eux,
-# restent réservés à main (via le filtre "if" sur gitea.ref) — un push sur
-# dev ne doit jamais redéployer la prod, seulement faire tourner tests/lint.
+# Les jobs "test-*"/"lint-*"/"sonarqube" tournent sur CHAQUE push (main et
+# dev) : jusqu'ici aucune étape de CI n'exécutait la suite de tests ni les
+# outils qualité, rien n'empêchait un commit cassé d'atteindre la
+# production (voir l'audit qualité de la Phase 0 du plan). "build-and-
+# push"/"deploy", eux, restent réservés à main (via le filtre "if" sur
+# gitea.ref) — un push sur dev ne doit jamais redéployer la prod, seulement
+# faire tourner tests/lint/sonar.
#
# lint-python/lint-js rejouent EXACTEMENT les hooks pre-commit locaux
# (.pre-commit-config.yaml) mais bloquants ici dès le départ (déjà tous
@@ -17,11 +18,11 @@ on:
# les hooks pre-commit ne protègent que la machine du committeur, jamais
# un push direct ou une PR mergée depuis ailleurs. djlint EXCLU
# volontairement (49 H021 "styles inline" déjà en backlog assumé, voir
-# CODE_QUALITY.md) — à ajouter ici quand ce lot sera traité.
-#
-# SonarQube (job CI + service prod) retiré pour l'instant — voir
-# CODE_QUALITY.md pour le contexte (accès instance locale/prod bloqué par
-# des soucis d'infra réseau, mis de côté volontairement).
+# CODE_QUALITY.md) — à ajouter ici quand ce lot sera traité. sonarqube,
+# lui, reste NON-BLOQUANT (continue-on-error) — remis en place le
+# 18/09/2026 une fois l'instance prod opérationnelle (voir CODE_QUALITY.md
+# pour l'historique du retrait temporaire), le temps que le rapport de
+# code smells soit entièrement trié.
# Secrets à configurer dans Gitea (Paramètres du dépôt > Actions > Secrets) :
# REGISTRY_HOST adresse du registre d'images (ex: gitea.exemple.com)
@@ -32,6 +33,8 @@ on:
# DEPLOY_USER utilisateur SSH sur ce serveur
# DEPLOY_SSH_KEY clé privée SSH (au format PEM) autorisée sur ce serveur
# DEPLOY_PATH dossier sur le serveur où vit docker-compose.prod.yml (ex: /home/deploy/forge-engine)
+# SONAR_TOKEN jeton d'analyse SonarQube (Mon compte > Security > Generate Token
+# sur sonar.forgebase.fr) — jamais le mot de passe admin.
#
# Utilise directement docker/ssh/scp en ligne de commande plutôt que des
# actions du marketplace, pour ne pas dépendre de la disponibilité de
@@ -131,6 +134,28 @@ jobs:
RUN npm run lint:css
DOCKERFILE
+ sonarqube:
+ runs-on: ubuntu-latest
+ # Non-bloquant pendant cette premiere periode (voir le bloc de
+ # commentaires en tete de fichier) — un echec ici n'empeche jamais
+ # build-and-push/deploy, contrairement a lint-python/lint-js.
+ continue-on-error: true
+ steps:
+ - uses: actions/checkout@v4
+ - name: Analyse SonarQube (rapport seul, non-bloquant)
+ run: |
+ # Meme neutralisation de .dockerignore que test-python : sonar-
+ # project.properties couvre aussi tests/ (sonar.tests).
+ mv .dockerignore .dockerignore.disabled-for-ci
+ docker build -f - -t forge-sonar:${{ gitea.sha }} . <<'DOCKERFILE'
+ FROM sonarsource/sonar-scanner-cli:latest
+ WORKDIR /usr/src
+ COPY . .
+ DOCKERFILE
+ docker run --rm forge-sonar:${{ gitea.sha }} \
+ -Dsonar.host.url=https://sonar.forgebase.fr \
+ -Dsonar.token=${{ secrets.SONAR_TOKEN }}
+
build-and-push:
needs: [test-python, test-js, lint-python, lint-js]
if: gitea.ref == 'refs/heads/main'
diff --git a/CODE_QUALITY.md b/CODE_QUALITY.md
index c47e4a05..907252c1 100644
--- a/CODE_QUALITY.md
+++ b/CODE_QUALITY.md
@@ -81,8 +81,10 @@ ignore de fichier entier ou de règle globale — voir section 4.
- `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 — retiré pour l'instant
-Le job CI `sonarqube` (`.gitea/workflows/deploy.yml`) et le service prod `sonarqube`/`sonar-postgres` (`docker-compose.prod.yml`) ont été retirés le 18/09/2026 : l'accès au dashboard (local WSL2 et prod derrière Caddy) est resté bloqué par des soucis d'infrastructure réseau (redirection de port WSL2/pare-feu Hyper-V côté local, réseau Docker partagé avec Caddy pas encore en place côté prod) sans lien avec le code du moteur — mis de côté volontairement plutôt que de continuer à bloquer sur de l'infra. Le lot "modernisation JS" (voir `docs/JS_MODERNIZATION_PLAN.md`) s'est arrêté après le lot 3 (`S2486`) pour cette même raison : les lots 4+ dépendent de scores Sonar exacts (complexité cognitive notamment) qu'aucun proxy fiable ne remplace. À reprendre une fois l'accès rétabli — voir section 6.
+### SonarQube
+- **CI (`sonar.forgebase.fr`)** : job `sonarqube` (`.gitea/workflows/deploy.yml`), scan à chaque push, **non-bloquant** (`continue-on-error: true`) le temps que le rapport soit entièrement trié. Retiré le 18/09/2026 (voir historique ci-dessous), remis en place le 18/09/2026 une fois l'instance prod confirmée opérationnelle.
+- **Service prod** (`docker-compose.prod.yml`) : reste retiré côté dépôt pour l'instant — l'instance prod opérationnelle n'est pas (encore) pilotée par ce fichier de ce côté-ci ; à réintégrer explicitement si besoin.
+- **Historique du retrait temporaire (16-18/09/2026)** : l'accès au dashboard (local WSL2 et prod derrière Caddy) était resté bloqué par des soucis d'infrastructure réseau (redirection de port WSL2/pare-feu Hyper-V côté local, réseau Docker partagé avec Caddy pas encore en place côté prod à ce moment) sans lien avec le code du moteur. Le lot "modernisation JS" (voir `docs/JS_MODERNIZATION_PLAN.md`) s'était arrêté après le lot 3 (`S2486`) pour cette même raison — les lots 4+ dépendent de scores Sonar exacts (complexité cognitive notamment) qu'aucun proxy fiable ne remplace parfaitement (le SonarLint de l'IDE a servi de solution de contournement fonctionnelle en attendant, voir lots 4 en cours directement sur les fichiers ouverts dans l'éditeur).
## 3. Lancer les checks en local
@@ -106,7 +108,7 @@ python -m pytest tests/ -q
node --test static/js/play/__tests__/*.test.js static/js/play/offline/__tests__/*.test.js static/js/scenes/__tests__/*.test.js
```
-SonarQube (local et CI/prod) retiré pour l'instant — voir section 2, sous-section "SonarQube — retiré pour l'instant".
+SonarQube : voir section 2, sous-section "SonarQube" — CI restaurée (non-bloquante), instance locale toujours bloquée (voir historique dans cette même sous-section).
## 4. Procédure de justification d'une exception
@@ -137,15 +139,18 @@ SonarQube (local et CI/prod) retiré pour l'instant — voir section 2, sous-sec
| `static/js/play/bindings.js:179` (`gameData = newData`) | `javascript:S2703` | Pattern volontaire de scripts globaux (pas des modules ES) : `gameData`/`screensData` sont déclarés une fois par `let` dans le `` (voir Phase 3). Plusieurs syntaxes de suppression testées (commentaire Jinja `{# #}`, commentaire JS natif dans un `