Skip to content

Commit c65c0bc

Browse files
ameernexus1claude
andcommitted
ci: add SonarQube analysis, quality gate and issue report
Mirrors the setup already running on hrms, qa-process-automation and be-automation, adapted to this repo. Placed between Verify Environment and Build, so the gate can actually stop a release -- this pipeline deploys, unlike the QC automation jobs where the scan runs after the work. Verify Environment still runs first so a missing .env variable fails fast and cheaply, before a scan is spent, keeping the existing "fail before anything is torn down" ordering intact. - SonarQube Analysis: runs the sonar-scanner CLI via the official sonarsource/sonar-scanner-cli image inside withSonarQubeEnv('MySonarQube'). The agent has Docker but no sonar-scanner on PATH and no SonarQube Scanner tool installation, so the image avoids adding global tool config. sonar.working.directory is redirected into the bind-mounted workspace: the image's baked-in /tmp/.scannerwork is owned by uid 1000 and unwritable under -u, and the override also lands report-task.txt where waitForQualityGate looks for it. - Quality Gate: waitForQualityGate(abortPipeline: true) inside a 5 minute timeout, so a missed webhook cannot hold the deploy hostage. - Fetch Sonar Issues / Publish Issue Report: paginates api/issues/search and renders the result on the build page via warnings-ng. Both wrapped in catchError(buildResult: 'UNSTABLE') so reporting cannot fail a deploy that already passed the gate. The merged JSON keeps the top-level total/p/ps fields, without which the warnings-ng parser fails format detection and silently reports zero issues. sonar-project.properties is new. This is a polyglot monorepo (282 Python, 266 TS, 188 TSX), so sources are listed explicitly to keep media_files/, database-docker/ and docs/ out. backend/analytics_server/tests and cli/source/__tests__ sit inside source roots, so they are excluded from main and declared as tests instead -- without that the scan fails outright with "file can't be indexed twice". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 119c779 commit c65c0bc

2 files changed

Lines changed: 143 additions & 0 deletions

File tree

Jenkinsfile

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,104 @@ pipeline {
6666
}
6767
}
6868

