──────
Build and Push Docker Images / detect-changes (push) Successful in 11s
Build and Push Docker Images / build-auth-api (push) Successful in 1m0s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 1m1s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m5s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m13s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m15s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s

### Summary of Changes
  1. Documentation Consolidation:
      • Relocated the docs/ folder and all repository .md files (README.md, DEPLOYMENT.md, DOCKER_README.md,
      DOMAINS_README.md, GETTING_STARTED.md, CI_CD_PORTAINER_GUIDE.md, ADMIN_DEPLOYMENT_SUMMARY.md,
      SECURITY_PRIVATE_FILES.md, handoff.md, azure-project-plan.md, plus service-specific READMEs) into .
      • Updated document links in README.md.
  2. AI Agent Project Map:
      • Created PROJECT_MAP.md in the project root containing complete architecture diagrams, service specs, ports, DB
      schemas, environment variables, and developer workflows.
      • Added PROJECT_MAP.md to .gitignore.
  3. Monitoring UI & Infrastructure Integration:
      • Added monitoring_ui service to docker-compose.yml (port 5175).
      • Configured Gitea workflow docker-build.yml for monitoring_ui builds.
      • Added CORS middleware headers in index.js.
      • Added nginx.conf copy step in Dockerfile.

  ──────
  ### Recommended Commit Message

    docs: consolidate project documentation into monitoring_ui and add agent project map

    - Move all markdown documentation files and docs/ folder into monitoring_ui/docs/
    - Update documentation hub index (monitoring_ui/docs/README.md) with relative paths
    - Generate comprehensive AI agent reference map (PROJECT_MAP.md) in repository root
    - Add PROJECT_MAP.md and monitoring_ui build output/env overrides to .gitignore
    - Integrate monitoring_ui service into docker-compose.yml and Gitea CI workflow
    - Configure CORS handling in employee_api for monitoring dashboard access
    ──────
  ### Alternative Short Commit Message

    refactor(docs): move docs to monitoring_ui and create AI agent PROJECT_MAP.md
