Skip to content

Dagger SonarQube

We’ve been experimenting with Dagger in our previous tests, but let’s not forget to check our code quality! 😎

SonarQube

SonarQube is an open-source code quality tool that ensures our code adheres to best practices and clean code principles.
SonarQube

It has many other features, but for now, we’ll focus on integrating Sonar analysis into our CI/CD pipeline using Dagger.

While there’s a Dagger module for SonarQube, I couldn’t get it to work with a self-hosted SonarQube URL:
Daggerverse Sonar

I might open a Pull Request or an Issue with the module maintainer to resolve this problem (or maybe it’s a skill issue 🫣).

In the meantime, we’ll do it ourselves! It’s just a SonarCLI container, so it shouldn’t be too difficult.


Integration

Let’s revisit our project lib-acl-json.
We already have the following functions to install packages and run unit tests:

@func()
async test(source: Directory): Promise<Directory> {
return this.buildEnv(source).withExec(["bun", "test"]).directory("/src");
}
@func()
buildEnv(source: Directory): Container {
const nodeCache = dag.cacheVolume("node");
return dag
.container()
.from("oven/bun")
.withDirectory("/src", source)
.withMountedCache("/root/.npm", nodeCache)
.withWorkdir("/src")
.withExec(["bun", "install"]);
}

Sonar Scanner CLI

Since Dagger natively supports working with containers, we’ll use the Sonar Scanner CLI image:
Sonar Scanner

Here’s the scan function:

@func()
async sonar(source: Directory, url: string, token: Secret): Promise<string> {
return dag
.container()
.from("sonarsource/sonar-scanner-cli")
.withUser("root")
.withDirectory("/usr/src", source)
.withSecretVariable("SONAR_TOKEN", token)
.withEnvVariable("SONAR_HOST_URL", url)
.withExec(["sonar-scanner"])
.stdout();
}
  • .stdout() returns the result as a string. If you prefer, you can omit it to return a Promise<Container>.
  • .withUser("root") ensures the container has the necessary permissions for analysis.

Testing the Function

First, set your token as an environment variable:

Fenêtre de terminal
export SONAR_TOKEN=sqa_8172debfdbzdjhugYA66871Y8gdybndeazudaojidpaldoqdlq

Now, run the function with dagger call:

Fenêtre de terminal
dagger call sonar --source=. --url=https://your-url-sonar.fr --token=env:SONAR_TOKEN

Result:

Dagger call sonar
Dagger Result sonar

The analysis was successful! 🎉


Adding Code Coverage

Currently, we don’t see code coverage in Sonar because we haven’t executed the tests or captured the results.
Let’s create a function that executes multiple steps:

@func()
async minimal(
source: Directory,
url: string,
token: Secret
): Promise<Container> {
const preBuild = this.buildEnv(source);
const unitTest = await this.test(preBuild.directory("/src"));
await this.sonar(unitTest, url, token);
return preBuild;
}
  • preBuild returns the directory after bun install, allowing us to build the project later.
  • unitTest stores the directory after running tests, ensuring coverage data is available for Sonar analysis.

Run the minimal function:

Fenêtre de terminal
dagger call minimal --source=. --url=https://your-url-sonar.fr --token=env:SONAR_TOKEN

Result:

Dagger Result sonar minimal

We see the steps bun install, test, and Sonar analysis completed successfully.
The project’s coverage percentage is now visible in the Sonar interface:

Coverage


Why Use a Minimal Function?

The minimal function lets developers run Sonar analysis without building the entire project.
If they want to build later, Dagger’s cache makes the process faster.


Full index.ts File

Here’s the complete index.ts:

import {
dag,
Container,
Directory,
object,
func,
Secret,
} from "@dagger.io/dagger";
@object()
export class LibAclJson {
@func()
async publish(
source: Directory,
version: string,
token: Secret,
url_sonar: string,
token_sonar: Secret
): Promise<void> {
const buildOutput = await this.build(source, url_sonar, token_sonar);
const publishContainer = dag
.container()
.from("node:lts")
.withWorkdir("/src")
.withDirectory("/src", buildOutput)
.withSecretVariable("TOKEN", token)
.withExec([
"sh",
"-c",
'echo "//registry.npmjs.org/:_authToken=${TOKEN}" > /root/.npmrc',
])
.withExec(["npm", "version", version, "--no-git-tag-version"])
.withExec(["npm", "publish", "--access public"]);
await publishContainer.exitCode();
}
@func()
async build(
source: Directory,
url: string,
token: Secret
): Promise<Directory> {
const preBuild = await this.minimal(source, url, token);
const buildContainer = preBuild.withExec([
"bun",
"build",
"src/index.ts",
"--outdir",
"./dist",
"--target",
"node",
]);
return buildContainer.directory("/src");
}
@func()
async minimal(
source: Directory,
url: string,
token: Secret
): Promise<Container> {
const preBuild = this.buildEnv(source);
const unitTest = await this.test(preBuild.directory("/src"));
await this.sonar(unitTest, url, token);
return preBuild;
}
@func()
async sonar(source: Directory, url: string, token: Secret): Promise<string> {
return dag
.container()
.from("sonarsource/sonar-scanner-cli")
.withUser("root")
.withDirectory("/usr/src", source)
.withSecretVariable("SONAR_TOKEN", token)
.withEnvVariable("SONAR_HOST_URL", url)
.withExec(["sonar-scanner"])
.stdout();
}
@func()
async test(source: Directory): Promise<Directory> {
return this.buildEnv(source).withExec(["bun", "test"]).directory("/src");
}
@func()
buildEnv(source: Directory): Container {
const nodeCache = dag.cacheVolume("node");
return dag
.container()
.from("oven/bun")
.withDirectory("/src", source)
.withMountedCache("/root/.npm", nodeCache)
.withWorkdir("/src")
.withExec(["bun", "install"]);
}
}

Demonstrating Cache Efficiency

Run the build function:

Fenêtre de terminal
dagger call build --source=. --url=https://your-url-sonar.fr --token=env:SONAR_TOKEN

Result: 4.4 seconds!

Cache Efficiency

With caching, only the build step needs to run.

Final Build Result


Conclusion

As you’ve seen, integration is straightforward thanks to containerized tools like Sonar Scanner.
Dagger’s use of containers makes local pipelines closely resemble production environments, streamlining CI/CD processes.


GitHub

Project link: lib-acl-json