Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
aa0401d
Format changes
NirajC-Microsoft May 6, 2026
e47b85c
Migrate Content Understanding from preview to GA and consolidate AI S…
Harsh-Microsoft May 6, 2026
cb2834f
Fix root-relative links in CustomizingAzdParameters.md
Harsh-Microsoft May 6, 2026
9d92b1e
Merge remote-tracking branch 'origin/dev' into psl-hb-us-41641
Harsh-Microsoft May 7, 2026
e5ba300
docs: add region requirement note for re-use Foundry project flow
Harsh-Microsoft May 7, 2026
9524e7b
docs: clarify region-mismatch consequence in re-use Foundry note
Harsh-Microsoft May 7, 2026
980b413
docs: fix root-relative DeploymentGuide.md links in re-use-foundry-pr…
Harsh-Microsoft May 7, 2026
0f069ac
Correct supported regions list in re-use-foundry-project.md
Harsh-Microsoft May 7, 2026
ab1cccb
Use Field(default_factory=list) for CU response collections
Harsh-Microsoft May 7, 2026
8aa9a61
Use Field(default_factory=list) for Page.lines and Page.paragraphs
Harsh-Microsoft May 7, 2026
483ec48
Update email format 4
NirajC-Microsoft May 7, 2026
db79814
Resolve Copilot Comment
NirajC-Microsoft May 7, 2026
259610e
Merge pull request #576 from NirajC-Microsoft/dev
Roopan-Microsoft May 8, 2026
1fbb362
Merge pull request #575 from microsoft/psl-hb-us-41641
Harsh-Microsoft May 8, 2026
9c3befd
Merge pull request #590 from microsoft/main
Roopan-Microsoft May 14, 2026
bd4e5f5
Fix: Collection dropdown chevron overlapping Import Document(s) butto…
Shubhangi-Microsoft May 14, 2026
4d6ee48
fix: use flex-wrap wrap for graceful wrapping at zoom >100%
Shubhangi-Microsoft May 14, 2026
d6066e3
Merge pull request #591 from microsoft/psl-bug43321
Prajwal-Microsoft May 15, 2026
8fec046
Merge remote-tracking branch 'origin/main' into prdc-conflict-resolve
Prajwal-Microsoft May 19, 2026
710c300
Merge pull request #594 from microsoft/prdc-conflict-resolve
Prajwal-Microsoft May 19, 2026
b96ad2a
chore: resolve Copilot review comments from PR #579
Shreyas-Microsoft May 20, 2026
57774f2
Merge pull request #598 from microsoft/psl-sw/pr579-review-fixes
Prajwal-Microsoft May 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions .github/workflows/validate-bicep-params.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
Shreyas-Microsoft marked this conversation as resolved.
EXIT_CODE=${PIPESTATUS[0]}
set -e
echo "## Infra Param Validation" >> "$GITHUB_STEP_SUMMARY"
Expand All @@ -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/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g' infra_output.txt)
if [ ! -f email_body.html ]; then
echo "<p>Email body was not generated. Please check the workflow logs.</p>" > 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: ("<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>")}' \
--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"
Expand All @@ -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/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g' infra_output.txt)
if [ ! -f email_body.html ]; then
echo "<p>Email body was not generated. Please check the workflow logs.</p>" > 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: ("<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>")}' \
--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"
Expand Down
270 changes: 270 additions & 0 deletions infra/scripts/validate_bicep_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
)
Comment thread
Shreyas-Microsoft marked this conversation as resolved.
Outdated


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 = "&#10060;" if has_errors else "&#9989;"

parts: list[str] = []

# --- Document wrapper (Outlook-compatible, no gradient/border-radius/box-shadow) ---
parts.append(
'<!DOCTYPE html><html><head><meta charset="utf-8"></head>'
'<body style="margin:0;padding:0;font-family:Segoe UI,Helvetica,Arial,sans-serif;'
'background-color:#ffffff;">'
'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"'
' style="background-color:#ffffff;">'
'<tr><td align="center" style="padding:0;">'
'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"'
' style="max-width:680px;background-color:#ffffff;">'
)

