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,113 @@ 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+ measures [measure ['metric' ]] = measure .get ('value' , '0' )
130+ return measures
131+ else :
132+ print (f"⚠️ Failed to fetch measures: { response .status_code } " )
133+ return {}
134+
135+ except Exception as e :
136+ print (f"⚠️ Error fetching measures: { str (e )} " )
137+ return {}
138+
139+ def create_comment_body (status , commit_sha , branch_name , target_branch , sonar_output , quality_gate_details = None , measures = None ):
65140 """Create formatted comment body for GitHub PR"""
66141
67142 # Determine status emoji and text
68- if result == "PASS " :
143+ if status == "PASSED " :
69144 result_emoji = "✅"
70145 result_text = "**PASSED**"
71146 else :
72147 result_emoji = "❌"
73148 result_text = "**FAILED**"
74149
150+ # Use measures if available, otherwise show N/A
151+ default_metrics = {
152+ 'ncloc' : 'N/A' ,
153+ 'duplicated_lines_density' : 'N/A' ,
154+ 'new_violations' : 'N/A' ,
155+ 'new_code_smells' : 'N/A' ,
156+ 'new_bugs' : 'N/A' ,
157+ 'new_vulnerabilities' : 'N/A' ,
158+ 'new_security_hotspots' : 'N/A' ,
159+ 'new_maintainability_rating' : 'N/A' ,
160+ 'reliability_rating' : 'N/A' ,
161+ 'new_security_rating' : 'N/A'
162+ }
163+
164+ # Merge measures with defaults
165+ metrics = default_metrics .copy ()
166+ if measures :
167+ metrics .update (measures )
168+
169+ # Get quality gate status
170+ quality_gate_status = quality_gate_details .get ('status' , status ) if quality_gate_details else status
171+
75172 # Truncate output if too long - show last 2000 characters for most relevant info
76173 max_output_length = 2000
77174 if len (sonar_output ) > max_output_length :
@@ -90,7 +187,25 @@ def create_comment_body(result, status, commit_sha, branch_name, target_branch,
90187- **Target:** `{ target_branch } `
91188- **Analysis Time:** { datetime .now ().strftime ('%Y-%m-%d %H:%M:%S UTC' )}
92189
93- ### 📋 Detailed Results
190+ ### � Key Metrics
191+ | Metric | Value |
192+ |--------|-------|
193+ | **New Lines of Code** | { metrics .get ('new_lines' , 'N/A' )} |
194+ | **New Duplicated Lines Density** | { metrics .get ('new_duplicated_lines_density' , metrics .get ('duplicated_lines_density' , 'N/A' ))} % |
195+ | **New Violations** | { metrics .get ('new_violations' , 'N/A' )} |
196+ | **New Code Smells** | { metrics .get ('new_code_smells' , 'N/A' )} |
197+ | **New Bugs** | { metrics .get ('new_bugs' , 'N/A' )} |
198+ | **New Vulnerabilities** | { metrics .get ('new_vulnerabilities' , 'N/A' )} |
199+ | **New Security Hotspots** | { metrics .get ('new_security_hotspots' , 'N/A' )} |
200+
201+ ### 🏆 Quality Ratings
202+ | Category | Rating |
203+ |----------|--------|
204+ | **New Maintainability Rating** | { metrics .get ('new_maintainability_rating' , 'N/A' )} |
205+ | **Reliability Rating** | { metrics .get ('reliability_rating' , 'N/A' )} |
206+ | **New Security Rating** | { metrics .get ('new_security_rating' , 'N/A' )} |
207+
208+ ### �📋 Detailed Results
94209<details>
95210<summary>Click to view SonarQube output</summary>
96211
@@ -117,30 +232,67 @@ def main():
117232
118233 parser .add_argument ("--branch_name" , required = True , help = "Source branch name" )
119234 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" )
235+ parser .add_argument ("--sonar_output" , help = "SonarQube scanner output (deprecated - use --sonar_output_file)" )
236+ parser .add_argument ("--sonar_output_file" , help = "Path to file containing SonarQube scanner output" )
237+ parser .add_argument ("--sonar_token" , help = "SonarQube token for API access" )
238+ parser .add_argument ("--sonar_url" , default = "https://sonarqube.silabs.net" , help = "SonarQube server URL" )
239+ parser .add_argument ("--project_key" , default = "github_matter_sdk" , help = "SonarQube project key" )
121240
122241 args = parser .parse_args ()
123242
124243 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 )
244+ # Read sonar output from file or use direct argument
245+ sonar_output = ""
246+ if args .sonar_output_file :
247+ try :
248+ with open (args .sonar_output_file , 'r' ) as f :
249+ sonar_output = f .read ()
250+ print (f"✅ Read SonarQube output from file: { args .sonar_output_file } " )
251+ except Exception as e :
252+ print (f"❌ Error reading sonar output file { args .sonar_output_file } : { str (e )} " )
253+ if args .sonar_output :
254+ sonar_output = args .sonar_output
255+ print ("⚠️ Falling back to direct sonar_output argument" )
256+ else :
257+ raise
258+ elif args .sonar_output :
259+ sonar_output = args .sonar_output
260+ print ("✅ Using direct sonar_output argument" )
261+ else :
262+ raise ValueError ("Either --sonar_output_file or --sonar_output must be provided" )
263+
264+ # Fetch detailed SonarQube information if token is provided
265+ quality_gate_details = None
266+ measures = None
267+
268+ if args .sonar_token :
269+ print ("🔍 Fetching detailed SonarQube quality gate information..." )
270+ quality_gate_details = fetch_sonarqube_quality_gate (
271+ args .sonar_token ,
272+ args .sonar_url ,
273+ args .project_key ,
274+ args .branch_name ,
275+ pr_key = args .pr_number
276+ )
277+
278+ print ("📊 Fetching SonarQube measures..." )
279+ measures = fetch_sonarqube_measures (
280+ args .sonar_token ,
281+ args .sonar_url ,
282+ args .project_key ,
283+ args .branch_name ,
284+ pr_key = args .pr_number
285+ )
135286
136287 # Create comment body
137288 comment_body = create_comment_body (
138- args .result ,
139289 args .status ,
140290 args .commit_sha ,
141291 args .branch_name ,
142292 args .target_branch ,
143- sonar_output
293+ sonar_output ,
294+ quality_gate_details ,
295+ measures
144296 )
145297
146298 # Post PR comment
0 commit comments