Integrating SonarQube Quality Gates with Your CI Test Pipeline on GitHub Actions

Integrating SonarQube Quality Gates with Your CI Test Pipeline on GitHub Actions

SonarQube quality gates block merges when code doesn't meet your team's standards — coverage thresholds, duplication limits, security ratings. This guide covers the full integration: running tests with coverage, sending results to SonarQube, and failing CI when the quality gate fails.


What Are SonarQube Quality Gates?

A Quality Gate is a set of conditions that code must pass before it can be merged or deployed. Examples:

  • Coverage on new code ≥ 80%
  • No new critical bugs
  • No new security vulnerabilities
  • Duplication < 3%
  • Maintainability rating ≥ A

Quality gates fail the CI build when conditions aren't met, giving teams a hard enforcement mechanism rather than a suggestion.


Architecture Overview

GitHub PR → GitHub Actions → Run tests with coverage → Send to SonarQube
                                                              ↓
                                                    Quality Gate check
                                                              ↓
                                                    Pass/Fail PR status

The key pieces:

  1. Tests run and generate a coverage report
  2. The SonarScanner analyzes the code and sends results to SonarQube
  3. SonarQube evaluates quality gate conditions
  4. GitHub Actions polls for the quality gate result and fails the build if it doesn't pass

SonarQube Setup

Option 1: SonarCloud (Hosted)

SonarCloud is free for open-source projects and requires no infrastructure:

  1. Sign up at sonarcloud.io with your GitHub account
  2. Import your repository
  3. Copy your project key and organization key

Option 2: Self-Hosted SonarQube

# docker-compose.yml for local SonarQube
services:
  sonarqube:
    image: sonarqube:10-community
    ports:
      - "9000:9000"
    environment:
      SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
      SONAR_JDBC_USERNAME: sonar
      SONAR_JDBC_PASSWORD: sonar
    volumes:
      - sonar_data:/opt/sonarqube/data
      - sonar_logs:/opt/sonarqube/logs
  
  db:
    image: postgres:15
    environment:
      POSTGRES_USER: sonar
      POSTGRES_PASSWORD: sonar
      POSTGRES_DB: sonar
    volumes:
      - postgresql:/var/lib/postgresql/data

volumes:
  sonar_data:
  sonar_logs:
  postgresql:

Create a Project

In SonarQube:

  1. Create a new project manually or via GitHub integration
  2. Go to Project Settings → Quality Gates → assign a gate
  3. Generate a project analysis token

Project Configuration

sonar-project.properties

Create this file in your repo root:

sonar.projectKey=my-org_my-project
sonar.projectName=My Project
sonar.projectVersion=1.0

# Source and test directories
sonar.sources=src
sonar.tests=tests
sonar.language=python

# Exclude from analysis
sonar.exclusions=**/migrations/**,**/node_modules/**,**/*.min.js

# Coverage report location (must match what your test runner generates)
sonar.python.coverage.reportPaths=coverage.xml
sonar.python.xunit.reportPaths=test-results.xml

For JavaScript/TypeScript:

sonar.projectKey=my-org_my-project
sonar.sources=src
sonar.tests=src
sonar.test.inclusions=**/*.test.ts,**/*.spec.ts
sonar.typescript.lcov.reportPaths=coverage/lcov.info
sonar.javascript.lcov.reportPaths=coverage/lcov.info

GitHub Actions Workflow

Python Project

name: CI + SonarQube Quality Gate
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test-and-analyze:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Required for SonarQube blame information
      
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      
      - name: Install dependencies
        run: pip install -r requirements.txt -r requirements-test.txt
      
      - name: Run tests with coverage
        run: |
          pytest \
            --cov=src \
            --cov-report=xml:coverage.xml \
            --cov-report=term-missing \
            --junitxml=test-results.xml \
            tests/
      
      - name: SonarQube Scan
        uses: SonarSource/sonarqube-scan-action@master
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
      
      - name: SonarQube Quality Gate check
        uses: SonarSource/sonarqube-quality-gate-action@master
        timeout-minutes: 5
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

JavaScript/TypeScript Project

