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 aPromise<Container>
..withUser("root")
ensures the container has the necessary permissions for analysis.
Testing the Function
First, set your token as an environment variable:
export SONAR_TOKEN=sqa_8172debfdbzdjhugYA66871Y8gdybndeazudaojidpaldoqdlq
Now, run the function with dagger call
:
dagger call sonar --source=. --url=https://your-url-sonar.fr --token=env:SONAR_TOKEN
Result:
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 afterbun 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:
dagger call minimal --source=. --url=https://your-url-sonar.fr --token=env:SONAR_TOKEN
Result:
We see the steps bun install
, test
, and Sonar analysis
completed successfully.
The project’s coverage percentage is now visible in the Sonar interface:
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:
dagger call build --source=. --url=https://your-url-sonar.fr --token=env:SONAR_TOKEN
Result: 4.4 seconds!
With caching, only the build step needs to run.
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