Building a Production-Ready SonarQube Scanner Plugin for Devtron CI/CD
How to build a reusable, secure, and language-agnostic SonarQube scanner plugin that enforces code quality gates across all application pipelines in Devtron.
TL;DR
- Built a custom Devtron plugin for SonarQube scanning
- Works across all languages (Java, Python, JS, TS, Go)
- Enforces Quality Gates and blocks deployments on failure
- Zero token exposure in pipeline logs
- One plugin reusable across all apps with minimal config
Introduction
When managing multiple application pipelines using Devtron an open-source Kubernetes-native CI/CD platform, teams often need a consistent way to enforce code quality standards across all applications without asking every team to set up SonarQube from scratch.
The challenge? Devtron's native SonarQube plugin can have limitations. This post walks through building a custom production-ready, reusable plugin that solves these challenges.
Why Build a Custom Plugin
- Native plugin limitations - The built-in Devtron SonarQube plugin may have authentication issues
- No modification access - You cannot modify Devtron's native plugins
- Reusability - One plugin all teams can use without writing their own scripts
- Security - Proper secret handling and token validation before every scan
Architecture Overview
Devtron CI Pipeline
|
|-- Pre-build Stage
| |-- SonarQube Scanner Plugin (custom)
| |-- Container Image <- sonar-scanner binary
| |-- Shell Script <- logic
| |-- sonar-project.properties <- per app repo
|
|-- Build Stage
| |-- Docker Image Build
|
|-- Post-build Stage
|-- Deploy
Key Insight: Separate the binary from the logic. The container image carries the sonar-scanner binary, while the shell script mounted at runtime by Devtron contains all the logic. This means you can update the script without rebuilding the image!
The Container Image
FROM alpine:3.20
RUN apk add --no-cache \
curl unzip openjdk17-jre bash \
nodejs npm libstdc++ libgcc \
&& rm -rf /var/cache/apk/*
RUN curl -sSLo /tmp/sonar.zip \
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-6.2.1.4610-linux-x64.zip" && \
unzip -q /tmp/sonar.zip -d /opt && \
mv /opt/sonar-scanner-6.2.1.4610-linux-x64 /opt/sonar-scanner && \
rm /tmp/sonar.zip && \
rm -rf /opt/sonar-scanner/jre && \
sed -i 's/use_embedded_jre=true/use_embedded_jre=false/' \
/opt/sonar-scanner/bin/sonar-scanner
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk
ENV PATH="/opt/sonar-scanner/bin:/usr/lib/jvm/java-17-openjdk/bin:$PATH"
ENV SONAR_SCANNER_OPTS="-Dsonar.nodejs.executable=/usr/bin/node"
Common Gotchas
1. Alpine needs BOTH libstdc++ AND libgcc
Missing either causes:
Error loading shared library libstdc++.so.6: No such file or directory
2. Never use $(which node) in ENV instruction
# Wrong
ENV SONAR_SCANNER_OPTS="-Dsonar.nodejs.executable=$(which node)"
# Correct
ENV SONAR_SCANNER_OPTS="-Dsonar.nodejs.executable=/usr/bin/node"
The Shell Script
Script Flow
1. Strip trailing slash from URL
2. Validate required variables
3. Security checks (token format, HTTPS)
4. Connectivity check
5. Token authentication via API
6. Source code validation
7. Run sonar-scanner
8. Check Quality Gate via API
9. Report status in logs
Security Handling
# Never print the token
set +x
# Validate token BEFORE scan
AUTH=$(curl --silent -u "$SONAR_TOKEN:" \
"$SONAR_HOST_URL/api/authentication/validate")
if ! echo "$AUTH" | grep -q '"valid":true'; then
echo "Token authentication FAILED!"
exit 1
fi
# Filter token from scanner output
/opt/sonar-scanner/bin/sonar-scanner $SCANNER_ARGS 2>&1 | \
grep -v "sonar.token" | \
grep -v "sonar.login"
Fix Trailing Slash Issue
SonarQube returns HTML instead of JSON when the URL has a trailing slash causing auth to fail even with a valid token!
# Always strip trailing slash first
SONAR_HOST_URL=$(echo "$SONAR_HOST_URL" | sed 's:/*$::')
Quality Gate Control
# FAIL_ON_QUALITY_GATE=false is useful for onboarding legacy projects
FAIL_ON_QUALITY_GATE="${FAIL_ON_QUALITY_GATE:-true}"
if [ "$QG_STATUS" = "ERROR" ]; then
if [ "$FAIL_ON_QUALITY_GATE" = "false" ]; then
echo "WARNING: Quality Gate failed but pipeline continues"
else
exit 1
fi
fi
Devtron Plugin Configuration
Plugin Form Settings
| Field | Value |
|---|---|
| Task Name | SonarQube Scanner with Quality Gate |
| Plugin ID | sonarqube-scanner-quality-gate |
| Version | 1.0.0 |
| Mount Code At | /scripts/sonar-scan.sh |
| Command | /bin/sh |
| Args | /scripts/sonar-scan.sh |
| Mount Code to Container | Yes |
Input Variables
| Variable | Type | Required | Default |
|---|---|---|---|
| SONAR_TOKEN | Secret | Yes | - |
| SONAR_HOST_URL | String | Yes | - |
| SONAR_PROJECT_KEY | String | Yes | - |
| SONAR_PROJECT_NAME | String | No | Same as key |
| SONAR_SOURCES | String | No | /sourcecode |
| SONAR_JAVA_BINARIES | String | No | . |
| SONAR_EXCLUSIONS | String | No | - |
| BRANCH_NAME | String | No | - |
| QUALITY_GATE_TIMEOUT | String | No | 300 |
| FAIL_ON_QUALITY_GATE | String | No | true |
Always use Secret type for SONAR_TOKEN - tokens appear in plain text in logs when using String type!
Per-App Configuration
Each app just needs one file added to its repo root.
JavaScript/TypeScript:
sonar.projectKey=my-app
sonar.projectName=My App
sonar.sources=src
sonar.exclusions=**/node_modules/**,**/coverage/**,**/*.test.js
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.sourceEncoding=UTF-8
Python:
sonar.projectKey=my-python-app
sonar.projectName=My Python App
sonar.language=py
sonar.python.version=3
sonar.sources=.
sonar.exclusions=**/__pycache__/**,**/*.pyc,**/venv/**
sonar.python.coverage.reportPaths=coverage.xml
sonar.sourceEncoding=UTF-8
Java Maven:
sonar.projectKey=my-java-app
sonar.projectName=My Java App
sonar.sources=src/main/java
sonar.java.binaries=target/classes
sonar.tests=src/test/java
sonar.sourceEncoding=UTF-8
Project Onboarding Automation
For each new project we automate everything via SonarQube API:
sh sonar-onboard.sh
# Output:
# Project created: my-app
# Group created: my-app-developers
# User added to group
# Permissions assigned
# Project set to PRIVATE
#
# CI TOKEN: sqa_xxxxxxxxxxxxx
# Add to Devtron as SONAR_TOKEN secret
Challenges and Solutions
| Challenge | Solution |
|---|---|
| Native plugin auth issues | Custom shell script plugin |
| Token exposed in logs | set +x and grep filter |
| Trailing slash in URL | Strip with sed first |
| Alpine missing C++ libs | Add libstdc++ and libgcc |
| Node.js path issue | Hardcode /usr/bin/node |
| Quality Gate blocking new projects | FAIL_ON_QUALITY_GATE=false |
| /bin/sh vs /bin/bash | Full POSIX sh compatible script |
Results
- One plugin reusable across all apps and languages
- Zero token exposure in pipeline logs
- Consistent quality gates across all teams
- New app onboarded in under 10 minutes
- Flexible gradual adoption with FAIL_ON_QUALITY_GATE
What's Next
- Coverage reports (JaCoCo, pytest-cov, lcov)
- Branch and PR analysis
- Slack notifications on Quality Gate failure
- Custom Grafana dashboard for scan trends
Resources
If this helped you drop a reaction and share with your team!
Top comments (0)