SuperAPI AI Directory

sonarqube-quality-gate-playbook

Testing & quality

View repository

Updated 10-15 16:43

About this skill

SonarQube Quality Gate Playbook

Apply this flow in small, verifiable iterations. Prioritize real impact and low risk.

Required inputs

Define these inputs before the first cycle:

  • SONAR_HOST_URL (example: http://127.0.0.1:9000)
  • SONAR_TOKEN with analysis permissions
  • SONAR_PROJECT_KEY and optional SONAR_PROJECT_NAME
  • SONAR_NEW_CODE_REFERENCE_BRANCH (default: main)
  • Monorepo paths: apps/backend, apps/frontend, packages/*
  • Test commands per app (backend/frontend) with coverage
  • Report paths lcov.info per app
  • Sonar file (sonar-project.properties) or equivalent parameters in CI
  • Exclusion pattern for coverage and analysis
  • Quality Gate thresholds (coverage, bugs, vulns, smells, duplication)
  • Build/pipeline command that must not break

Prioritization strategy

Follow this work order:

  1. New Code first
  2. New Bugs and Vulnerabilities
  3. Security Hotspots in status TO_REVIEW
  4. Coverage in critical modules and those touched by the PR
  5. New Code Smells (prioritize high severity)
  6. Duplication and technical debt in active areas
  7. Historical debt (only if it does not affect the sprint goal)

Impact heuristic

For each issue or file, calculate priority in a simple way:

priorityScore = (isNewCode*100) + (isBugOrVuln*80) + (isHotspotToReview*70) + severityWeight + (criticalPath*20) + (coverageGap*10) - (estimatedEffort*5)

Where severityWeight can be: blocker 40, critical 30, major 20, minor 10.

Anti coverage inflation rule

Do not accept tests that only raise the percentage without validating behavior:

  • Every new test must cover the happy case, the negative case, and the edge case
  • Snapshot-only as the sole assertion is prohibited
  • Avoid trivial tests of code without logic
  • If the test does not fail in the face of a real bug, it does not count as valuable coverage

Skill pipeline (step by step)

Coverage discrepancy runbook (reusable case)

Use this runbook when SonarQube shows much lower coverage than local.

Typical symptom

  • Local (Jest/Vitest): high coverage (for example >90%)
  • SonarQube: noticeably lower coverage (for example 40-60%)

Frequent root causes

  1. Paths SF: with backslashes (\\) in lcov.info generated on Windows.
  2. SonarQube running on Linux/containers without being able to map paths with \\.
  3. Shared packages with low coverage (example packages/utils) dragging down the global.

Quick diagnosis

  1. Review the first entries SF: of the lcov.info of each app/package.
  2. Check in the sonar-scanner logs whether there are unmapped or ignored coverage files.
  3. Compare coverage per module (backend/frontend/packages) to detect outliers.

Suggested commands:

Get-Content "apps\\backend\\coverage\\lcov.info" | Select-String "^SF:" | Select-Object -First 10
Get-Content "apps\\frontend\\coverage\\lcov.info" | Select-String "^SF:" | Select-Object -First 10

If paths like SF:src\\app.tsappear, normalize before the scan.

Recommended remediation

  1. Normalize paths to forward slash (/) in all lcov.info before sonar-scanner.
  2. Re-run coverage tests and scan.
  3. Increase coverage of the package/module with the lowest percentage so as not to skew the global.

Portable example (scripts/fix-lcov-paths.js):

const fs = require('node:fs');

const reports = [
  'apps/backend/coverage/lcov.info',
  'apps/frontend/coverage/lcov.info',
  'packages/utils/coverage/lcov.info'
];

for (const reportPath of reports) {
  if (!fs.existsSync(reportPath)) continue;
  const content = fs.readFileSync(reportPath, 'utf8');
  const normalized = content.replace(/\\\\/g, '/');
  fs.writeFileSync(reportPath, normalized);
}

CI Guardrail (fail if backslashes remain):

if grep -q 'SF:.*\\\\' apps/backend/coverage/lcov.info; then
  echo "ERROR: Backslashes encontrados en lcov.info"
  exit 1
fi

Step 1: Detect current gap

  1. Run tests with coverage in relevant backend/frontend/packages
  2. Run SonarQube analysis
  3. Raise metrics and new issues to prioritize

Generic commands:

bun run --cwd apps/backend test -- --coverage
bun run --cwd apps/frontend test -- --coverage

sonar-scanner `
  -Dsonar.host.url=$env:SONAR_HOST_URL `
  -Dsonar.token=$env:SONAR_TOKEN `
  -Dsonar.projectKey=$env:SONAR_PROJECT_KEY `
  -Dsonar.qualitygate.wait=true

curl -4 -s "$env:SONAR_HOST_URL/api/measures/component?component=$env:SONAR_PROJECT_KEY&metricKeys=coverage,new_coverage,bugs,new_bugs,vulnerabilities,new_vulnerabilities,code_smells,new_code_smells,duplicated_lines_density,new_duplicated_lines_density" -u "$env:SONAR_TOKEN:"

curl -4 -s "$env:SONAR_HOST_URL/api/issues/search?componentKeys=$env:SONAR_PROJECT_KEY&resolved=false&inNewCodePeriod=true&types=BUG,VULNERABILITY,CODE_SMELL&ps=500" -u "$env:SONAR_TOKEN:"

curl -4 -s "$env:SONAR_HOST_URL/api/hotspots/search?projectKey=$env:SONAR_PROJECT_KEY&status=TO_REVIEW&ps=500" -u "$env:SONAR_TOKEN:"

Step 2: Generate or update Sonar config

Create or update sonar-project.properties at root with the monorepo's real paths.

Step 3: Ensure real coverage reading

  • Verify existence of lcov.info
  • Configure sonar.javascript.lcov.reportPaths with all paths
  • Validate in the scanner logs that there are no missing coverage warnings
  • Verify that lines SF: use / and not \\
  • If there is a mixed Windows/Linux environment, run path normalization before the scan
  • Confirm that relevant shared packages also report coverage (not only apps)

Step 4: Run tests consistently

  • Use the same commands and flags locally and in CI
  • Publish coverage artifacts
  • Do not mix runners/flags between environments

Step 5: Iterate in small batches

  • Batch A: New Bugs/Vulns/Hotspots
  • Batch B: Coverage on New Code
  • Batch C: New Code Smells
  • Batch D: Duplication/debt in touched areas

Guided test generation (backend and frontend)

1) Identify files without coverage

  • Main source: SonarQube (files with uncovered lines in New Code)
  • Secondary source: lcov.info + modified files in the PR
git diff --name-only origin/main...HEAD | Where-Object { $_ -match '\.(ts|tsx)$' }

2) Choose the correct test type

  • Unit: pure logic and functions with few dependencies
  • Integration (backend): controllers + services + mocked repositories
  • Component (frontend): interactions, states, and accessibility
  • E2E: only critical flows, not as a replacement for unit/component

3) Mocking strategy

  • DB: fake repo or isolated test database
  • External HTTP: deterministic mocks (without hitting real services)
  • Time/random: control clock and random values
  • Avoid excessive mocks that hide real defects

4) Minimum test quality criteria

  • Clear Arrange/Act/Assert structure
  • Semantic assertions of expected behavior
  • Happy path + negative + edge cases mandatory
  • Stable test without flakiness
  • Behavior-oriented name

5) Naming and structure

  • Backend: *.spec.ts
  • Frontend: *.test.tsx
  • Recommended pattern:
    • should <resultado> when <condicion>
    • returns <error> when <input invalido>

Remediation of Code Smells, Bugs, and Security

Apply surgical fixes, without massive style changes.

Checklist by category:

  • Complexity: extract functions, use early return, reduce nesting
  • Duplication: consolidate utilities and avoid copy-paste
  • Nullability: explicit guards and safe defaults
  • Error handling: avoid catch empty, propagate with context
  • Leaks/resources: close timers, subscriptions, connections
  • Regex/DoS: avoid catastrophic patterns and limit input
  • Insecure dependencies: update vulnerable libraries
  • Sanitization: validate and sanitize input, avoid XSS/injection
  • Security Hotspots: review and document resolution for each item

Exclusion rules and trade-offs

Acceptable exclusions (with justification):

  • **/index.ts without logic
  • DTOs or simple types without branches
  • generated code (**/generated/**)
  • minimal bootstrap without business logic

Unacceptable exclusions:

  • services/use cases/controllers with logic
  • hooks and validators with branches
  • authentication/authorization code
  • exclude to pass the percentage without improving quality

Rule: each exclusion must record the technical reason.

Definition of Done and metrics

Declare cycle completed only with evidence:

  • Quality Gate in state PASSED
  • new_coverage >= 80
  • new_bugs = 0
  • new_vulnerabilities = 0
  • new_code_smells = 0
  • new_security_hotspots_reviewed = 100%
  • Duplication and debt within gate threshold
  • Build and pipelines green

Minimum evidence to attach:

  1. output of sonar-scanner with sonar.qualitygate.wait=true
  2. output of metrics/issues/hotspots APIs
  3. coverage by folder (backend/frontend/packages)
  4. summary of resolved issues (before/after)

Configuration snippets

sonar-project.properties

sonar.projectKey=my-org_my-monorepo
sonar.projectName=my-monorepo
sonar.sourceEncoding=UTF-8

sonar.sources=apps/backend/src,apps/frontend/src,packages
sonar.tests=apps/backend,apps/frontend,packages
sonar.test.inclusions=**/*.spec.ts,**/*.test.ts,**/*.test.tsx,**/*.spec.tsx

sonar.javascript.lcov.reportPaths=apps/backend/coverage/lcov.info,apps/frontend/coverage/lcov.info
sonar.newCode.referenceBranch=main

sonar.exclusions=**/dist/**,**/build/**,**/node_modules/**,**/*.d.ts
sonar.coverage.exclusions=**/index.ts,**/*.dto.ts,**/generated/**,**/*.stories.tsx

Jest backend (example)

import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov', 'html'],
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.dto.ts',
    '!src/**/index.ts',
    '!src/**/generated/**'
  ]
};

