The more code gets written with AI assistance, the easier it is to fall into a false sense of security. It compiles, tests pass, everything looks fine but underneath there can still be vulnerabilities, dead code, or metrics that don't actually mean anything because they're only computed halfway. So instead of stacking yet another "eyeball" AI review on top, I decided to add a hard, automated layer to my project: static code analysis that checks every PR against the same, measurable criteria.
I went with SonarQube Cloud, mainly because it has a free plan for open-source projects, and it also ships its own MCP server meaning the AI agent I work with can pull analysis results directly, without me copy-pasting reports back and forth. Below is a step-by-step account of how this rollout went in devset-ce, my local testing engine for event-driven systems on Kafka and RabbitMQ, based on three concrete commits and what real benefits it brought.
1. The analysis workflow in CI
Starting point: commit a6746d3, PR #44 adding .github/workflows/sonar.yml and sonar-project.properties.
The workflow builds the backend (Gradle, JDK 25), installs and tests the frontend (Node 22), and finally runs the official SonarSource/sonarqube-scan-action:
- name: Build backend
working-directory: devset-ce-be
run: ./gradlew build -x test
- name: Test frontend
working-directory: devset-ce-fe
run: npm run test
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@713881670b6b3676cda39549040e2d88c70d582e # v8.2.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
Notice ./gradlew build -x test backend tests were skipped at this stage. That was a deliberate first step, but it also created an immediately visible gap: Sonar got coverage from the frontend (via sonar.javascript.lcov.reportPaths), but none at all from the backend.
sonar-project.properties sets sources separately for backend and frontend, excludes build/, dist/, node_modules/, and test files from the main-code analysis, and points to backend tests separately via sonar.tests.
2. Real coverage with JaCoCo
Step two was commit 91822a8, PR #50 "wire backend JaCoCo coverage into SonarQube analysis". This directly addresses the gap from step 1.
Changes to build.gradle: adding the jacoco plugin (pinned to version 0.8.14), hooking it into the test and integTest tasks via finalizedBy jacocoTestReport, and configuring the report to merge execution data from both unit and integration tests:
plugins {
id 'java'
id 'jacoco'
...
}
jacoco {
toolVersion = "0.8.14"
}
jacocoTestReport {
dependsOn test
executionData fileTree(layout.buildDirectory.dir("jacoco")) { include "*.exec" }
reports {
xml.required = true
}
}
In the workflow, the backend build step changes from skipping tests to running the full test + report cycle:
- name: Build backend with coverage
working-directory: devset-ce-be
run: ./gradlew build integTest jacocoTestReport
And sonar-project.properties gets one new line tying it all together:
sonar.coverage.jacoco.xmlReportPaths=devset-ce-be/build/reports/jacoco/test/jacocoTestReport.xml
The result: SonarQube now calculates real backend code coverage (unit + integration), not just frontend numbers. This is what finally makes the coverage metric in Sonar trustworthy before this, it was partial and could give a false sense of security.
3. UTF-8, where you least expect to need it
The third change is commit 4921e9c, PR #54 a single line in build.gradle:
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
options.compilerArgs += "-parameters"
}
Without an explicit encoding setting, the Java compiler falls back to the platform's default encoding which can be inconsistent across different CI environments and can silently corrupt non-ASCII characters in sources or resources. A small fix, but exactly the kind that's easy to overlook until you start looking for it systematically instead of by accident.
What this actually gets you
After these three commits, I have a genuinely different starting point than before, when the only line of defense was a human code review:
-
A quality gate that blocks merges. New code that doesn't meet the thresholds (duplication, code smells, security hotspots) doesn't make it into
mainnobody has to police this manually in review. -
Trustworthy coverage instead of a number for show. Before PR #50, the backend built with
-x test, so the coverage metric in Sonar was essentially fiction. After wiring in JaCoCo (unit + integration), backend coverage reflects what's actually tested, so I can rely on that number when deciding where tests are missing. - Security hotspots and CVEs caught automatically. Static analysis plus the CVE resolution strategy in Gradle (PR #54) catch vulnerable dependencies and suspicious patterns before anyone sees them in review I'm no longer relying on a reviewer happening to remember a specific CVE.
- A whole class of bugs that normally slip through review. The UTF-8 encoding fix is a good example it's not something a human typically notices while reading a diff, yet it can silently corrupt data on a different CI environment.
- A shorter feedback loop thanks to MCP. The AI agent gets analysis results with full context (file, line, issue type) and proposes a fix itself I don't have to manually copy a report from a dashboard into my editor, so small fixes don't get pushed to "later."
- Metrics as living documentation of the project's state. Coverage, duplication, and open issue counts in Sonar give a measurable snapshot of repo quality at any point in time useful when an outside contributor is deciding whether it's worth getting involved.
The takeaway
These three commits are a good illustration of what adding static analysis to an existing project actually looks like in practice: not a one-time "big bang," but a series of small iterations. First, a basic workflow even with tests skipped. Then, filling in real coverage, because without it the metrics are only half the picture. Finally, a small fix that the analysis (or the build itself) would sooner or later have forced anyway.
What really speeds up this loop is SonarQube's MCP server the AI agent gets direct access to analysis results and context (which file, which line, what type of issue), so going from a flagged issue to an actual code fix is faster than manually shuttling reports between a dashboard and an editor.
The developer, the AI agent, and static analysis each do a different job here: the developer decides what makes business sense, AI speeds up the actual work, and Sonar makes sure neither side introduces a regression. Wired together in CI/CD, this means the quality gate and real coverage metrics run on every PR, before anything reaches main.
This workflow is part of devset-ce a local, source-available testing engine for event-driven systems on Kafka and RabbitMQ, built by [DevSet]. If you're working with message brokers and tired of writing the same Kafka scripts over and over, it might save you some time feel free to check it out, star it, or open an issue.
Top comments (0)