6767import logging
6868import argparse
6969from enum import Enum , auto
70- from typing import Optional , Dict , Any , List , Union , TextIO
70+ from typing import Optional
7171
7272# Module initialization
7373__module__ = os .path .basename (__file__ )
@@ -105,7 +105,7 @@ class GithubActionsFormatter(logging.Formatter):
105105 logging .ERROR : "error" ,
106106 logging .CRITICAL : "error"
107107 }
108-
108+
109109 def format (self , record : logging .LogRecord ) -> str :
110110 """Format the log record as a GitHub Actions annotation.
111111
@@ -116,14 +116,23 @@ def format(self, record: logging.LogRecord) -> str:
116116 Formatted string in GitHub Actions annotation format
117117 """
118118 # Extract file/line info if available in extra attributes
119+ is_boundry = getattr (record , "is_boundry" , False )
119120 title = getattr (record , "title" , None )
120121 file_path = getattr (record , "file_path" , None )
121122 line_num = getattr (record , "line_num" , None )
123+ end_line_num = getattr (record , "end_line_num" , None )
122124 col_num = getattr (record , "col_num" , None )
125+ end_col_num = getattr (record , "end_col_num" , None )
123126 # Get the annotation level based on log level
124127 annotation_level = self .LEVEL_MAPPING .get (record .levelno , "notice" )
125128 # Format the basic message
126129 message = super ().format (record )
130+ if is_boundry :
131+ if message and (len (message ) > 0 ):
132+ annotation_level = "group"
133+ return f"::group::{ message } "
134+ else :
135+ return "::endgroup::"
127136 # Create the annotation command
128137 command = f"::{ annotation_level } "
129138 if annotation_level not in "debug" :
@@ -132,8 +141,12 @@ def format(self, record: logging.LogRecord) -> str:
132141 command += f" file={ file_path } "
133142 if line_num :
134143 command += f",line={ line_num } "
144+ if end_line_num :
145+ command += f",endLine={ end_line_num } "
135146 if col_num :
136147 command += f",col={ col_num } "
148+ if end_col_num :
149+ command += f",endCol={ end_col_num } "
137150 if title :
138151 if file_path :
139152 command += f",title={ title } "
@@ -152,11 +165,11 @@ class MarkdownFormatter(logging.Formatter):
152165
153166 # Map between log levels and markdown formatting
154167 LEVEL_MAPPING = {
155- logging .DEBUG : ("💬 " , "Debug " ),
156- logging .INFO : ("ℹ️ " , "Info " ),
157- logging .WARNING : ("⚠️ " , "Warning " ),
158- logging .ERROR : ("❌ " , "Error " ),
159- logging .CRITICAL : ("🔥 " , "Critical " )
168+ logging .DEBUG : (":speech_balloon: " , "NOTE " ),
169+ logging .INFO : (":information_source: " , "NOTE " ),
170+ logging .WARNING : (":warning: " , "WARNING " ),
171+ logging .ERROR : (":x: " , "CAUTION " ),
172+ logging .CRITICAL : (":fire: " , "CAUTION " )
160173 }
161174
162175 def format (self , record : logging .LogRecord ) -> str :
@@ -170,20 +183,33 @@ def format(self, record: logging.LogRecord) -> str:
170183 """
171184 # Get the icon and level name
172185 icon , level_name = self .LEVEL_MAPPING .get (
173- record .levelno , ("ℹ️ " , "Info" )
186+ record .levelno , (":information_source: " , "Info" )
174187 )
188+ is_boundry = getattr (record , "is_boundry" , False )
175189 # Format the basic message
176190 message = super ().format (record )
191+ if is_boundry :
192+ if message and (len (message ) > 0 ):
193+ return f"<details><summary>{ message } </summary>\n "
194+ else :
195+ return "</details>"
177196 # Extract file/line info if available in extra attributes
197+ title_info = ""
198+ title = getattr (record , "title" , None )
178199 file_info = ""
179200 file_path = getattr (record , "file_path" , None )
180201 line_num = getattr (record , "line_num" , None )
202+ end_line_num = getattr (record , "end_line_num" , None )
203+ if title :
204+ title_info = f"**{ title } **{ icon } \n > "
181205 if file_path :
182- file_info = f"\n > 📁 `{ file_path } `"
206+ file_info = f"\n > :file_folder: `{ file_path } `"
183207 if line_num :
184208 file_info += f":{ line_num } "
209+ if end_line_num :
210+ file_info += f"-{ end_line_num } "
185211 # Format as markdown
186- return f"{ icon } ** { level_name } **: { message } { file_info } "
212+ return f"> [! { level_name } ] \n > { title_info } { message } { file_info } "
187213
188214
189215class ColorizedConsoleFormatter (logging .Formatter ):
@@ -214,8 +240,14 @@ def format(self, record: logging.LogRecord) -> str:
214240 """
215241 # Check if output is a terminal before using colors
216242 use_colors = hasattr (sys .stdout , 'isatty' ) and sys .stdout .isatty ()
243+ is_boundry = getattr (record , "is_boundry" , False )
217244 # Format the basic message
218245 message = super ().format (record )
246+ if is_boundry :
247+ if len (message ) > 0 :
248+ return f"[START] { message } "
249+ else :
250+ return "[ END ]"
219251 if use_colors :
220252 # Get the color code for this level
221253 color = self .COLORS .get (record .levelno , self .RESET )
@@ -313,29 +345,42 @@ def log(
313345 self ,
314346 message : str ,
315347 level : LogLevel = LogLevel .INFO ,
348+ is_boundry : Optional [bool ] = None ,
316349 title : Optional [str ] = None ,
317350 file_path : Optional [str ] = None ,
318351 line_num : Optional [int ] = None ,
319- col_num : Optional [int ] = None
352+ end_line_num : Optional [int ] = None ,
353+ col_num : Optional [int ] = None ,
354+ end_col_num : Optional [int ] = None
320355 ) -> None :
321356 """Log a message with the configured formatter.
322357
323358 Args:
324359 message: Message to log
325360 level: Log level to use
361+ group: Optional title for annotations
362+ title: Optional title for annotations
326363 file_path: Optional file path for annotations
327364 line_num: Optional line number for annotations
365+ end_line_num: Optional end-line number for annotations
328366 col_num: Optional column number for annotations
367+ end_col_num: Optional end-column number for annotations
329368 """
330369 extra = {}
370+ if is_boundry :
371+ extra ["is_boundry" ] = is_boundry
331372 if file_path :
332373 extra ["title" ] = title
333374 if file_path :
334375 extra ["file_path" ] = file_path
335376 if line_num :
336377 extra ["line_num" ] = line_num
378+ if end_line_num :
379+ extra ["end_line_num" ] = end_line_num
337380 if col_num :
338381 extra ["col_num" ] = col_num
382+ if end_col_num :
383+ extra ["end_col_num" ] = end_col_num
339384 self .logger .log (level .value , message , extra = extra if extra else None )
340385 # Also add ERROR and higher messages to GitHub Actions summary
341386 if level .value >= logging .ERROR and self .format_type == OutputFormat .MARKDOWN :
@@ -362,7 +407,7 @@ def debug(self, message: str, **kwargs) -> None:
362407 **kwargs: Additional parameters (title, file_path, line_num, col_num)
363408 """
364409 self .log (message , LogLevel .DEBUG , ** kwargs )
365-
410+
366411 def info (self , message : str , ** kwargs ) -> None :
367412 """Log an info message.
368413
@@ -371,7 +416,7 @@ def info(self, message: str, **kwargs) -> None:
371416 **kwargs: Additional parameters (title, file_path, line_num, col_num)
372417 """
373418 self .log (message , LogLevel .INFO , ** kwargs )
374-
419+
375420 def warning (self , message : str , ** kwargs ) -> None :
376421 """Log a warning message.
377422
@@ -380,7 +425,7 @@ def warning(self, message: str, **kwargs) -> None:
380425 **kwargs: Additional parameters (title, file_path, line_num, col_num)
381426 """
382427 self .log (message , LogLevel .WARNING , ** kwargs )
383-
428+
384429 def error (self , message : str , ** kwargs ) -> None :
385430 """Log an error message.
386431
@@ -389,7 +434,7 @@ def error(self, message: str, **kwargs) -> None:
389434 **kwargs: Additional parameters (title, file_path, line_num, col_num)
390435 """
391436 self .log (message , LogLevel .ERROR , ** kwargs )
392-
437+
393438 def critical (self , message : str , ** kwargs ) -> None :
394439 """Log a critical message.
395440
@@ -416,7 +461,7 @@ def configure_output_tool() -> CIOutputTool:
416461 Configured CIOutputTool instance
417462 """
418463 parser = argparse .ArgumentParser (
419- description = "CI/CD output formatting tool for GitHub Actions"
464+ description = "CI/CD output formatting tool for GitHub Actions" ,
420465 )
421466 parser .add_argument (
422467 "-f" , "--format" ,
@@ -436,29 +481,72 @@ def configure_output_tool() -> CIOutputTool:
436481 help = "Optional message to log"
437482 )
438483 parser .add_argument (
439- "--title" ,
484+ "-t" , "- -title" ,
440485 help = "File path for GitHub Actions annotations"
441486 )
442487 parser .add_argument (
443488 "--file" ,
444489 help = "File path for GitHub Actions annotations"
445490 )
446- parser .add_argument (
491+ group = parser .add_argument_group ()
492+ lineGroup = group .add_argument_group ()
493+ lineSubGroup = lineGroup .add_mutually_exclusive_group (required = False )
494+ colGroup = lineGroup .add_argument_group ()
495+ colSubGroup = colGroup .add_mutually_exclusive_group (required = False )
496+ lineSubGroup .add_argument (
447497 "--line" ,
498+ dest = "line" ,
448499 type = int ,
500+ metavar = "LINE" ,
449501 help = "Line number for GitHub Actions annotations"
450502 )
451- parser .add_argument (
503+ lineSubGroup .add_argument (
504+ "--start-line" ,
505+ dest = "line" ,
506+ type = int ,
507+ metavar = "START_LINE" ,
508+ help = "Line number for GitHub Actions annotations"
509+ )
510+ colSubGroup .add_argument (
452511 "--col" ,
512+ dest = "col" ,
453513 type = int ,
514+ metavar = "COL" ,
454515 help = "Column number for GitHub Actions annotations"
455516 )
517+ colSubGroup .add_argument (
518+ "--start-col" ,
519+ dest = "col" ,
520+ type = int ,
521+ metavar = "START_COL" ,
522+ help = "Column number for GitHub Actions annotations"
523+ )
524+ lineGroup .add_argument (
525+ "--end-line" ,
526+ dest = "end_line" ,
527+ type = int ,
528+ help = "End line number for GitHub Actions annotations"
529+ )
530+ colGroup .add_argument (
531+ "--end-col" ,
532+ dest = "end_col" ,
533+ type = int ,
534+ metavar = "END_COL" ,
535+ help = "End column number for GitHub Actions annotations"
536+ )
456537 parser .add_argument (
457538 "--log-level" ,
458539 choices = ["debug" , "info" , "warning" , "error" , "critical" ],
459540 default = "info" ,
460541 help = "Minimum log level to display"
461542 )
543+ parser .add_argument (
544+ "--group" ,
545+ dest = "is_group" ,
546+ default = False ,
547+ action = "store_true" ,
548+ help = "Optionally treat this as a group boundry. Ends if message is empty, otherwise starts"
549+ )
462550 args = parser .parse_args ()
463551 # Determine output format
464552 if args .format == "auto" :
@@ -474,15 +562,18 @@ def configure_output_tool() -> CIOutputTool:
474562 # Create and configure the tool
475563 tool = CIOutputTool (format_type , log_level )
476564 # Process message if provided
477- if args .message :
565+ if args .message or args . is_group :
478566 msg_level = getattr (LogLevel , args .level .upper ())
479567 tool .log (
480- args .message ,
568+ args .message if args . message else "" ,
481569 level = msg_level ,
570+ is_boundry = args .is_group ,
482571 title = args .title ,
483572 file_path = args .file ,
484573 line_num = args .line ,
574+ end_line_num = args .end_line ,
485575 col_num = args .col ,
576+ end_col_num = args .end_col ,
486577 )
487578 return tool
488579
@@ -495,10 +586,11 @@ def main() -> int:
495586 """
496587 try :
497588 tool = configure_output_tool ()
498- return 0
589+ if tool :
590+ return 0
499591 except Exception as e :
500592 print (f"Error initializing CI output tool: { e } " , file = sys .stderr )
501- return 1
593+ return 1
502594
503595
504596if __name__ == "__main__" :
0 commit comments