
SonarCloud – brilliant at analyzing code, absolutely terrible at explaining how to set it up. Seriously, how can one tool be so effective at finding bugs yet make its documentation feel like deciphering ancient hieroglyphics while blindfolded?
Today we’re tackling how to add pull request decoration in Azure DevOps with SonarQube Cloud (SonarCloud), written by someone who spent way too many hours banging their head against this particular wall. This guide builds on Marc Rufer’s excellent work on [HOWTO] Integrate SonarCloud analysis in an Azure DevOps YAML pipeline – the man who graciously helped rescue me from my own confusion.
Project Structure
Here’s what we’re working with:
├───deploy
│ ├───iac
│ │ ├───backend
│ │ └───vars
│ │ └───*.tf
│ └───pipelines
│ ├───*.yml
├───src
│ ├───AzureFunctions.Python
│ ├───AzureFunctions.Dotnet
│ ├───Client
│ ├───Domain
│ ├───Domain.Migrations
│ ├───Server
│ ├───Server.Services
│ ├───Server.Utilities
│ ├───Shared
│ └───Tests
│ ├───Unit.Tests
│ └───Integration.Tests
└───Solution.sln
Step 1: Write the Pipelines
I recommend following Marc’s guide for the details, but here’s my battle-tested configuration that actually works:
solution-quality.yml
name: Solution Quality Pipeline
trigger:
branches:
include:
- dev
- acc
- main
paths:
include:
- src
- deploy
stages:
- stage: Quality
displayName: Quality Build
pool:
vmImage: ubuntu-latest
jobs:
- template: jobs-build-quality.yml
parameters:
buildProjects: "**/*.sln"
solutionFolder: "/home/vsts/work/1/s/"
buildConfiguration: "Release"
jobs-build-quality.yml
parameters:
- name: buildProjects
type: string
default: "**/*.sln"
- name: solutionFolder
type: string
default: "/home/vsts/work/1/s/"
- name: buildConfiguration
type: string
default: "Release"
jobs:
- job: Build
displayName: Quality Build Solution
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
fetchDepth: 0
- task: UseDotNet@2
displayName: Get DotNet SDK
inputs:
packageType: sdk
useGlobalJson: true
- task: SonarCloudPrepare@3
displayName: "Prepare analysis configuration"
inputs:
SonarCloud: "AZ_DEVOPS_SERVICE_CONNECTION_NAME_HERE"
organization: NAME_OF_THE_SONARCLOUD_ORGANIZATION_HERE
projectKey: SONARCLOUD_PROJECT_KEY_HERE
projectName: "SOLUTON"
scannerMode: "dotnet"
extraProperties: |
scm.provider=git
sonar.python.version=3.12
sonar.terraform.provider.azure.version=4.27.0
sonar.terraform.tfconfigs=/**/deploy/infra/**/*.tf
sonar.projectBaseDir=/home/vsts/work/1/s/
sonar.cs.vscoveragexml.reportsPaths=${{parameters.solutionFolder}}/coverage.xml sonar.exclusions=**/bin/**,**/obj/**,**/*.dll,**/.yarn/**,**/node_modules/**,**/.pnp*,**/.xml,**/.md,**/.json,**/wwwroot/**,**/LogFiles/**,**/Domain.Migrations/**,**/venv/**,**/__pycache__/**,**/.python_packages/**
- task: DotNetCoreCLI@2
displayName: Build Solution #This will build the .NET Azure Functions app(s) too
inputs:
command: build
projects: ${{parameters.buildProjects}}
arguments: "--configuration ${{parameters.buildConfiguration}}"
- script: dotnet tool install --global dotnet-coverage
displayName: Install dotnet-coverage
- script: dotnet-coverage collect "dotnet test ${{parameters.solutionFolder}} --configuration ${{parameters.buildConfiguration}}" -f xml -o "coverage.xml"
displayName: Solution Tests Coverage
- template: jobs-build-python-functions.yml
parameters:
functionAppPath: "src/AzureFunctions.Python"
pythonVersion: "3.12"
- task: SonarCloudAnalyze@3
displayName: SonarCloud - Run code analysis
- task: SonarCloudPublish@3
displayName: SonarCloud - Publish quality gate result
jobs-build-python-functions.yml
parameters:
- name: functionAppPath
type: string
- name: pythonVersion
type: string
default: "3.12"
steps:
- task: UsePythonVersion@0
displayName: "🐍 Set Python version to ${{ parameters.pythonVersion }}"
inputs:
versionSpec: '${{ parameters.pythonVersion }}'
architecture: 'x64'
- bash: |
cd '${{ parameters.functionAppPath }}'
if [ -f extensions.csproj ]
then
dotnet build extensions.csproj --output ./bin
fi
pip install --target="./.python_packages/lib/site-packages" -r ./requirements.txt
displayName: '🐍 Install Python Function dependencies'
Step 2: Configure Branch Policies (The Moment of Truth)
Once you’ve set up your pipeline with the right permissions – again, check out Marc’s post for the detailed setup requirements -> head over to the branch policies for your main development branch. Mine is called ‘dev’, but yours might be ‘development’ or whatever naming convention you use.

You’ll want to add the SonarCloud pipeline to the build validation.
Step 3: Configure SonarQube Cloud (SonarCloud) Integration (The Critical Part Everyone Skips)
Once you’ve added the SonarQube Cloud (SonarCloud) pipeline to your build validation, head over to SonarCloud.io and navigate to your project page.
Go to Administration → Pull Requests and add a new Personal Access Token (PAT).
This is where the magic happens – SonarCloud needs this token to communicate back to Azure DevOps and update your pull request status checks.
Important: Use a technical user or service account in Azure DevOps instead of your personal PAT. This prevents the integration from breaking when people leave the team or change passwords. Create a dedicated service account specifically for SonarCloud integration – your future self will thank you when you’re not troubleshooting broken integrations at 2 AM.


This step is crucial – without the proper PAT configuration, SonarCloud can analyze your code but can’t send the results back to Azure DevOps. You’ll see analysis results in SonarCloud but no status checks in your pull requests, which defeats the whole purpose.
Step 4: Test Your Setup (The Moment of Truth)
Create a new pull request to your dev branch. Once the SonarCloud step finishes successfully, you should see comments appearing in your PR with code quality feedback.

But wait, there’s more! (And by more, I mean potential disasters)
You might encounter this delightful error that had me questioning my life choices:
##[error]ERROR: Error during SonarScanner execution
ERROR: Could not find the pullrequest with key '123'
ERROR: Caused by: Error 404 on https://sonarcloud.io/api/alm_integration/show_pullrequest?project=organisation-name_Solution&pullrequestKey=123 : {"errors":[{"msg":"You don't have permission, or the provided pullrequest with key '123' doesn't exist."}]}
If this happens to you (and it probably will, because apparently I’m not the only one who struggles with this), here’s how to fix it:
First: Check Your Repository Binding
The issue is likely that SonarCloud isn’t properly connected to your Azure DevOps repo.
Check SonarCloud repository binding:
- Go to SonarCloud → Your project → Administration → Integration
- Verify the “Repository” field points to the correct Azure DevOps repo
- Format should be:
{organization}/{project}/{repository}

If Nothing Shows Up: Your Organization Binding is Broken
This was my problem – the organization-level connection was toast.
- Go to SonarCloud → Organizations → Administration → Organization Binding
- If the PAT shows as invalid, there’s your smoking gun
- Add a new PAT – I used the service account’s PAT
- Make sure the service account has proper permissions in both SonarCloud and Azure DevOps
- Once valid, you should see projects appear in the repository binding section

Final Test: Create Chaos
Create a new PR with intentionally bad code – add a ridiculously smelly class to watch SonarCloud lose its mind.
There’s nothing quite like seeing SonarCloud tear apart your deliberately terrible code to confirm everything is working properly.
Here’s mine : SonarCloudPrTestService.cs
public class SonarCloudPrTestService // Violation: Should be in proper namespace { public string password = "admin123"; // Violation: Hard-coded password, public field public static int COUNT = 0; // Violation: Non-private static field // Violation: Too many parameters public void DoSomething(string a, string b, string c, string d, string e, string f, string g, string h) { var unused = "never used"; // Violation: Unused variable // Violation: Empty catch block try { var x = 10 / 10; } catch (Exception ex) { // Nothing here - violation! } // Violation: Cognitive complexity too high for (int i = 0; i < 10; i++) { if (i > 5) { for (int j = 0; j < 5; j++) { if (j == 2) { if (a != null) { if (b != null) { Console.WriteLine("Deep nesting"); // Violation: Deep nesting } } } } } } // Violation: Magic numbers if (COUNT == 42) { COUNT = 123; } } // Violation: Method too long (simulated with comments) public void VeryLongMethod() { Console.WriteLine("Line 1"); Console.WriteLine("Line 2"); Console.WriteLine("Line 3"); Console.WriteLine("Line 4"); Console.WriteLine("Line 5"); Console.WriteLine("Line 6"); Console.WriteLine("Line 7"); Console.WriteLine("Line 8"); Console.WriteLine("Line 9"); Console.WriteLine("Line 10"); Console.WriteLine("Line 11"); Console.WriteLine("Line 12"); Console.WriteLine("Line 13"); Console.WriteLine("Line 14"); Console.WriteLine("Line 15"); Console.WriteLine("Line 16"); Console.WriteLine("Line 17"); Console.WriteLine("Line 18"); Console.WriteLine("Line 19"); Console.WriteLine("Line 20"); // ... continuing would make this even worse } // Violation: Identical methods (code duplication) public string Method1() { var result = "Hello"; result += " World"; return result.ToUpper(); } public string Method2() { var result = "Hello"; result += " World"; return result.ToUpper(); } // Violation: No access modifier, poor naming void badmethodname() // Violation: Bad naming convention { var sql = "SELECT * FROM Users WHERE id = " + COUNT; // Violation: SQL injection risk } }