Retour au Blog
AI & Development3 juin 20259 min de lecture

Utiliser l'IA pour réparer ce que l'IA a cassé : le pont entre SonarQube et Copilot

DR

Doug Ribeiro

9 min de lecture

Copilot génère du code rapidement. SonarQube vous dit que ce code a des problèmes. Rien ne relie les deux. Le développeur doit lire le rapport Sonar, l'interpréter, revenir à l'éditeur, et demander manuellement à Copilot de corriger ce que Copilot a écrit en premier lieu. C'est un trou dans le workflow, pas dans l'outillage, et c'est l'agent à fort impact le moins cher que vous pouvez construire dès maintenant.

Le schéma est facile à imaginer. Une PR se fait signaler des problèmes, une partie est corrigée, le reste passe avec un 'pas bloquant', et un ou deux sont mal lus. Le temps qu'un senior la relise, la PR contient déjà une dette de qualité que l'outillage a détectée et qu'un humain a écartée. L'humain ne l'a pas ignorée par paresse. Il l'a ignorée parce que le feedback était un lien vers un dashboard, pas un changement actionnable.

La boucle qui manque

PR ouverte SonarQube analyse L'agent lit le rapport L'agent génère le prompt de correction Le développeur colle, relit, commite

L'agent a une seule mission : transformer un problème Sonar en un prompt que le développeur peut coller dans Copilot sans réfléchir. C'est tout. Il ne réécrit pas le code. Il n'ouvre pas des PRs automatiquement. Il comble le trou entre 'voici un problème' et 'voici une demande de correction avec le contexte complet'.

Une vérification de licence avant de planifier votre fin de semaine. L'analyse de pull request n'est pas dans SonarQube Community Build. Elle demande Developer Edition ou plus, et Community rejette sonar.pullrequest.key purement et simplement. Le chemin gratuit, c'est donc l'analyse de branche : lancez le scanner sur main, laissez l'agent lire les problèmes apparus depuis la dernière analyse, et postez les prompts sur le commit de merge plutôt que sur la PR ouverte. Si vous voulez le déclencheur sur PR sans payer, le plugin communautaire de branches (mc1arke/sonarqube-community-branch-plugin) ajoute l'analyse de branches et de PR à Community Build. Il est maintenu par la communauté et non supporté par SonarSource, alors lisez la matrice de compatibilité avant d'y brancher votre équipe. Tout ce qui suit est le même agent dans les deux cas. Seuls le déclencheur et l'endroit où il commente changent.

Pourquoi celui-ci en premier

Quand on me demande quel devrait être leur premier agent, c'est ce que je recommande à toute équipe qui utilise déjà Sonar et Copilot. Trois raisons :

  • L'entrée est structurée. SonarQube a une API propre qui retourne les problèmes avec rule ID, sévérité et localisation de fichier. Vous ne parsez pas du HTML ni ne faites du scraping.
  • La sortie est un blob de texte. L'agent produit un prompt. Pas d'exécution de code, pas de risque en production, pas de dépendances runtime.
  • La valeur est évidente dès la première semaine. Les développeurs ressentent le gain de temps dès la première PR avec des warnings Sonar.

Comparez cela à un agent de triage de bugs ou un agent de code review. Les deux sont utiles. Les deux demandent 10x plus d'effort pour atteindre une qualité utilisable. Le pont vers SonarQube est un projet de weekend qui est utilisé tous les jours.

Ce que fait réellement l'agent

Déclencheur : le webhook SonarQube qui se déclenche quand l'analyse se termine. Sur une PR si votre édition fait l'analyse de pull request, sur la branche sinon. Une GitHub Action qui tourne après que Sonar a fini fonctionne de la même manière.

Étapes, dans l'ordre :

  • Récupérer la liste des problèmes de cette PR depuis Sonar. Filtrer INFO, garder BLOCKER, CRITICAL, MAJOR.
  • Pour chaque problème, lire le fichier sur la plage de lignes signalée. Inclure 10 lignes de contexte au-dessus et en dessous.
  • Regrouper les problèmes par fichier. Un prompt Copilot par fichier, pas par problème. Les développeurs veulent corriger par fichier.
  • Générer un prompt structuré : la règle, pourquoi elle compte, l'extrait, le pattern de correction. Inclure les conventions du projet issues de votre base de connaissance.
  • Poster un seul commentaire sur la PR avec les prompts, repliables par fichier.

À quoi ressemble le prompt

markdown
## Fix prompt for `src/orders/order-summary.component.ts`

SonarQube flagged 3 issues in this file. Paste this into Copilot or Claude Code:

---

In `order-summary.component.ts`, fix the following issues while preserving existing behavior:

1. **Line 42 (typescript:S1481)**: unused local variable `totals`.
   Remove the variable. If it was intended for debugging, delete it, do not comment it out. The team convention is to remove dead code rather than leave it behind.

2. **Line 67 (typescript:S4123)**: `await` should only be used with promises.
   `formatAmount` returns a string, not a Promise. Drop the `await` keyword on that call.

3. **Line 88 (typescript:S6544)**: Promise returned in a context expecting `void`.
   The async callback passed to `addEventListener` returns a Promise, which is unhandled. Wrap the body in a non-async IIFE or handle the returned Promise explicitly. Prefer the IIFE pattern since the team avoids bare async handlers on DOM events.

Do not modify any file other than `order-summary.component.ts`. Keep imports sorted alphabetically. Return only the full updated file content, no explanation.

Remarquez ce qui est dans ce prompt et qui n'est pas dans un commentaire Sonar standard : la description de la règle traduite en langage humain, la convention d'équipe, la contrainte dure sur ce qu'il ne faut pas modifier, le format de sortie. C'est la partie que le développeur faisait dans sa tête. Maintenant c'est l'agent qui la fait.

Le câblage

Cet article fait binôme avec l'article sur le monorepo. L'agent réutilise ReActAgent de libs/base_agent/ et vit dans agents/sonar_bridge/. Moins de 100 lignes de vrai code, la majeure partie étant de la glue HTTP. Ce qui compte, c'est la forme : une poignée d'outils typés, un playbook, et une fonction de build que le runtime appelle.

python
# libs/tools/sonar.py
import os
import requests

SONAR_URL = os.environ["SONAR_URL"]
SONAR_TOKEN = os.environ["SONAR_TOKEN"]

PROJECT_KEY = os.environ["SONAR_PROJECT_KEY"]

def get_issues_for_pr(pr_key: str) -> list[dict]:
    r = requests.get(
        f"{SONAR_URL}/api/issues/search",
        params={
            # componentKeys is not optional. Without a project scope the
            # search does not come back with this PR's issues.
            "componentKeys": PROJECT_KEY,
            "pullRequest": pr_key,
            # Legacy taxonomy, still accepted. On SonarQube 10.2 and up
            # the current filter is impactSeverities=HIGH,MEDIUM.
            "severities": "BLOCKER,CRITICAL,MAJOR",
        },
        auth=(SONAR_TOKEN, ""),
        timeout=15,
    )
    r.raise_for_status()
    return r.json()["issues"]


# libs/tools/github.py
import os
import requests

GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]

def read_file_range(repo: str, ref: str, path: str, start: int, end: int) -> str:
    # Returns the flagged range plus 10 lines of context on each side.
    r = requests.get(
        f"https://api.github.com/repos/{repo}/contents/{path}",
        params={"ref": ref},
        headers={
            "Authorization": f"Bearer {GITHUB_TOKEN}",
            "Accept": "application/vnd.github.raw",
        },
        timeout=15,
    )
    r.raise_for_status()
    lines = r.text.splitlines()
    lo = max(0, start - 11)
    hi = min(len(lines), end + 10)
    return "\n".join(lines[lo:hi])

def comment_on_pr(repo: str, pr_number: int, body: str) -> None:
    r = requests.post(
        f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments",
        headers={"Authorization": f"Bearer {GITHUB_TOKEN}"},
        json={"body": body},
        timeout=15,
    )
    r.raise_for_status()

Trois outils, deux fichiers. Celui de Sonar cadre la recherche sur le projet et filtre par sévérité avant que l'agent ne voie la liste, donc le bruit de basse priorité n'atteint jamais le prompt ni le budget de tokens. La lecture GitHub retourne une fenêtre autour des lignes signalées pour que l'agent puisse raisonner sur la correction en contexte, pas seulement sur le rule ID. Le commentaire GitHub est une seule écriture à la fin de l'exécution, imposée par le playbook plutôt que par le code, donc le comportement est visible et modifiable sans redéploiement.

python
# agents/sonar_bridge/playbook.py
PLAYBOOK = """You are the Sonar-to-Copilot bridge agent.

Goal: turn a Sonar analysis report on a pull request into prompts a
developer can paste into Copilot or Claude Code without context-switching.

Process, in order:
1. Call get_issues_for_pr with the PR key from the trigger. Project
   scope and severity are already handled by the tool.
2. Group the remaining issues by file path.
3. For each file, call read_file_range on the flagged line ranges to
   pull the code into context.
4. For each file, synthesize one Copilot-ready prompt. Apply project
   conventions retrieved from the knowledge base.
5. Call comment_on_pr once with all per-file prompts in a single
   collapsible comment.

Hard rules:
- You generate prompts. You do not generate code.
- comment_on_pr is called at most once per run.
- If there are zero qualifying issues, exit without commenting.
"""

