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,131 @@ 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 = None , pr_key = None ):
66+ """Fetch detailed quality gate information from SonarQube API"""
67+ try :
68+ headers = {"Authorization" : f"Bearer { sonar_token } " }
69+
70+ # Build the API URL for quality gate status
71+ if pr_key :
72+ # For pull requests
73+ api_url = f"{ sonar_url } /api/qualitygates/project_status?projectKey={ project_key } &pullRequest={ pr_key } "
74+ elif branch_name :
75+ # For branches
76+ import urllib .parse
77+ encoded_branch = urllib .parse .quote (branch_name , safe = '' )
78+ api_url = f"{ sonar_url } /api/qualitygates/project_status?projectKey={ project_key } &branch={ encoded_branch } "
79+ else :
80+ # Main branch
81+ api_url = f"{ sonar_url } /api/qualitygates/project_status?projectKey={ project_key } "
82+
83+ print (f"🔍 Fetching quality gate from: { api_url } " )
84+ response = requests .get (api_url , headers = headers , timeout = 30 )
85+
86+ if response .status_code == 200 :
87+ data = response .json ()
88+ return data .get ('projectStatus' , {})
89+ else :
90+ print (f"⚠️ Failed to fetch quality gate details: { response .status_code } " )
91+ return None
92+
93+ except Exception as e :
94+ print (f"⚠️ Error fetching quality gate details: { str (e )} " )
95+ return None
96+
97+ def fetch_sonarqube_measures (sonar_token , sonar_url , project_key , branch_name = None , pr_key = None ):
98+ """Fetch detailed measures from SonarQube API"""
99+ try :
100+ headers = {"Authorization" : f"Bearer { sonar_token } " }
101+
102+ # Metrics we want to fetch
103+ metrics = [
104+ "new_bugs" , "new_vulnerabilities" , "new_violations" , "new_security_hotspots" ,
105+ "new_code_smells" , "new_coverage" , "new_duplicated_lines_density" ,
106+ "new_maintainability_rating" , "new_security_rating" , "new_lines" ,
107+ "bugs" , "vulnerabilities" , "violations" , "security_hotspots" , "code_smells" ,
108+ "coverage" , "duplicated_lines_density" , "ncloc" , "reliability_rating"
109+ ]
110+
111+ metric_keys = "," .join (metrics )
112+
113+ if pr_key :
114+ api_url = f"{ sonar_url } /api/measures/component?component={ project_key } &pullRequest={ pr_key } &metricKeys={ metric_keys } "
115+ elif branch_name :
116+ import urllib .parse
117+ encoded_branch = urllib .parse .quote (branch_name , safe = '' )
118+ api_url = f"{ sonar_url } /api/measures/component?component={ project_key } &branch={ encoded_branch } &metricKeys={ metric_keys } "
119+ else :
120+ api_url = f"{ sonar_url } /api/measures/component?component={ project_key } &metricKeys={ metric_keys } "
121+
122+ print (f"🔍 Fetching measures from: { api_url } " )
123+ response = requests .get (api_url , headers = headers , timeout = 30 )
124+
125+ if response .status_code == 200 :
126+ data = response .json ()
127+ measures = {}
128+ for measure in data .get ('component' , {}).get ('measures' , []):
129+ metric_name = measure ['metric' ]
130+ # For "new_" metrics, the value is often in the period object
131+ if 'period' in measure and measure ['period' ]:
132+ value = measure ['period' ].get ('value' , '0' )
133+ else :
134+ value = measure .get ('value' , '0' )
135+ measures [metric_name ] = value
136+ return measures
137+ else :
138+ print (f"⚠️ Failed to fetch measures: { response .status_code } " )
139+ return {}
140+
141+ except Exception as e :
142+ print (f"⚠️ Error fetching measures: { str (e )} " )
143+ return {}
144+
145+ def convert_rating_to_letter (rating_value ):
146+ """Convert SonarQube numeric rating (1-5) to letter grade (A-E)"""
147+ if rating_value == 'N/A' or rating_value is None or rating_value == '' :
148+ return 'N/A'
149+
150+ try :
151+ rating_num = int (float (rating_value ))
152+ rating_map = {1 : 'A' , 2 : 'B' , 3 : 'C' , 4 : 'D' , 5 : 'E' }
153+ return rating_map .get (rating_num , f'Unknown({ rating_value } )' )
154+ except (ValueError , TypeError ):
155+ return f'Invalid({ rating_value } )'
156+
157+ def create_comment_body (status , commit_sha , branch_name , target_branch , sonar_output , quality_gate_details = None , measures = None ):
65158 """Create formatted comment body for GitHub PR"""
66159
67160 # Determine status emoji and text
68- if result == "PASS " :
161+ if status == "PASSED " :
69162 result_emoji = "✅"
70163 result_text = "**PASSED**"
71164 else :
72165 result_emoji = "❌"
73166 result_text = "**FAILED**"
74167
168+ # Use measures if available, otherwise show N/A
169+ default_metrics = {
170+ 'new_lines' : 'N/A' ,
171+ 'new_duplicated_lines_density' : 'N/A' ,
172+ 'new_violations' : 'N/A' ,
173+ 'new_code_smells' : 'N/A' ,
174+ 'new_bugs' : 'N/A' ,
175+ 'new_vulnerabilities' : 'N/A' ,
176+ 'new_security_hotspots' : 'N/A' ,
177+ 'new_maintainability_rating' : 'N/A' ,
178+ 'reliability_rating' : 'N/A' ,
179+ 'new_security_rating' : 'N/A'
180+ }
181+
182+ # Merge measures with defaults
183+ metrics = default_metrics .copy ()
184+ if measures :
185+ metrics .update (measures )
186+
187+ # Get quality gate status
188+ quality_gate_status = quality_gate_details .get ('status' , status ) if quality_gate_details else status
189+
75190 # Truncate output if too long - show last 2000 characters for most relevant info
76191 max_output_length = 2000
77192 if len (sonar_output ) > max_output_length :
@@ -90,7 +205,25 @@ def create_comment_body(result, status, commit_sha, branch_name, target_branch,
90205- **Target:** `{ target_branch } `
91206- **Analysis Time:** { datetime .now ().strftime ('%Y-%m-%d %H:%M:%S UTC' )}
92207
93- ### 📋 Detailed Results
208+ ### � Key Metrics
209+ | Metric | Value |
210+ |--------|-------|
211+ | **New Lines of Code** | { metrics .get ('new_lines' , 'N/A' )} |
212+ | **New Duplicated Lines Density** | { metrics .get ('new_duplicated_lines_density' , metrics .get ('duplicated_lines_density' , 'N/A' ))} % |
213+ | **New Violations** | { metrics .get ('new_violations' , 'N/A' )} |
214+ | **New Code Smells** | { metrics .get ('new_code_smells' , 'N/A' )} |
215+ | **New Bugs** | { metrics .get ('new_bugs' , 'N/A' )} |
216+ | **New Vulnerabilities** | { metrics .get ('new_vulnerabilities' , 'N/A' )} |
217+ | **New Security Hotspots** | { metrics .get ('new_security_hotspots' , 'N/A' )} |
218+
219+ ### 🏆 Quality Ratings
220+ | Category | Rating |
221+ |----------|--------|
222+ | **New Maintainability Rating** | { convert_rating_to_letter (metrics .get ('new_maintainability_rating' , 'N/A' ))} |
223+ | **Reliability Rating** | { convert_rating_to_letter (metrics .get ('reliability_rating' , 'N/A' ))} |
224+ | **New Security Rating** | { convert_rating_to_letter (metrics .get ('new_security_rating' , 'N/A' ))} |
225+
226+ ### �📋 Detailed Results
94227<details>
95228<summary>Click to view SonarQube output</summary>
96229
@@ -117,30 +250,67 @@ def main():
117250
118251 parser .add_argument ("--branch_name" , required = True , help = "Source branch name" )
119252 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" )
253+ parser .add_argument ("--sonar_output" , help = "SonarQube scanner output (deprecated - use --sonar_output_file)" )
254+ parser .add_argument ("--sonar_output_file" , help = "Path to file containing SonarQube scanner output" )
255+ parser .add_argument ("--sonar_token" , help = "SonarQube token for API access" )
256+ parser .add_argument ("--sonar_url" , default = "https://sonarqube.silabs.net" , help = "SonarQube server URL" )
257+ parser .add_argument ("--project_key" , default = "github_matter_sdk" , help = "SonarQube project key" )
121258
122259 args = parser .parse_args ()
123260
124261 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 )
262+ # Read sonar output from file or use direct argument
263+ sonar_output = ""
264+ if args .sonar_output_file :
265+ try :
266+ with open (args .sonar_output_file , 'r' ) as f :
267+ sonar_output = f .read ()
268+ print (f"✅ Read SonarQube output from file: { args .sonar_output_file } " )
269+ except Exception as e :
270+ print (f"❌ Error reading sonar output file { args .sonar_output_file } : { str (e )} " )
271+ if args .sonar_output :
272+ sonar_output = args .sonar_output
273+ print ("⚠️ Falling back to direct sonar_output argument" )
274+ else :
275+ raise
276+ elif args .sonar_output :
277+ sonar_output = args .sonar_output
278+ print ("✅ Using direct sonar_output argument" )
279+ else :
280+ raise ValueError ("Either --sonar_output_file or --sonar_output must be provided" )
281+
282+ # Fetch detailed SonarQube information if token is provided
283+ quality_gate_details = None
284+ measures = None
285+
286+ if args .sonar_token :
287+ print ("🔍 Fetching detailed SonarQube quality gate information..." )
288+ quality_gate_details = fetch_sonarqube_quality_gate (
289+ args .sonar_token ,
290+ args .sonar_url ,
291+ args .project_key ,
292+ args .branch_name ,
293+ pr_key = args .pr_number
294+ )
295+
296+ print ("📊 Fetching SonarQube measures..." )
297+ measures = fetch_sonarqube_measures (
298+ args .sonar_token ,
299+ args .sonar_url ,
300+ args .project_key ,
301+ args .branch_name ,
302+ pr_key = args .pr_number
303+ )
135304
136305 # Create comment body
137306 comment_body = create_comment_body (
138- args .result ,
139307 args .status ,
140308 args .commit_sha ,
141309 args .branch_name ,
142310 args .target_branch ,
143- sonar_output
311+ sonar_output ,
312+ quality_gate_details ,
313+ measures
144314 )
145315
146316 # Post PR comment
0 commit comments