zum Inhalt springen
Kontakt

SonarCloud & PowerShell: SonarQube als Cloudservice auch für Azure DevOps Server nutzen

Um die Qualität von Software zu garantieren, gibt es verschiedene Testverfahren. Eines ist die statische Code-Analyse. Sie überprüft anhand formaler Regeln den Code, noch bevor die Software ausgeführt wird. Damit können Coding-Standards eingehalten, aber auch mögliche Fehler und Schwachstellen entdeckt werden.

SonarQube ist ein statisches Codeanalysetool, das die Qualität von Programmcode analysiert und Vorschläge zur Verbesserung macht. Es unterhält dazu eine grosse Bibliothek an Regeln und Best Practices von verschieden Programmiersprachen wie C#, HTML, CSS, JavaScript, TypeScript und vielen mehr.

SonarQube besteht hauptsächlich aus einem Server (1), einer Datenbank (2) mit den Regeln und einem Analyser (4), der sich in den Buildprozess einhängt und die Resultate an den Server schickt. Dazu können noch zusätzliche Plugins (3) die Funktionalität erweitern.

Der Sever wiederum hat drei Prozesse:
• Webserver, um den Server zu konfigurieren und die Analysen zu begutachten.
• Search Server basierend auf Elasticsearch, um die Suche vom Webserver zu ermöglichen.
• Analyse Engine, der die Analyse des Codes anhand der Regeln in der Datenbank vornimmt.

SonarQube Übersicht Bild

Ein «Quality Gate» ist ein Begriff aus dem Entwicklungsprojektmanagement und beschreibt ein Set von Qualitätsmerkmalen, die über die Freigabe des nächsten Projektschrittes entscheiden.

Auch bei SonarQube gibt es Quality Gates. Diese bestehen aus Metriken, die eingehalten werden müssen, wenn neuer Code in die bestehende Code Basis integriert werden soll. Um eine hohe Code Qualität zu gewährleisten, sollte dieser Schritt ein Teil des CI-Builds sein.

 

SonarCloud: SonarQube als Cloudservice

SonarCloud ist ein Cloudservice, der die Funktionalität von SonarQube als Service zur Verfügung stellt. Er lässt sich in verschiedene CI/CD-Pipelines integrieren, unter anderem auch in Azure DevOps. Leider lässt sich SonarCloud aber nicht so gut mit On-Prem Azure DevOps Server – ehemals Team Foundation Server (TFS) – kombinieren.

Einige der Limitationen sind:
• .NET Framework Applikationen können nur über das .NET Tool «SonarScanner» analysiert werden.
• Der Build-Status kann nicht durch das Analyseresultat beeinflusst werden. Daher ist der Build erfolgreich, auch wenn das Quality Gate fehlschlägt.
• Das Plugin für SonarCloud ist nur für Azure DevOps erhältlich.
• Auf Azure können die Beanstandungen von SonarCloud direkt in den Pull Request geschrieben werden. Dies ist On-Prem nicht möglich.

Vor allem die beiden ersten Punkte sind besonders ärgerlich, da sie die Funktionalität empfindlich einschränken. Bei .NET Framework Applikationen kann nicht einmal das Resultat vom SonarScanner im Build dargestellt werden. Da muss nach jedem Build aktiv selber auf SonarCloud nachgeschaut werden. Doch einige der Beschränkungen können mit «WD40 und Duct Tape» (sprich PowerShell) behoben werden.

 

Analyse von .NET Framework Applikationen via PowerShell

Normalerweise wird der SonarQube Analyser als Plugin auf dem Azure DevOps Server bzw. TFS installiert. Beim Analysieren von .NET Framework Applikationen kann sich das Plugin jedoch nicht mit SonarCloud verbinden. Glücklicherweise gibt es den Analyser auch noch als .NET Tool. Das kann in einem PowerShell Script verwendet werden, um den Prepare und den Analyse Schritt auszuführen.

SonarCloud prepare: Wird vor dem Build-Schritt ausgeführt.

