SonarCloud Code Analysis

2022-01-08

Configuration

Setting up SonarQube & SonarCloud for Azure DevOps is straight-forward and can be done by following the steps outlined here:

SonarQube installation/setup: https://docs.sonarqube.org/latest/setup/install-server/ (SonarQube requires Java 11 or Docker to work depending on whether you are installation the zip file or docker image).

SonarCloud installtion/setup is far more trivial and is configured when creating an account.

Continuous Integration

Once the SonarQube/Cloud has been configured you then need to configure your DevOps pipeline. In order to do so you will need to install the relevant extension: SonarQube: https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarqube SonarCloud: https://marketplace.visualstudio.com/items?itemName=SonarSource.sonarcloud

Installing the SonarQube/Cloud extension will give you access to three tasks in DevOps:

  1. Prepare Analysis Configuration
  2. Run Code Analysis
  3. Publish Quality Gate Result

The configuration process for the SonarQube/Cloud extensions are identical.

For the purposes of this work I have manually added the sonar configuration settings, however I recommend adding the settings to a file called sonar-project.properties and storing that in git with the solution.

Pipeline Process

Once the extensions are configured the build pipeline needs to be updated so that the analysis can be reported to SonarQube/Cloud correctly.

The build process should have the following steps:

  1. Checkout solution
  2. Install NodeJs
  3. Run npm ci
  4. Run the SonarQubePrepare/SonarCloudPrepare task
  5. Run npm build
  6. Run npm test
  7. Run SonarQubeAnalyse/SonarCloudAnalyse task
  8. Run SonarQubePublish/SonarCloudPublish task

Below is a sample YAML pipeline.

# Node.js
# Build a general Node.js project with npm.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

steps:
- task: NodeTool@0
  inputs:
    versionSpec: '10.x'
  displayName: 'Install Node.js'

 - script: |
     npm run lint
   displayName: 'npm lint'

- script: |
    npm ci
  displayName: 'npm ci'

- task: SonarCloudPrepare@1
  inputs:
    SonarCloud: 'SonarCloud'
    organization: '[ORGANISATION_NAME]' #this will come from setup
    scannerMode: 'CLI'
    configMode: 'manual'
    cliProjectKey: '[PROJECT_KEY_NAME]' #this will come from setup
    cliProjectName: '[PROJECT_NAME]' #this will come from setup
    cliSources: [COMMA_SEPARATED_LIST_OF_FOLDERS_TO_COVER] #e.g.'components,lib,pages,utils'
    extraProperties: |
      sonar.testExecutionReportPaths=test-report.xml
      sonar.javascript.lcov.reportPaths=coverage/lcov.info

- script: |
    npm run build
  displayName: 'npm build'

- script: |
    npm run test
  displayName: 'npm run test'

- task: SonarCloudAnalyze@1

- task: SonarCloudPublish@1
  inputs:
    pollingTimeoutSec: '300'

- task: PowerShell@2
  displayName: SonarCloud Result
  inputs:
    targetType: 'inline'
    script: |
      $token = [System.Text.Encoding]::UTF8.GetBytes([AUTHORISATION_TOKEN] + ":")
      $base64 = [System.Convert]::ToBase64String($token)

      $basicAuth = [string]::Format("Basic {0}", $base64)
      $headers = @{ Authorization = $basicAuth }

      $result = Invoke-RestMethod -Method Get -Uri https://sonarcloud.io/api/qualitygates/project_status?projectKey=[PROJECT_NAME] -Headers $headers
      $result | ConvertTo-Json | Write-Host

      if ($result.projectStatus.status -eq "OK") {
      Write-Host "Quality Gate Succeeded"
      }else{
      throw "Quality gate failed"
      }

- task: CopyFiles@2
  displayName: "Copy Output Directory"
  inputs:
    Contents: |
        **
        !.git/**/*
        !debug.log
    targetFolder: $(Build.ArtifactStagingDirectory)

Solution Setup

In order for SonarQube/Cloud to analyse the code correct there are a couple steps that need to be taken. The following packages need to be installed into the solution's dev dependencies:

  "devDependencies": {
    "@babel/core": "^7.12.3",
    "@babel/plugin-transform-react-jsx": "^7.12.1",
    "@babel/preset-env": "^7.12.1",
    "@typescript-eslint/eslint-plugin": "^4.5.0",
    "@typescript-eslint/parser": "^4.5.0",
    "babel-jest": "^26.6.1",
    "eslint": "^7.12.1",
    "eslint-plugin-jest": "^24.1.0",
    "eslint-plugin-react": "^7.21.5",
    "eslint-plugin-prettier": "^3.1.4",
    "eslint-plugin-react": "^7.21.5",
    "jest": "^26.6.1",
    "jest-sonar-reporter": "^2.0.0"
  }

You need to add a jest.config.js file with the following contents:

/*
 * For a detailed explanation regarding each configuration property, visit:
 * https://jestjs.io/docs/en/configuration.html
 */

module.exports = {

  // Automatically clear mock calls and instances between every test
  clearMocks: true,

  /*
   * Indicates whether the coverage information should be collected while executing the test
   */
   collectCoverage: true,

  // The directory where Jest should output its coverage files
  coverageDirectory: 'coverage',

  // The test environment that will be used for testing
  testEnvironment: 'node',

  /*
   * This option allows the use of a custom results processor
   */
  testResultsProcessor: 'jest-sonar-reporter'

};

You will need to configure babel correctly - below is a sample configuration in babel.config.js

module.exports = {
    presets: [
        [
            '@babel/preset-env',
            { 
                targets: { 
                    node: 'current' 
                } 
            }
        ]
    ],
    plugins: ['@babel/plugin-transform-react-jsx']
  };

All these files should be stored with the root of the solution.