name: CI + SonarQube Quality Gate
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test-and-analyze:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run tests with coverage
        run: npm run test:coverage
        # Assumes package.json has:
        # "test:coverage": "jest --coverage --coverageReporters=lcov"
      
      - name: SonarQube Scan
        uses: SonarSource/sonarqube-scan-action@master
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
      
      - name: Check Quality Gate
        uses: SonarSource/sonarqube-quality-gate-action@master
        timeout-minutes: 5
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

Configuring Quality Gates

In SonarQube's Quality Gate editor, the "Sonar way" default gate enforces:

  • Coverage on new code ≥ 80%
  • Duplication on new code ≤ 3%
  • Security rating = A
  • Reliability rating = A
  • Maintainability rating = A

Creating a Custom Gate

For stricter requirements:

Gate: Production Gate
Conditions:
  - Coverage on new code: ≥ 85%
  - Duplicated lines on new code: < 2%
  - Security Hotspots Reviewed: = 100%
  - New Bugs: = 0
  - New Vulnerabilities: = 0
  - New Code Smells: ≤ 10

For a more permissive gate during initial adoption:

Gate: Adoption Gate
Conditions:
  - Coverage on new code: ≥ 50%  (start low, increase over time)
  - New Vulnerabilities: = 0     (security is non-negotiable)
  - New Bugs (critical/blocker): = 0

PR Decoration

SonarQube adds inline comments to GitHub PRs when using the GitHub Actions integration:

## Quality Gate Status: ❌ Failed

Issues:
- ❌ Coverage on new code (58.3%) is less than 80%
- ✅ No new bugs
- ✅ No new vulnerabilities

**Coverage on new code:** 58.3% (required ≥ 80%)
- `src/payment/processor.py` — 45% coverage on new lines

This is enabled automatically when GITHUB_TOKEN is passed to the scan action.


Multi-Module Projects

For monorepos with multiple services:

# Run analysis per service but send to same SonarQube project
jobs:
  analyze-service-a:
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      
      - name: Test service-a
        run: cd services/service-a && pytest --cov=. --cov-report=xml
      
      - name: Analyze service-a
        uses: SonarSource/sonarqube-scan-action@master
        with:
          projectBaseDir: services/service-a
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

Or use a single sonar-project.properties at the root:

sonar.modules=service-a,service-b,service-c

service-a.sonar.projectName=Service A
service-a.sonar.sources=services/service-a/src
service-a.sonar.python.coverage.reportPaths=services/service-a/coverage.xml

service-b.sonar.projectName=Service B
service-b.sonar.sources=services/service-b/src
service-b.sonar.javascript.lcov.reportPaths=services/service-b/coverage/lcov.info

Branch Analysis and PR Decoration

For branch-specific analysis (requires Developer Edition or SonarCloud):

# In sonar-project.properties
sonar.pullrequest.key=${env.PR_NUMBER}
sonar.pullrequest.branch=${env.PR_BRANCH}
sonar.pullrequest.base=${env.TARGET_BRANCH}
sonar.pullrequest.github.repository=my-org/my-repo

The SonarSource GitHub Action handles this automatically for PRs.


Handling the Quality Gate Result in CI

The sonarqube-quality-gate-action step fails with exit code 1 when the quality gate fails. This automatically fails the GitHub Actions job, blocking PR merges if branch protection requires the check to pass.

Enable branch protection in GitHub:

  1. Settings → Branches → Add rule
  2. Select "Require status checks to pass before merging"
  3. Add "test-and-analyze" (or your job name) as a required check

Summary

SonarQube quality gates in GitHub Actions create a hard enforcement point:

  1. Tests run first — generate coverage and test result reports
  2. Scanner analyzes — sends metrics and code to SonarQube
  3. Gate evaluates — checks coverage, bugs, security, duplication
  4. CI fails or passes — blocked if gate conditions aren't met

Start with the "Sonar way" default gate and tighten conditions as your codebase matures. The key: start with coverage thresholds you can actually achieve today, then increase them incrementally. A gate set to 80% from day one on a codebase at 20% coverage just gets bypassed — start at 30%, enforce it strictly, then raise the bar each quarter.

Read more