[ cmdletbinding ()]
param (
[ string ] $projectKey ,
[ string ] $sonarCloudToken ,
[ string ] $organization
)
if ( - not ( dotnet tool list --global | Select-String -Pattern 'dotnet-sonarscanner' )) {
Write-Verbose "Install dotnet-sonarscanner"
dotnet tool install --global dotnet-sonarscanner
}
dotnet sonarscanner begin /k: $projectKey /d:sonar.login= $sonarCloudToken /d:sonar.host.url= "https://sonarcloud.io" /o: $organization
[cmdletbinding()] param ( [string]$projectKey, [string]$sonarCloudToken, [string]$organization ) if (-not (dotnet tool list --global | Select-String -Pattern 'dotnet-sonarscanner')) { Write-Verbose "Install dotnet-sonarscanner" dotnet tool install --global dotnet-sonarscanner } dotnet sonarscanner begin /k:$projectKey /d:sonar.login=$sonarCloudToken /d:sonar.host.url="https://sonarcloud.io" /o:$organization
[cmdletbinding()]
param (
    [string]$projectKey,
    [string]$sonarCloudToken,
    [string]$organization
)
    if (-not (dotnet tool list --global | Select-String -Pattern 'dotnet-sonarscanner')) {
        Write-Verbose "Install dotnet-sonarscanner"
        dotnet tool install --global dotnet-sonarscanner
    }

    dotnet sonarscanner begin /k:$projectKey /d:sonar.login=$sonarCloudToken /d:sonar.host.url="https://sonarcloud.io" /o:$organization

Sonarcloud Analyse: Wird nach dem Test Schritt ausgeführt.

[ cmdletbinding ()]
param (
[ string ] $sonarCloudToken
)
dotnet sonarscanner end /d:sonar.login= $sonarCloudToken
[cmdletbinding()] param ( [string]$sonarCloudToken ) dotnet sonarscanner end /d:sonar.login=$sonarCloudToken
 [cmdletbinding()]
 param ( 
    [string]$sonarCloudToken
)
    dotnet sonarscanner end /d:sonar.login=$sonarCloudToken

 

Wie bereits erwähnt, kann leider das Resultat nicht in der Zusammenfassung des Builds dargestellt werden. Ein letzter Knackpunkt beim Verwenden dieser Lösung ist, dass sie nur mit einer YAML-Builddefinition funktioniert. Glücklicherweise können klassische Builds einfach in YAML-Builds umgewandelt werden, indem man den Build als YAML darstellen lässt und den Inhalt in eine Datei im Repository selbst kopiert:

YAML Konvertierung Bild

 

Build «failen» lassen bei gescheitertem Quality Gate

Bei .NET Core Applikationen kann man glücklicherweise auf das Plugin von SonarQube zurückgreifen. Damit kann wenigstens das Resultat in der Zusammenfassung des Builds dargestellt werden. Ein Build beendet dennoch erfolgreich, auch wenn das Quality Gate fehlschlägt.
Auch hier muss man sich mit PowerShell behelfen:

