diff --git a/.github/workflows/validate-bicep-params.yml b/.github/workflows/validate-bicep-params.yml
index 3d8433b7..4ae614ee 100644
--- a/.github/workflows/validate-bicep-params.yml
+++ b/.github/workflows/validate-bicep-params.yml
@@ -33,9 +33,16 @@ jobs:
- name: Validate infra/ parameters
id: validate_infra
continue-on-error: true
+ env:
+ ACCELERATOR_NAME: ${{ env.accelerator_name }}
run: |
set +e
- python infra/scripts/validate_bicep_params.py --dir infra --strict --no-color --json-output infra_results.json 2>&1 | tee infra_output.txt
+ RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
+ python infra/scripts/validate_bicep_params.py --dir infra --strict --no-color \
+ --json-output infra_results.json \
+ --html-output email_body.html \
+ --accelerator-name "${ACCELERATOR_NAME}" \
+ --run-url "${RUN_URL}" 2>&1 | tee infra_output.txt
EXIT_CODE=${PIPESTATUS[0]}
set -e
echo "## Infra Param Validation" >> "$GITHUB_STEP_SUMMARY"
@@ -60,24 +67,23 @@ jobs:
name: bicep-validation-results
path: |
infra_results.json
+ email_body.html
retention-days: 30
- name: Send schedule notification on failure
if: github.event_name == 'schedule' && steps.result.outputs.status == 'failure'
env:
LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}
- GITHUB_REPOSITORY: ${{ github.repository }}
- GITHUB_RUN_ID: ${{ github.run_id }}
ACCELERATOR_NAME: ${{ env.accelerator_name }}
run: |
- RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
- INFRA_OUTPUT=$(sed 's/&/\&/g; s/\</g; s/>/\>/g' infra_output.txt)
+ if [ ! -f email_body.html ]; then
+ echo "
Email body was not generated. Please check the workflow logs.
" > email_body.html
+ fi
jq -n \
--arg name "${ACCELERATOR_NAME}" \
- --arg infra "$INFRA_OUTPUT" \
- --arg url "$RUN_URL" \
- '{subject: ("Bicep Parameter Validation Report - " + $name + " - Issues Detected"), body: ("Dear Team,
The scheduled Bicep Parameter Validation for " + $name + " has detected parameter mapping errors.
infra/ Results:
" + $infra + "
Run URL: " + $url + "
Please fix the parameter mapping issues at your earliest convenience.
Best regards,
Your Automation Team
")}' \
+ --rawfile body email_body.html \
+ '{subject: ("Bicep Parameter Validation Report - " + $name + " - Issues Detected"), body: $body}' \
| curl -X POST "${LOGICAPP_URL}" \
-H "Content-Type: application/json" \
-d @- || echo "Failed to send notification"
@@ -86,18 +92,16 @@ jobs:
if: github.event_name == 'schedule' && steps.result.outputs.status == 'success'
env:
LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}
- GITHUB_REPOSITORY: ${{ github.repository }}
- GITHUB_RUN_ID: ${{ github.run_id }}
ACCELERATOR_NAME: ${{ env.accelerator_name }}
run: |
- RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
- INFRA_OUTPUT=$(sed 's/&/\&/g; s/\</g; s/>/\>/g' infra_output.txt)
+ if [ ! -f email_body.html ]; then
+ echo "Email body was not generated. Please check the workflow logs.
" > email_body.html
+ fi
jq -n \
--arg name "${ACCELERATOR_NAME}" \
- --arg infra "$INFRA_OUTPUT" \
- --arg url "$RUN_URL" \
- '{subject: ("Bicep Parameter Validation Report - " + $name + " - Passed"), body: ("Dear Team,
The scheduled Bicep Parameter Validation for " + $name + " has completed successfully. All parameter mappings are valid.
infra/ Results:
" + $infra + "
Run URL: " + $url + "
Best regards,
Your Automation Team
")}' \
+ --rawfile body email_body.html \
+ '{subject: ("Bicep Parameter Validation Report - " + $name + " - Passed"), body: $body}' \
| curl -X POST "${LOGICAPP_URL}" \
-H "Content-Type: application/json" \
-d @- || echo "Failed to send notification"
diff --git a/infra/scripts/validate_bicep_params.py b/infra/scripts/validate_bicep_params.py
index 34ea8d48..6da7d91e 100644
--- a/infra/scripts/validate_bicep_params.py
+++ b/infra/scripts/validate_bicep_params.py
@@ -341,6 +341,246 @@ def print_report(results: list[ValidationResult], *, use_color: bool = True) ->
print(f"{c['ERROR']}Parameter mapping issues detected!{c['RESET']}")
+# ---------------------------------------------------------------------------
+# HTML email report
+# ---------------------------------------------------------------------------
+
+def _html_escape(text: str) -> str:
+ """Escape HTML special characters."""
+ return (
+ text.replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace('"', """)
+ )
+
+
+def generate_html_report(
+ results: list[ValidationResult],
+ *,
+ accelerator_name: str = "",
+ run_url: str = "",
+ scan_dir: str = "",
+) -> str:
+ """Build a structured HTML email body from validation results."""
+ total_errors = sum(
+ 1 for r in results for i in r.issues if i.severity == "ERROR"
+ )
+ total_warnings = sum(
+ 1 for r in results for i in r.issues if i.severity == "WARNING"
+ )
+ has_errors = total_errors > 0
+ overall_status = "Issues Detected" if has_errors else "Passed"
+ status_color = "#D32F2F" if has_errors else "#2E7D32"
+ status_bg = "#FFEBEE" if has_errors else "#E8F5E9"
+ status_icon = "❌" if has_errors else "✅"
+
+ parts: list[str] = []
+
+ # --- Document wrapper (Outlook-compatible, no gradient/border-radius/box-shadow) ---
+ parts.append(
+ ''
+ ''
+ ''
+ ''
+ ''
+ )
+
+ # --- Header banner (solid color, Outlook-safe) ---
+ parts.append(
+ f''
+ f''
+ f'Bicep Parameter Validation Report'
+ f''
+ f'{_html_escape(accelerator_name) if accelerator_name else "Accelerator"}'
+ f' — Automated Check '
+ f' | '
+ )
+
+ # --- Summary card ---
+ parts.append(
+ f''
+ f''
+ f'| '
+ f''
+ f'{status_icon} Overall Status: {overall_status}'
+ f' | '
+ f''
+ f''
+ )
+ # Accelerator name pill
+ if accelerator_name:
+ parts.append(
+ f''
+ f'Accelerator '
+ f'{_html_escape(accelerator_name)}'
+ f' | '
+ )
+ # Scan directory pill
+ if scan_dir:
+ parts.append(
+ f''
+ f'Scan Directory '
+ f'{_html_escape(scan_dir)}/'
+ f' | '
+ )
+ # Error count pill
+ err_pill_color = "#D32F2F" if total_errors > 0 else "#2E7D32"
+ parts.append(
+ f''
+ f'Errors '
+ f''
+ f'{total_errors} | '
+ )
+ # Warning count pill
+ warn_pill_color = "#F57C00" if total_warnings > 0 else "#2E7D32"
+ parts.append(
+ f''
+ f'Warnings '
+ f''
+ f'{total_warnings} | '
+ )
+ parts.append("
| | ")
+
+ # --- Per-pair detail sections ---
+ parts.append('')
+ for r in results:
+ errors = [i for i in r.issues if i.severity == "ERROR"]
+ warnings = [i for i in r.issues if i.severity == "WARNING"]
+
+ if not r.issues:
+ badge = (
+ 'PASS'
+ )
+ elif errors:
+ badge = (
+ 'FAIL'
+ )
+ else:
+ badge = (
+ 'WARN'
+ )
+
+ parts.append(
+ f''
+ f'| '
+ f'{badge} '
+ f''
+ f'{_html_escape(r.pair)}'
+ f''
+ f'{len(errors)} error(s), {len(warnings)} warning(s)'
+ f' | '
+ )
+
+ if r.issues:
+ # --- Errors section ---
+ if errors:
+ parts.append(
+ '| '
+ ''
+ '● Errors | '
+ ''
+ ''
+ ''
+ '| Parameter | '
+ 'Details | '
+ )
+ for idx, issue in enumerate(errors):
+ bg = "#ffffff" if idx % 2 == 0 else "#fff5f5"
+ parts.append(
+ f''
+ f'| '
+ f'{_html_escape(issue.param_name)} | '
+ f'{_html_escape(issue.message)} | '
+ f' '
+ )
+ parts.append(" | ")
+
+ # --- Warnings section ---
+ if warnings:
+ parts.append(
+ '| '
+ ''
+ '● Warnings | '
+ ''
+ ''
+ ''
+ '| Parameter | '
+ 'Details | '
+ )
+ for idx, issue in enumerate(warnings):
+ bg = "#ffffff" if idx % 2 == 0 else "#fffaf0"
+ parts.append(
+ f''
+ f'| '
+ f'{_html_escape(issue.param_name)} | '
+ f'{_html_escape(issue.message)} | '
+ f' '
+ )
+ parts.append(" | ")
+ else:
+ parts.append(
+ '| All parameters validated successfully.'
+ ' | '
+ )
+
+ parts.append(" ")
+
+ parts.append(" | ")
+
+ # --- Footer with run URL ---
+ footer_parts: list[str] = []
+ if run_url:
+ footer_parts.append(
+ f'View Workflow Run'
+ )
+ if has_errors:
+ footer_parts.append(
+ ''
+ 'Please fix the parameter mapping issues at your earliest convenience. '
+ )
+ footer_parts.append(
+ ''
+ 'Best regards, Your Automation Team '
+ )
+ parts.append(
+ f'| '
+ f'{"".join(footer_parts)} | '
+ )
+
+ # --- Close wrapper ---
+ parts.append(" |
")
+ return "".join(parts)
+
+
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
@@ -379,6 +619,23 @@ def main() -> int:
type=Path,
help="Write results as JSON to the given file path.",
)
+ parser.add_argument(
+ "--html-output",
+ type=Path,
+ help="Write a structured HTML email report to the given file path.",
+ )
+ parser.add_argument(
+ "--accelerator-name",
+ type=str,
+ default="",
+ help="Accelerator display name for the HTML report header.",
+ )
+ parser.add_argument(
+ "--run-url",
+ type=str,
+ default="",
+ help="Workflow run URL to include in the HTML report footer.",
+ )
args = parser.parse_args()
results: list[ValidationResult] = []
@@ -415,6 +672,19 @@ def main() -> int:
)
print(f"\nJSON report written to {args.json_output}")
+ # Optional HTML email report
+ if args.html_output:
+ scan_dir = str(args.dir) if args.dir else ""
+ html = generate_html_report(
+ results,
+ accelerator_name=args.accelerator_name,
+ run_url=args.run_url,
+ scan_dir=scan_dir,
+ )
+ args.html_output.parent.mkdir(parents=True, exist_ok=True)
+ args.html_output.write_text(html, encoding="utf-8")
+ print(f"HTML report written to {args.html_output}")
+
has_errors = any(r.has_errors for r in results)
return 1 if args.strict and has_errors else 0