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 statusThe key pieces:
- Tests run and generate a coverage report
- The SonarScanner analyzes the code and sends results to SonarQube
- SonarQube evaluates quality gate conditions
- 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:
- Sign up at sonarcloud.io with your GitHub account
- Import your repository
- 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:
- Create a new project manually or via GitHub integration
- Go to Project Settings → Quality Gates → assign a gate
- 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.xmlFor 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.infoGitHub 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: ≤ 10For 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): = 0PR 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 linesThis 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.infoBranch 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-repoThe 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:
- Settings → Branches → Add rule
- Select "Require status checks to pass before merging"
- Add "test-and-analyze" (or your job name) as a required check
Summary
SonarQube quality gates in GitHub Actions create a hard enforcement point:
- Tests run first — generate coverage and test result reports
- Scanner analyzes — sends metrics and code to SonarQube
- Gate evaluates — checks coverage, bugs, security, duplication
- 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.