param (
[ string ] $sonarCloudToken ,
[ string ] $sonarCloudProjectKey
)
$pullRequestId = $env :SYSTEM_PULLREQUEST_PULLREQUESTID
$branchName = $env :BUILD_SOURCEBRANCHNAME
$tokenBytes = [ System.Text.Encoding ] ::UTF8. GetBytes ( $sonarCloudToken + ":" )
$encodedToken = [ System.Convert ] :: ToBase64String ( $tokenBytes )
$basicAuth = [ string ] :: Format ( "Basic {0}" , $encodedToken )
$authHeader = @ { Authorization = $basicAuth }
if ( $pullRequestId ) {
$qualityGateUri = [ string ] :: Format ( "https://sonarcloud.io/api/qualitygates/project_status?projectKey={0}&pullRequest={1}" , $sonarCloudProjectKey , $pullRequestId )
} else {
$qualityGateUri = [ string ] :: Format ( "https://sonarcloud.io/api/qualitygates/project_status?projectKey={0}&branch={1}" , $sonarCloudProjectKey , $branchName )
}
$response = Invoke-RestMethod -Method Get -Uri $qualityGateUri -Headers $authHeader
$response | ConvertTo-Json | Write-Host
if ( $response .projectStatus.status -eq "OK" ) {
exit 0
} else {
exit 1
}
param ( [string]$sonarCloudToken, [string]$sonarCloudProjectKey ) $pullRequestId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID $branchName = $env:BUILD_SOURCEBRANCHNAME $tokenBytes = [System.Text.Encoding]::UTF8.GetBytes($sonarCloudToken + ":") $encodedToken = [System.Convert]::ToBase64String($tokenBytes) $basicAuth = [string]::Format("Basic {0}", $encodedToken) $authHeader = @{ Authorization = $basicAuth } if ($pullRequestId) { $qualityGateUri = [string]::Format("https://sonarcloud.io/api/qualitygates/project_status?projectKey={0}&pullRequest={1}", $sonarCloudProjectKey, $pullRequestId) } else { $qualityGateUri = [string]::Format("https://sonarcloud.io/api/qualitygates/project_status?projectKey={0}&branch={1}", $sonarCloudProjectKey, $branchName) } $response = Invoke-RestMethod -Method Get -Uri $qualityGateUri -Headers $authHeader $response | ConvertTo-Json | Write-Host if ($response.projectStatus.status -eq "OK") { exit 0 } else { exit 1 }
param (
    [string]$sonarCloudToken,
    [string]$sonarCloudProjectKey
)
    $pullRequestId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID
    $branchName = $env:BUILD_SOURCEBRANCHNAME
    $tokenBytes = [System.Text.Encoding]::UTF8.GetBytes($sonarCloudToken + ":")
    $encodedToken = [System.Convert]::ToBase64String($tokenBytes)
    $basicAuth = [string]::Format("Basic {0}", $encodedToken)
    $authHeader = @{ Authorization = $basicAuth }

    if ($pullRequestId) {
        $qualityGateUri = [string]::Format("https://sonarcloud.io/api/qualitygates/project_status?projectKey={0}&pullRequest={1}", $sonarCloudProjectKey, $pullRequestId)
    } else {
        $qualityGateUri = [string]::Format("https://sonarcloud.io/api/qualitygates/project_status?projectKey={0}&branch={1}", $sonarCloudProjectKey, $branchName)
    }

    $response = Invoke-RestMethod -Method Get -Uri $qualityGateUri -Headers $authHeader
    $response | ConvertTo-Json | Write-Host

    if ($response.projectStatus.status -eq "OK") {
        exit 0
    } else {
        exit 1
    }

 

Dieses Script holt sich das Resultat des Quality Gates des entsprechenden Projektes vom SonarCloud Server. Wenn der Build von einem «Pull Request» initiiert wurde, der von einer Branch policy beeinflusst wird, muss der Status des Quality Gates vom entsprechenden «Pull Request» geholt werden. Anderenfalls wird das Quality Gate vom entsprechenden Branch genommen. Anhand des Status wird dann der Exit Code des Scripts gesetzt.

 

Ein Beispiel: CI-Build Script für eine .NET Core Applikation

Ein CI-Build Script für eine .NET Core Applikation könnte also etwa so aussehen:

