88import os
99import requests
1010import sys
11+ import re
1112from datetime import datetime
1213
1314def 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
0 commit comments