Skip to content

Commit 0bdec21

Browse files
Merge pull request #427 from NirajC-Microsoft/dev
chore: Bicep Parameter validation email format change
2 parents b63d804 + a4c9a9c commit 0bdec21

2 files changed

Lines changed: 281 additions & 15 deletions

File tree

.github/workflows/validate-bicep-params.yml

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,16 @@ jobs:
3333
- name: Validate infra/ parameters
3434
id: validate_infra
3535
continue-on-error: true
36+
env:
37+
ACCELERATOR_NAME: ${{ env.accelerator_name }}
3638
run: |
3739
set +e
38-
python scripts/validate_bicep_params.py --dir infra --strict --no-color --json-output infra_results.json 2>&1 | tee infra_output.txt
40+
RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
41+
python scripts/validate_bicep_params.py --dir infra --strict --no-color \
42+
--json-output infra_results.json \
43+
--html-output email_body.html \
44+
--accelerator-name "${ACCELERATOR_NAME}" \
45+
--run-url "${RUN_URL}" 2>&1 | tee infra_output.txt
3946
EXIT_CODE=${PIPESTATUS[0]}
4047
set -e
4148
echo "## Infra Param Validation" >> "$GITHUB_STEP_SUMMARY"
@@ -60,24 +67,21 @@ jobs:
6067
name: bicep-validation-results
6168
path: |
6269
infra_results.json
70+
email_body.html
6371
retention-days: 30
6472

6573
- name: Send schedule notification on failure
6674
if: github.event_name == 'schedule' && steps.result.outputs.status == 'failure'
6775
env:
6876
LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}
69-
GITHUB_REPOSITORY: ${{ github.repository }}
70-
GITHUB_RUN_ID: ${{ github.run_id }}
7177
ACCELERATOR_NAME: ${{ env.accelerator_name }}
7278
run: |
73-
RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
74-
INFRA_OUTPUT=$(sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g' infra_output.txt)
79+
EMAIL_BODY=$(cat email_body.html)
7580
7681
jq -n \
7782
--arg name "${ACCELERATOR_NAME}" \
78-
--arg infra "$INFRA_OUTPUT" \
79-
--arg url "$RUN_URL" \
80-
'{subject: ("Bicep Parameter Validation Report - " + $name + " - Issues Detected"), body: ("<p>Dear Team,</p><p>The scheduled <strong>Bicep Parameter Validation</strong> for <strong>" + $name + "</strong> has detected parameter mapping errors.</p><p><strong>infra/ Results:</strong></p><pre>" + $infra + "</pre><p><strong>Run URL:</strong> <a href=\"" + $url + "\">" + $url + "</a></p><p>Please fix the parameter mapping issues at your earliest convenience.</p><p>Best regards,<br>Your Automation Team</p>")}' \
83+
--arg body "$EMAIL_BODY" \
84+
'{subject: ("Bicep Parameter Validation Report - " + $name + " - Issues Detected"), body: $body}' \
8185
| curl -X POST "${LOGICAPP_URL}" \
8286
-H "Content-Type: application/json" \
8387
-d @- || echo "Failed to send notification"
@@ -86,18 +90,14 @@ jobs:
8690
if: github.event_name == 'schedule' && steps.result.outputs.status == 'success'
8791
env:
8892
LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}
89-
GITHUB_REPOSITORY: ${{ github.repository }}
90-
GITHUB_RUN_ID: ${{ github.run_id }}
9193
ACCELERATOR_NAME: ${{ env.accelerator_name }}
9294
run: |
93-
RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
94-
INFRA_OUTPUT=$(sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g' infra_output.txt)
95+
EMAIL_BODY=$(cat email_body.html)
9596
9697
jq -n \
9798
--arg name "${ACCELERATOR_NAME}" \
98-
--arg infra "$INFRA_OUTPUT" \
99-
--arg url "$RUN_URL" \
100-
'{subject: ("Bicep Parameter Validation Report - " + $name + " - Passed"), body: ("<p>Dear Team,</p><p>The scheduled <strong>Bicep Parameter Validation</strong> for <strong>" + $name + "</strong> has completed successfully. All parameter mappings are valid.</p><p><strong>infra/ Results:</strong></p><pre>" + $infra + "</pre><p><strong>Run URL:</strong> <a href=\"" + $url + "\">" + $url + "</a></p><p>Best regards,<br>Your Automation Team</p>")}' \
99+
--arg body "$EMAIL_BODY" \
100+
'{subject: ("Bicep Parameter Validation Report - " + $name + " - Passed"), body: $body}' \
101101
| curl -X POST "${LOGICAPP_URL}" \
102102
-H "Content-Type: application/json" \
103103
-d @- || echo "Failed to send notification"

scripts/validate_bicep_params.py

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from __future__ import annotations
3030

3131
import argparse
32+
import html
3233
import json
3334
import re
3435
import sys
@@ -343,6 +344,241 @@ def print_report(results: list[ValidationResult], *, use_color: bool = True) ->
343344
print(f"{c['ERROR']}Parameter mapping issues detected!{c['RESET']}")
344345

345346

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

386639
results: list[ValidationResult] = []
@@ -417,6 +670,19 @@ def main() -> int:
417670
)
418671
print(f"\nJSON report written to {args.json_output}")
419672

673+
# Optional HTML email report
674+
if args.html_output:
675+
scan_dir = str(args.dir) if args.dir else ""
676+
html = generate_html_report(
677+
results,
678+
accelerator_name=args.accelerator_name,
679+
run_url=args.run_url,
680+
scan_dir=scan_dir,
681+
)
682+
args.html_output.parent.mkdir(parents=True, exist_ok=True)
683+
args.html_output.write_text(html, encoding="utf-8")
684+
print(f"HTML report written to {args.html_output}")
685+
420686
has_errors = any(r.has_errors for r in results)
421687
return 1 if args.strict and has_errors else 0
422688

0 commit comments

Comments
 (0)