trigger:
branches:
include:
- master
pool:
name: 'default'
variables:
solution: 'src/*.sln'
buildPlatform: 'Any CPU'
buildConfiguration: 'Release'
sonarCloudProjectKey: 'projectKey'
steps:
- task: SonarQubePrepare@4
inputs:
SonarQube: 'Sonarcloud'
scannerMode: 'CLI'
configMode: 'manual'
cliProjectKey: $ ( sonarCloudProjectKey )
cliSources: 'src/'
extraProperties: |
sonar.organization=leuchter
sonar.cs.opencover.reportsPaths=src/tests/**/coverage.opencover.xml
sonar.cs.vstest.reportsPaths=$(Agent.TempDirectory)/*.trx
- task: DotNetCoreCLI@2
displayName: Dotnet Build
inputs:
command: 'build'
projects: '$(solution)'
arguments: '--configuration $(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: Dotnet Test
inputs:
command: 'test'
arguments: '--no-build --configuration $(buildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=opencover --logger trx'
publishTestResults: true
projects: '$(solution)'
- task: SonarQubeAnalyze@4
- task: SonarQubePublish@4
inputs:
pollingTimeoutSec: '300'
- task: PowerShell@2
displayName: Validate quality gate
inputs:
filePath: 'buildscripts/scripts/checkSonarqubeQualityGate.ps1'
arguments: '$(SonarCloudToken) $(sonarCloudProjectKey)'
trigger: branches: include: - master pool: name: 'default' variables: solution: 'src/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' sonarCloudProjectKey: 'projectKey' steps: - task: SonarQubePrepare@4 inputs: SonarQube: 'Sonarcloud' scannerMode: 'CLI' configMode: 'manual' cliProjectKey: $(sonarCloudProjectKey) cliSources: 'src/' extraProperties: | sonar.organization=leuchter sonar.cs.opencover.reportsPaths=src/tests/**/coverage.opencover.xml sonar.cs.vstest.reportsPaths=$(Agent.TempDirectory)/*.trx - task: DotNetCoreCLI@2 displayName: Dotnet Build inputs: command: 'build' projects: '$(solution)' arguments: '--configuration $(buildConfiguration)' - task: DotNetCoreCLI@2 displayName: Dotnet Test inputs: command: 'test' arguments: '--no-build --configuration $(buildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=opencover --logger trx' publishTestResults: true projects: '$(solution)' - task: SonarQubeAnalyze@4 - task: SonarQubePublish@4 inputs: pollingTimeoutSec: '300' - task: PowerShell@2 displayName: Validate quality gate inputs: filePath: 'buildscripts/scripts/checkSonarqubeQualityGate.ps1' arguments: '$(SonarCloudToken) $(sonarCloudProjectKey)'
trigger:
  branches:
    include:
    - master

pool:
  name: 'default'
variables:
  solution: 'src/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'
  sonarCloudProjectKey: 'projectKey'

steps:
- task: SonarQubePrepare@4
  inputs:
    SonarQube: 'Sonarcloud'
    scannerMode: 'CLI'
    configMode: 'manual'
    cliProjectKey: $(sonarCloudProjectKey)
    cliSources: 'src/'
    extraProperties: |
      sonar.organization=leuchter
      sonar.cs.opencover.reportsPaths=src/tests/**/coverage.opencover.xml
      sonar.cs.vstest.reportsPaths=$(Agent.TempDirectory)/*.trx

- task: DotNetCoreCLI@2
  displayName: Dotnet Build
  inputs:
    command: 'build'
    projects: '$(solution)'
    arguments: '--configuration $(buildConfiguration)'

- task: DotNetCoreCLI@2
  displayName: Dotnet Test
  inputs:
    command: 'test'
    arguments: '--no-build --configuration $(buildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=opencover --logger trx'
    publishTestResults: true
    projects: '$(solution)'

- task: SonarQubeAnalyze@4

- task: SonarQubePublish@4
  inputs:
    pollingTimeoutSec: '300'

- task: PowerShell@2
  displayName: Validate quality gate
  inputs:
    filePath: 'buildscripts/scripts/checkSonarqubeQualityGate.ps1'
    arguments: '$(SonarCloudToken) $(sonarCloudProjectKey)'

 

Mein Fazit

Man merkt deutlich, dass SonarCloud ein modernes Produkt ist, das entwickelt wurde, um zusammen mit anderen modernen Produkten genutzt zu werden. Viele Features, die den Umgang mit der Analyse für den Entwickler einfacher machen, können nur mit Azure DevOps genutzt werden. Selbst Basisfunktionalitäten wie die Analyse von .NET Framework Applikationen müssen mit PowerShell Scripts zusammengeklebt werden. Aber schlussendlich haben wir SonarCloud erfolgreich in die On-Prem Azure DevOps Server bzw. TFS Buildpipeline integriert.

 

Hast du Fragen zu SonarCloud, .NET Framework Applikationen, On-Prem Azure DevOps / TFA oder PowerShell Scripts? Lass mir einen Kommentar da oder kontaktiere uns für ein unverbindliches Beratungsgespräch zu deiner individuellen Lösung:

Beratungstermin vereinbaren!