# --- Header banner (solid color, Outlook-safe) ---
parts.append(
f'<tr><td style="background-color:#0078D4;padding:20px 24px;color:#ffffff;">'
f'<h1 style="margin:0 0 4px 0;font-size:20px;font-weight:600;color:#ffffff;">'
f'Bicep Parameter Validation Report</h1>'
f'<p style="margin:0;font-size:13px;color:#ffffff;">'
f'{_html_escape(accelerator_name) if accelerator_name else "Accelerator"}'
f' &mdash; Automated Check</p>'
f'</td></tr>'
)

# --- Summary card ---
parts.append(
f'<tr><td style="padding:16px 24px 12px 24px;">'
f'<table role="presentation" width="100%" cellpadding="0" cellspacing="0"'
f' style="background-color:{status_bg};border-left:4px solid {status_color};">'
f'<tr><td style="padding:12px 16px;">'
f'<span style="font-size:16px;font-weight:600;color:{status_color};">'
f'{status_icon} Overall Status: {overall_status}</span>'
f'</td></tr>'
f'<tr><td style="padding:4px 16px 12px 16px;">'
f'<table role="presentation" cellpadding="0" cellspacing="0"><tr>'
)
# Accelerator name pill
if accelerator_name:
parts.append(
f'<td style="padding-right:20px;vertical-align:top;">'
f'<span style="font-size:11px;color:#666;">Accelerator</span><br>'
f'<strong style="font-size:13px;">{_html_escape(accelerator_name)}'
f'</strong></td>'
)
# Scan directory pill
if scan_dir:
parts.append(
f'<td style="padding-right:20px;vertical-align:top;">'
f'<span style="font-size:11px;color:#666;">Scan Directory</span><br>'
f'<strong style="font-size:13px;">{_html_escape(scan_dir)}/</strong>'
f'</td>'
)
# Error count pill
err_pill_color = "#D32F2F" if total_errors > 0 else "#2E7D32"
parts.append(
f'<td style="padding-right:20px;vertical-align:top;">'
f'<span style="font-size:11px;color:#666;">Errors</span><br>'
f'<strong style="font-size:13px;color:{err_pill_color};">'
f'{total_errors}</strong></td>'
)
# Warning count pill
warn_pill_color = "#F57C00" if total_warnings > 0 else "#2E7D32"
parts.append(
f'<td style="vertical-align:top;">'
f'<span style="font-size:11px;color:#666;">Warnings</span><br>'
f'<strong style="font-size:13px;color:{warn_pill_color};">'
f'{total_warnings}</strong></td>'
)
parts.append("</tr></table></td></tr></table></td></tr>")

# --- Per-pair detail sections ---
parts.append('<tr><td style="padding:8px 24px 0 24px;">')
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 = (
'<span style="display:inline-block;padding:2px 8px;'
'font-size:11px;font-weight:700;'
'color:#2E7D32;background-color:#E8F5E9;">PASS</span>'
)
elif errors:
badge = (
'<span style="display:inline-block;padding:2px 8px;'
'font-size:11px;font-weight:700;'
'color:#D32F2F;background-color:#FFEBEE;">FAIL</span>'
)
else:
badge = (
'<span style="display:inline-block;padding:2px 8px;'
'font-size:11px;font-weight:700;'
'color:#F57C00;background-color:#FFF3E0;">WARN</span>'
)

parts.append(
f'<table role="presentation" width="100%" cellpadding="0"'
f' cellspacing="0" style="margin-bottom:12px;border:1px solid #e0e0e0;">'
f'<tr><td style="background-color:#fafafa;padding:10px 12px;'
f'border-bottom:1px solid #e0e0e0;">'
f'{badge} '
f'<strong style="font-size:13px;">'
f'{_html_escape(r.pair)}</strong>'
f'<span style="float:right;font-size:11px;color:#888;">'
f'{len(errors)} error(s), {len(warnings)} warning(s)</span>'
f'</td></tr>'
)