export default config;

Vitest frontend (example)

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'jsdom',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov', 'html'],
      reportsDirectory: './coverage',
      include: ['src/**/*.{ts,tsx}'],
      exclude: ['src/**/*.stories.tsx', 'src/**/index.ts']
    }
  }
});

GitHub Actions (example)

name: quality-gate

on:
  pull_request:
  push:
    branches: [main]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: oven-sh/setup-bun@v2
      - run: bun install --frozen-lockfile

      - name: Backend tests with coverage
        run: bun run --cwd apps/backend test -- --coverage

      - name: Frontend tests with coverage
        run: bun run --cwd apps/frontend test -- --coverage

      - name: Normalize lcov paths (Windows/Linux safe)
        run: node scripts/fix-lcov-paths.js

      - name: Validate lcov path format
        run: |
          if grep -q 'SF:.*\\\\' apps/backend/coverage/lcov.info; then
            echo "ERROR: Backslashes encontrados en backend lcov.info"
            exit 1
          fi

      - name: Build
        run: bun run build

      - name: SonarQube Scan
        env:
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        run: |
          sonar-scanner \
            -Dsonar.host.url=$SONAR_HOST_URL \
            -Dsonar.token=$SONAR_TOKEN \
            -Dsonar.qualitygate.wait=true

