Pedro Roque

Pedro Roque

Software Architect

Integrating Azure Pipelines with SonarCloud

September 4, 2020
devops
sonar
azure
pipelines

Today's post is about the integration of SonarCloud with Azure Pipelines.

SonarCloud is the SaaS version of the well-known SonarQube, a tool considered the standard for static code analysis.

This post's purpose is to show how SonarCloud can be integrated into a pull request validation pipeline, maximizing our ability to detect problems or bugs in the code.

Let's start with a standard pull request validation pipeline, assuming an Azure service connection to SonarCloud has already been created.

1trigger: none
2
3pool:
4  vmImage: "ubuntu-latest"
5
6variables:
7  buildConfiguration: "Release"
8  buildSolution: "src/discografia.sln"
9  testProjects: "src/tests/**/*.csproj"
10
11steps:
12  - task: DotNetCoreCLI@2
13    name: "Restore"
14    inputs:
15      projects: $(buildSolution)
16      command: "restore"
17
18  - task: DotNetCoreCLI@2
19    name: "Build"
20    displayName: "dot net build $(buildConfiguration)"
21    inputs:
22      command: "build"
23      projects: $(buildSolution)
24      arguments: "--configuration $(buildConfiguration)"
25
26  - task: DotNetCoreCLI@2
27    name: "RunTests"
28    displayName: "dot net test $(buildConfiguration)"
29    inputs:
30      command: "test"
31      projects: $(testProjects)
32      arguments: "--configuration $(buildConfiguration) --logger trx"
33      publishTestResults: true

With this configuration, we will restore all solution dependencies, execute the build, and run the tests. At this moment, there is no integration with Sonar.

When we run the pipeline, everything seems normal except for the test coverage, which appears as not configured.

first run

To generate the test coverage report, three steps are necessary:

  1. Install the coverlet.msbuild package in all test projects.
  2. Modify the test step to collect the coverage data.
  3. Add a task to publish the coverage report.
1  - task: DotNetCoreCLI@2
2    name: "RunTests"
3    displayName: "dot net test $(buildConfiguration)"
4    inputs:
5      command: "test"
6      projects: $(testProjects)
7      arguments: "--configuration $(buildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=Cobertura /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/ --logger trx"
8      publishTestResults: true
9
10  - task: PublishCodeCoverageResults@1
11    displayName: "Publish code coverage report"
12    inputs:
13      codeCoverageTool: "Cobertura"
14      summaryFileLocation: "$(Build.SourcesDirectory)/**/*.cobertura.xml"

Now it's possible to see the coverage report in the pipeline

build

To integrate with SonarCloud, we need to add 3 new steps to our pipeline and fix the coverage report generation:

  1. Prepare the code analysis for SonarCloud immediately before the build step.
1  - task: SonarCloudPrepare@1
2    inputs:
3      SonarCloud: $(sonar.connection)
4      organization: $(sonar.organization)
5      scannerMode: "MSBuild"
6      projectKey: $(sonar.projectKey)
7      projectName: $(sonar.projectName)
8      extraProperties: |
9        sonar.exclusions=**/obj/**,**/*.dll
10        sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/**/coverage.opencover.xml
11        sonar.cs.vstest.reportsPaths=$(Build.SourcesDirectory)/**/*.trx

2. Modify the coverage report generation

To have the code coverage report in Azure Pipelines and SonarCloud, we need to solve a problem: Azure Pipelines doesn't accept the OpenCover format, and SonarCloud doesn't take the Cobertura format!

As a solution, we generate the report in the OpenCover format and add a script to convert it to Cobertura.

1  - task: DotNetCoreCLI@2
2    name: "RunTests"
3    displayName: "dot net test $(buildConfiguration)"
4    inputs:
5      command: "test"
6      projects: $(testProjects)
7      arguments: "--configuration $(buildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/ --logger trx"
8      publishTestResults: true
9
10  - script: |
11      dotnet tool install dotnet-reportgenerator-globaltool --tool-path . 
12      ./reportgenerator "-reports:$(Build.SourcesDirectory)/TestResults/Coverage/coverage.opencover.xml" "-targetdir:coverage/Cobertura" "-reporttypes:Cobertura;HTMLInline;HTMLChart"
13    condition: eq( variables['Agent.OS'], 'Linux' )
14    displayName: Run Reportgenerator on Linux
15
16  - script: |
17      dotnet tool install dotnet-reportgenerator-globaltool --tool-path .
18      .\reportgenerator.exe "-reports:$(Build.SourcesDirectory)/TestResults/Coverage/coverage.opencover.xml" "-targetdir:coverage/Cobertura" "-reporttypes:Cobertura;HTMLInline;HTMLChart"
19    condition: eq( variables['Agent.OS'], 'Windows_NT' )
20    displayName: Run Reportgenerator on Windows

