How to add pull request decoration in Azure DevOps with SonarQube Cloud (SonarCloud) for idiots

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:

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
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *