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,119 @@ 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 create_comment_body (status , commit_sha , branch_name , target_branch , sonar_output , quality_gate_details = None , measures = None ):
65146 """Create formatted comment body for GitHub PR"""
66147
67148 # Determine status emoji and text
68- if result == "PASS " :
149+ if status == "PASSED " :
69150 result_emoji = "✅"
70151 result_text = "**PASSED**"
71152 else :
72153 result_emoji = "❌"
73154 result_text = "**FAILED**"
74155
156+ # Use measures if available, otherwise show N/A
157+ default_metrics = {
158+ 'new_lines' : 'N/A' ,
159+ 'new_duplicated_lines_density' : 'N/A' ,
160+ 'new_violations' : 'N/A' ,
161+ 'new_code_smells' : 'N/A' ,
162+ 'new_bugs' : 'N/A' ,
163+ 'new_vulnerabilities' : 'N/A' ,
164+ 'new_security_hotspots' : 'N/A' ,
165+ 'new_maintainability_rating' : 'N/A' ,
166+ 'reliability_rating' : 'N/A' ,
167+ 'new_security_rating' : 'N/A'
168+ }
169+
170+ # Merge measures with defaults
171+ metrics = default_metrics .copy ()
172+ if measures :
173+ metrics .update (measures )
174+
175+ # Get quality gate status
176+ quality_gate_status = quality_gate_details .get ('status' , status ) if quality_gate_details else status
177+
75178 # Truncate output if too long - show last 2000 characters for most relevant info
76179 max_output_length = 2000
77180 if len (sonar_output ) > max_output_length :
@@ -90,7 +193,25 @@ def create_comment_body(result, status, commit_sha, branch_name, target_branch,
90193- **Target:** `{ target_branch } `
91194- **Analysis Time:** { datetime .now ().strftime ('%Y-%m-%d %H:%M:%S UTC' )}
92195
93- ### 📋 Detailed Results
196+ ### � Key Metrics
197+ | Metric | Value |
198+ |--------|-------|
199+ | **New Lines of Code** | { metrics .get ('new_lines' , 'N/A' )} |
200+ | **New Duplicated Lines Density** | { metrics .get ('new_duplicated_lines_density' , metrics .get ('duplicated_lines_density' , 'N/A' ))} % |
201+ | **New Violations** | { metrics .get ('new_violations' , 'N/A' )} |
202+ | **New Code Smells** | { metrics .get ('new_code_smells' , 'N/A' )} |
203+ | **New Bugs** | { metrics .get ('new_bugs' , 'N/A' )} |
204+ | **New Vulnerabilities** | { metrics .get ('new_vulnerabilities' , 'N/A' )} |
205+ | **New Security Hotspots** | { metrics .get ('new_security_hotspots' , 'N/A' )} |
206+
207+ ### 🏆 Quality Ratings
208+ | Category | Rating |
209+ |----------|--------|
210+ | **New Maintainability Rating** | { metrics .get ('new_maintainability_rating' , 'N/A' )} |
211+ | **Reliability Rating** | { metrics .get ('reliability_rating' , 'N/A' )} |
212+ | **New Security Rating** | { metrics .get ('new_security_rating' , 'N/A' )} |
213+
214+ ### �📋 Detailed Results
94215<details>
95216<summary>Click to view SonarQube output</summary>
96217
@@ -117,30 +238,67 @@ def main():
117238
118239 parser .add_argument ("--branch_name" , required = True , help = "Source branch name" )
119240 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" )
241+ parser .add_argument ("--sonar_output" , help = "SonarQube scanner output (deprecated - use --sonar_output_file)" )
242+ parser .add_argument ("--sonar_output_file" , help = "Path to file containing SonarQube scanner output" )
243+ parser .add_argument ("--sonar_token" , help = "SonarQube token for API access" )
244+ parser .add_argument ("--sonar_url" , default = "https://sonarqube.silabs.net" , help = "SonarQube server URL" )
245+ parser .add_argument ("--project_key" , default = "github_matter_sdk" , help = "SonarQube project key" )
121246
122247 args = parser .parse_args ()
123248
124249 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 )
250+ # Read sonar output from file or use direct argument
251+ sonar_output = ""
252+ if args .sonar_output_file :
253+ try :
254+ with open (args .sonar_output_file , 'r' ) as f :
255+ sonar_output = f .read ()
256+ print (f"✅ Read SonarQube output from file: { args .sonar_output_file } " )
257+ except Exception as e :
258+ print (f"❌ Error reading sonar output file { args .sonar_output_file } : { str (e )} " )
259+ if args .sonar_output :
260+ sonar_output = args .sonar_output
261+ print ("⚠️ Falling back to direct sonar_output argument" )
262+ else :
263+ raise
264+ elif args .sonar_output :
265+ sonar_output = args .sonar_output
266+ print ("✅ Using direct sonar_output argument" )
267+ else :
268+ raise ValueError ("Either --sonar_output_file or --sonar_output must be provided" )
269+
270+ # Fetch detailed SonarQube information if token is provided
271+ quality_gate_details = None
272+ measures = None
273+
274+ if args .sonar_token :
275+ print ("🔍 Fetching detailed SonarQube quality gate information..." )
276+ quality_gate_details = fetch_sonarqube_quality_gate (
277+ args .sonar_token ,
278+ args .sonar_url ,
279+ args .project_key ,
280+ args .branch_name ,
281+ pr_key = args .pr_number
282+ )
283+
284+ print ("📊 Fetching SonarQube measures..." )
285+ measures = fetch_sonarqube_measures (
286+ args .sonar_token ,
287+ args .sonar_url ,
288+ args .project_key ,
289+ args .branch_name ,
290+ pr_key = args .pr_number
291+ )
135292
136293 # Create comment body
137294 comment_body = create_comment_body (
138- args .result ,
139295 args .status ,
140296 args .commit_sha ,
141297 args .branch_name ,
142298 args .target_branch ,
143- sonar_output
299+ sonar_output ,
300+ quality_gate_details ,
301+ measures
144302 )
145303
146304 # Post PR comment
0 commit comments