69+
stage('SonarQube Analysis') {
70+
steps {
71+
// CLUSTOX: placed before Build/Deploy on purpose -- this is a deploy
72+
// pipeline, so the gate below has to be able to stop a release. It sits
73+
// after Verify Environment so a missing .env variable still fails fast
74+
// and cheaply, before spending a scan.
75+
//
76+
// The agent has Docker but no sonar-scanner CLI and no SonarQube Scanner
77+
// tool installation, so the official scanner image is used.
78+
// sonar.working.directory is redirected into the bind-mounted workspace:
79+
// the image's baked-in /tmp/.scannerwork is owned by uid 1000 and
80+
// unwritable under -u, and the override also lands report-task.txt where
81+
// waitForQualityGate looks for it.
82+
withSonarQubeEnv('MySonarQube') {
83+
sh '''
84+
docker run --rm \
85+
-u "$(id -u):$(id -g)" \
86+
-e SONAR_HOST_URL="$SONAR_HOST_URL" \
87+
-e SONAR_TOKEN="$SONAR_AUTH_TOKEN" \
88+
-e SONAR_USER_HOME=/tmp/.sonar \
89+
-v "$WORKSPACE:/usr/src" \
90+
-w /usr/src \
91+
sonarsource/sonar-scanner-cli:latest \
92+
-Dsonar.working.directory=/usr/src/.scannerwork \
93+
-Dsonar.projectVersion="${GIT_COMMIT:-$BUILD_NUMBER}"
94+
'''
95+
}
96+
}
97+
}
98+
99+
stage('Quality Gate') {
100+
steps {
101+
// CLUSTOX: bounded wait. waitForQualityGate depends on SonarQube calling
102+
// back to /sonarqube-webhook/; without the timeout a missed webhook would
103+
// hang this stage until the build is killed, holding the deploy hostage.
104+
timeout(time: 5, unit: 'MINUTES') {
105+
waitForQualityGate abortPipeline: true
106+
}
107+
}
108+
}
109+
110+
stage('Fetch Sonar Issues') {
111+
steps {
112+
// CLUSTOX: reporting only -- catchError keeps a SonarQube outage or an
113+
// expired token from blocking a deploy that already passed the gate. Uses
114+
// the read-only token, not the scan credential, and sends it as an
115+
// Authorization header so it cannot land in proxy or access logs.
116+
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
117+
withCredentials([string(credentialsId: 'sonarqube-readonly-token', variable: 'SONAR_RO_TOKEN')]) {
118+
sh '''
119+
set -eu
120+
PS=500
121+
TMP=$(mktemp -d)
122+
trap 'rm -rf "$TMP"' EXIT
123+
page=1
124+
while : ; do
125+
curl --fail --silent --show-error \
126+
-H "Authorization: Bearer $SONAR_RO_TOKEN" \
127+
-o "$TMP/page-$page.json" \
128+
"https://sonar.theclustox.com/api/issues/search?componentKeys=middleware&resolved=false&ps=$PS&p=$page"
129+
total=$(jq -r '.paging.total' "$TMP/page-$page.json")
130+
fetched=$(( page * PS ))
131+
echo "fetched page $page (up to $fetched of $total open issues)"
132+
[ "$fetched" -ge "$total" ] && break
133+
[ "$fetched" -ge 10000 ] && { echo "WARNING: capped at 10000 issues"; break; }
134+
page=$(( page + 1 ))
135+
done
136+
# The warnings-ng SonarQube parser sniffs the response format from
137+
# the top-level keys, so the merged document has to look like one
138+
# big api/issues/search page -- dropping total/p/ps makes it
139+
# silently parse to zero issues.
140+
jq -s '
141+
(map(.issues) | add) as $iss |
142+
(.[0].paging.total) as $tot |
143+
{
144+
total: $tot,
145+
p: 1,
146+
ps: ($iss | length),
147+
paging: {pageIndex: 1, pageSize: ($iss | length), total: $tot},
148+
effortTotal: (map(.effortTotal // 0) | add),
149+
issues: $iss,
150+
components: (map(.components // []) | add | unique_by(.key))
151+
}' "$TMP"/page-*.json > sonar-issues.json
152+
echo "merged $(jq '.issues | length' sonar-issues.json) issues into sonar-issues.json"
153+
'''
154+
}
155+
}
156+
}
157+
}
158+
159+
stage('Publish Issue Report') {
160+
steps {
161+
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
162+
recordIssues(tools: [sonarQube(pattern: 'sonar-issues.json')])
163+
}
164+
}
165+
}
166+
69167
stage('Build') {
70168
steps {
71169
// CLUSTOX: builds docker-compose.prod.yml, which uses the

sonar-project.properties

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# SonarQube configuration for middleware.
2+
# Consumed by the sonar-scanner CLI in the "SonarQube Analysis" stage of the Jenkinsfile.
3+
# sonar.projectVersion is supplied by the pipeline (-Dsonar.projectVersion=$GIT_COMMIT).
4+
5+
sonar.projectKey=middleware
6+
sonar.projectName=middleware
7+
sonar.sourceEncoding=UTF-8
8+
9+
# Polyglot monorepo: Python analytics backend, Next.js web server, TypeScript CLI.
10+
# Sources are listed explicitly rather than using "." so that media_files/,
11+
# database-docker/ and docs/ are not indexed as application code.
12+
sonar.sources=backend/analytics_server,web-server/src,web-server/pages,cli/source
13+
14+
# Two of these test roots live inside a source root, so they are removed from main
15+
# in sonar.exclusions below and declared here instead -- otherwise the scan fails
16+
# with "file can't be indexed twice". web-server/e2e sits outside sonar.sources, so
17+
# it needs no exclusion.
18+
sonar.tests=backend/analytics_server/tests,cli/source/__tests__,web-server/e2e
19+
20+
sonar.python.version=3.10,3.11,3.12
21+
22+
sonar.exclusions=\
23+
backend/analytics_server/tests/**,\
24+
cli/source/__tests__/**,\
25+
**/*.test.ts,\
26+
**/*.test.tsx,\
27+
**/*.spec.ts,\
28+
**/*.spec.tsx,\
29+
**/node_modules/**,\
30+
**/.next/**,\
31+
**/dist/**,\
32+
**/build/**,\
33+
**/__pycache__/**,\
34+
**/*.pyc,\
35+
**/*.min.js,\
36+
web-server/libdefs/**,\
37+
**/next-env.d.ts,\
38+
.scannerwork/**
39+
40+
# No coverage report is produced by this pipeline, so coverage will read as 0%.
41+
sonar.coverage.exclusions=\
42+
**/tests/**,\
43+
**/__tests__/**,\
44+
web-server/e2e/**,\
45+
backend/dev_scripts/**

0 commit comments

Comments
 (0)