if r.issues:
# --- Errors section ---
if errors:
parts.append(
'<tr><td style="padding:8px 12px 4px 12px;">'
'<strong style="font-size:12px;color:#D32F2F;">'
'&#9679; Errors</strong></td></tr>'
'<tr><td style="padding:0 12px;">'
'<table role="presentation" width="100%" cellpadding="0"'
' cellspacing="0" style="font-size:12px;border:1px solid #f5c6cb;">'
'<tr style="background-color:#FFEBEE;">'
'<th style="text-align:left;padding:6px 10px;'
'border-bottom:1px solid #f5c6cb;width:180px;">Parameter</th>'
'<th style="text-align:left;padding:6px 10px;'
'border-bottom:1px solid #f5c6cb;">Details</th></tr>'
)
for idx, issue in enumerate(errors):
bg = "#ffffff" if idx % 2 == 0 else "#fff5f5"
parts.append(
f'<tr style="background-color:{bg};">'
f'<td style="padding:5px 10px;border-bottom:1px solid #f5c6cb;'
f'vertical-align:top;font-family:Consolas,monospace;'
f'font-size:11px;word-break:break-all;">'
f'{_html_escape(issue.param_name)}</td>'
f'<td style="padding:5px 10px;border-bottom:1px solid #f5c6cb;'
f'vertical-align:top;">{_html_escape(issue.message)}</td>'
f'</tr>'
)
parts.append("</table></td></tr>")

# --- Warnings section ---
if warnings:
parts.append(
'<tr><td style="padding:8px 12px 4px 12px;">'
'<strong style="font-size:12px;color:#F57C00;">'
'&#9679; Warnings</strong></td></tr>'
'<tr><td style="padding:0 12px 8px 12px;">'
'<table role="presentation" width="100%" cellpadding="0"'
' cellspacing="0" style="font-size:12px;border:1px solid #ffe0b2;">'
'<tr style="background-color:#FFF3E0;">'
'<th style="text-align:left;padding:6px 10px;'
'border-bottom:1px solid #ffe0b2;width:180px;">Parameter</th>'
'<th style="text-align:left;padding:6px 10px;'
'border-bottom:1px solid #ffe0b2;">Details</th></tr>'
)
for idx, issue in enumerate(warnings):
bg = "#ffffff" if idx % 2 == 0 else "#fffaf0"
parts.append(
f'<tr style="background-color:{bg};">'
f'<td style="padding:5px 10px;border-bottom:1px solid #ffe0b2;'
f'vertical-align:top;font-family:Consolas,monospace;'
f'font-size:11px;word-break:break-all;">'
f'{_html_escape(issue.param_name)}</td>'
f'<td style="padding:5px 10px;border-bottom:1px solid #ffe0b2;'
f'vertical-align:top;">{_html_escape(issue.message)}</td>'
f'</tr>'
)
parts.append("</table></td></tr>")
else:
parts.append(
'<tr><td style="padding:10px 12px;color:#2E7D32;'
'font-size:12px;">All parameters validated successfully.'
'</td></tr>'
)

parts.append("</table>")

parts.append("</td></tr>")

# --- Footer with run URL ---
footer_parts: list[str] = []
if run_url:
footer_parts.append(
f'<a href="{_html_escape(run_url)}" style="display:inline-block;'
f'padding:8px 16px;background-color:#0078D4;color:#ffffff;'
f'text-decoration:none;font-size:12px;'
f'font-weight:600;">View Workflow Run</a>'
)
if has_errors:
footer_parts.append(
'<p style="margin:10px 0 0 0;font-size:12px;color:#555;">'
'Please fix the parameter mapping issues at your earliest convenience.</p>'
)
footer_parts.append(
'<p style="margin:10px 0 0 0;font-size:12px;color:#999;">'
'Best regards,<br>Your Automation Team</p>'
)
parts.append(
f'<tr><td style="padding:14px 24px 20px 24px;border-top:1px solid #e0e0e0;">'
f'{"".join(footer_parts)}</td></tr>'
)

# --- Close wrapper ---
parts.append("</table></td></tr></table></body></html>")
return "".join(parts)


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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] = []
Expand Down Expand Up @@ -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

Expand Down
Loading