Le playbook est l'endroit où le comportement de l'agent est spécifié. Remarquez ce qu'il contient : les étapes du processus, les règles dures, le format de sortie. Ce qu'il ne contient pas : le guide de traduction règle par règle. Ça vit dans la base de connaissance, récupéré au runtime, parce que les conventions d'équipe changent et je ne veux pas redéployer l'agent à chaque fois.

python
# agents/sonar_bridge/agent.py
from libs.base_agent import ReActAgent, AgentConfig
from libs.memory import MemoryStack
from libs.telemetry import Telemetry
from libs.tools import sonar, github
from .playbook import PLAYBOOK

def build() -> ReActAgent:
    return ReActAgent(
        config=AgentConfig(
            name="sonar_bridge",
            version="0.1.0",
            playbook=PLAYBOOK,
            tools=[
                sonar.get_issues_for_pr,
                github.read_file_range,
                github.comment_on_pr,
            ],
            # Confidence in the prompt it drafted for a file. Below
            # this, the agent lists the issue and writes no prompt.
            confidence_threshold=0.85,
        ),
        memory=MemoryStack.default(),
        telemetry=Telemetry.default(),
    )

La dernière pièce est le trigger. Un handler FastAPI dans apps/api/ reçoit le webhook Sonar, extrait le PR key et appelle build().run({"pr_key": ...}). L'agent de base fait le reste : récupère des exécutions passées similaires depuis le vector store, exécute la boucle ReAct, écrit la télémétrie dans Postgres, poste le commentaire sur GitHub. Le seuil est placé au-dessus de la valeur par défaut de la bibliothèque parce qu'un mauvais prompt de correction coûte plus cher qu'un prompt manquant. Un développeur qui colle dans Copilot un prompt faux et sûr de lui livre le mauvais changement en croyant que Sonar l'a approuvé. Si vous avez construit la structure du monorepo en premier, ajouter un nouvel agent consiste essentiellement à écrire les trois fichiers ci-dessus et les tests qui vont avec.

La partie non évidente : les conventions d'équipe

Un problème Sonar hors contexte, c'est une violation de règle. Un problème Sonar avec le contexte d'équipe, c'est une directive spécifique. L'agent a besoin de savoir des choses comme 'nous préférons async/await aux observables bruts', 'nous n'utilisons jamais var', 'notre pattern de null-check, c'est l'optional chaining, pas un ternaire'. C'est là qu'entre en jeu votre base de connaissance, que ce soit un vault Obsidian, un dossier de markdown dans le repo, ou un espace Notion.

L'agent lit la règle, cherche la convention d'équipe pour ce type de règle, et les fusionne. C'est la différence entre 'ajouter un null check' (générique) et 'ajouter un null check en utilisant l'optional chaining, le pattern qu'on utilise dans services/' (spécifique et cohérent avec le codebase).

Ce qu'il faut mesurer

Si vous livrez cela, suivez deux chiffres.

  • Temps moyen entre le rapport Sonar et le commit de correction. Avant l'agent, c'était probablement mesuré en jours ou jamais. Après, cela doit être en heures.
  • Pourcentage de problèmes signalés effectivement corrigés avant le merge. Avant l'agent, mesurez votre référence, parce qu'elle est généralement plus basse que l'équipe ne le suppose. Après, comparez à cette référence et à rien d'autre. Un chiffre venu de l'équipe de quelqu'un d'autre n'est pas une cible.

Ne suivez pas 'problèmes résolus'. Les développeurs peuvent fermer des problèmes en les marquant won't fix, et ce chiffre ment. Suivez les vrais changements de code sur les lignes signalées.

Ce qui peut mal tourner

Deux modes de défaillance à prévoir.

D'abord, l'agent génère des prompts pour des problèmes qui sont en fait des faux positifs. Sonar en a. Si l'agent ne sait pas quelles règles votre équipe supprime habituellement, il produit du bruit. Le remède, c'est de nourrir l'agent avec votre historique de suppression pour qu'il apprenne quelles règles rétrograder ou sauter entièrement. Même vector store, query différente.

Ensuite, les développeurs se mettent à traiter la sortie du prompt comme si c'était la correction. Ils la collent dans Copilot, acceptent la suggestion, commitent. Sans review. Ça ruine tout le propos. L'agent est un outil de brouillon, pas un décideur. Fixez l'attente tôt : le développeur reste propriétaire de la correction.

Copilot a accéléré l'écriture du code. SonarQube a accéléré la détection de ses défauts. Le trou entre les deux était un workflow, pas un outil. C'est là que vit l'agent utile le moins cher.

Références

AIAgentsSonarQubeCopilotCode Quality