Skip to content

Commit 7eafe41

Browse files
Add more details
1 parent 7e01dc0 commit 7eafe41

File tree

3 files changed

+234
-37
lines changed

3 files changed

+234
-37
lines changed

jenkins_integration/Jenkinsfile

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ pipeline
2828
def sonarCommitSha = ""
2929

3030
withDockerContainer(image: 'sonarsource/sonar-scanner-cli:latest') {
31-
def sonarResult = pipelineFunctions.publishSonarAnalysis()
31+
def sonarResult = pipelineFunctions.publishSonarAnalysis(false)
3232

33-
// Extract results for GitHub reporting
33+
// Extract results for GitHub posting outside Docker
3434
staticAnalysisResult = sonarResult.result
3535
staticAnalysisStatus = sonarResult.status
3636
staticAnalysisOutput = sonarResult.output
@@ -40,7 +40,8 @@ pipeline
4040
echo "Static Analysis Status: ${staticAnalysisStatus}"
4141
echo "Commit SHA from SonarQube: ${sonarCommitSha}"
4242
}
43-
// Send results to GitHub PR only
43+
44+
// Post to GitHub outside of Docker container
4445
try {
4546
if (env.CHANGE_ID) {
4647
pipelineFunctions.send_sonar_results_to_github(
@@ -50,11 +51,12 @@ pipeline
5051
staticAnalysisOutput,
5152
env.CHANGE_ID,
5253
env.CHANGE_BRANCH,
53-
env.CHANGE_TARGET
54+
env.CHANGE_TARGET,
55+
null // sonar_token will be fetched from credentials
5456
)
5557
echo "✅ Posted SonarQube results to GitHub PR #${env.CHANGE_ID}"
5658
} else {
57-
echo "ℹ️ Skipping GitHub PR comment (not a PR or bypass enabled)"
59+
echo "ℹ️ Skipping GitHub PR comment (not a PR)"
5860
}
5961
} catch (Exception githubEx) {
6062
echo "❌ Failed to post to GitHub PR: ${githubEx.getMessage()}"

jenkins_integration/github/send_sonar_results_to_github.py

Lines changed: 193 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import os
99
import requests
1010
import sys
11+
import re
1112
from datetime import datetime
1213

1314
def post_pr_comment(github_token, repo_owner, repo_name, pr_number, comment_body):
@@ -61,17 +62,137 @@ def post_commit_status(github_token, repo_owner, repo_name, commit_sha, state, d
6162
print(f"Response: {response.text}")
6263
return False
6364

64-
def create_comment_body(result, status, commit_sha, branch_name, target_branch, sonar_output):
65+
def fetch_sonarqube_quality_gate(sonar_token, sonar_url, project_key, branch_name, pr_number=None):
66+
"""Fetch SonarQube quality gate information"""
67+
try:
68+
headers = {
69+
'Authorization': f'Bearer {sonar_token}',
70+
'Accept': 'application/json'
71+
}
72+
73+
component_key = project_key
74+
if pr_number:
75+
component_key = f"{project_key}/pull/{pr_number}"
76+
77+
qg_url = f"{sonar_url}/api/qualitygates/project_status"
78+
qg_params = {'projectKey': component_key}
79+
80+
qg_response = requests.get(qg_url, headers=headers, params=qg_params, timeout=30)
81+
82+
if qg_response.status_code == 200:
83+
qg_data = qg_response.json()
84+
if 'projectStatus' in qg_data:
85+
return qg_data['projectStatus']
86+
else:
87+
print(f"⚠️ Could not fetch quality gate status: {qg_response.status_code}")
88+
except Exception as e:
89+
print(f"⚠️ Error fetching quality gate: {str(e)}")
90+
91+
return None
92+
93+
def fetch_sonarqube_measures(sonar_token, sonar_url, project_key, branch_name, pr_number=None):
94+
"""Fetch SonarQube measures from SonarQube server API"""
95+
metrics = {
96+
'duplicated_lines': 'N/A',
97+
'new_violations': 'N/A',
98+
'new_code_smells': 'N/A',
99+
'new_maintainability_rating': 'N/A',
100+
'new_bugs': 'N/A',
101+
'reliability_rating': 'N/A',
102+
'new_vulnerabilities': 'N/A',
103+
'new_security_rating': 'N/A',
104+
'new_security_hotspots': 'N/A',
105+
'ncloc': 'N/A'
106+
}
107+
108+
try:
109+
# Prepare headers for SonarQube API
110+
headers = {
111+
'Authorization': f'Bearer {sonar_token}',
112+
'Accept': 'application/json'
113+
}
114+
115+
# Determine component key - use pullRequest key if it's a PR analysis
116+
component_key = project_key
117+
if pr_number:
118+
component_key = f"{project_key}/pull/{pr_number}"
119+
120+
# Define metrics to fetch from SonarQube API
121+
metric_keys = [
122+
'duplicated_lines',
123+
'new_violations',
124+
'new_code_smells',
125+
'new_maintainability_rating',
126+
'new_bugs',
127+
'reliability_rating',
128+
'new_vulnerabilities',
129+
'new_security_rating',
130+
'new_security_hotspots',
131+
'ncloc'
132+
]
133+
134+
# Fetch metrics from SonarQube API
135+
metrics_url = f"{sonar_url}/api/measures/component"
136+
params = {
137+
'component': component_key,
138+
'metricKeys': ','.join(metric_keys)
139+
}
140+
141+
print(f"🔍 Fetching metrics from SonarQube: {metrics_url}")
142+
print(f"📊 Component: {component_key}")
143+
144+
response = requests.get(metrics_url, headers=headers, params=params, timeout=30)
145+
146+
if response.status_code == 200:
147+
data = response.json()
148+
149+
if 'component' in data and 'measures' in data['component']:
150+
for measure in data['component']['measures']:
151+
metric_key = measure.get('metric')
152+
value = measure.get('value', 'N/A')
153+
154+
if metric_key in metrics:
155+
metrics[metric_key] = value
156+
print(f"📈 {metric_key}: {value}")
157+
else:
158+
print("⚠️ No measures found in SonarQube response")
159+
else:
160+
print(f"❌ Failed to fetch metrics from SonarQube: {response.status_code}")
161+
print(f"Response: {response.text}")
162+
163+
except Exception as e:
164+
print(f"❌ Error fetching SonarQube metrics: {str(e)}")
165+
166+
return metrics
167+
168+
def create_comment_body(status, commit_sha, branch_name, target_branch, sonar_output, quality_gate_details=None, measures=None):
65169
"""Create formatted comment body for GitHub PR"""
66170

67171
# Determine status emoji and text
68-
if result == "PASS":
172+
if status == "PASSED":
69173
result_emoji = "✅"
70174
result_text = "**PASSED**"
71175
else:
72176
result_emoji = "❌"
73177
result_text = "**FAILED**"
74178

179+
# Use measures if available, otherwise show N/A
180+
metrics = measures if measures else {
181+
'ncloc': 'N/A',
182+
'duplicated_lines': 'N/A',
183+
'new_violations': 'N/A',
184+
'new_code_smells': 'N/A',
185+
'new_bugs': 'N/A',
186+
'new_vulnerabilities': 'N/A',
187+
'new_security_hotspots': 'N/A',
188+
'new_maintainability_rating': 'N/A',
189+
'reliability_rating': 'N/A',
190+
'new_security_rating': 'N/A'
191+
}
192+
193+
# Get quality gate status
194+
quality_gate_status = quality_gate_details.get('status', status) if quality_gate_details else status
195+
75196
# Truncate output if too long - show last 2000 characters for most relevant info
76197
max_output_length = 2000
77198
if len(sonar_output) > max_output_length:
@@ -90,7 +211,26 @@ def create_comment_body(result, status, commit_sha, branch_name, target_branch,
90211
- **Target:** `{target_branch}`
91212
- **Analysis Time:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}
92213
93-
### 📋 Detailed Results
214+
### � Key Metrics
215+
| Metric | Value |
216+
|--------|-------|
217+
| **Lines of Code (NCLOC)** | {metrics['ncloc']} |
218+
| **Duplicated Lines** | {metrics['duplicated_lines']} |
219+
| **New Violations** | {metrics['new_violations']} |
220+
| **New Code Smells** | {metrics['new_code_smells']} |
221+
| **New Bugs** | {metrics['new_bugs']} |
222+
| **New Vulnerabilities** | {metrics['new_vulnerabilities']} |
223+
| **New Security Hotspots** | {metrics['new_security_hotspots']} |
224+
225+
### 🏆 Quality Ratings
226+
| Category | Rating |
227+
|----------|--------|
228+
| **New Maintainability Rating** | {metrics['new_maintainability_rating']} |
229+
| **Reliability Rating** | {metrics['reliability_rating']} |
230+
| **New Security Rating** | {metrics['new_security_rating']} |
231+
| **Quality Gate Details** | {metrics['quality_gate_details']} |
232+
233+
### �📋 Detailed Results
94234
<details>
95235
<summary>Click to view SonarQube output</summary>
96236
@@ -117,30 +257,67 @@ def main():
117257

118258
parser.add_argument("--branch_name", required=True, help="Source branch name")
119259
parser.add_argument("--target_branch", required=True, help="Target branch name")
120-
parser.add_argument("--sonar_output_file", required=True, help="Path to file containing SonarQube scanner output")
260+
parser.add_argument("--sonar_output", help="SonarQube scanner output (deprecated - use --sonar_output_file)")
261+
parser.add_argument("--sonar_output_file", help="Path to file containing SonarQube scanner output")
262+
parser.add_argument("--sonar_token", help="SonarQube token for API access")
263+
parser.add_argument("--sonar_url", default="https://sonarqube.silabs.net", help="SonarQube server URL")
264+
parser.add_argument("--project_key", default="github_matter_sdk", help="SonarQube project key")
121265

122266
args = parser.parse_args()
123267

124268
try:
125-
# Read SonarQube output from file
126-
try:
127-
with open(args.sonar_output_file, 'r', encoding='utf-8') as f:
128-
sonar_output = f.read()
129-
except FileNotFoundError:
130-
print(f"❌ Error: SonarQube output file not found: {args.sonar_output_file}")
131-
sys.exit(1)
132-
except Exception as e:
133-
print(f"❌ Error reading SonarQube output file: {str(e)}")
134-
sys.exit(1)
269+
# Read sonar output from file or use direct argument
270+
sonar_output = ""
271+
if args.sonar_output_file:
272+
try:
273+
with open(args.sonar_output_file, 'r') as f:
274+
sonar_output = f.read()
275+
print(f"✅ Read SonarQube output from file: {args.sonar_output_file}")
276+
except Exception as e:
277+
print(f"❌ Error reading sonar output file {args.sonar_output_file}: {str(e)}")
278+
if args.sonar_output:
279+
sonar_output = args.sonar_output
280+
print("⚠️ Falling back to direct sonar_output argument")
281+
else:
282+
raise
283+
elif args.sonar_output:
284+
sonar_output = args.sonar_output
285+
print("✅ Using direct sonar_output argument")
286+
else:
287+
raise ValueError("Either --sonar_output_file or --sonar_output must be provided")
288+
289+
# Fetch detailed SonarQube information if token is provided
290+
quality_gate_details = None
291+
measures = None
292+
293+
if args.sonar_token:
294+
print("🔍 Fetching detailed SonarQube quality gate information...")
295+
quality_gate_details = fetch_sonarqube_quality_gate(
296+
args.sonar_token,
297+
args.sonar_url,
298+
args.project_key,
299+
args.branch_name,
300+
args.pr_number
301+
)
302+
303+
print("📊 Fetching SonarQube measures...")
304+
measures = fetch_sonarqube_measures(
305+
args.sonar_token,
306+
args.sonar_url,
307+
args.project_key,
308+
args.branch_name,
309+
args.pr_number
310+
)
135311

136312
# Create comment body
137313
comment_body = create_comment_body(
138-
args.result,
139314
args.status,
140315
args.commit_sha,
141316
args.branch_name,
142317
args.target_branch,
143-
sonar_output
318+
sonar_output,
319+
quality_gate_details,
320+
measures
144321
)
145322

146323
# Post PR comment

jenkins_integration/jenkinsFunctions.groovy

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,46 @@
11
/**
22
* Send SonarQube results to GitHub PR using Python script
33
*/
4-
def send_sonar_results_to_github(commit_sha, result, status, sonar_output, pr_number, branch_name, target_branch) {
4+
def send_sonar_results_to_github(commit_sha, result, status, sonar_output, pr_number, branch_name, target_branch, sonar_token = null) {
55
withCredentials([
6-
usernamePassword(credentialsId: 'Matter-Extension-GitHub', usernameVariable: 'GITHUB_APP', passwordVariable: 'GITHUB_ACCESS_TOKEN')
6+
usernamePassword(credentialsId: 'Matter-Extension-GitHub', usernameVariable: 'GITHUB_APP', passwordVariable: 'GITHUB_ACCESS_TOKEN'),
7+
string(credentialsId: 'sonarqube_token', variable: 'SONAR_SECRET')
78
]) {
9+
// Use passed token or get from credentials
10+
def actualSonarToken = sonar_token ?: SONAR_SECRET
811
// Write sonar output to a temporary file to avoid "Argument list too long" error
912
def tempFile = "${env.WORKSPACE}/sonar_output_${BUILD_NUMBER}.txt"
1013
writeFile file: tempFile, text: sonar_output
1114

1215
try {
13-
sh """
14-
python3 -u jenkins_integration/github/send_sonar_results_to_github.py \\
15-
--github_token \${GITHUB_ACCESS_TOKEN} \\
16-
--repo_owner "SiliconLabsSoftware" \\
17-
--repo_name "matter_sdk" \\
18-
--pr_number ${pr_number} \\
19-
--commit_sha ${commit_sha} \\
20-
--result ${result} \\
21-
--status ${status} \\
22-
--branch_name "${branch_name}" \\
23-
--target_branch "${target_branch}" \\
24-
--sonar_output_file "${tempFile}"
25-
"""
16+
// Get SonarQube server URL with fallbacks
17+
def sonarHost = env.SONAR_HOST_URL ?: env.SONAR_SERVER_URL ?: "https://sonarqube.silabs.net"
18+
19+
echo "Using SonarQube host: ${sonarHost}"
20+
echo "Available SonarQube environment variables:"
21+
echo "SONAR_HOST_URL: ${env.SONAR_HOST_URL}"
22+
echo "SONAR_SERVER_URL: ${env.SONAR_SERVER_URL}"
23+
echo "Sonar token available: ${actualSonarToken != null && !actualSonarToken.isEmpty()}"
24+
25+
// Use environment variable to avoid Groovy string interpolation security warning
26+
withEnv(["SONAR_TOKEN=${actualSonarToken}", "SONAR_OUTPUT_FILE=${tempFile}"]) {
27+
sh """
28+
python3 -u jenkins_integration/github/send_sonar_results_to_github.py \\
29+
--github_token "\${GITHUB_ACCESS_TOKEN}" \\
30+
--repo_owner "SiliconLabsSoftware" \\
31+
--repo_name "matter_sdk" \\
32+
--pr_number ${pr_number} \\
33+
--commit_sha ${commit_sha} \\
34+
--result ${result} \\
35+
--status ${status} \\
36+
--branch_name "${branch_name}" \\
37+
--target_branch "${target_branch}" \\
38+
--sonar_output_file "\${SONAR_OUTPUT_FILE}" \\
39+
--sonar_url "${sonarHost}" \\
40+
--sonar_token "\${SONAR_TOKEN}" \\
41+
--project_key "github_matter_sdk"
42+
"""
43+
}
2644
} finally {
2745
// Clean up temporary file
2846
sh "rm -f '${tempFile}'"
@@ -34,7 +52,7 @@ def send_sonar_results_to_github(commit_sha, result, status, sonar_output, pr_nu
3452
/**
3553
* Publishes static analysis results to SonarQube.
3654
*/
37-
def publishSonarAnalysis() {
55+
def publishSonarAnalysis(postToGitHub = false) {
3856

3957
// Use the SonarQube environment defined in Jenkins
4058
withSonarQubeEnv('Silabs SonarQube') {

0 commit comments

Comments
 (0)