Skip to content

Commit c13dc35

Browse files
2 parents 670f5ac + 08e30c9 commit c13dc35

9 files changed

Lines changed: 503 additions & 36 deletions

File tree

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

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

6674
- name: Send schedule notification on failure
6775
if: github.event_name == 'schedule' && steps.result.outputs.status == 'failure'
6876
env:
6977
LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}
70-
GITHUB_REPOSITORY: ${{ github.repository }}
71-
GITHUB_RUN_ID: ${{ github.run_id }}
7278
ACCELERATOR_NAME: ${{ env.accelerator_name }}
7379
run: |
74-
RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
75-
INFRA_OUTPUT=$(sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g' infra_output.txt)
80+
EMAIL_BODY=$(cat email_body.html)
7681
7782
jq -n \
7883
--arg name "${ACCELERATOR_NAME}" \
79-
--arg infra "$INFRA_OUTPUT" \
80-
--arg url "$RUN_URL" \
81-
'{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>")}' \
84+
--arg body "$EMAIL_BODY" \
85+
'{subject: ("Bicep Parameter Validation Report - " + $name + " - Issues Detected"), body: $body}' \
8286
| curl -X POST "${LOGICAPP_URL}" \
8387
-H "Content-Type: application/json" \
8488
-d @- || echo "Failed to send notification"
@@ -87,18 +91,14 @@ jobs:
8791
if: github.event_name == 'schedule' && steps.result.outputs.status == 'success'
8892
env:
8993
LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}
90-
GITHUB_REPOSITORY: ${{ github.repository }}
91-
GITHUB_RUN_ID: ${{ github.run_id }}
9294
ACCELERATOR_NAME: ${{ env.accelerator_name }}
9395
run: |
94-
RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
95-
INFRA_OUTPUT=$(sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g' infra_output.txt)
96+
EMAIL_BODY=$(cat email_body.html)
9697
9798
jq -n \
9899
--arg name "${ACCELERATOR_NAME}" \
99-
--arg infra "$INFRA_OUTPUT" \
100-
--arg url "$RUN_URL" \
101-
'{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>")}' \
100+
--arg body "$EMAIL_BODY" \
101+
'{subject: ("Bicep Parameter Validation Report - " + $name + " - Passed"), body: $body}' \
102102
| curl -X POST "${LOGICAPP_URL}" \
103103
-H "Content-Type: application/json" \
104104
-d @- || echo "Failed to send notification"

infra/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
@@ -341,6 +342,241 @@ def print_report(results: list[ValidationResult], *, use_color: bool = True) ->
341342
print(f"{c['ERROR']}Parameter mapping issues detected!{c['RESET']}")
342343

343344

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

384637
results: list[ValidationResult] = []
@@ -415,6 +668,19 @@ def main() -> int:
415668
)
416669
print(f"\nJSON report written to {args.json_output}")
417670

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

src/App/src/App.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
import React from 'react';
1+
import React, { useEffect } from 'react';
22
import './App.css';
33
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
44
import { HomePage, PlanPage } from './pages';
55
import { useWebSocket } from './hooks/useWebSocket';
6+
import { useAppDispatch } from './store/hooks';
7+
import { fetchCurrentUser } from './store/slices/appSlice';
68

79
function App() {
810
useWebSocket();
11+
const dispatch = useAppDispatch();
12+
13+
useEffect(() => {
14+
dispatch(fetchCurrentUser());
15+
}, [dispatch]);
916

1017
return (
1118
<Router>

src/App/src/commonComponents/components/Panels/PanelFooter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const PanelFooter: React.FC<{ children: React.ReactNode }> = ({ children }) => {
44
return (
55
<div
66
style={{
7-
padding: "24px 16px",
7+
padding: "24px 8px",
88
width:'100%'
99
}}
1010
>

0 commit comments

Comments
 (0)