GitLab CI (example)

stages:
  - test
  - quality

variables:
  GIT_DEPTH: "0"

test_and_build:
  stage: test
  image: oven/bun:1
  script:
    - bun install --frozen-lockfile
    - bun run --cwd apps/backend test -- --coverage
    - bun run --cwd apps/frontend test -- --coverage
    - node scripts/fix-lcov-paths.js
    - bun run build
  artifacts:
    when: always
    paths:
      - apps/backend/coverage/
      - apps/frontend/coverage/

sonarqube:
  stage: quality
  image: sonarsource/sonar-scanner-cli:latest
  dependencies:
    - test_and_build
  script:
    - sonar-scanner -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.token=$SONAR_TOKEN -Dsonar.qualitygate.wait=true
  allow_failure: false

Skill README

Use this quick summary during execution:

  1. run tests with coverage
  2. run sonar-scanner with gate wait
  3. prioritize New Code -> Bugs/Vulns -> Hotspots -> Coverage -> Smells
  4. iterate in small batches and attach evidence

Base commands:

  • bun run --cwd apps/backend test -- --coverage
  • bun run --cwd apps/frontend test -- --coverage
  • sonar-scanner -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.token=$SONAR_TOKEN -Dsonar.qualitygate.wait=true

PR Checklist

  • No new Bugs or Vulnerabilities
  • No new Code Smells
  • New Coverage >= 80%
  • New Hotspots reviewed
  • No unjustified exclusions
  • Tests with happy, negative, and edge cases
  • Build and CI green
  • Sonar evidence attached
  • lcov.info without backslashes in lines SF:
  • Shared packages without a strong coverage gap

Command templates ready to paste

$env:SONAR_HOST_URL="http://127.0.0.1:9000"
$env:SONAR_TOKEN="<token>"
$env:SONAR_PROJECT_KEY="<project-key>"

bun run --cwd apps/backend test -- --coverage
bun run --cwd apps/frontend test -- --coverage
bun run build

sonar-scanner `
  -Dsonar.host.url=$env:SONAR_HOST_URL `
  -Dsonar.token=$env:SONAR_TOKEN `
  -Dsonar.projectKey=$env:SONAR_PROJECT_KEY `
  -Dsonar.qualitygate.wait=true

curl -4 -s "$env:SONAR_HOST_URL/api/measures/component?component=$env:SONAR_PROJECT_KEY&metricKeys=new_coverage,new_bugs,new_vulnerabilities,new_code_smells,new_duplicated_lines_density" -u "$env:SONAR_TOKEN:"

Defaults to adjust if there is uncertainty

  • New Code branch: main
  • New Coverage threshold: 80
  • maximum duplication new code: 3%
  • coverage exclusions: only files without logic
  • batch strategy: 1 PR per critical category