This commit is contained in:
Sven laptop
2026-09-20 23:33:55 +02:00
parent f935bbeb8c
commit 635a130f0b
195 changed files with 28309 additions and 957 deletions
@@ -0,0 +1,65 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Deploy — IaC Execution & Health Verification
> ⛔ **Read [`references/blocked-patterns.md`](references/blocked-patterns.md) before your first resource-creating command.**
>
> The trigger is **any** command that can create Azure resources — `az … create`, `az … up`, `azd up`, `azd provision`, `terraform apply` — not only `az deployment sub create`. Keying it to the declarative command alone made the rule unreachable on the path that most needs it: an agent that decided to provision imperatively never ran `az deployment sub create`, so it never reached the file explaining that imperative provisioning is forbidden.
>
> **This phase deploys a template. It does not create resources by hand.** If `infra/` holds no template, you are not ready to deploy — return to scaffold and generate one. Reaching for `az containerapp up` because the template is failing is the one fallback that is never available.
## Quick Reference
| Property | Value |
|----------|-------|
| Best for | Executing validated IaC against Azure, health-checking deployed resources |
| Inputs | `prepare-plan.json` + `scaffold-manifest.json` from `.copilot-azure/sessions/{id}/` |
| Outputs | `deploy-result.json` written to session directory |
| Parent | [azure-app-onboard](../instructions.md) |
## When to Use This Agent
Invoked by the `azure-app-onboard` orchestrator at Phase 4 when `scaffold-manifest.json` exists with `files[]` and `validationResult`. Not directly user-routable.
> **Return to orchestrator:** When complete, return control to `azure-app-onboard` for handoff (Step 10). Do NOT start new phases.
## When NOT to Use
| Scenario | Use Instead |
|----------|-------------|
| Plan architecture, map services, estimate costs | [prepare](../prepare/instructions.md) |
| Generate IaC files from a plan | `azure-app-onboard` Step 7 (scaffold) |
| Run `azd up` or execute existing deployment templates | `azure-deploy` |
| Debug a running app after deployment | `azure-diagnostics` |
| Optimize existing Azure spending | `azure-cost` |
## Workflow
> ⛔ **Sub-agent delegation is MANDATORY for Step 0.** Read `subagent-preflight.md`, then dispatch as a `task` with the **COMPLETE and UNMODIFIED** template text between `<<<TEMPLATE_START>>>` / `<<<TEMPLATE_END>>>` delimiters. Do NOT summarize or rewrite the template — the sub-agent needs every "Read [file]" instruction to produce a correct `deploy-checklist.md`. Append session artifact data AFTER the template block. If your next action after reading the template is anything other than `task`, you are executing it inline instead of delegating.
> ⛔ **Healing loop:** ask user after 3 attempts, then every 5 (counter = `healingAttempts[].length`).
> ⛔ **Region lock:** Before `az deployment` retry, compare `--location` against `prepare-plan.json.deploymentVariables.location`. If changed → re-approval gate required. Update plan after approval.
> ⛔ **After compaction or any `az deployment`/`az webapp deploy`/`az acr build`/failed health check: re-read `deploy-checklist.md`.** If missing → fill from [`deploy-checklist-template.md`](references/deploy-checklist-template.md). On significant context loss: also re-read this instructions.md.
> ⛔ **Deterministic resource inventory (MANDATORY — do NOT rely on memory for what was created).** The `capture_deployment_inventory` MCP tool records exactly which Azure resources this session created by diffing a `resources.list()` snapshot taken **before** the first deployment against one taken **after** each attempt. Call it:
> - **Step 5b** with `phase: "baseline"` — BEFORE the first `az deployment` (once).
> - **Every time the user resumes this deployment** with `phase: "baseline"` — AFTER the user chooses Resume and the subscription is resolved, but BEFORE any further command that can provision resources. Preserve the inventory already in `deploy-result.json`; subsequent captures cover only the resumed segment and must be unioned into the preserved `createdResources[]`/`orphanedResourceGroups[]` by normalized resource ID/case-insensitive RG name.
> - **After EVERY `az deployment` attempt** (success, failure, or healing retry) with `phase: "capture"`, passing `expectedResourceGroup`, all `deploymentNames[]`, and every `resourceGroups[]` touched (including abandoned healing RGs).
> - **On the Step 9 failure path** before returning `status: "failed"`.
> It holds the baseline snapshot **in memory** (no files are written to the workspace) and, on `phase: "capture"`, returns the created resources classified as `expected`/`failed`/`orphaned`/`unverified` plus the orphaned resource groups. Write that output into `deploy-result.json.createdResources[]` and `orphanedResourceGroups[]` — the durable record. ⛔ **Build ready-to-run cleanup commands from `failed` entries only.** `orphaned` means "appeared during the deploy window but no deployment claimed it" — it may be a healing stray, or it may belong to someone else on a shared subscription, so present it as "review before deleting" with no delete command. If the tool returns `inventoryUnverified`, present no cleanup list at all (see [`references/mcp-tools.md`](references/mcp-tools.md)). After a resume, merge the new segment into the durable record rather than replacing prior entries. The tool never deletes anything and never writes to disk; it only reports.
| # | Step | Action | Artifact | Reference |
|---|------|--------|----------|-----------|
| 0 | **Dispatch preflight sub-agent** | ⛔ **You MUST dispatch [`subagent-preflight.md`](references/subagent-preflight.md) as a `task`.** ⛔ agent_type: `"task"` — NEVER `"general-purpose"`. Read the template, then your NEXT action MUST be `task`. If after reading the template your next action is `powershell`, `view`, or anything other than `task`, STOP — you are executing inline instead of delegating. Writes `deploy-checklist.md`. **`view` it immediately after return.** | `deploy-checklist.md` | ⛔ **You MUST read [`subagent-preflight.md`](references/subagent-preflight.md)** |
| 1 | **Read upstream artifacts** | Load `prepare-plan.json` + `scaffold-manifest.json`. Check `validationResult`. Resolve subscription + deployment variables. | — | — |
| 3 | **Preflight checks** | Auth, **mandatory what-if preview**, RBAC, RG per `deploy-checklist.md` § Preflight. | — | ⛔ **You MUST read `deploy-checklist.md`** (re-read if compaction occurred) |
| 4 | **Deploy approval gate** | Present cost + resource summary per `deploy-checklist.md` § Deploy approval gate format. | — | — |
| 5b | **Write deploy-result.json skeleton + baseline inventory** | ⛔ Read [`deploy-schemas.ts`](references/deploy-schemas.ts), write skeleton (`status: "in-progress"`). Must exist BEFORE first `az` command. ⛔ **Then call `capture_deployment_inventory` with `phase: "baseline"`** (passing `sessionId`, `subscriptionId`) BEFORE the first `az deployment` — this snapshots pre-existing resources (held in memory) so the post-deploy diff is accurate. | `deploy-result.json` | ⛔ **You MUST read [`deploy-schemas.ts`](references/deploy-schemas.ts)** |
| 6 | **Execute deployment** | ⛔ **BEFORE `az deployment sub create`:** Generate portal link — `$dn="{deploymentName}"; $r="/subscriptions/{subId}/providers/Microsoft.Resources/deployments/$dn"; $l="https://portal.azure.com/#view/Microsoft_Azure_Resources/DeploymentDetails.MenuView/~/overview/id/$($r.Replace('/','%2F'))"; Write-Output "LINK=$l"`. ⛔ **Do NOT auto-open the link** — never call `Start-Process` (or any browser launcher) on it. Print the bare URL in chat (ctrl-clickable) so the user can open it if they choose.<br>Auto-generate ALL app-internal `@secure()` params (`openssl rand -base64 32 \| tr -d '/+='`), NEVER `ask_user` for secrets (databases use managed identity — no passwords); on retry reuse from `deploy-secrets.env` — NEVER regenerate (see deploy-safety.md § Deploy Checklist). THEN deploy IaC. ⛔ **After the deployment command returns (success OR failure), call `capture_deployment_inventory` with `phase: "capture"`** (`expectedResourceGroup`, all `deploymentNames[]`, every `resourceGroups[]` used) to record what was actually created and surface any orphans early. | — | ⛔ **You MUST read `deploy-checklist.md`** § Execute deployment |
| 6b | **Deploy application code** | ⛔ Deploy code for EVERY service in `prepare-plan.json.services[]`. Follow `deploy-checklist.md` § Code deploy. | — | ⛔ **You MUST read `deploy-checklist.md`** § Code deploy |
| 7 | **Health-check + SCM re-disable** | HTTP GET per endpoint (max 3 iterations). ⛔ **Multi-service apps:** Also inspect the response body for error patterns (`connection refused`, `MODULE_NOT_FOUND`, `localhost`, `SET-IN-DEPLOY-PHASE`) — HTTP 200 alone does not mean functional when the app depends on another service or KV secrets. Then ⛔ for EVERY App Service and Premium/EP1 Functions app — but ⛔ **NOT** Flex Consumption function apps, which have no SCM basic-auth policy (see [code-deployment-functions-flex.md](references/code-deployment-functions-flex.md)) — run BOTH commands, no exceptions: `az rest --method put --url "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{app}/basicPublishingCredentialsPolicies/scm?api-version=2023-12-01" --headers "Content-Type=application/json" --body '{"properties":{"allow":false}}'` then verify: `az rest --method get --url "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{app}/basicPublishingCredentialsPolicies/scm?api-version=2023-12-01" --query properties.allow -o tsv` (must return `false`). | `deploy-result.json` full | ⛔ **You MUST read `deploy-checklist.md`** § Health check |
| 8 | **Finalize artifacts** | ⛔ Read [`deploy-schemas.ts`](references/deploy-schemas.ts). ⛔ Re-read `deploy-checklist.md` § Artifact verification — follow ALL 5 checks. ⛔ **Run a final `capture_deployment_inventory` (`phase: "capture"`) and populate `deploy-result.json.createdResources[]` + `orphanedResourceGroups[]` FROM its returned output** — do NOT hand-author these fields. ⛔ **No "live"/handoff message until you overwrite the skeleton `deploy-result.json`** — flip `status` off `"in-progress"` (→ `succeeded`/`failed`) and fill healthStatus, endpoints, completedUtc, deploymentNames, healingAttempts. Write `deployment-summary.md` (status table + health + portal link(s) + cleanup commands — same content as your handoff message). Update `context.json` — add `"deploy"` to `completedPhases`, `currentPhase: null`, `lastModifiedUtc`. Read back to confirm `status != "in-progress"` and `"deploy"` ∈ `completedPhases`. ⛔ **Then STOP — return to orchestrator. No further CLI commands.** | `deploy-result.json` final + `deployment-summary.md` + `context.json` update | ⛔ **You MUST read [`deploy-schemas.ts`](references/deploy-schemas.ts)** + ⛔ **Re-read `deploy-checklist.md` § Artifact verification** |
| 9 | **Error handling + healing** | ⛔ **Only if Steps 6/6b/7 returned nonzero exit code or health check failed.** Skip entirely on clean deploys. Classify errors, healing loop, PLAN_LEVEL_CHANGE re-approval per `deploy-checklist.md` § During healing. ⛔ **Even on unrecoverable failure:** first run `capture_deployment_inventory` (`phase: "capture"`) to record what was partially created, then write `deploy-result.json` with `status: "failed"`, `errorDetails`, and `createdResources[]`/`orphanedResourceGroups[]` from its returned output, and surface cleanup commands built from its `failed` entries only (list `orphaned` entries separately as "review before deleting", with no delete command) — before returning to orchestrator. `deploy-result.json` and the cleanup report must ALWAYS exist, especially on failure. | — | ⛔ **You MUST read [`error-classification.md`](references/error-classification.md)** |
@@ -0,0 +1,59 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Approval Gate Template
Display template for the deploy approval gate. Present after preflight validation, before execution.
## Display Format
```
## Deploy Approval
🏢 **Subscription:** {context.json.azure.subscriptionName} (`{context.json.azure.subscriptionId}`)
📁 **Resource Group:** {context.json.azure.resourceGroup}
🌍 **Region:** {context.json.azure.region}
**Services:**
| Service | SKU | Region | Resource Name |
|---------|-----|--------|---------------|
{for each service in prepare-plan.json.services[]}
**💰 Estimated Monthly Cost:** ${costEstimate.monthlyUsd}/month
| Service | SKU | Monthly |
|---------|-----|---------|
{for each item in costEstimate.breakdown[]}
{costEstimate.disclaimer}
**🔒 Validation:** {scaffold-manifest.json.validationResult.status}
{if FLAGGED findings from scaffold self-review → ⚠️ list each}
**📋 What-if preview:** {N} resources to create, {N} to modify, {N} unchanged
{if any Delete operations → ⚠️ list each deleted resource with name + type}
**Generated Files:**
{for each file in scaffold-manifest.json.files[]}
**📦 Deployment Summary:**
{Display the "What's Being Deployed" table here, built from prepare-plan.json.services[] — showing which components map to which Azure services, SKUs, and estimated costs. (deployment-summary.md is not written until the deploy/handoff phase)}
---
**🚀 Ready to deploy? (Yes / Run manually / Edit plan / Cancel)**
```
## Response Handlers
| Response | Action |
|----------|--------|
| **Yes** | Execute deployment (Step 6 of deploy workflow). Do NOT re-confirm. |
| **Run manually** | Show exact CLI commands based on `iacFormat`:<br>**Bicep (subscription-scope, default):** `az deployment sub create --subscription {subscriptionId} --location {location} --template-file infra/main.bicep --parameters @infra/main.parameters.json --query properties.provisioningState -o tsv`<br>**Bicep (resource-group scope, after 403 fallback):** `az deployment group create --resource-group {rg} --template-file infra/main.bicep --parameters @infra/main.parameters.json --query properties.provisioningState -o tsv`<br>**Terraform (alternative):** `cd infra && terraform init && terraform plan -out=tfplan && terraform apply tfplan`. Stop. |
| **Edit plan** | Ask what to change → write to `context.json.overrides[]` → re-run prepare (Step 4) → re-scaffold → return to approval gate |
| **Cancel** | Preserve all session artifacts. Say "💾 Session preserved — resume anytime." Stop. |
## Rules
- ⛔ Gate is the LAST content in the response — no continued execution until user replies
- ⛔ SKU column must show exact Azure SKU code + tier name (e.g., "B1 Linux (Basic)", "P1v3 (Premium)") — not generic labels like "cheapest tier". F1/D1/Free are never selected.
- Always show the monthly cost estimate — there is no $0 tier (B1 floor ~$13/mo)
- Surface ALL `FLAGGED` self-review findings — user must see risks before approving
- If validation failed, show failures and block Yes option until resolved
@@ -0,0 +1,36 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Blocked Patterns
Commands the agent must NEVER execute. Block decisions are non-negotiable — user must run blocked commands manually outside AppOnboard.
| Pattern | Action | Reason |
|---------|--------|--------|
| `rm -rf` (any path outside a fresh temp dir) | ⛔ Block | Prevents accidental deletion of IaC, app code, or session artifacts — especially `infra/`, `.azure/`, `.copilot-azure/`. |
| `git reset --hard`, `git checkout -- <path>`, `git restore`, `git clean` | ⛔ Block | Discards uncommitted work. During region-fallback healing the agent edits Bicep/app config; these wipe the user's unstaged changes irrecoverably. |
| `git push --force` / `--force-with-lease` (any branch) | ⛔ Block | Prevents force-push of generated code over remote history. |
| `--no-verify` (on `git commit` / `git push`) | ⛔ Block | Bypasses hooks (secret-scan, lint) that guard the commit. |
| `DROP TABLE` / `DROP DATABASE` | ⛔ Block | Prevents data loss |
| `terraform destroy` | ⛔ Block | Prevents accidental teardown (user must run manually) |
| `az group delete` | ⛔ HARD BLOCK | **NEVER delete resource groups yourself.** During healing: if switching regions/RGs, add the old RG to your `orphanedResourceGroups[]` list (per `OrphanResourceGroup` in [`deploy-schemas.ts`](deploy-schemas.ts)) instead of deleting it. At handoff: emit `az group delete` commands in the handoff message for the USER to run — the agent never executes them. If you are about to type `az group delete` into a terminal command, STOP — you are violating this rule. Track it in `orphanedResourceGroups[]` instead. |
| `az containerapp up --source` / `az containerapp create` | ⛔ Block | Creates ACR + CA Environment + Log Analytics imperatively — orphan resources invisible to `terraform destroy`, `az deployment sub delete`, and session tag-based bulk cleanup. State drift from IaC is unrecoverable. The Container App MUST be created via Bicep `az deployment sub create` — for code deploy on an existing CA use `az containerapp update --source` (Step 6d) |
| `az appservice plan update` | ⛔ Block | Imperative SKU change — edit Bicep + redeploy |
| `az webapp update` | ⛔ Block | Imperative resource modification — all changes via IaC |
| `az functionapp update` | ⛔ Block | Imperative resource modification — all changes via IaC |
| `az webapp deployment source config-zip` | ⛔ Block | Requires SCM basic auth — use `az webapp deploy` (Entra auth). ⛔ **This block is the `webapp` command only.** `az functionapp deployment source config-zip` is the **ALLOWED** deploy channel for Flex Consumption function apps (blob-container package, not SCM basic auth) — see [`code-deployment-functions-flex.md`](code-deployment-functions-flex.md). |
| `az webapp deploy --track-status` | ⛔ Block | `--track-status` flag does not exist. Remove it. |
| `az webapp up` / `az webapp create` / `az appservice plan create` | ⛔ Block | Creates App Service Plan + App imperatively — bypasses IaC entirely |
| `az containerapp update` (config changes) | ⛔ Block | Imperative resource modification — all changes via IaC |
| `az containerapp update --revision-suffix` (no config changes) | ⚠️ ALLOWED | KV secret rotation only — when KV secrets were updated post-deploy and a new revision is needed to pick up cached values |
| `az webapp delete` | ⛔ Block | Imperative resource deletion — destroys resources outside IaC |
| `az appservice plan delete` | ⛔ Block | Imperative plan deletion — remove from Bicep + redeploy instead |
| `az containerapp update --image` | ⛔ Block (during healing) | Imperative image swap causes IaC drift — update Bicep + redeploy |
| Inline secret values in CLI args | ⛔ Block | `--parameters password=MyP@ss$word!` breaks shell escaping and leaks secrets in terminal history. Pass app-internal secrets via `main.parameters.json`, `terraform.tfvars`, or an `@secure()` param from a variable reloaded from `deploy-secrets.env` (no Key Vault). |
| Writing secrets to temp files on disk | ⛔ Block | ⛔ NEVER write rendered secret values to arbitrary temp files. App-internal secrets are passed as `@secure()` params and stored on the compute resource (App Service app settings / CA native secrets); the only persisted copy is the git-ignored `deploy-secrets.env` reload cache under `.copilot-azure/`. Temp files risk exposure in crash dumps, logs, and unprotected storage. |
| `az group create` (during healing) | ⛔ HARD BLOCK — **one sanctioned exception** | **NEVER create resource groups imperatively during healing.** All RG creation must go through `az deployment sub create` with Bicep `targetScope = 'subscription'`. If you need a new RG for region fallback, update the Bicep region parameter and redeploy. **Sole exception:** the documented 403 scope-fallback in [`deploy-safety.md`](deploy-safety.md) § 403 Scope Fallback — when `az deployment sub create` returns 403, rescoping Bicep to RG-scope requires `az group create` (with all 5 AppOnboard tags). That path is allowed. |
| `az rest --method put/patch` (for individual resource creation) | ⛔ HARD BLOCK | **NEVER create individual Azure resources via REST API as a fallback for Bicep failures.** After a deployment failure, the ONLY allowed remediation is: fix the Bicep parameters/template → re-run `az deployment sub create`. Compiling Bicep→ARM and deploying via REST is still imperative resource creation. |
| Disabling a security control to unblock — `require_secure_transport`/TLS → OFF, HTTPS-only off, KV purge protection off, auth off (via `az ... parameter set` OR editing the Bicep) | ⛔ HARD BLOCK | **NEVER weaken a security control to make a failing deploy pass.** A DB TLS handshake failure means the *client* lacks SSL config — fix the client (prereq `W-MYSQL-SSL`/`W-PG-SSL`) or surface the tradeoff to the user. Downgrading the server control is forbidden. |
| `Compress-Archive -Path $files.FullName` | ⛔ Block | Absolute paths flatten directory structure — app crashes on `./src/app` not found. Use `System.IO.Compression.ZipFile` with relative paths from workspace root. On Windows, normalize: `$entryName = $relativePath.Replace('\', '/')`. |
> **Repos with existing `azure.yaml`:** See [`pipeline-rules.md`](../../references/pipeline-rules.md) § azure.yaml prohibition. Deploy via `az deployment sub create` — do NOT run `azd up`.
@@ -0,0 +1,84 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Code Deployment — App Service (and Premium/EP1 Functions)
After IaC deployment creates the Azure resources, deploy application code.
> ⛔ **Functions on Flex Consumption (the default Functions floor) do NOT deploy through this SCM/Kudu path.** They use a blob-container package channel with no `basicPublishingCredentialsPolicies` toggle. Use [code-deployment-functions-flex.md](code-deployment-functions-flex.md) instead. This file applies to App Service web apps and to Functions only on **Premium (EP1)**.
> ⛔ **`--subscription {subscriptionId}` on EVERY `az` command.**
> ⛔ **Verify `SCM_DO_BUILD_DURING_DEPLOYMENT=true` is active BEFORE deploying.** ARM timing can delay propagation. Check: `az webapp config appsettings list -g {rg} -n {app} --query "[?name=='SCM_DO_BUILD_DURING_DEPLOYMENT'].value" -o tsv`. If not `true`: `az webapp config appsettings set -g {rg} -n {app} --settings SCM_DO_BUILD_DURING_DEPLOYMENT=true ENABLE_ORYX_BUILD=true`. Wait 10s. If `az webapp deploy` reports "Build successful. Time: 0(s)", Oryx was skipped — use Kudu zipdeploy instead.
> ⛔ **`ORYX_DISABLE_COMPRESSION=true`** and **`WEBSITES_CONTAINER_START_TIME_LIMIT=1800`** must be in Bicep app settings (from `prepare-plan.json.deployStrategy.requiredAppSettings`).
## Pre-Deploy Verification (Step 6a)
> ⛔ **TypeScript projects:** Oryx with `NODE_ENV=production` skips devDependencies. If `typescript`, `@types/*`, or build tools are in `devDependencies`, move them to `dependencies` before zipdeploy. Alternative: set `NPM_CONFIG_PRODUCTION=false` as app setting so Oryx installs devDeps during build.
> ⛔ **Wait for App Service to stabilize** (B1: ~30-90s cold start on a freshly created plan). Poll `az webapp show -g {rg} -n {app} --query state` every 10s, max 2 min. If not `Running`, check logs.
When `deployStrategy.codeDeployPattern == "startup-install"`, surface: "⚠️ First cold start: 2-5 min (native module compilation)."
## Zip Deploy (Step 6b)
SCM lifecycle: enable → deploy → re-disable.
**Enable SCM:**
```powershell
az rest --method put --url "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{app}/basicPublishingCredentialsPolicies/scm?api-version=2023-12-01" --headers "Content-Type=application/json" --body '{"properties":{"allow":true}}'
```
**Choose deploy method:**
| Runtime | Needs Oryx? | Method |
|---|---|---|
| Python, Node.js, Ruby, PHP | Yes | **Kudu zipdeploy** (`/api/zipdeploy`) |
| .NET, Java, static front-end | No | `az webapp deploy --type zip` |
⛔ **OneDeploy NEVER triggers Oryx** — use Kudu zipdeploy for runtimes needing server-side install.
⛔ **NEVER use `az webapp deployment source config-zip`** — deprecated.
⛔ **`az webapp deploy` does NOT support `--track-status`.**
### Kudu Zipdeploy (Oryx-Dependent Runtimes)
For apps needing server-side package installation (Python, Node.js, Ruby, PHP), use Kudu zipdeploy directly:
```powershell
# Get publishing credentials
$creds = az webapp deployment list-publishing-credentials --subscription {sub} -g {rg} -n {app} --query "{user:publishingUserName, pass:publishingPassword}" -o json | ConvertFrom-Json
$auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$($creds.user):$($creds.pass)"))
# Deploy via Kudu zipdeploy (triggers Oryx pip install)
Invoke-WebRequest -Uri "https://{app}.scm.azurewebsites.net/api/zipdeploy?isAsync=true" -Method POST -InFile $zipPath -Headers @{Authorization="Basic $auth"} -ContentType "application/zip" -UseBasicParsing
# Poll until build completes
for ($i = 1; $i -le 40; $i++) {
Start-Sleep -Seconds 15
$resp = Invoke-WebRequest -Uri "https://{app}.scm.azurewebsites.net/api/deployments/latest" -Headers @{Authorization="Basic $auth"} -UseBasicParsing
$deploy = $resp.Content | ConvertFrom-Json
if ($deploy.complete -eq $true) { break }
}
```
Prerequisites:
- `SCM_DO_BUILD_DURING_DEPLOYMENT=true` must be set (verified in Step 6a)
- Runtime manifest must be at the zip root — Oryx detects the runtime from it:
- Python: `requirements.txt` (or `pyproject.toml`)
- Node.js: `package.json` (+ lockfile)
- Ruby: `Gemfile`
- PHP: `composer.json`
- Do NOT pre-install packages locally — let Oryx run the install remotely
> **SCM auth lifecycle (REST API toggle — no Bicep edits):**
>
> IaC has `scm.allow: true` (deploy convenience). Deploy phase:
> Deploy code → health check → re-disable via REST API → verify `false`.
> If re-disable fails, log but don't block — add postDeployRecommendation.
**Zip creation:** Use `System.IO.Compression.ZipFile` with relative paths from workspace root. **On Windows, normalize entry paths: `$entryName = $relativePath.Replace('\', '/')` — ZipFile preserves backslashes which Linux App Service cannot resolve.** Never use `Compress-Archive -Path $files.FullName` — absolute paths flatten the directory structure, causing app crashes (`./src/app` not found).
## Database Post-Deploy Verification
> ⛔ **You MUST read [`database-post-deploy.md`](database-post-deploy.md)** for migration discovery, execution via App Service SSH, error handling, and PostgreSQL-specific checks.
@@ -0,0 +1,98 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Code Deployment — Container Apps
After IaC deployment creates the Container App with a placeholder image (`mcr.microsoft.com/azuredocs/containerapps-helloworld:latest`), deploy the actual application code. This is **Phase 2** of the two-phase Container Apps deployment pattern.
> ⛔ **`--subscription {subscriptionId}` on EVERY `az` command** (from `context.json.azure.subscriptionId`). Without it, the CLI uses whatever subscription is currently active — which may have changed since the prepare phase. This applies to ALL commands below.
> ⛔ **Phase 2 is NOT optional.** If IaC deployed a Container App with a placeholder image, you MUST execute Phase 2 before health checks. Do NOT leave a placeholder image and tell the user to deploy code manually.
**Determine the code deploy path:**
| Condition | Path | Steps |
|-----------|------|-------|
| ACR exists in IaC (`services[]` has Container Registry) | ACR build + image update | Steps 1–4 below |
| No ACR in IaC, simple app (single Dockerfile or source) | Add ACR to IaC + redeploy, then ACR build | Step 0 + Steps 1–4 |
| No ACR in IaC, no Dockerfile, Oryx-compatible (Node/Python/Go/.NET) | `az containerapp update --source .` | Step 5 (Oryx shortcut) |
## Step 0 — Add ACR to IaC (if not already present)
Add `container-registry.bicep` module (ACR Basic, `adminUserEnabled: false`) + AcrPull role assignment for CA's managed identity (role GUID: `7f951dda-4ed3-4680-a7ca-43fe172d538d`). Redeploy IaC, wait ~60s for AcrPull role propagation. Log as healing attempt.
⛔ Update `prepare-plan.json` (add ACR to `services[]`), `scaffold-manifest.json.files[]`, and `costEstimate`.
## Steps 1–4 — ACR Build + Image Update (IaC-compliant)
| Step | Command | Notes |
|------|---------|-------|
| 1. Build image | `az acr build --subscription {subscriptionId} -r {acrName} -t {appName}:latest . --no-logs` | Builds from workspace root. If `buildRequirements.hasBuildKitSyntax`, use `-f Dockerfile.azure` (see BuildKit handling below). ⛔ **Build-time env vars:** If Dockerfile has `ARG NEXT_PUBLIC_*` or `ARG VITE_*`, pass `--build-arg NEXT_PUBLIC_API_URL=https://{api-fqdn}` to inject the deployed API URL. These vars are baked into the JS bundle at build time — runtime env vars have no effect on client-side code. Get the API FQDN from Phase 1 output: `az containerapp show -g {rg} -n {apiApp} --query properties.configuration.ingress.fqdn -o tsv`. |
| 2. Update Bicep image | Edit `infra/modules/{containerapp}.bicep`: replace placeholder with `image: '{acrLoginServer}/{appName}:latest'`. Add ACR registry config: `registries: [{ server: acrLoginServer, identity: 'system' }]` | IaC-only — no imperative `az containerapp update` |
| 3. Redeploy | `az deployment sub create --subscription {subscriptionId} --location {location} --template-file infra/main.bicep --parameters @infra/main.parameters.json --parameters containerImage='{acrLoginServer}/{appName}:latest' --name app-onboard-code-deploy-{timestamp} --query properties.provisioningState -o tsv` | Emit new portal link. ⛔ No DB password param — databases are managed-identity only (see [Parameter Pass-Through](#parameter-pass-through-bicep-redeploy-safety)). |
| 4. Verify revision | `az containerapp show --subscription {subscriptionId} -g {rg} -n {ca} --query "{revision:properties.latestReadyRevisionName, image:properties.template.containers[0].image}" -o json` | Confirm image is not the placeholder |
> ⛔ **`az acr build` failures count toward the `deploy-result.json.healingAttempts[]` counter.** After 3 failed builds (even with different root causes), pause and present a diagnosis to the user: "Web image build failed 3 times: [root causes]. Continue healing? (Yes / Cancel)." Each build fix attempt = 1 healing entry. Cross-reference deploy instructions.md healing loop rule.
> ⛔ **Windows: `az acr build` may fail with `UnicodeEncodeError`.** Azure CLI log streaming crashes on non-ASCII characters (✓, ✗) using Windows `charmap` codec. Append `--no-logs` to the build command. Build success/failure is still reported via exit code.
## Step 5 — Oryx Shortcut (no ACR needed, no Dockerfile)
```powershell
az containerapp update --subscription {subscriptionId} -g {rg} -n {ca} --source . --set-env-vars NODE_ENV=production
```
> ⛔ **`az containerapp update --source` is allowed for code deploy ONLY.** This is different from `az containerapp up --source` (which is ⛔ BLOCKED because it creates resources imperatively). `update --source` updates an EXISTING Container App — no state drift.
## App-Internal Secrets (native CA secrets — No Key Vault, no seeding step)
App-internal secrets are set as **native Container Apps secrets** directly in the Bicep, with the value supplied as an `@secure()` param on the deployment command. ⛔ **No Key Vault, no `az keyvault secret set` step.** Generate each secret ONCE and pass it on every `az deployment sub create`:
```powershell
$secretKey = (openssl rand -base64 32) -replace '[/+=]','' # generate ONCE, persist to deploy-secrets.env
az deployment sub create ... --parameters secretKey=$secretKey --query properties.provisioningState -o tsv
```
> ⛔ Reload the SAME value from `deploy-secrets.env` on every retry/redeploy (shell variables don't persist between tool calls). Pass it to EVERY deployment so the desired-state apply keeps the CA secret stable.
## After Code Deploy (all paths)
- Wait 30–60s for the new revision to become ready
- Proceed to Step 7 (Health-Check Endpoints)
- If health check returns placeholder content → image update didn't take effect. Check `az containerapp revision list`
> ⛔ **`az containerapp revision restart` does NOT pick up a changed secret.** Container Apps bind secret values at revision *creation* time. To pick up an updated native secret, create a NEW revision by redeploying Bicep (preferred) or `az containerapp update --revision-suffix rev{timestamp}`.
> ⛔ **ACR registries require the CA managed identity + AcrPull to exist first.** Phase 1 of the two-phase deploy creates the Container App with a placeholder image and `registries: []` (native secrets MAY be set in Phase 1 — they have no RBAC dependency). ACR `registries` with `identity: 'system'` fails in Phase 1 because the CA's managed identity doesn't exist yet (no principalId → AcrPull fails → "Operation expired"). After Phase 1 completes and RBAC propagates (~60s), Phase 2 redeploys with ACR registries + real image + correct `targetPort`.
## Config-File Apps (Go/Viper, Spring Boot, etc.)
Creating Azure-specific config (e.g., `config-azure.yml`) is fine for non-secret values.
> ⛔ **NEVER bake secrets into config files COPY'd into Docker images.**
**Secret injection:** Config uses placeholders → `az containerapp secret set` → `az containerapp update --set-env-vars KEY=secretref:name` → framework env var override.
> **Go/Viper:** `AutomaticEnv()` maps `POSTGRES_PASSWORD` → `postgres_password` (underscores), NOT `postgres.password`. Without `SetEnvKeyReplacer(strings.NewReplacer(".", "_"))`, env vars can't override nested keys.
## BuildKit Dockerfile Handling
ACR's `az acr build` uses the classic Docker builder — it does NOT support BuildKit. When `buildRequirements.hasBuildKitSyntax == true`: build using the ACR-compatible copy instead: `az acr build -f Dockerfile.azure .`. The user's original Dockerfile stays untouched. If `Dockerfile.azure` doesn't exist yet, create it by stripping BuildKit syntax from the original Dockerfile per [`dockerfile-generation.md § ACR Build Compatibility`](../../scaffold/references/dockerfile-generation.md).
## Parameter Pass-Through (Bicep Redeploy Safety)
⛔ **On every redeploy, pass the SAME values you originally used** — a full desired-state apply, not a patch, so an omitted param reverts to its default. The one param that bites:
- **`containerImage`** — omitting it reverts the Container App to the placeholder image.
> ⛔ **No `administratorLoginPassword` (or any DB password/key) param exists** — databases are Entra/managed-identity only. There is nothing to re-pass and no `deploy-secrets.env` DB password. App-internal secrets (e.g. `SECRET_KEY`) live in Key Vault and are read via the app's MI — they are not passed as deploy params.
```powershell
az deployment sub create ... `
--parameters containerImage='{acrLoginServer}/{appName}:latest' `
--query properties.provisioningState -o tsv
```
## Database Post-Deploy Verification
> ⛔ **You MUST read [`database-post-deploy.md`](database-post-deploy.md)** for migration discovery, execution via Container Apps exec, error handling, and PostgreSQL-specific checks.
@@ -0,0 +1,93 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Code Deployment — Azure Functions (Flex Consumption)
How to deploy application code to a **Flex Consumption** function app after its IaC is provisioned. This is a
**different channel** from App Service — read this instead of
[code-deployment-appservice.md](code-deployment-appservice.md) whenever the compute is Functions on Flex
Consumption (the default Functions floor). Only **Functions on Premium (EP1)** use the App Service SCM/Kudu
path.
> ⛔ **`--subscription {subscriptionId}` on EVERY `az` command.**
## The Flex deploy model (why the App Service path does not apply)
Flex Consumption has a **single deployment path**: you build and zip your project, the package is written to a
**blob storage container**, and the app runs from that package on startup. There is **no** Kudu
`SCM_DO_BUILD_DURING_DEPLOYMENT`/Oryx-on-SCM build, **no** `az webapp deploy --type zip`, and **no**
`basicPublishingCredentialsPolicies` (`scm`/`ftp`) enable/re-disable dance. Do not run any of those against a
Flex app — they either error or silently do nothing.
## Deploy the package
Build/zip the project, then deploy with the Functions channel. Prefer a **remote build** for interpreted
runtimes so native wheels/modules are built on Linux, not on the local machine:
```powershell
# Python / Node / TypeScript — zip the PROJECT ROOT (manifest at the zip root) and request a remote build.
az functionapp deployment source config-zip `
--subscription {sub} -g {rg} -n {app} `
--src $zipPath --build-remote true
# .NET (isolated) / Java / Go — deploy a precompiled package (build output at the zip root); no remote build.
az functionapp deployment source config-zip --subscription {sub} -g {rg} -n {app} --src $zipPath
```
`func azure functionapp publish {app}` (Azure Functions Core Tools) is an equivalent channel and also performs
a remote build — use it if Core Tools is already the project's workflow.
- The runtime is fixed by `functionAppConfig.runtime` in IaC — do **not** pass or set `FUNCTIONS_WORKER_RUNTIME`.
- The deploying identity writes the package to the deployment container. With shared-key access disabled, that
identity (the signed-in user for a local deploy, or the CI principal) needs **Storage Blob Data Contributor**
on the deployment storage account, in addition to the function app's own identity role.
- After deploy, sync is automatic; then run the health check below.
## ⛔ Local (non-CI) deploys: hardening can lock you out — do not deny-by-default before first deploy
This pipeline hardens network access, which is CI/CD-friendly but **can block a deploy run from a developer
machine.** Two Flex-specific facts matter:
1. **Flex outbound is a platform-managed gateway pool, not a per-instance IP.** An SCM/site allow rule built
from the deployer's observed public IP (e.g. via ipify) will **not** match the channel's real egress, so it
does not unblock the deploy and often changes between attempts. Do **not** try to punch a `/32` SCM hole to
fix a 403 — it is the wrong lever for Flex.
2. **Model the main site and the deployment/SCM surface independently.** Restricting one must not implicitly
deny the other.
**Rules for the deploy phase:**
- ⛔ **Do NOT set `publicNetworkAccess: 'Disabled'` or a deny-by-default rule on the function app or its
deployment storage BEFORE the first successful code deploy + health check.** Provision with access open,
deploy, verify, *then* harden.
- Before hardening, **tell the user** (once, plainly): *"This deployment hardens network access for
production. Publishing the code from your machine happens first, while access is still open; after the app
is healthy I'll restrict it. If you need to redeploy later from your machine, you may need to temporarily
re-open access or deploy from CI."*
- If a package deploy returns **HTTP 403**, the cause is network hardening applied too early or a missing
Storage Blob Data role — **not** a missing SCM allow rule. Re-open public access on the app + deployment
storage, confirm the deployer's role assignment, redeploy, then re-harden. Never fall back to imperative
provisioning.
- ⛔ **Azure inserts a terminal "Deny all" rule as the last entry of any `ipSecurityRestrictions` /
`scmIpSecurityRestrictions` list — this is the expected default sentinel, NOT an error.** Validation must
treat a trailing deny-all with priority `2147483647` (name `Deny all`/`Explicit deny`) as normal. Only flag
it if there are **no** preceding allow rules AND you intended the surface to be reachable.
## Hardening (only AFTER a healthy deploy)
If the plan calls for a locked-down app, apply `publicNetworkAccess`/IP restrictions as the **last** step,
after the health check passes. Keep the deployment storage reachable by the app identity. Record any access
you re-open for a local deploy so cleanup is idempotent (re-running the step must not error if the rule is
already gone).
## Health check
HTTP GET the function endpoint(s) (max 3 iterations, honoring cold start). A Flex app can take up to ~30s to
initialize on first start; `System.TimeoutException`/gRPC host messages during that window are startup noise,
not deploy failures. Inspect the response body for error patterns (`connection refused`, `MODULE_NOT_FOUND`,
`SET-IN-DEPLOY-PHASE`) — HTTP 200 alone is not proof of health when the app depends on another service.
## Database post-deploy
> ⛔ If the app uses a managed database, run [`database-post-deploy.md`](database-post-deploy.md) for migration
> discovery and execution.
@@ -0,0 +1,57 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Code Deployment — Static Web Apps
## Static Web Apps — Content Deployment (Step 6c)
Deploy content using the SWA CLI with deployment token.
**Pre-check:** Verify `swa` CLI is installed: `npx --yes @azure/static-web-apps-cli --version`. If not available, install: `npm install -g @azure/static-web-apps-cli`.
> ⛔ **Pre-deploy: Build the frontend if SPA source (not pre-built HTML).** Check if the SWA component directory has a `package.json` (or equivalent manifest) with a `build` script. If yes, detect the package manager from the lockfile (`package-lock.json` → npm, `yarn.lock` → yarn, `pnpm-lock.yaml` → pnpm, `bun.lock`/`bun.lockb` → bun; default to npm if no lockfile) and run:
> ```powershell
> cd {component-path} # e.g., web/
> {pm} install # npm install, yarn install, pnpm install, etc.
> {pm} run build # npm run build, yarn build, pnpm build, etc.
> ```
> The deploy path should point to the **build output directory** — read the framework config to determine the output dir (`vite.config.*` → `build.outDir`, `next.config.*` → `.next/` or `out/`, CRA → `build/`; default to `dist/`). If no `package.json` exists (plain HTML/CSS/JS), deploy the source directory directly — no build needed.
>
> ⛔ **This is NOT optional for SPA frameworks.** Raw JSX/TSX/Vue/Svelte source files cannot be served by SWA — the app must be compiled to static HTML/JS/CSS first. Skipping the build produces a broken deployment.
> ⛔ **Pre-deploy: Update frontend config with deployed backend URLs.** If `prereq-output.json.cloudSdkSwaps[]` mapped cloud SDK endpoints (AWS API Gateway, GCP Cloud Functions) to Azure equivalents, the frontend config file (`config.ts`, `.env`, `environment.ts`) still references the original cloud URLs. After IaC deploy (Step 6), read `deploy-result.json.endpoints[]` to get the deployed Azure backend URLs. Update the frontend config with these URLs before running `swa deploy`. This is a string replacement in the config file — NOT a code rewrite. Do NOT defer as a post-deploy step — the SWA will show a broken page if the frontend calls non-existent AWS/GCP endpoints.
| Step | Command | Notes |
|------|---------|-------|
| 1. Get token | `$token = az staticwebapp secrets list --name {swa} -g {rg} --query "properties.apiKey" -o tsv 2>$null` | Store in variable first — do NOT pass inline. ⛔ **On Windows, use `2>$null`** (not `2>&1`) — Azure CLI Python warnings corrupt `-o tsv` output |
| 2. Set env var | `$env:SWA_CLI_DEPLOYMENT_TOKEN = $token` | ⛔ Use env var ONLY — do NOT pass `--deployment-token $token` as CLI arg (leaks token in process args / transcript) |
| 3. Copy to temp | `$tempDir = "C:\temp\swa-deploy"; Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue; New-Item -ItemType Directory -Path $tempDir -Force \| Out-Null; Copy-Item -Path .\* -Destination $tempDir -Recurse -Exclude @('.git','.copilot-azure','node_modules','.azure','infra')` | ⛔ **MANDATORY on Windows** — `$env:TEMP` often contains spaces (e.g., `C:\Users\Jane Doe\AppData\Local\Temp`). Always use a short, space-free path on the FIRST attempt — do NOT use `$env:TEMP` and retry. On macOS/Linux, use `/tmp/swa-deploy` instead. **If a build step ran:** copy the build output directory (e.g., `{component}/dist/`) instead of the entire workspace — `Copy-Item -Path {component}\dist\* -Destination $tempDir -Recurse`. |
| 4. Deploy | `swa deploy $tempDir --app-name {swaName} --env production` | ⛔ **`--app-name` is MANDATORY** — without it, the SWA CLI launches an interactive "create new project?" prompt that fails in automation. `{swaName}` = the SWA resource name from `prepare-plan.json.naming.resources[]`. SWA CLI reads `SWA_CLI_DEPLOYMENT_TOKEN` from env var automatically — do NOT pass `--deployment-token` flag (leaks token in command args) |
| 5. Clean up | `Remove-Item -Recurse -Force $tempDir` | Clean temp dir after successful deploy |
⛔ **`az staticwebapp deploy` does NOT exist** — the correct CLI is `swa deploy` (from `@azure/static-web-apps-cli`).
## Fallback: Direct StaticSitesClient Upload
If `swa deploy` fails (binary crash, path errors on Windows), use the underlying `StaticSitesClient.exe` directly:
```powershell
# Find the binary bundled with @azure/static-web-apps-cli
$swaCliPath = (Get-Command swa).Source | Split-Path -Parent
$client = Get-ChildItem -Path $swaCliPath -Recurse -Filter "StaticSitesClient*" | Select-Object -First 1
# Upload from PARENT directory with relative paths (avoids "identical to artifact folder" error)
Push-Location (Split-Path $tempDir -Parent)
& $client.FullName upload --app (Split-Path $tempDir -Leaf) --apiToken $token --skipAppBuild true
Pop-Location
```
> ⛔ **Run from a PARENT directory** with a relative `--app` path. Running from inside the app directory causes `StaticSitesClient` to error with "Current directory cannot be identical to or contained within artifact folders."
## Finalize deploy-result.json (after `swa deploy` succeeds)
⛔ **`swa deploy` succeeding is NOT the end of the deploy phase.** SWA finalizes through the **generic Step 8** (see [`deploy-checklist-template.md`](deploy-checklist-template.md) §"Before handoff (Step 8)") — overwrite the `deploy-result.json` skeleton IN PLACE with the full `DeployResult` contract, not a `status`+`subscriptionId` stub. Only the values Step 8 can't derive on the SWA path are below:
- **Hostname** — `swa deploy` produces no ARM endpoint output, so fetch it: `$swaHost = az staticwebapp show -n {swa} -g {rg} --query defaultHostname -o tsv`
- `endpoints[]` — `[{ name, url: "https://$swaHost", healthStatus: "healthy" }]` (HTTP GET `https://$swaHost/` → 2xx confirms healthy)
- `resourceIds[]` — include the `Microsoft.Web/staticSites` resource id
@@ -0,0 +1,136 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Database Post-Deploy Verification (Managed-Identity / Entra-Only)
Run schema migrations on AppOnboard-created databases (listed in `prepare-plan.json.services[]`) before health checks. The app must be running first — if it's crashing, fix that before attempting migrations.
> ⛔ **No passwords anywhere.** Databases are provisioned Entra-only (no `administratorLogin`/`administratorLoginPassword`, no access keys). Admin operations (create DB, grant the app's managed identity a role, run migrations) authenticate with an **Entra access token** minted for the deploying principal, which the Bicep set as the server's Entra administrator. There is no `deploy-secrets.env` DB password and no `pgAdminPassword` parameter.
## 0. Mint an Entra token for the deploying principal
The deployer is the Entra admin on the server (set by the DB module's `administrators` child resource). Mint a short-lived token and use it as the "password" for CLI connections. `{entraAdminName}` = the deployer's UPN / app display name (the `login` set in the server's `administrators` block).
```powershell
# PostgreSQL / MySQL Flexible Server (OSS RDBMS audience)
$dbToken = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv
# Azure SQL (SQL audience)
$sqlToken = az account get-access-token --resource https://database.windows.net/ --query accessToken -o tsv
```
## 1. Create the app database (if needed)
> ⛔ **Azure PostgreSQL/MySQL Flexible Server only creates the system `postgres`/`mysql` database by default.** If the app's config references a named database (e.g., `car_sale_db`, `myapp_production`), create it BEFORE the container starts — using token auth, never a password:
>
> ```powershell
> az postgres flexible-server execute -n {pg} -g {rg} -u "{entraAdminName}" -p $dbToken `
> -d postgres --querytext "CREATE DATABASE {dbName};"
> ```
>
> Detect the database name from: (1) `prereq-output.json.initCommands[]` with `type: "db-migrate"`, (2) app config files (`config-docker.yml`, `.env`, `database.yml`), (3) compose `POSTGRES_DB` env var. If the container crashes with `database "X" does not exist`, this step was missed. (MySQL app DBs are emitted in Bicep as a `flexibleServers/databases` child — no CLI step needed.)
## 2. Grant the app's managed identity a database role
The app authenticates with its **managed identity**, which must exist as a database principal. Do this ONCE, as the Entra admin, before migrations. `{appMiName}` = the compute resource name (system-assigned MI display name); `{appMiObjectId}` = its object id.
**PostgreSQL** — two connections are required. The `pgaadauth_*` functions exist **only on the `postgres` database** (running them on the app DB fails with `No function matches the given name and argument types`), and for a **managed identity** you must use the `_with_oid` variant with `objectType 'service'` (the name-only `pgaadauth_create_principal` can't resolve a managed identity by name). Roles are cluster-wide, so create the principal on `postgres`, then GRANT on `{dbName}`:
```powershell
# 2a. Create the Entra principal for the app MI — MUST run on the `postgres` database.
az postgres flexible-server execute -n {pg} -g {rg} -u "{entraAdminName}" -p $dbToken -d postgres --querytext @"
SELECT * FROM pgaadauth_create_principal_with_oid('{appMiName}', '{appMiObjectId}', 'service', false, false);
"@
# 2b. Grant privileges — run on the app database {dbName} (the role already exists cluster-wide).
# NOTE: a PowerShell here-string does NOT treat "" as an escape — use a single " around the identifier
# (doubling it emits `TO ""myapp"";` → Postgres `zero-length delimited identifier`).
az postgres flexible-server execute -n {pg} -g {rg} -u "{entraAdminName}" -p $dbToken -d {dbName} --querytext @"
GRANT ALL PRIVILEGES ON DATABASE {dbName} TO "{appMiName}";
GRANT ALL ON SCHEMA public TO "{appMiName}";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "{appMiName}";
"@
```
**MySQL:** `CREATE AADUSER '{appMiName}' IDENTIFIED BY '{appMiObjectId}'; GRANT ALL PRIVILEGES ON `` `{dbName}`.* `` TO '{appMiName}'@'%'; FLUSH PRIVILEGES;`
**Azure SQL:** `CREATE USER [{appMiName}] FROM EXTERNAL PROVIDER;` then add to `db_datareader`, `db_datawriter`, and (if the app runs migrations) `db_ddladmin`.
**Discover the migration command** from the codebase (check in order):
| Signal | Command | Working directory |
|--------|---------|-------------------|
| `alembic.ini` exists | `alembic upgrade head` | Directory containing `alembic.ini` |
| Django `manage.py` exists | `python manage.py migrate` | Directory containing `manage.py` |
| `prisma/schema.prisma` exists | `npx prisma migrate deploy` | Project root |
| EF `Migrations/` directory | `dotnet ef database update` | Project root |
| Rails `db/migrate/` directory | `rails db:migrate` | Project root |
| Sequelize `migrations/` + `.sequelizerc` | `npx sequelize-cli db:migrate` | Project root |
## Execute via the Deployed Environment
Migrations run where the managed identity lives — inside the app, not from your workstation.
> ⛔ **Authenticate with a token, never a password — and PREFER the app's own credential.**
> - **(1) Preferred — `DefaultAzureCredential`:** if the app's DB config uses `DefaultAzureCredential` / `Authentication=Active Directory Default`, the migration command works **unchanged** and authenticates as the app MI. No token handling, no `curl`/`jq`, works on slim/distroless images. Use this whenever possible.
> - **(2) Fallback — inject an MI token:** only if the app cannot self-authenticate. Fetch a token from the container's IMDS endpoint and pass it as the driver password env var (see below). ⛔ This requires `curl` + `jq` (or `wget`) **in the image** — absent on `slim`/`distroless` builds. If the tools aren't present, do NOT hand-roll it: surface a `FLAGGED` finding + `postDeployRecommendation` to add `DefaultAzureCredential` to the app's DB config.
> - ⛔ Never re-enable password auth to unblock.
### App Service (Linux only)
```powershell
az webapp ssh -n {app} -g {rg} --subscription {sub}
```
Then, **inside the SSH session** (the container's own shell — no PowerShell layer), for the token-injection fallback (requires `curl`+`jq` in the image):
```bash
export PGPASSWORD="$(curl -s "$IDENTITY_ENDPOINT?resource=https://ossrdbms-aad.database.windows.net&api-version=2019-08-01" -H "X-IDENTITY-HEADER: $IDENTITY_HEADER" | jq -r .access_token)"
export PGUSER={appMiName} PGSSLMODE=require
{migration_command}
```
### Container Apps
**Preferred (DefaultAzureCredential — nothing to inject, works on distroless):**
```powershell
az containerapp exec -n {ca} -g {rg} --subscription {sub} --command '{migration_command}'
```
**Fallback (token injection; image must have `curl`+`jq`):**
```powershell
# ⛔ SINGLE-QUOTE the whole --command so $(...) and $IDENTITY_* evaluate IN THE CONTAINER, not on the deployer.
# (A double-quoted PowerShell string would run curl locally and expand $IDENTITY_* to empty → empty PGPASSWORD.)
az containerapp exec -n {ca} -g {rg} --subscription {sub} --command 'sh -lc "export PGPASSWORD=$(curl -s \"$IDENTITY_ENDPOINT?resource=https://ossrdbms-aad.database.windows.net&api-version=2019-08-01\" -H \"X-IDENTITY-HEADER: $IDENTITY_HEADER\" | jq -r .access_token); export PGUSER={appMiName} PGSSLMODE=require; {migration_command}"'
```
> Preferred over both: bake the migration into the app's startup (`initCommands[]` → `appCommandLine`) using the app's own `DefaultAzureCredential` config, so migrations run token-based on every cold start (idempotent) with no exec step and no `curl`/`jq` dependency.
## Error Handling
If the migration command fails, classify as `IAC_ERROR` and check based on the database type:
- **AAD token / auth failure** (`password authentication failed`, `Login failed for token-identified principal`) → the app MI was not granted a DB role (§2), or Entra-admin/RBAC propagation hasn't completed. Verify the app MI principal exists, wait 60s, retry.
- DB unreachable → check firewall rules (PostgreSQL: `AllowAllAzureServicesAndResourcesWithinAzureIps`, SQL: server firewall, MySQL: similar)
- Extension/feature missing → check DB-specific config (PostgreSQL: `azure.extensions`, SQL: compatibility level, MySQL: `require_secure_transport`)
- Module not found → verify the runtime includes the migration tool
> ⛔ **Never weaken auth to unblock.** Do NOT set `passwordAuth: 'Enabled'`, add an `administratorLoginPassword`, or re-enable access keys to make a failing migration pass. Fix the grant (§2) or the client token config.
## PostgreSQL-Specific Checks
Run BEFORE migrations when `services[]` includes PostgreSQL Flexible Server:
1. **Token connectivity:** `az postgres flexible-server execute -n {pg} -g {rg} -u "{entraAdminName}" -p $dbToken -d postgres --querytext "SELECT 1"` — if this fails, the firewall rule is missing, the deployer is not the Entra admin, or AAD propagation hasn't completed. Check `AllowAllAzureServicesAndResourcesWithinAzureIps` exists and the `administrators` child deployed, wait 60s, retry
2. **Extension availability:** `az postgres flexible-server parameter show -g {rg} -n {pg} --name azure.extensions --query value -o tsv` — verify the extensions the app needs (e.g., `uuid-ossp` for Alembic/Django UUID fields) are in the allow-list. If missing, the Bicep module should have set them — check `infra/modules/postgresql.bicep`
## MySQL Flexible Server Checks
Run BEFORE migrations when `services[]` includes MySQL Flexible Server:
1. **Token connectivity:** `az mysql flexible-server execute -n {mysql} -u "{entraAdminName}" -p $dbToken -d mysql -q "SELECT 1"` — if this fails, check the firewall rule, that the Entra admin is set, and AAD propagation. Note: `execute` resolves by server name (no `-g` needed)
2. **SSL enforcement:** `az mysql flexible-server parameter show -g {rg} -n {mysql} --name require_secure_transport --query value -o tsv` — verify matches the app's connection SSL mode
## Azure SQL Checks
Run BEFORE migrations when `services[]` includes Azure SQL:
1. **Database online:** `az sql db show -g {rg} -s {sqlServer} -n {dbName} --query status -o tsv` — verify the database is `Online`
2. **Server firewall:** `az sql server firewall-rule list -g {rg} -s {sqlServer} -o table` — verify `AllowAllWindowsAzureIps` (0.0.0.0 → 0.0.0.0) exists for Azure-internal access
3. **Entra-only auth:** verify `azureADOnlyAuthentication` is `true` and the app MI has a contained user (§2)
@@ -0,0 +1,111 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Deploy Checklist Template (compaction-safe — generated at Step 5b)
Long-running deploy sessions lose rules when the conversation compacts. At Step 5b, generate a checklist file tailored to this deployment. Write it to disk so it survives compaction — re-reading costs ~100 tokens.
**Write** to `.copilot-azure/sessions/{id}/deploy-checklist.md` using the `create` tool at Step 5b.
**Re-read** via `view` after every long-running command (`az deployment`, `az webapp deploy`, `az acr build`), after each failed health check, and after any conversation compaction.
## How to generate
Read `prepare-plan.json` to determine the service types, then build the checklist from the template below. **Replace `{placeholders}` with real values** and **delete sections that don't apply** (e.g., remove the App Service section for a Container Apps deploy).
```markdown
# Deploy Checklist for {appName}
# RG: {rgName} | Sub: {subscriptionId} | Session: {sessionId}
## ⛔ Secret generation (BEFORE first az deployment)
- ⛔ **No database/cache/storage passwords or access keys exist** — those services are Entra/managed-identity only (no `@secure()` DB password params, no `administratorLoginPassword`). See [database-post-deploy.md](database-post-deploy.md).
- Auto-generate ONLY app-internal `@secure()` params (e.g. `SECRET_KEY`, JWT key) before first `az deployment sub create` — NEVER `ask_user`
- ⛔ On ANY retry OR redeploy (incl. after a conversation compaction): read the SAME `@secure()` value back from `deploy-secrets.env` (or `deploy-audit.log`) — NEVER regenerate. Pass it to EVERY deployment. An app-internal secret that's regenerated overwrites the live on-compute value → auth 500s while provisioning still reports success.
## ⛔ Read deploy/instructions.md
- You MUST `view` deploy/instructions.md BEFORE running any `az deployment` command
- Path: `.github/agents/azure-deploy/deploy/instructions.md`
- If you have not read it in this conversation (or since the last compaction), read it NOW
- It covers preflight checks, portal links, what-if, SCM lifecycle, deploy-result.json schema, audit logging, and health checks — skip it and none of these happen
## After every `az` command
- Append 2 lines to `deploy-audit.log`: `{timestamp} | {command} | started` then `{timestamp} | {command} | succeeded/failed`
## After IaC deployment (Step 6)
- ⛔ Call `capture_deployment_inventory` (`phase: "capture"`) after the deployment command returns — records created resources via a before/after `resources.list()` diff and flags orphans
- Verify 5 tags: `az group show -n {rgName} --query tags`
- ⛔ Do NOT set startup command or app settings via CLI — they are already in Bicep from scaffold. If `az webapp show` doesn't reflect them yet, wait 30s and re-check (ARM propagation delay). Do NOT run `az webapp config` imperatively.
Required: app-onboard-skill, app-onboard-session-id, created-at, environment, deployed-by
- Verify portal link is still correct if healing changed the deployment name
## Code deploy — Functions (Flex Consumption) (delete if not using Flex Functions)
- ⛔ **Read [`code-deployment-functions-flex.md`](code-deployment-functions-flex.md).** Flex is NOT App Service: no SCM/Kudu, no `basicPublishingCredentialsPolicies`, no `SCM_DO_BUILD_DURING_DEPLOYMENT`/`ENABLE_ORYX_BUILD`.
- Deploy the package: `az functionapp deployment source config-zip --subscription {sub} -g {rgName} -n {appName} --src {zip} [--build-remote true]` (remote build for Python/Node) — or `func azure functionapp publish {appName}`.
- ⛔ **Local (non-CI) deploy warning — say it to the user:** hardening network access can block publishing from your machine. Deploy with access OPEN, verify health, THEN harden. An ipify-derived `/32` SCM allow rule will NOT match Flex egress (platform gateway pool) — do not try it.
- ⛔ A trailing `Deny all` rule (priority `2147483647`) in `ipSecurityRestrictions`/`scmIpSecurityRestrictions` is the expected platform sentinel — NOT an error.
- The app identity needs a Storage Blob Data role on the deployment storage (shared-key is disabled).
## Code deploy — App Service (delete if not using App Service)
- ⛔ **Deploy command: `az webapp deploy --type zip`** (Entra-capable, supports `--async`). NEVER `az webapp deployment source config-zip` — it needs SCM basic auth and is disallowed.
- Wait for stabilization: `az webapp show -g {rgName} -n {appName} --query state` → "Running"
- Verify `SCM_DO_BUILD_DURING_DEPLOYMENT=true` is active before deploy (ARM timing can delay)
- If build reports "0 seconds" but app needs deps: re-set the setting, wait 10s, retry
- If 0s persists after 2 retries: fall back to Kudu `/api/zipdeploy`
- Python: if no `antenv/` after deploy, use Kudu `/api/zipdeploy` immediately (OneDeploy may skip Oryx)
- Windows zip paths: normalize with `.Replace('\', '/')` before creating zip entries
- ⛔ Verify ORYX_DISABLE_COMPRESSION=true is set (prevents output.tar.zst extraction failures at startup — applies to ALL tiers, not just F1)
- Set WEBSITES_CONTAINER_START_TIME_LIMIT=1800 for safety
- TypeScript apps: verify `typescript` and `@types/*` are in `dependencies` (not devDependencies) — Oryx with NODE_ENV=production skips devDeps. Alternative: set app setting NPM_CONFIG_PRODUCTION=false
- Enable SCM before zip deploy, re-disable after: `az rest --method put` → allow:false → verify
- After deploy: check response body for Azure default page ("Your app service is up and running" = app didn't start)
## Code deploy — Container Apps (delete if not using Container Apps)
- Phase 2 is NOT optional — deploy actual image, don't leave placeholder
- Wait ~60s for RBAC propagation (AcrPull role) before code deploy
- BuildKit Dockerfiles: create Dockerfile.azure without --mount syntax
- Pass real image on EVERY Bicep redeploy: --parameters containerImage='{acr}/{app}:latest'
- Native CA secrets: `revision restart` does NOT refresh — must create new revision
- ACR build failures count toward healing counter
- Windows: append `--no-logs` to `az acr build` to avoid UnicodeEncodeError
- ⛔ After the revision is ready, run an explicit live HTTP probe against the ingress FQDN (`curl -sSfL`/`iwr` on `*.azurecontainerapps.io`) — this call IS the health check; capture the result into `deploy-result.json.endpoints[].healthStatus`.
## Code deploy — Static Web Apps (delete if not using SWA)
- ⛔ **Build before deploy (SPA only):** If the SWA component has a manifest (`package.json`) with a `build` script: detect the package manager from the lockfile, run install + build, then deploy the build output directory (not raw source). Plain HTML repos (no manifest): skip build, deploy source directly.
- ⛔ **Build-time env vars:** Before `npm run build`, set `VITE_API_BASE_URL` / `NEXT_PUBLIC_API_URL` / `REACT_APP_API_URL` to the deployed backend URL from `deploy-result.json.endpoints[]`. These are baked into the JS bundle at build time — runtime SWA env vars have no effect on client-side code.
- If frontend config references cloud SDK endpoints (AWS API Gateway, GCP), update with deployed Azure backend URLs from `deploy-result.json.endpoints[]` before `swa deploy`
- Use `swa deploy` (NOT `az staticwebapp deploy` — doesn't exist)
- `--app-name {swaName}` is mandatory
- Store token in $env:SWA_CLI_DEPLOYMENT_TOKEN — never as CLI arg
## During healing / retries
- ⛔ REGION LOCK: Deploy region MUST match plan region ({region}). Any region change → RE-PRESENT deploy approval gate with old and new region. Do NOT silently switch. After approval: update `prepare-plan.json.services[].region`, `deploymentVariables.location`, AND append attempt number to `naming.suffix` (e.g., `edd6` → `edd602`). Recompute ALL resource names from the new suffix before redeploying — globally unique names (App Service, Storage, ACR) from the old region may be soft-deleted and unavailable.
- ⛔ IaC-only: NEVER use `az containerapp update --image`, `az webapp update`, `az appservice plan delete`, or `az group create` — fix the Bicep and redeploy via `az deployment sub create`
- ⛔ IaC-only for app-managed roles: NEVER `az role assignment create` for AcrPull or KV Secrets User — Bicep-managed (deterministic GUID), so an imperative grant collides on redeploy (`RoleAssignmentExists`). Missing app role = fix the Bicep module and redeploy. (Deployer/subscription-scope 403s are the ONLY exception — see [`error-classification.md`](error-classification.md).)
- ⛔ **On error: read [`error-classification.md`](error-classification.md)** to classify the failure and follow the prescribed remediation. Do NOT ad-hoc heal without reading the classification.
- ⛔ **Never weaken a security control to unblock** — do NOT flip `require_secure_transport`/TLS, HTTPS-only, KV purge protection, or auth OFF to make a failing deploy pass. A DB TLS handshake failure = fix the client SSL config (prereq `W-MYSQL-SSL`) or ask the user; never downgrade the server.
- Count ALL attempts in deploy-result.json.healingAttempts[]
After 3: STOP and ask user ("Yes / I have a suggestion / Stop")
- NEVER run `az group delete` — track in orphanedResourceGroups[] (run `capture_deployment_inventory` after the healing attempt so the abandoned RG's resources are recorded deterministically)
- ⛔ **RG deletion timeout:** If you ran `az group delete --no-wait`, wait max 2 minutes then `ask_user`: "Resource group deletion is slow. Wait longer / Proceed without cleanup / Cancel." Do NOT poll indefinitely.
- Region/SKU/service changes require re-approval gate
## Before handoff (Step 8)
- ⛔ Read [`deploy-schemas.ts`](deploy-schemas.ts) for exact DeployResult field names
- ⛔ Run a final `capture_deployment_inventory` (`phase: "capture"`) — then populate `createdResources[]` and `orphanedResourceGroups[]` from its returned output (do NOT hand-author them)
- Finalize `deploy-result.json` — overwrite skeleton IN PLACE (keep exact field names, do NOT rename): status (lowercase `succeeded`/`failed`), resourceGroupName, subscriptionId, deploymentNames (all used), resourceIds, endpoints, healthStatus (worst across endpoints), duration.completedUtc, resourceResults from `az deployment operation list`, createdResources + orphanedResourceGroups from the inventory. Read back to verify.
- ⛔ `deployment-summary.md` — generate from `deploy-result.json` fields (Status, Health, Portal Links, Cleanup). NOT a separate data source.
- ⛔ `context.json` — add "deploy" to completedPhases, set currentPhase to null, update lastModifiedUtc. VERIFY by reading back.
- SCM re-disabled (App Service) or image param set (Container Apps)
- If prereq found migration frameworks: run migrations before declaring healthy
## Artifact verification (Step 8 — MANDATORY)
⛔ Before returning to orchestrator, verify ALL artifacts exist by reading each one back:
1. `deploy-result.json` — MUST contain (exact names): `status` (lowercase `succeeded`/`failed`), `resourceGroupName`, `subscriptionId`, `deploymentNames[]`, `resourceIds[]`, `endpoints[]`, `healthStatus`, `duration.completedUtc`, `resourceResults[]`, `createdResources[]`, `orphanedResourceGroups[]`. `createdResources[]`/`orphanedResourceGroups[]` come from a final `capture_deployment_inventory` (`phase: "capture"`) — if empty or missing, run it NOW and write its output. Missing/renamed fields → rewrite with real values NOW
2. `deploy-audit.log` — MUST exist with ≥2 entries (started + result for at least 1 command). Missing → reconstruct from memory
3. `deployment-summary.md` — MUST contain Status, Health, Portal Links sections. Missing → generate from deploy-result.json
4. `context.json` — MUST have `"deploy"` in `completedPhases`, `currentPhase: null`, updated `lastModifiedUtc`
5. ⛔ **Endpoint completeness** — EVERY service in `prepare-plan.json.services[]` that hosts application code MUST have a corresponding entry in `deploy-result.json.endpoints[]` with code deployed and a valid `healthStatus` (`healthy`, `degraded`, `unreachable`, `unknown`). If ANY compute endpoint is missing or has code not deployed, set `partial: true` and `status: "failed"`. A deployment with undeployed user components is NOT `"succeeded"`.
If ANY artifact is missing or incomplete, write it NOW — do NOT return to orchestrator without all 5 checks passing.
⛔ **Then STOP — return to orchestrator. No further CLI commands or agent invocations.**
```
@@ -0,0 +1,66 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Deploy Safety
Hook-based safety rules for the deploy phase. Block destructive operations.
## deploy-result.json Skeleton
Created by scaffold validate sub-agent. If missing at deploy Step 5b, create from [`deploy-schemas.ts`](deploy-schemas.ts) with `status: "in-progress"`. Append to `deploymentNames[]` on each healing retry.
## Deploy Checklist (compaction-safe — generated at Step 5b)
> ⛔ **You MUST read [`deploy-checklist-template.md`](deploy-checklist-template.md)** at Step 5b to generate the checklist. Write the result to `.copilot-azure/sessions/{id}/deploy-checklist.md`. Re-read the generated checklist after every long-running command, failed health check, and conversation compaction.
## Finalize deploy-result.json (Step 8)
Overwrite the skeleton with real values:
- `status` → `"succeeded"` or `"failed"`
- `deploymentNames` → ALL names used (initial + retries)
- `healthStatus` → worst across endpoints
- `duration.completedUtc` → now
- `resourceResults` → one entry per resource from `az deployment operation list`
## Blocked Patterns
> ⛔ **You MUST read [`blocked-patterns.md`](blocked-patterns.md)** before running ANY `az` command during the deploy phase. This file contains every command the agent is forbidden from executing. Block decisions are non-negotiable — user must run blocked commands manually outside AppOnboard.
## 403 Scope Fallback
When `az deployment sub create` returns 403 (insufficient subscription-scope permissions), do NOT halt immediately:
1. **Restructure Bicep to RG-scope** — change `targetScope = 'subscription'` to resource-group scope, remove the `Microsoft.Resources/resourceGroups` resource.
2. **Create the RG via CLI** — `az group create -n {rg} -l {region} --tags app-onboard-skill=true app-onboard-session-id={sessionId} created-at={createdAt} environment={environmentName} deployed-by={deployedBy}`. All 5 AppOnboard tags MUST be included.
3. **Retry with `az deployment group create`** — use `--resource-group {rg}` instead of subscription scope.
4. **Regenerate portal link** for RG-scope — `$resId` must include `/resourceGroups/{rg}`: `$resId = "/subscriptions/{subscriptionId}/resourceGroups/{rg}/providers/Microsoft.Resources/deployments/$deploymentName"`. Re-run `Write-Output "LINK=$l"` and print the new bare URL. ⛔ Do NOT auto-open it (no `Start-Process`).
5. **If retry ALSO fails with 403** → classify as `ENVIRONMENT_BLOCKING`. Surface required role: `az role assignment create --role Contributor --assignee {user} --scope /subscriptions/{sub}/resourceGroups/{rg}`.
## Deploy Checklist
> ⛔ **Use sync shells** so state persists. **App-internal secrets only** (e.g. `SECRET_KEY`, JWT signing key, third-party API keys) are generated at deploy and passed as `@secure()` params → stored on the compute resource (App Service app settings / Container Apps native secrets). ⛔ **No Key Vault.** ⛔ **There are NO database/cache/storage passwords or access keys** — those services are managed-identity + token (see [database-post-deploy.md](database-post-deploy.md)). Persist any app-internal secret to `.copilot-azure/sessions/{id}/deploy-secrets.env` — generate each ONCE (URL-safe, no `/+=`), reload in every later shell, and pass to EVERY deployment. Never regenerate an existing key (the desired-state apply would overwrite the live value). The file is a git-ignored cross-shell reload cache. NEVER echo or log rendered secret values.
>
> ⛔ **URL-safe app-internal secrets** when embedded in URLs. Forbidden chars: `# @ / ? % : & = + ;`.
> ⛔ **`az webapp deploy` does NOT support `--track-status`.**
> ⛔ **`az rest` on Windows PowerShell:** ALWAYS include `--headers "Content-Type=application/json"`.
> ⛔ **Suppress deployment output:** Add `--query properties.provisioningState -o tsv` to deployment commands. For `az acr build`, append `--no-logs`.
## Post-Deploy Tag Verification
After deployment, verify all 5 AppOnboard tags: `az group show -n {rg} --query tags -o json`. Re-apply missing via `az tag update`.
## Deployment Operation Polling
For deployments with >5 resources, poll every 30s: `az deployment operation list --name {name} --subscription {sub} --query "[?properties.provisioningState=='Failed']" -o table`. Wait for FULL completion before healing — collect ALL errors in one pass.
## Re-Approval Gates
Region, service type, or SKU changes from user-approved values → re-present approval gate. Resource name changes → informational only. Same-deployment retries → no re-approval.
## Antipatterns
⛔ Do NOT `az group delete --no-wait` then `az group create` same name — background deletion takes 5-15 min and destroys the new RG. Use a different name or wait: `az group wait --name {rg} --deleted --timeout 900`.
## Artifact Reconciliation After Healing
After ANY healing that changes deployed resources, update `prepare-plan.json`, `scaffold-manifest.json`, and `context.json` to reflect actual state. Track orphaned RGs in `deploy-result.json.orphanedResourceGroups[]` immediately when switching to a new RG.
@@ -0,0 +1,150 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Deploy artifact schema — deploy-result.json.
* Read by deploy instructions.md Step 8 (finalize artifacts).
*/
export type HealthStatus = "healthy" | "degraded" | "unreachable" | "unknown";
// ─── Deploy healing types ────────────────────────────────────────────────────
export type DeployErrorClassification = "IAC_ERROR" | "INFRA_TRANSIENT" | "ENVIRONMENT_BLOCKING";
export type DeployHealingPhase = "validation" | "deployment";
/** Tracks a resource group created during a healing attempt that is no longer
* the final deployment target. Surfaced at handoff for manual cleanup. */
export interface OrphanResourceGroup {
/** Azure resource group name */
name: string;
/** Region where the RG was created. Omitted when derived by the deterministic
* `capture_deployment_inventory` diff, which doesn't track a region. */
region?: string;
/** Which healing attempt created or targeted this RG. Omitted when derived by the
* deterministic `capture_deployment_inventory` diff rather than a tracked healing attempt. */
healingAttempt?: number;
/** Why this RG was abandoned (e.g., "region fallback to westus2") */
reason: string;
}
export type DeployHealingAction = "routed-to-scaffold" | "retried" | "surfaced-to-user";
export type DeployHealingResult = "fixed" | "still-failing" | "blocked";
// ─── Deterministic resource inventory (capture_deployment_inventory) ─────────
/** How a resource created during this session relates to the tracked deployment(s).
* Computed deterministically by the `capture_deployment_inventory` MCP tool from a
* before/after `resources.list()` diff — NOT inferred from chat history. */
export type CreatedResourceClassification =
/** Reported `Succeeded` by a tracked deployment and located in the final target RG.
* Part of the working deployment — never a cleanup candidate. */
| "expected"
/** Reported by a tracked deployment but with a non-succeeded provisioning state.
* Confirmed to belong to this deployment, so a delete command may be offered. */
| "failed"
/** Appeared during the deploy window but no tracked deployment reported it, or it landed
* outside the final target RG. Probably a healing retry or an imperative `az` fallback —
* but on a shared subscription it may belong to someone else entirely. Surface for the
* user to REVIEW; never describe it as safe to delete. */
| "orphaned"
/** The tracked deployment's ARM operations could not be read (permissions, throttling,
* transient failure), so nothing could be attributed. Never a cleanup candidate. */
| "unverified";
/** A resource that exists now because of this session (post − baseline diff). */
export interface CreatedResource {
/** Full ARM resource ID. */
id: string;
name?: string;
type?: string;
/** Resource group parsed from the ARM ID. */
resourceGroup?: string;
/** Provisioning state reported by the deployment operations, when known. */
provisioningState?: string;
classification: CreatedResourceClassification;
}
export interface DeployHealingError {
source: string;
detail: string;
classification: DeployErrorClassification;
}
export interface DeployHealingAttempt {
attempt: number;
phase: DeployHealingPhase;
errors: DeployHealingError[];
action: DeployHealingAction;
result: DeployHealingResult;
/** Resource group targeted by this attempt — useful for audit and debugging */
resourceGroupName?: string;
/** True when this healing attempt changed the service type or region — requires re-approval */
planLevelChange?: boolean;
/** What changed: "service-type", "region", "sku" */
changeType?: "service-type" | "region" | "sku";
/** Original value before the change (e.g., "App Service B1 eastus") */
originalValue?: string;
/** New value after the change (e.g., "Container Apps Consumption eastus") */
newValue?: string;
}
// ─── deploy-result.json ──────────────────────────────────────────────────────
export interface DeployEndpoint {
name: string;
url: string;
healthStatus: HealthStatus;
}
export interface DeployDuration {
startedUtc: string;
completedUtc: string;
}
export type DeployStatus = "in-progress" | "succeeded" | "failed";
export type ResourceDeployStatus = "succeeded" | "failed" | "skipped";
export interface ResourceResult {
resourceId: string;
type: string;
status: ResourceDeployStatus;
error?: string;
}
export interface DeployResult {
sessionId: string;
/** Azure subscription ID from context.json.azure.subscriptionId */
subscriptionId: string;
/** All ARM deployment names used during this session (initial + healing retries).
* First entry is the initial deployment; subsequent entries are from scope/RG changes. */
deploymentNames: string[];
resourceGroupName: string;
status: DeployStatus;
resourceIds: string[];
endpoints: DeployEndpoint[];
healthStatus: HealthStatus;
duration: DeployDuration;
warnings: string[];
partial: boolean;
resourceResults: readonly ResourceResult[];
/** Resources that exist now because of this session, computed deterministically by
* `capture_deployment_inventory` (before/after `resources.list()` diff). Includes
* expected, failed, and orphaned resources — the source of truth for the handoff
* cleanup section. Populated at Step 8 and on the Step 9 failure path. */
createdResources: readonly CreatedResource[];
/** RGs created during healing that are not the final deployment target.
* Derived from `createdResources` (resources whose RG != the final target RG).
* Surfaced at handoff (Step 9) with manual cleanup commands. */
orphanedResourceGroups: readonly OrphanResourceGroup[];
/** Set when `capture_deployment_inventory` could not read the deployment's ARM operations,
* so `createdResources` could not be attributed. When true, present NO cleanup list and
* tell the user to review the resource group in the portal. */
inventoryUnverified?: boolean;
/** Why verification failed: `"forbidden"` (missing
* `Microsoft.Resources/deployments/operations/read`), `"throttled"`, or `"error"`. */
inventoryUnverifiedReason?: string;
healingAttempts?: readonly DeployHealingAttempt[];
}
@@ -0,0 +1,62 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Error Classification
Three error categories for deploy-time failures.
## Multi-Error Triage
> ⛔ When 3+ errors, classify ALL before fixing ANY. Group by root cause. Present: "Deploy failed with {N} errors from {M} root causes."
## Categories
### `IAC_ERROR` — Route Back to Scaffold
| Example | Fix |
|---------|-----|
| Invalid property name | Call `mcp_bicep_build_bicep` with `{ filePath: "infra/main.bicep" }` for structured error details, then fix. Fallback: `az bicep build` |
| Wrong SKU for region | Substitute from `rejectedAlternatives[]` |
| Missing required field | Call `mcp_bicep_build_bicep` for structured error, fix from diagnostics. Fallback: `az bicep build` |
| Policy violation | Substitute per policy |
| API version not found | Call `mcp_bicep_list_az_resource_types_for_provider` with `{ providerNamespace: "..." }`. Use latest GA — no `-preview`. Fallback: `az provider show` |
| `listKeys()` in output | ⛔ Security risk — replace with managed-identity access (no shared keys, no Key Vault) |
| Redis `InvalidRequestBody` for `properties.sku.name` | Bicep type issue — create via `az redis create --sku Basic --vm-size c0`, switch Bicep to `existing` keyword |
**Flow:** Deploy → classifies IAC_ERROR → scaffold self-healing → re-validate → retry.
### `INFRA_TRANSIENT` — Retry with Backoff
| Example | Strategy |
|---------|----------|
| ARM 429, 409 conflict, RBAC delay, network timeout | 30s → 60s → 120s (3 max) |
| Container Apps `Operation expired` | Check root cause first: image pull failure, port mismatch, health probe timeout, crash loop. Port mismatch → IAC_ERROR. Image pull → retry. Crash → ENVIRONMENT_BLOCKING. |
After 3 failures → escalate to user.
### `ENVIRONMENT_BLOCKING` — Surface to User
| Example | Action |
|---------|--------|
| 403 on sub-scope deploy | ⛔ Try [403 Scope Fallback](deploy-safety.md) first (RG-scope). Only block if retry also fails |
| 403 on RG-scope | Surface: `az role assignment create --role Contributor --assignee {objectId} --scope {rg}` |
| AuthorizationFailed / deny assignment | Get user objectId, suggest role assignment command. Deny assignments → contact admin |
| Quota exhausted | ⛔ PLAN_LEVEL_CHANGE — HALT, present region fallback with re-approval |
| Region doesn't support resource | ⛔ PLAN_LEVEL_CHANGE — same as quota |
| `LocationIsOfferRestricted` | ⛔ PLAN_LEVEL_CHANGE — read `quotaValidation.offerRestrictions[]` for unblocked regions |
| MI sidecar OOM on F1/B1 | Upgrade SKU, remove MI, or switch to Container Apps |
| AADSTS530084 / TF auth failure | Re-scaffold as Bicep + `az deployment group create`. Never fall back to imperative CLI |
> ⛔ **During ALL healing: NEVER run `az group delete`, `az postgres flexible-server delete`, `az redis delete`, `az webapp delete`, or any destructive resource deletion.** Track failed RGs in `deploy-result.json.orphanedResourceGroups[]` instead. The user deletes at handoff. If you need a clean region retry, use a NEW RG name (append `-2`) — do NOT delete-and-recreate. ⛔ **After each failed/healing `az deployment`, call `capture_deployment_inventory` (`phase: "capture"`, passing every `resourceGroups[]` including the abandoned one)** so orphaned resources from the healing retry are recorded deterministically rather than left for you to remember.
## Healing Trace
Log to `deploy-result.json.healingAttempts[]`: `{ attempt, phase, errors: [{ source, detail, classification }], action, result, planLevelChange, changeType, originalValue, newValue }`.
> ⛔ **Repeat failure (same error 2+):** Read [iac-resources.md](../../references/iac-resources.md) § Deploy Troubleshooting and `fetch_webpage` the matching URL.
After 3 failed cycles: write `partial: true`, surface remaining errors. Do NOT auto-rollback.
## Known Platform Bugs / Deploy Timing
See [`pipeline-rules-runtime.md`](../../references/pipeline-rules-runtime.md).
@@ -0,0 +1,98 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Health Check Patterns
Post-deployment health verification for AppOnboard-deployed resources.
## HTTP Endpoints
For each endpoint: HTTPS GET, 30s timeout, 3 retries (10s/20s/40s backoff).
| Status | Health | Note |
|--------|--------|------|
| 2xx | `healthy` | **Verify not a placeholder page (see below)** |
| 401/403 | `healthy` | Auth working, app running |
| 5xx ×3 | `degraded` | |
| Timeout/DNS ×3 | `unreachable` | |
> ⛔ **DB-backed apps: a 200 on `/` is NOT healthy.** When `prepare-plan.json.services[]` includes a database, probing only `/` (or any non-DB route) just proves the web server booted. Probe at least one **data-backed route** (derive from the app's detected routes, e.g. a REST resource path) and inspect the body for DB errors (`insecure transport`, `Access denied`, `connection refused`, `Unknown database`, `doesn't exist`) → mark `degraded`, not `healthy`.
### HTTP Redirect Handling (Container Apps)
> ⛔ **ACA health probes do NOT follow HTTP redirects.** A 301/302 response from the probe path causes `ActivationFailed` — the probe treats it as a failure, not a redirect.
If the first health check returns **301 or 302**:
1. Read the `Location` header: `curl -sI "https://{fqdn}{probePath}" | Select-String "^location:" -CaseSensitive:$false`
2. Update `probePath` in Bicep to the redirect target (e.g., `/wetty/` → `/wetty`)
3. Redeploy: `az deployment sub create` with updated Bicep
4. Re-check health after new revision activates
Common redirect patterns: Express trailing-slash normalization (`/app/` → `/app`), framework-level path canonicalization, HTTPS redirects on mixed-content paths.
### App Service Default Page Detection
> ⛔ **HTTP 200 ≠ app started.** Azure serves its own default page with 200 when the app fails to start — false positive.
After HTTP 200 from App Service, check first 2KB of body:
| Body contains | Meaning |
|---------------|--------|
| `"Your app service is up and running"` | Default page — app didn't start |
| `"Time to take the next step and deploy your code"` | Default page — no code or app failed |
| `"Hey, Python developers!"` / `"Hey, Node.js developers!"` | Runtime default — app didn't start |
| `"Error 503"` / `"Application Error"` | App crashed on startup |
If detected → `healthStatus: "degraded"` + warning: `"App Service default page detected — check logs: az webapp log tail -g {rg} -n {app}"`.
## Non-HTTP Resources
```bash
az resource show --ids {resourceId} --query "properties.provisioningState" -o tsv
```
`Succeeded` → `healthy`. `Failed` → `degraded`. Other → `unknown`.
| Service | Health Signal |
|---------|---------------|
| Container Apps | `latestReadyRevisionName` not empty + HTTP on ingress FQDN |
| App Service | HTTP GET `https://{name}.azurewebsites.net/` + `/health` |
| Static Web Apps | HTTP GET `https://{defaultHostname}/` → 2xx = `healthy` (hostname: `az staticwebapp show -n {swa} -g {rg} --query defaultHostname -o tsv`) |
| Azure SQL | `provisioningState` + `az sql db show` |
| Cosmos DB | `provisioningState` |
| Storage | `provisioningState` + `statusOfPrimary` |
| Functions | HTTP trigger URL + HTTP check |
> ⛔ **Container Apps — run an explicit live HTTP probe (the pipeline status is NOT sufficient).** After `latestReadyRevisionName` is set, run an observable request against the ingress FQDN and capture the result into `deploy-result.json.endpoints[].healthStatus`:
> ```powershell
> iwr "https://{ingressFqdn}/{probePath}" -UseBasicParsing # PowerShell
> curl -sSfL "https://{ingressFqdn}/{probePath}" # bash
> ```
> This live HTTP call against `*.azurecontainerapps.io` IS the health verification — do NOT infer health from the revision's internal status alone.
## Output
Write to `deploy-result.json.endpoints[]`:
```jsonc
{
"name": "api",
"url": "https://myapp-ca-dev-a1b2.azurecontainerapps.io",
"healthStatus": "healthy" // healthy | degraded | unreachable | unknown
}
```
Overall `healthStatus` = worst status across all endpoints. If any `unreachable` → overall `unreachable`.
## Functional Endpoint Verification
> ⛔ **A 200 on `/` only proves the web server booted — not that the app works.** After the HTTP check, confirm the app actually functions against the services the plan provisioned (database, cache), not just that it responds.
Health checks only confirm the web server is responding. Exercise a route that depends on the backing services — for example:
| Pattern | Functional Check |
|---------|-----------------|
| Database in the plan (MySQL/PostgreSQL/SQL/Cosmos) | Probe a route that reads/writes the DB (a detected app route, NOT `/` — root often serves a static page with no DB access, so 200 on `/` masks broken DB connectivity). A 5xx or a DB error in the body (`connection refused`, `does not exist`, token/auth failure) → `degraded` — usually the app MI wasn't granted a DB role (see [database-post-deploy.md](database-post-deploy.md) § 2). |
| `FIRST_SUPERUSER` env var or `prestart.sh`/`init_db()` | After health passes, attempt login endpoint. If 401/500 → startup scripts may have failed. Trigger `az containerapp revision restart` to re-run startup. |
| Migration frameworks (Alembic, Django, Prisma, EF) | After health passes, check `prereq-output.json` for migration signals. If found, run migrations per [database-post-deploy.md](database-post-deploy.md). |
| Two-phase Container Apps (managed identity) | Wait 60s after Phase 2 for AcrPull RBAC propagation. If the DB fails with auth/token errors, the app MI may not yet have its DB role — grant it (see [database-post-deploy.md](database-post-deploy.md)) and create a new revision. |
@@ -0,0 +1,104 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Deploy Phase — MCP Tools
Phase-exclusive tool parameters for the deploy phase. For shared tools (`subscription_list`, `group_list`, `extension_cli_install`, `get_azure_bestpractices`), see [mcp-tool-reference.md](../../references/mcp-tool-reference.md).
> **Troubleshooting:** If a tool call fails with unknown parameter or missing command errors, consult the official docs: <https://learn.microsoft.com/en-us/azure/developer/azure-mcp-server/tools/>
---
## `capture_deployment_inventory` (Copilot on Rails extension tool)
Deterministic resource-inventory capture provided by the Azure Resources extension (in-proc MCP, `copilot-azure-resources-extension-tools/*`). Snapshots the subscription's resources via ARM and diffs before/after to record exactly what this session created. Holds the baseline in memory (no workspace files). **Report-only — never deletes.**
| Param | Required | Description |
|-------|----------|-------------|
| `sessionId` | ✅ | App Onboard session id; locates `.copilot-azure/sessions/{id}/`. |
| `subscriptionId` | ✅ | Target subscription. |
| `phase` | ✅ | `"baseline"` (before first deployment) or `"capture"` (after each attempt / on failure / at finalize). |
| `expectedResourceGroup` | — | Final target RG; created resources outside it are flagged `orphaned`. |
| `deploymentNames` | — | All ARM deployment names (initial + healing) used to classify created resources. |
| `resourceGroups` | — | RGs touched (incl. abandoned healing RGs) so RG-scoped deployment operations are read. |
Holds the baseline snapshot in memory (no files written). On `phase: "capture"` returns the created resources classified `expected`/`failed`/`orphaned`/`unverified` plus orphaned resource groups; write those into `deploy-result.json.createdResources[]`/`orphanedResourceGroups[]`.
⛔ **Classification confidence differs, and your wording to the user MUST reflect it:**
| Classification | Meaning | How to present it |
|---|---|---|
| `expected` | Tracked deployment reported it as `Succeeded` in the target RG. | Part of the working deployment. Never suggest deleting it. |
| `failed` | Tracked deployment reported it with a non-succeeded state. | Confirmed to belong to this deployment. Safe to offer a delete command. |
| `orphaned` | Appeared during the deploy window but **no tracked deployment reported it**. | It *may* be a healing/imperative stray — or another person's resource on a shared subscription. Present as "review before deleting", never as "safe to delete" or "created by this deployment". |
| `unverified` | Deployment operations could not be read at all, so nothing could be attributed. | Say the inventory could not be verified and point the user at the portal. Do **not** produce any cleanup list or delete command. |
When the tool returns `inventoryUnverified: true`, record it in `deploy-result.json` as `inventoryUnverified` + `inventoryUnverifiedReason` and skip the cleanup section entirely.
See the [Phase 4 Tool Map](#phase-4-tool-map) below and [`../instructions.md`](../instructions.md) Steps 5b/6/8/9.
---
## `mcp_azure_mcp_deploy` (hierarchical — deploy-phase sub-commands)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `deploy_app_logs_get` | `workspace-folder`, `azd-env-name` | `limit` (default 200) | ✅ |
| `deploy_architecture_diagram_generate` | `workspaceFolder`, `services[]` (complex array with name, path, language, port, azureComputeHost, dependencies, settings) | `projectName` | ✅ |
## `mcp_azure_mcp_resourcehealth` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `resourcehealth_availability-status_get` | *(none)* | `subscription`, `resourceId` (full ARM ID), `resource-group` | ✅ |
| `resourcehealth_health-events_list` | *(none)* | `subscription`, `event-type` (ServiceIssue\|PlannedMaintenance\|HealthAdvisory\|Security), `status` (Active\|Resolved), `tracking-id`, `filter`, `query-start-time`, `query-end-time` | ✅ |
## `mcp_azure_mcp_monitor` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `monitor_activitylog_list` | `resource-name` | `resource-group`, `resource-type`, `hours`, `event-level` (Critical\|Error\|Informational\|Verbose\|Warning), `top`, `subscription` | ✅ |
| `monitor_metrics_query` | `resource`, `metric-names` (comma-sep), `metric-namespace` | `resource-group`, `resource-type`, `start-time`, `end-time`, `interval`, `aggregation` (Average\|Maximum\|Minimum\|Total\|Count), `filter`, `max-buckets` | ✅ |
| `monitor_metrics_definitions` | `resource` | `resource-group`, `resource-type`, `metric-namespace`, `search-string`, `limit` | ✅ |
| `monitor_workspace_list` | *(none)* | `subscription` | ✅ |
| `monitor_workspace_log_query` | `resource-group`, `workspace`, `table`, `query` (KQL or "recent"\|"errors") | `hours`, `limit`, `subscription` | ✅ |
| `monitor_resource_log_query` | `resource-id` (full ARM ID), `table`, `query` | `hours`, `limit`, `subscription` | ✅ |
## `mcp_azure_mcp_appservice` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `appservice_webapp_get` | *(none)* | `app`, `resource-group`, `subscription` | ✅ |
| `appservice_webapp_deployment_get` | `resource-group`, `app` | `deployment-id`, `subscription` | ✅ |
| `appservice_webapp_diagnostic_diagnose` | `resource-group`, `app`, `detector-name` (Availability\|CpuAnalysis\|MemoryAnalysis) | `start-time`, `end-time`, `interval`, `subscription` | ✅ |
| `appservice_webapp_diagnostic_list` | `resource-group`, `app` | `subscription` | ✅ |
| `appservice_webapp_settings_get-appsettings` | `resource-group`, `app` | `subscription` | ✅ (⚠️ secret) |
| `appservice_database_add` | `resource-group`, `app`, `database-type` (SqlServer\|MySQL\|PostgreSQL\|CosmosDB), `database-server`, `database` | `connection-string`, `subscription` | ❌ |
| `appservice_webapp_settings_update-appsettings` | `resource-group`, `app`, `setting-name`, `setting-update-type` (add\|set\|delete) | `setting-value`, `subscription` | ❌ (destructive) |
## `mcp_azure_mcp_role` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `role_assignment_list` | `scope` (ARM scope path) | `subscription` | ✅ |
---
## Phase 4 Tool Map
| Tool | Sub-command | AppOnboard Step | Purpose |
|------|-----------|----------|---------|
| `capture_deployment_inventory` | *(baseline)* | Step 5b | Snapshot pre-existing resources before the first deployment |
| `capture_deployment_inventory` | *(capture)* | Steps 6, 8, 9 | Diff resources.list() to record created/orphaned/failed resources; build cleanup commands from `failed` entries only |
| `mcp_azure_mcp_resourcehealth` | `resourcehealth_availability-status_get` | Step 7 | Post-deploy health. Call with `resourceId` from `deploy-result.json.resourceIds[]` |
| `mcp_azure_mcp_resourcehealth` | `resourcehealth_health-events_list` | Step 3 | Pre-deploy outage check. `event-type: "ServiceIssue"`, `status: "Active"` |
| `mcp_azure_mcp_monitor` | `monitor_activitylog_list` | Step 7+ | Failed deployment analysis. `resource-name` from deploy output, `event-level: "Error"`, `hours: 1` |
| `mcp_azure_mcp_monitor` | `monitor_metrics_query` | Step 7 | Performance validation. Requires `metric-namespace` from `monitor_metrics_definitions` first |
| `mcp_azure_mcp_appservice` | `appservice_webapp_get` | Step 7 | Verify App Service state, hostnames, runtime |
| `mcp_azure_mcp_appservice` | `appservice_webapp_deployment_get` | Step 7 | Verify deployment completed successfully |
| `mcp_azure_mcp_appservice` | `appservice_webapp_diagnostic_diagnose` | Step 7 | Run `Availability` detector on deployed app |
| `mcp_azure_mcp_deploy` | `deploy_app_logs_get` | Step 7 | App logs for azd-deployed apps only. Requires `workspace-folder` + `azd-env-name` |
| `mcp_azure_mcp_role` | `role_assignment_list` | Step 3 | Preflight RBAC check. `scope: "/subscriptions/{sub}/resourceGroups/{rg}"` |
| `mcp_azure_mcp_subscription_list` | *(flat)* | Step 1 | Resolve target subscription |
| `mcp_azure_mcp_group_list` | *(flat)* | Step 3 | Verify RG exists before deploy |
| `mcp_azure_mcp_extension_cli_install` | *(flat)* | Step 3 | Ensure az/azd/func CLI installed |
@@ -0,0 +1,54 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Portal Monitoring Links
Generate the portal link BEFORE deploying — the deployment name is deterministic, so the link works before the deployment even starts. The user wants to watch resources being created in real time.
## Generate via PowerShell — NEVER construct manually
The `%2F` encoding is critical and models consistently decode it back to `/` during text generation, producing broken links. Always use `.Replace('/', '%2F')`.
```powershell
# Subscription-scope deployment
$deploymentName = "app-onboard-deploy-$("{sessionId}".Substring(0,8))"
$resId = "/subscriptions/{subscriptionId}/providers/Microsoft.Resources/deployments/$deploymentName"
$link = "https://portal.azure.com/#view/HubsExtension/DeploymentDetailsBlade/~/overview/id/$($resId.Replace('/', '%2F'))"
Write-Output "LINK=$link"
```
Read `LINK=` from terminal output and print the URL in chat on its own bare line — no backticks, no markdown. Then deploy:
```powershell
az deployment sub create --name $deploymentName --subscription {subscriptionId} --location {location} --template-file infra/main.bicep --parameters @infra/main.parameters.json
```
### RG-scope (403 fallback)
Use `az deployment group create --name $deploymentName --resource-group {rg}` and adjust `$resId` to include `/resourceGroups/{rg}`:
```powershell
$resId = "/subscriptions/{subscriptionId}/resourceGroups/{rg}/providers/Microsoft.Resources/deployments/$deploymentName"
$link = "https://portal.azure.com/#view/HubsExtension/DeploymentDetailsBlade/~/overview/id/$($resId.Replace('/', '%2F'))"
Write-Output "LINK=$link"
```
For Terraform (no single ARM deployment): `https://portal.azure.com/#@{tenantId}/resource/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/activitylog`
> 💡 Resolve `{subscriptionId}` and `{resourceGroup}` from `context.json`. For Terraform, resolve `{tenantId}` via `az account show --query tenantId -o tsv`.
## Same-Scope Retries vs New Names
The portal link stays valid for same-scope retries — ARM overwrites in-place. Generate a new name (e.g., `$deploymentName = "app-onboard-deploy-{first8}-2"`) **only** when scope or RG changes.
## Chat Output Rules
1. **Terminal command:** the PowerShell snippet above (outputs the bare URL via `Write-Output "LINK=..."`). ⛔ **Do NOT auto-open the link in a browser** — never call `Start-Process` (or any browser-launching command) on the portal URL. Just print it so the user can open it if they choose.
2. **Read the terminal output** — find the line starting with `LINK=` and extract the URL
3. **Chat output:** paste the bare URL on its own line — no backticks, no markdown, no emoji on the same line
⛔ **Link must be ctrl+clickable.** The URL MUST be the ONLY content on its line — no emoji, no text, no backticks, no markdown formatting, no markdown link syntax on the same line. Terminals auto-linkify bare URLs but ONLY when the URL is alone on the line.
⛔ **Emit a NEW link whenever deployment name changes.** When healing causes a redeploy with a different `--name`, you MUST re-run the PowerShell snippet with the new name and print the new link:
```
⚠️ Previous deployment link is stale — use this one:
https://portal.azure.com/.../{newDeploymentName}
```
@@ -0,0 +1,129 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Preflight Checks
Pre-deployment validation steps. Run after user approval, before deployment execution.
> AppOnboard runs direct deployment (no `azd`).
## Check Sequence
Branch on `scaffold-manifest.json.iacFormat`:
### 0. Auth Token Verification
```bash
az account show
```
- Success → proceed. Active subscription + tenant confirmed.
- Failure → `ENVIRONMENT_BLOCKING`. Suggest `az login` (plain, no scope).
- ⛔ NEVER suggest `az login --scope https://graph.microsoft.com/.default` — Graph scope is irrelevant for ARM deployments.
### 0b. Resource Name Availability
Check globally-unique names before deploy: `az acr check-name`, `az storage account check-name`, `az webapp show`, `az keyvault show`. Name taken → suggest alternate from `prepare-plan.json.naming.suffix`: "Name `{name}` taken. Use `{altName}`?"
### 0c. F1/Free Tier Warning
If plan includes F1/D1/free SKUs, surface at deploy gate (do NOT block):
> ⚠️ Free tier: no custom domains, no SSL, no always-on, 60 min/day compute (F1). Dev/test only.
### 0d. RBAC Scope Pre-Check
```bash
az role assignment list --assignee {userId} --scope /subscriptions/{sub} --query "[].roleDefinitionName" -o tsv
```
Subscription-scope deploy requires `Contributor`/`Owner` on subscription. Missing → `ENVIRONMENT_BLOCKING` with `az role assignment create` command.
### 1. Deployment Preview
⛔ **MANDATORY — do NOT skip.** What-if validates + previews in one call. Use `what-if` exclusively — `az deployment sub/group validate` hits a known CLI bug (HTTP stream consumed error). If what-if fails, log + warn user — do not skip to execution.
#### Bicep (subscription scope)
```bash
az deployment sub what-if \
--name "{deploymentName}" \
--location {location} \
--template-file infra/main.bicep \
--parameters @infra/main.parameters.json \
--subscription {subscriptionId} \
--what-if-result-format FullResourcePayloads
```
#### Bicep (resource-group scope)
```bash
az deployment group create \
--resource-group {rg} \
--template-file infra/main.bicep \
--parameters @infra/main.parameters.json \
--subscription {subscriptionId} \
--what-if \
--what-if-result-format FullResourcePayloads
```
- Review changes: `Create`, `Modify`, `Delete`, `NoChange`. Surface `Delete` as warnings — user must acknowledge.
- Auth error → `ENVIRONMENT_BLOCKING`.
#### Terraform
```bash
terraform plan -out=tfplan -detailed-exitcode
```
- Exit 0 → no changes. Exit 2 → changes (normal). Exit 1 → error.
- Surface `destroy` as warnings — user must acknowledge. Auth error → `ENVIRONMENT_BLOCKING`.
### 3. RBAC Permission Check
```bash
az role assignment list \
--assignee {currentUserObjectId} \
--scope /subscriptions/{sub}/resourceGroups/{rg} \
--query "[].roleDefinitionName" -o tsv
```
Required: `Contributor` or `Owner` on the target resource group. If missing → `ENVIRONMENT_BLOCKING` with remediation command.
### 4. SKU Quota Verification
⛔ **`what-if` does NOT catch quota errors.** It returns `Succeeded` even when target SKU has limit=0.
If `prepare-plan.json.quotaValidation.verified == true` → proceed.
Otherwise → **read [sku-quota-validation.md](../../prepare/references/sku-quota-validation.md)** and run direct quota checks NOW (per-provider API patterns, offer restrictions). If limit=0 → HALT, present region fallback. Skip regions in `quotaValidation.checkedRegions` with zero availability.
⛔ `SubscriptionIsOverQuotaForSku` or `LocationIsOfferRestricted` in deploy output → HALT. See [error-classification.md](error-classification.md).
### 5. Resource Group Existence
```bash
az group show --name {rg} --query "location" -o tsv 2>/dev/null
```
- Exists → verify location matches `prepare-plan.json` region. Mismatch → warn.
- Not exists → will be created by deployment (if `main.bicep` has subscription scope).
## Error Handling
Each check runs independently. Collect all results, then present structured report.
| Check | Fail Behavior |
|-------|---------------|
| Deployment preview | Warn, don't block (can fail on unsupported types) |
| RBAC | Block. Surface `az role assignment create`. |
| RG check | Warn on location mismatch. Don't block. |
## Report Format
```
## Preflight Results
✅ IaC syntax: valid (terraform validate / bicep build)
⚠️ Deployment preview: 3 creates, 0 destroys, 1 update
✅ RBAC: Contributor role confirmed
✅ Resource group: rg-myapp-dev (eastus2)
```
@@ -0,0 +1,81 @@
<!-- azure-cor-disclaimer -->
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
# Subagent Template — Deploy Preflight & Checklist Generation
Read deploy reference files and distill deployment-specific rules into `deploy-checklist.md`. This is the main agent's ONLY source of deploy rules.
## Critical Rules
- ⛔ Do NOT invoke any agents, run `az` commands, or modify IaC/app code. Read-only sub-agent.
- ⛔ Do NOT read `deploy-schemas.ts` or `error-classification.md` — main agent reads those on-demand.
## Input (provided by caller)
| Field | Source |
|-------|--------|
| `prepare-plan.json` content | services[], naming, region, costEstimate, deploymentVariables |
| `scaffold-manifest.json` content | deployCommand, validationResult, files[] |
| `context.json` content | azure.subscriptionId, subscriptionName, resourceGroup, sessionId, intent |
| `prereq-output.json` content | buildRequirements, warnings[], components[] |
| Session folder path + working directory | Required |
## Output
| Artifact | Location |
|----------|----------|
| `deploy-checklist.md` | Session folder |
| `deploy-result.json` skeleton (if missing) | Session folder |
| Summary (≤300 tokens) | Return to caller |
## Workflow
### Step 1 — Read session artifacts + write skeleton
Read all 4 session artifacts. Extract: services[], naming, costEstimate, deployCommand, validationResult, deploymentVariables, subscriptionId, sessionId, buildRequirements, warnings[], quotaValidation.
If `deploy-result.json` missing, write skeleton with these EXACT field names (do NOT rename): `{ sessionId, subscriptionId, resourceGroupName, deploymentNames: ["app-onboard-deploy-{first 8 of sessionId}"], status: "in-progress", startedUtc, resourceIds: [], endpoints: [], healthStatus: "unknown", resourceResults: [], healingAttempts: [] }`.
### Step 2 — Read safety + blocked patterns refs
Read [deploy-safety.md](deploy-safety.md) and [blocked-patterns.md](blocked-patterns.md). Bake into checklist sections: deploy-result.json rules, blocked commands table, shell rules (sync shells, secrets persistence, no --track-status, az rest headers on Windows), 403 scope fallback (4-step procedure with real values), post-deploy tag verification, deployment operation polling (conditional: >5 resources), re-approval gates, antipatterns, artifact reconciliation.
### Step 3 — Read preflight + approval gate refs
Read [preflight-checks.md](preflight-checks.md) and [approval-gate-template.md](approval-gate-template.md).
Bake preflight into checklist: auth token check, resource name availability (real names), RBAC scope pre-check, ⛔ MANDATORY what-if command (pre-filled with real deploymentName, region, subscriptionId), RG existence check, offer restriction check (conditional: if `offerRestrictionsVerified` false AND DB services in plan).
Bake approval gate VERBATIM with real values: subscription, RG, region, service table, cost table, validation status, files list, response handlers with exact CLI commands. If F1/D1 detected, append warning.
### Step 4 — Read code-deployment + health refs
Read ONLY the code-deployment ref(s) matching the compute types in `prepare-plan.json.services[]`. **Do NOT read** references for compute types absent from the plan:
- If `App Service` or `Functions` in plan → read [code-deployment-appservice.md](code-deployment-appservice.md)
- If `Container Apps` in plan → read [code-deployment-container-apps.md](code-deployment-container-apps.md)
- If `Static Web Apps` in plan → read [code-deployment-swa.md](code-deployment-swa.md)
Bake per-service-type `## Code deploy` sections (one per compute service — do NOT merge).
Read [health-check-patterns.md](health-check-patterns.md). Bake health check section: HTTP checks (timeout 30s, 3 retries), status interpretation, Azure default page detection strings, non-HTTP resource checks, functional verification (conditional).
### Step 5 — Read conditional refs + handoff + write checklist
**Database (conditional):** If DB services in plan → read [database-post-deploy.md](database-post-deploy.md). Bake migration discovery, execution commands, PG-specific checks.
Read [../../references/handoff-protocol.md](../../references/handoff-protocol.md). Bake cleanup commands (with real rg, sessionId), orphan listing, healing summary, post-deploy recommendations, agent-based next steps, auth-aware handoff.
**Write `deploy-checklist.md`** to session folder. If it already exists, replace all content via `edit`; if not, use `create`:
```
# Deploy Checklist for {appName}
# RG: {rgName} | Sub: {subscriptionId} | Session: {sessionId}
# ⚠️ If compaction recently occurred: re-read deploy/instructions.md Steps 6-8
```
Delete inapplicable sections (e.g., remove App Service section for Container Apps deploys).
> ⛔ **Copy `## Before handoff (Step 8)` and `## Artifact verification (Step 8 — MANDATORY)` from the template VERBATIM.** Do NOT paraphrase, merge, or weaken `⛔` markers. These sections are the compaction-safe finalization anchor — diluting them causes artifact writes to be skipped after compaction.
### Step 6 — Return summary
Return ≤300 tokens: preflight warnings, deploy command, service types, confirmation of checklist write, database migration note if applicable.