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' 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' 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'' + ) + + # --- Summary card --- + parts.append( + f'") + + # --- Per-pair detail sections --- + 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'' + ) + + # --- Close wrapper --- + parts.append("
' + f'

' + f'Bicep Parameter Validation Report

' + f'

' + f'{_html_escape(accelerator_name) if accelerator_name else "Accelerator"}' + f' — Automated Check

' + f'
' + f'' + f'' + f'
' + f'' + f'{status_icon} Overall Status: {overall_status}' + f'
' + f'' + ) + # Accelerator name pill + if accelerator_name: + parts.append( + f'' + ) + # Scan directory pill + if scan_dir: + parts.append( + f'' + ) + # Error count pill + err_pill_color = "#D32F2F" if total_errors > 0 else "#2E7D32" + parts.append( + f'' + ) + # Warning count pill + warn_pill_color = "#F57C00" if total_warnings > 0 else "#2E7D32" + parts.append( + f'' + ) + parts.append("
' + f'Accelerator
' + f'{_html_escape(accelerator_name)}' + f'
' + f'Scan Directory
' + f'{_html_escape(scan_dir)}/' + f'
' + f'Errors
' + f'' + f'{total_errors}
' + f'Warnings
' + f'' + f'{total_warnings}
') + 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'' + ) + + if r.issues: + # --- Errors section --- + if errors: + parts.append( + '' + '") + + # --- Warnings section --- + if warnings: + parts.append( + '' + '") + else: + parts.append( + '' + ) + + parts.append("
' + f'{badge} ' + f'' + f'{_html_escape(r.pair)}' + f'' + f'{len(errors)} error(s), {len(warnings)} warning(s)' + f'
' + '' + '● Errors
' + '' + '' + '' + '' + ) + for idx, issue in enumerate(errors): + bg = "#ffffff" if idx % 2 == 0 else "#fff5f5" + parts.append( + f'' + f'' + f'' + f'' + ) + parts.append("
ParameterDetails
' + f'{_html_escape(issue.param_name)}{_html_escape(issue.message)}
' + '' + '● Warnings
' + '' + '' + '' + '' + ) + for idx, issue in enumerate(warnings): + bg = "#ffffff" if idx % 2 == 0 else "#fffaf0" + parts.append( + f'' + f'' + f'' + f'' + ) + parts.append("
ParameterDetails
' + f'{_html_escape(issue.param_name)}{_html_escape(issue.message)}
All parameters validated successfully.' + '
") + + parts.append("
' + f'{"".join(footer_parts)}
") + 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