@@ -341,6 +341,246 @@ def print_report(results: list[ValidationResult], *, use_color: bool = True) ->
341341 print (f"{ c ['ERROR' ]} Parameter mapping issues detected!{ c ['RESET' ]} " )
342342
343343
344+ # ---------------------------------------------------------------------------
345+ # HTML email report
346+ # ---------------------------------------------------------------------------
347+
348+ def _html_escape (text : str ) -> str :
349+ """Escape HTML special characters."""
350+ return (
351+ text .replace ("&" , "&" )
352+ .replace ("<" , "<" )
353+ .replace (">" , ">" )
354+ .replace ('"' , """ )
355+ )
356+
357+
358+ def generate_html_report (
359+ results : list [ValidationResult ],
360+ * ,
361+ accelerator_name : str = "" ,
362+ run_url : str = "" ,
363+ scan_dir : str = "" ,
364+ ) -> str :
365+ """Build a structured HTML email body from validation results."""
366+ total_errors = sum (
367+ 1 for r in results for i in r .issues if i .severity == "ERROR"
368+ )
369+ total_warnings = sum (
370+ 1 for r in results for i in r .issues if i .severity == "WARNING"
371+ )
372+ has_errors = total_errors > 0
373+ overall_status = "Issues Detected" if has_errors else "Passed"
374+ status_color = "#D32F2F" if has_errors else "#2E7D32"
375+ status_bg = "#FFEBEE" if has_errors else "#E8F5E9"
376+ status_icon = "❌" if has_errors else "✅"
377+
378+ parts : list [str ] = []
379+
380+ # --- Document wrapper (Outlook-compatible, no gradient/border-radius/box-shadow) ---
381+ parts .append (
382+ '<!DOCTYPE html><html><head><meta charset="utf-8"></head>'
383+ '<body style="margin:0;padding:0;font-family:Segoe UI,Helvetica,Arial,sans-serif;'
384+ 'background-color:#ffffff;">'
385+ '<table role="presentation" width="100%" cellpadding="0" cellspacing="0"'
386+ ' style="background-color:#ffffff;">'
387+ '<tr><td align="center" style="padding:0;">'
388+ '<table role="presentation" width="100%" cellpadding="0" cellspacing="0"'
389+ ' style="max-width:680px;background-color:#ffffff;">'
390+ )
391+
392+ # --- Header banner (solid color, Outlook-safe) ---
393+ parts .append (
394+ f'<tr><td style="background-color:#0078D4;padding:20px 24px;color:#ffffff;">'
395+ f'<h1 style="margin:0 0 4px 0;font-size:20px;font-weight:600;color:#ffffff;">'
396+ f'Bicep Parameter Validation Report</h1>'
397+ f'<p style="margin:0;font-size:13px;color:#ffffff;">'
398+ f'{ _html_escape (accelerator_name ) if accelerator_name else "Accelerator" } '
399+ f' — Automated Check</p>'
400+ f'</td></tr>'
401+ )
402+
403+ # --- Summary card ---
404+ parts .append (
405+ f'<tr><td style="padding:16px 24px 12px 24px;">'
406+ f'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"'
407+ f' style="background-color:{ status_bg } ;border-left:4px solid { status_color } ;">'
408+ f'<tr><td style="padding:12px 16px;">'
409+ f'<span style="font-size:16px;font-weight:600;color:{ status_color } ;">'
410+ f'{ status_icon } Overall Status: { overall_status } </span>'
411+ f'</td></tr>'
412+ f'<tr><td style="padding:4px 16px 12px 16px;">'
413+ f'<table role="presentation" cellpadding="0" cellspacing="0"><tr>'
414+ )
415+ # Accelerator name pill
416+ if accelerator_name :
417+ parts .append (
418+ f'<td style="padding-right:20px;vertical-align:top;">'
419+ f'<span style="font-size:11px;color:#666;">Accelerator</span><br>'
420+ f'<strong style="font-size:13px;">{ _html_escape (accelerator_name )} '
421+ f'</strong></td>'
422+ )
423+ # Scan directory pill
424+ if scan_dir :
425+ parts .append (
426+ f'<td style="padding-right:20px;vertical-align:top;">'
427+ f'<span style="font-size:11px;color:#666;">Scan Directory</span><br>'
428+ f'<strong style="font-size:13px;">{ _html_escape (scan_dir )} /</strong>'
429+ f'</td>'
430+ )
431+ # Error count pill
432+ err_pill_color = "#D32F2F" if total_errors > 0 else "#2E7D32"
433+ parts .append (
434+ f'<td style="padding-right:20px;vertical-align:top;">'
435+ f'<span style="font-size:11px;color:#666;">Errors</span><br>'
436+ f'<strong style="font-size:13px;color:{ err_pill_color } ;">'
437+ f'{ total_errors } </strong></td>'
438+ )
439+ # Warning count pill
440+ warn_pill_color = "#F57C00" if total_warnings > 0 else "#2E7D32"
441+ parts .append (
442+ f'<td style="vertical-align:top;">'
443+ f'<span style="font-size:11px;color:#666;">Warnings</span><br>'
444+ f'<strong style="font-size:13px;color:{ warn_pill_color } ;">'
445+ f'{ total_warnings } </strong></td>'
446+ )
447+ parts .append ("</tr></table></td></tr></table></td></tr>" )
448+
449+ # --- Per-pair detail sections ---
450+ parts .append ('<tr><td style="padding:8px 24px 0 24px;">' )
451+ for r in results :
452+ errors = [i for i in r .issues if i .severity == "ERROR" ]
453+ warnings = [i for i in r .issues if i .severity == "WARNING" ]
454+
455+ if not r .issues :
456+ badge = (
457+ '<span style="display:inline-block;padding:2px 8px;'
458+ 'font-size:11px;font-weight:700;'
459+ 'color:#2E7D32;background-color:#E8F5E9;">PASS</span>'
460+ )
461+ elif errors :
462+ badge = (
463+ '<span style="display:inline-block;padding:2px 8px;'
464+ 'font-size:11px;font-weight:700;'
465+ 'color:#D32F2F;background-color:#FFEBEE;">FAIL</span>'
466+ )
467+ else :
468+ badge = (
469+ '<span style="display:inline-block;padding:2px 8px;'
470+ 'font-size:11px;font-weight:700;'
471+ 'color:#F57C00;background-color:#FFF3E0;">WARN</span>'
472+ )
473+
474+ parts .append (
475+ f'<table role="presentation" width="100%" cellpadding="0"'
476+ f' cellspacing="0" style="margin-bottom:12px;border:1px solid #e0e0e0;">'
477+ f'<tr><td style="background-color:#fafafa;padding:10px 12px;'
478+ f'border-bottom:1px solid #e0e0e0;">'
479+ f'{ badge } '
480+ f'<strong style="font-size:13px;">'
481+ f'{ _html_escape (r .pair )} </strong>'
482+ f'<span style="float:right;font-size:11px;color:#888;">'
483+ f'{ len (errors )} error(s), { len (warnings )} warning(s)</span>'
484+ f'</td></tr>'
485+ )
486+
487+ if r .issues :
488+ # --- Errors section ---
489+ if errors :
490+ parts .append (
491+ '<tr><td style="padding:8px 12px 4px 12px;">'
492+ '<strong style="font-size:12px;color:#D32F2F;">'
493+ '● Errors</strong></td></tr>'
494+ '<tr><td style="padding:0 12px;">'
495+ '<table role="presentation" width="100%" cellpadding="0"'
496+ ' cellspacing="0" style="font-size:12px;border:1px solid #f5c6cb;">'
497+ '<tr style="background-color:#FFEBEE;">'
498+ '<th style="text-align:left;padding:6px 10px;'
499+ 'border-bottom:1px solid #f5c6cb;width:180px;">Parameter</th>'
500+ '<th style="text-align:left;padding:6px 10px;'
501+ 'border-bottom:1px solid #f5c6cb;">Details</th></tr>'
502+ )
503+ for idx , issue in enumerate (errors ):
504+ bg = "#ffffff" if idx % 2 == 0 else "#fff5f5"
505+ parts .append (
506+ f'<tr style="background-color:{ bg } ;">'
507+ f'<td style="padding:5px 10px;border-bottom:1px solid #f5c6cb;'
508+ f'vertical-align:top;font-family:Consolas,monospace;'
509+ f'font-size:11px;word-break:break-all;">'
510+ f'{ _html_escape (issue .param_name )} </td>'
511+ f'<td style="padding:5px 10px;border-bottom:1px solid #f5c6cb;'
512+ f'vertical-align:top;">{ _html_escape (issue .message )} </td>'
513+ f'</tr>'
514+ )
515+ parts .append ("</table></td></tr>" )
516+
517+ # --- Warnings section ---
518+ if warnings :
519+ parts .append (
520+ '<tr><td style="padding:8px 12px 4px 12px;">'
521+ '<strong style="font-size:12px;color:#F57C00;">'
522+ '● Warnings</strong></td></tr>'
523+ '<tr><td style="padding:0 12px 8px 12px;">'
524+ '<table role="presentation" width="100%" cellpadding="0"'
525+ ' cellspacing="0" style="font-size:12px;border:1px solid #ffe0b2;">'
526+ '<tr style="background-color:#FFF3E0;">'
527+ '<th style="text-align:left;padding:6px 10px;'
528+ 'border-bottom:1px solid #ffe0b2;width:180px;">Parameter</th>'
529+ '<th style="text-align:left;padding:6px 10px;'
530+ 'border-bottom:1px solid #ffe0b2;">Details</th></tr>'
531+ )
532+ for idx , issue in enumerate (warnings ):
533+ bg = "#ffffff" if idx % 2 == 0 else "#fffaf0"
534+ parts .append (
535+ f'<tr style="background-color:{ bg } ;">'
536+ f'<td style="padding:5px 10px;border-bottom:1px solid #ffe0b2;'
537+ f'vertical-align:top;font-family:Consolas,monospace;'
538+ f'font-size:11px;word-break:break-all;">'
539+ f'{ _html_escape (issue .param_name )} </td>'
540+ f'<td style="padding:5px 10px;border-bottom:1px solid #ffe0b2;'
541+ f'vertical-align:top;">{ _html_escape (issue .message )} </td>'
542+ f'</tr>'
543+ )
544+ parts .append ("</table></td></tr>" )
545+ else :
546+ parts .append (
547+ '<tr><td style="padding:10px 12px;color:#2E7D32;'
548+ 'font-size:12px;">All parameters validated successfully.'
549+ '</td></tr>'
550+ )
551+
552+ parts .append ("</table>" )
553+
554+ parts .append ("</td></tr>" )
555+
556+ # --- Footer with run URL ---
557+ footer_parts : list [str ] = []
558+ if run_url :
559+ footer_parts .append (
560+ f'<a href="{ _html_escape (run_url )} " style="display:inline-block;'
561+ f'padding:8px 16px;background-color:#0078D4;color:#ffffff;'
562+ f'text-decoration:none;font-size:12px;'
563+ f'font-weight:600;">View Workflow Run</a>'
564+ )
565+ if has_errors :
566+ footer_parts .append (
567+ '<p style="margin:10px 0 0 0;font-size:12px;color:#555;">'
568+ 'Please fix the parameter mapping issues at your earliest convenience.</p>'
569+ )
570+ footer_parts .append (
571+ '<p style="margin:10px 0 0 0;font-size:12px;color:#999;">'
572+ 'Best regards,<br>Your Automation Team</p>'
573+ )
574+ parts .append (
575+ f'<tr><td style="padding:14px 24px 20px 24px;border-top:1px solid #e0e0e0;">'
576+ f'{ "" .join (footer_parts )} </td></tr>'
577+ )
578+
579+ # --- Close wrapper ---
580+ parts .append ("</table></td></tr></table></body></html>" )
581+ return "" .join (parts )
582+
583+
344584# ---------------------------------------------------------------------------
345585# CLI
346586# ---------------------------------------------------------------------------
@@ -379,6 +619,23 @@ def main() -> int:
379619 type = Path ,
380620 help = "Write results as JSON to the given file path." ,
381621 )
622+ parser .add_argument (
623+ "--html-output" ,
624+ type = Path ,
625+ help = "Write a structured HTML email report to the given file path." ,
626+ )
627+ parser .add_argument (
628+ "--accelerator-name" ,
629+ type = str ,
630+ default = "" ,
631+ help = "Accelerator display name for the HTML report header." ,
632+ )
633+ parser .add_argument (
634+ "--run-url" ,
635+ type = str ,
636+ default = "" ,
637+ help = "Workflow run URL to include in the HTML report footer." ,
638+ )
382639 args = parser .parse_args ()
383640
384641 results : list [ValidationResult ] = []
@@ -415,6 +672,19 @@ def main() -> int:
415672 )
416673 print (f"\n JSON report written to { args .json_output } " )
417674
675+ # Optional HTML email report
676+ if args .html_output :
677+ scan_dir = str (args .dir ) if args .dir else ""
678+ html = generate_html_report (
679+ results ,
680+ accelerator_name = args .accelerator_name ,
681+ run_url = args .run_url ,
682+ scan_dir = scan_dir ,
683+ )
684+ args .html_output .parent .mkdir (parents = True , exist_ok = True )
685+ args .html_output .write_text (html , encoding = "utf-8" )
686+ print (f"HTML report written to { args .html_output } " )
687+
418688 has_errors = any (r .has_errors for r in results )
419689 return 1 if args .strict and has_errors else 0
420690
0 commit comments