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,112 @@ 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+ "bugs" , "vulnerabilities" , "violations" , "security_hotspots" , "code_smells" ,
107+ "coverage" , "duplicated_lines_density" , "ncloc"
108+ ]
109+
110+ metric_keys = "," .join (metrics )
111+
112+ if pr_key :
113+ api_url = f"{ sonar_url } /api/measures/component?component={ project_key } &pullRequest={ pr_key } &metricKeys={ metric_keys } "
114+ elif branch_name :
115+ import urllib .parse
116+ encoded_branch = urllib .parse .quote (branch_name , safe = '' )
117+ api_url = f"{ sonar_url } /api/measures/component?component={ project_key } &branch={ encoded_branch } &metricKeys={ metric_keys } "
118+ else :
119+ api_url = f"{ sonar_url } /api/measures/component?component={ project_key } &metricKeys={ metric_keys } "
120+
121+ print (f"🔍 Fetching measures from: { api_url } " )
122+ response = requests .get (api_url , headers = headers , timeout = 30 )
123+
124+ if response .status_code == 200 :
125+ data = response .json ()
126+ measures = {}
127+ for measure in data .get ('component' , {}).get ('measures' , []):
128+ measures [measure ['metric' ]] = measure .get ('value' , '0' )
129+ return measures
130+ else :
131+ print (f"⚠️ Failed to fetch measures: { response .status_code } " )
132+ return {}
133+
134+ except Exception as e :
135+ print (f"⚠️ Error fetching measures: { str (e )} " )
136+ return {}
137+
138+ def create_comment_body (status , commit_sha , branch_name , target_branch , sonar_output , quality_gate_details = None , measures = None ):
65139 """Create formatted comment body for GitHub PR"""
66140
67141 # Determine status emoji and text
68- if result == "PASS " :
142+ if status == "PASSED " :
69143 result_emoji = "✅"
70144 result_text = "**PASSED**"
71145 else :
72146 result_emoji = "❌"
73147 result_text = "**FAILED**"
74148
149+ # Use measures if available, otherwise show N/A
150+ default_metrics = {
151+ 'ncloc' : 'N/A' ,
152+ 'duplicated_lines_density' : 'N/A' ,
153+ 'new_violations' : 'N/A' ,
154+ 'new_code_smells' : 'N/A' ,
155+ 'new_bugs' : 'N/A' ,
156+ 'new_vulnerabilities' : 'N/A' ,
157+ 'new_security_hotspots' : 'N/A' ,
158+ 'new_maintainability_rating' : 'N/A' ,
159+ 'reliability_rating' : 'N/A' ,
160+ 'new_security_rating' : 'N/A'
161+ }
162+
163+ # Merge measures with defaults
164+ metrics = default_metrics .copy ()
165+ if measures :
166+ metrics .update (measures )
167+
168+ # Get quality gate status
169+ quality_gate_status = quality_gate_details .get ('status' , status ) if quality_gate_details else status
170+
75171 # Truncate output if too long - show last 2000 characters for most relevant info
76172 max_output_length = 2000
77173 if len (sonar_output ) > max_output_length :
@@ -90,7 +186,25 @@ def create_comment_body(result, status, commit_sha, branch_name, target_branch,
90186- **Target:** `{ target_branch } `
91187- **Analysis Time:** { datetime .now ().strftime ('%Y-%m-%d %H:%M:%S UTC' )}
92188
93- ### 📋 Detailed Results
189+ ### � Key Metrics
190+ | Metric | Value |
191+ |--------|-------|
192+ | **Lines of Code (NCLOC)** | { metrics .get ('ncloc' , 'N/A' )} |
193+ | **Duplicated Lines Density** | { metrics .get ('new_duplicated_lines_density' , metrics .get ('duplicated_lines_density' , 'N/A' ))} % |
194+ | **New Violations** | { metrics .get ('new_violations' , 'N/A' )} |
195+ | **New Code Smells** | { metrics .get ('new_code_smells' , 'N/A' )} |
196+ | **New Bugs** | { metrics .get ('new_bugs' , 'N/A' )} |
197+ | **New Vulnerabilities** | { metrics .get ('new_vulnerabilities' , 'N/A' )} |
198+ | **New Security Hotspots** | { metrics .get ('new_security_hotspots' , 'N/A' )} |
199+
200+ ### 🏆 Quality Ratings
201+ | Category | Rating |
202+ |----------|--------|
203+ | **New Maintainability Rating** | { metrics .get ('new_maintainability_rating' , 'N/A' )} |
204+ | **Reliability Rating** | { metrics .get ('reliability_rating' , 'N/A' )} |
205+ | **New Security Rating** | { metrics .get ('new_security_rating' , 'N/A' )} |
206+
207+ ### �📋 Detailed Results
94208<details>
95209<summary>Click to view SonarQube output</summary>
96210
@@ -117,30 +231,67 @@ def main():
117231
118232 parser .add_argument ("--branch_name" , required = True , help = "Source branch name" )
119233 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" )
234+ parser .add_argument ("--sonar_output" , help = "SonarQube scanner output (deprecated - use --sonar_output_file)" )
235+ parser .add_argument ("--sonar_output_file" , help = "Path to file containing SonarQube scanner output" )
236+ parser .add_argument ("--sonar_token" , help = "SonarQube token for API access" )
237+ parser .add_argument ("--sonar_url" , default = "https://sonarqube.silabs.net" , help = "SonarQube server URL" )
238+ parser .add_argument ("--project_key" , default = "github_matter_sdk" , help = "SonarQube project key" )
121239
122240 args = parser .parse_args ()
123241
124242 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 )
243+ # Read sonar output from file or use direct argument
244+ sonar_output = ""
245+ if args .sonar_output_file :
246+ try :
247+ with open (args .sonar_output_file , 'r' ) as f :
248+ sonar_output = f .read ()
249+ print (f"✅ Read SonarQube output from file: { args .sonar_output_file } " )
250+ except Exception as e :
251+ print (f"❌ Error reading sonar output file { args .sonar_output_file } : { str (e )} " )
252+ if args .sonar_output :
253+ sonar_output = args .sonar_output
254+ print ("⚠️ Falling back to direct sonar_output argument" )
255+ else :
256+ raise
257+ elif args .sonar_output :
258+ sonar_output = args .sonar_output
259+ print ("✅ Using direct sonar_output argument" )
260+ else :
261+ raise ValueError ("Either --sonar_output_file or --sonar_output must be provided" )
262+
263+ # Fetch detailed SonarQube information if token is provided
264+ quality_gate_details = None
265+ measures = None
266+
267+ if args .sonar_token :
268+ print ("🔍 Fetching detailed SonarQube quality gate information..." )
269+ quality_gate_details = fetch_sonarqube_quality_gate (
270+ args .sonar_token ,
271+ args .sonar_url ,
272+ args .project_key ,
273+ args .branch_name ,
274+ pr_key = args .pr_number
275+ )
276+
277+ print ("📊 Fetching SonarQube measures..." )
278+ measures = fetch_sonarqube_measures (
279+ args .sonar_token ,
280+ args .sonar_url ,
281+ args .project_key ,
282+ args .branch_name ,
283+ pr_key = args .pr_number
284+ )
135285
136286 # Create comment body
137287 comment_body = create_comment_body (
138- args .result ,
139288 args .status ,
140289 args .commit_sha ,
141290 args .branch_name ,
142291 args .target_branch ,
143- sonar_output
292+ sonar_output ,
293+ quality_gate_details ,
294+ measures
144295 )
145296
146297 # Post PR comment
0 commit comments