3. Finally, we run the Code Analysis and publish the results

1  - task: SonarCloudAnalyze@1
2  - task: PublishCodeCoverageResults@1
3    displayName: "Publish code coverage report"
4    inputs:
5      codeCoverageTool: "Cobertura"
6      summaryFileLocation: "$(Build.SourcesDirectory)/**/Cobertura.xml"
7  - task: SonarCloudPublish@1
8    inputs:
9      pollingTimeoutSec: "300"

Complete Pipeline:

1trigger: none
2pool:
3  vmImage: "ubuntu-latest"
4
5variables:
6  buildConfiguration: "Release"
7  buildSolution: "src/discografia.sln"
8  testProjects: "src/tests/**/*.csproj"
9  sonar.connection: "SonarCloud"
10  sonar.organization: "highonsoftware"
11  sonar.projectKey: "rcm-discografia"
12  sonar.projectName: "RCM Backend"
13
14steps:
15  - task: DotNetCoreCLI@2
16    name: "Restore"
17    inputs:
18      projects: $(buildSolution)
19      command: "restore"
20
21  - task: SonarCloudPrepare@1
22    inputs:
23      SonarCloud: $(sonar.connection)
24      organization: $(sonar.organization)
25      scannerMode: "MSBuild"
26      projectKey: $(sonar.projectKey)
27      projectName: $(sonar.projectName)
28      extraProperties: |
29        sonar.exclusions=**/obj/**,**/*.dll
30        sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/**/coverage.opencover.xml
31        sonar.cs.vstest.reportsPaths=$(Build.SourcesDirectory)/**/*.trx
32
33  - task: DotNetCoreCLI@2
34    name: "Build"
35    displayName: "dot net build $(buildConfiguration)"
36    inputs:
37      command: "build"
38      projects: $(buildSolution)
39      arguments: "--configuration $(buildConfiguration)"
40
41  - task: DotNetCoreCLI@2
42    name: "RunTests"
43    displayName: "dot net test $(buildConfiguration)"
44    inputs:
45      command: "test"
46      projects: $(testProjects)
47      arguments: "--configuration $(buildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=$(Build.SourcesDirectory)/TestResults/Coverage/ --logger trx"
48      publishTestResults: true
49
50  - script: |
51      dotnet tool install dotnet-reportgenerator-globaltool --tool-path . 
52      ./reportgenerator "-reports:$(Build.SourcesDirectory)/TestResults/Coverage/coverage.opencover.xml" "-targetdir:coverage/Cobertura" "-reporttypes:Cobertura;HTMLInline;HTMLChart"
53    condition: eq( variables['Agent.OS'], 'Linux' )
54    displayName: Run Reportgenerator on Linux
55
56  - script: |
57      dotnet tool install dotnet-reportgenerator-globaltool --tool-path .
58      .\reportgenerator.exe "-reports:$(Build.SourcesDirectory)/TestResults/Coverage/coverage.opencover.xml" "-targetdir:coverage/Cobertura" "-reporttypes:Cobertura;HTMLInline;HTMLChart"
59    condition: eq( variables['Agent.OS'], 'Windows_NT' )
60    displayName: Run Reportgenerator on Windows
61
62  - task: SonarCloudAnalyze@1
63
64  - task: PublishCodeCoverageResults@1
65    displayName: "Publish code coverage report"
66    inputs:
67      codeCoverageTool: "Cobertura"
68      summaryFileLocation: "$(Build.SourcesDirectory)/**/Cobertura.xml"
69
70  - task: SonarCloudPublish@1
71    inputs:
72      pollingTimeoutSec: "300"

We have a pull request validation pipeline that is appropriately integrated with SonarCloud.

In a future post, we will see how to directly integrate the code analysis results into our PR.