diff --git a/evals/azure-validate/e2e-eval.yaml b/evals/azure-validate/e2e-eval.yaml
index 5f496e9bf..50142c0cd 100644
--- a/evals/azure-validate/e2e-eval.yaml
+++ b/evals/azure-validate/e2e-eval.yaml
@@ -178,6 +178,53 @@ stimuli:
- name: "(?i)^(bash|powershell|pwsh)$"
command: "(?i)azd provision --no-prompt"
+ # Jest: "runs the validate-deployment script for a standalone Bicep (az CLI) project"
+ # No azure.yaml → azure-prepare selects the Bicep/AZCLI recipe, whose validation is the
+ # shared validate-deployment.{sh,ps1} helper. The agent must RUN that script (not azd,
+ # not raw az deployment create).
+ - name: "Runs validate-deployment Script - Standalone Bicep (az CLI)"
+ prompt: "My static status page is ready. I deploy it directly with the Azure CLI (az deployment) using my Bicep templates in ./infra — I do NOT use azd. Please prepare and validate it for deployment to my current subscription in eastus2."
+ environment:
+ files:
+ - src: fixture/bicep-cli/index.html
+ dest: index.html
+ - src: fixture/bicep-cli/infra/main.bicep
+ dest: infra/main.bicep
+ - src: fixture/bicep-cli/infra/resources.bicep
+ dest: infra/resources.bicep
+ - src: fixture/bicep-cli/infra/main.parameters.json
+ dest: infra/main.parameters.json
+ constraints:
+ max_turns: 50
+ tags:
+ type: integration
+ tier: full
+ cost: llm
+ area: behavior
+ skill: azure-validate
+ followUp:
+ - "Continue with recommended options until complete."
+ earlyTerminate: '[{"type":"skill-call","skill":"azure-deploy"},{"type":"tool-call-match","toolPattern":"bash|powershell|pwsh|run_in_terminal","argsPattern":"validate-deployment\\.(sh|ps1)"},{"type":"tool-call-match","toolPattern":"bash|powershell|pwsh|run_in_terminal","argsPattern":"az\\s+deployment\\b.*\\b(create|up)\\b"}]'
+ graders:
+ # azure-validate WAS invoked; azure-deploy was NOT
+ - type: skill-invocation
+ config:
+ required:
+ - azure-validate
+ disallowed:
+ - azure-deploy
+ # The shared validate-deployment script WAS run; no actual deployment happened
+ - type: tool-calls
+ config:
+ required:
+ - name: "(?i)^(bash|powershell|pwsh)$"
+ command: "(?i)validate-deployment\\.(sh|ps1)"
+ disallowed:
+ - name: "(?i)^(bash|powershell|pwsh)$"
+ command: "(?i)azd\\s+(up|deploy)\\b"
+ - name: "(?i)^(bash|powershell|pwsh)$"
+ command: "(?i)az\\s+deployment\\b.*\\bcreate\\b"
+
# ═══════════════════════════════════════════
# brownfield-dotnet-validate
# Source: describe("brownfield-dotnet-validate")
diff --git a/evals/azure-validate/fixture/bicep-cli/index.html b/evals/azure-validate/fixture/bicep-cli/index.html
new file mode 100644
index 000000000..51394e1b4
--- /dev/null
+++ b/evals/azure-validate/fixture/bicep-cli/index.html
@@ -0,0 +1,15 @@
+
+
+
Status Page
+
+ Service Status
+
+
+
diff --git a/evals/azure-validate/fixture/bicep-cli/infra/main.bicep b/evals/azure-validate/fixture/bicep-cli/infra/main.bicep
new file mode 100644
index 000000000..50db4acd2
--- /dev/null
+++ b/evals/azure-validate/fixture/bicep-cli/infra/main.bicep
@@ -0,0 +1,32 @@
+targetScope = 'subscription'
+
+@minLength(1)
+@maxLength(64)
+@description('Name of the environment that can be used as part of naming resource convention')
+param environmentName string
+
+@minLength(1)
+@description('Primary location for all resources')
+param location string
+
+var tags = { 'azd-env-name': environmentName }
+var resourceToken = toLower(uniqueString(subscription().id, environmentName, location))
+
+resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = {
+ name: 'rg-${environmentName}'
+ location: location
+ tags: tags
+}
+
+module web './resources.bicep' = {
+ name: 'web'
+ scope: rg
+ params: {
+ location: location
+ tags: tags
+ resourceToken: resourceToken
+ }
+}
+
+output AZURE_LOCATION string = location
+output AZURE_TENANT_ID string = tenant().tenantId
diff --git a/evals/azure-validate/fixture/bicep-cli/infra/main.parameters.json b/evals/azure-validate/fixture/bicep-cli/infra/main.parameters.json
new file mode 100644
index 000000000..071681447
--- /dev/null
+++ b/evals/azure-validate/fixture/bicep-cli/infra/main.parameters.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "environmentName": {
+ "value": "statuspage"
+ },
+ "location": {
+ "value": "eastus2"
+ }
+ }
+}
diff --git a/evals/azure-validate/fixture/bicep-cli/infra/resources.bicep b/evals/azure-validate/fixture/bicep-cli/infra/resources.bicep
new file mode 100644
index 000000000..2f2de33e6
--- /dev/null
+++ b/evals/azure-validate/fixture/bicep-cli/infra/resources.bicep
@@ -0,0 +1,18 @@
+@description('Location for all resources')
+param location string = resourceGroup().location
+
+param tags object = {}
+param resourceToken string
+
+resource staticWebApp 'Microsoft.Web/staticSites@2022-09-01' = {
+ name: 'swa-${resourceToken}'
+ location: location
+ tags: union(tags, { 'azd-service-name': 'web' })
+ sku: {
+ name: 'Free'
+ tier: 'Free'
+ }
+ properties: {}
+}
+
+output STATIC_WEB_APP_URL string = staticWebApp.properties.defaultHostname
diff --git a/plugin/skills/azure-validate/references/recipes/azcli/README.md b/plugin/skills/azure-validate/references/recipes/azcli/README.md
index b2553f381..e6f198f5b 100644
--- a/plugin/skills/azure-validate/references/recipes/azcli/README.md
+++ b/plugin/skills/azure-validate/references/recipes/azcli/README.md
@@ -9,84 +9,54 @@ Validation steps for Azure CLI deployments.
## Validation Steps
-- [ ] 1. Azure CLI Installation
-- [ ] 2. Authentication
-- [ ] 3. Bicep Compilation
-- [ ] 4. Template Validation
-- [ ] 5. What-If Preview
-- [ ] 6. Docker Build (if containerized)
-- [ ] 7. Azure Policy Validation
+- [ ] 1. Core Validation (CLI, auth, build, validate, what-if) — run [`validate-deployment` script](../scripts/validate-deployment.sh)
+- [ ] 2. Docker Build (if containerized)
+- [ ] 3. Azure Policy Validation
## Validation Details
-### 1. Azure CLI Installation
+### 1. Core Validation Script
-Verify Azure CLI is installed:
+The core validation checks are a fixed, deterministic sequence. Run the shared
+**validate-deployment** helper instead of executing and parsing each command by hand. It
+confirms the Azure CLI is installed and authenticated, compiles the Bicep template
+(`az bicep build`), validates it against the target scope (`az deployment ... validate`),
+and runs a what-if preview — printing a compact PASS/FAIL summary plus a what-if change
+count (Create/Modify/Delete).
-```bash
-az version
-```
-
-**If not installed:**
-```
-mcp_azure_mcp_extension_cli_install(cli-type: "az")
-```
-
-### 2. Authentication
+- Bash: [`../scripts/validate-deployment.sh`](../scripts/validate-deployment.sh)
+- PowerShell: [`../scripts/validate-deployment.ps1`](../scripts/validate-deployment.ps1)
-```bash
-az account show
-```
+**Subscription scope:**
-**If not logged in:**
```bash
-az login
+../scripts/validate-deployment.sh --scope sub --location
```
-
-**Set subscription:**
-```bash
-az account set --subscription
+```powershell
+../scripts/validate-deployment.ps1 -Scope sub -Location
```
-### 3. Bicep Compilation
+**Resource group scope:**
```bash
-az bicep build --file ./infra/main.bicep
+../scripts/validate-deployment.sh --scope group --resource-group
```
-
-### 4. Template Validation
-
-```bash
-# Subscription scope
-az deployment sub validate \
- --location \
- --template-file ./infra/main.bicep \
- --parameters ./infra/main.parameters.json
-
-# Resource group scope
-az deployment group validate \
- --resource-group \
- --template-file ./infra/main.bicep \
- --parameters ./infra/main.parameters.json
+```powershell
+../scripts/validate-deployment.ps1 -Scope group -ResourceGroup
```
-### 5. What-If Preview
+Defaults: `--template ./infra/main.bicep`, `--parameters ./infra/main.parameters.json`
+(skipped if absent). Pass `--subscription ` to target a specific subscription.
-```bash
-# Subscription scope
-az deployment sub what-if \
- --location \
- --template-file ./infra/main.bicep \
- --parameters ./infra/main.parameters.json
-
-# Resource group scope
-az deployment group what-if \
- --resource-group \
- --template-file ./infra/main.bicep \
- --parameters ./infra/main.parameters.json
-```
+**Interpreting results:**
+
+- `OVERALL: PASS` — all five checks passed; record the summary in Section 7 (Validation Proof).
+- Any step `FAIL` — the script prints the failing command's error. Remediate:
+ - **Authenticated** fails → `az login`, then `az account set --subscription `.
+ - **Azure CLI installed** fails → install via `mcp_azure_mcp_extension_cli_install(cli-type: "az")`.
+ - Otherwise see [Error handling](./errors.md).
-### 6. Docker Build (if containerized)
+### 2. Docker Build (if containerized)
**Before building**, validate the Docker build context:
@@ -105,7 +75,7 @@ npm install --package-lock-only
docker build -t :test ./src/
```
-### 7. Azure Policy Validation
+### 3. Azure Policy Validation
See [Policy Validation Guide](../../policy-validation.md) for instructions on retrieving and validating Azure policies for your subscription.
diff --git a/plugin/skills/azure-validate/references/recipes/bicep/README.md b/plugin/skills/azure-validate/references/recipes/bicep/README.md
index 3789938ed..7d28edbb8 100644
--- a/plugin/skills/azure-validate/references/recipes/bicep/README.md
+++ b/plugin/skills/azure-validate/references/recipes/bicep/README.md
@@ -10,64 +10,52 @@ Validation steps for standalone Bicep deployments.
## Validation Steps
-- [ ] 1. Bicep Compilation
-- [ ] 2. Template Validation
-- [ ] 3. What-If Preview
-- [ ] 4. Authentication
-- [ ] 5. Linting (optional)
-- [ ] 6. Azure Policy Validation
+- [ ] 1. Core Validation (CLI, auth, build, validate, what-if) — run [`validate-deployment` script](../scripts/validate-deployment.sh)
+- [ ] 2. Linting (optional)
+- [ ] 3. Azure Policy Validation
## Validation Details
-### 1. Bicep Compilation
+### 1. Core Validation Script
-```bash
-az bicep build --file ./infra/main.bicep
-```
+The core validation checks are a fixed, deterministic sequence — identical to the AZCLI
+recipe. Run the shared **validate-deployment** helper instead of executing and parsing each
+command by hand. It confirms the Azure CLI is installed and authenticated, compiles the Bicep
+template (`az bicep build`), validates it against the target scope (`az deployment ...
+validate`), and runs a what-if preview — printing a compact PASS/FAIL summary plus a what-if
+change count (Create/Modify/Delete).
-**Pass:** No output (compiles cleanly)
-**Fail:** Shows line numbers and errors
+- Bash: [`../scripts/validate-deployment.sh`](../scripts/validate-deployment.sh)
+- PowerShell: [`../scripts/validate-deployment.ps1`](../scripts/validate-deployment.ps1)
-### 2. Template Validation
+**Subscription scope:**
```bash
-# Subscription scope
-az deployment sub validate \
- --location \
- --template-file ./infra/main.bicep \
- --parameters ./infra/main.parameters.json
-
-# Resource group scope
-az deployment group validate \
- --resource-group \
- --template-file ./infra/main.bicep \
- --parameters ./infra/main.parameters.json
+../scripts/validate-deployment.sh --scope sub --location
+```
+```powershell
+../scripts/validate-deployment.ps1 -Scope sub -Location
```
-### 3. What-If Preview
+**Resource group scope:**
```bash
-az deployment sub what-if \
- --location \
- --template-file ./infra/main.bicep \
- --parameters ./infra/main.parameters.json
-```
-
-**Expected output:**
+../scripts/validate-deployment.sh --scope group --resource-group
```
-Resource and property changes are indicated with these symbols:
- + Create
- ~ Modify
- - Delete
+```powershell
+../scripts/validate-deployment.ps1 -Scope group -ResourceGroup
```
-### 4. Authentication
+Defaults: `--template ./infra/main.bicep`, `--parameters ./infra/main.parameters.json`
+(skipped if absent). Pass `--subscription ` to target a specific subscription.
-```bash
-az account show
-```
+**Interpreting results:**
+
+- `OVERALL: PASS` — all checks passed; record the summary in Section 7 (Validation Proof).
+- Any step `FAIL` — the script prints the failing command's error. If **Authenticated** fails,
+ run `az login`. Otherwise see [Error handling](./errors.md).
-### 5. Linting (optional)
+### 2. Linting (optional)
Use Bicep linter rules:
@@ -75,7 +63,7 @@ Use Bicep linter rules:
az bicep lint --file ./infra/main.bicep
```
-### 6. Azure Policy Validation
+### 3. Azure Policy Validation
See [Policy Validation Guide](../../policy-validation.md) for instructions on retrieving and validating Azure policies for your subscription.
@@ -83,10 +71,7 @@ See [Policy Validation Guide](../../policy-validation.md) for instructions on re
| Check | Command | Pass |
|-------|---------|------|
-| Bicep compiles | `az bicep build` | ☐ |
-| Template valid | `az deployment validate` | ☐ |
-| What-if passes | `az deployment what-if` | ☐ |
-| Auth valid | `az account show` | ☐ |
+| Core validation (CLI, auth, build, validate, what-if) | `validate-deployment` script | ☐ |
| Policies validated | MCP Policy tool | ☐ |
## References
diff --git a/plugin/skills/azure-validate/references/recipes/scripts/validate-deployment.ps1 b/plugin/skills/azure-validate/references/recipes/scripts/validate-deployment.ps1
new file mode 100644
index 000000000..f91cbe7ca
--- /dev/null
+++ b/plugin/skills/azure-validate/references/recipes/scripts/validate-deployment.ps1
@@ -0,0 +1,155 @@
+<#
+.SYNOPSIS
+ Runs the standard Azure CLI pre-deployment validation sequence for a Bicep
+ template and reports PASS/FAIL for each step. Shared by the AZCLI and Bicep
+ validation recipes.
+.DESCRIPTION
+ Executes, in order:
+ 1. az version - Azure CLI is installed
+ 2. az account show - authenticated to Azure
+ 3. az bicep build - template compiles cleanly
+ 4. az deployment ... validate - template validates against the target scope
+ 5. az deployment ... what-if - preview changes (Create/Modify/Delete summary)
+ Emits per-step PASS/FAIL lines and an OVERALL result.
+ Exit codes: 0 = every step passed; 1 = a validation step failed;
+ 2 = usage / argument error.
+.PARAMETER Scope
+ Deployment scope: 'sub' or 'group' (required).
+.PARAMETER Location
+ Location (required when -Scope sub).
+.PARAMETER ResourceGroup
+ Resource group name (required when -Scope group).
+.PARAMETER Template
+ Bicep template path. Default: ./infra/main.bicep
+.PARAMETER Parameters
+ Parameters file path. Default: ./infra/main.parameters.json
+ (skipped automatically if the file does not exist).
+.PARAMETER Subscription
+ Subscription to target (optional).
+.EXAMPLE
+ .\validate-deployment.ps1 -Scope sub -Location eastus
+.EXAMPLE
+ .\validate-deployment.ps1 -Scope group -ResourceGroup my-rg `
+ -Template ./infra/main.bicep -Parameters ./infra/main.parameters.json
+#>
+param(
+ [ValidateSet("sub", "group")][string]$Scope,
+ [string]$Location,
+ [string]$ResourceGroup,
+ [string]$Template = "./infra/main.bicep",
+ [string]$Parameters = "./infra/main.parameters.json",
+ [string]$Subscription
+)
+
+# Validate arguments
+if (-not $Scope) {
+ Write-Error "-Scope is required and must be 'sub' or 'group'."
+ exit 2
+}
+if ($Scope -eq "sub" -and -not $Location) {
+ Write-Error "-Location is required when -Scope is 'sub'."
+ exit 2
+}
+if ($Scope -eq "group" -and -not $ResourceGroup) {
+ Write-Error "-ResourceGroup is required when -Scope is 'group'."
+ exit 2
+}
+
+# Build shared argument arrays
+$subArgs = @()
+if ($Subscription) { $subArgs = @("--subscription", $Subscription) }
+
+$paramArgs = @()
+if (Test-Path $Parameters) {
+ $paramArgs = @("--parameters", $Parameters)
+} else {
+ Write-Host "NOTE: parameters file '$Parameters' not found; validating without --parameters."
+}
+
+if ($Scope -eq "sub") {
+ $scopeTargetArgs = @("--location", $Location)
+ $scopeDesc = "subscription (location: $Location)"
+} else {
+ $scopeTargetArgs = @("--resource-group", $ResourceGroup)
+ $scopeDesc = "resource group '$ResourceGroup'"
+}
+
+# Track overall result (0 = all passed, 1 = at least one failure).
+$overall = 0
+
+Write-Host "=== Azure deployment validation ==="
+Write-Host "Template: $Template"
+Write-Host "Scope: $scopeDesc"
+Write-Host ""
+
+# Step 1: Azure CLI installed
+Write-Host "--- Step 1: Azure CLI installed (az version) ---"
+az version *> $null
+if ($LASTEXITCODE -eq 0) {
+ Write-Host "PASS: Azure CLI is installed."
+} else {
+ Write-Host "FAIL: Azure CLI not found. Install it, then re-run."
+ Write-Host ""
+ Write-Host "OVERALL: FAIL"
+ exit 1
+}
+Write-Host ""
+
+# Step 2: Authenticated
+Write-Host "--- Step 2: Authenticated (az account show) ---"
+$accountName = az account show @subArgs --query name -o tsv 2>$null
+if ($LASTEXITCODE -eq 0) {
+ Write-Host "PASS: Authenticated (subscription: $accountName)."
+} else {
+ Write-Host "FAIL: Not logged in. Run 'az login' (and 'az account set --subscription ')."
+ $overall = 1
+}
+Write-Host ""
+
+# Step 3: Bicep compilation
+Write-Host "--- Step 3: Bicep compilation (az bicep build) ---"
+$buildOutput = az bicep build --file $Template 2>&1
+if ($LASTEXITCODE -eq 0) {
+ Write-Host "PASS: Template compiles cleanly."
+} else {
+ Write-Host "FAIL: Bicep compilation errors:"
+ Write-Host ($buildOutput | Out-String)
+ $overall = 1
+}
+Write-Host ""
+
+# Step 4: Template validation
+Write-Host "--- Step 4: Template validation (az deployment $Scope validate) ---"
+$validateOutput = az deployment $Scope validate @scopeTargetArgs --template-file $Template @paramArgs @subArgs 2>&1
+if ($LASTEXITCODE -eq 0) {
+ Write-Host "PASS: Template validated against the target scope."
+} else {
+ Write-Host "FAIL: Template validation errors:"
+ Write-Host ($validateOutput | Out-String)
+ $overall = 1
+}
+Write-Host ""
+
+# Step 5: What-if preview
+Write-Host "--- Step 5: What-if preview (az deployment $Scope what-if) ---"
+$whatifOutput = az deployment $Scope what-if @scopeTargetArgs --template-file $Template @paramArgs @subArgs 2>&1
+if ($LASTEXITCODE -eq 0) {
+ $lines = $whatifOutput | Out-String -Stream
+ $createCount = ($lines | Where-Object { $_ -match '^\s*\+ ' }).Count
+ $modifyCount = ($lines | Where-Object { $_ -match '^\s*~ ' }).Count
+ $deleteCount = ($lines | Where-Object { $_ -match '^\s*- ' }).Count
+ Write-Host "PASS: What-if completed. Changes -> Create: $createCount, Modify: $modifyCount, Delete: $deleteCount"
+} else {
+ Write-Host "FAIL: What-if errors:"
+ Write-Host ($whatifOutput | Out-String)
+ $overall = 1
+}
+Write-Host ""
+
+# Overall result (drives the exit code)
+if ($overall -eq 0) {
+ Write-Host "OVERALL: PASS"
+} else {
+ Write-Host "OVERALL: FAIL"
+}
+exit $overall
diff --git a/plugin/skills/azure-validate/references/recipes/scripts/validate-deployment.sh b/plugin/skills/azure-validate/references/recipes/scripts/validate-deployment.sh
new file mode 100755
index 000000000..595406c9f
--- /dev/null
+++ b/plugin/skills/azure-validate/references/recipes/scripts/validate-deployment.sh
@@ -0,0 +1,184 @@
+#!/usr/bin/env bash
+# validate-deployment.sh
+# Runs the standard Azure CLI pre-deployment validation sequence for a Bicep
+# template and reports PASS/FAIL for each step. Shared by the AZCLI and Bicep
+# validation recipes.
+#
+# Steps (in order):
+# 1. az version - Azure CLI is installed
+# 2. az account show - authenticated to Azure
+# 3. az bicep build - template compiles cleanly
+# 4. az deployment ... validate - template validates against the target scope
+# 5. az deployment ... what-if - preview changes (with a Create/Modify/Delete summary)
+#
+# Usage:
+# ./validate-deployment.sh --scope sub --location [options]
+# ./validate-deployment.sh --scope group --resource-group [options]
+#
+# Options:
+# --scope Deployment scope (required)
+# --location Location (required when --scope sub)
+# --resource-group Resource group (required when --scope group)
+# --template Bicep template (default: ./infra/main.bicep)
+# --parameters Parameters file (default: ./infra/main.parameters.json;
+# skipped automatically if the file does not exist)
+# --subscription Subscription to target (optional)
+#
+# Examples:
+# ./validate-deployment.sh --scope sub --location eastus
+# ./validate-deployment.sh --scope group --resource-group my-rg \
+# --template ./infra/main.bicep --parameters ./infra/main.parameters.json
+#
+# Exit codes:
+# 0 - every validation step passed
+# 1 - a validation step failed
+# 2 - usage / argument error (unknown or valueless option, missing required flag)
+
+set -uo pipefail
+
+# Ensure an option that consumes a value actually has one ($@ = remaining args).
+need_val() {
+ [ "$#" -ge 2 ] || { echo "ERROR: $1 requires a value." >&2; exit 2; }
+}
+
+SCOPE=""
+LOCATION=""
+RESOURCE_GROUP=""
+TEMPLATE="./infra/main.bicep"
+PARAMETERS="./infra/main.parameters.json"
+SUBSCRIPTION=""
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --scope) need_val "$@"; SCOPE="$2"; shift 2 ;;
+ --location) need_val "$@"; LOCATION="$2"; shift 2 ;;
+ --resource-group) need_val "$@"; RESOURCE_GROUP="$2"; shift 2 ;;
+ --template) need_val "$@"; TEMPLATE="$2"; shift 2 ;;
+ --parameters) need_val "$@"; PARAMETERS="$2"; shift 2 ;;
+ --subscription) need_val "$@"; SUBSCRIPTION="$2"; shift 2 ;;
+ -h|--help)
+ grep '^#' "$0" | grep -v '^#!' | sed 's/^# \{0,1\}//'
+ exit 0 ;;
+ *)
+ echo "Unknown argument: $1" >&2
+ exit 2 ;;
+ esac
+done
+
+# Validate arguments
+if [ "$SCOPE" != "sub" ] && [ "$SCOPE" != "group" ]; then
+ echo "ERROR: --scope must be 'sub' or 'group'." >&2
+ exit 2
+fi
+if [ "$SCOPE" = "sub" ] && [ -z "$LOCATION" ]; then
+ echo "ERROR: --location is required when --scope is 'sub'." >&2
+ exit 2
+fi
+if [ "$SCOPE" = "group" ] && [ -z "$RESOURCE_GROUP" ]; then
+ echo "ERROR: --resource-group is required when --scope is 'group'." >&2
+ exit 2
+fi
+
+# Build shared argument arrays
+SUB_ARGS=()
+[ -n "$SUBSCRIPTION" ] && SUB_ARGS=(--subscription "$SUBSCRIPTION")
+
+PARAM_ARGS=()
+if [ -f "$PARAMETERS" ]; then
+ PARAM_ARGS=(--parameters "$PARAMETERS")
+else
+ echo "NOTE: parameters file '$PARAMETERS' not found; validating without --parameters."
+fi
+
+if [ "$SCOPE" = "sub" ]; then
+ SCOPE_TARGET_ARGS=(--location "$LOCATION")
+ SCOPE_DESC="subscription (location: $LOCATION)"
+else
+ SCOPE_TARGET_ARGS=(--resource-group "$RESOURCE_GROUP")
+ SCOPE_DESC="resource group '$RESOURCE_GROUP'"
+fi
+
+# Track overall result (0 = all passed, 1 = at least one failure).
+OVERALL=0
+
+echo "=== Azure deployment validation ==="
+echo "Template: $TEMPLATE"
+echo "Scope: $SCOPE_DESC"
+echo ""
+
+# Step 1: Azure CLI installed
+echo "--- Step 1: Azure CLI installed (az version) ---"
+if az version >/dev/null 2>&1; then
+ echo "PASS: Azure CLI is installed."
+else
+ echo "FAIL: Azure CLI not found. Install it, then re-run."
+ # Nothing else can run without the CLI.
+ echo ""
+ echo "OVERALL: FAIL"
+ exit 1
+fi
+echo ""
+
+# Step 2: Authenticated
+echo "--- Step 2: Authenticated (az account show) ---"
+if ACCOUNT_NAME=$(az account show "${SUB_ARGS[@]}" --query name -o tsv 2>/dev/null); then
+ echo "PASS: Authenticated (subscription: ${ACCOUNT_NAME:-unknown})."
+else
+ echo "FAIL: Not logged in. Run 'az login' (and 'az account set --subscription ')."
+ OVERALL=1
+fi
+echo ""
+
+# Step 3: Bicep compilation
+echo "--- Step 3: Bicep compilation (az bicep build) ---"
+BUILD_OUTPUT=$(az bicep build --file "$TEMPLATE" 2>&1)
+BUILD_RC=$?
+if [ $BUILD_RC -eq 0 ]; then
+ echo "PASS: Template compiles cleanly."
+else
+ echo "FAIL: Bicep compilation errors:"
+ echo "$BUILD_OUTPUT"
+ OVERALL=1
+fi
+echo ""
+
+# Step 4: Template validation
+echo "--- Step 4: Template validation (az deployment $SCOPE validate) ---"
+VALIDATE_OUTPUT=$(az deployment "$SCOPE" validate "${SCOPE_TARGET_ARGS[@]}" \
+ --template-file "$TEMPLATE" \
+ "${PARAM_ARGS[@]}" "${SUB_ARGS[@]}" 2>&1)
+VALIDATE_RC=$?
+if [ $VALIDATE_RC -eq 0 ]; then
+ echo "PASS: Template validated against the target scope."
+else
+ echo "FAIL: Template validation errors:"
+ echo "$VALIDATE_OUTPUT"
+ OVERALL=1
+fi
+echo ""
+
+# Step 5: What-if preview
+echo "--- Step 5: What-if preview (az deployment $SCOPE what-if) ---"
+WHATIF_OUTPUT=$(az deployment "$SCOPE" what-if "${SCOPE_TARGET_ARGS[@]}" \
+ --template-file "$TEMPLATE" \
+ "${PARAM_ARGS[@]}" "${SUB_ARGS[@]}" 2>&1)
+WHATIF_RC=$?
+if [ $WHATIF_RC -eq 0 ]; then
+ CREATE_COUNT=$(echo "$WHATIF_OUTPUT" | grep -c '^[[:space:]]*+ ')
+ MODIFY_COUNT=$(echo "$WHATIF_OUTPUT" | grep -c '^[[:space:]]*~ ')
+ DELETE_COUNT=$(echo "$WHATIF_OUTPUT" | grep -c '^[[:space:]]*- ')
+ echo "PASS: What-if completed. Changes -> Create: $CREATE_COUNT, Modify: $MODIFY_COUNT, Delete: $DELETE_COUNT"
+else
+ echo "FAIL: What-if errors:"
+ echo "$WHATIF_OUTPUT"
+ OVERALL=1
+fi
+echo ""
+
+# Overall result
+if [ $OVERALL -eq 0 ]; then
+ echo "OVERALL: PASS"
+else
+ echo "OVERALL: FAIL"
+fi
+exit $OVERALL