Update readme's, branches and actions
Build and Push Docker Images / detect-changes (push) Successful in 9s
Build and Push Docker Images / build-auth-api (push) Successful in 1m5s
Build and Push Docker Images / build-employee-api (push) Successful in 1m1s
Build and Push Docker Images / build-transit-api (push) Successful in 57s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m5s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m16s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m13s
Build and Push Docker Images / deploy-test-node (push) Successful in 5s

This commit is contained in:
Sven laptop
2026-09-21 00:15:05 +02:00
parent 635a130f0b
commit 217dfd3abc
161 changed files with 644 additions and 21618 deletions
+31 -7
View File
@@ -4,6 +4,8 @@ on:
push:
branches:
- dev
- main
- develop
- auth-api
- employee-api
- transit-api
@@ -13,6 +15,8 @@ on:
pull_request:
branches:
- dev
- main
- develop
env:
REGISTRY: gitea.testinstance.nl
@@ -31,6 +35,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect changed paths
id: filter
@@ -64,7 +70,7 @@ jobs:
build-auth-api:
needs: detect-changes
if: ${{ (gitea.ref_name == 'auth-api' || gitea.ref_name == 'dev') && needs.detect-changes.outputs.auth_api == 'true' }}
if: ${{ (gitea.ref_name == 'auth-api' || gitea.ref_name == 'dev' || gitea.ref_name == 'main' || gitea.ref_name == 'develop') && needs.detect-changes.outputs.auth_api == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -88,6 +94,9 @@ jobs:
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
echo "${image_base}:${{ gitea.ref_name }}" >> "$GITHUB_OUTPUT"
echo "${image_base}:${{ gitea.sha }}" >> "$GITHUB_OUTPUT"
if [[ "${{ gitea.ref_name }}" == "dev" || "${{ gitea.ref_name }}" == "main" || "${{ gitea.ref_name }}" == "develop" ]]; then
echo "${image_base}:latest" >> "$GITHUB_OUTPUT"
fi
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Build and push auth_api
@@ -99,7 +108,7 @@ jobs:
build-employee-api:
needs: detect-changes
if: ${{ (gitea.ref_name == 'employee-api' || gitea.ref_name == 'dev') && needs.detect-changes.outputs.employee_api == 'true' }}
if: ${{ (gitea.ref_name == 'employee-api' || gitea.ref_name == 'dev' || gitea.ref_name == 'main' || gitea.ref_name == 'develop') && needs.detect-changes.outputs.employee_api == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -123,6 +132,9 @@ jobs:
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
echo "${image_base}:${{ gitea.ref_name }}" >> "$GITHUB_OUTPUT"
echo "${image_base}:${{ gitea.sha }}" >> "$GITHUB_OUTPUT"
if [[ "${{ gitea.ref_name }}" == "dev" || "${{ gitea.ref_name }}" == "main" || "${{ gitea.ref_name }}" == "develop" ]]; then
echo "${image_base}:latest" >> "$GITHUB_OUTPUT"
fi
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Build and push employee_api
@@ -134,7 +146,7 @@ jobs:
build-transit-api:
needs: detect-changes
if: ${{ (gitea.ref_name == 'transit-api' || gitea.ref_name == 'dev') && needs.detect-changes.outputs.transit_api == 'true' }}
if: ${{ (gitea.ref_name == 'transit-api' || gitea.ref_name == 'dev' || gitea.ref_name == 'main' || gitea.ref_name == 'develop') && needs.detect-changes.outputs.transit_api == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -158,6 +170,9 @@ jobs:
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
echo "${image_base}:${{ gitea.ref_name }}" >> "$GITHUB_OUTPUT"
echo "${image_base}:${{ gitea.sha }}" >> "$GITHUB_OUTPUT"
if [[ "${{ gitea.ref_name }}" == "dev" || "${{ gitea.ref_name }}" == "main" || "${{ gitea.ref_name }}" == "develop" ]]; then
echo "${image_base}:latest" >> "$GITHUB_OUTPUT"
fi
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Build and push transit_api
@@ -169,7 +184,7 @@ jobs:
build-error-log-api:
needs: detect-changes
if: ${{ (gitea.ref_name == 'error-log-api' || gitea.ref_name == 'dev') && needs.detect-changes.outputs.error_log_api == 'true' }}
if: ${{ (gitea.ref_name == 'error-log-api' || gitea.ref_name == 'dev' || gitea.ref_name == 'main' || gitea.ref_name == 'develop') && needs.detect-changes.outputs.error_log_api == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -193,6 +208,9 @@ jobs:
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
echo "${image_base}:${{ gitea.ref_name }}" >> "$GITHUB_OUTPUT"
echo "${image_base}:${{ gitea.sha }}" >> "$GITHUB_OUTPUT"
if [[ "${{ gitea.ref_name }}" == "dev" || "${{ gitea.ref_name }}" == "main" || "${{ gitea.ref_name }}" == "develop" ]]; then
echo "${image_base}:latest" >> "$GITHUB_OUTPUT"
fi
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Build and push error_log_api
@@ -204,7 +222,7 @@ jobs:
build-admin-ui:
needs: detect-changes
if: ${{ (gitea.ref_name == 'admin-ui' || gitea.ref_name == 'dev') && needs.detect-changes.outputs.admin_ui == 'true' }}
if: ${{ (gitea.ref_name == 'admin-ui' || gitea.ref_name == 'dev' || gitea.ref_name == 'main' || gitea.ref_name == 'develop') && needs.detect-changes.outputs.admin_ui == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -228,6 +246,9 @@ jobs:
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
echo "${image_base}:${{ gitea.ref_name }}" >> "$GITHUB_OUTPUT"
echo "${image_base}:${{ gitea.sha }}" >> "$GITHUB_OUTPUT"
if [[ "${{ gitea.ref_name }}" == "dev" || "${{ gitea.ref_name }}" == "main" || "${{ gitea.ref_name }}" == "develop" ]]; then
echo "${image_base}:latest" >> "$GITHUB_OUTPUT"
fi
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Build and push admin_ui
@@ -239,7 +260,7 @@ jobs:
build-monitoring-ui:
needs: detect-changes
if: ${{ (gitea.ref_name == 'monitoring-ui' || gitea.ref_name == 'dev') && needs.detect-changes.outputs.monitoring_ui == 'true' }}
if: ${{ (gitea.ref_name == 'monitoring-ui' || gitea.ref_name == 'dev' || gitea.ref_name == 'main' || gitea.ref_name == 'develop') && needs.detect-changes.outputs.monitoring_ui == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -263,6 +284,9 @@ jobs:
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
echo "${image_base}:${{ gitea.ref_name }}" >> "$GITHUB_OUTPUT"
echo "${image_base}:${{ gitea.sha }}" >> "$GITHUB_OUTPUT"
if [[ "${{ gitea.ref_name }}" == "dev" || "${{ gitea.ref_name }}" == "main" || "${{ gitea.ref_name }}" == "develop" ]]; then
echo "${image_base}:latest" >> "$GITHUB_OUTPUT"
fi
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Build and push monitoring_ui
@@ -283,7 +307,7 @@ jobs:
build-admin-ui,
build-monitoring-ui,
]
if: ${{ gitea.event_name == 'push' && gitea.ref_name == 'dev' }}
if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && gitea.event_name == 'push' && (gitea.ref_name == 'dev' || gitea.ref_name == 'main') }}
runs-on: ubuntu-latest
steps:
-1
View File
@@ -1 +0,0 @@
0.13.1
@@ -1,4 +0,0 @@
{
"name": "azure-debug-generate",
"version": "0.0.1"
}
@@ -1,92 +0,0 @@
<!-- 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.
# Azure Debug Generate
> **AUTHORITATIVE GUIDANCE — MANDATORY COMPLIANCE**
>
> This document is the **official, canonical source** for generating VS Code debug
> configuration from an approved plan. You **MUST** follow these instructions
> exactly as written. When in doubt, defer to this document. Do not improvise,
> infer, or substitute steps.
---
## Global Rules (NO EXCEPTIONS)
1. **Plan is the source of truth** — Read `.azure/vscode-debug-plan.md` and generate exactly what it specifies. Only generate artifacts for rows where `Generate` is checked (`[x]`).
2. **Update plan progressively** — Mark steps complete as you go; update **Last Updated** timestamp on every status change
3. ❌ **Destructive actions require `ask_user`** — Always confirm before overwriting, deleting, or modifying existing files
4. **Preserve existing config** — Never silently overwrite project configuration files or `docker-compose.yml`. Merge or ask first.
5. **Scope — VS Code debug setup only** — These instructions are for generating local debug configurations in VS Code. Cloud architecture, IaC generation, provisioning, and deployment are handled by the **azure-deploy** agent through the complete **azure-app-onboard** pipeline.
6. **Warn on limited support** — When a project type, runtime, or emulator declared in the plan has no matching reference file, emit a `⚠️ LIMITED SUPPORT:` warning — [limited-support.md](references/limited-support.md).
---
## Autopilot mode
**Active when** the invoking chat query begins with `[AUTOPILOT MODE]`, **or** `.azure/vscode-debug-plan.md` contains `executionMode: auto` or equivalent. When active, run fully unattended:
- **Do NOT call `ask_user`.** This is the tail of an autopilot chain over a freshly generated project, so overwriting/merging the scaffolded config files is expected and safe — proceed without prompting (still preserve and merge existing configs rather than blindly overwriting).
- Validation (Phase 3) still runs in full — autopilot never skips validation or the `Implemented` status write. Setting status to `Implemented` is what signals the workflow is complete (the extension restores its auto-approve setting at that point).
---
## Phase 1: Pre-Flight
Verify the plan and environment before generating any files.
| # | Action | Reference |
|---|--------|-----------|
| 1 | **Verify plan** — Confirm `.azure/vscode-debug-plan.md` exists with status `Approved`. Set status to `Executing` and update **Last Updated**. | `.azure/vscode-debug-plan.md` |
| 2 | **Load references** — For each service in the plan's Services table (where Generate is checked), load the corresponding project-type and runtime reference files. If no reference file exists, emit a limited-support warning. | [limited-support.md](references/limited-support.md) |
| 3 | **Run pre-flight checks** — Stale data directories and port conflicts. | [preflight.md](references/preflight.md) |
---
## Phase 2: Generate
The plan drives implementation. Read each section of `.azure/vscode-debug-plan.md` and generate the corresponding artifacts. Use the reference files for implementation details — the plan specifies WHAT to generate, the references specify HOW.
Follow the generation steps in [generate.md](references/generate.md) in order.
---
## Phase 3: Validate
> ⚠️ **CRITICAL:** You MUST complete every validation step before proceeding. Do NOT mark the task as complete, do NOT set status to `Implemented`, and do NOT deliver a closing message until validation is finished and the checklist is updated with real results.
| # | Action | Reference |
|---|--------|-----------|
| 1 | **Validate each launch configuration** — Follow every step in validation.md for each non-compound config. | [validation.md](references/validation.md) |
| 2 | **Validate the compound configuration** — Faithfully run each compound's orchestration per validation.md § Step 9, never inferring the result from the individual configs. | [validation.md](references/validation.md) § Step 9 |
| 3 | **Tear down validation processes** — Stop **every** process and emulator validation spawned (`docker compose down`) and verify all app HTTP, debug, and emulator ports are free again, so a subsequent user F5 starts from a clean slate. | [validation.md](references/validation.md) § Final Teardown |
| 4 | **Update Debug Configuration Checklist** — Create or update the `## Debug Configuration Checklist` section in `.azure/vscode-debug-plan.md` with real ✅ or ❌ results for each configuration. | [validation.md](references/validation.md) § Plan Integration |
| 5 | **Set status** — Only after every checklist stub has been replaced with a real result, set plan status to `Implemented` and update **Last Updated**. | `.azure/vscode-debug-plan.md` |
> ⛔ **VALIDATION IS NOT OPTIONAL.** Do NOT set status to `Implemented` until every stub in the Debug Configuration Checklist has been replaced with a real ✅ or ❌ result. A checklist with any remaining stubs or missing entries means validation is incomplete — go back and finish it. This is the single most common failure mode.
---
## Outputs
| Artifact | Location |
|----------|----------|
| **Plan** (updated) | `.azure/vscode-debug-plan.md` |
| Docker Compose | `docker-compose.yml` |
| VS Code Debug Config | `.vscode/launch.json` — see [project-types/](references/project-types/) and [runtimes/](references/runtimes/) |
| VS Code Build Config | `.vscode/tasks.json` — see [project-types/](references/project-types/) and [runtimes/](references/runtimes/) |
| VS Code Extensions | `.vscode/extensions.json` — see [generate.md](references/generate.md) § assembly protocol |
| VS Code Settings | `.vscode/settings.json` — see [generate.md](references/generate.md) § assembly protocol |
| Connection Strings | `local.settings.json` or `.env` |
| Convenience Scripts | Runtime-specific script runner (see [runtimes/](references/runtimes/)) |
| API Test Collections | `api-test-collections/{service-id}/<test-name>/invoke.{sh,ps1}` |
---
## Post-Generation
After validation completes with status `Implemented`, **stop**. Do not present next steps or a closing message — the `azure-debug-generate` agent handles post-generation guidance and interactive next steps.
## Troubleshooting
For Edge or Chrome related startup issues, consult [chromium.md](references/project-types/frontend-spa/debug-adapters/chromium.md).
@@ -1,224 +0,0 @@
<!-- 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.
# API Test Collection Patterns
> Reference for generating `api-test-collections/{service-id}/` scripts, where `{service-id}` is the canonical service ID derived from the plan's **Service Label** column (see [generate.md](generate.md) § Service ID Derivation). Only generate test collections for services whose **Generate** column is checked in the plan. Scripts should be language-agnostic commands that exercise the running app and test its integration with any live emulators.
---
## HTTP
**HTTP patterns** use `{baseUrl}` — the project type supplies the base URL (e.g., `http://localhost:7071/api` for Functions). All other patterns target the emulator directly and are reusable across project types.
### GET request
```sh
curl -i "{baseUrl}/{FunctionName}"
```
### POST with JSON body
```sh
curl -i -X POST "{baseUrl}/{FunctionName}" \
-H "Content-Type: application/json" \
-d @sample-data.json
```
> **`{baseUrl}` by project type:**
>
> | Project Type | Base URL |
> |-------------|---------|
> | Azure Functions | `http://localhost:7071/api` |
> | Container App | `http://localhost:{port}` (from Dockerfile `EXPOSE`) |
> | App Service | `http://localhost:{port}` (from framework dev server) |
---
## Storage (Azurite — Blob / Queue / Table)
> Requires Azurite running. Uses `--connection-string "UseDevelopmentStorage=true"` for all commands.
### Blob trigger — upload a file
```sh
az storage blob upload \
--connection-string "UseDevelopmentStorage=true" \
--container-name {container-name} \
--name "sample-file.json" \
--file sample-file.json \
--overwrite
```
### Queue trigger — send a message
```sh
az storage message put \
--connection-string "UseDevelopmentStorage=true" \
--queue-name {queue-name} \
--content '{"id": "test-001", "data": "sample"}'
```
### Table trigger — insert an entity
```sh
az storage entity insert \
--connection-string "UseDevelopmentStorage=true" \
--table-name {table-name} \
--entity PartitionKey=pk RowKey=rk001 Value=test
```
---
## Cosmos DB
> Requires Cosmos DB Emulator running on `https://localhost:8081`. TLS verification must be disabled for local calls.
### Insert a document
```sh
curl -k -X POST "https://localhost:8081/dbs/{database}/colls/{collection}/docs" \
-H "Authorization: type=master&ver=1.0&sig=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" \
-H "Content-Type: application/json" \
-H "x-ms-documentdb-partitionkey: [\"test\"]" \
-H "x-ms-version: 2018-12-31" \
-d '{"id": "test-001", "partitionKey": "test", "data": "sample"}'
```
> `-k` disables TLS verification for the emulator's self-signed cert. Never use in production.
---
## Service Bus
> Requires Service Bus Emulator running. Uses curl against the Service Bus Emulator's HTTP endpoint.
### Send a message to a queue
```sh
curl -i -X POST "http://localhost:5672/messages" \
-H "Content-Type: application/json" \
-H "BrokerProperties: {\"Label\": \"test\"}" \
-d '{"id": "test-001", "data": "sample"}'
```
> The Service Bus Emulator's HTTP endpoint and port may vary. Check the emulator documentation and docker-compose configuration for the correct URL. For SDK-based testing, use the Azure Service Bus SDK with the emulator connection string from `emulators/` config.
### Send a message to a topic
```sh
curl -i -X POST "http://localhost:5672/messages" \
-H "Content-Type: application/json" \
-H "BrokerProperties: {\"Label\": \"test\"}" \
-d '{"id": "test-001", "data": "sample"}'
```
> Adjust the URL path for topic-specific endpoints per the emulator's API surface.
---
## Event Hubs
> Requires Event Hubs Emulator running.
### Send an event
```sh
curl -i -X POST "http://localhost:5672/messages" \
-H "Content-Type: application/json" \
-d '{"id": "test-001", "data": "sample"}'
```
> The Event Hubs Emulator's HTTP endpoint and port may vary. Check the emulator documentation and docker-compose configuration for the correct URL. For SDK-based testing, use the Azure Event Hubs SDK with the emulator connection string from `emulators/` config.
---
## Timer (Azure Functions only)
Timer triggers cannot be fired by an external event — the Functions host fires them on schedule. Use the Functions admin API to trigger them on demand:
```sh
curl -i -X POST "http://localhost:7071/admin/functions/{FunctionName}" \
-H "Content-Type: application/json" \
-d '{}'
```
> This calls the Functions admin endpoint which is only available locally. The `{}` body is required; the timer trigger ignores it.
---
## Generation Rules
When generating API test collections during Phase 2:
1. Resolve endpoints from the implemented route registrations, not only `.azure/project-plan.md`. The project plan intentionally omits derived authentication routes, so include implemented registration, login, and current-user endpoints when `API Login` is enabled.
2. Create one top-level subdirectory per service: `api-test-collections/{service-id}/`, where `{service-id}` is derived from the plan's **Service Label** column (see [generate.md](generate.md) § Service ID Derivation). Only generate for services whose **Generate** column is checked in the plan.
3. Within each service directory, generate one subdirectory per trigger/endpoint found during inventory.
4. Name the trigger directory after the trigger: `{trigger-type}-{function-or-endpoint-name}` (e.g., `http-GetOrder`, `blob-ProcessUpload`).
5. Create an `invoke` script (`.sh` on macOS/Linux, `.ps1` on Windows) with the appropriate pattern from this file, substituting discovered values (function name, container name, etc.).
6. Create a `sample-data.json` or `sample-message.json` next to the invoke script when the test requires a body.
7. On macOS/Linux, make the script executable (`chmod +x`).
> **Do not generate timer test scripts** unless the user explicitly requests it — they're rarely needed for local debugging.
---
## Plan Section Formatting Rules
When writing the **API Test Collections** section of the plan, the heading format may vary by trigger type. In all cases, the subfolder names under `api-test-collections/{service-id}/` (e.g., `http-register`, `http-createOrder`) should also be referenced in each section's markdown heading when they differ so users can easily see which routes map to each invokable script.
---
### HTTP triggers / web API endpoints
```
### {METHOD} {route} [{🔒}] `{folder-name}`
```
- **`{METHOD} {route}`** — HTTP verb and full route path (e.g., `GET /api/health`)
- **`🔒`** — Include this emoji when the endpoint requires authentication (any auth scheme: Bearer JWT, API key, etc.). Omit entirely for anonymous/public endpoints.
- **`` `{folder-name}` ``** — The exact folder name under `api-test-collections/{service-id}/` (e.g., `` `http-health` ``)
**Examples:**
```markdown
### GET /api/health `http-health`
### POST /api/auth/register `http-register`
### GET /api/auth/me 🔒 `http-getMe`
### POST /api/orders 🔒 `http-createOrder`
```
**Auth key** — add this once at the top of the API Test Collections section, just after the folder tree, when any 🔒 routes are present:
```markdown
> 🔒 = requires authentication (replace `<token>` with a JWT from the login endpoint before running)
```
---
### Non-HTTP triggers (blob, queue, Service Bus, Event Hubs, etc.)
```
### {TriggerType}: {function-or-resource-name} `{folder-name}`
```
- **`{TriggerType}`** — Human-readable trigger category: `Blob`, `Queue`, `Service Bus`, `Event Hubs`, `Table`, `Cosmos DB`, etc.
- **`{function-or-resource-name}`** — The function name or the specific resource being targeted (container name, queue name, topic name, etc.)
- **`` `{folder-name}` ``** — The exact folder name under `api-test-collections/{service-id}/` (e.g., `` `blob-ProcessUpload` ``)
**Examples:**
```markdown
### Blob: uploads container `blob-processUpload`
### Queue: order-requests `queue-sendOrder`
### Service Bus: invoices topic `servicebus-sendInvoice`
### Event Hubs: telemetry `eventhubs-sendTelemetry`
```
> The 🔒 indicator does not apply to non-HTTP triggers — they are invoked by pushing data into the resource directly, not via an authenticated HTTP call.
@@ -1,68 +0,0 @@
<!-- 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.
# {Emulator Name}
> **Template** — Copy this file to `emulators/{name}.md` when adding a new emulator.
---
## Docker Image
<!-- Official image and recommended pinned tag. -->
```
{org}/{image}:{tag}
```
## docker-compose Service Block
<!-- Complete service YAML block, ready to paste. Includes ports, volumes, health check. -->
```yaml
services:
{service-name}:
image: {org}/{image}:{tag}
ports:
- "{host-port}:{container-port}"
volumes:
- ./.{service-name}:/data
restart: unless-stopped
```
## Connection String
<!-- Default local connection string for the app to use. -->
```
{connection-string}
```
## Required App Environment Variables
<!-- Variable names the app must set to point at this emulator. -->
| Variable | Value |
|----------|-------|
| `{VAR_NAME}` | `{value}` |
## Healthcheck (Database Emulators Only)
<!-- If this emulator is a database, include a healthcheck block in the docker-compose service above AND document it here. The migration service (see migrations.md) depends on `condition: service_healthy` to wait for the database before running migrations. Without a healthcheck, auto-migration will not work. -->
<!-- Delete this section if the emulator is not a database. -->
## Container Runtime Support
<!-- State whether this emulator has been certified for Docker, Podman, or both. -->
<!-- The compose service block is engine-agnostic, but some Microsoft emulator images make Docker-specific assumptions (privileged mode, architecture, licensing). -->
<!-- If this emulator is NOT yet certified for Podman, say so here so the generation phase emits a `⚠️ LIMITED SUPPORT` warning when the plan selects Podman. -->
| Docker | Podman |
|--------|--------|
| {✅ certified / 🔲 planned} | {✅ certified / 🔲 planned} |
## Notes
<!-- Platform caveats (arm64/x86), known issues, resource requirements. -->
<!-- Note any Podman-specific caveats here (SELinux `:Z` labels on Linux, rootless port ranges, Podman-machine bind-mount paths). -->
@@ -1,50 +0,0 @@
<!-- 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.
# Azurite (Azure Blob / Queue / Table Storage)
## Docker Image
```
mcr.microsoft.com/azure-storage/azurite
```
## docker-compose Service Block
```yaml
services:
azurite:
image: mcr.microsoft.com/azure-storage/azurite
# Overriding the default command requires re-specifying --blobHost/--queueHost/--tableHost 0.0.0.0
# so Azurite listens on all interfaces (the image's default). Without them, Azurite falls back to
# 127.0.0.1 inside the container and becomes unreachable from the host via port mapping.
# --skipApiVersionCheck allows newer Azure SDK API versions to work with older Azurite releases.
command: azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --skipApiVersionCheck
ports:
- "10000:10000"
- "10001:10001"
- "10002:10002"
volumes:
- ./.azurite:/data
restart: unless-stopped
```
## Connection String
```
UseDevelopmentStorage=true
```
## Required App Environment Variables
| Variable | Value |
|----------|-------|
| `AzureWebJobsStorage` (Functions) | `UseDevelopmentStorage=true` |
| `AZURE_STORAGE_CONNECTION_STRING` (SDK) | `UseDevelopmentStorage=true` |
## Notes
- Ports: 10000 (Blob), 10001 (Queue), 10002 (Table)
- **Consolidation:** If multiple storage bindings are detected (blob + queue + table), use a **single** Azurite service — not one per binding type.
- The Event Hubs Emulator requires Azurite for checkpointing. If both are needed, the `azurite` service is shared.
- **Container runtime:** Certified for both **Docker** and **Podman** — the service block above is unchanged for either engine. Under Podman on Windows/macOS the ports are published from the Podman machine to the host exactly as with Docker; no `:Z`/`:z` volume label is needed for the `./.azurite` bind mount.
@@ -1,128 +0,0 @@
<!-- 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.
# PostgreSQL
> PostgreSQL has no Azure-provided emulator. Use the standard `postgres` Docker image for local development. If the project targets **Azure Cosmos DB for PostgreSQL**, note in the plan that no local emulator is available.
## Docker Image
```
postgres:16
```
## docker-compose Service Block
Declare the local credentials **once** in a workspace-root `.env`, then reference them everywhere
else. Compose interpolates `${...}` from `.env` (or the shell environment), so the same values feed
the database service and every application service.
`.env` is required — without it Compose interpolates `${POSTGRES_USER}` to an empty string and the
database silently fails to authenticate:
```
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=localdev
```
> ⛔ **`.gitignore` must list `.env` before you create it.** A credential that reaches a commit is
> compromised and has to be rotated — deleting it in a later commit leaves the value in history and
> in every existing clone and fork. Private repositories are no exception. If the project has no
> `.gitignore`, or has one that omits `.env`, fix that before writing the file.
> **Never inline a concrete `user:password@host` URL** in a generated file — build it from the
> variables above. Beyond hard-coding a credential, such literals are rewritten by secret-redaction
> filters, and a masked value starting with `*` is a fatal YAML parse error: YAML reads a leading
> `*` as an alias reference.
```yaml
services:
postgres:
image: postgres:16
ports:
- "5432:5432"
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 5s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
# Declare the named volume at the top level of the compose file (merge into an
# existing top-level `volumes:` key if the file already has one).
volumes:
postgres_data:
```
> ⛔ **Use a named volume for the Postgres data dir — not a `./.postgres` bind mount.** On its first
> start Postgres's `initdb` sets the data directory's permissions to `0700`, which requires `chown`/`chmod`
> on the mount. Against a **host bind mount on Windows/macOS** — especially **rootless Podman**, where the
> container user can't change ownership of a Windows-side path — that fails with
> `could not change permissions of directory "/var/lib/postgresql/data": Operation not permitted`, and the
> database never initializes. A **named volume** is managed inside the engine's VM (ext4), so `initdb`
> succeeds on Docker Desktop and Podman alike. This also sidesteps the bind-mount performance and
> permission quirks Postgres hits on Docker Desktop for Windows/macOS. (Azurite and most other emulators
> tolerate a workspace bind mount fine — this named-volume rule is specific to database emulators like
> Postgres that `chown` their data directory.)
## Connection String
The host depends on where the client runs:
| Client location | Host |
|---|---|
| Host machine (VS Code debug target, `npm` task) | `localhost` |
| Another docker-compose service | `postgres` (the service name) |
```
postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}
```
## Required App Environment Variables
Declare these in the workspace-root **`.env`**, alongside the `POSTGRES_*` values:
| Variable | Value |
|----------|-------|
| `DATABASE_URL` | `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}` |
| `POSTGRES_CONNECTION_STRING` | `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}` |
> Use whichever variable name the project's ORM or SDK expects. Both forms above are shown as reference.
> When the value is set on a docker-compose service, replace the `localhost` host with `postgres`.
> **`.env` must declare every key `.env.example` declares.** `.env.example` is documentation —
> nothing loads it, so a key that appears only there is undefined at run time.
> **A runtime settings file does not cover host-run tasks.** `local.settings.json` is read by the
> Azure Functions host and a compose `environment:` block by its own container; neither reaches a
> VS Code task that runs a tool directly on the host, such as `npm run db:migrate`. That client
> fails with "Unable to acquire a connection", which reads like an unready database but is a
> missing variable — it never opened a socket. Put the value in `.env` so both paths resolve it.
## Healthcheck
The healthcheck is included in the docker-compose service block above. It uses `pg_isready` to verify PostgreSQL is accepting connections. The migration service (see [migrations.md](../migrations.md)) depends on `condition: service_healthy` to wait for readiness before running migrations.
```yaml
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 5s
timeout: 5s
retries: 5
start_period: 30s
```
## Notes
- Port 5432 is the standard PostgreSQL port.
- Default credentials (`postgres`/`postgres`) are intentionally simple for local dev and are declared in the workspace-root `.env`. Never use in production.
- Data is persisted to the **named volume `postgres_data`** (managed by the container engine, not a workspace folder). Reset it with `docker compose down -v` / `podman compose down -v`, or `docker volume rm <project>_postgres_data`. Because it is not a workspace directory, it needs no `.vscode/settings.json` `files.exclude` entry and is not part of the workspace stale-**directory** preflight check.
- **Container runtime:** Certified for both **Docker** and **Podman** — the service block, healthcheck, and named `postgres_data` volume are unchanged for either engine. The named volume (not a bind mount) is what lets `initdb` run under rootless Podman on Windows/macOS. The `condition: service_healthy` gate that migrations depend on is honored by both `docker compose` and `podman compose`.
@@ -1,292 +0,0 @@
<!-- 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.
# Artifact Generation
Cross-cutting rules, step sequence, and assembly protocols for generating local development configuration files from an approved plan.
---
## Reading the Plan
The plan's tables drive all generation:
| Plan Section | What It Drives |
|-------------|----------------|
| **Services** table | Which services get launch.json/tasks.json entries. |
| **Emulators** table | Which emulator compose services to generate. |
| **Orchestrator** table | The container runtime (Docker/Podman) and the **Compose Command** (`docker compose` / `podman compose`) to write into every emulator/migration task. |
| **Migrations** table | Which migration compose services to generate. |
| **API Test Collections** table | Which API test scripts to generate. |
| **Convenience Scripts** table | Which convenience scripts to generate. |
> **Container runtime is a plan value — never hard-code `docker compose`.** Read the **Compose Command** cell from the plan's Orchestrator table and use it verbatim for every generated Compose invocation (Start Emulators task, migration task, convenience scripts, validation, teardown). It is `docker compose` by default and `podman compose` when the plan selected Podman. The generated `docker-compose.yml` itself is **identical for either engine** — only the command that drives it changes. In the examples throughout these references, `docker compose` stands in for the plan's Compose Command; substitute `podman compose` when the plan says Podman.
### Targeted Resolution
The plan provides high-level intent. For implementation details, perform **targeted resolution scans** of the workspace:
- **Migration details** — Scan for migration directory path, existing migration scripts, connection env var names. See [migrations.md](migrations.md).
- **API endpoints** — Parse function definitions or route handlers to get specific endpoint names, methods, routes, auth levels.
- **Connection string keys** — Check `local.settings.json`, `.env`, or app config for existing key names.
- **Existing config** — Check for existing `.vscode/launch.json`, `.vscode/tasks.json`, `docker-compose.yml` to determine merge vs create.
- **Framework details** — For frontend SPAs, detect the specific framework (Vite, Next.js, Angular, CRA) and dev server port from config files.
- **TypeScript source maps** — For TypeScript services, verify `tsconfig.json` has `"sourceMap": true` in `compilerOptions`. Without it, breakpoints in `.ts` files appear as unverified. See `runtimes/node.md` § Debugger Properties.
---
## Generation Steps
For each service in the plan's Services table (where Generate is checked), generate the following artifacts in order. The plan specifies WHAT to generate; the reference files linked below specify HOW.
| # | Action | Reference |
|---|--------|-----------|
| 1 | **Generate docker-compose** — For each emulator in the plan's Emulators table, load the emulator reference and assemble the docker-compose service block. If migrations are checked, add healthcheck and migration service. | [emulators/](emulators/), [migrations.md](migrations.md) |
| 2 | **Generate VS Code debug config** — Assemble `launch.json` and `tasks.json` entries from the project-type and runtime references. For multi-service workspaces, generate compound configuration. | generate.md § Source Ownership, [project-types/](project-types/), [runtimes/](runtimes/), [multi-service.md](multi-service.md) |
| 3 | **Generate VS Code workspace config** — Assemble `.vscode/extensions.json` and `.vscode/settings.json` from project-type and runtime references. Add emulator data directory exclusions. | generate.md § VS Code Extension Recommendations, generate.md § VS Code Workspace Settings |
| 4 | **Configure connection strings** — Update `local.settings.json`, `.env`, or app config with emulator connection strings. Never overwrite existing values. | `project-types/{type}.md` § Connection Strings, `emulators/{name}.md` § Required App Environment Variables |
| 5 | **Generate convenience scripts** — For each checked script in the plan's Convenience Scripts table, add to the project's native script runner. | `runtimes/{rt}.md` § Convenience Scripts |
| 6 | **Generate migrations** — If the plan's Migrations table has checked rows, do targeted resolution for migration details, then generate the docker-compose migration service. | [migrations.md](migrations.md) |
| 7 | **Generate API test collections** — If the plan's API Test Collections table has checked rows, do targeted resolution for endpoints/triggers, then generate test scripts. | [api-test-collections.md](api-test-collections.md) |
> **Podman emulator certification.** When the plan's container runtime is **Podman**, each emulator's reference file declares its Podman status under `## Container Runtime Support`. Azurite and PostgreSQL are certified. For any emulator not marked Podman-certified, emit a `⚠️ LIMITED SUPPORT: Emulator "{name}" is not yet certified for Podman` warning (per [limited-support.md](limited-support.md)) and generate a best-effort compose service anyway — do **not** silently swap the runtime back to Docker.
---
## Dependency Availability
> ⚠️ **Do not assume CLI tools or packages are installed in the target project.**
Before writing any script or task command that invokes a CLI tool (e.g., `rimraf`, `concurrently`, `cross-env`):
1. **Check** — Verify the tool is already a project dependency.
2. **Add dependency** — Add it as a project dev dependency. This ensures it is version-locked and works consistently across all machines.
3. **Ask if uncertain** — Use `ask_user` if the tool is expensive, opinionated, or has multiple alternatives.
---
## Local Credentials
> ⚠️ **Never inline a literal credential into a task or a compose file.** Declare local credentials
> once in a workspace-root `.env` and reference them through Compose `${VAR}` interpolation
> everywhere else.
| Correct | Do **not** generate |
|---|---|
| Compose `${VAR}` interpolation from `.env` | A literal credential inlined into a task or compose file |
A concrete credential literal is rewritten by secret-redaction filters, and a masked value that
begins with `*` is a fatal YAML parse error — YAML reads a leading `*` as an alias reference, so the
whole compose file stops parsing.
Interpolation only resolves if the variable is actually defined: Compose reads `${...}` from `.env`
or the shell environment, never from a service's own `environment:` block. Whenever a generated
file references a variable — a compose service, a host task, or a connection string — `.env` must
declare every variable it needs, or the value silently becomes an empty string.
> ⛔ **`.gitignore` must list `.env` before you create it.** A credential that reaches a commit is
> compromised and has to be rotated — deleting it in a later commit leaves it in history, in every
> clone, and in every fork. Private repositories are no exception. If the project has no
> `.gitignore`, or has one that omits `.env`, fix that first.
See `emulators/{name}.md` § Required App Environment Variables for the per-emulator variable set.
---
## VS Code Debug & Task Configuration
Assemble `.vscode/launch.json` and `.vscode/tasks.json` by combining properties from the detected **project type** and **runtime** references. Use the source ownership table below to determine which file provides each property.
### Source Ownership
| Concern | Server-side project source | Browser SPA source |
|---------|---------------------------|-------------------|
| Debugger type (`node`, `coreclr`) | `runtimes/{rt}.md` § Debugger Properties | `project-types/frontend-spa/debug-adapters/{adapter}.md` |
| Debug port | `runtimes/{rt}.md` § Debugger Properties | N/A (uses dev server URL) |
| Request mode (`attach` / `launch`) | `project-types/{type}.md` § Runtime Wiring | `project-types/frontend-spa/frontend-spa.md` § Runtime Wiring |
| Top-level startup task (type, command, problem matcher) | `project-types/{type}.md` § VS Code Task Configuration | `project-types/frontend-spa/frontend-spa.md` § VS Code Task Configuration |
| Build chain tasks (install, clean, watch) | `runtimes/{rt}.md` § Build Chain | N/A (dev server handles compilation) |
| Runtime-specific launch properties (`outFiles`, `processName`) | `runtimes/{rt}.md` § Debugger Properties | N/A |
| Compound configuration | [multi-service.md](multi-service.md) § Compound Debug Configuration | [multi-service.md](multi-service.md) § Compound Debug Configuration |
| Working directory (`cwd`) rules | generate.md § Working Directory (`cwd`) Rules | generate.md § Working Directory (`cwd`) Rules |
| Task `runOptions` rules | generate.md § Task `runOptions` Rules | generate.md § Task `runOptions` Rules |
| Emulator startup task (`Start Emulators`) | `runtimes/{rt}.md` § Build Chain | N/A (backend service owns emulators) |
> `project-types/{type}.md` § VS Code Task Configuration provides **concrete task JSON per runtime** — use those blocks directly. `runtimes/{rt}.md` § Build Chain provides the dependency tasks that the startup task's `dependsOn` references.
### Service ID Derivation
Derive a canonical service ID from the plan's **Service Label** column: lowercase, kebab-case (e.g., "Functions API" → `functions-api`, "Web App" → `web-app`). This ID is used for:
- Task labels (e.g., `functions-api: func host start`)
- Launch config naming (use the plan's **Launch Config Name** column directly)
- Compound config member references
If two services resolve to the same ID, append the project type: `payments-api-functions`.
> ⛔ Every generated task label should conform to `{service-id}: {task name}` (e.g., `functions-api: func host start`, `functions-api: dotnet build`). Wherever a task label is referenced — generation blocks, `dependsOn` chains, `preLaunchTask` values, validation Ready-Signal tables, and validation checklists — it **MUST** use this `{service-id}:` form. Instruction files and any examples that show a label without the `{service-id}:` prefix added are illustrating the latter part of the label; resolve it to the full form before writing or matching.
### Task Chain Shape (Server-side only)
> Browser-based projects (e.g., Frontend SPA) skip the build chain — the dev server task is the only task. See `project-types/{type}.md` § VS Code Task Configuration.
```
"{service-id}: {top-level-task}" ← project-type-specific (see project-types/{type}.md)
├── dependsOn: "{service-id}: {watch-task}" ← from runtimes/{rt}.md
│ └── dependsOn: "{service-id}: {clean-task}"
│ └── dependsOn: "{service-id}: {install-task}"
└── dependsOn: "Start Emulators" ← only when emulators are required
```
> Adjust task labels and commands for alternative package managers (`yarn`, `pnpm`, `gradle`). The key invariant is the chain shape: **install → clean → build/watch → top-level task** (with `Start Emulators` as a sibling dependency of the top-level task, NOT nested under install). Some runtimes skip steps — use only what applies.
### Project Type Path Resolution
Most project types have a single reference file at `project-types/{type}.md`. Some use a subdirectory:
| Project Type | Reference Path |
|--------------|---------------|
| `functions` | `project-types/functions.md` |
| `frontend-spa` | `project-types/frontend-spa/frontend-spa.md` |
When instructions reference `project-types/{type}.md`, resolve via this table. If the type is not listed, look for `project-types/{type}.md` first, then `project-types/{type}/{type}.md`.
### Working Directory (`cwd`) Rules
> ⚠️ **CRITICAL for multi-service repos.** Without correct `cwd`, commands like `npm install` or `func host start` will run from the workspace root and fail.
Use the **Service Root** column from the plan's Services table to determine the `cwd` for each task.
| Task Scope | `cwd` Setting | Example |
|------------|--------------|---------|
| **Per-service tasks** (install, clean, watch, build, top-level) | `"options": { "cwd": "${workspaceFolder}/{service-root}" }` | `"cwd": "${workspaceFolder}/api"` |
| **Shared tasks** (Start Emulators) | Workspace root (omit `cwd` — it defaults to workspace root) | — |
| **Single-service repos** | Omit `cwd` — workspace root is the service root | — |
### Task `runOptions` Rules
**Every** task in `tasks.json` must include `runOptions` with `instanceLimit: 1` and `instancePolicy: "silent"` — install, clean, watch, build, top-level, emulator, **and the sequenced compound task** from [multi-service.md](multi-service.md) § Compound Debug Configuration. There is no task type this is optional for; if you are writing a `"label"`, you are writing `runOptions` beside it.
This prevents duplicate task instances and silently skips re-invocations when a task is already running (e.g., from compound + individual `preLaunchTask` chains). The compound task is the one that needs it most, not least: it is reachable both directly and as a `preLaunchTask`, so it is the likeliest to be invoked twice.
### Start Emulators Task
When the plan includes emulators, generate a shared `Start Emulators` task. This task is a **sibling dependency** of each service's top-level startup task (e.g., `func host start`). Do NOT place `Start Emulators` as a dependency of build chain tasks like `npm install` or `npm watch` — it belongs in the startup task's `dependsOn` array alongside the build chain prerequisite.
Use the plan's **Compose Command** for the task `command` — `docker compose up -d` by default, or `podman compose up -d` when the plan's Orchestrator selected Podman.
```json
{
"type": "shell",
"label": "Start Emulators",
"command": "docker compose up -d",
"problemMatcher": [],
"runOptions": { "instanceLimit": 1, "instancePolicy": "silent" }
}
```
### instanceLimit and instancePolicy
Set **`instanceLimit: 1`** and **`instancePolicy: "silent"`** on every task. You never want parallel instances of the same build or startup task, and `"silent"` ensures that duplicate invocations are skipped without prompting the user.
### Problem Matchers
**Background tasks (`isBackground: true`) MUST have a real `problemMatcher`.** An empty matcher (`"problemMatcher": []`) on a background task causes a blocking VS Code dialog ("This task is tracked by a problem matcher"). Look up the correct matcher from the relevant reference file:
- **Runtime tasks** (build, watch) → `runtimes/{rt}.md` § VS Code Problem Matchers
- **Project-type tasks** (top-level startup) → `project-types/{type}.md` § VS Code Task Configuration
- **Frontend dev servers** → `project-types/frontend-spa/frontend-spa.md` § Framework Lookup Table
For non-background tasks:
| Task Type | Matcher |
|-----------|---------|
| Short-lived commands (`npm install`, `npm clean`, `docker compose up -d`) | `"problemMatcher": []` |
| Dependency-only tasks (only `dependsOn`, no `command`) | Omit `problemMatcher` entirely |
Short-lived commands use an empty matcher to suppress matching on tasks that don't produce compiler-style output.
### Example
```json
{
"type": "shell",
"label": "npm install",
"command": "npm install",
"runOptions": { "instanceLimit": 1, "instancePolicy": "silent" },
"problemMatcher": []
}
```
---
## VS Code Extension Recommendations (`.vscode/extensions.json`)
Aggregate extension recommendations from the detected **runtime** and **project type** into `.vscode/extensions.json`. Each source lists its required extensions in a `## VS Code Extension Recommendations` section with an `Extension ID | Why Required` table.
### Assembly Protocol
| Step | Action | Details |
|------|--------|---------|
| 1. Collect | Gather from runtime | Read extension IDs from `runtimes/{rt}.md § VS Code Extension Recommendations` |
| 2. Collect | Gather from project type | Read extension IDs from `project-types/{type}.md § VS Code Extension Recommendations` |
| 3. Deduplicate | Remove duplicates | Each extension ID appears once in the final list |
| 4. Write | Output `.vscode/extensions.json` | Contribute to the file — do not replace existing entries |
### Output Format
```json
{
"recommendations": [
"{runtime-extension-1}",
"{project-type-extension-1}"
]
}
```
---
## VS Code Workspace Settings (`.vscode/settings.json`)
Aggregate workspace settings from the detected **runtime**, **project type**, and **emulator configuration** into `.vscode/settings.json`. Each source lists its contributed settings in a `## VS Code Workspace Settings` section with a `Setting | Value | Why` table.
### Assembly Protocol
| Step | Action | Details |
|------|--------|---------|
| 1. Collect | Gather from runtime | Read settings from `runtimes/{rt}.md § VS Code Workspace Settings` |
| 2. Collect | Gather from project type | Read settings from `project-types/{type}.md § VS Code Workspace Settings` |
| 3. Collect | Gather emulator exclusions | Derive data directory exclusions from `docker-compose.yml` `volumes:` mounts |
| 4. Collect | Gather required debug settings | Add the mandatory keys from generate.md § Required Debug Settings |
| 5. Write | Output `.vscode/settings.json` | Merge into the file — contribute without replacing existing entries or user customizations |
### Required Debug Settings
Always merge these keys into the workspace `.vscode/settings.json` on **every** run which is workspace scope only. Preserve any existing entries; if a key is already present, leave its value untouched.
| Setting | Value | Why |
|---------|-------|-----|
| `debug.onTaskErrors` | `"debugAnyway"` | Suppresses VS Code's blocking "task errors" modal on F5, which can appear as a project launching blocker when it's often not. |
```json
{
"debug.onTaskErrors": "debugAnyway"
}
```
### Emulator Data Directory Exclusions
When emulators are configured with **workspace bind mounts** (e.g. Azurite's `./.azurite:/data`), add their data directories to both `files.exclude` and `search.exclude` in **`.vscode/settings.json`** to reduce workspace noise:
```json
{
"files.exclude": {
"**/.azurite": true
},
"search.exclude": {
"**/.azurite": true
}
}
```
> Derive directory names from the actual **bind-mount** `volumes:` entries in `docker-compose.yml` (a `./.name:/path` host mount) — do not hardcode. **Named volumes** (e.g. Postgres's `postgres_data`) live inside the container engine, not the workspace, so they need no exclusion. Each emulator's data pattern is defined in `emulators/{name}.md`.
@@ -1,74 +0,0 @@
<!-- 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.
# Limited Support Warnings
> Emit a standardized warning when the skill detects a feature that is not yet fully supported. The warning format is consistent across all feature categories so the user always knows what to expect.
---
## ⛔ Detection Algorithm — MANDATORY
For every project type, runtime, and emulator declared in the plan's tables, follow this procedure **exactly**:
1. **Check for a matching reference file** — List the files and subdirectories in the category folder (ignoring `_template.md`). If any filename or subdirectory name loosely matches the declared value (ignoring case, spaces, dashes, and underscores), the feature has a reference file.
2. **Check the status inside the reference file** — Open the matched reference file and look for a status indicator. If the file is marked `🔲 Planned` (in a frontmatter note, status field, or top-level callout), the feature has **limited support** despite having a reference file. You **MUST** emit a warning.
3. **If a match exists AND the file is not marked Planned** → the feature is fully supported. Proceed normally.
4. **If no match exists** → the feature has limited support. You **MUST** emit a warning. Do NOT substitute a different, supported feature.
| Category | Category Folder | Path Notes |
|----------|-----------------|------------|
| Project type | `references/project-types/` | Some types use subdirectories (e.g., `frontend-spa/frontend-spa.md`). Match against both filenames and subdirectory names. |
| Runtime | `references/runtimes/` | |
| Emulator | `references/emulators/` | |
> ⚠️ **Do NOT skip this check.** Every declared feature MUST be verified against the category folder before generating artifacts.
---
## Warning Format
```
⚠️ LIMITED SUPPORT: {Category} "{value}" is not yet fully supported.
```
Where:
- `{Category}` — a short label for the feature area (e.g., `Project type`, `Runtime`, `Emulator`)
- `{value}` — the specific feature declared in the plan (e.g., `python`, `Container App`, `Cosmos DB`)
---
## ⛔ Emission Protocol — MANDATORY
When a limited-support feature is detected, you **MUST** follow this exact sequence:
### Step 1: Emit in assistant message
Write the canonical warning in your **regular assistant message text**. This is mandatory — the warning must be visible in the chat output, not hidden inside a tool call.
```
⚠️ LIMITED SUPPORT: Emulator "Durable Task Scheduler" is not yet fully supported.
```
### Step 2: Confirm with user
For the **first** limited-support feature encountered in the session, call `ask_user` to confirm whether to proceed:
```
ask_user(
question: "⚠️ LIMITED SUPPORT: {Category} \"{value}\" is not yet fully supported. Would you still like me to put forth a best-effort attempt?",
choices: [
"Yes, proceed with best effort",
"No, stop here"
]
)
```
If the user agrees, treat that as blanket consent for the rest of the session. Any additional limited-support features discovered later should still be emitted as a warning — but do **not** call `ask_user` again.
> Emit each `⚠️ LIMITED SUPPORT:` warning exactly once per `(Category, value)` pair in assistant messages.
---
## No Silent Substitution
**Do NOT silently substitute a supported alternative** when a feature has limited support (e.g. switching a Container App to Azure Functions, or Python to Node.js). Always generate for the project type, runtime, and emulators declared in the plan, even when support is limited.
@@ -1,87 +0,0 @@
<!-- 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 Migrations — Generation
Generate compose migration services from the plan's Migrations table. The plan records WHAT migration tool is in use and which service needs it. This reference covers HOW to generate the compose configuration.
> The migration service is ordinary compose content and is **identical for Docker or Podman**. Any task or command that drives it (`docker compose up db-migrate`) uses the plan's Orchestrator **Compose Command** — substitute `podman compose` when the plan selected Podman.
---
## Targeted Resolution
The plan's Migrations table provides: `Generate | Service | Migration Tool`. Before generating the docker-compose migration service, perform targeted resolution to fill in the details:
| Detail | How to Resolve |
|--------|---------------|
| **Migration directory** | Scan for tool-specific directories: `prisma/migrations/`, `migrations/`, `Migrations/`, `alembic/` |
| **Migration command** | Check the project's script runner (e.g., `package.json` scripts) for an existing migration command. If found, use it. If not, construct from the tool name. |
| **Target database service** | Match against the plan's Emulators table — the database emulator's compose service name |
| **Connection env var** | Check `local.settings.json`, `.env`, or the migration tool's config file for the variable name |
| **Compose-network connection string** | Same shape as the local connection string but with the compose service name as host instead of `localhost` |
| **Existing script** | Check whether a migration script already exists in the project's script runner |
### Migration Script Lookup
| Detection Evidence | Instruction |
|--------------------|-------------|
| Existing migration script in project (e.g., `npm run db:migrate`) | Use it as-is in the docker-compose service |
| Migration tool detected but no script | Create a script in the project's native script runner that wraps the tool's CLI command (e.g., `"db:migrate": "npx prisma migrate deploy"` in `package.json`) |
| Raw SQL files only, no migration tool | Recommend and install a lightweight migration tool as a dev dependency (e.g., `node-pg-migrate` for Node.js). Ask the user before installing. |
---
## Docker Compose Patterns
Two patterns are needed: a **healthcheck** on the database service and a one-shot **migration service**.
### Healthcheck Pattern
When migrations are present, the target database service **must** have a healthcheck so the migration service can use `depends_on` with `condition: service_healthy`. The healthcheck definition belongs in the emulator's docker-compose config — see the emulator reference files in [emulators/](emulators/).
### Migration Service Pattern
```yaml
services:
db-migrate:
image: ${RUNTIME_IMAGE}
working_dir: /app
depends_on:
${DATABASE_SERVICE}:
condition: service_healthy
volumes:
- ./:/app:ro
${EXTRA_VOLUME_MOUNTS}
environment:
${CONNECTION_ENV_VAR}: ${CONNECTION_STRING_FOR_COMPOSE_NETWORK}
entrypoint: ${MIGRATION_SCRIPT}
restart: "no"
```
**Filling in the template — use resolved details:**
| Placeholder | How to determine |
|-------------|-----------------|
| `RUNTIME_IMAGE` | A Docker image that provides the language runtime. See the table below. |
| `DATABASE_SERVICE` | The compose service name for the target database (from Emulators table) |
| `CONNECTION_ENV_VAR` | The environment variable the migration tool expects (from targeted resolution) |
| `CONNECTION_STRING_FOR_COMPOSE_NETWORK` | Same shape as local connection string but with compose service name as host |
| `EXTRA_VOLUME_MOUNTS` | Additional mounts needed for the ecosystem. See the table below. |
| `MIGRATION_SCRIPT` | The project's migration script command (from targeted resolution) |
**Runtime images and extra volume mounts:**
| Ecosystem | Image | Extra Volume Mounts | Notes |
|-----------|-------|-------------------|-------|
| Node.js / TypeScript | `node:{major}-slim` | `./node_modules:/app/node_modules:ro` | Mount node_modules separately for native modules |
| .NET | `mcr.microsoft.com/dotnet/sdk:{version}` | — | Best-effort — emit limited support warning |
| Python | `python:{version}-slim` | `./.venv:/app/.venv:ro` (if applicable) | Best-effort — emit limited support warning |
| Java | `eclipse-temurin:{version}` | — | Best-effort — emit limited support warning |
| Go | `golang:{version}` | — | Best-effort — emit limited support warning |
> **Key properties:**
> - `depends_on` with `condition: service_healthy` — waits for the database to accept connections
> - `volumes` with `:ro` — mounts project files read-only for safety
> - `restart: "no"` — runs once per `docker compose up`, does not restart after exit
> - Mount ecosystem-specific dependency directories when the migration tool is installed as a project dependency
@@ -1,119 +0,0 @@
<!-- 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.
# Multi-Service Orchestration — Generation
> Applies when the plan's Services table has **2+ rows** (excluding the compound config row). Governs compound debug configuration, working directory rules, startup ordering, and partial configuration handling.
---
## Port Assignment
When two or more services share the same runtime, each needs a unique debug port. Look up the `Base debug port` from `runtimes/{rt}.md` and assign ports sequentially: first service gets the base port, second gets base + 1, third gets base + 2, etc.
> Browser-based project types (e.g., Frontend SPA) do not use debug ports — they connect via the dev server URL instead.
---
## Partial Configuration Handling
Check each service root for existing VS Code debug config before generating anything. A service is considered already configured if it has an existing debug configuration entry in `.vscode/launch.json` matching its service ID or Launch Config Name.
| State | Action |
|-------|--------|
| **Fully configured service** | Skip all artifact generation for that service; carry its existing config into the compound configuration unchanged |
| **Partially configured service** | Generate only what is missing (e.g. tasks but no debug config → generate debug config only) |
| **Unconfigured service** | Generate all artifacts as normal |
Adding a second service to an existing single-service repo is safe — the original service's config is preserved and the new service is added alongside it.
---
## Compound Debug Configuration
> ⛔ **MANDATORY:** When 2+ service roots are detected (including Frontend SPA projects), a compound debug configuration **must** be generated. A frontend SPA counts as a service root — it does not need emulators, but it does need a debug config entry and inclusion in the compound.
> ⚠️ **Working directory:** Multi-service task chains require correct `cwd` on every per-service task. See [generate.md § Working Directory (`cwd`) Rules](generate.md) — without it, commands like `npm install` or `func host start` run from the workspace root and fail.
Use the plan's Services table to assemble the compound config. The compound config row in the plan specifies the Launch Config Name (e.g., "Debug All Services").
### Startup Ordering
VS Code compound launch configurations always start their listed configurations **in parallel**. There is no `dependsOrder` property for compounds — only individual tasks support sequenced dependencies. This means you cannot directly tell a compound to "start the backend before the frontend."
When a frontend service proxies to a local backend (e.g., a dev server proxy for API calls), the backend must be ready before the frontend starts. Otherwise the frontend proxy may produce `ECONNREFUSED` errors on startup. Since compounds cannot enforce this ordering, the workaround is to push the sequencing into a **compound task** that uses `dependsOrder: "sequence"`, and set that task as the compound's `preLaunchTask`.
> The plan may include a note like "ℹ️ **Proxy detected:**" indicating this dependency. Check the frontend's config files (e.g., `vite.config.ts` `server.proxy`) to confirm.
#### Pattern
**1. Generate a sequenced compound task** that starts services in order. Give it any descriptive label (referred to below as `{sequenced-compound-task}`):
```json
{
"label": "{sequenced-compound-task}",
"dependsOn": [
"{backend-service-id}: {backend-top-level-task}",
"{frontend-service-id}: {frontend-top-level-task}"
],
"dependsOrder": "sequence",
"runOptions": { "instanceLimit": 1, "instancePolicy": "silent" }
}
```
The backend is listed first. Its `problemMatcher` (from `project-types/{type}.md`) signals "ready" before the frontend task starts.
**2. Set the compound config's `preLaunchTask`** to the sequenced task:
```json
{
"name": "{Launch Config Name from plan's compound row}",
"configurations": ["{Backend Launch Config Name}", "{Frontend Launch Config Name}"],
"preLaunchTask": "{sequenced-compound-task}",
"stopAll": true
}
```
**3. Individual configs keep their own `preLaunchTask`** so they work standalone:
```json
{
"name": "{Backend Launch Config Name}",
"preLaunchTask": "{backend-service-id}: {backend-top-level-task}"
}
```
```json
{
"name": "{Frontend Launch Config Name}",
"preLaunchTask": "{frontend-service-id}: {frontend-top-level-task}"
}
```
**4. Set `instanceLimit: 1` and `instancePolicy: "silent"` on background tasks** to prevent duplicate instances.
When the compound runs, the sequenced compound task starts both services via `dependsOrder: "sequence"`. Then each individual configuration's `preLaunchTask` fires again — but those services are already running. With `instanceLimit: 1` and `instancePolicy: "silent"`, the duplicate invocation is silently skipped and the existing instance keeps running.
#### Why This Pattern Is Necessary
| Concern | How it's solved |
|---------|----------------|
| Compounds can't sequence configurations | The sequenced compound **task** (`dependsOrder: "sequence"`) handles ordering instead |
| Backend must be ready before frontend starts | Backend is listed first in `dependsOn`; its problem matcher signals readiness |
| Debuggers must not attach before services are running | The compound's `preLaunchTask` ensures all services are started before any debugger attaches |
| Individual configs must still work standalone | Each config has its own `preLaunchTask` pointing to its service's top-level task |
| Duplicate task invocations from compound + individual preLaunchTasks | `instanceLimit: 1` + `instancePolicy: "silent"` silently skips the duplicate — the first instance keeps running |
---
### Deduplicated Startup Graph
Any generated compound configuration MUST satisfy **all** of the following. Each rule is checkable against `tasks.json` / `launch.json`:
| # | Rule | How to check |
|---|------|--------------|
| 1 | **Each service's top-level task appears exactly once** across the compound's effective task graph (the transitive `dependsOn` closure of the sequenced compound task). No service's start task is reachable via two different paths. | Expand the `dependsOn` closure of the sequenced compound task; every service's top-level task label occurs exactly once. |
| 2 | **The sequenced compound task is the single owner of startup ordering.** No per-service task re-declares a `dependsOn` on another service's start task; ordering lives only in the sequenced compound task. | No service start task lists another service's start task in its own `dependsOn`. |
| 3 | **Every background task sets `instanceLimit: 1` and `instancePolicy: "silent"`.** | Every long-running task in the chain carries both properties. |
Because the individual configs keep their own `preLaunchTask` (so they work standalone), the compound WILL invoke each service's start task a second time. Rule 3 makes that second invocation a silent no-op instead of a duplicate process, while Rules 1 and 2 ensure the *first* pass never double-starts a service either.
@@ -1,88 +0,0 @@
<!-- 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.
# Pre-Flight Checks
Verify the plan exists and environment is ready before proceeding onwards to generating files.
## Container Runtime Readiness Check
Read the container runtime from the plan's Orchestrator table (**Docker**, **Podman**, or **Podman (Docker-compatible)**) and confirm the engine is actually running before you generate or validate anything. A CLI on PATH is not the same as a running engine.
- **Docker** — confirm the daemon is reachable (`docker info`). If it errors, tell the user to start Docker Desktop / the Docker service and wait, then retry.
- **Podman (Docker-compatible)** — the plan runs the Podman engine through its Docker-compatible socket, and the generated tasks use `docker compose`. Confirm the socket answers with `docker info` (it will report the Podman server). If `docker info` errors, the Docker-compatible socket isn't up: tell the user to enable Docker compatibility in Podman Desktop and ensure the **Podman machine** is started (same machine check as below), then retry.
- **Podman** — confirm the engine is reachable (`podman info`). On **Windows and macOS** this needs a running **Podman machine**:
```bash
podman machine list --format '{{.Name}} {{.Running}}'
```
- If a machine exists but is **stopped**, ask the user before starting it (starting a VM is a stateful, resource-consuming action):
```
ask_user(
question: "Your Podman machine \"podman-machine-default\" is stopped. Start it so the emulators can run?",
choices: [
"Yes, start the Podman machine",
"No, I'll start it myself"
]
)
```
On approval, run `podman machine start` and wait for it to report ready.
- If **no machine exists**, do NOT silently `podman machine init` — tell the user to run `podman machine init && podman machine start` (a one-time, multi-minute setup) and re-run once it is ready.
> ⛔ **NEVER switch the plan's container runtime on your own — not at readiness, not during generation, not during validation.** The runtime in the Orchestrator table was chosen (and, when the user asked for a specific engine, chosen *by them*). If the selected engine fails at **any** point — the engine won't start, a container won't come up, ports don't forward to the host, a volume/permission error, a health check never passes — **STOP and surface the blocker to the user with `ask_user`**, describing the failure and the options (fix the engine, or explicitly switch to the other engine). Do **not** silently rewrite the Orchestrator row, the `Start Emulators` task, or the convenience scripts to a different engine and continue. Silently falling back — e.g. from a user-requested Podman to Docker because Podman had a networking or bind-mount issue — is a **failure of this agent**, even if the resulting app works: the user asked for Podman and must decide, not discover after the fact that Docker ran everything.
>
> When you do surface a runtime blocker, prefer offering the **fix** first (many Podman-on-Windows issues are config, not dead ends — e.g. use the WSL machine provider for host port forwarding; database emulators must use a named volume, not a workspace bind mount, so `initdb` can `chown` its data dir). Only switch engines if the user explicitly chooses to.
## Stale Data Directory Check
Before generating any files, check for leftover emulator state from a previous run — both **workspace data directories** (bind-mounted emulators such as Azurite's `.azurite/`, `.cosmos/`, `.servicebus/`) and **named volumes** (database emulators such as Postgres's `postgres_data`; see [emulators/postgres.md](emulators/postgres.md)). Stale state can cause container startup failures — for example, PostgreSQL's `initdb` will refuse to initialize if the data directory (the `postgres_data` volume) already contains files from an incompatible or partially-initialized cluster.
- **Bind-mounted data directories:** list them with `ls`/`Get-ChildItem` in the workspace.
- **Named volumes:** list them with `docker compose ls` / `docker volume ls` (or the `podman` equivalents) — a project-scoped `*_postgres_data` volume from a prior run is the database equivalent of a stale directory.
If any stale state is found:
1. **List all found directories and named volumes** with their sizes.
2. **Ask the user how to proceed** using `ask_user`:
```
ask_user(
question: "Leftover emulator state was found from a previous run:\n\n- .azurite/ (12 MB, workspace directory)\n- postgres_data (45 MB, named volume)\n\nThese can cause container startup failures. How would you like to handle this?",
choices: [
"Delete them and start fresh (recommended)",
"Keep them — I want to preserve the existing data"
]
)
```
3. **If the user chooses to delete** — Remove bind-mounted directories with platform-appropriate removal (`rm -rf` on macOS/Linux, `Remove-Item -Recurse -Force` on Windows), and remove named volumes with `docker compose down -v` / `podman compose down -v` (or `docker volume rm <project>_postgres_data`), before proceeding with generation.
4. **If the user wants to keep them** — Proceed, but warn that containers may fail to start. If they do fail, offer to clean up at that point.
5. **Never delete data directories or volumes silently** — Always confirm with the user first.
---
## Port Conflict Check
Before generating any files, scan all ports required by the planned emulators (e.g. `lsof -i -P -n`). For each occupied port, identify the process name and PID.
If any conflicts are found:
1. **List all conflicts clearly** — port number, process name, PID.
2. **Ask the user how to proceed** using `ask_user`:
```
ask_user(
question: "The following ports are already in use on your machine:\n\n- Port 5432 → postgres (PID 1234)\n\nThese ports are needed by the planned emulators. How would you like to handle this?",
choices: [
"Help me remap the conflicting ports to alternatives",
"I'll handle it myself — proceed with the plan as-is"
]
)
```
3. **If the user wants help remapping** — Propose alternative port numbers, update all references in the plan and project files (docker-compose service ports, connection strings, convenience scripts, VS Code debug config), then resume generation.
4. **If the user will handle it themselves** — Proceed with generation using the original ports.
5. **Never remap ports or modify config silently** — Always confirm with the user before making changes.
@@ -1,169 +0,0 @@
<!-- 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.
# {Type} — Project Type
> **Template** — Copy this file to `project-types/{type}.md` when adding a new server-side project type. For browser-based project types, use `project-types/frontend-spa/` as a reference — the structure differs (Framework Lookup Table replaces Startup Command, debug adapters replace Debugger Properties).
---
## Detection Signals
<!-- Files/packages/patterns that identify this project type. Used during the plan phase. -->
| Signal | Notes |
|--------|-------|
| `{file}` | {description} |
---
## Prerequisites
<!-- Tools or CLIs required by this project type (not the runtime — those go in runtimes/{rt}.md § Prerequisites).
Example: Azure Functions Core Tools for functions.md. Omit this section if the project type has no additional tool requirements. -->
| Tool | Detection Command | Required For | Install Link |
|------|-------------------|-------------|-------------|
| `{tool}` | `{command}` | {purpose} | `{install-url}` |
---
## Runtime Support Matrix
<!-- Tracks implementation readiness — separate from Runtime Wiring below.
Fill in status per runtime: ✅ Full, ⚠️ Emulators only, 🔲 Planned. -->
| Runtime | Status | Reference |
|---------|--------|-----------|
| node-ts | | |
| node-js | | |
| dotnet | | |
| python | | |
| java | | |
| go | | |
---
## Dependency Discovery
<!-- How Azure service dependencies are found: bindings, SDK scan, or framework conventions. -->
<!-- Maps to emulators/{name}.md entries. -->
| Dependency Signal | Azure Service | Emulator |
|------------------|---------------|---------|
| `{signal}` | {service} | [{name}](../emulators/{name}.md) |
---
## Startup Command
<!-- How the app starts locally. E.g.: func host start, docker compose up, npm run dev.
For VS Code custom task types (e.g., "type": "func"), the executable prefix is supplied
by the extension — omit it from the task JSON command. Show the full CLI equivalent here. -->
```
{command}
```
---
## Runtime Wiring
<!-- Quick-reference index mapping runtimes to project-type-specific task configuration.
See § VS Code Task Configuration below for the concrete task JSON per runtime.
See generate.md § Source Ownership for how project-type and runtime refs combine. -->
> See **§ VS Code Task Configuration** below for the concrete task JSON for each runtime.
| Runtime | Startup task label | Task type | Problem matcher | Request Mode | Status | Reference |
|---------|--------------------|-----------|-----------------|--------------|--------|-----------|
| node-ts | {label} | {shell\|func\|...} | {matcher} | {attach\|launch} | | |
| node-js | {label} | {shell\|func\|...} | {matcher} | {attach\|launch} | | |
| dotnet | {label} | {shell\|func\|...} | {matcher} | {attach\|launch} | | |
| python | {label} | {shell\|func\|...} | {matcher} | {attach\|launch} | | |
| java | {label} | {shell\|func\|...} | {matcher} | {attach\|launch} | | |
| go | {label} | {shell\|func\|...} | {matcher} | {attach\|launch} | | |
### VS Code Debug Configuration
<!-- Only needed for browser-based project types where the debugger type comes from the project type
rather than the runtime (e.g., Frontend SPA uses "chrome").
Server-side project types get their debugger type from runtimes/{rt}.md instead — omit this section for those. -->
### VS Code Task Configuration
<!-- THE authoritative source for top-level task JSON for this project type.
Provide one concrete JSON block per implemented runtime.
Include type, label, command, problemMatcher, isBackground, runOptions, and dependsOn.
The dependsOn array references task labels from runtimes/{rt}.md § Build Chain.
See generate.md § Task runOptions Rules for instanceLimit and instancePolicy guidance. -->
### Connection Strings
<!-- Where emulator conn strings are placed. E.g.: local.settings.json, .env, compose env vars -->
| Emulator | Key | Value | File |
|----------|-----|-------|------|
| {emulator} | `{VAR_NAME}` | `{value}` | `{file}` |
---
## API Test Collections
<!-- Which test patterns to generate for this project type. Reference api-test-collections.md for the script templates. -->
See [api-test-collections.md](../api-test-collections.md) for all test script patterns.
---
## VS Code Extension Recommendations (`.vscode/extensions.json`)
<!-- Extensions required by this project type. Aggregated with runtime extensions into .vscode/extensions.json by generate.md. -->
| Extension ID | Why Required |
|--------------|-------------|
| `{extension-id}` | {reason} |
---
## VS Code Workspace Settings (`.vscode/settings.json`)
<!-- Settings contributed by this project type. Aggregated with runtime settings into .vscode/settings.json by generate.md. -->
| Setting | Value | Why |
|---------|-------|-----|
| `{setting.key}` | `{value}` | {reason} |
---
## Validation Signals
<!-- Used by validation.md during Phase 3 to verify the generated debug configuration works.
When adding a new project type, you MUST fill in these tables — validation.md references them. -->
### Ready Signal
| Top-Level Task | Ready Signal (stdout) |
|----------------|----------------------|
| {task label} | `"{pattern}"` |
### HTTP Verification
| Curl Target | Expected Status | Notes |
|-------------|-----------------|-------|
| `http://localhost:{port}/{path}` | `{status}` | {notes} |
---
## Checklist — {Type} Project Validation
<!-- Post-generation self-check for this project type. Verify generated artifacts are correct — do not run or start anything. -->
After generating `launch.json`, `tasks.json`, and `extensions.json`, verify the following were produced correctly:
1. ✅ Startup task exists in `tasks.json` with the correct type and problem matcher
2. ✅ `launch.json` `preLaunchTask` points to the startup task
3. ✅ `.vscode/extensions.json` includes project-type extensions listed above
4. ✅ `dependsOn` chain includes runtime build/watch task and `Start Emulators` (when emulators are required)
> Runtime-specific checks (e.g., build task, debugger type) are defined in `runtimes/{rt}.md`.
@@ -1,40 +0,0 @@
<!-- 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.
# {Adapter} — Browser Debug Adapter
> **Template** — Copy this file to `{adapter}.md` when adding a new browser debug adapter.
## VS Code Debugger Type
| Browser | VS Code Debugger Type | Required Extension |
|---------|----------------------|-------------------|
| {browser} | `{type}` | {extension or "None — built-in"} |
---
## Launch Configuration
```json
{
"name": "{id} (debug)",
"type": "{type}",
"request": "launch",
"url": "http://localhost:{port from Framework Lookup Table}",
"preLaunchTask": "{id} dev"
}
```
| Field | Source |
|-------|--------|
| `type` | VS Code Debugger Type from table above |
| `url` | Default Port from [frontend-spa.md § Framework Lookup Table](../frontend-spa.md) |
| `preLaunchTask` | `{id} dev` — the dev server task from [frontend-spa.md § VS Code Task Configuration](../frontend-spa.md) |
<!-- Add any adapter-specific fields and notes below. -->
---
## Notes
<!-- Document adapter-specific behavior, extra fields, or required extensions here. -->
@@ -1,43 +0,0 @@
<!-- 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.
# Blazor WASM — Browser Debug Adapter
> 🔲 **Planned** — This adapter is not yet implemented. Emit a `⚠️ LIMITED SUPPORT:` warning per [limited-support.md](../../../limited-support.md).
## VS Code Debugger Type
| Browser / Runtime | VS Code Debugger Type | Required Extension |
|-------------------|----------------------|-------------------|
| Blazor WASM (.NET) | `blazorwasm` | `ms-dotnettools.csharp` |
---
## Launch Configuration
```json
{
"name": "{id} (debug)",
"type": "blazorwasm",
"request": "launch",
"url": "http://localhost:{port from Framework Lookup Table}",
"browser": "chrome",
"cwd": "${workspaceFolder}/{service-root}",
"preLaunchTask": "{id} dev"
}
```
| Field | Source |
|-------|--------|
| `type` | Always `blazorwasm` |
| `url` | Default Port from [frontend-spa.md § Framework Lookup Table](../frontend-spa.md) |
| `browser` | Which browser to launch — defaults to `chrome` |
| `cwd` | .NET project root |
| `preLaunchTask` | `{id} dev` — the dev server task from [frontend-spa.md § VS Code Task Configuration](../frontend-spa.md) |
---
## Notes
- The `blazorwasm` adapter is provided by the C# extension, not built into VS Code.
- The launch config shape differs from the CDP-based `chrome`/`msedge` adapter — it requires `browser` and `cwd` fields, and does not use `webRoot`.
@@ -1,76 +0,0 @@
<!-- 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.
# Chromium — Browser Debug Adapter
> Covers all **Chromium-based browsers** (Chrome, Edge, etc.). The launch configuration is identical — only the `type` field differs.
## VS Code Debugger Type
| Browser | VS Code Debugger Type | Required Extension |
|---------|----------------------|-------------------|
| Chrome | `chrome` | None — built-in |
| Edge | `msedge` | None — built-in |
Use the browser recorded in the plan's Prerequisites section (see [prerequisites.md](../../../../../shared-references/prerequisites.md) § Browser detection) to pick the `type`.
---
## Launch Configuration
```json
{
"name": "{id} (debug)",
"type": "{chrome or msedge}",
"request": "launch",
"url": "http://localhost:{port from Framework Lookup Table}",
"webRoot": "${workspaceFolder}/{service-root}",
"preLaunchTask": "{id} dev"
}
```
| Field | Source |
|-------|--------|
| `type` | Browser Debugger Type from table above — matches the browser recorded in the plan's Prerequisites (`chrome` for Chrome, `msedge` for Edge) |
| `url` | Default Port from [frontend-spa.md § Framework Lookup Table](../frontend-spa.md) |
| `webRoot` | **Framework root** — see `webRoot` Resolution below |
| `preLaunchTask` | `{id} dev` — the dev server task from [frontend-spa.md § VS Code Task Configuration](../frontend-spa.md) |
---
## `webRoot` Resolution
> **Universal rule:** `webRoot` must ALWAYS be the **framework root directory** (where the framework's config file lives), NEVER a subdirectory within it. The dev server determines its URL path structure relative to this root. Setting `webRoot` to a subdirectory like `src/` causes path doubling and breaks breakpoint resolution.
This applies to **all frameworks** — any framework where the config file lives in a parent directory of `src/` will exhibit the same bug.
| Framework | Config file that defines the root | `webRoot` value |
|-----------|----------------------------------|-----------------|
| Vite | `vite.config.*` | Directory containing `vite.config.*` |
| CRA | `package.json` (with `react-scripts`) | Directory containing `package.json` |
| Angular | `angular.json` | Workspace root (or project root in monorepo) |
| Next.js | `next.config.*` | Directory containing `next.config.*` |
| Blazor WASM | `*.csproj` | Directory containing `*.csproj` |
> ⛔ **Before writing `webRoot`**, verify: does `{service-root}` contain the framework config file (e.g., `vite.config.ts`, `angular.json`)? If the plan's Service Root points to a `src/` subdirectory that does NOT contain the config file, walk up to the parent that does.
---
## Notes
- The `"request": "launch"` mode opens a new browser window with CDP debugging enabled — breakpoints work immediately.
- `webRoot` maps the browser's served files back to workspace source files for correct breakpoint resolution.
- Chrome and Edge share the same CDP protocol, so all behavior is identical between them.
---
## Troubleshooting — first-launch browser failure
> ⚠️ A browser debug config can fail on the **first** launch (**F5**) — the browser window doesn't open, or the debugger never attaches. This is almost always **browser process / first-run state, NOT a defect in the generated `launch.json`**. The two common causes are (1) a browser instance is already running, and (2) a one-time development HTTPS certificate prompt interrupts startup (e.g. after a TLS/dev-cert setup by the app or a local emulator).
**Don't assume a first-launch failure is a bad config.** The generated config may be correct — jumping straight to rewriting it (e.g. swapping `launch`→`attach` or hand-authoring a dedicated Edge process task) may treat the wrong cause. Guide the user through the following fixes below **first**:
- **Close every window of the debug browser** (all Edge windows, or all Chrome windows), then press **F5** again. An already-running browser is the most common cause of a failed debug launch.
- If a **development certificate / HTTPS prompt** appeared during the first run, accept it, then close all browser windows and retry so the trusted certificate takes effect.
- If it still fails, explore if you can switch the config `type` between `msedge` and `chrome` (whichever other Chromium browser is installed), or use `editor-browser` to debug inside VS Code.
- **Only if normal launch keeps failing after the above**, fall back to launching the browser explicitly with a dedicated remote-debugging port and its own `--user-data-dir`, then switch the config to `attach`. This is heavier but a viable last resort.
@@ -1,181 +0,0 @@
<!-- 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.
# Project Type: Frontend SPA
Reference guide for local development setup of frontend single-page application projects.
> All SPA frameworks share the same VS Code debug configuration shape: browser debugger type (e.g., `chrome`, `msedge`), `launch` request mode, dev server as a prerequisite task. The per-framework differences — runtime, startup command, default port, and background problem matcher — are resolved from the Framework Lookup Table below.
---
## Detection Signals
Match against these signals to classify a workspace root as a frontend SPA. If the workspace clearly is an SPA but doesn't match a specific signal below, still classify it as Frontend SPA — use the Framework Lookup Table's fallback row for configuration.
| Signal | Notes |
|--------|-------|
| `vite.config.*` or `vite` in devDependencies | Vite-based SPA |
| `next.config.*` or `next` in dependencies | Next.js app |
| `angular.json` | Angular app |
| `react-scripts` in dependencies | Create React App |
| `*.razor` files + `Microsoft.AspNetCore.Components.WebAssembly` in `*.csproj` | Blazor WASM |
---
## Prerequisites
No VS Code extension is required — browser debugging uses VS Code's built-in adapter. A Chromium-based browser (Chrome or Edge) must be installed, though; that is captured as a Debug prerequisite via [prerequisites.md](../../../../shared-references/prerequisites.md) § Browser detection. Runtime prerequisites (e.g., Node.js) are listed in `runtimes/{rt}.md § Prerequisites`.
---
## Runtime Support Matrix
Runtime support for Frontend SPAs is tracked per-framework in the **Framework Lookup Table** below. Each row's `Status` column indicates implementation readiness.
---
## Dependency Discovery
Frontend SPAs communicate with backend services via HTTP during local development — they do not connect to Azure emulators directly. In monorepo setups, Azure service dependencies (storage, databases, etc.) are handled by the backend project type. In standalone SPA projects, the backend may already be running as a deployed service or a separate local process — no emulator setup is needed for the SPA itself.
---
## Backend Proxy Dependencies
Frontend dev servers often proxy API requests to a local backend during development. In multi-service setups, if a proxy config is detected pointing to another local service, that backend service's startup task should be a dependency of the frontend's dev server task to prevent `ECONNREFUSED` errors during initial page load.
| Framework | Proxy Config Location |
|-----------|----------------------|
| Vite | `server.proxy` in `vite.config.*` |
| Create React App | `"proxy"` field in `package.json` |
| Angular | `proxy.conf.json` |
| Next.js | `rewrites()` in `next.config.*` |
---
## Startup Command
The startup command varies per framework. See the **Framework Lookup Table** below for the specific command for each detected framework (e.g., `npm run dev` for Vite, `npm start` for Angular).
---
## Framework Lookup Table
Use this table to resolve per-framework values when generating the VS Code configuration. The `Status` column is the source of truth for implementation readiness — if a framework is not ✅ Implemented, emit a `⚠️ LIMITED SUPPORT:` warning per [limited-support.md](../../limited-support.md).
> **To add a new framework:** add a row with its runtime, detection signal, startup command, default port, problem matcher patterns, and status.
| Framework | Runtime | Detection | Startup Command | Default Port | Ready Pattern (begins) | Ready Pattern (ends) | Status |
|-----------|---------|----------|-----------------|--------------|----------------------|---------------------|--------|
| Vite | node | `vite.config.*` or `vite` in devDependencies | `npm run dev` | 5173 | `VITE` | `Local:` | ✅ Implemented |
| Next.js | node | `next.config.*` or `next` in dependencies | `npm run dev` | 3000 | `\s*ready` | `started server on` | ✅ Implemented |
| Angular | node | `angular.json` | `npm start` | 4200 | `Compiling` | `Compiled successfully` | ✅ Implemented |
| Create React App | node | `react-scripts` in dependencies | `npm start` | 3000 | `Starting the development server` | `Compiled` | ✅ Implemented |
| Blazor WASM | dotnet | `*.razor` + `WebAssembly` SDK in `*.csproj` | `dotnet watch run` | 5000 | `Now listening on` | `Application started` | 🔲 Planned |
| other | — | No match | — | — | — | — | [limited-support.md](../../limited-support.md) |
> ⚠️ **ANSI escape codes:** Dev servers often wrap output in color codes (e.g., `\x1b[32m...\x1b[0m`). Prefer plain-text anchors that appear outside styled regions (e.g., `Local:` instead of `ready in \d+`) to avoid regex mismatches.
---
## Runtime Wiring
<!-- For Frontend SPAs, all configuration comes from this file — the Framework Lookup Table and VS Code Debug Configuration.
Browser-based projects do not use runtimes/{rt}.md for debugger type or build chain.
See generate.md § Source Ownership for the server-side vs browser distinction. -->
| Startup command | Startup task label | Task type | Problem matcher | Request Mode |
|----------------|-------------------|-----------|-----------------|--------------|
| From Framework Lookup Table | `{id} dev` | `shell` | From Framework Lookup Table | `launch` |
### VS Code Debug Configuration
The request mode is always `launch` (VS Code opens the browser). The browser (Chrome or Edge) is the one recorded in the plan's Prerequisites section during the browser detection pass (see [prerequisites.md](../../../../shared-references/prerequisites.md) § Browser detection) — use its debug adapter `type` (`chrome` for Chrome, `msedge` for Edge). The user can change the browser in the plan before approval.
Look up the launch configuration template from the corresponding adapter file in [`debug-adapters/`](debug-adapters/):
| Browser / Adapter | Adapter File | Status |
|-------------------|-------------|--------|
| Chromium (Chrome, Edge, etc.) | [debug-adapters/chromium.md](debug-adapters/chromium.md) | ✅ Implemented |
| Blazor WASM (.NET) | [debug-adapters/blazorwasm.md](debug-adapters/blazorwasm.md) | 🔲 Planned |
| ∞ | [debug-adapters/_template.md](debug-adapters/_template.md) | — |
> **To add a new debug adapter:** copy `debug-adapters/_template.md` to `debug-adapters/{adapter}.md` and add a row to this table.
### VS Code Task Configuration
The top-level task is the framework's dev server. No runtime build chain — the dev server handles compilation internally. The task label follows the pattern `"{id} dev"`.
```json
{
"type": "shell",
"label": "{id} dev",
"command": "{command from Framework Lookup Table}",
"options": { "cwd": "${workspaceFolder}/{service-root}" },
"isBackground": true,
"runOptions": { "instanceLimit": 1, "instancePolicy": "silent" },
"problemMatcher": {
"owner": "{framework name, lowercased}",
"pattern": { "regexp": "^$" },
"background": {
"activeOnStart": true,
"beginsPattern": "{Ready Pattern (begins) from Framework Lookup Table}",
"endsPattern": "{Ready Pattern (ends) from Framework Lookup Table}"
}
}
}
```
### Connection Strings
Not applicable — Frontend SPAs do not connect to Azure emulators directly. In monorepo setups, connection strings are owned by the backend project type.
---
## API Test Collections
Not applicable — Frontend SPAs do not expose API endpoints. In monorepo setups, API test collections are owned by the backend project type. See [api-test-collections.md](../../api-test-collections.md) for backend patterns.
---
## VS Code Extension Recommendations (`.vscode/extensions.json`)
No project-type-specific extensions. Browser debugging uses VS Code's built-in capabilities.
> Framework-specific extensions (if any) would be listed here. Runtime extensions are listed in `runtimes/{rt}.md`.
---
## VS Code Workspace Settings (`.vscode/settings.json`)
No project-type-specific workspace settings.
---
## Validation Signals
Used by [validation.md](../../validation.md) during Phase 3 to verify the generated debug configuration works.
### Ready Signal
Use the `Ready Pattern (begins)` and `Ready Pattern (ends)` columns from the **Framework Lookup Table** above for the detected framework. The ready signal is observed on stdout of the dev server task.
### HTTP Verification
| Curl Target | Expected Status | Notes |
|-------------|-----------------|-------|
| `http://localhost:{dev-server-port}` | `200` or `301` | Use the resolved dev server port from the Framework Lookup Table. Validates the dev server started — do NOT launch a browser. Framework-specific redirects (e.g., `301` for Next.js) are acceptable. |
---
## Checklist — Frontend SPA Project Validation
After generating `launch.json` and `tasks.json`, verify the following were produced correctly:
1. ✅ Dev server task exists in `tasks.json` with a custom `background` problem matcher using the correct begin/end patterns from the Framework Lookup Table
2. ✅ `launch.json` uses the browser debug adapter matching the browser recorded in the plan's Prerequisites (`chrome` or `msedge`) with `"request": "launch"`
3. ✅ `launch.json` `url` matches the framework's default port from the Framework Lookup Table
4. ✅ `launch.json` `preLaunchTask` points to the dev server task
> Runtime-specific checks (e.g., build task, debugger type) are defined in `runtimes/{rt}.md`.
@@ -1,281 +0,0 @@
<!-- 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.
# Project Type: Azure Functions
Reference guide for local development setup of Azure Functions projects.
---
## Detection Signals
| Signal | Notes |
|--------|-------|
| `host.json` present | Primary signal — required |
| Azure Functions SDK in dependencies | Confirms it's a Functions project — identified during the plan phase |
---
## Prerequisites
| Tool | Detection Command | Required For | Install Link |
|------|-------------------|-------------|-------------|
| Azure Functions Core Tools | `func --version` | Run Functions host locally | [aka.ms/azure-functions-core-tools](https://aka.ms/azure-functions-core-tools) |
---
## Runtime Support Matrix
| Runtime | Status | Reference |
|---------|--------|-----------|
| node-ts | ✅ Implemented | [runtimes/node.md](../runtimes/node.md) |
| node-js | ✅ Implemented | [runtimes/node.md](../runtimes/node.md) |
| dotnet (Functions isolated) | ✅ Implemented | [runtimes/dotnet.md](../runtimes/dotnet.md) |
| python | 🔲 Planned | [limited-support.md](../limited-support.md) |
| java | 🔲 Planned | [limited-support.md](../limited-support.md) |
> **Limited-support runtimes:** When a runtime with limited support is detected, emit a `⚠️ LIMITED SUPPORT:` warning per [limited-support.md](../limited-support.md) and ask the user whether to proceed. If the user agrees, proceed with best-effort generation for all artifacts (emulators, debug config, tasks). Do not silently skip debug/launch configuration — let the user decide.
---
## Dependency Discovery
Scan every `function.json` for its `"type"` binding field, **or** scan Python/Java source files for trigger decorator/attribute names. Each binding maps to an emulator.
### Binding → Emulator Mapping
| Binding Type(s) | Azure Service | Default Ports | Connection String | Status | Reference |
|----------------|---------------|---------------|-------------------|--------|-----------|
| `blobTrigger`, `blob` | Blob Storage | 10000 | `UseDevelopmentStorage=true` | ✅ Implemented | [emulators/azurite.md](../emulators/azurite.md) |
| `queueTrigger`, `queue` | Queue Storage | 10001 | `UseDevelopmentStorage=true` | ✅ Implemented | [emulators/azurite.md](../emulators/azurite.md) |
| `table` | Table Storage | 10002 | `UseDevelopmentStorage=true` | ✅ Implemented | [emulators/azurite.md](../emulators/azurite.md) |
| `cosmosDBTrigger`, `cosmosDB` | Cosmos DB | 8081, 10250–10254 | — | 🔲 Planned | [limited-support.md](../limited-support.md) |
| `serviceBusTrigger`, `serviceBus` | Service Bus | 5672 | — | 🔲 Planned | [limited-support.md](../limited-support.md) |
| `eventHubTrigger`, `eventHub` | Event Hubs | 9093 | — | 🔲 Planned | [limited-support.md](../limited-support.md) |
| `sql`, `sqlTrigger` | Azure SQL | 1433 | — | 🔲 Planned | [limited-support.md](../limited-support.md) |
| `httpTrigger` | (built-in) | — | — | ✅ Implemented | — |
| `timerTrigger` | (built-in) | — | — | ✅ Implemented | — |
> **Azurite consolidation:** If multiple storage bindings (blob + queue + table) are detected, create a **single** Azurite service — not one per binding type.
### Services Without Azure Emulators
| Binding Type | Azure Service | Recommendation |
|-------------|---------------|----------------|
| `signalR` | Azure SignalR | Use a dev-tier Azure SignalR instance |
| PostgreSQL (SDK, not a binding) | Azure Database for PostgreSQL | [emulators/postgres.md](../emulators/postgres.md) |
---
## Startup Command
> For VS Code `"type": "func"` tasks, omit the `func` executable prefix — the Azure Functions extension supplies it. The equivalent CLI commands are shown below for reference.
**Node.js (TypeScript & JavaScript):**
```
languageWorkers__node__arguments="--inspect=9229" func host start
```
> ⚠️ The Azure Functions Core Tools do **not** automatically enable the Node.js debugger. You **must** supply `--inspect=9229` to the Node worker so it opens a debug port for VS Code to attach to. Without it, the `attach` configuration connects to nothing. In the VS Code `func` task, set this via `options.env` (`languageWorkers__node__arguments`) rather than a command-line flag — the env-var form avoids shell/JSON quoting issues and uses the same `languageWorkers__<runtime>__arguments` convention across runtimes.
**dotnet:**
```
func host start
```
> For .NET, the Functions host spawns a worker process that VS Code attaches to via `coreclr` — no additional debug flags are needed on the command line.
---
## Runtime Wiring
<!-- Quick-reference index. See § VS Code Task Configuration below for the concrete task JSON per runtime. -->
> See **§ VS Code Task Configuration** below for the concrete task JSON for each runtime.
| Runtime | Startup task label | Task type | Problem matcher | Request mode | Status | Reference |
|---------|--------------------|-----------|-----------------|--------------|--------|-----------|
| node-ts | `{service-id}: func host start` | `func` | `$func-node-watch` | `attach` | ✅ Implemented | [runtimes/node.md](../runtimes/node.md) |
| node-js | `{service-id}: func host start` | `func` | `$func-node-watch` | `attach` | ✅ Implemented | [runtimes/node.md](../runtimes/node.md) |
| dotnet | `{service-id}: func host start` | `func` | `$func-dotnet-watch` | `attach` | ✅ Implemented | [runtimes/dotnet.md](../runtimes/dotnet.md) |
| python | `{service-id}: func host start` | `func` | `$func-python-watch` | `attach` | 🔲 Planned | [limited-support.md](../limited-support.md) |
| java | `{service-id}: func host start` | `func` | `$func-java-watch` | `attach` | 🔲 Planned | [limited-support.md](../limited-support.md) |
> `{service-id}` is the kebab-case ID derived from the plan's Service Label column — see [generate.md § Service ID Derivation](../generate.md).
> **dotnet `processName` warning.** The .NET `coreclr` (request: `attach`) configuration requires the literal `processName` in `launch.json` — `.exe` suffix on Windows, no extension on macOS/Linux. Without it, F5 fails with `"No process with the specified name is currently running"`. Do NOT use `${command:pickProcess}`. See [runtimes/dotnet.md § processName Determination](../runtimes/dotnet.md).
The startup step `dependsOn`:
1. The runtime-specific build/watch task label from `runtimes/{rt}.md` § Build Chain (e.g., `{service-id}: npm watch` for node-ts, `{service-id}: dotnet build` for dotnet)
2. `"Start Emulators"` (only when emulators are required — omit when the plan has no checked emulators)
### VS Code Task Configuration
The top-level task uses the VS Code `func` task type provided by the Azure Functions extension (`ms-azuretools.vscode-azurefunctions`). The launch configuration's `preLaunchTask` points to this task.
> **Task label scoping:** All task labels MUST be prefixed with the service ID (e.g., `functions-api: func host start`). This prevents label collisions in multi-service workspaces. See [generate.md § Service ID Derivation](../generate.md).
#### Debug Argument Injection
The Functions host runs your code in a separate **language-worker** process, so the worker must start with the runtime's debug flag. Inject it through the task's `options.env` using the `languageWorkers__<runtime>__arguments` convention, then point a matching `attach` config at the resulting port. Only the env key, the flag value, and the attach `type`/`port` change per runtime — the rest of the `func host start` task stays identical, so new runtimes slot straight into this table.
| Runtime | `options.env` setting | Value to inject | Debug port | `attach` type | Status |
|---------|-----------------------|-----------------|------------|---------------|--------|
| node-ts | `languageWorkers__node__arguments` | `--inspect=9229` | 9229 | `node` | ✅ Implemented |
| node-js | `languageWorkers__node__arguments` | `--inspect=9229` | 9229 | `node` | ✅ Implemented |
**node-ts** (has watch task):
```json
{
"type": "func",
"label": "{service-id}: func host start",
"command": "host start",
"options": {
"cwd": "${workspaceFolder}/{path-to-functions-project}",
"env": { "languageWorkers__node__arguments": "--inspect=9229" }
},
"problemMatcher": "$func-node-watch",
"isBackground": true,
"runOptions": { "instanceLimit": 1, "instancePolicy": "silent" },
"dependsOn": ["{service-id}: npm watch", "Start Emulators"]
}
```
> Remove `"Start Emulators"` from `dependsOn` when the plan has no checked emulators.
**node-js** (no compile/watch step):
```json
{
"type": "func",
"label": "{service-id}: func host start",
"command": "host start",
"options": {
"cwd": "${workspaceFolder}/{path-to-functions-project}",
"env": { "languageWorkers__node__arguments": "--inspect=9229" }
},
"problemMatcher": "$func-node-watch",
"isBackground": true,
"runOptions": { "instanceLimit": 1, "instancePolicy": "silent" },
"dependsOn": ["{service-id}: npm install", "Start Emulators"]
}
```
> Remove `"Start Emulators"` from `dependsOn` when the plan has no checked emulators.
> `dependsOn`: first entry is the runtime-specific prerequisite — watch task for TypeScript, install task for JavaScript. The exact task labels come from `runtimes/{rt}.md` § Build Chain, prefixed with the service ID.
**dotnet** (compiled — requires build before host start):
```json
{
"type": "func",
"label": "{service-id}: func host start",
"command": "host start",
"options": { "cwd": "${workspaceFolder}/{path-to-functions-project}" },
"problemMatcher": "$func-dotnet-watch",
"isBackground": true,
"runOptions": { "instanceLimit": 1, "instancePolicy": "silent" },
"dependsOn": ["{service-id}: dotnet build", "Start Emulators"]
}
```
> Remove `"Start Emulators"` from `dependsOn` when the plan has no checked emulators.
> **dotnet `processName`:** The `coreclr` attach configuration requires a literal `processName` in `launch.json`. See [runtimes/dotnet.md § processName Determination](../runtimes/dotnet.md) for how to derive it from the `.csproj`, including cross-platform rules (`.exe` suffix on Windows only).
#### .NET Isolated Worker Version Constraints
Functions Worker **2.x** is required for .NET 10:
- `Microsoft.Azure.Functions.Worker >= 2.50.0`
- `Microsoft.Azure.Functions.Worker.Sdk >= 2.0.5`
When detecting a .NET Functions project, verify these minimum versions. Worker 2.x uses the canonical `func host start` + `coreclr` (request: `attach`) flow where the Functions host spawns the worker process.
### Connection Strings
| Emulator | Key | Value | Status | Reference |
|----------|-----|-------|--------|-----------|
| Azurite (storage) | `AzureWebJobsStorage` | `UseDevelopmentStorage=true` | ✅ Implemented | [emulators/azurite.md](../emulators/azurite.md) |
| Cosmos DB | {detected from bindings} | — | 🔲 Planned | [limited-support.md](../limited-support.md) |
| Service Bus | {detected from bindings} | — | 🔲 Planned | [limited-support.md](../limited-support.md) |
| Event Hubs | {detected from bindings} | — | 🔲 Planned | [limited-support.md](../limited-support.md) |
| SQL Edge | {detected from bindings} | — | 🔲 Planned | [limited-support.md](../limited-support.md) |
| PostgreSQL | {detected from code} | See [emulators/postgres.md](../emulators/postgres.md) | ✅ Implemented | [emulators/postgres.md](../emulators/postgres.md) |
> **Key discovery:** Except for `AzureWebJobsStorage` (a well-known Azure Functions convention), connection string key names are not fixed. Perform targeted resolution — check `local.settings.json`, `.env`, binding configurations, and SDK usage to detect the actual key names. Use the detected names — do not invent defaults.
> **Never overwrite** existing values in `local.settings.json` — only add missing keys.
---
## API Test Collections
See [api-test-collections.md](../api-test-collections.md) for all test script patterns. For this project type, generate tests for:
- HTTP triggers → HTTP patterns with `baseUrl: http://localhost:7071/api`
- Blob triggers → Storage § Blob trigger pattern
- Queue triggers → Storage § Queue trigger pattern
- Timer triggers → Timer § admin API pattern (only if explicitly requested)
- Cosmos DB triggers → Cosmos DB pattern
- Service Bus triggers → Service Bus pattern
- Event Hub triggers → Event Hubs pattern
---
## VS Code Extension Recommendations (`.vscode/extensions.json`)
Contribute the following to `.vscode/extensions.json`.
| Extension ID | Why Required |
|--------------|-------------|
| `ms-azuretools.vscode-azurefunctions` | Contributes the `"type": "func"` task type |
---
## VS Code Workspace Settings (`.vscode/settings.json`)
Contribute the following to `.vscode/settings.json`.
| Setting | Value | Why |
|---------|-------|-----|
| `azureFunctions.showProjectWarning` | `false` | Suppresses the "failed to detect project" prompt that fires when the extension scans the workspace — our generated config already handles project setup |
| `azureFunctions.validateEmulators` | `false` | Suppresses emulator validation warnings from the extension — emulators are managed via user's orchestrator configuration |
---
## Validation Signals
Used by [validation.md](../validation.md) during Phase 3 to verify the generated debug configuration works.
### Ready Signal
| Top-Level Task | Ready Signal (stdout) |
|----------------|----------------------|
| `{service-id}: func host start` | `"Host lock lease acquired"` or `"Functions host started"` |
> The `Top-Level Task` column uses the canonical `{service-id}:`-prefixed label — see [generate.md § Service ID Derivation](../generate.md). Resolve `{service-id}` to the same value used during generation before matching against `tasks.json`.
### HTTP Verification
| Curl Target | Expected Status | Notes |
|-------------|-----------------|-------|
| First discovered anonymous `httpTrigger` route (e.g., `http://localhost:7071/api/{function-name}`) | `200` | Port `7071` is the Functions host HTTP port; debug port `9229` is for the debugger only. Use the first anonymous HTTP trigger found during targeted resolution. If only function-key/admin routes exist, skip HTTP verification with a warning rather than assuming a route. |
---
## Checklist — Functions Project Validation
After generating `launch.json`, `tasks.json`, and `extensions.json`, verify the following were produced correctly:
1. ✅ `{service-id}: func host start` task exists in `tasks.json` with `"type": "func"`
2. ✅ `launch.json` `preLaunchTask` points to `{service-id}: func host start`
3. ✅ `.vscode/extensions.json` includes `ms-azuretools.vscode-azurefunctions`
4. ✅ `local.settings.json` contains all required connection string keys (e.g., `AzureWebJobsStorage`)
5. ✅ `dependsOn` chain includes the runtime build/watch task and `Start Emulators` (when emulators are required)
> Runtime-specific checks (e.g., `dotnet build` task, `processName` derivation) are defined in `runtimes/{rt}.md`.
@@ -1,138 +0,0 @@
<!-- 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.
# {Runtime} — Debug & Build Configuration
> **Template** — Copy this file to `runtimes/{rt}.md` when adding a new runtime.
---
## Prerequisites
<!-- Required tools, SDKs, version managers — language toolchain only. -->
<!-- Do NOT list project-type-specific tools here (e.g., Functions Core Tools belongs in functions.md). -->
| Tool | Detection Command | Required For | Install Link |
|------|-------------------|-------------|-------------|
| `{tool}` | `{tool} --version` | {purpose} | `{install-url}` |
---
## Debugger Properties
<!-- Generic debug properties for this runtime.
See project-types/{type}.md § Runtime Wiring for how these combine with the host command.
See generate.md § Source Ownership for how runtime and project-type refs compose. -->
| Property | Value | Notes |
|----------|-------|-------|
| Debug protocol | `{protocol}` | The wire protocol the runtime exposes (e.g., `Node Inspector`, `CoreCLR DAP`, `debugpy DAP`, `JDWP`, `Delve DAP`). |
| VS Code debugger type | `{type}` | The VS Code debugger adapter identifier (e.g., `node`, `coreclr`, `debugpy`, `java`, `go`). |
| Base debug port | `{port}` | Default debug port for this runtime; overridden per-service in monorepos |
### VS Code Problem Matchers
<!-- Problem matchers for VS Code tasks.json. -->
| Task | Watch Problem Matcher | Build Problem Matcher |
|------|----------------------|----------------------|
| {task} | `{matcher}` | `{matcher}` |
---
## Build Chain
<!-- Build steps owned by this runtime: install, build/watch.
The startup task is provided by the project type's Runtime Wiring table, not this file.
Wire: startup task depends on ["build/watch step", "Start Emulators"]. -->
Chain shape (startup task comes from the project type):
```
"{service-id}: {startup task}" ← from project-types/{type}.md Runtime Wiring
├── dependsOn: "{service-id}: {build/watch step}" ← this file
└── dependsOn: "Start Emulators" ← only when emulators are required
```
> **Task label scoping:** All task labels MUST be prefixed with the service ID derived from the plan's Service Label column. See [generate.md § Service ID Derivation](../generate.md).
### Build Commands
| Step | Command | Purpose | Background? |
|------|---------|---------|------------|
| Start Emulators | `docker compose up -d` | Start all emulator services (idempotent — no-op if already running) | No |
See [generate.md](../generate.md) § Task `runOptions` Rules for how these build steps are rendered into VS Code task configuration.
---
## Convenience Scripts
<!-- HOW scripts are registered for this runtime (package.json, Makefile, pyproject.toml, scripts/ dir).
The plan's Convenience Scripts table drives WHICH scripts to generate — do not hardcode script names here. -->
The plan's Convenience Scripts table specifies WHICH scripts to generate. This section covers HOW to register them for this runtime.
**Script runner:** `{file}` (e.g., `package.json` scripts, `scripts/` directory, `Makefile`, `pyproject.toml`)
**Run command pattern:** `{command}` (e.g., `npm run {script}`, `./scripts/{script}.sh`)
> When the runtime's script runner is not inherently cross-platform (e.g., standalone scripts vs `package.json`), generate platform-appropriate scripts or document cross-platform alternatives.
### Script Format
<!-- Show the format for adding a script entry to this runtime's script runner. -->
### Common Script Implementations
Use these implementations when building scripts from the plan:
| Script Purpose | Typical Command | Notes |
|---------------|-----------------|-------|
| Start emulators | `docker compose up -d` | Idempotent — safe to re-run |
| Stop emulators | `docker compose down` | Stops and removes containers |
| Clean emulator data | Stop containers with volumes, then remove bind-mount data directories | Run `docker compose down -v` (`-v` also drops **named volumes** like Postgres's `postgres_data`), then remove `{data-dirs}` = `./.{name}` **bind-mount** directories derived from `docker-compose.yml` `volumes:` mounts. Use platform-appropriate removal. |
| Run migrations | `{migration tool CLI command}` | See [migrations.md](../migrations.md) for how to determine the command |
---
## VS Code Extension Recommendations (`.vscode/extensions.json`)
<!-- Extensions required by this runtime. Aggregated with project-type extensions into .vscode/extensions.json by generate.md. -->
| Extension ID | Why Required |
|--------------|-------------|
| `{extension-id}` | {reason} |
---
## VS Code Workspace Settings (`.vscode/settings.json`)
<!-- Settings contributed by this runtime. Aggregated with project-type settings into .vscode/settings.json by generate.md. -->
| Setting | Value | Why |
|---------|-------|-----|
| `{setting.key}` | `{value}` | {reason} |
---
## Checklist — {Runtime} Validation
> ⛔ **MANDATORY — runs during Phase 3 validation after all artifacts are generated.** You MUST verify every item below. Do NOT skip, assume, or approximate results.
<!-- Post-generation self-check for this runtime. Verify generated artifacts are correct — do not run or start anything. -->
After generating VS Code configuration, verify the following were produced correctly:
### Post-Generation Checks
1. ✅ Build task exists in `tasks.json` with the correct problem matcher
2. ✅ `launch.json` uses the correct debugger type and request mode
3. ✅ `.vscode/extensions.json` includes runtime extensions listed above
### Live Validation Checks
<!-- These checks run during Phase 3 validation (validation.md Step 7), after the ready signal is observed.
Add debugger-specific verifications here (e.g., process attachment, source map verification).
validation.md delegates to this section — if you add checks here, they WILL be executed. -->
> Project-type-specific checks (e.g., startup task, connection strings) are defined in `project-types/{type}.md`.
@@ -1,194 +0,0 @@
<!-- 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.
# .NET / C# — Debug & Build Configuration
> Covers **.NET** projects. Project-type-specific hosting (Functions, App Service, etc.) is defined in `project-types/{type}.md` — this file covers the .NET runtime layer only.
## Prerequisites
| Tool | Detection Command | Required For | Install Link |
|------|-------------------|-------------|-------------|
| .NET SDK | `dotnet --version` | Build and run .NET projects | [dotnet.microsoft.com](https://dotnet.microsoft.com/download) |
| C# extension | VS Code extension `ms-dotnettools.csharp` installed | Contributes the `coreclr` debugger type required for F5 debugging | [Marketplace](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) |
---
## Debugger Properties
<!-- Generic debug properties for this runtime.
See project-types/{type}.md § Runtime Wiring for how these combine with the host command.
See generate.md § Source Ownership for how runtime and project-type refs compose. -->
| Property | Value | Notes |
|----------|-------|-------|
| Debug protocol | `CoreCLR DAP` | .NET Core / .NET 5+ debug adapter protocol |
| VS Code debugger type | `coreclr` | Contributed by the C# extension (`ms-dotnettools.csharp`) |
| Request mode | `attach` | VS Code attaches to a running .NET process by `processName` |
| Base debug port | — | CoreCLR attach uses `processName`, not a port |
### VS Code Problem Matchers
| Task | Watch Problem Matcher | Build Problem Matcher |
|------|----------------------|----------------------|
| `dotnet build` | — | `$msCompile` |
---
## processName Determination
The `coreclr` debugger (request: `attach`) matches `processName` **literally** against the OS process list. Getting this wrong means F5 silently fails.
### Cross-Platform Rules
| Platform | Process name | Example |
|----------|-------------|---------|
| Windows | `{AssemblyName}.exe` | `Scrapbook.Api.exe` |
| macOS / Linux | `{AssemblyName}` (no extension) | `Scrapbook.Api` |
> ⛔ **Windows requires the `.exe` suffix.** Writing `"Scrapbook.Api"` instead of `"Scrapbook.Api.exe"` produces:
>
> ```
> No process with the specified name is currently running.
> ```
### Deriving `AssemblyName`
The process name is derived from the project's `.csproj`:
1. If `<AssemblyName>` is defined, use that value
2. Otherwise, the `.csproj` filename without extension (e.g., `Functions.csproj` → `Functions`)
3. The `.csproj` **MUST** have `<OutputType>Exe</OutputType>` — otherwise no executable is produced and there's no target to debug
**Verify before writing `launch.json`:** after `dotnet build`, confirm the executable exists at `{projectDir}/bin/Debug/{tfm}/{AssemblyName}[.exe]`.
> ❌ **Do NOT use `${command:pickProcess}` or `${command:azureFunctions.pickProcess}`** — both pop a blocking dialog every F5. Use literal `processName`.
---
## Build Chain
Tasks owned by this runtime: build. The startup task and its dependency wiring are provided by the project type's Runtime Wiring table — not this file.
> **Task label scoping:** All task labels MUST be prefixed with the service ID (e.g., `functions-api: dotnet build`). See [generate.md § Service ID Derivation](../generate.md).
Chain shape (startup task comes from the project type):
```
"{service-id}: {startup task}" ← from project-types/{type}.md Runtime Wiring
├── dependsOn: "{service-id}: dotnet build" ← this file
└── dependsOn: "Start Emulators" ← only when emulators are required
```
### Build Commands
| Step | Task Label | Command | Purpose | Background? |
|------|-----------|---------|---------|------------|
| build | `{service-id}: dotnet build` | `dotnet build {path-to-csproj} --configuration Debug` | Restore + compile | No |
```json
{
"label": "{service-id}: dotnet build",
"type": "process",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/{path-to-csproj}",
"--configuration",
"Debug"
],
"problemMatcher": "$msCompile",
"group": "build",
"runOptions": { "instanceLimit": 1, "instancePolicy": "silent" }
}
```
> .NET does not have a watch-based incremental compile step in the debug chain (unlike TypeScript's `tsc --watch`). Each F5 re-runs `dotnet build`.
See [generate.md](../generate.md) § Task `runOptions` Rules for how these build steps are rendered into VS Code task configuration.
---
## Convenience Scripts
The plan's Convenience Scripts table specifies WHICH scripts to generate. This section covers HOW to register them for .NET projects.
Because .NET projects do not have a built-in script runner equivalent to `npm run`, convenience scripts live as standalone scripts under `scripts/`.
**Script runner:** Scripts in `scripts/` directory
**Run command pattern:** Platform-dependent (see below)
### Script Format
Generate scripts appropriate for the user's platform:
| Platform | File extension | Shebang / header | Make executable |
|----------|---------------|-------------------|-----------------|
| macOS / Linux | `.sh` | `#!/bin/bash` | `chmod +x` |
| Windows | `.ps1` | — | — (PowerShell scripts run directly) |
> When the platform is ambiguous, generate `.sh` scripts — they also work on Windows via Git Bash or WSL.
### Common Script Implementations
Use these implementations when building scripts from the plan:
| Script Purpose | Typical Command | Notes |
|---------------|-----------------|-------|
| Start emulators | `docker compose up -d` | Idempotent — safe to re-run |
| Stop emulators | `docker compose down` | Stops and removes containers |
| Clean emulator data | Stop containers with volumes, then remove bind-mount data directories | Run `docker compose down -v` (`-v` also drops **named volumes** like Postgres's `postgres_data`), then remove `{data-dirs}` = `./.{name}` **bind-mount** directories derived from `docker-compose.yml` `volumes:` mounts. Use platform-appropriate removal (e.g., `rm -rf` on macOS/Linux, `Remove-Item -Recurse` on Windows) |
| Run migrations | `{migration tool CLI command}` | See [migrations.md](../migrations.md) for how to determine the command |
---
## VS Code Extension Recommendations (`.vscode/extensions.json`)
<!-- Extensions required by this runtime. Aggregated with project-type extensions into .vscode/extensions.json by generate.md. -->
| Extension ID | Why Required |
|--------------|-------------|
| `ms-dotnettools.csharp` | Contributes the `"type": "coreclr"` debugger for .NET attach/launch |
| `ms-dotnettools.csdevkit` | C# Dev Kit — adds Solution Explorer, test discovery, and enhanced debugging experience |
> Project-type-specific extensions (e.g., `ms-azuretools.vscode-azurefunctions` for Functions) are listed in `project-types/{type}.md`.
---
## VS Code Workspace Settings (`.vscode/settings.json`)
<!-- Settings contributed by this runtime. Aggregated with project-type settings into .vscode/settings.json by generate.md. -->
| Setting | Value | Why |
|---------|-------|-----|
| `files.exclude: **/bin` | `true` | Hide .NET build output from explorer |
| `files.exclude: **/obj` | `true` | Hide .NET intermediate build files from explorer |
> These exclusions reduce workspace noise. They are deep-merged with any existing `files.exclude` entries — see [generate.md § VS Code Workspace Settings](../generate.md).
---
## Checklist — .NET Runtime Validation
> ⛔ **MANDATORY — runs during Phase 3 validation after all artifacts are generated.** You MUST verify every item below. Do NOT skip, assume, or approximate results.
After generating VS Code configuration, verify the following were produced correctly:
### Post-Generation Checks
1. ✅ `dotnet build` task exists in `tasks.json` with `$msCompile` problem matcher
2. ✅ `processName` in `launch.json` matches the `AssemblyName` derived from `.csproj` (platform-appropriate — see § processName Determination)
3. ✅ `launch.json` uses `"type": "coreclr"` with `"request": "attach"`
4. ✅ `.vscode/extensions.json` includes `ms-dotnettools.csharp` and `ms-dotnettools.csdevkit`
### Live Validation Checks
These checks run during Phase 3 validation ([validation.md](../validation.md) Step 7), after the ready signal is observed:
1. ✅ Verify the target process exists for attachment — the `processName` in `launch.json` must match a running OS process:
- **Windows PowerShell:** `Get-Process -Name "<processName without .exe>" -ErrorAction SilentlyContinue` → must return a process
- **macOS/Linux:** `pgrep -x "<processName>"` → must return a PID
2. ✅ On Windows, confirm `processName` includes the `.exe` suffix (e.g., `"Scrapbook.Api.exe"`, NOT `"Scrapbook.Api"`)
3. ✅ If process check fails, fix `launch.json` before marking the config ✅
> If the process is not found, the F5 attach WILL fail with: `"No process with the specified name is currently running"`
@@ -1,190 +0,0 @@
<!-- 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.
# Node.js — Debug & Build Configuration
> Covers both **JavaScript** and **TypeScript** projects. The debugger properties are identical; only the build chain differs.
## Prerequisites
| Tool | Detection Command | Required For | Install Link |
|------|-------------------|-------------|-------------|
| Node.js | `node --version` | All Node projects | [nodejs.org](https://nodejs.org/) |
| npm | `npm --version` | Dependency management | (bundled with Node) |
---
## Debugger Properties
| Property | Value | Notes |
|----------|-------|-------|
| Debug protocol | `Node Inspector` | V8 inspector protocol over WebSocket |
| VS Code debugger type | `node` | Maps Node Inspector to VS Code's built-in Node debugger |
| Base debug port | `9229` | Default Node.js inspector port |
| Auto-restart | `true` | Re-attach after the host restarts on file changes |
### `outFiles` (node-ts only — REQUIRED)
For any TypeScript Node.js project, the `launch.json` attach configuration **must** include `outFiles` pointing to the compiled JavaScript output. Without it, VS Code cannot locate `.js.map` files and breakpoints set in `.ts` source files will not trigger.
**Derivation:** Read the project's `tsconfig.json` to determine the output directory:
1. Check `compilerOptions.outDir` — this is the compiled output root (e.g., `"outDir": "./dist"`)
2. Construct the glob: `${workspaceFolder}/{service-root}/{outDir}/**/*.js`
3. If `outDir` is not set, fall back to the project root: `${workspaceFolder}/{service-root}/**/*.js`
> `{service-root}` is the path from the workspace root to the project directory. In a single-project workspace this is empty (just `${workspaceFolder}/dist/**/*.js`). In a monorepo it includes the nested path (e.g., `${workspaceFolder}/services/functions/dist/**/*.js`).
**Example:**
```json
{
"name": "My API (debug)",
"type": "node",
"request": "attach",
"port": 9229,
"restart": true,
"outFiles": ["${workspaceFolder}/services/functions/dist/**/*.js"],
"preLaunchTask": "{service-id}: func host start"
}
```
> `preLaunchTask` uses the canonical `{service-id}:`-prefixed task label — see [generate.md § Service ID Derivation](../generate.md). The example above corresponds to a `functions-api` service, so the resolved value is `functions-api: func host start`.
> **Verify:** `tsconfig.json` must have `"sourceMap": true` in `compilerOptions` for any TypeScript project that will be debugged. Without source maps, VS Code breakpoints in `.ts` files cannot bind to the compiled `.js` output and will appear as gray (unverified) dots — even when the debugger is successfully attached. The build/watch task must run **before** the startup task so compiled output exists when the debugger attaches.
### VS Code Problem Matchers
| Variant | Watch Problem Matcher | Build Problem Matcher |
|---------|----------------------|----------------------|
| node-ts | `$tsc-watch` | `$tsc` |
| node-js | — | — |
> **Monorepo / multi-service:** When multiple Node services are present, each is assigned a sequential debug port starting from the base port defined in the project type's Runtime Wiring table. See [multi-service.md](../multi-service.md) for port assignment rules.
---
## Variant Detection
| Signal | Variant | Notes |
|--------|---------|-------|
| `tsconfig.json` present | **node-ts** | TypeScript — requires compile step |
| `package.json` without `tsconfig.json` | **node-js** | Plain JavaScript — no compile step |
---
## Build Chain
Tasks owned by this runtime: install, clean, build, watch. The startup task and its dependency wiring are provided by the project type's Runtime Wiring table — not this file.
> **Task label scoping:** All task labels MUST be prefixed with the service ID (e.g., `functions-api: npm watch`). See [generate.md § Service ID Derivation](../generate.md).
### Build Commands
#### node-ts (TypeScript)
```
"{service-id}: {startup task}" ← from project-types/{type}.md Runtime Wiring
├── dependsOn: "{service-id}: npm watch"
│ └── dependsOn: "{service-id}: npm clean"
│ └── dependsOn: "{service-id}: npm install"
└── dependsOn: "Start Emulators" ← only when emulators are required
```
| Step | Task Label | Command | Purpose | Background? |
|------|-----------|---------|---------|------------|
| install | `{service-id}: npm install` | `npm install` | Installs dependencies | No |
| clean | `{service-id}: npm clean` | `npm run clean` | Cleans build output | No |
| watch | `{service-id}: npm watch` | `npm run watch` | Runs `tsc --watch` for incremental builds | ✅ Yes |
| build | `{service-id}: npm build` | `npm run build` | One-shot build (used outside debug flow) | No |
#### node-js (JavaScript)
```
"{service-id}: {startup task}" ← from project-types/{type}.md Runtime Wiring
├── dependsOn: "{service-id}: npm install"
└── dependsOn: "Start Emulators" ← only when emulators are required
```
| Step | Task Label | Command | Purpose | Background? |
|------|-----------|---------|---------|------------|
| install | `{service-id}: npm install` | `npm install` | Installs dependencies | No |
> No compile, clean, or watch step — JavaScript runs directly.
> **Monorepo / alternative package managers:** Adjust commands if the project uses `yarn`, `pnpm`, or a monorepo layout. The key invariant is the chain shape: **install → [clean → build/watch →] startup task** (compile steps only for TypeScript).
See [generate.md](../generate.md) § Task `runOptions` Rules for how these build steps are rendered into VS Code task configuration.
---
## Convenience Scripts
The plan's Convenience Scripts table specifies WHICH scripts to generate. This section covers HOW to register them for Node.js projects.
**Script runner:** `package.json` `"scripts"` block
**Run command pattern:** `npm run {script-name}`
### Script Format
Each script is a shell command added to `package.json` `"scripts"`. Example entry:
```json
{
"{script-name}": "{shell command}"
}
```
### Common Script Implementations
Use these implementations when building scripts from the plan:
| Script Purpose | Typical Command | Notes |
|---------------|-----------------|-------|
| Start emulators | `docker compose up -d` | Idempotent — safe to re-run |
| Stop emulators | `docker compose down` | Stops and removes containers |
| Clean emulator data | `docker compose down -v && rimraf {data-dirs}` | `-v` (`--volumes`) also removes **named volumes** (e.g. Postgres's `postgres_data`). `{data-dirs}` = space-separated `./.{name}` **bind-mount** directories derived from `docker-compose.yml` `volumes:` mounts (e.g., `.azurite`). Use `rimraf` for cross-platform compatibility. Requires `rimraf` in `devDependencies` — see [generate.md § Dependency Availability](../generate.md). |
| Run migrations | `{migration tool CLI command}` | See [migrations.md](../migrations.md) for how to determine the command |
---
## VS Code Extension Recommendations (`.vscode/extensions.json`)
No recommended extensions.
---
## VS Code Workspace Settings (`.vscode/settings.json`)
<!-- Settings contributed by this runtime. Aggregated with project-type settings into .vscode/settings.json by generate.md. -->
| Setting | Value | Why |
|---------|-------|-----|
| `files.exclude: **/node_modules` | `true` | Hide dependency tree from explorer — large and noisy |
> This exclusion reduces workspace noise. It is deep-merged with any existing `files.exclude` entries — see [generate.md § VS Code Workspace Settings](../generate.md).
---
## Checklist — Node.js Runtime Validation
> ⛔ **MANDATORY — runs during Phase 3 validation after all artifacts are generated.** You MUST verify every item below. Do NOT skip, assume, or approximate results.
After generating VS Code configuration, verify the following were produced correctly:
### Post-Generation Checks
1. ✅ `launch.json` uses `"type": "node"` with the correct debug port
2. ✅ For TypeScript: `launch.json` includes `"outFiles"` derived from `tsconfig.json` `outDir` (e.g., `["${workspaceFolder}/{service-root}/{outDir}/**/*.js"]`)
3. ✅ For TypeScript: `tsconfig.json` has `"sourceMap": true` — without it, breakpoints in `.ts` files appear as gray (unverified) dots
4. ✅ For TypeScript: watch task exists in `tasks.json` with `$tsc-watch` problem matcher
5. ✅ For TypeScript: build chain follows install → clean → watch dependency order
### Live Validation Checks
These checks run during Phase 3 validation ([validation.md](../validation.md) Step 7), after the ready signal is observed:
1. ✅ For TypeScript: verify `tsconfig.json` has `"sourceMap": true` in `compilerOptions`. If missing, add `"sourceMap": true` and re-run the build task before marking the config ✅
> The Node Inspector debug port (`9229`) is handled automatically by the Functions host or `--inspect` flag.
> Project-type-specific checks (e.g., `{service-id}: func host start` task, connection strings) are defined in `project-types/{type}.md`.
@@ -1,139 +0,0 @@
<!-- 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.
# Validation
Verify that the generated VS Code debug configuration actually works. This phase runs after all artifacts are generated (Phase 2) and before the closing message.
> ⛔ **MANDATORY.** You MUST execute every step in this file for each launch configuration. Do NOT skip, assume, or approximate results. Do NOT proceed to the closing message until every checklist entry has a real ✅ or ❌ result.
> **Compose command comes from the plan.** Every `docker compose …` invocation below is a stand-in for the plan's Orchestrator **Compose Command** — use `docker compose` by default, or `podman compose` when the plan selected Podman. The commands (`up -d`, `ps`, `logs`, `down`) are identical across both engines.
> ⛔ **Do NOT switch the container runtime to make validation pass.** If an emulator fails to start, a port doesn't reach the host, a volume/permission error occurs, or a health check never passes on the plan's selected engine, **STOP and surface it to the user** (per [preflight.md § Container Runtime Readiness Check](preflight.md)) — offer the fix first, and only switch engines if the user explicitly chooses to. Silently rewriting the Orchestrator/tasks to the other engine and re-validating is a failure even if the app then works.
---
## Validation Algorithm
Steps 1–8 apply to each **non-compound** launch configuration in `.vscode/launch.json`; Step 9 then validates each **compound** configuration.
For each **non-compound** launch configuration in `.vscode/launch.json`:
### Step 1: Resolve the Task Chain
- Read the config's `preLaunchTask` value
- Trace the full `dependsOn` chain in `.vscode/tasks.json` to resolve the dependency order
### Step 2: Verify Script Dependencies
- For each task in the resolved chain, verify that its command can actually execute:
- **Package scripts** (e.g., `npm run clean`, `dotnet build`) — Confirm a matching script entry or build target exists in the project
- **CLI tool invocations** (e.g., `rimraf`, `concurrently`) — Confirm the tool is installed as a project dependency
- If a dependency is missing, add it as a project dev dependency before proceeding (see [generate.md § Dependency Availability](generate.md))
### Step 3: Start Services
- Run prerequisite tasks first (install, clean, emulators), then start the `preLaunchTask` itself as a background process
### Step 4: Verify Emulators
- If a `docker-compose.yml` was generated, verify all services started correctly after `docker compose up -d` (or `podman compose up -d`):
- **Long-running services** (database emulators, Azurite) → should be running and healthy
- **One-shot services** (e.g., `db-migrate`) → should have exited with code 0
- Use `docker compose ps` and `docker compose logs <service>` (or the `podman compose` equivalents) to check
- If any service failed, diagnose the issue, fix the configuration, and re-run until all services are healthy or exited cleanly
- Only mark the config ❌ after exhausting reasonable fix attempts
### Step 5: Confirm Ready Signal
- Watch stdout from the **top-level task** for the ready signal. Look up the expected pattern from `project-types/{type}.md` § Validation Signals § Ready Signal.
### Step 6: Confirm HTTP Reachability
- After the ready signal, confirm with `curl` using the **application HTTP port** (not the debug port). Look up the expected URL and status from `project-types/{type}.md` § Validation Signals § HTTP Verification.
> Use the curl template: `curl -s -o /dev/null -w "%{http_code}" <target>`
> **HTTP verification not applicable:** If the project type's HTTP Verification table says "N/A" or no anonymous/public endpoint is available (e.g., all routes require auth keys), skip HTTP verification. The config can still pass (✅) based on the ready signal alone — note "HTTP verification skipped: {reason}" in the checklist entry.
### Step 7: Per-Debugger-Type Checks
- Run additional checks based on the `type` field in the launch configuration. See the [Per-Runtime Validation Checks](#per-runtime-validation-checks) section below. If no additional checks are listed for the debugger type, skip this step.
### Step 8: Cleanup
- Tear down **every** process this config started — not just the last one. Kill the top-level `preLaunchTask` process **and every task in its resolved `dependsOn` chain** (dev servers, `func host`, watchers, and any emulators started for this config), then confirm the app HTTP port and the debug port are released (e.g. `lsof -i :<port>` returns nothing) before moving to the next config. A lingering process here causes the next config — or the compound — to hit "port already in use".
### Step 9: Validate the Compound Configuration
For each **compound** launch configuration, faithfully run its orchestration — do NOT infer the result from the individual configs.
> ⛔ **Do NOT skip running the compound.** Configs that each pass standalone can still fail when launched together: overlapping or duplicated `dependsOn` chains can start a service more than once, or a second invocation can grab an already-bound port. The user must never see this. You MUST run the compound's orchestration and observe the real result.
- **Confirm the startup graph is deduplicated.** Before running, verify the compound satisfies every rule in [multi-service.md § Deduplicated Startup Graph](multi-service.md) — the compound's effective task graph must start each service exactly once. If any rule is violated, fix the generated config before running; do not validate a graph that can duplicate a service.
- **Run the compound orchestration.** Execute the compound's `preLaunchTask` (the sequenced compound task chain) exactly as VS Code would — run its `dependsOn` members in `dependsOrder: "sequence"`. This is the same terminal-driven orchestration used for the individual configs.
- **Assert each service starts exactly once.** Watch the task output: each service's top-level task must produce exactly one running instance. When an individual config's `preLaunchTask` fires again for an already-running service, that duplicate invocation MUST be a silent no-op (via `instancePolicy: "silent"`) — never a second process. If any service starts a second instance or grabs an already-bound port, that is considered a fail and should be fixed before proceeding.
- **Assert readiness and HTTP reachability per service.** For each member service, confirm its ready signal (as in Step 5) and HTTP reachability (as in Step 6), reusing the same `project-types/{type}.md § Validation Signals` lookups the per-config algorithm uses. Every service must reach its ready signal; every service with an HTTP endpoint must return the expected status via `curl`.
- **Record the result.** Mark the compound ✅ only after **each service started exactly once AND reached its ready signal AND (where applicable) passed HTTP reachability**. Otherwise mark it ❌ with the duplicate/failure evidence.
> **Known limitation:** As with individual configs, the agent validates task orchestration and readiness through the terminal — it does not attach VS Code debuggers to the compound's member configs. Keep the assertion focused on **started exactly once + ready + reachable**.
- **Tear down the compound.** Stop every process the compound started (all member configs and their full task chains) and confirm their ports are released, exactly as in Step 8. This feeds into the final teardown sweep below.
---
## Validation Signal Lookup
Ready signals and HTTP verification targets are defined in each project-type reference file under `§ Validation Signals`. Load the project-type file for the service being validated and read its signal tables.
| Information | Where to find it |
|-------------|-----------------|
| Ready signal (stdout pattern) | `project-types/{type}.md` § Validation Signals § Ready Signal |
| HTTP verification (curl target, expected status) | `project-types/{type}.md` § Validation Signals § HTTP Verification |
| Debugger-specific checks (processName, etc.) | `runtimes/{rt}.md` § Checklist — Live Validation Checks |
| Runtime-specific details (debug port, outFiles) | `runtimes/{rt}.md` § Debugger Properties |
> **Path resolution:** Some project types use subdirectories — see [generate.md § Project Type Path Resolution](generate.md) for the lookup table.
---
## Per-Runtime Validation Checks
Additional runtime-specific checks beyond the generic algorithm. These run after the ready signal and HTTP verification.
> ⛔ You **MUST** load and execute the runtime's live validation checks. Do NOT skip this step or assume the checks pass.
- Load `runtimes/{rt}.md` § Checklist and execute every item under **Live Validation Checks**. Each runtime's checklist contains debugger-specific verifications (e.g., source map verification for Node.js, process attachment verification for .NET).
---
## Final Teardown — Free All Ports Before Handing Back
After all individual configs **and** every compound have been validated, and **before** the Plan Integration / status write, sweep and stop everything validation spun up:
1. **Stop every lingering background process** you started during validation — every dev server, `func host`, watcher, and task/language process, across all configs and the compound. Nothing you launched may still be running.
2. **Stop every emulator you started.** If validation ran `docker compose up` (or `podman compose up`), run the matching `docker compose down` / `podman compose down` (or stop the specific services you started). Do not leave Azurite, database emulators, or any compose service running.
3. **Verify the ports are free again.** Confirm that every port the generated configs will use is released — application HTTP ports, debug ports, and emulator ports. Use `lsof -i :<port>` (and the plan's `docker compose ps` / `podman compose ps` for emulators); each must show nothing bound. If any port is still held, find and stop the owning process before finishing.
> A subsequent user **F5** must start from a completely clean slate. Do NOT proceed to the closing message or set status to `Implemented` while any validation-spawned process or emulator is still running, or while any of these ports is still bound.
---
## Plan Integration
After validating all configurations, **create or update** the `## Debug Configuration Checklist` section in `.azure/vscode-debug-plan.md`. If the section does not exist, add it at the end of the plan before closing.
```
## Debug Configuration Checklist
Debug Configuration Checklist:
✅ <config-name> — <ready signal + curl result>
✅ <config-name> — <ready signal + curl result>
✅ <compound-name> — each service started once + ready + curl result
```
One line per config (non-compound and compound). For a **non-compound** config, ✅ requires the ready signal observed AND curl confirmed (or curl skipped with a valid reason). For a **compound** config, ✅ requires the real compound test from Step 9 — each member service started **exactly once** AND reached its ready signal AND (where applicable) passed HTTP reachability — never an inferred pass from the individual results.
@@ -1,4 +0,0 @@
{
"name": "azure-debug-plan",
"version": "0.0.1"
}
@@ -1,68 +0,0 @@
<!-- 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.
# Azure Debug Plan
> **AUTHORITATIVE GUIDANCE — MANDATORY COMPLIANCE**
>
> This document is the **official, canonical source** for classifying a workspace
> and generating a VS Code debug setup plan. You **MUST** follow these instructions
> exactly as written. When in doubt, defer to this document. Do not improvise,
> infer, or substitute steps.
---
## Global Rules (NO EXCEPTIONS)
1. **Update plan progressively** — Mark steps complete as you go; update **Last Updated** timestamp on every status change
2. ❌ **Destructive actions require `ask_user`** — Always confirm before overwriting, deleting, or modifying existing files
3. **Preserve existing config** — Never silently overwrite project configuration files or `docker-compose.yml`. Merge or ask first.
4. **Scope — VS Code debug setup only** — This instruction set classifies the workspace and generates a plan. Cloud architecture, IaC generation, provisioning, and deployment are handled by the **azure-deploy** agent through the complete **azure-app-onboard** pipeline.
---
## Autopilot mode (overrides the approval STOP)
**Active when** the invoking chat query begins with `[AUTOPILOT MODE]`, **or** `.azure/project-plan.md` records `executionMode: auto` or equivalent. When active, run fully unattended:
- **Phase 0–1 still run in full**. When generating the plan `.azure/vscode-debug-plan.md`, also record `executionMode: auto` or similar. Follow any specific instructions per the template.
- Skip the step that instructs to run `openLocalPlanView` and do NOT wait for approval. Set status straight to `Approved`, then invoke `azure-debug-generate` as you normally would, but with the chat arg prefixed with `[AUTOPILOT MODE] `.
- **Reuse the project-plan prerequisites.** When `.azure/project-plan.md` already lists Prerequisites, carry them over wholesale instead of re-deriving them — see [inventory.md § Step 1](references/inventory.md) for the detail.
- Never call `ask_user` for non-destructive steps. Scan completeness is unchanged — autopilot suppresses **the preview and approval gates only**.
## Workflow
> **Two phases — Classify → Plan — then STOP.**
>
> Do NOT generate any configuration files (docker-compose, launch.json, tasks.json, etc.).
> All `.azure/` artifacts must be created inside the **workspace root**, not a session-state folder.
---
## Phase 0: Classify
Scan the workspace for service roots. Produce a `services[]` list.
| Action | Reference |
|--------|-----------|
| Check for `.azure/project-plan.md` — if found, read it for advisory context. | — |
| Scan all subdirectories; detect project type + runtime per service root | [classify.md](references/classify.md) |
| If 2+ service roots: assign service IDs, deduplicate emulators, plan compound debug config | [multi-service.md](references/multi-service.md) |
---
## Phase 1: Plan
Scan dependencies, detect configuration, and generate the plan directly. The user reviews and edits the plan markdown before approving.
| # | Action | Reference |
|---|--------|-----------|
| 1 | **Detect prerequisites** — Check required tools and VS Code extensions | [inventory.md](references/inventory.md) § Step 1 |
| 2 | **Scan Azure dependencies** — For each service, scan bindings (Functions) or SDK packages (other types) to identify Azure service dependencies | [inventory.md](references/inventory.md) § Step 2 |
| 3 | **Map dependencies to emulators** — Map each detected Azure dependency to its local emulator. Deduplicate across services. | [inventory.md](references/inventory.md) § Step 2 |
| 4 | **Detect container runtime & Compose provider** — Scan for an existing `docker-compose.yml`/`compose.yaml` and detect the available container runtime per [prerequisites.md § Container runtime detection](../../shared-references/prerequisites.md). Record the chosen runtime (**Docker**, **Podman**, or **Podman (Docker-compatible)**) and its Compose command in the plan's Orchestrator table. **Prefer the Podman engine when it's ready** — native `podman compose`, or `docker compose` when the Docker CLI is backed by Podman; fall back to Docker, and default to Docker Compose only when neither is confirmed. | [prerequisites.md](../../shared-references/prerequisites.md) |
| 5 | **Detect migrations** — Scan for migration files, dependencies, and scripts | [migrations.md](references/migrations.md) |
| 6 | **Inventory API test opportunities** — List HTTP endpoints and triggers per service | [inventory.md](references/inventory.md) § Step 3 |
| 7 | **Write plan** — Generate `.azure/vscode-debug-plan.md` from scan results. Fill all sections completely. | [plan-template.md](references/plan-template.md) |
| 8 | **Present plan** — Show plan to user and ask for approval. Highlight any ❓ prerequisites to double-check. The user can edit the plan directly before approving. On approval, update status to `Approved`. | `.azure/vscode-debug-plan.md` |
---
> **❌ STOP HERE** — Do NOT proceed to artifact generation. Do NOT generate docker-compose.yml, launch.json, tasks.json, or any other configuration files. Present the plan to the user and wait for approval. After approval, the custom agent wrapper will invoke `azure-debug-generate` to handle generation.
@@ -1,33 +0,0 @@
<!-- 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.
# Classify Workspace
Determine the project type(s) and runtime(s) for each service root in the workspace. Classification produces an array of service contexts — even single-service workspaces produce a one-item list so the rest of the flow is uniform.
> **Always scan the full directory tree** — not just the workspace root. Service roots nested in subdirectories (e.g. `./api/`, `./web/`) must be found regardless of project layout.
> Ignore: `node_modules/`, `.git/`, `dist/`, `build/`, `bin/`, `obj/`.
### ⚠️ Exclude Non-Service Directories
Only include directories that represent **runnable services** (APIs, web apps, functions, workers, etc.). Exclude directories that are **shared libraries, utility packages, or common modules** — these are consumed by services but are not independently launchable and should never appear as a service.
Common exclusion signals:
- Directory name or `package.json` name contains `shared`, `common`, `utils`, `lib`, or `helpers`
- No entry point (no `main`, `start` script, `host.json`, `server.*`, or framework config)
- Used as a dependency by other service roots (e.g. via workspace references or `file:` dependencies)
- Project type is `library` — a package that exports modules but is not runnable on its own
---
## Step 1: Detect Project Types
Scan every subdirectory and classify each service root by project type. See [project-types.md](project-types.md) for the detection table and per-type nuances.
## Step 2: Detect Runtimes
For each service root, determine the language/runtime and version. See [runtimes.md](runtimes.md) for the detection table and per-runtime nuances.
## Output
Classification produces a `services[]` list of `{ root, projectType, runtime, ... }` entries. Even single-service workspaces produce a one-item list. This information will be used in follow-up sections.
@@ -1,53 +0,0 @@
<!-- 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.
# Inventory
Scan the workspace to populate the plan. For multi-service workspaces, loop over each service in `services[]` from classify.md; deduplicate emulators across services per [multi-service.md](multi-service.md).
---
## Step 1: Prerequisites
### Autopilot: Reuse the Project Plan Prerequisites
**Autopilot mode only.** In autopilot, if `.azure/project-plan.md` exists and lists Prerequisites (its `### Run` and `### Debug` groups), carry those prerequisites over wholesale into the debug plan instead of re-deriving them — the planning stage already inventoried them, and autopilot has no approval gate to catch a dropped prerequisite. Add any prerequisites the workspace scan reveals that project-plan is missing, and don't silently override a project-plan entry. Only when project-plan has no Prerequisites do you fall back to deriving them from the workspace scan below.
In interactive (non-autopilot) mode, derive prerequisites from the workspace scan as usual; the user reviews and edits the plan before approving.
### Deriving Prerequisites from the Workspace Scan
Identify required tools and then inventory them by following [prerequisites.md](../../shared-references/prerequisites.md).
The required tools are derived from a scan of the currently opened workspace project — check only the tools and extensions relevant to the detected project types, runtimes, and Azure bindings. Both tool sets defined in prerequisites.md apply here — the **Run** tools (Node.js, .NET SDK, Python, Functions Core Tools, ...) and the **Debug** tools (Docker, Docker Compose, VS Code extensions, ...) — since debugging exercises the full local stack.
For every detected project type, include its VS Code debug-integration extension from the Debug Tools table as its own Debug row (e.g. an Azure Functions project always includes `ms-azuretools.vscode-azurefunctions`) — these extensions are required for the debug experience and are separate from the Run-group CLI/runtime tools, so never omit them.
---
## Step 2: Azure Dependencies
For each service, identify Azure service dependencies by scanning bindings or SDK packages.
- **Functions projects:** Scan bindings per [project-types.md](project-types.md) § functions
- **Other project types:** Scan dependency files (e.g. `package.json`, `requirements.txt`, `*.csproj`) for packages that indicate an Azure service dependency
The table below shows common SDK-to-service mappings — this is **not exhaustive**. Any package that implies connectivity to an Azure service should be mapped accordingly.
| Example Packages | Azure Service | Emulator |
|-----------------|--------------|----------|
| `@azure/storage-blob`, `@azure/storage-queue`, `@azure/data-tables` | Azure Storage | azurite |
| `pg`, `postgres`, `@prisma/client`, `typeorm`, `sequelize`, `Npgsql`, `psycopg2` | PostgreSQL | postgresql |
| `@azure/cosmos` | Cosmos DB | cosmosdb-emulator |
| `@azure/service-bus` | Service Bus | servicebus-emulator |
| `@azure/event-hubs` | Event Hubs | eventhubs-emulator |
| `mssql`, `Microsoft.Data.SqlClient` | Azure SQL | azure-sql-edge |
> Multiple storage bindings (blob + queue + table) consolidate to a **single** azurite entry.
> Cross-check `local.settings.json`, `.env`, and app config for existing connection references to confirm findings.
---
## Step 3: API Test Collection Inventory
For each service, identify whether it exposes testable HTTP endpoints or triggers and provide a brief summary for the plan. Inventory the implemented route and trigger registrations in the workspace, not only the route table in `.azure/project-plan.md`. The project plan intentionally omits derived authentication routes, so include implemented registration, login, and current-user endpoints when `API Login` is enabled. Detailed endpoint parsing happens during the generation phase.
@@ -1,47 +0,0 @@
<!-- 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 Migrations — Detection
When a database dependency is found, detect the migration tool so the plan can record it. The generation phase uses this to configure migration automation within the orchestrator.
---
## Detection
Scan three layers, then synthesize.
### Layer 1: Migration Files
Non-exhaustive detection pattern examples:
| Pattern | Tool |
|---------|------|
| `prisma/migrations/` | Prisma |
| `alembic/`, `alembic.ini` | Alembic |
| `**/migrations/*.py` | Django |
| `Migrations/*.cs` | EF Core |
| `flyway.conf` | Flyway |
| `migrations/*.sql` | Raw SQL |
### Layer 2: Dependencies
Check dependency manifests for migration tools, ORMs with built-in migration support, and database driver packages.
### Layer 3: Scripts
Check script runners (`package.json`, `Makefile`, etc.) for existing migration commands (grep for common migration key words like: `migrate`, `schema`, `seed`).
### Synthesis
1. Cross-reference all three layers — they should agree
2. If an existing migration command exists, use it (don't invent a new one)
3. If layers conflict, ask the user which tool is active
### Insufficient Evidence
If a database dependency exists but no migration strategy is found across all three layers:
1. Do not guess
2. Ask the user via `ask_user` how they manage schema changes
3. Record the gap in the plan's Migrations section with `⚠️ Not detected`
@@ -1,48 +0,0 @@
<!-- 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.
# Multi-Service Orchestration
> Runs when `classify.md` finds **2+ service roots**, before inventory scanning begins.
---
## Service ID Assignment
Derive a short kebab-case ID per service root. Use project manifest name if available, otherwise fall back to directory name.
| Runtime | Manifest Source | Field |
|---------|----------------|-------|
| `node-ts`, `node-js` | `package.json` | `"name"` |
| `dotnet` | `*.csproj` | `<AssemblyName>` or filename |
| *Other* | Service directory name | Fallback when no manifest name exists |
If two IDs collide, append the project type (e.g. `payments-api` becomes `payments-api-functions`).
---
## Emulator Deduplication
Collect emulator lists from all services. Each emulator appears **once** in the plan's Emulators table — multiple services may depend on the same emulator.
---
## Partial Configuration
Check each service root for existing debug config before planning.
| State | Plan Action |
|-------|------------|
| Fully configured | Skip |
| Partially configured | Generate only missing artifacts |
| Unconfigured | Full generation |
---
## Compound Debug Configuration
Required when 2+ service roots are detected (including Frontend SPAs).
### Startup Dependencies
If a frontend SPA has a proxy pointing to a local backend (detected via [project-types.md](project-types.md) § Backend Proxy Dependencies), record the `proxyTarget` service ID on that service entry. The compound config uses this to order startup (backends before frontends) that depend on them.
@@ -1,222 +0,0 @@
<!-- 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.
# Plan Template
> Generate `.azure/vscode-debug-plan.md` using this template. This file is the
> **single source of truth** for the generation phase. The generation phase reads this plan and
> generates all artifacts from it — no re-scanning of the workspace is needed.
>
> The plan is generated directly from the workspace scan. The user reviews the plan,
> edits it as needed, then approves it before generation proceeds.
## ⛔ BLOCKING REQUIREMENT
You **MUST** create this plan file and get user approval BEFORE generating any configuration files.
---
## Markdown Table Integrity
When editing markdown tables with `replace_string_in_file` or `multi_replace_string_in_file`, always **read the file back** after the edit and verify that each table row is on its own line. Markdown tables require exactly one row per line — a missing newline between `|---|` and `| data |` breaks parsing completely.
**Post-edit verification rule:** After any edit to `.azure/vscode-debug-plan.md` that modifies a table, immediately read the affected lines back to confirm the table renders correctly (header, separator, and each data row on separate lines). If rows are concatenated, fix before proceeding.
---
## ⚠️ Mermaid Label Quoting — ALWAYS Quote Every Label
In the `## Architecture Diagram` mermaid block, **every node label and every edge label MUST be wrapped in double quotes.** This rule is unconditional — quote every label, every time, even when it looks harmless.
```mermaid
graph LR
%% ✅ CORRECT — every label quoted
API["Commerce API<br/>Azure Functions :7071"] -->|"@azure/storage-blob"| AZ["Azurite<br/>:10000"]
API -->|"pg"| PG[("PostgreSQL<br/>:5432")]
%% ❌ WRONG — unquoted labels; the leading @ is a parse error
%% API[Commerce API<br/>Azure Functions :7071] -->|@azure/storage-blob| AZ[Azurite]
```
**Why:** mermaid v11 uses `@` for edge-ID (`e1@-->`) and node-metadata (`id@{ shape: ... }`) syntax, so an `@` at the **start** of an unquoted label is lexed as a link ID and the whole diagram fails to parse with `Expecting 'AMP', 'COLON', ... got 'LINK_ID'`. Scoped npm package names (`@azure/storage-blob`, `@azure/identity`, `@prisma/client`) are the most common trigger, and they are exactly what shows up in Azure dependency edge labels. The debug-plan webview renders this block with mermaid — a parse error replaces the diagram with a raw `mermaid (error)` code block.
Quoting is always valid in mermaid and also protects labels containing `(`, `)`, `:`, `-`, `/`, `#`, or `·`. Do **not** try to detect which labels are "risky" — just quote all of them.
---
## Template
````markdown
# Azure Debug Plan
> This plan is the source of truth for generating the
> VS Code debug setup in this workspace.
>
> **Status:** {Planning | Approved | Executing | Implemented}
> **Execution Mode:** {Auto | Guided}
> **Created:** {ISO-8601 datetime}
> **Last Updated:** {ISO-8601 datetime}
>
> <!-- Guided Mode (default) - hand-holds the user through review and approval before generating. -->
> <!-- Auto Mode (aka YOLO mode) — skips approval gates and runs generation unattended. -->
---
## Prerequisites
<!-- All required tools and VS Code extensions with install status — list both the Run and Debug tool sets defined in prerequisites.md. -->
<!-- Service(s): the service(s) that need the tool (e.g. `api`, `worker`); use `*` for global toolchain shared by all services. -->
<!-- Installed/Version come from the detection pass and must be re-run whenever the whole plan is (re)generated or the tool set changes — never leave a placeholder. Installed is only ✅ (found) or ❓ (couldn't confirm). ❓ is informational — it just prompts the user to double-check the tool is installed. -->
<!-- Do NOT add an Install column or any install links/URLs. The plan webview appends an Install link deterministically from a built-in catalog keyed on the tool name; links authored here are ignored. -->
| Tool / Extension | Category | Service(s) | Installed | Version |
|------------------|----------|------------|-----------|---------|
| {name} | {Runtime / Package manager / …} | {service(s) or *} | {✅/❓} | {version or —} |
> ⚠️ **Action required:** Confirm any tool or extension marked ❓ is installed and ready before approving this plan — rerun the recheck to confirm CLI tools provided by a version manager.
---
## Debug Configurations
<!-- One row per detected service root. Each row maps to a VS Code debug configuration in launch.json. -->
<!-- The generation phase loads project-types.md and runtimes.md based on these values. -->
<!-- Services with Generate = No are excluded from all generation but shown for reference. -->
<!-- Azure Dependencies drive emulator matching — see Emulators section. -->
<!-- Debug Config Name is the VS Code debug configuration display name shown in the Run & Debug dropdown. -->
<!-- For multi-service workspaces, include a compound debug config row at the end. -->
<!-- ✏️ User can edit: Generate (check/uncheck), Debug Config Name -->
Each checked row below produces a VS Code debug configuration in the `.vscode/launch.json`.
| Generate | Debug Config Name | Service Label | Service Root | Project Type | Runtime | Version | Azure Dependencies |
|----------|--------------------|---------------|--------------|--------------|---------|---------|-----|
| {[x] / [ ]} | {e.g. Payments API (debug)} | {label} | {path} | {type} | {runtime} | {version} | {comma-separated azure service labels} |
<!-- Example: -->
<!-- | [x] | Payments API (debug) | Payments API | ./api | functions | node-ts | 20.x | Azure Storage, Azure PostgreSQL | -->
<!-- | [x] | Customer Portal (debug) | Customer Portal | ./web | frontend-spa | node-ts | 20.x | — | -->
<!-- | [x] | Debug All Services | Debug All Services | | *Compound Config* |||| -->
<!-- Project Type descriptions are shown in a collapsible block so the table stays compact. -->
<!-- Only include project types that appear in the table above. -->
<details>
<summary>ℹ️ Project Type Descriptions</summary>
| Project Type | Description |
|-------------|-------------|
| {type} | {brief description of what this project type means} |
<!-- Example: -->
<!-- | functions | Azure Functions — serverless compute with triggers and bindings | -->
<!-- | frontend-spa | Single-page application served by a dev server (Vite, Next.js, Angular, etc.) | -->
<!-- | app-service | HTTP server application (Express, Fastify, Flask, ASP.NET, Spring Boot, etc.) | -->
<!-- | container-app | Containerized application running via Dockerfile | -->
</details>
<!-- If a frontend SPA has a proxy config pointing to a local backend, add a note: -->
<!-- > ℹ️ **Proxy detected:** Customer Portal proxies requests to Payments API (via `vite.config.ts`). The compound config should start backends before frontends. -->
---
## Orchestrator
<!-- Records the container runtime and Compose provider used to run the emulator containers. -->
<!-- Container Runtime is Docker, Podman, or "Podman (Docker-compatible)"; Compose Command is the CLI the generation phase writes into every emulator task (`docker compose` or `podman compose`). -->
<!-- Detected from the workspace (e.g. an existing docker-compose.yml) and the container-runtime detection pass in prerequisites.md. -->
<!-- Prefer the Podman engine when it's detected as ready (native Podman, else Podman via Docker's socket), even if Docker is too; otherwise Docker. If no runtime is detected, default to Docker Compose (Docker). The generated compose file is identical for every case. -->
<!-- Docker-compatibility mode: when `docker`/`docker compose` is backed by the Podman engine, record Container Runtime = "Podman (Docker-compatible)" but keep Compose Command = `docker compose` — that's the command that talks to the Docker-compatible socket. -->
<!-- ✏️ User can edit: Container Runtime / Compose Command (to switch engines, e.g. Docker → Podman) -->
| Orchestrator | Container Runtime | Compose Command | Description |
|-------------|-------------------|-----------------|-------------|
| {Docker Compose / Podman Compose} | {Docker / Podman / Podman (Docker-compatible)} | {`docker compose` / `podman compose`} | {description} |
<!-- Example (native Podman, preferred when ready): -->
<!-- | Podman Compose | Podman | `podman compose` | Uses Podman (rootless) with `podman compose` to orchestrate emulators; the compose file is unchanged from Docker | -->
<!-- Example (Podman via Docker's socket — engine is Podman, command stays docker compose): -->
<!-- | Docker Compose | Podman (Docker-compatible) | `docker compose` | Runs the Podman engine through its Docker-compatible socket; `docker compose` drives Podman, compose file unchanged | -->
<!-- Example (Docker, the fallback): -->
<!-- | Docker Compose | Docker | `docker compose` | Uses Docker Compose to orchestrate emulators and dependent services during local development | -->
---
## Emulators
<!-- One row per deduplicated emulator. Dependent Service is the Azure service this emulator replaces. -->
| Dependent Service | Emulator | Purpose |
|-------------------|----------|---------|
| {azure service label} | {emulator name} | {description of what this emulator provides} |
<!-- Example: -->
<!-- | Azure Storage | Azurite Container | Blob and queue storage for photo uploads and background processing | -->
<!-- | PostgreSQL | PostgreSQL Container | Relational database for user accounts, couples, and photo metadata | -->
---
## Architecture Diagram
{One sentence describing how the app connects to its dependencies during debugging.}
```mermaid
graph LR
%% Generated from Debug Configurations + Emulators tables above.
%% Show each service as a node, each emulator as a node,
%% and edges for the Azure Dependencies that connect them.
%% MANDATORY: wrap EVERY node label and EVERY edge label in double quotes —
%% e.g. API["Commerce API<br/>Azure Functions :7071"] -->|"@azure/storage-blob"| AZ["Azurite"]
%% An unquoted leading @ (scoped npm packages) is parsed as mermaid edge-ID
%% syntax and breaks the whole diagram. See "Mermaid Label Quoting" above.
```
---
## Migrations
<!-- Only include this section when database migrations are detected for one or more services. Omit entirely if not applicable. -->
<!-- ✏️ User can edit: Generate (check/uncheck) -->
When selected, the generation phase creates automated VS Code tasks that run migration scripts on launch — so emulator databases are automatically provisioned with the correct schema and seed data before the app starts debugging. No manual migration steps needed.
| Generate | Service | Migration Tool |
|----------|---------|---------------|
| {[x] / [ ]} | {service label} | {tool name, e.g. Prisma / Knex / Drizzle / EF Core} |
---
## API Test Collections
<!-- Only include this section when one or more services expose testable HTTP endpoints or triggers. Omit entirely if not applicable. -->
<!-- ✏️ User can edit: Generate (check/uncheck) -->
When selected, the generation phase produces lightweight, runnable API test scripts in the project so you can quickly smoke-test endpoints and triggers once everything is launched and connected locally.
| Generate | Service | Description |
|----------|---------|-------------|
| {[x] / [ ]} | {service label} | {collapsible lists of HTTP endpoints and/or triggers — see format below} |
<!-- Description cell format: use collapsible <details> blocks (collapsed by default) to keep the table compact. -->
<!-- Include an HTTP Endpoints section and/or a Triggers section as applicable. Each route/trigger on its own line. -->
<!-- Example: -->
<!-- | [x] | Functions API | <details><summary>HTTP Endpoints (16)</summary><br>GET /api/health<br>POST /api/auth/register<br>POST /api/auth/login<br>POST /api/auth/logout<br>GET /api/auth/me<br>GET /api/couples<br>POST /api/couples<br>GET /api/photos<br>POST /api/photos<br>DELETE /api/photos/:id<br>GET /api/albums<br>POST /api/albums<br>PUT /api/albums/:id<br>DELETE /api/albums/:id<br>GET /api/tags<br>POST /api/tags<br><br></details><details><summary>Triggers (2)</summary><br>blobTrigger — uploads<br>timerTrigger — cleanup</details> | -->
---
## Convenience Scripts
<!-- The generation phase generates only checked scripts into the project's script runner. -->
<!-- ✏️ User can edit: Generate (check/uncheck) -->
| Generate | Script | Registered In | Description |
|----------|--------|---------------|-------------|
| {[x] / [ ]} | {script name} | {path to file where script is registered, e.g. ./package.json} | {what the script does} |
<!-- Example: -->
<!-- | [x] | emulators:start | ./package.json | Start all emulators in the background, preserving existing data | -->
<!-- | [x] | emulators:stop | ./package.json | Stop all running emulators | -->
<!-- | [x] | emulators:clean | ./package.json | Stop emulators and delete all data (fresh start) | -->
<!-- | [x] | db:migrate | ./package.json | Apply pending database migrations to the emulator database | -->
@@ -1,71 +0,0 @@
<!-- 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.
# Project Types
> These are **common examples, not an exhaustive list**. If a service root does not match
> any of these, classify it by whatever best describes its purpose (e.g. `background-worker`, `console`, etc.).
## Detection Table
| Detection Signals | Project Type | Include in Plan? |
|-------------------|-------------|------------------|
| `host.json` + Azure Functions SDK | **functions** | ✅ Yes |
| SPA framework in `package.json`, or `vite.config.*` / `next.config.*` / `angular.json` (no `host.json`) | **frontend-spa** | ✅ Yes |
| HTTP framework (Express, Fastify, Flask, FastAPI, ASP.NET, Spring Boot, etc.) | **app-service** | ✅ Yes |
| `Dockerfile` with a containerized application | **container-app** | ✅ Yes |
| No entry point, no framework, exports modules only (shared/common/util packages) | **library** | ❌ Exclude |
---
## functions
For Azure Functions projects, scan bindings to identify Azure service dependencies. Parse `function.json` files or decorator/attribute bindings in source code.
> **Implicit dependency:** All Azure Functions projects require Azure Storage for the host runtime (trigger management, lease coordination, internal state). Always emit an Azurite emulator entry in the plan regardless of whether application-level storage SDK packages are detected.
| Binding | Azure Service | Emulator |
|---------|--------------|----------|
| `blobTrigger`, `blob` | Blob Storage | azurite |
| `queueTrigger`, `queue` | Queue Storage | azurite |
| `table` | Table Storage | azurite |
| `httpTrigger` | (built-in) | — |
| `timerTrigger` | (built-in) | — |
| `warmupTrigger` | (built-in) | — |
| `durableClient`, `orchestrationTrigger`, `activityTrigger` | Durable Functions | durable-task-scheduler |
| `cosmosDBTrigger`, `cosmosDB` | Cosmos DB | cosmosdb-emulator |
| `serviceBusTrigger`, `serviceBus` | Service Bus | servicebus-emulator |
| `eventHubTrigger`, `eventHub` | Event Hubs | eventhubs-emulator |
| `eventGridTrigger`, `eventGrid` | Event Grid | — |
| `signalRTrigger`, `signalR` | SignalR Service | — |
| `sql`, `sqlTrigger` | Azure SQL | azure-sql-edge |
> Multiple storage bindings (blob + queue + table) consolidate to a **single** azurite entry.
> This table is **not exhaustive** — map any other bindings to their Azure service accordingly.
---
## frontend-spa
Frontend SPA projects do not require emulators or Azure bindings, but they **are** service roots. When a frontend is detected alongside a backend, the workspace is multi-service and **must** produce a compound debug configuration.
### Framework Detection
| Framework | Detection Signals |
|-----------|-------------------|
| Vite | `vite.config.*` or `vite` in devDependencies |
| Next.js | `next.config.*` or `next` in dependencies |
| Angular | `angular.json` |
| Create React App | `react-scripts` in dependencies |
| Blazor WASM | `*.razor` + `WebAssembly` SDK in `*.csproj` |
### Backend Proxy Dependencies
If a proxy config points to a local backend, record the dependency so the compound debug configuration can order startup (backends before frontends).
| Framework | Proxy Config Location |
|-----------|----------------------|
| Vite | `server.proxy` in `vite.config.*` |
| Create React App | `"proxy"` in `package.json` |
| Angular | `proxy.conf.json` |
| Next.js | `rewrites()` in `next.config.*` |
@@ -1,33 +0,0 @@
<!-- 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.
# Runtimes
> These are **common examples, not an exhaustive list**. If a service root uses a runtime
> not listed here, identify it by the language and toolchain present (e.g. `rust`, `ruby`, etc.).
## Detection Table
| Detection Signals | Runtime | Version Source |
|-------------------|---------|---------------|
| `package.json` + `tsconfig.json` | **node-ts** | `engines.node` / `.nvmrc` / `.node-version` |
| `package.json` (no `tsconfig.json`) | **node-js** | Same |
| `*.csproj` | **dotnet** | `<TargetFramework>` element (e.g. `net8.0` → `8.0`) |
| `requirements.txt`, `pyproject.toml`, or `Pipfile` | **python** | `.python-version` / `pyproject.toml` `requires-python` |
| `pom.xml` or `build.gradle` | **java** | `<java.version>` / `sourceCompatibility` |
| `go.mod` | **go** | `go` directive in `go.mod` |
---
## dotnet
### Version Detection
Read `<TargetFramework>` from the `.csproj` to determine the runtime version (e.g. `net8.0` → `8.0`).
### Assembly Name
Derive the assembly name for the plan's Service Label:
1. If `<AssemblyName>` is set → use that value
2. Otherwise → `.csproj` filename without extension (e.g. `Functions.csproj` → `Functions`)
@@ -1,4 +0,0 @@
{
"name": "azure-deploy",
"version": "0.0.1"
}
@@ -1,131 +0,0 @@
<!-- 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.
# Migration database access — tier ladder
How to reach the provisioned database to run migrations. **Work down the tiers in order.**
Record the tier you used in `deploy-result.json`.
The generated IaC already grants the *deployed app* database access via
`AllowAllAzureServicesAndResourcesWithinAzureIps` (`0.0.0.0`, consented at the Scaffold Gate).
That rule allows **Azure services**, not arbitrary public IPs — so compute already running inside
Azure can reach the database and a developer laptop cannot. This is why tiers 1 and 2 need no
network change and tier 3 does.
## Tier 1 — Exec inside the deployed app (default)
The migration tool, its dependencies, and the connection string already exist in the running app.
Nothing to install, no secret to move, no network change.
**Container Apps**
```powershell
az containerapp exec -n {ca} -g {rg} --subscription {sub} --command "{migration_command}"
# If the migration tool lives outside WORKDIR:
az containerapp exec -n {ca} -g {rg} --subscription {sub} --command "cd /app/backend && alembic upgrade head"
```
**App Service (Linux only)**
```powershell
az webapp ssh -n {app} -g {rg} --subscription {sub}
```
Discover `{migration_command}` from the table in
[`deploy/references/database-post-deploy.md`](../deploy/references/database-post-deploy.md).
**Move to tier 2 when:**
- The image is a slim/multi-stage build that dropped the migration CLI (`alembic`, `prisma`,
`dotnet-ef`, `sequelize-cli` not on `PATH`).
- Container Apps has scaled to zero and there is no replica to exec into.
- App Service on **Windows** (`az webapp ssh` is Linux-only).
- `exec` cannot return a usable exit code or output for verification.
> ⛔ `az containerapp exec` is TTY-oriented. Capture the output explicitly and verify the schema
> state afterward — a zero exit code from the shell is **not** proof the migration applied.
## Tier 2 — One-shot job in the same environment
Runs inside the same Container Apps environment (and therefore the same VNet), so it inherits the
app's database connectivity with **no firewall change**. This is the correct choice when the app
image lacks migration tooling or cannot be exec'd into.
Use a job image that contains the migration tool (commonly the app's *build* stage rather than its
runtime stage), the same connection configuration as the app, and a `Never` restart policy. Delete
the job once it completes, and record its creation in `deploy-result.json` so it is not mistaken
for an orphaned resource by `capture_deployment_inventory`.
**Move to tier 3 only when:** there is no Azure-side compute in the database's network at all (for
example a database provisioned without an accompanying app service), or the job cannot be created.
## Tier 3 — Temporary single-IP firewall rule (last resort)
⛔ **Preconditions — all must hold. If any fails, do not proceed; fall back to tier 2 or fail the
deploy.**
1. Tiers 1 and 2 are genuinely impossible. State why in `deploy-result.json`.
2. The server's `publicNetworkAccess` is already **Enabled**. If it is Disabled, or the server is
reachable only through a private endpoint, **stop** — do not enable public access.
3. The migration can run with the developer's own Entra credentials. Do **not** copy the admin
password to the local machine to satisfy this tier.
**Use the extension's tools.** They scope the rule to a single IP and guarantee its removal even
if this session dies — which prompt instructions cannot:
- `open_database_migration_access` → records a lease, then creates `cor-tempmigration-{sessionId}-{expiry}`
- `close_database_migration_access` → deletes that rule and clears the lease
The lease is written **before** the rule is created, and the extension reconciles outstanding
leases **on its next activation**. So if this session crashes, is compacted, or the window closes
mid-migration, the rule is removed when the workspace is reopened rather than surviving unnoticed.
> ⛔ Two things these tools do **not** do, so do not tell the user otherwise. They do **not**
> snapshot or compare the server's full firewall rule list — they only ever create and delete their
> own uniquely-named rule, and never read, modify, or remove any other rule. And cleanup on a crash
> happens at the **next activation**, not instantly. If `close_database_migration_access` reports
> that it could not remove the rule, the rule is still there until then: say exactly that, name the
> rule, and fail the deploy.
`open_database_migration_access` refuses rather than guessing when it cannot confirm the server's
network posture, or when ten exceptions are already outstanding in the workspace. A refusal is not
a failure to route around — fall back to tier 2, or fail the deploy.
Both are `copilot-azure-resources-extension-tools/*` tools. If they are not in your active tool
list, load them with `tool_search` → `activate_tools` as described in the agent's MCP tools
section — do **not** treat them as unavailable and fall back to `az`.
**Only if those tools genuinely cannot be loaded**, use `az` — and obey every rule below. Note that
this path has no crash safety at all: nothing records what you changed, so restoring it is entirely
your responsibility.
- **Record the baseline first**, before any change:
`az postgres flexible-server firewall-rule list`, `az mysql flexible-server firewall-rule list`,
or `az sql server firewall-rule list`. Write it into the session directory.
- **Add only a single-IP rule** (`startIpAddress` equal to `endIpAddress`) for the current client.
- **Never** widen to `0.0.0.0`–`255.255.255.255`, **never** disable enforcement, **never** delete
or modify a pre-existing rule, **never** touch VNet rules or `publicNetworkAccess`.
- **Restore on every path** — success, failure, timeout, or user abort — so the firewall matches
the recorded baseline exactly.
- **If restoration fails**, treat it as a deploy failure, name the exact rule that remains, and do
not report a clean deployment.
### Determining the client IP
Do **not** call a third-party IP echo service. Let Azure tell you: attempt the connection and read
the refusal, which contains the egress IP as Azure sees it — the only value the rule needs.
```text
Azure SQL: Client with IP address 'x.x.x.x' is not allowed to access the server.
PostgreSQL: no pg_hba.conf entry for host "x.x.x.x"
```
Confirm the detected IP with the user before creating the rule.
## Verification (all tiers)
A green HTTP status is not proof. Confirm the migration tool reports up to date **and** that the
expected tables exist, then re-run the health check so the app exercises the migrated schema.
If migrations cannot be completed, write the failure into `deploy-result.json` and treat the deploy
as failed rather than reporting success over an un-migrated database.
@@ -1,65 +0,0 @@
<!-- 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)** |
@@ -1,59 +0,0 @@
<!-- 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
@@ -1,36 +0,0 @@
<!-- 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`.
@@ -1,84 +0,0 @@
<!-- 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.
@@ -1,98 +0,0 @@
<!-- 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.
@@ -1,93 +0,0 @@
<!-- 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.
@@ -1,57 +0,0 @@
<!-- 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
@@ -1,136 +0,0 @@
<!-- 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)
@@ -1,111 +0,0 @@
<!-- 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.**
```
@@ -1,66 +0,0 @@
<!-- 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.
@@ -1,150 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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[];
}
@@ -1,62 +0,0 @@
<!-- 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).
@@ -1,98 +0,0 @@
<!-- 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. |
@@ -1,104 +0,0 @@
<!-- 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 |
@@ -1,54 +0,0 @@
<!-- 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}
```
@@ -1,129 +0,0 @@
<!-- 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)
```
@@ -1,81 +0,0 @@
<!-- 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.
@@ -1,80 +0,0 @@
---
name: azure-deploy
description: "End-to-end Azure deployment instructions for the azure-deploy agent: from app analysis to a running Azure deployment with cost estimates and pre-deploy approval. Auto-detects the right Azure services, scaffolds infrastructure code, and deploys — tailored to your app, not a template. Handles moving existing apps to Azure without rewriting or with minimal changes."
license: MIT
metadata:
author: Microsoft
version: "1.2.1"
---
<!-- 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.
# Azure App Onboard
> ⛔ **Every repo goes through the full pipeline (Steps 1–10). No exceptions.** Do not skip steps, refuse, or short-circuit based on what you recognize. Follow the Workflow table below sequentially — read each step's references before acting.
## Quick Reference
| Property | Value |
|----------|-------|
| Best for | Developers who know what to build but not which Azure services to use |
| Inputs | Business idea or existing codebase, budget/scale preferences (optional) |
| Outputs | Architecture plan, cost estimate, IaC files, deployed Azure resources |
| Phases | Discover → Architect → Scaffold → Deploy (self-contained, no external agent calls) |
## When to Use This Agent
- Deploy existing code without knowing which Azure services to use
- Check if your existing code is ready to deploy to Azure
- Move an existing app to Azure without rewriting or with minimal changes
- Get cost estimates before committing to infrastructure
- Understand architecture decisions and rejected alternatives
- Get answers to Azure architecture or service selection questions (e.g., "What database should I use?")
- Get guided Azure onboarding without prior experience
## When NOT to Use
| Scenario | Use Instead |
|----------|-------------|
| Run `azd up` or execute an existing deployment | `azure-deploy` |
| Optimize existing Azure spend | `azure-cost` |
| Generate Bicep/Terraform for a known architecture | `azure-prepare` |
| Validate infrastructure or run preflight checks | `azure-validate` |
| Troubleshoot a running Azure deployment | `azure-diagnostics` |
| Deploy to or manage AKS/Kubernetes directly | `azure-kubernetes` |
| Look up or list existing Azure resources | `azure-resource-lookup` |
## Pipeline Rules
> ⛔ **You MUST read [`references/pipeline-rules.md`](references/pipeline-rules.md) at the start of every AppOnboard session.** It contains approval gates, phase lifecycle, session artifacts, deploy-as-is, and security baseline rules.
## Workflow
> ⛔ **Deploy recovery:** After deploy gate approval OR before any `az deployment`/`az webapp deploy`/`az acr build` — if you haven't read `deploy/instructions.md`, read `.copilot-azure/sessions/{id}/deploy-checklist.md` first, then `deploy/instructions.md`. ⛔ Always use this pipeline's own deploy phase (`deploy/instructions.md`) — do NOT hand off to any other deployment workflow.
> ⛔ **Post-scaffold transition (MANDATORY):** Immediately after `scaffold-manifest.json` is written, YOUR NEXT ACTION MUST be Step 8 (Deploy Approval Gate) — NOT a summary report, NOT a "here are the generated files" message, NOT a completion signal. Confirm `context.json` has `completedPhases: [...,"scaffold"]` + `currentPhase: "deploy"` (update it yourself if the scaffold subagent didn't). Re-read [approval-gates.md § Deploy Gate](references/approval-gates.md) if evicted from context (scaffold reference loading is heavy), then present the exact prompt: **"🚀 Ready to deploy? (Yes / Run manually / Edit plan / Cancel)"**. This gate is the LAST content in your response — wait for the user's reply.
| # | Step | Action | Reference |
|---|------|--------|-----------|
| 1 | **Session check + Azure login** | Create/resume session, verify Azure CLI auth, resolve subscription + user identity | ⛔ **You MUST read [session-protocol.md](references/session-protocol.md)** |
| 2 | **Scope triage** | Check azd markers, triage question. Empty workspace or code-only (no infra) → Step 3 directly. | ⛔ Read [intent-gathering.md](references/intent-gathering.md) § Scope Triage |
| 3 | **Prereq scan** | ⛔ Skip if `completedPhases` includes `"prereq"`. Otherwise: evaluate repo readiness, write `prereq-output.json`, update `context.json`. **Halt if:** `overallHealth: "blocked"` OR `routeToSkill` set. | ⛔ **You MUST read and follow [prereq/instructions.md](prereq/instructions.md)** |
| 4 | **Gather intent** | Present prereq results, confirm stack + Azure services, ask remaining questions. | ⛔ Read [intent-gathering.md](references/intent-gathering.md) § After Prereq Returns |
| 5 | **Plan architecture** | Write `prepare-plan.json`. | ⛔ **You MUST read [prepare/instructions.md](prepare/instructions.md)** |
| 6 | **Scaffold approval gate** | Display plan for user approval BEFORE generating any files. | ⛔ Read [approval-gates.md](references/approval-gates.md) § Scaffold Gate |
| 7 | **Scaffold** | Generate IaC, self-review. Write `scaffold-manifest.json`. Update `context.json`. | ⛔ **You MUST read [scaffold/instructions.md](scaffold/instructions.md)** |
| 8 | **Deploy approval gate** | Display validation summary. ⛔ After approval: FIRST read deploy-checklist.md → deploy/instructions.md. Always use this pipeline's own `deploy/instructions.md` — do NOT hand off elsewhere. | ⛔ Read [approval-gates.md](references/approval-gates.md) § Deploy Gate |
| 9 | **Deploy** | Execute IaC, health-check. Write `deploy-result.json`. | ⛔ **You MUST read [deploy/instructions.md](deploy/instructions.md)** |
| 10 | **Handoff** | Surface deployment identity, cleanup commands, next steps. | ⛔ **You MUST read [`handoff-protocol.md`](references/handoff-protocol.md)** |
## Error Handling
| Error | Remediation |
|-------|-------------|
| Phase fails | Halt, report phase + error. User decides: retry, skip, abort. |
| MCP server unavailable | Skip affected checks, add disclaimer to `costEstimate.assumptions[]` and every approval gate. |
| Missing RBAC | Report required role + `az role assignment` command. |
> **Shared references:** [MCP tools](references/mcp-tool-reference.md) (cross-phase tool parameters) | [IaC resources](references/iac-resources.md) (Azure resource docs for troubleshooting)
@@ -1,74 +0,0 @@
<!-- 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.
# Prepare — Architecture Planning & Cost Estimation
## Quick Reference
| Property | Value |
|----------|-------|
| Best for | Mapping app components to Azure services with cost estimation and quota validation |
| Inputs | `prereq-output.json` + `context.json` from `.copilot-azure/sessions/{id}/` |
| Outputs | `prepare-plan.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 2 when `prereq-output.json` exists. Not directly user-routable.
> **Return to orchestrator:** When complete, return control to `azure-app-onboard`. Do NOT directly invoke scaffold or deploy.
## When NOT to Use
| Scenario | Use Instead |
|----------|-------------|
| Code readiness or prereq scanning | `azure-app-onboard` Step 3 (prereq) |
| IaC generation from a completed plan | `azure-app-onboard` Step 7 (scaffold) |
| Deploying resources to Azure | `azure-app-onboard` Step 9 (deploy) |
| Optimizing existing Azure spend | `azure-cost` |
| Estimating VM-specific costs | `azure-compute` |
| Enterprise landing zone architecture | `azure-enterprise-infra-planner` |
## MCP Tools
| Tool | Purpose |
|------|----------|
| `mcp_azure_mcp_pricing` / `azure-pricing` (router → `command: pricing_get`) | Cost estimation (inline — see Step 6). Fallback: dispatch [`subagent-pricing.md`](references/subagent-pricing.md) |
| `mcp_azure_mcp_policy` | Subscription policy constraints |
| `az rest` | Quota validation (via sub-agent — see Step 5) |
| `mcp_azure_mcp_cloudarchitect` → `cloudarchitect_design` | WAF-aligned architecture design |
| `mcp_azure_mcp_wellarchitectedframework` | Per-service WAF guidance |
| `mcp_azure_mcp_advisor` → `advisor_recommendation_list` | Optimization recommendations |
## Workflow
| # | Step | Action | Reference |
|---|------|--------|-----------|
| 1 | **Read session state** | Load `prereq-output.json` + `context.json`. Resolve subscription | Cross-ref [subscription-resolution.md](../references/subscription-resolution.md) if needed |
| 2 | **Query policy constraints** | Inline MCP: fetch policy + advisor recommendations | `mcp_azure_mcp_policy` + `mcp_azure_mcp_advisor` |
| 3 | **Map components to services** | Per-component Azure service selection, Dockerfile routing, deploy-as-is. ⛔ Existing Azure Functions components MUST remain separate Azure Functions services. | ⛔ **You MUST read [service-mapping.md](references/service-mapping.md) and [deploy-strategy.md](references/deploy-strategy.md)** |
| 4 | **Select SKUs + WAF analysis** | Budget-aware SKU selection, inline WAF service guidance | ⛔ **You MUST read [sku-matrix.md](references/sku-matrix.md)** |
| 5 | **Validate quotas + region capacity** | ⛔ Read [`subagent-quota.md`](references/subagent-quota.md) → dispatch as `task` (NEXT action MUST be `task`, ⛔ agent_type: `"task"` — NEVER `"general-purpose"`). Copy the **COMPLETE and UNMODIFIED** template text into the task prompt between `<<<TEMPLATE_START>>>` / `<<<TEMPLATE_END>>>` delimiters — do NOT summarize. Append the caller-provided inputs listed in [`subagent-quota.md`](references/subagent-quota.md)'s Input table AFTER the template block. ⛔ **After dispatching, proceed to Step 6 (cost estimation) while the subagent runs. Do NOT run quota checks yourself — the subagent handles it. Collect subagent results before Step 9 (write plan).** | ⛔ **You MUST read [`subagent-quota.md`](references/subagent-quota.md)** |
| 6 | **Estimate costs** | ⛔ **You MUST read [pricing-guide.md](references/pricing-guide.md)** for methodology, then [pricing-guide-services.md](references/pricing-guide-services.md) for per-service filters. Call the pricing router (`mcp_azure_mcp_pricing`/`azure-pricing`) with `command: "pricing_get"` + a `parameters{}` object inline per paid service. If MCP unavailable or fails → ⛔ Read [`subagent-pricing.md`](references/subagent-pricing.md) → dispatch as `task` (NEXT action MUST be `task`, ⛔ agent_type: `"task"` — NEVER `"general-purpose"`). Copy the **COMPLETE and UNMODIFIED** template text into the task prompt between `<<<TEMPLATE_START>>>` / `<<<TEMPLATE_END>>>` delimiters — do NOT summarize. Append data (services[], region, budget tier) AFTER the template block. Write results to `prepare-plan.json.costEstimate`. | [pricing-guide.md](references/pricing-guide.md) |
| 7 | **Generate naming** | Centralized naming: suffix, prefix, all resource names | ⛔ **You MUST read [naming-patterns.md](references/naming-patterns.md)** |
| 8 | **Determine IaC format** | Existing non-Azure `.tf` → `ask_user` Bicep vs TF, write to `overrides[].iacFormat`. No `.tf` → default Bicep. | (inline) |
| 9 | **Write prepare-plan.json** | Per `PreparePlan` schema. Include postDeployRecommendations, deploymentVariables | ⛔ **You MUST read [prepare-schemas.ts](references/prepare-schemas.ts)** for `PreparePlan` schema |
| 10 | **Return summary** | Structured summary for orchestrator approval gate | (inline — 1 line) |
| 11 | **Validate plan** | 4-dimension check: Goal Alignment, WAF Alignment, Dependency Completeness, Deployment Viability. Fix inline on failure, document tradeoffs in `assumptions[]`. | All must pass before writing |
### Step 5 — Post-Quota Validation
> ⛔ **NEVER present a region without checking quota first.** Skipping quota validation causes cascading deploy failures and extended healing loops.
> ⛔ If plan includes PostgreSQL/MySQL, verify `offerRestrictionsVerified: true` — if false/missing, region is blocked. Do NOT proceed to scaffold with unchecked DB services.
> ⛔ **Paid ≠ unlimited.** Every compute SKU — including B1, Consumption, and Serverless tiers — has a per-subscription, per-region quota. Do NOT skip quota checks. (F1/D1/Free are never selected — the compute floor is B1.)
> ⛔ After region fallback, update ALL `services[].region` in `prepare-plan.json`. Do not leave stale values.
## Error Handling
| Error | Remediation |
|-------|-------------|
| Pricing API 400 | Verify `--sku` included in the query |
| MCP pricing unavailable | Dispatch [`subagent-pricing.md`](references/subagent-pricing.md) as `task` fallback (uses direct HTTP to `prices.azure.com`) |
| Prereq output missing | Trigger prereq backfill |
| Quota check fails | Fall back to best-effort estimate + disclaimer |
| Override conflicts | Re-run from Step 3 with new constraints |
@@ -1,124 +0,0 @@
<!-- 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 Strategy
Determine how application code will be deployed to Azure based on prereq scan results. The prepare phase writes `deployStrategy` to `prepare-plan.json`; scaffold encodes it in Bicep; deploy executes it.
## Deployment Patterns
Three patterns — select based on `prereq-output.json.components[].buildRequirements`:
> ⛔ **Dockerfile ≠ Container Apps.** A Dockerfile that only serves static files (nginx, httpd, `COPY . /usr/share/nginx/html`) is NOT a backend app. Route static-only Dockerfiles as static sites per `service-mapping.md § Static Dockerfile sites`, not Pattern C.
| Pattern | When | `deployStrategy` needed? |
|---------|------|--------------------------|
| **A: Oryx auto-build** | No native modules, no Dockerfile | Yes — startup command + app settings go in Bicep at scaffold time |
| **B: Startup-install** | Native modules detected (`hasNativeModules: true`) | Yes — startup command + fallback install + app settings in Bicep |
| **C: Container-only** | Has Dockerfile that runs a server process (Express, Flask, uvicorn, etc.) | No — route to Container Apps (Dockerfile IS the deploy strategy) |
**Additional routing:**
| Condition | Action |
|-----------|--------|
| Jib build plugin (Java + `com.google.cloud.tools.jib`) | Container-only via Jib push to ACR — no Dockerfile needed |
---
## Pattern A: Oryx Auto-Build (Default)
**Languages:** Node.js, Python, .NET, Go, Java, PHP, Ruby
Oryx detects the stack from project manifests, installs dependencies, and builds automatically during zip deploy.
**Still write `deployStrategy` to `prepare-plan.json`** — even though Oryx auto-detects, the startup command and app settings MUST be in Bicep at scaffold time (not generated at deploy time). This eliminates imperative CLI commands during deploy.
```json
"deployStrategy": {
"codeDeployPattern": "oryx-auto",
"requiredAppSettings": {
"SCM_DO_BUILD_DURING_DEPLOYMENT": "true",
"ENABLE_ORYX_BUILD": "true",
"ORYX_DISABLE_COMPRESSION": "true"
},
"reason": "Standard Oryx build — no native modules, no Dockerfile. Compression disabled to avoid startup extraction delays. No custom appCommandLine — Oryx launcher handles decompression + start."
}
```
Read `prereq-output.json.entryPoint` for the app's start file — do NOT re-read manifests. Build the startup command from `package.json` `start` script (Node.js) or framework convention (Python gunicorn, .NET/Go/Java Oryx-native).
> ⛔ **Do NOT set a custom `appCommandLine` for Pattern A.** Let Oryx use `package.json` `start` script or framework defaults natively. A custom `appCommandLine` (`cd /home/site/wwwroot && node {entryPoint}`) replaces the Oryx launcher entirely — the launcher handles `node_modules.tar.gz` decompression, and bypassing it causes `MODULE_NOT_FOUND` crashes. Only set `appCommandLine` when `initCommands[]` has `required: true` entries (migrations).
>
> ⛔ **TypeScript projects:** Verify `typescript` + `@types/*` are in `dependencies` (not `devDependencies`) — Oryx production mode skips devDeps, causing `tsc` build failures.
>
> ⛔ **When `initCommands[]` has `required: true` entries:** Set `startupCommand` to prepend migrations: `"cd /home/site/wwwroot && {initCommand} && {framework-default-start}"`. Migrations are idempotent — safe on every cold start. Otherwise, omit `startupCommand` entirely (let Oryx handle it).
Scaffold encodes `startupCommand` → Bicep `appCommandLine`, and `requiredAppSettings` → Bicep `siteConfig.appSettings`. Deploy only does: wait → zip → health check.
---
## Pattern B: Startup-Install (Native Modules)
When native modules are detected, Oryx may fail to compile them. The startup-install pattern provides a two-layer safety net.
### Two-Layer Strategy
1. **Primary — Oryx zip build:** `SCM_DO_BUILD_DURING_DEPLOYMENT=true` + `ENABLE_ORYX_BUILD=true` tells Oryx to run dependency installation during the Kudu-side zip deploy. The Kudu build environment on App Service Linux has `gcc`, `make`, and build tools available, so native compilation CAN succeed here.
2. **Fallback — startup-install command:** `appCommandLine` runs dependency installation on first container boot IF the dependency directory doesn't exist. The existence guard ensures it only runs when needed — subsequent restarts skip it because `/home` is persistent storage.
Both layers are set in Bicep at scaffold time. The startup command is insurance — not the primary mechanism.
> **Why two layers?** `az webapp deploy --type zip` uses the OneDeploy API, which may not trigger Oryx even with `SCM_DO_BUILD_DURING_DEPLOYMENT=true`. The startup command catches this case. If Oryx DID build successfully, the guard skips the redundant install.
### Deploy Strategy Schema
Write to `prepare-plan.json.deployStrategy`:
```json
"deployStrategy": {
"codeDeployPattern": "startup-install",
"startupCommand": "cd /home/site/wwwroot && if [ ! -d node_modules ]; then npm install --production; fi && node index.js",
"requiredAppSettings": {
"WEBSITES_CONTAINER_START_TIME_LIMIT": "1800",
"SCM_DO_BUILD_DURING_DEPLOYMENT": "true",
"ENABLE_ORYX_BUILD": "true",
"ORYX_DISABLE_COMPRESSION": "true"
},
"reason": "Native module (better-sqlite3 via node-gyp) requires server-side npm install."
}
```
Replace `startupCommand` and `reason` with language-specific values from the entry point table below.
### Entry Point & Startup Commands
Same as Pattern A, but Node.js adds a dependency guard: `if [ ! -d node_modules ]; then npm install --production; fi` before the start command.
> ⛔ **Inline commands only.** Never generate a `.sh` startup script file — CRLF causes `bash` exit code 2.
> ⛔ **Python: do NOT use `venv` in startup commands.**
### SKU Implications
The compute floor is **B1 (Basic, ~$13/mo)** — F1/D1/Free are never selected (managed identity is mandatory and the free-tier MI sidecar OOMs). When native modules, TypeScript build, large deps, or a WSGI/ASGI server push resource needs higher, size up from B1 (B2/S1) and surface the reason at the approval gate: "⚠️ {reason}. {sku} required." Never size below B1.
### Container Timeout
`WEBSITES_CONTAINER_START_TIME_LIMIT` controls how long Azure waits for the container to start responding.
| Value | Use case |
|-------|----------|
| 230 (default) | Standard apps, no native compilation |
| 1800 (max) | Startup-install — native compilation takes 2-5 min. Python with scipy/scikit-learn can take longer |
Always set to `1800` when `codeDeployPattern == "startup-install"`.
---
## Pattern C: Container-Only
When the component has a Dockerfile with backend logic, route to **Container Apps**. The Dockerfile IS the deploy strategy — no `deployStrategy` needed in `prepare-plan.json`.
The deploy phase handles: ACR build → image push → Bicep redeploy with real image. See [code-deployment-container-apps.md](../../deploy/references/code-deployment-container-apps.md).
For Java apps using Jib (`build.gradle` + `com.google.cloud.tools.jib`), the build produces a container image without a Dockerfile — push to ACR via `jib` task.
@@ -1,87 +0,0 @@
<!-- 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.
# Prepare Phase — MCP Tools
Phase-exclusive tool parameters for the prepare phase. For shared tools (`subscription_list`, `group_list`, `get_azure_bestpractices`, `extension_cli_install`), 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/>
---
## `mcp_azure_mcp_pricing` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `pricing_get` | *(none)* | `service` (service name, e.g. "Azure App Service"), `sku` (`armSkuName` value), `region` (ARM region name), `currency` (e.g. "USD"), `filter` (OData filter for retail prices API) | ✅ |
**Usage:** Query Azure retail pricing. Use `service` + `region` for broad queries, `sku` for exact SKU lookup (when `armSkuName` is populated — e.g., Redis Cache, App Service Premium). Use `filter` for advanced OData queries against the retail prices API. See [pricing-guide.md](pricing-guide.md) for per-service filter strings and meter name mappings.
## `mcp_azure_mcp_quota` (hierarchical)
> ⛔ **Do NOT use for AppOnboard quota checks.** Use `az rest` with the Quota REST API instead — see [sku-quota-validation.md](sku-quota-validation.md). The MCP quota tool returns misleading "No Limit" values for unsupported resource types — "No Limit" means the quota API doesn't support that resource type, NOT unlimited capacity.
| Command | Required Params | Optional Params | Read-Only |
|---------|----------------|-----------------|-----------|
| `quota_usage_check` | `region`, `resource-types` (ARM type, e.g. `Microsoft.Compute/virtualMachines`) | `subscription` | ✅ |
| `quota_region_availability_list` | `resource-types` (ARM type) | `subscription` | ✅ |
## `mcp_azure_mcp_cloudarchitect` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `cloudarchitect_design` | *(none)* | `question`, `question-number`, `total-questions`, `answer`, `next-question-needed` (bool), `confidence-score` (0.0–1.0), `state` (JSON — tracks architectureComponents, architectureTiers, requirements{explicit,implicit,assumed}, confidenceFactors) | ✅ |
**Usage:** Multi-turn conversational tool. For single-shot use, populate `state` with known requirements, set `confidence-score` ≥ 0.7 and `next-question-needed: false` to get a direct recommendation without follow-ups.
## `mcp_azure_mcp_wellarchitectedframework` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `wellarchitectedframework_serviceguide_get` | *(none)* | `service` (case-insensitive, hyphens/underscores/spaces OK, e.g. "cosmos-db", "App Service", "cosmosdb") | ✅ |
**Usage:** Omit `service` to list all supported services. Provide `service` to get per-service guidance across all 5 WAF pillars.
## `mcp_azure_mcp_advisor` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `advisor_recommendation_list` | *(none)* | `subscription`, `resource-group` | ✅ |
## `mcp_azure_mcp_group_resource_list`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| `resource-group` | `subscription`, `tenant` | ✅ |
Returns: resource names, IDs, types, locations within the group.
## `mcp_azure_mcp_policy` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| *(subscription scope)* | *(none)* | `subscription` | ✅ |
**Usage:** Bulk fetch subscription policy constraints — blocked resource types, required tags, allowed regions. Use in prepare Step 2 alongside `advisor_recommendation_list` to surface governance constraints early.
---
## Phase 2 Tool Map
| Tool | Sub-command | AppOnboard Step | Purpose |
|------|-----------|----------|---------|
| `mcp_azure_mcp_cloudarchitect` | `cloudarchitect_design` | Step 3 | Architecture validation. Single-shot: populate `state` from `context.json`, set `confidence-score: 0.8` |
| `mcp_azure_mcp_wellarchitectedframework` | `wellarchitectedframework_serviceguide_get` | Step 4 | Per-service WAF guidance. Call with `service: "{service-name}"` for each planned service |
| `mcp_azure_mcp_advisor` | `advisor_recommendation_list` | Step 2 | Get existing subscription recommendations alongside policy query |
| `mcp_azure_mcp_policy` | *(hierarchical)* | Step 2 | Subscription policy constraints — blocked types, required tags, allowed regions |
| `mcp_azure_mcp_pricing` | `pricing_get` | Step 6 | Cost estimation. Read [pricing-guide.md](pricing-guide.md) first |
| `mcp_azure_mcp_quota` | ⛔ Do NOT use | Step 5 | ⛔ Read [sku-quota-validation.md](sku-quota-validation.md) and use `az rest` with Quota REST API instead |
| `mcp_azure_mcp_subscription_list` | *(flat)* | Step 1 | Resolve target subscription if not in context |
| `mcp_azure_mcp_group_list` | *(flat)* | Step 7 | List existing RGs for reuse/conflict detection |
| `mcp_azure_mcp_group_resource_list` | *(flat)* | Step 7 | Detect naming collisions in target RG |
---
## Tool Pitfalls
- **`mcp_azure_mcp_pricing` → `pricing_get`:** Use `--sku` when querying by `armSkuName` (e.g., Redis Cache, App Service Premium). For services without `armSkuName`, use `filter` and `meterName` matching instead. Omitting all filter params returns too many results.
@@ -1,64 +0,0 @@
<!-- 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.
# Naming Patterns
Per-resource naming rules for AppOnboard-generated Azure resources. Apply in Step 7 (Generate naming).
> **Reference:** [Azure naming conventions](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-naming) — link, don't duplicate. Check docs for updates.
## Default Pattern
```
{abbreviation}-{resourcePrefix}
```
Where `resourcePrefix` = `{project}-{env}-{suffix}` (generated once in prepare Step 7, stored in `prepare-plan.json.naming.resourcePrefix`).
- `{project}` — app name from `context.json.app.name` (lowercase, alphanumeric + hyphens)
- `{env}` — `dev` / `staging` / `prod` (from intent or default `dev`)
- `{suffix}` — first 4 chars of session UUID (e.g., `a1d5`). Generated once, stored in `naming.suffix`
- `{abbreviation}` — short service prefix from the table below (e.g., `app`, `st`, `rg`)
**Examples with `resourcePrefix = myapp-dev-a1d5`:**
- Resource Group: `rg-myapp-dev-a1d5`
- App Service: `app-myapp-dev-a1d5`
- Storage: `stmyappdeva1d5` (alphanumeric only)
> ⛔ **No redundancy.** The resource name is `{abbr}-{resourcePrefix}` — NOT `{project}-{abbr}-{project}-{env}-{suffix}`. The project name appears ONCE in the prefix.
>
> ⛔ **Scaffold reads naming from the plan — does NOT generate or modify resource names.**
Override via `context.json.overrides[]` with `key: "naming.pattern"`.
## Per-Resource Rules
| Resource | Abbreviation | Max Length | Allowed Chars | Globally Unique? | Example |
|----------|-------------|------------|---------------|------------------|---------|
| Resource Group | `rg` | 90 | Alphanumeric, hyphens, underscores, periods, parens | No (but include suffix to avoid cross-session collisions) | `rg-myapp-dev-a1d5` |
| App Service | `app` | 60 | Alphanumeric, hyphens | **Yes** | `app-myapp-dev-a1d5` |
| Container App | `ca` | 32 | Lowercase alphanumeric, hyphens | No (within env) | `ca-myapp-dev-a1d5` |
| Container Registry | `cr` | 50 | **Alphanumeric only** | **Yes** | `crmyappdeva1d5` |
| Azure SQL Server | `sql` | 63 | Lowercase alphanumeric, hyphens | **Yes** | `sql-myapp-dev-a1d5` |
| Cosmos DB | `cosmos` | 44 | Lowercase alphanumeric, hyphens | **Yes** | `cosmos-myapp-dev-a1d5` |
| Storage Account | `st` | 24 | **Lowercase alphanumeric only** | **Yes** | `stmyappdeva1d5` |
| Log Analytics | `log` | 63 | Alphanumeric, hyphens | No (within RG) | `log-myapp-dev-a1d5` |
| App Insights | `appi` | 260 | Most chars | No (within RG) | `appi-myapp-dev-a1d5` |
| Service Bus | `sb` | 50 | Alphanumeric, hyphens | **Yes** | `sb-myapp-dev-a1d5` |
| Functions | `func` | 60 | Alphanumeric, hyphens | **Yes** | `func-myapp-dev-a1d5` |
| Static Web Apps | `swa` | 40 | Alphanumeric, hyphens | No (suffix still required — Rule 1) | `swa-myapp-dev-a1d5` |
| Redis Cache | `redis` | 63 | Alphanumeric, hyphens | **Yes** | `redis-myapp-dev-a1d5` |
> **CAF deviation:** Azure Cloud Adoption Framework uses `sbns` for Service Bus namespace. AppOnboard uses `sb` for brevity — either is acceptable.
## Rules
1. **ALL resources get the `{suffix}`** — including resource groups. This prevents cross-session naming collisions when the same app is deployed multiple times. The suffix is a 4-char random string generated once per session.
2. **Container Registry + Storage Account:** strip hyphens (alphanumeric only). ⛔ **Mechanical transform — do NOT re-derive char-by-char:** `('cr' + resourcePrefix).replace(/-/g,'').toLowerCase()`, truncate to ≤50 (ACR) / ≤24 (Storage). Worked example: `resourcePrefix = bezkoder-dev-18a3` → `cr` + `bezkoderdev18a3` → `crbezkoderdev18a3`. Copy the pattern; do not spell out the concatenation.
3. **Validate length** after substitution — Storage Account and Container App (24 / 32 chars, alphanumeric) are the tightest constraints. Budget for `{project}`: `24 - len(abbreviation) - len(env) - len(suffix) - 3 - 2` (separators + healing reserve). For `st` + `dev` (Storage strips hyphens): ~10 chars max. The 2-char reserve ensures room for a healing suffix (e.g., `edd6` → `edd602`) on region fallback. **Truncation when over budget:**
1. Truncate at the nearest hyphen boundary within budget (e.g., `broken-web-app` at 10 → `broken-web`)
2. If no hyphen within budget: hard truncate, no trailing hyphen
3. Verify truncated name doesn't collide with reserved words (Rule 4)
4. Recompute ALL resource names with the truncated project — do NOT mix long and short names
4. **Never use reserved words** as resource names (`admin`, `login`, `root`, `test`)
5. Populate `naming.resources[]` in `prepare-plan.json` with concrete names after validation
@@ -1,201 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Prepare artifact schema — prepare-plan.json.
* Read by prepare instructions.md Step 9 (write prepare-plan.json).
*/
// ─── Shared type (inlined from session-schemas.ts to avoid cross-ref) ────────
export interface PostDeployRecommendation {
title: string;
reason: string;
effort: "low" | "medium" | "high";
services?: string[];
}
// ─── Healing types (prepare phase) ───────────────────────────────────────────
export type PrepareHealingTrigger = "quota" | "policy" | "rubric" | "region";
export type PrepareIssueClassification = "AUTO_FIXABLE" | "NEEDS_USER_INPUT";
export type PrepareHealingResult = "fixed" | "needs-input" | "still-failing";
export interface PrepareHealingIssue {
dimension: string;
detail: string;
classification: PrepareIssueClassification;
}
export interface PrepareHealingFix {
service: string;
change: string;
reason: string;
}
export interface PrepareHealingAttempt {
attempt: number;
trigger: PrepareHealingTrigger;
issues: PrepareHealingIssue[];
fixes: PrepareHealingFix[];
result: PrepareHealingResult;
}
// ─── prepare-plan.json ───────────────────────────────────────────────────────
export interface PlannedService {
name: string;
sku: string;
purpose: string;
component: string;
region: string;
resourceName: string;
/** Exact engine version for managed database services (MySQL/PostgreSQL Flexible Server),
* sourced from the provider capabilities API during quota validation — NOT guessed.
* ARM rejects major-only strings (e.g. MySQL '8.0'); use the exact supported patch
* (e.g. '8.0.21'). Omit for non-DB services. */
version?: string;
}
export interface CostBreakdownItem {
service: string;
sku: string;
monthlyUsd: number;
note?: string;
}
export interface CostEstimate {
monthlyUsd: number;
currency: "USD";
breakdown: CostBreakdownItem[];
disclaimer: string;
}
export interface RejectedAlternative {
service: string;
reason: string;
}
export interface NamingResource {
type: string;
name: string;
}
export interface NamingConfig {
pattern: string;
/** The computed prefix used for all resource names: {project}-{env}-{suffix}.
* deploymentVariables.environmentName MUST equal this value. */
resourcePrefix: string;
resources: NamingResource[];
}
export type QuotaSource = "cli" | "fallback";
export interface QuotaCheck {
resource: string;
region: string;
required: number;
available: number;
sufficient: boolean;
provider: string;
currentUsage: number;
currentLimit: number;
adjustable: boolean;
source: QuotaSource;
}
export interface DeployStrategy {
/** Deploy pattern — see deploy-strategy.md for the three patterns.
* "oryx-auto": Oryx handles build + run (Pattern A — default for most apps).
* "startup-install": native module compilation at startup (Pattern B).
* "container-only": Dockerfile-based deploy via ACR (Pattern C).
* Omit when Oryx auto-build is sufficient and no special startup is needed. */
codeDeployPattern?: "oryx-auto" | "startup-install" | "container-only";
/** Startup command for native module compilation (goes into appCommandLine) */
startupCommand?: string;
/** Required app settings for the deploy strategy (e.g., SCM_DO_BUILD_DURING_DEPLOYMENT) */
requiredAppSettings?: Record<string, string>;
/** Human-readable reason for choosing this strategy */
reason: string;
}
export interface InstrumentationConfig {
appInsightsEnabled: boolean;
reason: string;
}
export interface DeploymentVariables {
/** MUST equal naming.resourcePrefix (e.g., "myapp-dev-a1d5"), NOT the env label ("dev") */
environmentName: string;
location: string;
sessionId: string;
deployedBy: string;
/** Variable names that could not be resolved at prepare time — deploy reads, not asks */
deferred?: string[];
}
export interface OfferRestriction {
/** Azure provider namespace (e.g., "Microsoft.DBforPostgreSQL") */
provider: string;
/** Region checked */
region: string;
/** Whether the offer is restricted in this region */
restricted: boolean;
/** Reason string from capabilities API, if any */
reason?: string;
}
export interface QuotaValidation {
/** True when all planned services had quota confirmed */
verified: boolean;
/** How quota was verified — persists in session artifact, survives context compaction */
method?: "cli" | "what-if" | "unverifiable";
/** The specific region that passed validation */
verifiedRegion?: string;
/** The specific SKU that was validated (e.g., "B1", "S1") */
verifiedSku?: string;
/** Regions checked during validation */
checkedRegions?: string[];
/** Resources that failed quota checks */
failedResources?: string[];
/** Reason when verified is false (e.g., "quota CLI unavailable", "all regions zero") */
reason?: string;
/** Offer restriction results for database services (PostgreSQL, MySQL).
* Populated by prepare Step 5 when database services are in the plan.
* `LocationIsOfferRestricted` is NOT caught by what-if — this is the only pre-deploy check. */
offerRestrictions?: readonly OfferRestriction[];
/** Derived from offerRestrictions[]: true when every database service in services[]
* has at least one entry in offerRestrictions[]. Set this based on the array contents,
* not independently. Deploy gate halts if false/missing when services include PostgreSQL/MySQL. */
offerRestrictionsVerified?: boolean;
}
export interface PreparePlan {
services: PlannedService[];
costEstimate: CostEstimate;
rejectedAlternatives: RejectedAlternative[];
naming: NamingConfig;
quotas: readonly QuotaCheck[];
assumptions: string[];
/** IaC format for scaffold — "bicep" (default) or "terraform" */
iacFormat: "bicep" | "terraform";
/** Application database name the app expects to exist (from compose env such as
* MYSQL_DATABASE / MYSQLDB_DATABASE / POSTGRES_DB, a connection string, or ORM config).
* Scaffold emits a `flexibleServers/databases` child resource for this so the schema DB
* exists in IaC before the container starts; the conformance gate's DB-NAME-PRESENT check
* asserts it. Omit only when no managed database is in the plan. */
appDbName?: string;
postDeployRecommendations?: PostDeployRecommendation[];
/** Instrumentation decision — object with boolean + reason, NOT a bare boolean */
instrumentation?: InstrumentationConfig;
/** Deployment variables resolved at prepare time — scaffold/deploy read, not re-ask */
deploymentVariables?: DeploymentVariables;
/** Quota validation result from proactive capacity check */
quotaValidation?: QuotaValidation;
/** Code deploy strategy for native module apps — see deploy-strategy.md.
* Only populated when Pattern B (startup-install) is needed. */
deployStrategy?: DeployStrategy;
healingAttempts?: readonly PrepareHealingAttempt[];
}
@@ -1,200 +0,0 @@
<!-- 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.
# App Onboard Prepare — Per-Service Pricing Patterns
Filter strings, meter names, and formulas per Azure service. For methodology and troubleshooting, see [pricing-guide.md](pricing-guide.md).
### Container Apps
No `armSkuName`. Filter: `serviceName eq 'Azure Container Apps' and armRegionName eq '{region}' and priceType eq 'Consumption' and isPrimaryMeterRegion eq true`
**Key meters:** `Standard vCPU Active Usage` (1 Second), `Standard Memory Active Usage` (1 GiB Second), `Standard Requests` (1M).
**Monthly (Consumption, 1 vCPU, 2 GiB, 8h active/day):**
`(vCPU_active_rate × 3600 × 8 × 30) + (memory_active_rate × 2 × 3600 × 8 × 30)`
**Free grant:** 180K vCPU-sec + 360K GiB-sec + 2M requests/sub/month. Scale-to-zero = $0 idle. When `min_replicas >= 1`: add idle cost from idle meters.
---
### Container Registry (ACR)
Empty `armSkuName` — ⛔ do NOT filter by `sku` (returns `[]`). Filter: `serviceName eq 'Container Registry' and armRegionName eq '{region}' and priceType eq 'Consumption'`, then match `meterName`.
| Tier | `meterName` | `unitOfMeasure` | Monthly |
|------|-------------|-----------------|---------|
| Basic | `Basic Registry Unit` | `1/Day` | `retailPrice × 30` (~$5) |
| Standard | `Standard Registry Unit` | `1/Day` | `retailPrice × 30` (~$20) |
| Premium | `Premium Registry Unit` | `1/Day` | `retailPrice × 30` (~$50) |
⛔ **`1/Day` → × 30, NOT × 730.** The other ACR meters (`Task vCPU Duration` = `1 Second` build compute, `Data Stored` = `1 GB/Month`) are usage-based — exclude from the fixed monthly unless the app builds heavily. ACR Basic fixed cost ≈ **$5/mo**.
---
### App Service
**MCP call:** `pricing_get --service "Azure App Service" --region "{region}"`
**Meter-to-SKU mapping:**
| SKU | `meterName` | Notes |
|-----|-------------|-------|
| B1 | `B1` | Compute floor — no F1/Free (see [sku-matrix.md](sku-matrix.md)) |
| B2 | `B2` | |
| S1 | `S1 App` | |
| P1v3 | `P1 v3 App` | Linux/Windows have different `productName` |
**Gotcha:** `armSkuName` empty for Basic/Standard. Match on `meterName`.
**Monthly:** `retailPrice × 730`
---
### Azure SQL Database
**DTU model** — no `armSkuName`. Filter by `productName` + `meterName`.
| SKU | `productName` contains | `meterName` (unit: 1/Day) |
|-----|----------------------|-------------|
| Basic (5 DTU) | `SQL Database Single Basic` | `B DTU` |
| S0 (10 DTU) | `SQL Database Single Standard` | `S0 DTUs` |
| S1 (20 DTU) | `SQL Database Single Standard` | `S1 DTUs` |
**Monthly (DTU):** `retailPrice × 30`
**vCore model:** `productName` filters — Serverless: `'SQL Database General Purpose - Serverless - Compute Gen5'`. Hyperscale: `'SQL Database Single/Elastic Pool Hyperscale'`. Monthly: `retailPrice × 730`.
---
### Cosmos DB
Filter: `serviceName eq 'Azure Cosmos DB' and armRegionName eq '{region}' and priceType eq 'Consumption' and isPrimaryMeterRegion eq true`
| Model | Match on | Monthly formula |
|-------|----------|----------------|
| Provisioned | `meterName eq '100 RU/s'`, `productName eq 'Azure Cosmos DB'` | `(targetRU / 100) × retailPrice × 730` |
| Autoscale | `productName eq 'Azure Cosmos DB autoscale'`, `meterName` starts with `AP` | varies |
| Serverless | `productName eq 'Azure Cosmos DB serverless'`, `meterName eq '1M RUs'` | `retailPrice × estimatedMillionsOfRU` |
> ⚠️ **Provisioned 100 RU/s gotcha:** The `isPrimaryMeterRegion eq true` entry has `retailPrice: 0` (free-tier placeholder). The real price (~$0.008/hr in eastus) has `isPrimaryMeterRegion: false`. Drop `isPrimaryMeterRegion` from the filter or match on `retailPrice > 0`.
**Storage:** `meterName eq 'Data Stored'` with `productName eq 'Azure Cosmos DB'` (1 GB/Month). Note: this meter has `isPrimaryMeterRegion: false` — drop that filter or query without it.
**Free tier:** 1,000 RU/s + 25 GB per account (one per subscription).
---
### Storage
No `armSkuName`. Filter by `meterName` + `productName`.
**Filter (Hot LRS Blob):**
```
serviceName eq 'Storage' and armRegionName eq '{region}' and meterName eq 'Hot LRS Data Stored' and productName eq 'Blob Storage'
```
**Meter-to-SKU mapping:**
| SKU matrix tier | `meterName` | `productName` |
|----------------|-------------|---------------|
| Standard_LRS | `Hot LRS Data Stored` | `Blob Storage` or `General Block Blob v2` |
| Standard_GRS | `Hot GRS Data Stored` | `Blob Storage` or `General Block Blob v2` |
| Premium_LRS | `Premium LRS Data Stored` | `Premium Block Blob` |
**Monthly:** `retailPrice × estimatedGB`
---
### Service Bus
**Filter:**
```
serviceName eq 'Service Bus' and armRegionName eq '{region}'
```
> ⚠️ **Do NOT add `isPrimaryMeterRegion eq true`** — all Service Bus meters have `isPrimaryMeterRegion: false`. Adding that filter returns zero useful results.
**Key meters:**
| Tier | `meterName` | Unit | Notes |
|------|-------------|------|-------|
| Basic | `Basic Messaging Operations` | 1M | Usage-based only |
| Standard | `Standard Base Unit` | 1/Hour | Base cost (always-on) |
| Standard | `Standard Messaging Operations` | 1M | Tiered: first 13M free |
| Premium | `Premium Messaging Unit` | 1/Hour | Per messaging unit |
**Monthly (Standard):** `baseUnitRate × 730` + operations beyond free tier. Standard tiered: first 13M free.
**Gotcha:** Standard Base Unit has two entries — hourly and monthly. Use hourly × 730.
---
### Redis Cache
`armSkuName` IS populated — `pricing_get --sku` works.
**MCP call:** `pricing_get --service "Redis Cache" --sku "{armSkuName}" --region "{region}"`
**armSkuName pattern:** `Azure_Redis_Cache_{Tier}_{Size}_Cache` (e.g., `Azure_Redis_Cache_Basic_C0_Cache`, `Azure_Redis_Cache_Standard_C1_Cache`). Query the MCP tool for exact pricing.
**Gotcha:** Basic tier entries have `isPrimaryMeterRegion: false`. The standard filter strips them out. Query by `armSkuName` directly or omit `isPrimaryMeterRegion` and match on `productName eq 'Azure Redis Cache Basic'`.
**Monthly:** `retailPrice × 730`
---
### Azure Database for PostgreSQL Flexible Server
`armSkuName` IS populated — `pricing_get --sku` works. ⛔ **`skuName` is CASE-SENSITIVE:** `B1MS` works, `B1ms` returns 0 results.
**MCP call:** `pricing_get --service "Azure Database for PostgreSQL" --sku "{armSkuName}" --region "{region}"`
**armSkuName values:** ⛔ Case-sensitive. Small Burstable uses short names (`B1MS`, `B2S`); larger Burstable + GP/MO use `Standard_` prefix.
| SKU | `armSkuName` |
|-----|-------------|
| Burstable B1ms | `B1MS` |
| Burstable B2s | `B2S` |
| Burstable B2ms | `Standard_B2ms` |
| General Purpose D2ds_v5 | `Standard_D2ds_v5` |
**Storage:** Query separately — `meterName eq 'Storage Data Stored'` and `productName` containing `Flexible Server Storage`. Unit: 1 GiB/Month. Default 32 GiB included at ~$0.115/GiB/mo.
**Monthly (compute):** `retailPrice × 730`
**Monthly (storage):** `storageRate × storageSizeGB`
**Total:** compute + storage (e.g., B1ms + 32 GiB ≈ $12.41 + $3.68 = ~$16.09)
---
### Functions
**Consumption plan:** Not in API. Free: 1M executions + 400K GB-seconds/month.
**Flex/Premium** — filter: `serviceName eq 'Functions' and armRegionName eq '{region}' and priceType eq 'Consumption' and isPrimaryMeterRegion eq true`
- Flex: `On Demand Execution Time` (1 GB Second), `On Demand Total Executions` (10), `Always Ready Baseline`
- Premium: `Premium vCPU Duration` (1 Hour), `Premium Memory Duration` (1 GiB Hour). Monthly: `vCPU_rate × 730 + memory_rate × GiB × 730`
---
### Static Web Apps
**Not in the Retail Prices API.** Fixed pricing — query [azure.microsoft.com/pricing/details/app-service/static](https://azure.microsoft.com/en-us/pricing/details/app-service/static/):
- **Standard tier (floor — Free is never selected):** Flat monthly per app, unlimited bandwidth, 5 custom domains, SLA, private endpoints
---
### Log Analytics / Application Insights
**Filter:**
```
serviceName eq 'Azure Monitor' and armRegionName eq '{region}' and priceType eq 'Consumption' and isPrimaryMeterRegion eq true
```
**Key meters:**
- `Platform Logs Data Processed` (unit: 1 GB)
- Log Analytics ingestion — match `meterName` containing `Data Ingestion`
**Free grant:** First 5 GB/month of Log Analytics data ingestion.
**Monthly:** `retailPrice × estimatedGB` (subtract 5 GB free grant).
@@ -1,73 +0,0 @@
<!-- 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.
# App Onboard Prepare — Pricing Guide
> ⛔ **SELF-CHECK:** If `costEstimate.breakdown[]` has ANY `monthlyUsd > 0`, at least one `pricing_get` or `az rest` pricing API call MUST appear in this session. Estimating from memory or training data is NEVER acceptable — reference values in this file may be outdated. If API fails after 2 attempts, use the reference value WITH disclaimer: `"⚠️ Estimated from reference — live API unavailable."`
## No Free-Tier Shortcut — Compute Always Has Cost
> ⛔ **There is no $0 shortcut.** Compute has a floor (App Service **B1**, Static Web Apps **Standard**, Functions **Flex Consumption**) — F1/D1/Free/Consumption are never selected (see [sku-matrix.md](sku-matrix.md)). So any app with a compute component has a non-zero monthly cost and MUST get a live pricing call. Do NOT write a $0 estimate for a compute app.
Some services can still fall within free **grant** limits (they are metered, not a fixed free SKU) — note the grant in the estimate but still price the paid meters:
| Service | Free grant (still price paid usage above it) |
|---------|-----------------------------------------------|
| Functions (Flex Consumption) | First 250K executions + 100K GB-s/month |
| Cosmos DB | Free-tier account: 1000 RU/s + 25 GB (one per subscription) |
| Container Apps | First 180K vCPU-s + 360K GiB-s + 2M requests/month |
| Log Analytics | First 5 GB/month ingestion |
---
## App Service Quick Reference
| SKU | `skuName` | Monthly |
|-----|-----------|---------|
| B1 Linux | `B1` | ~$13.14 |
> ⛔ **Case-sensitive:** use `B1` not `b1`. Use `skuName` (not `armSkuName`) for Basic/Standard.
---
## How to Use
1. Pick SKU from [sku-matrix.md](sku-matrix.md) based on budget intent.
2. Find service section in [pricing-guide-services.md](pricing-guide-services.md) for filter strings and formulas.
3. Call the pricing router — tool `mcp_azure_mcp_pricing` (VS Code) / `azure-pricing` (CLI) — with `intent`, `command: "pricing_get"`, and a `parameters` object (NOT `--flags`). Parallel OK. ⛔ At least one filter inside `parameters`: `sku`, `service`, `region`, `service-family`, or `filter`. ⛔ **`sku` matches `armSkuName`, not `skuName`** — use it ONLY when `armSkuName` is populated (App Service, MySQL/PostgreSQL, Redis). Empty-`armSkuName` services (ACR, Storage, Cosmos) return `[]` or 400 for `sku`/`service` — use a raw `filter` on `serviceName` + `meterName` instead.
4. Apply the monthly multiplier from each meter's `unitOfMeasure` (see § Monthly Multiplier) — NOT a per-service constant.
5. Cross-check returned `retailPrice` against planned SKU.
## Monthly Multiplier — by Meter Unit
⛔ **Read `unitOfMeasure` per price record — NEVER blanket `× 730`** (one service mixes units, e.g. ACR returns `1/Day` + `1 Second` + `1 GB/Month`):
| `unitOfMeasure` | Monthly formula |
|-----------------|-----------------|
| `1 Hour` / `1/Hour` | `retailPrice × 730` |
| `1/Day` / `1 Day` | `retailPrice × 30` |
| `1 GB/Month` | `retailPrice × estimatedGB` (already monthly) |
| `1 Second` / `1 GiB Second` | `retailPrice × activeUnitsPerMonth` (usage-based) |
| `10K` / `1M` operations | `retailPrice × estimatedOps ÷ unitSize` |
## Usage-Based Services
When AI/OpenAI/LLM components detected: add `"💸 Usage-based — excluded from total"` to `costEstimate.breakdown[]` and `"⚠️🤖 AI inference costs excluded"` to `assumptions[]`. Surface at EVERY approval gate.
## Service Pricing Reference
See [pricing-guide-services.md](pricing-guide-services.md) for per-service filter strings, meter names, and monthly formulas.
**Key rules:**
- `armSkuName` is empty for most PaaS — filter by `meterName` or `productName`
- Monthly multiplier: read `unitOfMeasure` (§ Monthly Multiplier) — never assume ×730
- Functions Consumption / Static Web Apps: not in API
- Default `isPrimaryMeterRegion eq true` + `priceType eq 'Consumption'` — but check the service section in [pricing-guide-services.md](pricing-guide-services.md) first; some drop `isPrimaryMeterRegion` (Service Bus, Cosmos 100 RU/s, Redis Basic).
- Service names are case-sensitive
## Troubleshooting
When `pricing_get` returns empty: (1) Check `armSkuName` populated/case-sensitive? (2) HTTP fallback: `https://prices.azure.com/api/retail/prices?$filter={filter}`. (3) After 2 failures, use [pricing-guide-services.md](pricing-guide-services.md) with disclaimer. (4) Log to `costEstimate.apiFailures[]`.
> ⛔ **Never claim user has free credits remaining.** Use: "Check balance at portal."
@@ -1,130 +0,0 @@
<!-- 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.
# Service Mapping Tables
Component→Azure service selection. Apply `context.json.intent` as modifiers, `context.json.overrides[]` as hard constraints, and policy constraints as filters.
## Hosting
> ⛔ **Existing framework beats generic component type.** If a component path contains a `host.json` file
> and an Azure Functions SDK or worker configuration, select Azure Functions for that component. Do not
> route it through the generic REST/GraphQL API row, merge it into a frontend service, or package it as an
> SWA-managed API. When a SPA and Functions component coexist, `services[]` must contain separate frontend
> and Azure Functions entries whose `component` values match the detected component names.
> ⛔ **Implicit dependencies:** When selecting Container Apps and the component has a Dockerfile (or `hasDockerfile: true` in prereq), **ALWAYS include Container Registry (Basic)** in `services[]`. Container Apps requires ACR to host custom images — omitting it forces an imperative add during deploy, wasting a healing round. ACR Basic is $0.17/day (~$5/mo).
| Component Type | Primary Service | Alternatives | Selection Signal |
|---------------|----------------|-------------|-----------------|
| SPA Frontend | Static Web Apps | Blob + CDN | React/Vue/Angular, no SSR |
| Existing Azure Functions project | Azure Functions (Flex Consumption) | Azure Functions Premium | `host.json` plus Functions SDK or worker configuration |
| SSR Web App | Container Apps | App Service, AKS | Next.js/Nuxt, server-rendered |
| REST/GraphQL API | Container Apps | App Service, Functions, AKS | Express/Fastify/Flask/FastAPI |
| Background Worker | Container Apps (scale-to-zero) | Functions, AKS | Celery/Bull/Agenda, no HTTP |
| Scheduled Task | Functions (Timer) | Container Apps Jobs | Cron patterns, periodic execution |
| Event Processor | Functions | Container Apps + KEDA | Event-driven, queue/topic consumer |
| Microservices (K8s) | AKS | Container Apps | kubectl/helm in repo, CRDs, service mesh |
| GPU/ML Workloads | AKS | Azure ML | GPU requirements, training workloads |
**Stack shortcuts:** Containers (Docker, microservices) → Container Apps or AKS. Serverless (event-driven, variable traffic) → Functions. Traditional web (PaaS preference) → App Service.
**Frontend plus Functions:** Default to a detached frontend host and a separately deployed Function App.
For Static Web Apps, configure the Function App CORS allowlist with the SWA origin. Do not change the
Function App's runtime, authentication provider, or app settings to `azureStaticWebApps`. Only link the
existing Function App as an SWA backend when `context.json.overrides[]` records explicit user approval for
`linkFunctionsToStaticWebApp`; linking does not remove the Function App service or change its deployment
channel.
**AKS vs Container Apps:** Use Container Apps when scale-to-zero needed, no K8s expertise, or KEDA-driven event processing. Delegate AKS planning to the `azure-kubernetes` agent.
**App Service vs Container Apps:** App Service preferred for single-process apps with no container orchestration (B1 floor, ~$13/mo — no free tier is offered). Container Apps preferred for REST/GraphQL APIs (scaffold generates Dockerfile if missing), scale-to-zero, multi-container/sidecar, or event-driven KEDA scaling. Budget affects SKU tier (Consumption vs Dedicated), not compute service type. Container Apps Consumption scales to zero (near-$0 idle) — the closest thing to free, and it supports managed identity.
**Static Dockerfile sites (nginx/httpd serving HTML):**
- **Primary:** Static Web Apps **Standard** — global CDN, custom auth/CORS, managed-identity BYO backends.
> ⛔ **SWA region availability:** Validate via `az provider show --namespace Microsoft.Web --query "resourceTypes[?resourceType=='staticSites'].locations" -o tsv`.
- **Alternative:** App Service **B1** — ⛔ for static content use Windows B1 (IIS serves `index.html` natively) or Linux B1 (`linuxFxVersion: 'STATICSITE|1.0'`). ⛔ Do NOT use `NODE|*`/`PHP|*`/`PYTHON|*` for static sites — causes 504/503 cold start.
- **If Docker required:** Container Apps (scale-to-zero) or App Service B1+ (custom containers).
**Plain HTML (no package manager, no Dockerfile):** Static Web Apps Standard preferred, or Windows App Service B1 (IIS serves natively; Linux: `linuxFxVersion: 'STATICSITE|1.0'`).
## Data
| Need | Primary Service | Alternatives | Selection Signal |
|------|----------------|-------------|-----------------|
| Relational | Azure SQL | PostgreSQL Flexible, MySQL Flexible | SQL schema, transactions, joins |
| Document/NoSQL | Cosmos DB | — | JSON docs, global distribution |
| Graph (Gremlin) | Cosmos DB (Gremlin API) | — | Graph traversal, relationships |
> **Cosmos DB Serverless:** Each Serverless account supports ONE API type (SQL, MongoDB, Gremlin, Table, Cassandra). Multi-API apps require separate Serverless accounts.
| Cache | Redis Cache | — | Session store, rate limiting |
| Files/Blobs | Blob Storage | Files Storage | File uploads, static assets |
| Search | AI Search | — | Full-text search requirements |
| MariaDB/MySQL | MySQL Flexible Server | — | `mariadb:*` or `mysql:*` in compose / connection config |
| Elasticsearch/OpenSearch | AI Search | Azure Monitor (for Kibana) | `elasticsearch:*` or `opensearch:*` in compose |
| S3-compatible / MinIO | Blob Storage | — | `minio/*` in compose, S3 SDK usage |
## Integration
| Need | Primary Service | Selection Signal |
|------|----------------|-----------------|
| Message Queue | Service Bus | Point-to-point, ordered, transactions |
| Pub/Sub | Event Grid | Event routing, fan-out |
| Streaming / Kafka | Event Hubs | High-throughput telemetry, logs. **Kafka protocol compatible** — apps using `kafka-clients`, Spring Kafka, `confluent-kafka-python` connect by changing bootstrap URL + SASL config only |
| Multi-step orchestration | Durable Functions + Durable Task Scheduler | DTS is the recommended managed backend |
| Low-code workflow | Logic Apps | Integration-heavy, visual designer |
> **docker-compose → Azure mapping:** Map each `detectedServices[].type` to its PaaS equivalent using these tables. **Unmapped services** (SSH daemons, custom sidecars — no managed equivalent): offer a Linux VM (B1s ~$7/mo) as companion at the scaffold gate. Present as option, not forced. Write to `postDeployRecommendations[]`.
## Supporting (always include)
| Service | Purpose |
|---------|---------|
| Log Analytics | Centralized logging |
| Application Insights | Monitoring + APM |
| Managed Identity | Service-to-service auth (zero secrets) |
## Specialized Routing
Check before mapping — delegate to specialized agent if matched:
| Signal | Delegate To |
|--------|-------------|
| Copilot SDK, `@github/copilot-sdk` | `azure-hosted-copilot-sdk` |
| Foundry agent, AI agent deployment | `microsoft-foundry` |
> ⛔ **Non-Azure cloud SDK deps** (AWS/GCP/Firebase) are handled by prereq — the pipeline does NOT reach prepare with cloud SDK findings present. If you see cloud SDK findings here, prereq failed to stop — HALT and do NOT continue architecture planning.
## Non-Azure Terraform Resource Mapping
When `context.json.detectedInfraProvider.terraform` is `"gcp"` or `"aws"`, read existing `.tf` files and map cloud resources to Azure equivalents. These mappings provide architecture signal for service selection — use alongside component detection.
### GCP → Azure
| GCP Terraform Resource | Azure Equivalent | Notes |
|------------------------|-----------------|-------|
| `google_cloud_run_v2_service` | Container Apps | Map scaling, env vars, VPC config |
| `google_sql_database_instance` (POSTGRES) | PostgreSQL Flexible Server | Map tier, backup, maintenance config |
| `google_sql_database_instance` (MYSQL) | MySQL Flexible Server | Map tier, backup config |
| `google_artifact_registry_repository` | Container Registry (ACR) | Basic tier unless geo-replication needed |
| `google_pubsub_topic` / `google_pubsub_subscription` | Service Bus | Map topic/subscription model |
| `google_secret_manager_secret` | On-compute app secret (App Service app settings / CA native secrets) — ⛔ no Key Vault | App-internal secrets only; Azure resource auth uses managed identity |
| `google_firestore_database` | Cosmos DB (NoSQL API) | Map indexes, TTL config |
| `google_cloudfunctions2_function` | Azure Functions | Map triggers, runtime |
| `google_service_account` + `google_project_iam_member` | Managed Identity + RBAC | Map role bindings |
Other GCP resources (storage, redis, compute network, VPC connector, cloud tasks) map 1:1 to their Azure equivalents (Blob Storage, Redis Cache, VNet, Queue Storage).
### AWS → Azure
| AWS Terraform Resource | Azure Equivalent | Notes |
|------------------------|-----------------|-------|
| `aws_ecs_service` / `aws_ecs_task_definition` | Container Apps | Map task def → container config |
| `aws_rds_instance` (postgres/mysql) | PostgreSQL/MySQL Flexible Server | Map instance class → SKU |
| `aws_lambda_function` | Azure Functions | Map runtime, handler, triggers |
| `aws_dynamodb_table` | Cosmos DB (NoSQL API) | Map capacity, indexes |
| `aws_sns_topic` | Service Bus or Event Grid | Map subscriptions |
| `aws_secretsmanager_secret` | On-compute app secret (App Service app settings / CA native secrets) — ⛔ no Key Vault | App-internal secrets only; Azure resource auth uses managed identity |
Other AWS resources (S3, ECR, SQS, ElastiCache) map 1:1 to their Azure equivalents (Blob Storage, ACR, Queue Storage/Service Bus, Redis Cache). SQS → Service Bus if FIFO ordering needed.
@@ -1,46 +0,0 @@
<!-- 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.
# SKU Selection Matrix
Select SKU based on `context.json.intent.budget`. Cross-reference with `intent.scale` for right-sizing.
## Budget Tiers
> ⛔ **Free/shared tiers (App Service F1/D1, Static Web Apps Free) are NOT offered.** Every app requires a managed identity for Azure resource auth, and the free-tier MI sidecar OOMs on Linux; free SWA also lacks CORS/custom-auth config these apps need. The compute floor is **B1 (App Service)** / **Standard (Static Web Apps)**. Never emit F1, D1, or Free.
| Service | cost-optimized | balanced | performance |
|---------|---------------|----------|-------------|
| Container Apps | Consumption (scale-to-zero) | Consumption | Dedicated |
| App Service | B1 (Basic) | B1 / S1 | P1v3 |
| Azure SQL | Basic (5 DTU) | Standard (S0) | Premium / Serverless |
| Cosmos DB | Serverless | Provisioned (400 RU) | Provisioned (autoscale) |
| Storage | Standard_LRS | Standard_GRS | Premium_LRS |
| Static Web Apps | Standard | Standard | Standard |
| Service Bus | Basic | Standard | Premium |
| Redis Cache | Basic C0 | Standard C1 | Premium P1 |
| Functions | Flex Consumption | Flex Consumption | Premium EP1 |
| Log Analytics | PerGB2018 | PerGB2018 | PerGB2018 (dedicated cluster) |
> ⚠️ **Log Analytics `Free` SKU is deprecated.** ARM rejects it with some API versions and retention settings. Always use `PerGB2018` — first 5 GB/month is free anyway.
> ⛔ **Functions floor is Flex Consumption — NOT Consumption or Elastic Premium.** The Consumption and Elastic Premium plans back `AzureWebJobsStorage` with Azure Files, which does **not** support managed-identity connections and therefore **requires `allowSharedKeyAccess` on the storage account** — banned by the managed-identity-only rule (and the KV workaround for the Files connection string is also banned). **Flex Consumption** supports identity-based `AzureWebJobsStorage` (`AzureWebJobsStorage__accountName` + `AzureWebJobsStorage__credential=managedidentity`), so it is the only Functions plan compatible with `allowSharedKeyAccess: false`. Never select Consumption/Elastic Premium — this is the same floor-raise as App Service F1→B1.
## Modifier Rules
- `intent.scale = "Large"` (100K+ users) → bump minimum one tier above cost-optimized
- `intent.budget = "performance"` + `intent.scale = "Small"` → don't over-provision; use balanced tier
- Policy denies a SKU → fall back to next available tier (never below the B1 / SWA Standard floor), add to `assumptions[]`
- Cosmos DB Serverless: max 1K RU/s burst, no geo-replication — only for intermittent/dev workloads. Sustained read-heavy → Provisioned.
## Default Behavior
- Single-component app + cost-optimized → cheapest **viable** SKU at or above the floor (App Service B1, Static Web Apps Standard)
- Simple web app with no DB → ~$13–$20/month (B1 minimum — no $0 tier)
- Always output exact SKU codes: "App Service B1 (Basic)" not "cheapest tier"
## Auto-Cheapest for Fast-Track
⛔ **If `prereq-output.json.fastTrackEligible == true` AND `context.json.intent.budget` is unset, treat as `cost-optimized`.** Fast-track means single-component + no DB + no auth + no Dockerfile. Pick **App Service B1 (Basic)** for dynamic apps that need a runtime (Node.js/Python starter templates), or **Static Web Apps Standard** for pure static HTML/JS/CSS with no server-side runtime. The user still sees the SKU + monthly cost in the scaffold approval gate — they can override via "Edit plan". Do NOT ask the user about budget unless they mention cost first (per [intent-gathering.md](../../references/intent-gathering.md)).
⛔ **There is no free promise — fast-track starts at the B1 / SWA Standard floor.** If the chosen floor SKU is unavailable during deploy prep (no B1 quota — see [sku-quota-validation.md](sku-quota-validation.md) § After Checking), fall back to the **cheapest AVAILABLE** tier at or above the floor. Record an `assumptions[]` note stating WHY the SKU differs, so the approval gate presents the resulting cost.
@@ -1,108 +0,0 @@
<!-- 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.
# SKU Quota Validation Procedure
Pre-deploy quota and offer restriction checks. Read during prepare Step 5 before deploying.
For SKU selection (budget tiers, modifier rules, defaults), see [sku-matrix.md](sku-matrix.md).
## Quota Validation Procedure
> ⛔ **Use `az rest`, NOT `az quota list`.** The `az quota list` CLI extension triggers a full extension metadata scan on startup. If ANY installed extension has a permission error (common: `azure-devops` WinError 5 on Windows), the entire command fails. `az rest` is a built-in command that bypasses extension loading entirely and hits the same REST API. Quota increases are free — you only pay for resources actually used.
> ⛔ **`what-if` does NOT catch App Service quota errors.** `az deployment sub what-if` returns `Succeeded` even when the target SKU has limit=0. The quota rejection only surfaces at actual `az deployment sub create` time. This means the prepare-phase quota check is the ONLY pre-deploy safety net — do not skip or shortcut it.
> ⛔ **Do NOT use `az vm list-usage`, `az appservice list-locations`, or `mcp_azure_mcp_quota`** for quota checks. They return misleading data — see [Anti-Patterns](#anti-patterns) below.
### Region Selection
Build the scan list dynamically — do NOT hardcode a fixed set of regions:
1. **User's preferred region** — read `context.json.azure.region` or `context.json.overrides[]` for a region preference. If stated, scan that region first.
2. **Nearest alternates** — add 3–4 regions geographically close to the user's preferred region from the global pool below.
3. **If no user preference** — default to a well-supported region (e.g., `eastus2`) and scan 4 alternates from the user's likely geography (infer from subscription tenant location or ask).
**Global region pool:** `eastus2`, `eastus`, `westus2`, `centralus`, `westeurope`, `northeurope`, `australiaeast`, `japaneast`, `southeastasia`, `brazilsouth`
> **Agent: adapt shell syntax to detected environment.** PowerShell shown below; use equivalent syntax on bash/zsh (e.g., `for region in eastus eastus2 ...; do ... done`).
> ⛔ **PowerShell `?` in URLs:** When building `az rest` URLs with variable interpolation, PowerShell may strip `?` from `?api-version=`. Always use the URL **inline in double quotes** (as shown below), NOT via a `$url` variable. If you must use a variable, wrap the `?` with a backtick: `` `?api-version= ``.
### Per-Provider Scripts
Use `az rest` for all quota checks. Query BOTH limit AND usage (limit alone is insufficient).
**App Service:** Query quota + usages endpoints per region:
```powershell
$sub = '{subscriptionId}'; $sku = '{sku}'
@('{userRegion}','{alt1}','{alt2}','{alt3}') | ForEach-Object {
$limit = az rest --method get --url "https://management.azure.com/subscriptions/$sub/providers/Microsoft.Web/locations/$_/providers/Microsoft.Quota/quotas/$sku?api-version=2023-02-01" --query "properties.limit.value" -o tsv 2>$null
$used = az rest --method get --url "https://management.azure.com/subscriptions/$sub/providers/Microsoft.Web/locations/$_/providers/Microsoft.Quota/usages/$sku?api-version=2023-02-01" --query "properties.usages.value" -o tsv 2>$null
# limit=0 with used=-1 is the API's "SKU not offered here" sentinel — treat limit<=0 as BLOCKED and clamp negative usage so 0-(-1) does NOT become a false-positive 1.
$ln = if ($limit) { [int]$limit } else { $null }; $un = if ($used) { [int]$used } else { 0 }
$avail = if ($null -eq $ln) { 'unknown' } elseif ($ln -le 0) { 0 } else { $ln - [math]::Max(0, $un) }
Write-Host "$_ : $sku limit=$limit available=$avail"
}
```
**Container Apps:** `/usages` gives usage+limit in one call:
```powershell
az rest --method get --url "https://management.azure.com/subscriptions/$sub/providers/Microsoft.App/locations/{region}/usages?api-version=2024-03-01" --query "value[?name.value=='ManagedEnvironmentCount'].{used:currentValue, limit:limit}" -o json
```
**Static Web Apps:** No `Microsoft.Quota` provider. SWA **Standard** is the floor (Free is never selected — see [sku-matrix.md](sku-matrix.md)); Standard has no per-subscription app cap to check. No quota gate needed.
**Storage** — default limit 250 accounts/region. Rarely exhausted — skip programmatic check unless the plan requires multiple storage accounts.
### Interpret Results
- `available > 0` → AVAILABLE. `available = 0` / `limit <= 0` → BLOCKED (a `limit=0`, `used=-1` response is the API sentinel for "SKU not offered in this region" — the script clamps it so it does not read as available). 404/empty → fallback candidate.
- `az rest` fails → `quotaValidation: { verified: false, method: "unverifiable" }`.
### After Checking
1. Only offer regions with **confirmed** capacity — "try anyway" on zero/unconfirmed quota is a known deploy failure.
1b. **Floor SKU missing in requested region but present elsewhere** → present BOTH, ranked by cost: (a) the floor SKU (B1 / SWA Standard / Flex Consumption) in the nearest confirmed region, (b) the cheapest available SKU IN the requested region. Both show monthly cost + an `assumptions[]` note. User picks — never silently relocate (region may be a data-residency/latency requirement).
2. **Floor SKU unavailable in ALL regions** → step down the fallback ladder to the **cheapest available** tier at or above the floor (never below B1 / SWA Standard / Flex Consumption; let live quota decide the exact SKU):
- No option at the floor → the cheapest available higher tier the app supports. Add an `assumptions[]` note stating why (e.g., "No B1 quota in {checkedRegions}; selected {sku}").
- All tiers exhausted → **HALT**: specify region, switch compute type, request increase at portal, or cancel.
3. Write `prepare-plan.json.quotaValidation`: `{ verified: true, method: "cli", verifiedRegion, verifiedSku, checkedRegions[], failedResources[] }`.
### Offer Restriction Check (Database Services)
> ⛔ `what-if`/`validate` do NOT catch `LocationIsOfferRestricted`. Use capabilities API.
| Provider | API Version |
|----------|-------------|
| PostgreSQL | `2022-12-01` |
| MySQL | `2023-12-30` |
```powershell
$sub = '{subscriptionId}'; $provider = 'Microsoft.DBforPostgreSQL'; $apiVer = '2022-12-01'
@('{userRegion}','{alt1}','{alt2}','{alt3}') | ForEach-Object {
$result = az rest --method get --url "https://management.azure.com/subscriptions/$sub/providers/$provider/locations/$_/capabilities?api-version=$apiVer" --query "value[0].supportedFlexibleServerEditions[0].name" -o tsv 2>$null
if ($result) { Write-Host "$_ : $provider AVAILABLE ($result)" } else { Write-Host "$_ : $provider BLOCKED (offer restricted)" }
}
```
> For MySQL: change `$provider = 'Microsoft.DBforMySQL'` and `$apiVer = '2023-12-30'`.
⛔ JMESPath MUST start with `value[0].`. URL MUST include `/locations/{region}/`. Empty/null response = BLOCKED. Write results to `quotaValidation.offerRestrictions[]`.
> ⛔ **Select the engine version deterministically from the capabilities payload** — match the app's detected version, upgrading only to the nearest compatible release. The payload lists supported versions at `value[0].supportedFlexibleServerEditions[0].supportedServerVersions[].name` (e.g. MySQL: `5.7`, `8.0.21`, `8.4`, `9.5`). Using the **detected DB version passed by the caller** (from `context.json.detectedServices[]`):
> 1. If the **exact detected version** (or its exact patch) is in the supported list → use it.
> 2. Else use the **lowest supported version whose major ≥ the detected major** (detected `5.7`, supported `[5.7, 8.0.21, 8.4, 9.5]` → `8.0.21`). Picking the lowest compatible major — not the newest — avoids the 60+ minute provisioning hangs seen on brand-new majors (e.g. `9.x`) and keeps compatibility with the app's driver/ORM.
> Return it in the quota output's per-service `version` field; the orchestrator copies it to `prepare-plan.json.services[].version` at plan-write (exact patch required — see [prepare-schemas.ts](prepare-schemas.ts) `version`). Record the bump in `assumptions[]` if the detected version was upgraded.
### Anti-Patterns
⛔ `az quota list` (extension failures), `az vm list-usage` (wrong layer), `az appservice list-locations` (ignores quota), `mcp_azure_mcp_quota` (misleading), `what-if`/`validate` (false positives).
## Deploy Gate Re-Validation
If `quotaValidation.verified == false` at deploy gate: re-run Per-Provider Scripts above. Pass → update quotaValidation. Fail → present alternatives. `az rest` fails → warn and proceed.
## Sub-Agent Delegation
When delegating from prepare Step 5, provide: `subscriptionId`, SKU list from `prepare-plan.json.services[].sku`, preferred region + fallbacks, list of managed database services, and this file's content. See [subagent-quota.md](subagent-quota.md) for the template.
@@ -1,56 +0,0 @@
<!-- 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 — Cost Estimation (Step 6)
Estimate monthly costs for planned Azure services using Azure Retail Prices API.
## References to Read Internally
Read BOTH before making any pricing calls:
- [pricing-guide.md](pricing-guide.md) — methodology, free-grant handling, API patterns, troubleshooting
- [pricing-guide-services.md](pricing-guide-services.md) — per-service filter strings, meter names, formulas
## Input (provided by caller)
| Field | Required |
|-------|----------|
| `services[]` with service type and selected SKU per service | YES |
| `region` — deployment region | YES |
| Budget tier (free / balanced / performance) | YES |
## Output
Return JSON (≤500 tokens):
```json
{
"costEstimate": {
"monthlyTotal": 42.50,
"currency": "USD",
"breakdown": [
{ "service": "App Service", "sku": "B1", "monthlyUsd": 13.14, "formula": "retailPrice × 730" },
{ "service": "PostgreSQL Flexible Server", "sku": "Standard_B1ms", "monthlyUsd": 24.82, "formula": "compute + storage" }
],
"freeGrants": [
{ "service": "Container Apps", "grant": "180K vCPU-sec + 360K GiB-sec", "monthlySavings": 0 }
],
"assumptions": [],
"disclaimer": "Estimate based on Azure Retail Prices API. Verify at azure.com/pricing."
}
}
```
## Rules
- ⛔ **Do NOT invoke any other agents or hand off** — no external agent calls of any kind. You are a pricing subagent only. Use direct HTTP to `https://prices.azure.com/api/retail/prices` for price queries (MCP pricing was already attempted inline by the caller).
- ⛔ **No $0 shortcut — compute always has cost** (App Service B1 / SWA Standard / Functions Flex Consumption floor). Price every compute app via live API; note free **grants** (Cosmos free-tier account, Container Apps/Functions grants) but still price paid meters. Do NOT return $0 for a compute app.
- ⛔ `armSkuName` is case-sensitive — use `B1` not `b1`
- ⛔ For services with empty `armSkuName` (Container Apps, Functions Flex Consumption, ACR, Storage, Cosmos): use `filter`/`meterName` matching — a `sku` filter returns `[]`
- ⛔ **Monthly multiplier = each meter's `unitOfMeasure`** (`1 Hour`→×730, `1/Day`→×30 [ACR/registry, SQL DTU], `1 GB/Month`→×GB, `1 Second`→usage) — NEVER blanket ×730; applying ×730 to a `1/Day` meter overstates ~24×
- ⛔ Use direct HTTP to `https://prices.azure.com/api/retail/prices` for all price lookups. Do NOT use `mcp_azure_mcp_pricing` — it was already tried inline and failed.
- ⛔ Never hardcode dollar amounts — always query live prices
- ⛔ If pricing API returns 400: verify `--sku` included in the query
## Token Budget
≤500 tokens for cost estimate report.
@@ -1,74 +0,0 @@
<!-- 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 — Quota Validation (Step 5)
Validate SKU quota and offer restrictions across candidate regions before presenting region choices.
## Critical Rules
- ⛔ **Do NOT invoke any other agents or hand off** — no external agent calls of any kind. You are a quota-check subagent only.
- **Read [`sku-quota-validation.md`](sku-quota-validation.md) before executing ANY quota or offer restriction check.** It contains the per-provider API patterns, anti-patterns to avoid, offer restriction checks for database services, region selection logic, and output schema. All procedures live there — follow them exactly.
## Input (provided by caller)
| Field | Required |
|-------|---------|
| `subscriptionId` | YES |
| SKU list from Step 4 (service type + SKU per service) | YES |
| Restricted-offer services (PostgreSQL, MySQL) | If present in plan |
| **Detected DB version per DB service** (from `context.json.detectedServices[]`, e.g. MySQL `5.7`) | ⛔ REQUIRED if a DB is in the plan — the version-selection algorithm in [`sku-quota-validation.md`](sku-quota-validation.md) needs it. |
## Output
Return JSON (≤500 tokens):
```jsonc
{
"quotaResults": [
{
"region": "{checked region}",
"services": [
{ "service": "{provider}", "sku": "{sku}", "limit": "{from API}", "used": "{from API}", "available": "{limit - used > 0}" },
// if DB service: add "offerRestricted": "{from capabilities API}", "version": "{selected per sku-quota-validation.md — exact patch, never major-only '8.0'}"
],
"allAvailable": "{true only if ALL services in this region have available=true}"
}
// one entry per checked region
],
"recommendedRegion": "{first region where allAvailable=true}",
"checkedRegions": ["{all regions checked}"],
"offerRestrictions": [
// one entry per DB service+region checked — derive from capabilities API response
{ "provider": "{namespace}", "region": "{region}", "restricted": "{true if blocked}", "reason": "{from API or null}" }
],
"offerRestrictionsVerified": "{true only if every DB service from input has ≥1 entry in offerRestrictions[]}"
}
```
⛔ **Caller:** copy each DB service's returned `version` into `prepare-plan.json.services[].version` at plan-write — scaffold needs the exact patch (ARM rejects major-only `'8.0'`).
## Workflow
1. Read [`sku-quota-validation.md`](sku-quota-validation.md)
2. Run the per-provider quota checks for every SKU across all candidate regions
3. If restricted-offer services are in the input, run the offer restriction check for each database service in each candidate region per `sku-quota-validation.md` § Offer Restriction Check
4. Return results per the Output schema above (≤500 tokens)
## Anti-Patterns (from sku-quota-validation.md — repeated here as guardrails)
- ⛔ `az vm list-usage` — wrong provider, misleading data
- ⛔ `az appservice list-locations` — lists locations, NOT quota
- ⛔ `az appservice list-usages` — wrong scope
- ⛔ `mcp_azure_mcp_quota` — unreliable for App Service
- ⛔ `az quota list` — extension loading fails on Windows
- ⛔ Use `az rest` for ALL quota checks
## Rules
- ⛔ Paid ≠ unlimited — B1, Flex Consumption, Serverless all have per-subscription, per-region quotas (F1/Free are never selected — floor is B1)
- ⛔ Check BOTH limit AND current usage — `limit=1, usage=1` means FULL
- ⛔ For PostgreSQL/MySQL: run offer restriction check per `sku-quota-validation.md` § Offer Restriction Check
## Token Budget
≤500 tokens for quota results report.
@@ -1,26 +0,0 @@
<!-- 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.
# Validation Rubric
Run all 4 dimensions before writing `prepare-plan.json`. All must pass — a failure in any dimension triggers the [Error Handling](../instructions.md#error-handling) procedures.
## Dimensions
| Dimension | Pass Criteria |
|-----------|---------------|
| **Goal Alignment** | Every `context.json.intent` field reflected in service/SKU choice. No orphaned or omitted components: every detected deployable component maps to its own compute service or an explicitly approved shared host. Every detected Azure Functions component maps to Azure Functions, even when paired with a Static Web Apps frontend. `overrides[]` honored. |
| **WAF Alignment** | **Cost:** SKU matches budget; alternatives document cost tradeoffs. **Reliability:** Production plans include zone-redundant SKUs, GRS storage. **Security:** Managed identity everywhere; app-internal secrets stored on-compute (⛔ no Key Vault); private endpoints where budget allows. **Ops:** Log Analytics + App Insights included. **Performance:** SKU right-sized to scale — no over/under-provisioning. See [Azure WAF Service Guides](https://learn.microsoft.com/en-us/azure/well-architected/service-guides/) for per-service alignment. |
| **Dependency Completeness** | Every service has its dependencies present (Container Apps → Log Analytics; App Service → App Insights for monitoring; all services → managed identity + RBAC/data-plane role for auth — databases are Entra-only, no connection-string password). Cross-service references consistent (App Insights → Log Analytics workspace). |
| **Deployment Viability** | SKUs exist in target region (validated by quota/region check). No policy-blocked resources. Resource names conform to Azure naming rules. Quota sufficient or flagged with remediation. No free/shared SKUs (compute floor is B1 / SWA Standard / Functions Flex Consumption). |
## Applying
- **During plan creation (steps 3–7):** Use Goal Alignment and WAF Alignment as selection criteria. Use Dependency Completeness as cross-check after mapping.
- **Before writing (step 10):** Run all 4 as validation pass. Deployment Viability catches issues that surface after quota/naming.
- **On failure:** Fix inline via the [Error Handling](../instructions.md#error-handling) procedures. Document tradeoffs (e.g., WAF Reliability vs cost-optimized budget) in `assumptions[]`.
## References
- [Azure Well-Architected Framework](https://learn.microsoft.com/en-us/azure/well-architected/) — pillar definitions and tradeoff guidance
- [WAF Service Guides](https://learn.microsoft.com/en-us/azure/well-architected/service-guides/) — per-service WAF alignment checklists
@@ -1,131 +0,0 @@
---
name: azure-app-onboard-prereq
description: "Assess whether source code is ready to deploy to Azure — the check BEFORE infrastructure work. Evaluates build health, app completeness, dependencies and local services, stack compatibility, and deployment feasibility. Answers questions about what your app needs before it can be deployed — frameworks, dependencies, and configuration. Checks whether dependencies are compatible and identifies deployment blockers and unsupported frameworks. WHEN: \"evaluate my repo\", \"is my app ready to deploy\", \"what does my app need to deploy\", \"what do I need before deploying\", \"does my app need\", \"can I ship this to Azure\", \"scan my repo for issues\", \"is this app deployable\", \"check if my app is ready for Azure\", \"do I need a Dockerfile\", \"what's blocking my deployment\", \"are there any blockers\", \"are my dependencies compatible\", \"does Azure support my framework\", \"what needs to change before deploying\", \"check my app configuration\"."
license: MIT
metadata:
author: Microsoft
version: "1.2.1"
---
<!-- 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.
# Azure App Onboard Prereq — Repository Evaluation
Evaluate a user's repository for build health, app completeness, and Azure deployment feasibility — before infrastructure planning. Produces per-component verdicts (PASS/WARN/FAIL) consumed by downstream phases.
> **Orchestrator relationship:** Called by the deploy agent's [`instructions.md`](../instructions.md) at Step 3. Return control to `instructions.md` (continue at Step 4) after writing artifacts — do NOT invoke downstream phases directly.
Phase 1 of 4 in AppOnboard pipeline. Session: `.copilot-azure/sessions/{session-id}/`. Reads `context.json`. Writes `components[]`, `repo{}`, `detectedInfra[]`. Produces `prereq-output.json`. Schema: [`prereq-schemas.ts`](references/prereq-schemas.ts) — `PrereqOutput`, `BuildRequirements`. Direct entry supported.
## When NOT to Use
| Signal | Redirect |
|--------|----------|
| Validate infrastructure (Bicep/TF/azure.yaml) | **azure-validate** |
| Generate IaC | **azure-prepare** |
| End-to-end idea-to-production | **azure-app-onboard** |
| Run `azd up` or deploy | **azure-deploy** |
## Rules
> ⛔ **ABSOLUTE PROHIBITION — `npm install`, `npm test`, `npx jest`, `pytest`, and ALL install/build/test commands are NEVER allowed.**
> Under NO circumstances may you run `npm install`, `npm test`, `npx jest`, `pip install`, `pytest`, `dotnet build`, `dotnet restore`, `dotnet test`, `go mod download`, `cargo build`, or ANY package-manager install, build, or test command during the prereq phase. Do NOT run test suites to verify code — check for test config files statically instead. The prereq phase is read-only evaluation + static-only verification.
> **ONLY exception — two sanctioned contexts, both consent-gated:** (a) code the agent **modified** during migration/remediation (see [remediation-protocol.md](references/remediation-protocol.md) step 6), or (b) code the agent **wrote** from scratch on the zero-code path (see [zero-code-path.md](references/zero-code-path.md)). In either case, install/build/test runs ONLY via the user-confirmed build-validation gate ([build-check.md](references/build-check.md) Step 3), after the user answers that specific per-command consent prompt. General prior consent never counts.
1. ⛔ **Full pipeline (Steps 1–8), no exceptions.** All prompts → Step 1 directly. Answer specific questions AS PART OF findings (Step 5), not before.
2. ⛔ **No sub-agents for evaluation.** 3-axis evaluation is inline. **Exception**: zero-code-path scaffolding (Step 2).
3. Code/destructive modifications require `ask_user`. Max 3 questions before results. Direct entry: don't repeat orchestrator's intent questions.
## MCP Tools
| Tool | Purpose |
|------|---------|
| `mcp_azure_mcp_get_azure_bestpractices` | Validate detected stack patterns against Azure best practices |
| `mcp_azure_mcp_extension_cli_install` | Check/install required CLI tools (az, azd, func) |
## Workflow
### Step 1: Session Check
**Orchestrator entry:** Session exists — read `context.json`, proceed to Step 2.
**Direct entry:** Check `.copilot-azure/sessions/active-session.json`:
- **Exists** → ⛔ read [session-protocol.md](references/session-protocol.md) for resume/fresh gate. Do NOT proceed until user answers.
- **Missing** → create session: generate UUID, `New-Item -ItemType Directory -Path ".copilot-azure/sessions/{uuid}" -Force`, write `context.json` + `active-session.json` via `create` tool.
Then: `az account show` → merge `{id, name, tenantId}` into `context.json.azure`. ⛔ Session MUST exist on disk before any scanning.
### Step 2: Scan Workspace
Scan for project files. Detect components, `repo{}`, `detectedInfra[]`, `detectedServices[]`. Classify Terraform providers. Check CLI availability. Stack detection conflicts: user explicit statement wins (write to `context.json`, mark scan as override); scan-only → confirm with user; multiple stacks → show all and ask (see [component-mapping.md](references/component-mapping.md)); no code → [zero-code-path.md](references/zero-code-path.md).
> ⛔ **Preserve Azure Functions service roots.** A directory with `host.json` plus an Azure Functions SDK or
> worker signal (`@azure/functions`, `Microsoft.NET.Sdk.Functions`, Functions Python decorators, or a
> `FUNCTIONS_WORKER_RUNTIME` setting) is an Azure Functions component. Record its exact path and identify its
> framework as Azure Functions in both `context.json.components[]` and `prereq-output.json.components[]`.
> Do not classify HTTP-triggered Functions as a generic REST API. When a SPA and Functions root both exist,
> record two components even if the frontend calls the backend through `/api`.
> If no project files, no Dockerfile, AND no index.html → ⛔ read [zero-code-path.md](references/zero-code-path.md).
> ⛔ **Cloud SDK early gate.** Grep for `aws-sdk|@aws-sdk|boto3|google-cloud|@google-cloud|firebase`. If functional deps found → read [cloud-sdk-migration.md](references/cloud-sdk-migration.md), then `ask_user`: **"Redirect to Azure Cloud Migrate"** (set `routeToSkill: "azure-cloud-migrate"`) · **"Continue evaluation anyway"** (finish readiness eval + SDK→Azure mapping, then STOP at Step 8 — no plan until the deps are swapped) · **"Cancel"**.
### Step 3: Per-Component Evaluation
| Sub-step | Action | Reference |
|----------|--------|-----------|
| 3.1 | **Build check** | ⛔ **You MUST read [build-check.md](references/build-check.md)** |
| 3.2 | **Completeness check** | ⛔ **You MUST read [completeness-check.md](references/completeness-check.md)** |
| 3.3 | **Deployability check** | ⛔ **You MUST read [deployability-check.md](references/deployability-check.md)** |
| 3.3a | **Component mapping** (conditional) | Read [component-mapping.md](references/component-mapping.md) ONLY IF >1 project manifest found (monorepo) |
Populate `buildRequirements` per component after evaluation. Verdict propagation, tier rules, and f1Viable aggregation are in [readiness-gate.md](references/readiness-gate.md) and the individual check references.
### Step 4: Write Artifacts + Readiness Gate
⛔ Verify `context.json` exists on disk. Read [readiness-gate.md](references/readiness-gate.md) (verdicts, tiers, batch-then-approve, fast-track) then [prereq-artifacts.md](references/prereq-artifacts.md) (write procedures, schemas).
### Step 5: Present Findings
Per [readiness-gate.md § Present Findings](references/readiness-gate.md) — show verdicts grouped by severity before proceeding.
### Step 6: Remediation (conditional)
⛔ **You MUST read [remediation-protocol.md](references/remediation-protocol.md)** IF any ❌ FAIL verdict, 🔧 Recommended Fix, or ⚠️ WARN with `fixPhase: "prereq"` exists. Contains remediation loop, static verification, re-eval mandate, post-remediation artifact updates, and the build-validation consent gate. If all verdicts are ✅ PASS or ⚠️ WARN without `fixPhase: "prereq"`, skip to Step 7.
### Step 7: Write Final State
`completedPhases` already has `"prereq"` + `currentPhase: null` (from Step 4). Then:
> ⛔ **Write `lastScanCommit`.** Run `git rev-parse HEAD` and store the full 40-character SHA as `context.json.repo.lastScanCommit`. Required — staleness guard in Step 1 compares to HEAD on resume to detect changes.
### Step 8: Route
⛔ **Mandatory — do NOT skip this step.**
> **Routing fields:** All routing writes `routeToSkill` and `routeReason` to `context.json`.
> **Post-remediation context:** If Step 6 ran, lead the routing prompt with: "Remediation complete — {N} issues fixed, your app is now {overallHealth}."
> ⛔ **Evaluate rows top to bottom — first match wins.**
| # | Condition | Action |
|---|-----------|--------|
| 1 | `routeToSkill` set (any entry) | `ask_user`: "Redirect to {routeToSkill}" / "Not now". ⛔ Pipeline stops — do NOT proceed to architecture planning. |
| 2 | `cloudSdkFindings[]` non-empty (user chose "Continue evaluation anyway") | Present the cloud-SDK → Azure swap mapping as 🔶 blockers, then `ask_user` with this exact prompt: **"🔶 Cloud SDK migration required — these dependencies must be swapped before this app can deploy to Azure. (Redirect to azure-cloud-migrate / Stop — swap manually and re-run)"** — Redirect sets `routeToSkill: "azure-cloud-migrate"`, Stop halts. ⛔ Pipeline stops — do NOT proceed to architecture planning, and do NOT offer a "continue to prepare" option; the app can't deploy until the deps are swapped. |
| 3 | Orchestrator + no `routeToSkill` | Tell the user: "✅ Your app has been evaluated and is ready — let's plan your Azure deployment." Then return to the deploy agent's [`instructions.md`](../instructions.md) and continue at Step 4 (Gather intent). ⛔ Do NOT stop, do NOT wait for user input, do NOT narrate internal handoffs. The user already consented to the full pipeline at scope triage. |
| 4 | Direct + ready/readyWithCaveats + no Azure infra | `ask_user`: "Deploy to Azure (full pipeline)" → invoke `azure-app-onboard` / "Not now" |
| 5 | Direct + ready/readyWithCaveats + existing Azure infra | `ask_user`: "Start fresh" → invoke `azure-app-onboard` / "Use existing infra" → invoke `azure-prepare` / "Not now" |
| 6 | Direct + blocked | Report blocker summary + "Fix and re-run." |
Severity tiers (🛑🔶❌🔧⚠️✅) are defined in [readiness-gate.md](references/readiness-gate.md).
## Outputs
| Artifact | Location | Consumer |
|----------|----------|----------|
| Session context | `context.json` → `components[]`, `repo{}`, `detectedInfra[]`, `detectedServices[]` | All downstream phases |
| Prereq output | `prereq-output.json` | prepare phase (via `azure-app-onboard`) |
| Readiness report | `.copilot-azure/sessions/{uuid}/readiness-report.md` | User (offline reference) |
@@ -1,97 +0,0 @@
<!-- 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.
# Build Check
> ⛔ **No build/install/test commands during this check.** Use static analysis only.
Detect the project's language/framework stack and assess build health. Static detection (manifest reading) is **default**. Build execution is optional, requires user confirmation.
> **Dynamic detection:** Agent determines build commands from the project manifest (e.g., `package.json` `scripts` for the project-specific build script name). Table below is guidance, not a fixed lookup.
## Step 1: Detect Stack
Scan the workspace for project files to determine the technology stack.
| File | Language/Framework |
|------|-------------------|
| `package.json` | Node.js |
| `package.json` + `tsconfig.json` | TypeScript |
| `requirements.txt` / `pyproject.toml` | Python |
| `*.csproj` / `*.sln` | .NET |
| `pom.xml` | Java (Maven) |
| `build.gradle` / `build.gradle.kts` | Java/Kotlin (Gradle) |
| `go.mod` | Go |
| `Cargo.toml` | Rust |
| `Gemfile` | Ruby |
| `composer.json` | PHP |
| `docker-compose.yml` / `compose.yml` | (dependency source — parsed during deployability check) |
| `build.gradle` + `com.google.cloud.tools.jib` | Java (Jib) |
**Modern package manager lockfiles:** `uv.lock` → uv, `bun.lock` / `bun.lockb` → bun.
> ⚠️ If **no project file** is found, check for a `Dockerfile`. If a Dockerfile exists, note container-dependent build in findings.
### Multi-Project Detection
Search recursively for project manifests, skip `node_modules`/`.git`/`dist`/`build`/`vendor`/`.venv`/`__pycache__`/`.terraform`/`bin`/`obj`. Each directory with a project file = one component.
## Step 2: Static Detection (Default)
Read project manifests to infer build health **without executing any commands**.
**Check for:** missing dependencies (imports not in manifest), version conflicts, obvious misconfigurations (missing `main`/`start` script), lock file presence.
> **Package manager detection:** `package-lock.json` → npm, `yarn.lock` → yarn, `pnpm-lock.yaml` → pnpm. Default to npm if no lockfile.
### Import → Manifest Cross-Check
Scan source files for imports not in the dependency manifest. This catches pre-existing repo bugs that cause build failures on Azure.
**Node.js/TypeScript:** Scan `*.ts`, `*.tsx`, `*.js`, `*.jsx`, `*.mjs` for `import` / `require` statements. Extract package name (first path segment, `@scope/name` for scoped). Check against `dependencies` + `devDependencies`. Skip Node.js built-ins and relative paths.
**Python:** Scan `*.py` for `import {pkg}` / `from {pkg} import`, cross-reference against `requirements.txt` / `pyproject.toml`. Skip stdlib.
| File location | Severity |
|---|---|
| Build-time config (`next.config.*`, `webpack.config.*`, `vite.config.*`, `babel.config.*`, `postcss.config.*`, `tailwind.config.*`) | ❌ FAIL — build crashes at config load |
| Entry point / source — **package absent from manifest** | ❌ FAIL — `MODULE_NOT_FOUND` at runtime |
| Entry point / source — **version mismatch** | 🔧 Recommended Fix |
| Test files only | ⚠️ WARN |
**Scope:** Always scan ALL config files. For `src/`/`app/`/`pages/`/`lib/`, limit to 20 source files per component. Do NOT scan `node_modules/`, `.venv/`, `dist/`, `build/`.
**When ❌ FAIL or 🔧 Fix found:** Include in batch-then-approve: "Add `{package}` to `package.json` dependencies."
| Outcome | Verdict |
|---------|---------|
| No issues found | ✅ PASS |
| Warnings in manifest | ⚠️ WARN |
| Obvious errors | ❌ FAIL |
| No build system detected | ⚠️ WARN |
| Only Dockerfile found | ⚠️ WARN |
**Dependency vintage check:** ALL pinned deps 5+ years old AND ecosystem has known breaking changes (e.g., `werkzeug.contrib.*` removed, `flask.ext.*` removed, `itsdangerous<1.0` API changed) → ❌ FAIL. Grep for imports from removed modules.
**Transitive dependency check (post-migration):** After upgrading dependencies during 🔶 Major Migration, run install **through the build-validation gate (Step 3) — the migration-intent choice does NOT authorize install; the user must answer that specific per-command consent prompt first** to catch transitive deps. Also run entry-point import to catch import-time validation errors (e.g., WTForms `Email()` requires `email-validator`).
**SKU sizing signal:** Check `f1Viable` per [dependency-compatibility.md § SKU Sizing Signals](dependency-compatibility.md). F1 is never selected (floor is B1); a vintage ❌ FAIL requiring 🔶 Major Migration (>5 files) sets `f1Viable: false` as a signal to size **up** from B1.
## Step 3: Build Execution (Optional — User-Confirmed)
⛔ **Build-validation gate.** Before running ANY install/build/test command, ask via `ask_user`: "I'd like to run `{command}` to verify the build. Run it? (Yes / Skip)". A general prior consent (e.g., "fix my issues", "yes", "go ahead", "fix them") does NOT constitute consent — the user must answer THIS specific question with the exact command named. If they say Skip, continue with static-only verdicts.
Read manifest to determine actual build script. Install deps first. Capture output. Timeout 5 min.
| Outcome | Verdict |
|---------|---------|
| Exit code 0 | ✅ PASS |
| Succeeds with warnings | ⚠️ WARN |
| Exit code ≠ 0 | ❌ FAIL |
| Timeout >5 min | ⚠️ WARN |
## Native Module Detection
See [dependency-compatibility.md § Native Module Detection](dependency-compatibility.md) for the canonical detection procedure, edge cases, and package table. Results go to `buildRequirements.hasNativeModules`.
> ⛔ **Prebuild-install exception:** Packages with `prebuild-install` (without `node-gyp`) in lockfile use prebuilt binaries → `hasNativeModules: false`. Key examples: `better-sqlite3` v12+ = prebuilt, `sharp` v0.33+ = prebuilt, `bcrypt` = always native, `canvas` = always native. Check the lockfile — do not rely on package name alone.
@@ -1,32 +0,0 @@
<!-- 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.
# Non-Azure Cloud Service Dependencies
> Detected during Step 2 manifest scan. The `ask_user` redirect gate is handled in instructions.md Step 2 — this file defines the classification rules.
Functional cloud SDK deps are 🔶 `CLOUD_SDK_MIGRATION`. Populate `prereq-output.json.cloudSdkFindings[]`.
| Found Dependency | Azure Equivalent |
|-----------------|-----------------|
| AWS DynamoDB (`AWSSDK.DynamoDBv2`, `@aws-sdk/client-dynamodb`), GCP Firestore | Cosmos DB (NoSQL API) |
| AWS Cognito (`AWSSDK.CognitoIdentityProvider`, `amazon-cognito-identity-js`), Firebase auth (`firebase-admin`) | Entra ID / Entra External ID |
| AWS S3, GCP Cloud Storage (`@google-cloud/storage`), MinIO | Azure Blob Storage |
| AWS Lambda, GCP Cloud Functions (handler signatures) | Azure Functions (rewrite handlers) |
| AWS SQS (`@aws-sdk/client-sqs`, `AWSSDK.SQS`), GCP Cloud Tasks (`google-cloud-tasks`) | Queue Storage / Service Bus |
| AWS SNS (`@aws-sdk/client-sns`, `AWSSDK.SimpleNotificationService`), GCP Pub/Sub (`google-cloud-pubsub`) | Service Bus / Event Grid |
| Firebase (`firebase`, `firebase-admin`) full stack | Entra ID + Cosmos DB + Functions |
> **Observability carve-out:** Observability deps are ⚠️ WARN (app runs without them), NOT 🔶. Use this table to distinguish:
>
> | Package | Classification | Why |
> |---------|---------------|-----|
> | `@google-cloud/opentelemetry-*` | ⚠️ WARN | Telemetry — app works without it |
> | `@google-cloud/logging` | ⚠️ WARN | Logging — app works without it |
> | `@google-cloud/monitoring` | ⚠️ WARN | Monitoring — app works without it |
> | `aws-xray-sdk`, `aws-rum-web` | ⚠️ WARN | Tracing/RUM — app works without it |
> | `google-cloud-tasks` | 🔶 CLOUD_SDK_MIGRATION | Functional — app breaks without it |
> | `google-cloud-pubsub` | 🔶 CLOUD_SDK_MIGRATION | Functional — app breaks without it |
> | `google-cloud-storage` | 🔶 CLOUD_SDK_MIGRATION | Functional — app breaks without it |
> | `@aws-sdk/client-dynamodb`, `boto3` (DynamoDB) | 🔶 CLOUD_SDK_MIGRATION | Functional — app breaks without it |
> | `@aws-sdk/client-sqs`, `@aws-sdk/client-sns` | 🔶 CLOUD_SDK_MIGRATION | Functional — app breaks without it |
@@ -1,99 +0,0 @@
<!-- 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.
# Completeness Check
> ⛔ **No build/install/test commands during this check.** Use static analysis only.
Verify repository has required components for a deployable app.
### 1. Entry Point
Verify main/index file exists for each component.
| Stack | Expected Entry Point |
|-------|---------------------|
| Node.js | `main` or `start` script in `package.json` |
| Python | `app.py`, `main.py`, `manage.py`, or entry in `pyproject.toml` |
| .NET | `Program.cs` or `Startup.cs` with `<OutputType>Exe</OutputType>` |
| Java | Class with `public static void main` or `@SpringBootApplication` |
| Go | `main.go` in `package main` |
| Static | `index.html` at root or in output folder |
| Outcome | Verdict |
|---------|---------|
| Entry point found and file exists on disk | ✅ PASS |
| Ambiguous (multiple candidates) | ⚠️ WARN |
| No entry point | ❌ FAIL |
| Entry point declared but file missing | ❌ FAIL — `MODULE_NOT_FOUND` |
### 2. Dependency Manifest
| Outcome | Verdict |
|---------|---------|
| Manifest found with dependencies | ✅ PASS |
| Manifest exists but empty deps | ⚠️ WARN |
| No manifest found | ❌ FAIL (unless static site) |
> ⛔ **Oryx reads manifests ONLY at repo root.** Subdirectory manifests → ⚠️ WARN (🔧 Fix): create root wrapper (Python: `-r {subdir}/requirements.txt`, Node: workspaces). Add to batch-then-approve.
### 3. Configuration
| Outcome | Verdict |
|---------|---------|
| Config properly externalized | ✅ PASS |
| Hardcoded values but no secrets | ⚠️ WARN |
| Hardcoded secrets in source (no env var fallback) | ❌ FAIL |
| Env var + hardcoded fallback default | ⚠️ WARN |
| `.env` with placeholder values + runtime validation | ✅ PASS |
> **Compose file credentials:** Literal `*PASSWORD=<value>` with NO `${VAR}` → ❌ FAIL. `${VAR:-default}` → ⚠️ WARN. `${VAR}` only → ✅ PASS. ⛔ Do NOT downgrade based on filename ("dev-only") — treat every compose file as potentially production-bound.
### 4. Documentation
README with build/run instructions → ✅ PASS. Sparse/no README → ⚠️ WARN.
### 5. Listening Port
Web apps must bind a port. Detect via `app.listen`, `PORT` env var, framework port config, `WebApplication.CreateBuilder()` (implicit 5000/5001 .NET 6+).
| Outcome | Verdict |
|---------|---------|
| Port binding detected | ✅ PASS |
| Non-web (CLI, worker, function) | ✅ PASS — N/A |
| Web app with no port binding | ❌ FAIL |
### 6. Static Asset Integrity
Parse `href`/`src` from HTML tags. Check relative to HTML file directory. Ignore external URLs.
| Outcome | Verdict |
|---------|---------|
| All referenced assets found | ✅ PASS |
| Broken favicon only | ⚠️ WARN |
| Broken `<link>`, `<script>`, `<img>` reference | 🔧 Recommended Fix — set `fixPhase: "prereq"` |
### 7. Container Readiness
| Outcome | Verdict |
|---------|---------|
| Dockerfile + .dockerignore present | ✅ PASS |
| Dockerfile, no .dockerignore | ⚠️ WARN |
| Multi-process container detected | ⚠️ WARN |
| `CMD.*uv run` or `CMD.*poetry run` | ⚠️ WARN — dep sync at startup fails as non-root. **fix:** "Replace with direct command" **fixPhase:** `prereq` |
| `EXPOSE` port mismatch with app | 🔧 Fix — mismatch causes 502. Extract to `buildRequirements.exposedPort` |
### Stack-Specific Checks
Verify these patterns. Assess severity with tier definitions from [readiness-gate.md](readiness-gate.md) — only ❌ FAIL if causes deploy failure or startup crash.
- **Node.js:** `engines` field, session store type (MemoryStore = ephemeral), health endpoint
- **Express:** trust proxy when secure cookies are used behind reverse proxy
- **Any web app:** health endpoint (`/health`, `/healthz`), README documentation
- **Static sites:** health endpoint is N/A (responds 200 on `/`)
> ⛔ Default these to **⚠️ WARN / `fixPhase: "postdeploy"`** — an app that deploys and runs (missing trust proxy, README, in-memory sessions) is **not** `blocked`. Escalate to ❌ FAIL / `prereq` only when the case actually breaks THIS deploy: `engines` when the app needs a runtime the platform default won't provide, or a health endpoint when a probe is wired to a route the app lacks.
> **Do not short-circuit.** Iterate ALL sub-checks (1–7 + stack-specific) per component.
Severity tiers are defined in [readiness-gate.md](readiness-gate.md). Use the verdict tables in checks 1–7 above for deterministic outcomes. For judgment calls, assess based on deployment impact to this specific app.
@@ -1,100 +0,0 @@
<!-- 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.
Component-to-Azure mapping and existing infrastructure detection. Part of the [deployability check](deployability-check.md).
## Step 1: Component Mapping Feasibility
Determine if each detected component maps to a known Azure service.
| Component Type | Mappable Azure Services |
|----------------|------------------------|
| SPA / Static Site | Static Web Apps, Blob + CDN |
| Existing Azure Functions project | Azure Functions |
| SSR Web App | Container Apps, App Service |
| REST / GraphQL API | Container Apps, App Service, Functions |
| Background Worker | Container Apps, Functions |
| Scheduled Task | Functions (Timer Trigger) |
| Event Processor | Functions, Container Apps |
| CLI Tool | Not directly deployable — flag |
| Outcome | Verdict |
|---------|---------|
| All components map to Azure services | ✅ PASS |
| Some components need clarification | ⚠️ WARN |
| Unknown component type, can't map | ⚠️ WARN — ask user for context |
| Component is fundamentally incompatible | ❌ FAIL |
## Preserve Existing Compute Models
The table above describes what can host newly designed components. It does not permit replatforming an
existing component whose framework already selects a compute model.
- `host.json` plus an Azure Functions SDK or worker signal identifies an Azure Functions project. Map it to
Azure Functions even when all of its triggers are HTTP triggers.
- Keep separate service roots as separate components. A SPA root and a Functions root produce a frontend
component and a backend component.
- Static Web Apps may host the frontend, but an existing Functions root must not become an SWA-managed API.
Do not move or copy its source under an SWA API directory and do not change its runtime or authentication
provider to `azureStaticWebApps`.
- A detached Static Web App plus Function App with explicit CORS is the default. Linking the separately
deployed Function App as an SWA backend requires explicit user approval and must be recorded in
`context.json.overrides[]`.
## Step 2: Existing Infrastructure Check
Check if the repo already has Azure infrastructure or deployment config.
| Found | Implication |
|-------|-------------|
| `azure.yaml` | AZD project — may only need update |
| `infra/*.bicep` | Bicep IaC exists |
| `infra/*.tf` or `*.tf` | Terraform IaC exists — classify provider (see below) |
| `Dockerfile` | Containerization ready |
| `.github/workflows/` | CI/CD configured |
| `azure-pipelines.yml` | Azure DevOps CI/CD |
| `docker-compose.yml` | Multi-container setup — parse for service dependencies |
| None of the above | Greenfield — full prep needed |
Record what exists; this feeds into recipe selection during azure-prepare.
### Terraform Provider Classification
When `.tf` files are detected, read `versions.tf`, `provider.tf`, or `main.tf` to identify `required_providers`. Classify and write to `context.json.detectedInfraProvider.terraform`:
| `required_providers` contains | Classification | Scaffold behavior |
|-------------------------------|---------------|-------------------|
| `hashicorp/azurerm` only | `"azure"` | Halt — existing Azure IaC |
| `hashicorp/google` or `hashicorp/google-beta` (no `azurerm`) | `"gcp"` | Generate Azure TF alongside |
| `hashicorp/aws` (no `azurerm`) | `"aws"` | Generate Azure TF alongside |
| Multiple cloud providers including `azurerm` | `"multi"` | Halt — Azure IaC already present |
| Multiple cloud providers without `azurerm` | `"multi"` | Generate Azure TF alongside |
| No provider block found or only non-cloud providers | `"unknown"` | Halt — ask user to clarify |
Also check for `azure.yaml` coexistence: if BOTH `azure.yaml` AND non-Azure `.tf` exist → `azure.yaml` takes priority → route to `azure-deploy`, not AppOnboard scaffold.
### Compose Service Dependency Extraction
When `docker-compose.yml` or `compose.yml` is found, parse `services:` for infrastructure dependencies. Map known images to `detectedServices[]` entries (`DetectedService` in `session-schemas.ts`):
| Image pattern | `type` | Version source |
|--------------|--------|----------------|
| `postgres:*` | `postgresql` | Image tag (e.g., `postgres:16` → `"16"`) |
| `redis:*` / `redis/redis-stack:*` | `redis` | Image tag |
| `*kafka*` (bitnami, confluent, etc.) | `kafka` | Image tag |
| `elasticsearch:*` / `opensearchproject/*` | `elasticsearch` | Image tag |
| `mariadb:*` | `mariadb` | Image tag |
| `mongo:*` | `mongodb` | Image tag |
| `rabbitmq:*` | `rabbitmq` | Image tag |
| `minio/*` | `minio` | Image tag |
| `mysql:*` | `mysql` | Image tag |
Set `source: "compose"` on each. If no tag or `latest`, omit `version`. Skip the app's own service entries (services with `build:` context pointing to the repo).
### Compose Hostname Detection
After extracting compose services, grep app source code and config files for compose service names used as hostnames. Compose DNS names (e.g., `postgres`, `redis`, `api`) resolve inside Docker networks but NOT on Azure PaaS.
**Detection:** For each extracted service name, search app config files (`.env`, `config.*`, `application.*`, `settings.*`) and source code for patterns like `host=<service_name>`, `<service_name>:<port>`, `://<service_name>:`, or `<service_name>.` used as a hostname. Common examples: `host=postgres`, `redis://redis:6379`, `PGHOST=db`.
**Verdict:** ⚠️ WARN — `id: W-COMPOSE-HOSTNAME`. "App references Docker Compose service name `{name}` as a hostname. On Azure, use the managed service endpoint (set via environment variable) instead." Include in `postDeployRecommendations[]`: `{ "title": "Replace compose hostnames with Azure endpoints", "reason": "Compose DNS names don't resolve on Azure PaaS", "effort": "low", "services": ["{mapped Azure service}"] }`.
@@ -1,143 +0,0 @@
<!-- 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.
Dependency compatibility checks for Azure. Part of the [deployability check](deployability-check.md).
## EOL / Unsupported Runtimes
| Verdict | Condition |
|---------|----------|
| 🔶 Major Migration | .NET Framework 4.x, ASP.NET Core 2.1, Python 2.x |
| ❌ FAIL or 🔶 | Node.js < 18, Java < 11 — config-only upgrade → ❌ FAIL (fixable), API changes needed → 🔶 |
> **Quick test:** ONE config value change, no import changes? → ❌ FAIL. Otherwise → 🔶.
**Ecosystem-era check (Python):** ALL pinned deps pre-2018, no Python 3.10+ wheels → ❌ FAIL. Signals: `flask_script`, `werkzeug<1.0`, `itsdangerous<1.0`, imports from `werkzeug.contrib.*` / `flask.ext.*`.
## EOL / Unmaintained Frameworks
| Verdict | Condition |
|---------|-----------|
| 🔶 Major Migration | Flask < 2.0, Django < 3.2, Express < 4.0, Rails < 6.0, Spring Boot < 2.7 |
| ⚠️ WARN | React < 16, Angular < 14, Next.js < 13 |
## Archived / Abandoned Repositories
| Signal | Verdict |
|--------|--------|
| `archived: true` + EOL stack | 🔶 Major Migration |
| `archived: true` (current runtime) | ⚠️ WARN |
| README "deprecated"/"unmaintained" + EOL | 🔶 Major Migration |
| README "deprecated"/"unmaintained" (current) | ⚠️ WARN |
> **Remediation scope:** Fixing all blockers requires major upgrade OR >5 files → 🔶 Major Migration.
## Intentionally Vulnerable Applications
Detect via **code structure first**, metadata second. These apps are designed to be exploited — vulnerability IS the product.
**Code signals (check first):**
- Directory `vulnerabilities/` with subdirs like `sqli/`, `xss/`, `csrf/`, `fi/` (file inclusion)
- Security-level config toggling vulnerability severity (`default_security_level`, `security.level`, `difficulty`)
- Source files with intentionally unsanitized `$_GET`/`$_POST`/`$_REQUEST` passed to SQL queries, shell commands (`exec`, `shell_exec`, `system `), or `eval`/`include` -- that are systematic across 3+ files (not a single bug)
- `hackable/`/`exploit/` directories, or files with names like `command_injection.php`, `brute_force.php`
**Metadata signals (secondary check):**
- Project/package description or repo About containing: "deliberately vulnerable", "intentionally insecure", "vulnerable by design", "security training", "penetration testing", "do not deploy to production"
- README/License warning against internet-facing deployment
**Verdict:** ≥2 code signals OR 1 code + 1 metadata → 🛑 HALT. Single metadata only → ⚠️ WARN (could be a disclosure).
> ⛔ **🛑 HALT is a verdict, not an exit.** On 🛑 HALT your NEXT action MUST be the Step-4 write — persist all 3 artifacts (`prereq-output.json`, `context.json`, `readiness-report.md`) with `overallHealth: "blocked"` via the `create` tool, then read them back, BEFORE printing any halt summary to the user. A halt message with no `prereq-output.json` on disk is a failure — the deterministic `blocked` verdict must be persisted first, because the halt message can end the turn.
## Non-Azure Cloud SDK Dependencies
Functional cloud SDK deps → 🔶 blockers; classification and observability carve-out in [cloud-sdk-migration.md](cloud-sdk-migration.md). The redirect gate (instructions.md Step 2) and the deploy-blocking stop (instructions.md Step 8 Row 2) own all routing — no `routeToSkill` decision happens here.
## Platform-Specific Dependencies
| Dependency Type | Verdict |
|----------------|---------|
| Native OS binaries (`.so`, `.dll`) | ⚠️ WARN |
| Native Node.js addons on free/low-tier SKUs | ⚠️ WARN |
| GPU-required libraries (CUDA) | ⚠️ WARN |
| Local file system writes, file-based DBs | ⚠️ WARN — ephemeral on PaaS |
| BuildKit Dockerfile syntax (`--mount`, `# syntax=`) | ⚠️ WARN `W-BUILDKIT` — set `buildRequirements.hasBuildKitSyntax: true`. **ACR `az acr build` does NOT support BuildKit** — scaffold must generate `Dockerfile.azure`. **fix:** "Generate ACR-compatible Dockerfile.azure" **fixPhase:** `scaffold` |
| Jib container build (no Dockerfile) | ⚠️ WARN — note Jib path for scaffold |
| Redis client without TLS config | ⚠️ WARN `W-REDIS-TLS` — **fix:** "Add TLS config" **fixPhase:** `prereq`. **Config key registration:** if the app uses a config library that requires keys to be pre-registered before env var override (Go/Viper `Unmarshal()`, Spring `@ConfigurationProperties`), the config file must also declare the TLS key (e.g., add `tlsEnabled: false` to YAML) — otherwise the env var is silently ignored. Detection: grep for `viper.Unmarshal`, `mapstructure`, `@ConfigurationProperties`. |
| PostgreSQL client with SSL disabled | ⚠️ WARN `W-PG-SSL` — **fix:** "Set SSL mode env var" **fixPhase:** `scaffold` |
| MySQL client without TLS config | ⚠️ WARN `W-MYSQL-SSL` — **fix:** "Enable client TLS for Azure MySQL" **fixPhase:** `prereq`. Most MySQL drivers/ORMs need an in-code SSL option (no SSL env var like Postgres has), so this is a client-config change → prereq remediation batch (like `W-REDIS-TLS`), not IaC-only scaffold. Detection: MySQL in the plan (`mysql:*` in compose, or a MySQL driver/ORM — e.g. `mysql2`, `sequelize` dialect mysql, `typeorm`, `prisma`, `knex`) with no SSL/TLS option in the client config. |
| Go Viper without env key replacer | ⚠️ WARN `W-VIPER-ENV` — **fix:** "Add SetEnvKeyReplacer call" **fixPhase:** `prereq` |
| Licensed/proprietary SDKs | ⚠️ WARN |
## Hardcoded Localhost URLs
| Pattern | Verdict | ID | fixPhase |
|---|---|---|---|
| `localhost`, `127.0.0.1`, `0.0.0.0` in API base URLs, CORS origins, webhook URLs, service discovery | ⚠️ WARN | `W-LOCALHOST-URL` | `scaffold` |
> **Exclude:** database connection strings, dev-only files, test files.
## Dockerfile Analysis
| Pattern | Verdict | ID | fixPhase |
|---|---|---|---|
| `CMD.*uv run` / `ENTRYPOINT.*uv run` | ⚠️ WARN | `W-UV-RUN` | `prereq` |
| `CMD.*poetry run` / `ENTRYPOINT.*poetry run` | ⚠️ WARN | `W-POETRY-RUN` | `prereq` |
> Also check `docker-compose.yml` `command:` overrides.
## Native Module Detection
Static lockfile analysis only.
| Language | Signal | Where |
|----------|--------|-------|
| Node.js | `"node-gyp"` in lockfile | `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml` |
| Python | `numpy`, `pandas`, `grpcio`, `Pillow`, `bcrypt`, `psycopg2` (not `-binary`), `cryptography`, `lxml`, `scipy`, `scikit-learn` | `requirements.txt`, `pyproject.toml` |
| .NET | `[DllImport]`, `NativeLibrary.Load` | `.cs` files |
| Go | `import "C"` (cgo) | `.go` files |
| PHP | `ext-*` requirements | `composer.json` |
> ⛔ **Lockfile grep is the ONLY valid evidence.** Do NOT set `hasNativeModules` from package name alone. `prebuild-install` without `node-gyp` = prebuilt → `hasNativeModules: false`. Key: `bcrypt` ✅ native, `bcryptjs` ❌ JS. `psycopg2` ✅, `psycopg2-binary` ❌. `sharp` v0.33+ ❌ prebuilt. `canvas` ✅ always. No lockfile → `"unknown"`, ⚠️ WARN.
When `hasNativeModules: true`: `f1Viable: false`, `f1BlockReason: "native modules ({signal})"`. (F1/D1/Free are never offered regardless — see below.)
## SKU Sizing Signals — Beyond Native Modules
> ⛔ **F1/D1/Free are never selected** (managed identity is mandatory and the free-tier MI sidecar OOMs). The floor is always **B1**. `f1Viable` is retained for back-compat but is effectively always `false`; treat these signals as "size **up** from B1" (B2/S1), never as "drop to free".
| Condition | f1BlockReason / sizing note |
|-----------|---------------|
| Large dep tree (Python >10, Node lockfile >500KB, .NET >20 NuGet) | `"large dependency tree"` — consider B2 |
| Build-time compilation (`tsconfig.json` + `"build"` script) | `"build-time compilation"` |
| WSGI/ASGI server (`gunicorn`/`uvicorn`/`daphne`) | `"WSGI/ASGI server"` |
| 🔶 Major Migration (>5 files) | `"major migration"` |
Prepare always selects **B1 (~$13/mo) minimum**; the signals above only push sizing higher.
## First-Run Initialization
Detect init steps needed before first HTTP request — run automatically in dev but cause 500s on Azure.
| Framework | Signal | Init command |
|-----------|--------|-------------|
| Flask-Migrate/Alembic | `flask_migrate`/`alembic` + `migrations/` | `flask db upgrade` |
| Django | `django` + `manage.py` + `*/migrations/` | `python manage.py migrate` |
| Prisma | `@prisma/client` + `prisma/schema.prisma` | `npx prisma migrate deploy` |
| TypeORM | `typeorm` + `migrations/`/`ormconfig` | `npx typeorm migration:run` |
| Sequelize | `sequelize` + `migrations/` | `npx sequelize-cli db:migrate` |
| EF Core | `EntityFrameworkCore.Tools` in csproj | `dotnet ef database update` |
**Seed/bootstrap:** Flask `@app.cli.command('deploy')` → `flask deploy`. Django `fixtures/` → `manage.py loaddata`. Seed commands: `required: false`.
Migration framework + `migrations/` dir → ⚠️ WARN, write to `prereq-output.json.initCommands[]` (schema in [`prereq-schemas.ts`](prereq-schemas.ts)). ORM without `migrations/` dir → ✅ PASS. Prepare prepends required `initCommands` to `deployStrategy.startupCommand`; scaffold encodes in `appCommandLine`. Migrations are idempotent — safe on every cold start.
## Database & Storage
| Found | Assessment |
|-------|-----------|
| File-based DB (SQLite, LevelDB, DuckDB) | ⚠️ WARN — ephemeral on PaaS. Deploy as-is, suggest managed DB in `postDeployRecommendations[]` |
| Local file storage | Needs Azure Blob Storage |
| In-memory cache only | Consider Redis |
| Managed DB connection string | Ready — update connection |
@@ -1,48 +0,0 @@
<!-- 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.
# Deployability Check
> ⛔ **No build/install/test commands — `npm install`, `npm test`, `dotnet build`, `dotnet restore`, `dotnet test`, `pip install`, `pytest`, `go mod download`, `cargo build`. Use static analysis only during this check.**
Assess whether the repository can feasibly be deployed to Azure and whether a preparation plan can be created.
> ⛔ **You MUST read the following in order. Skip items marked conditional if the condition is not met:**
>
> 1. [component-mapping.md](component-mapping.md) — Steps 1–2: Component→Azure mapping, existing infrastructure detection, Terraform provider classification, compose service extraction. **Conditional: monorepo only (>1 project manifest found).** For single-component repos, skip to Step 3.
> 2. [dependency-compatibility.md](dependency-compatibility.md) — Step 3: EOL runtimes/frameworks, archived repos, vulnerable apps, platform deps, Dockerfile analysis, native module detection
> 3. Steps 4–5 below
## Step 4: Recipe Feasibility
Assess whether at least one deployment recipe is viable.
| Check | Question |
|-------|----------|
| AZD feasible? | Standard web app stack? No exotic build requirements? |
| Container feasible? | Can be Dockerized? Or already has Dockerfile? |
| Functions feasible? | Event-driven or HTTP-triggered stateless handlers? |
| Terraform feasible? | User has TF experience or existing TF files? |
| Outcome | Verdict |
|---------|---------|
| At least one recipe clearly viable | ✅ PASS |
| Viable with modifications (config change, Dockerfile tweak) | 🔧 Recommended Fix — note required changes |
| Viable but requires significant rework (>5 files, architecture change) | 🔶 Major Migration — warn about scope |
| Minor platform concerns (ephemeral storage, missing .dockerignore) | ⚠️ WARN — informational |
| No viable recipe identified | ❌ FAIL |
## Step 5: Specialized Deployment Detection
Check if the repo's stack requires a specialized deployment agent. If a match is found, set `context.json.routeToSkill` and `routeReason` so Step 8 routes directly.
> **Non-Azure cloud SDK deps** (AWS/GCP SDKs, Firebase, etc.) — evaluated and carried as 🔶 blockers (no `routeToSkill`); prereq stops at Step 8. See [dependency-compatibility.md § Non-Azure Cloud SDK Dependencies](dependency-compatibility.md).
| Dependency / Pattern | `routeToSkill` | `routeReason` |
|---------------------|----------------|---------------|
| `@github/copilot-sdk`, `github-copilot-sdk`, `GitHub.CopilotSdk` | `azure-hosted-copilot-sdk` | `copilot-sdk-detected` |
| `azure_ai_projects`, `azure-ai-agents`, `foundry-agents` | `microsoft-foundry` | `foundry-agents-detected` |
## f1Viable Aggregation (deprecated signal)
F1/D1/Free are never selected (floor is B1), so `f1Viable` is effectively always `false`. Still populate `f1BlockReason` when a blocker is found during [dependency-compatibility.md § Native Module Detection](dependency-compatibility.md) or [§ SKU Sizing Signals](dependency-compatibility.md) — it drives sizing **up** from B1 (B2/S1), never a drop to free.
@@ -1,28 +0,0 @@
<!-- 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.
Artifact write procedures for the prereq phase exit. Read at Step 4 of the [readiness gate](readiness-gate.md).
## Write Artifacts
> ⛔ **Write all 3 artifacts before exiting prereq.** Downstream phases read these.
> ⛔ **Use `create` tool for ALL session files — NEVER `powershell`.** Terminal is OK ONLY for read-only ops: UUID generation, git info, file existence checks.
> ⛔ **Path scoping:** ALL `create` calls target `.copilot-azure/sessions/{uuid}/`. After writing, read back to confirm correct path.
1. **`prereq-output.json`** — ⛔ Read [`prereq-schemas.ts`](prereq-schemas.ts) for `PrereqOutput` interface.
> ⛔ **Per-component verdicts MUST persist.** Every entry in `components[]` MUST include a `verdicts` object: `{ "build", "completeness", "deployability" }` with values `PASS`/`WARN`/`FAIL` (`build` may be `SKIPPED` when build validation was skipped; `completeness`/`deployability` are never `SKIPPED`). Downstream readiness scoring and the prepare phase read these — never omit them.
>
> ⛔ **Warnings MUST persist.** Every ⚠️ WARN → `warnings[]`: `{ "id": "W-{ID}", "component", "axis", "summary", "detail", "fix", "fixPhase" }`. Both `fix` and `fixPhase` are required — validate before writing.
>
> ⛔ **Health endpoint:** Write detected path to `healthEndpoint` (e.g., `"/api/v1/health/"`). If none → `null` + `W-HEALTH` warning with `fixPhase: "scaffold"`.
>
> ⛔ **Entry point:** Write to `entryPoint` (e.g., `"index.js"`). `null` for .NET/Go/Java (Oryx handles startup).
>
> ⛔ **`postDeployRecommendations[]`:** For each ⚠️ WARN, write per `PostDeployRecommendation` schema from [`session-schemas.ts`](session-schemas.ts): `{ "title", "reason", "effort": "low|medium|high", "services": [] }`.
2. **`context.json`** — ⛔ Use `edit` (not `create` — Step 1 already created it). Populate `components[]`, `repo`, `detectedInfra[]`, `detectedServices[]`, `app.name` (from primary component's project manifest or workspace root dir name). Append `"prereq"` to `completedPhases` NOW (before presenting), set `currentPhase: null`, update `lastModifiedUtc`.
3. **`readiness-report.md`** — Summary table (Build/Completeness/Deployability verdicts), detected stack, all warnings with actionable detail. Clean markdown, no rigid template.
> ⛔ **Phase exit gate:** All 3 artifacts must exist. If any missing, write NOW.
@@ -1,87 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Prereq-phase TypeScript interfaces for prereq-output.json.
*
* Standalone schema for azure-app-onboard-prereq — this agent can run independently.
*
* Shared types used across phases (AppOnboardComponent, PostDeployRecommendation,
* DetectedService, AppOnboardContext) are defined in:
* session-schemas.ts (local copy in this phase's references/)
*
* Context schema (AppOnboardContext, AppOnboardIntent) is defined in:
* session-schemas.ts (local copy in this phase's references/)
*/
// ─── prereq-output.json ──────────────────────────────────────────────────────
export interface BuildRequirements {
hasNativeModules: boolean;
hasDockerfile: boolean;
/** @deprecated F1/D1/Free are never offered (managed identity is mandatory and the
* free-tier MI sidecar OOMs). Retained for back-compat; treat as always false.
* The compute floor is always B1. */
f1Viable: boolean;
/** True when Dockerfile uses BuildKit-only syntax (--mount, # syntax=docker/dockerfile:1) */
hasBuildKitSyntax?: boolean;
/** Port from EXPOSE directive — used for Container Apps targetPort */
exposedPort?: number;
/** Estimated native module compilation time in seconds (typically 30–300s;
* larger compiled dependencies like scipy may take 600+s).
* Used by deploy phase to set WEBSITES_CONTAINER_START_TIME_LIMIT. */
estimatedInstallTime?: number;
/** @deprecated F1 is never selected. Retained as a "size up from B1" signal.
* Examples: "native modules (node-gyp)", "large dependency tree (27 pinned deps)",
* "build-time compilation (TypeScript tsc)", "major migration (Flask 0.12→2.3)" */
f1BlockReason?: string;
}
/** Structured warning from prereq evaluation */
export interface PrereqWarning {
id: string;
component: string;
axis: "build" | "completeness" | "deployability";
summary: string;
detail: string;
/** What to do about this warning — actionable fix instruction.
* Examples: "Set PGSSLMODE=require env var in IaC", "Add trust proxy setting to Express app" */
fix: string;
/** When the fix should be applied in the pipeline.
* "prereq" = prereq can fix this NOW with user approval (code/config changes that prevent deploy failures).
* "scaffold" = handled in generated IaC only (env var override, probe config). NEVER modifies user code.
* "deploy-gate" = surface at deploy approval gate for user awareness. No code changes.
* "post-deploy" = informational — add to postDeployRecommendations[], no action during pipeline. */
fixPhase: "prereq" | "scaffold" | "deploy-gate" | "post-deploy";
}
export interface PrereqOutput {
// AppOnboardComponent[] — see session-schemas.ts
components: unknown[];
/** Structured warnings — see prereq-artifacts.md for write rules */
warnings: PrereqWarning[];
detectedStack: string;
/** Prereq-only: auto-approves readiness gate + simplifies prepare alt analysis.
* Does NOT skip any phase, gate, reference read, or validation. */
fastTrackEligible: boolean;
overallHealth?: "ready" | "readyWithCaveats" | "blocked";
/** Build-time requirements detected from manifests and lockfiles */
buildRequirements?: BuildRequirements;
/** Detected health endpoint path (e.g., "/health", "/api/v1/health/").
* Used by scaffold for Azure health probe config and deploy for curl health check.
* null if no health endpoint found (triggers W-HEALTH warning). */
healthEndpoint?: string | null;
/** Application entry point detected from manifest (e.g., "index.js", "main.py", "cmd/main.go").
* Used by prepare/deploy-strategy.md for startupCommand generation. */
entryPoint?: string;
/** Structured recommendations derived from WARN findings.
* Merged into prepare-plan.json.postDeployRecommendations[] by the prepare phase.
* PostDeployRecommendation type — see session-schemas.ts */
postDeployRecommendations?: unknown[];
/** First-run init commands (DB migrations, seed data) detected from
* framework signals + migrations/ dir. Prepare prepends required entries
* to deployStrategy.startupCommand. See dependency-compatibility.md § First-Run. */
initCommands?: { type: string; framework: string; command: string; required: boolean }[];
}
@@ -1,102 +0,0 @@
<!-- 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.
# Readiness Gate — Step 4
## Write Artifacts
⛔ **You MUST read [`prereq-artifacts.md`](prereq-artifacts.md)** for complete artifact write procedures and phase exit checklist.
---
## Severity Tiers
| Verdict | Icon | Meaning |
|---------|------|---------|
| Hard Halt | 🛑 | App is intentionally vulnerable — pipeline stops, no fix possible |
| Major Migration | 🔶 | Large-scope change (EOL runtime, cloud SDK migration, >5 files) — redirect or warn |
| Critical | ❌ FAIL | Deployment will fail — agent can fix (≤5 files, config-level) |
| Recommended Fix | 🔧 | App deploys but has quality/security issues — agent offers fix |
| Warning | ⚠️ WARN | Informational, non-blocking — can proceed with caveats |
| Pass | ✅ PASS | No issues |
---
## Overall Health Gate
**Compute `overallHealth`:** ALL ✅ PASS → `"ready"` | Any ⚠️ WARN no ❌ FAIL → `"readyWithCaveats"` | ANY ❌ FAIL → `"blocked"`
**Component `readiness.status` alignment:**
- `"ready"` → `readiness.status: "ready"`
- `"readyWithCaveats"` → `readiness.status: "ready"` (WARNs aren't "needs fixes")
- `"blocked"` → `readiness.status: "needsFixes"`
- After remediation → `readiness.status: "fixesApplied"`
⛔ `readiness.status: "needsFixes"` requires at least one ❌ FAIL. If all ⚠️/✅, use `"ready"`.
---
## Critical Readiness Gate
⛔ **Verdict propagation cross-check** before computing `overallHealth`:
1. Any finding with `verdict: "FAIL"` → axis verdict MUST be `"FAIL"`.
2. Any finding with `verdict: "WARN"` + `fixPhase: "prereq"` → escalate to `"FAIL"` (prevents wasting a deploy cycle). ⛔ Escalate only WARNs that would actually break THIS deploy (build/startup failure, or a health probe wired to a route the app lacks). Issues that deploy and run fine — missing trust proxy, README, in-memory sessions — stay `fixPhase: "postdeploy"`/`"scaffold"`; `engines`/health-endpoint escalate only on a real version/probe mismatch (see [completeness-check.md](completeness-check.md) § Stack-Specific Checks).
| Tiers | Reference file |
|-------|---------------|
| 🛑 🔶 🔧 ⚠️ | [`dependency-compatibility.md`](dependency-compatibility.md) |
| ❌ 🔧 | [`completeness-check.md`](completeness-check.md) |
| ❌ | [`build-check.md`](build-check.md) |
**Post-evaluation HALT cross-check:** Intentionally vulnerable apps (≥2 code signals from dependency-compatibility.md) → `overallHealth: "blocked"` MUST be written to `prereq-output.json` on disk (Step 4) BEFORE any halt message. If the artifact is not on disk when you reach the halt, write it NOW via the `create` tool and read it back — do NOT present the halt until it exists.
---
## Batch-Then-Approve Flow
⛔ **Artifacts before message.** Write AND read back all 3 artifacts (`prereq-output.json`, `context.json`, `readiness-report.md`) to confirm they exist on disk BEFORE presenting any findings, cloud-SDK stop prompt, or 🛑 hard-halt message. Those messages can end the turn, so every artifact MUST already be persisted — NEVER batch artifact writes after the message.
1. **Detect ALL issues first** — full 3-axis scan, all components.
2. **Present ALL findings at once** — summary: "🔍 Readiness: 2 critical, 1 recommended fix, 3 warnings". Group: 🛑 → 🔶 → ❌ → 🔧 → ⚠️.
3. **Fix plan** — for ❌, 🔧, 🔶, ⚠️ with `fixPhase: "prereq"`: describe WHAT and WHY. ⛔ Exclude 🔶 with `routeToSkill` set. Never include 🛑.
4. **User choice** (based on highest severity):
- **🛑:** Pipeline stops. No formal gate.
- **🔶 + others:** "Fix {N} issues including {M} migration(s) — scope warning" / "Fix blockers only" / "Continue with risks" / "Cancel"
- **🔶 only:** "Attempt migration" / "Continue as-is" / "Cancel"
- **❌/🔧/⚠️ with fixPhase prereq:** "Fix {N} deployment issues" / "Continue with risks" / "Cancel"
5. **After approval** → apply fixes per [remediation-protocol.md](remediation-protocol.md).
> ⛔ **Two-gate rule:** Intent approval ≠ fix execution approval. Present the fix prompt here even if user agreed earlier.
---
## Fast-Track
Single-component + no DB + no auth + **no Dockerfile** → `fastTrackEligible: true`.
---
## Present Findings (Step 5)
⛔ Do NOT skip — user must see scan results before pipeline continues.
**Part 1 — Summary:** "🔍 Readiness: {N} critical, {M} fixes, {K} warnings" (or "✅ Ready").
**Part 2 — Per-axis reasoning:** Verdict icon + 1–2 sentence summary per axis.
**Part 3 — Findings table:** Grouped by severity. Include warning ID and actionable detail.
**Data-loss warnings** (SQLite, in-memory sessions, local file storage) require explicit acknowledgment. Other ⚠️ are informational.
End with: "📄 Full evaluation saved to `readiness-report.md`."
### Remediation Decision Gate
⛔ **STOP after presenting findings.** Options:
1. **"Fix deployment issues"** — fix all actionable items (❌, 🔧, ⚠️ with `fixPhase: "prereq"`). Re-evaluate after.
2. **"I have context — let me guide the fixes"**
3. **"Continue without fixing — I accept the risks"**
Wait for explicit choice. Generic "Yes"/"Go ahead" ≠ remediation consent — clarify if ambiguous.
> ⛔ **Remediation budget:** Max 3 cycles. See [remediation-protocol.md](remediation-protocol.md) step 7.
> ⛔ **ARTIFACT CHECKPOINT.** After presenting findings, verify all 3 artifacts exist: `context.json`, `prereq-output.json`, `readiness-report.md`. Write any missing ones NOW.
@@ -1,47 +0,0 @@
<!-- 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.
# Remediation Protocol — Step 6
## Pre-check
> **🛑 Hard Halt?** Skip remediation. Set `overallHealth: "blocked"`, write artifacts, go to Step 7.
> **🔶 Major Migration?** If user chose "Attempt migration," enter loop. Warn changes may need review.
## Remediation Scope
⛔ **Fix ONLY Azure-deployment blockers:** missing dependencies causing build/startup failure, missing entry point, startup crashes, Azure-required configuration (port binding, env var externalization, managed-DB client TLS/SSL — e.g. `W-MYSQL-SSL`). Everything else → `postDeployRecommendations[]`.
## Remediation Loop
If any ❌ FAIL, 🔧 Fix, or ⚠️ WARN with `fixPhase: "prereq"` exist:
1. Present ALL together in one batch. Lead with: "Found {N} blockers, {M} fixes, {P} prereq-phase warnings. Fix all?"
> ⛔ `fixPhase: "prereq"` is a remediation trigger regardless of severity. If left unfixed → deploy-time failures. Warnings with `fixPhase: "scaffold"/"deploy-gate"/"post-deploy"` are NOT included.
2. After fixes, ⛔ **re-run full Step 3 evaluation (all 3 axes)** on affected components. Re-read each reference file and re-evaluate inline.
3. ⛔ **Verify fixes via static analysis only.** File exists → exports match imports (grep) → no syntax errors → config values present. Do NOT run install/build/test commands.
4. ⛔ **Re-read instructions.md before applying fixes** if it hasn't been read this turn.
5. ⛔ **Print after re-evaluation:** `🔄 Re-evaluation complete — ✅ N issues resolved, ❌ M remaining.` If M > 0, loop back.
6. **Build-validation gate (agent-modified code only).** After re-evaluation passes AND agent modified >2 source files, ask via `ask_user`: "I've fixed {N} files. Want me to install, build, and test? (Yes / Skip)". ⛔ **General prior consent** (e.g., "fix my issues", "yes", "go ahead", "fix them") **does NOT constitute consent to run install/build/test.** The user must answer THIS specific question. If they say Skip, proceed to Step 7 without running commands. Max 3 build-fix attempts.
7. ⛔ **Max 3 cycles total** (re-evaluations + build-fix retries). Then STOP and ask: "Keep trying" (grants 3 more) / "Guide fixes" / "Stop — accept remaining issues".
## Post-Remediation Artifact Updates
⛔ Re-evaluation = full 3-axis re-scan, NOT just reading fixed files. After ANY successful remediation, update all 3 artifacts per [prereq-artifacts.md](prereq-artifacts.md):
1. `context.json`: `readiness.status: "fixesApplied"`, updated verdicts, `fixes[]`, updated `statusSummary`
2. `prereq-output.json`: Rewrite completely via `create` tool. Recompute `overallHealth` from updated verdicts.
3. `readiness-report.md`: Append "Post-Fix Re-evaluation" section.
⛔ Re-derive verdicts from findings — do NOT carry forward pre-fix values. Per axis: worst finding verdict wins. Then: any axis FAIL → `"blocked"`, else any WARN → `"readyWithCaveats"`, else `"ready"`.
## Write Final State (Step 7)
⛔ Write components to BOTH `prereq-output.json.components[]` AND `context.json.components[]`. `context.json.components[]` is authoritative for downstream.
⛔ **`fixesApplied` requires re-evaluation evidence** — verify `🔄 Re-evaluation complete` was printed.
Set `readiness.status` per component: All PASS → `ready` | Fixes applied + re-eval passed → `fixesApplied` | Unresolved FAILs → `needsFixes`.
Update `context.json`: append `"prereq"` to `completedPhases`, set `currentPhase: null`, update `lastModifiedUtc`.
> ⛔ `overallHealth` MUST be one of: `"ready"`, `"readyWithCaveats"`, `"blocked"`.
> ⛔ `repo.lastScanCommit` is required — run `git rev-parse HEAD`.
@@ -1,63 +0,0 @@
<!-- 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.
# Session Protocol — Step 1
## All Prompts Are Actionable
> ⛔ **ALL prompts that activate this agent are actionable — go directly to Step 1.** Do NOT answer the user's question, give an overview of capabilities, or describe what AppOnboard can do before starting the pipeline. "Can Azure figure out my app?" and "Deploy my app" are the same action: Step 1 → Step 2 → scan. The user's phrasing (question vs command) does NOT change the workflow.
## Session Check
Resolve active session via pointer file.
> ⛔ **YOU MUST CREATE A SESSION BEFORE DOING ANY WORK — INCLUDING SCANNING**
>
> 1. **STOP** — Do not answer the user's question, scan code, or plan architecture yet
> 2. **CHECK** — Read `.copilot-azure/sessions/active-session.json`.
> - **Pointer exists** →
> 1. ⛔ **Read [`session-schemas.ts`](session-schemas.ts)** to get the exact field names and types for `AppOnboardContext`, `PrereqOutput`, and `PreparePlan`. Do not guess field names. Then read the pointed-to session's `context.json`. Display: "Found session from [lastModifiedUtc] — {statusSummary}."
> 2. ⛔ **MANDATORY `ask_user` GATE — execute this step NOW, before ANY branching.** Call `ask_user`: "Resume this session or start fresh?" **Do NOT auto-resume, do NOT skip ahead to the staleness check, do NOT present cached findings.** Nothing else happens until the user answers. WHY: stale sessions from prior test runs cause the agent to silently reuse outdated results and skip phase instructions.md reads. The staleness check alone cannot catch this — only the user knows whether the prior session is still relevant.
> 3. **Branch on user's answer** (only after `ask_user` returns):
> - Resume → **Staleness check (prereq phase only):** If `completedPhases` includes `"prereq"`, run `git rev-parse HEAD` and compare to `context.json.repo.lastScanCommit`. If the commit is **different** OR `lastScanCommit` is **missing** → tell the user: "Repo has changed since last scan — re-running prereq." Proceed to instructions.md Step 2 (skip session creation). If identical → present cached findings from `prereq-output.json` and go to instructions.md Step 8 (Route). Then ⛔ **Read the phase instructions.md for the NEXT phase** (derive from `completedPhases`). E.g., prereq done → read `prepare/instructions.md`. Then continue from that phase.
> - Start fresh → generate a new UUID via `[guid]::NewGuid().ToString()`, create a new session folder, update `active-session.json` to point to the new session. Old session folder is never touched again.
> - **Pointer missing but session folders exist** → list folders under `.copilot-azure/sessions/`. If 1 folder: adopt it (read its `context.json`, write `active-session.json` pointing to it, show summary). If 2+: show a numbered list with `statusSummary` + `lastModifiedUtc` from each, ask user to pick one or start fresh. Write pointer for the chosen session.
> - **No sessions at all** → generate a UUID by running `[guid]::NewGuid().ToString()` in the terminal. ⛔ **You MUST generate the UUID via a terminal command — do NOT hardcode a placeholder like `a1b2c3d4-e5f6-7890-abcd-ef1234567890`.** Create the session directory: `New-Item -ItemType Directory -Path ".copilot-azure/sessions/{uuid}" -Force`. Then write a **minimal** `context.json` using the `create` tool — only these 3 fields are known immediately: `{ "sessionId": "{uuid}", "createdUtc": "{ISO 8601 now}", "intent": { "userPrompt": "{user's first message verbatim}" } }`. Write `active-session.json` with `activeSessionId: {uuid}` using the `create` tool.
> 3. **PRUNE** — After resolving the active session, check remaining session folders. Delete any where `context.json.lastModifiedUtc` is >7 days ago. **Never delete the active session** (the one `active-session.json` points to).
> 4. **VERIFY** — Confirm `context.json` exists and is valid JSON. If missing or malformed, halt and retry creation — do NOT continue to Step 2 without a verified session.
> 5. **CONFIRM** — Begin your first response with: "Started session at `.copilot-azure/sessions/{uuid}/`" (new) or "Resuming session from [date] — {statusSummary}" (existing)
> 6. **THEN** proceed to Step 2
>
> ⛔ **Ordering: session FIRST, scanning SECOND.** If you scan the workspace or read project files before writing `context.json`, you have violated the session-first rule. The session must exist before ANY code analysis.
>
> ⛔ **Shell fallback:** If PowerShell/terminal hangs on first attempt (no output after 10s), use the `create` tool directly for session directory and file writes. Do NOT retry shell commands more than once.
>
> ⛔ **Path scoping: ALL `create` tool calls for session artifacts MUST target `.copilot-azure/sessions/{active-session-id}/`.** Writing to any other session folder is forbidden.
## CLI Availability
Call `mcp_azure_mcp_extension_cli_install` with `cli-type: "az"` to verify Azure CLI is available. If missing, surface installation instructions before proceeding. Downstream phases (prepare, deploy) require it. Fallback: skip if MCP tool unavailable.
## Azure Login Gate
**Azure login gate (mandatory):** Run `az account show --query "{id:id, name:name, tenantId:tenantId}" -o json` with a **5-second timeout** (PowerShell: `Start-Process` with `-Wait` or inline timeout; if command hangs beyond 5s, treat as failure). If it succeeds, merge `subscriptionId`, `subscriptionName`, `tenantId` into `context.json.azure` (use `replace_string_in_file` or rewrite the file — the minimal context.json from Step 1 sub-step 2 may not have the `azure` key yet).
> ⛔ **If `az account show` fails or hangs:** ⛔ **You MUST read [`subscription-resolution.md`](subscription-resolution.md)** and follow its fallback procedure. Do NOT proceed to Step 2 without a resolved subscription. Do NOT leave `context.json.azure` empty and continue. Every downstream phase (prepare, scaffold validation, deploy) requires Azure auth — proceeding without it produces incomplete results.
## User Identity Detection
**User identity detection (for `deployed-by` tag):** If `az account show` succeeded, also run `az ad signed-in-user show --query displayName -o tsv` (5-second timeout). Write the result to `context.json.azure.userDisplayName`. Fallback if `az ad` fails: use `az account show --query user.name -o tsv` (returns UPN/email). If both fail, leave empty — prepare phase will resolve. This value becomes the `deployed-by` tag on ALL resources — resolving it once here prevents inconsistent tag values across resources.
## Subscription Detection Method
> ⛔ **`az account show` is the ONLY subscription detection method in Step 1.** Do NOT call `mcp_azure_mcp_subscription_list` here — that tool returns ALL subscriptions across ALL tenants and causes a lengthy picker detour. `az account show` returns the CLI's active subscription in <1 second. MCP subscription list is reserved for prepare Step 1 when the user explicitly wants a different subscription.
## Artifact Locations
| Location | Artifacts |
|----------|-----------|
| `.copilot-azure/sessions/{uuid}/` | `context.json`, `prereq-output.json`, `prepare-plan.json`, `scaffold-manifest.json`, `deploy-result.json` |
## Phase-gated Reference Loading
> ⛔ **Phase-gated reference loading.** Do NOT pre-read reference files for downstream phases. Read each phase's references only when entering that step. Scaffold references are irrelevant during deploy; prepare references are irrelevant during scaffold. Each phase instructions.md specifies its own required reads.
@@ -1,152 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Context + shared TypeScript interfaces for AppOnboard session artifacts:
* context.json, active-session.json, and shared types used across all phases.
*
* Per-phase schemas (load only when entering that phase):
* - prereq-schemas.ts (in azure-app-onboard-prereq) — prereq-output.json (PrereqOutput, BuildRequirements, CloudSdkFinding)
* - session-schemas-prepare.ts — prepare-plan.json (PreparePlan, PlannedService, etc.)
* - session-schemas-deploy.ts — scaffold-manifest.json, deploy-result.json
*
* Source of truth for JSON artifacts in `.copilot-azure/sessions/{session-id}/`.
*/
// ─── shared types (used across all phases) ───────────────────────────────────
export interface AppOnboardComponentStack {
language: string;
framework: string;
version: string;
}
export type ReadinessStatus = "ready" | "fixesApplied" | "needsFixes" | "unknown";
export interface AppOnboardComponentReadiness {
status: ReadinessStatus;
fixes: string[];
}
export type VerdictLevel = "PASS" | "WARN" | "FAIL" | "SKIPPED";
export interface AppOnboardComponentVerdicts {
build: VerdictLevel;
completeness: Exclude<VerdictLevel, "SKIPPED">;
deployability: Exclude<VerdictLevel, "SKIPPED">;
}
export interface AppOnboardComponentFinding {
category: "build" | "completeness" | "deployability";
verdict: VerdictLevel;
summary: string;
fix: string | null;
}
export interface AppOnboardComponent {
name: string;
path: string;
stack: AppOnboardComponentStack;
readiness: AppOnboardComponentReadiness;
verdicts?: AppOnboardComponentVerdicts;
findings?: readonly AppOnboardComponentFinding[];
}
export interface AppOnboardAzureTarget {
subscriptionId: string;
/** Display name of the subscription (from `az account show --query name`).
* Shown at both approval gates so the user can verify the target. */
subscriptionName: string;
resourceGroup: string;
region: string;
}
export interface AppOnboardRepoInfo {
remote: string | null;
}
export interface AppOnboardOverride {
key: string;
value: string;
reason: string;
}
export interface AppOnboardAppInfo {
name: string;
}
export interface PostDeployRecommendation {
title: string;
reason: string;
effort: "low" | "medium" | "high";
services?: string[];
}
export interface DetectedService {
type: string;
version?: string;
source: "compose" | "config" | "code";
}
// ─── context.json ─────────────────────────────────────────────────────────────
export interface AppOnboardIntent {
userPrompt: string;
description: string;
users?: string;
auth?: string;
scale?: string;
budget?: string;
/** Set to true after prereq scan refines intent */
refinedFromScan?: boolean;
/** Facts discovered by the prereq scan */
scanDiscoveredFacts?: string[];
}
export type AppOnboardPhase = "info" | "prereq" | "prepare" | "scaffold" | "deploy" | "cicd" | "observe";
export interface AppOnboardContext {
sessionId: string;
createdUtc: string;
lastModifiedUtc: string;
currentPhase: AppOnboardPhase | null;
completedPhases: readonly AppOnboardPhase[];
/** Human-readable 1-line summary of where the session stands, updated at each phase exit.
* Displayed in the session picker when the user resumes or switches sessions. */
statusSummary?: string;
intent: AppOnboardIntent;
components: AppOnboardComponent[];
azure: AppOnboardAzureTarget;
repo: AppOnboardRepoInfo;
app?: AppOnboardAppInfo;
/** Infrastructure file types detected in repo: dockerfile, terraform, bicep, azure-yaml, github-actions */
detectedInfra: readonly string[];
/** Cloud provider targeted by detected IaC. Only populated when `.tf` or `.bicep` files found.
* Used by scaffold to distinguish "existing Azure IaC" (halt) from "non-Azure IaC" (generate Azure TF alongside). */
detectedInfraProvider?: {
terraform?: "azure" | "gcp" | "aws" | "multi" | "unknown";
};
/** Service dependencies parsed from docker-compose, config files, or code imports */
detectedServices: readonly DetectedService[];
overrides: AppOnboardOverride[];
/** The agent to route to next. Set by Step 2 (cloud SDK gate), Step 5 (specialized deployment detection),
* or Step 8 (normal routing based on health + infra). Examples: "azure-cloud-migrate",
* "azure-hosted-copilot-sdk", "microsoft-foundry", "azure-app-onboard", "azure-prepare". */
routeToSkill?: string;
/** Why this route was chosen. Examples: "cloud-sdk-migration", "copilot-sdk-detected",
* "foundry-agents-detected", "ready-no-infra", "ready-existing-infra". */
routeReason?: string;
}
// ─── active-session.json ─────────────────────────────────────────────────────
/** Pointer file at `.copilot-azure/sessions/active-session.json`.
* Avoids scanning all session folders on startup — read this one file
* to find the active session, then read that session's context.json. */
export interface ActiveSessionPointer {
activeSessionId: string;
}
// PrereqOutput, BuildRequirements → see azure-app-onboard-prereq/references/prereq-schemas.ts
@@ -1,70 +0,0 @@
<!-- 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 — Starter App Scaffold (Zero-Code Path Step 4)
Generate a minimal, Azure-compatible starter project from scratch based on user requirements.
## Critical Rules
- ⛔ **Do NOT invoke any other agents or hand off** — no external agent calls. You are a code generation sub-agent only.
- ⛔ **Do NOT generate Azure infrastructure** (Bicep, Terraform, `azure.yaml`). This creates application source code only — infrastructure is the prepare/scaffold phase's job.
- ⛔ **Do NOT install dependencies** — no `npm install`, `pip install`, or any package manager commands. The main agent handles the build-validation gate after you return.
## Input (provided by caller)
| Field | Source | Required |
|-------|--------|----------|
| App description | User's answer to "What kind of app?" or `context.json.intent.userPrompt` | YES |
| Chosen stack | Stack the user accepted or overrode (e.g., "Node.js/Express", "Python/FastAPI") | YES |
| Workspace root | Absolute path to write files | YES |
| Data needs | `true` if user described database/storage needs ("with a database", "stores tasks") | YES |
| Multi-page | `true` if user described multiple views/pages ("three tabs", "dashboard + settings") | YES |
## Workflow
### Step 1 — Apply starter patterns
Generate health endpoints, follow stack conventions, and avoid common mistakes:
**Health endpoints (MANDATORY):**
- `/healthz` — liveness: `200 { status: "ok" }`. Must return 2xx directly (no redirects), allow anonymous access.
- `/readyz` — readiness: check DB/cache/deps. `200` when ready, `503` when not.
- Container Apps: `httpGet.port` must match `targetPort` in ingress config.
**Stack conventions:**
- **Node.js/Express:** Listen on `process.env.PORT || 3000`. `"start"` script required. `"engines": { "node": ">=20" }`. Production deps in `dependencies`.
- **Python (Flask/FastAPI):** Production: `gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app`. Include `gunicorn` in `requirements.txt`. Bind `0.0.0.0`, not `127.0.0.1`.
- **Next.js/React:** Static export: `output: 'export'`. Hybrid SSR works on SWA.
**Project structure:** `src/` for app code (routes, entry, server). `.env.sample`, `.gitignore`, `package.json`/`requirements.txt`, `README.md`.
**Common mistakes to avoid:** hardcoded port, dev server in production, no health endpoint, missing `start` script, secrets in source, no CORS middleware, missing `engines.node`, Python missing `gunicorn`, Python using `passlib` (unmaintained, breaks on Python 3.12+ with bcrypt 5.x — use `bcrypt>=4.0` directly).
### Step 2 — Generate files
Scaffold a minimal starter project. Include:
- Entry point file with a working HTTP server, `/healthz` (liveness), and `/readyz` (readiness) endpoints
- Package manifest (`package.json`, `requirements.txt`, `*.csproj`, `go.mod`) with minimal production dependencies
- `.env.sample` listing required environment variables (at minimum: `PORT`)
- `.gitignore` appropriate for the stack
- README.md with project name, one-line description, and local run instructions
- If data needs is `true`: add a placeholder data model/schema file and an in-memory or file-based data layer (NOT a cloud database client — that's scaffold phase's job)
- If multi-page is `true`: scaffold route stubs or page components
### Step 3 — Return file list
Return the list of files written to the workspace so the main agent can verify and pass to build validation.
## Output
| Artifact | Location |
|----------|----------|
| Application source files | Workspace root (conventional layout per Step 1 patterns) |
| File list | Return to caller — array of relative paths written |
## Rules
- Scaffold dynamically based on app description — no hardcoded templates. Read the stack ecosystem's conventions (e.g., `npm init` patterns for Node, `dotnet new webapi` patterns for .NET).
- Code must be functional enough to start locally (e.g., `node src/server.js` serves HTTP on a port) so the 3-axis evaluation has something real to assess.
- Follow the project structure and common mistakes guidance from Step 1 exactly.
@@ -1,17 +0,0 @@
<!-- 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.
# Subscription Resolution — Defensive Fallback
The `azure-app-onboard` orchestrator resolves the subscription at Step 1 (login hard gate) and writes `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure` before any phase runs. In normal operation, `context.json.azure.subscriptionId` is always set by the time prepare runs.
At prepare phase entry, verify `context.json.azure.subscriptionId` is set. If it is (expected path), use it — done.
If `context.json.azure` is somehow empty, resolve now rather than halting the flow:
1. **Check env vars** — if `AZURE_SUBSCRIPTION_ID` is set, use it directly (with `AZURE_TENANT_ID` if set). Write `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure`, done.
2. **Run `az account show`** — `az account show --query "{id:id, name:name, tenantId:tenantId}" -o json`. If it succeeds, **auto-select** — write `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure`. Do NOT run `az account list` or present a picker.
3. **Fallback: `mcp_azure_mcp_subscription_list` + picker** — only if `az account show` fails. Call `mcp_azure_mcp_subscription_list` to retrieve all subscriptions (returns `subscriptionId`, `displayName`, `isDefault`).
- **1 subscription** → auto-select, no question. Write `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure`.
- **2+ subscriptions** → present a picker via `ask_user`: list each subscription as a choice `"{displayName} ({subscriptionId})"` with the default marked. The user selects one. Write `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure`.
4. **MCP tool fails** → run `az login` (interactive browser login). If that fails (no browser, remote session), fall back to `az login --use-device-code`. After login succeeds, retry from step 2. Do NOT proceed without a resolved subscription.
@@ -1,31 +0,0 @@
<!-- 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.
# Zero-Code Path
When the workspace is empty (no project files, no Dockerfile), prereq scaffolds a starter project before evaluating.
## Step 0: Check Existing Context
⛔ **Before asking anything, check `context.json.intent.userPrompt`.** The orchestrator (or the user's original message on direct entry) already captured what the user wants. If `userPrompt` contains an app description (e.g., "build me a todo app", "REST API with auth", "a notes app with three tabs"), treat it as the answer to "what are you building?" and skip directly to Step 2 (recommend stack). Only ask Step 1's question if `userPrompt` is vague ("help me get started", "deploy to Azure") or empty.
## Flow
1. **Ask what they want to build** (skip if Step 0 resolved it): *"What kind of app are you building?"* (e.g., "a todo app", "REST API"). Users know what they want, not what stack to use.
2. **Recommend a stack** based on the app description (from `userPrompt` or Step 1): *"For a REST API with a database, I'd suggest Node.js with a framework like Express or Fastify. Sound good?"* User can accept or override.
3. ⛔ **Confirm before scaffolding** via `ask_user`.
4. **Scaffold minimal starter project** — ⛔ **You MUST read [subagent-starter-scaffold.md](subagent-starter-scaffold.md).** Your NEXT action MUST be a `task` call with the FULL template text (verbatim) + app description + chosen stack + workspace path + data needs flag + multi-page flag. Do NOT generate code inline — the sub-agent applies starter patterns internally and writes files to the workspace. After the sub-agent returns, verify the file list is non-empty and proceed to Step 5.
5. **Validate generated code** — ⛔ **When the agent has WRITTEN code from scratch, offer to run build validation before evaluation.** The generated code has never been tested. Present: **"I've scaffolded your app. Want me to install dependencies, build, and run tests? (Yes / Skip)"** ⛔ **General prior consent** (e.g., "yes", "go ahead", "fix it") **does NOT constitute consent** — the user must answer THIS specific question. If they say Skip, run nothing.
- If **Yes**: run `npm install` → `npm run build` → `npm test` (or stack equivalent). If any step fails, fix the issue and retry (max 2 attempts). This is allowed because the agent WROTE the code — it's not an existing repo.
- If **Skip**: proceed to Step 6. The deploy phase will handle builds via Oryx/ACR.
- ⛔ **This gate applies ONLY to code the agent generated from scratch.** It does NOT apply to existing repos — those follow the deploy-as-is principle with the ABSOLUTE PROHIBITION on `npm install` during prereq.
6. Run the full 3-axis evaluation (from prereq Step 3) on the scaffolded code.
7. Continue to prereq Step 4 (write artifacts + readiness gate).
## Rules
- Max 3 interactions before scaffolding begins (counting from Step 1 — Step 0's `userPrompt` check doesn't count as an interaction).
- Scaffold dynamically based on app description — no hardcoded templates. Read the stack ecosystem's conventions (e.g., `npm init` patterns for Node, `dotnet new webapi` patterns for .NET).
- If user gives no app description after 2 attempts AND `userPrompt` was also vague: *"I can't evaluate an empty workspace without knowing what you want to build. Try: 'I want to build a REST API' or 'Help me scaffold a Node.js API.'"*
- ⛔ **Do NOT generate Azure infrastructure (Bicep/Terraform/azure.yaml) here.** This path creates application source code only. Infrastructure is the scaffold phase's job.
- The generated code should be functional enough to start locally (e.g., `node index.js` serves HTTP on a port) so prereq's evaluation has something real to assess.
@@ -1,86 +0,0 @@
<!-- 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 Gates — Steps 6 & 8
> **Gate summary:** AppOnboard has **2 approval gates**: (1) **Scaffold Gate** (orchestrator Step 6) — approve architecture plan before generating IaC, (2) **Deploy Gate** (orchestrator Step 8 / deploy/instructions.md Step 4) — approve cost + resource summary before `az deployment`. Both are mandatory and SEPARATE — scaffold approval does NOT imply deploy approval.
## Scaffold Approval Gate (Step 6)
Display the architecture plan for user approval BEFORE generating any files:
> ⛔ **Open the plan view FIRST.** Call `open_deploy_plan_view` before displaying the gate text — it renders the `prepare-plan.json` you just wrote (services, SKUs, region, cost) in a side-by-side view. Call it once per gate; it is a display action only and never replaces the chat approval prompt below.
> ⛔ **Record the CLI prerequisites through the MCP tool — not in chat.** The plan view has a prerequisites section for the two CLIs this stage depends on, and it is populated **only** by `record_deploy_prerequisites`. Before (or right alongside) `open_deploy_plan_view`:
>
> 1. Probe each CLI with its version command in the user's own default shell — **azd** → `azd version`, **az** → `az version`.
> 2. Call `record_deploy_prerequisites` with one entry per tool: `installed: true` when the command returned a version, otherwise `installed: false`. Include the detected `version` when you have it.
> 3. Do **not** pass install links or display names — the view resolves those from its own catalog.
>
> Example: `record_deploy_prerequisites({ tools: [{ id: "azd", installed: true, version: "1.9.2" }, { id: "az", installed: false }] })`
>
> Checking the CLIs by hand and describing the result in chat does **not** satisfy this — the user's plan view stays empty. Never write these into `prepare-plan.json`; that is the vendored pipeline's artifact. If the tool is not directly listed, load it first per "Azure Resources MCP Tools" in [`azure-deploy.agent.md`](../../azure-deploy.agent.md) — do **not** conclude it is unavailable.
> ⛔ **Resource group edit is MANDATORY in the gate display.** Show this exact block:
> ```
> 🏢 **Subscription:** {subscriptionName} (`{subscriptionId}`)
> 📦 **Resource Group:** {rg-name} ({region})
> Want a different name or region? Say "Edit plan".
> ```
> ⛔ **Gate MUST show Subscription (name + ID), Resource Group, and Region** as standalone lines above the service table — see [pipeline-rules.md § Approval gates](pipeline-rules.md). Do NOT omit or bury in a table.
Also display: services + SKUs + region + resource names + monthly cost estimate + files to generate. **Check `context.json.overrides[]` for `iacFormat`** — if Terraform, display "Terraform templates (`infra/*.tf`)"; if Bicep (default), display "Bicep templates (`infra/main.bicep`)". Show resource names so the user sees what will be created.
> ⛔ **Surface plan assumptions.** If `prepare-plan.json.assumptions[]` is present (e.g., the B1-floor SKU was unavailable and a higher tier was selected), display each note prefixed with ⚠️ ABOVE the approval prompt — the user MUST see WHY the cost or SKU differs from the default. Do not bury or omit them.
Verify file list against target SKU. ⛔ **Exclude `azure.yaml` from file list** (see pipeline-rules.md).
> ⛔ **Container Apps code deploy preview (when plan includes Container Apps).** After the service table, preview the deploy path: build via ACR, replace placeholder images, redeploy. If `buildRequirements.hasBuildKitSyntax == true`, note ACR-compatible versions will be created.
> ⛔ **Data-loss warnings at the gate.** If `prereq-output.json.warnings[]` contains any data-loss findings (SQLite on App Service, in-memory sessions, local file storage), surface them prominently with ⛔ formatting ABOVE the approval prompt. The user must see these before approving — do not bury them in a table or omit them.
> ⛔ **Database network access (when plan includes PostgreSQL/MySQL).** Show a **Database access** line above the approval prompt: the default `AllowAllAzureServicesAndResourcesWithinAzureIps` (`0.0.0.0`) lets the app connect but opens the server to **all** Azure services. ⛔ **Render the exposure as its own bold sentence so a fast "Yes" is still an informed "Yes":** **"Proceeding opens the database to all Azure services — not just this app."** Offer the alternative: *"For private networking (VNet integration + private endpoint), say **'Private access'** — AppOnboard does not build private networking itself, so it will hand off to `azure-enterprise-infra-planner`, which then owns the secure networking design **and** its deployment. AppOnboard stops here — it does not resume afterward."* If the user proceeds (Yes), that counts as consent to the `0.0.0.0` rule.
> ⛔ **Private networking redirect.** If the user chooses **Private access** (here or during Edit plan): set `context.json.routeToSkill: "azure-enterprise-infra-planner"` and `routeReason: "private-networking-requested"`, then **HALT** — do NOT generate IaC. Tell the user: *"AppOnboard can't generate private networking (VNet + private endpoint). Handing off to azure-enterprise-infra-planner, which will design the secure topology and deploy it from here — it takes over the rest of the onboarding. Your AppOnboard session is saved for reference."* Then hand off to the `azure-enterprise-infra-planner` agent. This mirrors the prereq `routeToSkill` halt (instructions.md Step 3) but fires at the Scaffold Gate.
> ⛔ **Pick the prompt variant FIRST (based on whether the plan has a database), then use it verbatim — do NOT paraphrase or reword:**
> - **Plan includes PostgreSQL/MySQL** → **"✅ Ready to proceed with scaffolding? (Yes / Edit plan / Private access / Cancel)"** — the `ask_user` choices MUST be exactly: `Yes`, `Edit plan`, `Private access`, `Cancel`.
> - **No database in plan** → **"✅ Ready to proceed with scaffolding? (Yes / Edit plan / Cancel)"** — the `ask_user` choices MUST be exactly: `Yes`, `Edit plan`, `Cancel`.
>
> The `ask_user` choices MUST match the prompt text exactly — never name an option in one and omit it from the other (e.g. `Private access` must be a selectable choice, not something the user has to type).
> ⛔ **RESPONSE BOUNDARY — MANDATORY.** The approval gate MUST be the LAST content in your response. Do NOT generate any files, write any IaC, create any Dockerfiles, or execute any commands in the same response as the gate. Your next action MUST be reading the user's reply. If the user has not yet responded, WAIT — do not proceed.
Three options:
- **Yes** → proceed to Step 7 (scaffold only — NOT deploy)
- **Edit plan** → ask what to change → write to `context.json.overrides[]` → re-run Step 5 → show updated gate
- **Cancel** → preserve session artifacts for later resumption, stop
## Deploy Approval Gate (Step 8)
This is a SEPARATE gate from Step 6.
> ⛔ **Context refresh:** Before presenting this gate, re-read this instructions.md Steps 8-9 if you have not read them in the last 5 turns. Scaffold reference loading (Steps 6-7) consumes significant context — deploy rules may have been evicted.
Display:
- Validation status from `scaffold-manifest.json.validationResult`
- Self-review summary (count of VERIFIED/PLAUSIBLE/FLAGGED findings)
- Resource group name + region
- Services + SKUs + estimated cost
- End with **"🚀 Ready to deploy? (Yes / Run manually / Edit plan / Cancel)"**
> ⛔ **After deploy approval:** Your NEXT action MUST be: read `deploy/instructions.md`, then read `.copilot-azure/sessions/{id}/deploy-checklist.md`. Do NOT hand off to any other deploy workflow — this agent uses its own embedded deploy phase.
If "Run manually" is selected → point to [deploy-checklist-template.md § Deployment Summary](../deploy/references/deploy-checklist-template.md) for manual execution steps.
If validation failed or any FLAGGED findings exist at L1 (Security) or L3 (Hallucination), block **Yes** until resolved.
Only after user approves: proceed to deploy phase (Step 9). `context.json` already has `currentPhase: "deploy"` from the post-scaffold checkpoint (main instructions.md Step 7).
> ⛔ **Before entering deploy:** ⛔ Read [`deploy/instructions.md`](../deploy/instructions.md) before any deployment action. After mid-session compaction, re-read `deploy/instructions.md` Steps 4-8. You MUST write `deploy-result.json`.
> ⛔ Deploy via `az deployment sub create` (see [pipeline-rules.md](pipeline-rules.md)). AppOnboard-generated `azure.yaml` found → never delete/overwrite; move to `.copilot-azure/sessions/<id>/replaced-files/` (mirror path).
> ⛔ **Container Apps code deploy is NOT optional.** After IaC placeholder deploys, complete: `az acr build` → update image params → redeploy → health check. Do NOT present manual CLI "Next Steps" for core deploy tasks. If `hasBuildKitSyntax`, create `Dockerfile.azure` first.
> ⛔ **Phase exit:** `deploy-result.json` MUST be written before proceeding to Step 9. See deploy/instructions.md for the full exit gate checklist.
@@ -1,87 +0,0 @@
<!-- 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.
# azd Template Routing
When prereq detects an existing azd template, AppOnboard routes to `azure-prepare` instead of continuing the greenfield pipeline. AppOnboard is a greenfield deployment agent — repos with existing Azure IaC belong to the prepare → validate → deploy pipeline.
## Detection
Before the scope triage question (Step 2), do a quick file-system check (workspace root + `infra/` only — never scan `.copilot-azure/`). Route if ALL of these are true:
| Condition | Where to check |
|-----------|----------------|
| `azure.yaml` exists in workspace root | File system scan |
| `*.bicep` or `*.tf` files exist in `infra/` | File system scan |
| `azure.yaml` has `services:` with at least 1 entry | Read `azure.yaml` in workspace root |
If only `azure.yaml` is present without IaC files (partial azd setup), continue AppOnboard pipeline — the repo needs IaC generated.
## Gate — presented as the scope triage question
When an azd template is detected, the scope triage question is replaced with an azd-aware version (see [intent-gathering.md § Scope triage](intent-gathering.md)). Present:
```
📦 **Existing Azure deployment setup detected**
Your repo already has:
- `azure.yaml` — Azure Developer CLI configuration
- `infra/` — {Bicep|Terraform} infrastructure templates
{list any other detected infra: Dockerfiles, CI/CD workflows}
This is a complete azd template — it already defines how to build and deploy your app.
**How would you like to proceed?**
1. **Deploy with existing setup** — I'll hand off to `azure-prepare`, which works with azd templates natively. It will analyze your IaC, plan the deployment, and walk you through `azd up`.
2. **Start fresh** — Ignore the existing IaC and build new infrastructure from scratch using AppOnboard's greenfield pipeline. Your existing files won't be modified.
3. **Just scan for readiness** — Keep the prereq results (your app is {ready|needs fixes}) and stop here.
```
## Routing protocol
**Option 1 — Deploy with existing setup:**
1. Write routing state to `context.json`:
```json
{
"routeToSkill": "azure-prepare",
"routeReason": "existing-azd-template",
"completedPhases": [],
"currentPhase": null,
"statusSummary": "Routed to azure-prepare — existing azd template detected, prereq scan skipped (not needed for existing IaC)"
}
```
2. Tell the user:
```
✅ Your app is healthy — prereq scan found no blockers.
Since your repo has a complete azd setup, I'm handing off to **azure-prepare** — it's purpose-built
for repos with existing IaC and works natively with `azd up`.
```
3. **Invoke azure-prepare directly:** Hand off to the `azure-prepare` agent, then follow its workflow using the user's original prompt from `context.json.intent.userPrompt`. This is the same pattern as prereq invocation in Step 3 — AppOnboard hands off and the agent follows its instructions. Do NOT ask the user to type a command.
4. **STOP the AppOnboard pipeline.** Do NOT continue to Step 5 (plan architecture). Do NOT generate IaC. Do NOT run `azd up` yourself. azure-prepare owns the rest of the conversation.
**Option 2 — Start fresh:**
1. Write override to `context.json.overrides[]`: `{ "key": "ignoreExistingInfra", "value": "true", "reason": "User chose greenfield over existing azd template" }`
2. If `infra/` directory exists, rename it to `infra.bak/` (single folder rename). This preserves the user's existing IaC as a backup before scaffold writes new files.
3. Continue AppOnboard pipeline from Step 5 (plan architecture).
4. Scaffold Step 3 is skipped (override exists) — the backup was already done here.
**Option 3 — Just scan:**
1. Update `context.json.statusSummary` to reflect the scan-only outcome.
2. Present prereq results summary. STOP.
## Edge cases
| Scenario | Handling |
|----------|----------|
| `azure.yaml` exists but `infra/` is empty | NOT an azd template — continue AppOnboard (repo needs IaC) |
| `azure.yaml` exists with `infra.provider: terraform` | Route same as Bicep — azure-prepare handles both |
| User chose "Start fresh" then hits scaffold guard | Scaffold guard bypassed via `ignoreExistingInfra` override |
| Prereq found blockers AND repo has azure.yaml | Present blockers first (prereq triage), then present azd gate. Blockers take priority. |
| `azure.yaml`/`infra/` looks like it might be an AppOnboard leftover | It's ours only if **any** `.copilot-azure/sessions/*/scaffold-manifest.json` `files[]` lists it (checking every session, not just the active one, catches leftovers from a prior abandoned run); otherwise treat as the user's → route → STOP. ⛔ Never decide by git commit status; never delete/overwrite — move to `.copilot-azure/sessions/<id>/replaced-files/` (mirror path). |
@@ -1,140 +0,0 @@
<!-- 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.
# Handoff Protocol — Step 9
Offer next steps: CI/CD setup, monitoring, domain config, **🗑️ resource cleanup**, agent-based suggestions. Session artifacts remain for deferred pickup.
> ⛔ **Handoff MUST include ALL FOUR sections: (1) Deployment Identity, (2) Cleanup Commands, (3) Redeploy Command, (4) Post-Deploy Recommendations.** Missing any section = incomplete handoff. Do NOT skip cleanup even if deployment failed. Do NOT skip identity even if no resources were created. Do NOT skip recommendations even if the list is empty (print "No post-deploy recommendations.").
## Deployment Identity
> ⛔ **Start handoff with deployment identity.** First lines of the handoff response MUST be:
> ```
> 🏢 Subscription: {context.json.azure.subscriptionName} ({context.json.azure.subscriptionId})
> 📁 Resource Group: {context.json.azure.resourceGroup}
> 🌍 Region: {context.json.azure.region}
> 🔗 Portal: https://portal.azure.com/#@/resource/subscriptions/{subId}/resourceGroups/{rgName}/overview
> ```
> Source: `context.json.azure`. This is the user's quickest path to finding their resources after the session ends.
See [deploy-checklist-template.md § Deployment Summary](../deploy/references/deploy-checklist-template.md) for the full deployment summary format.
## Artifact Self-Check
> ⛔ **Artifact self-check — MANDATORY before handoff.** Verify these exist before presenting cleanup or next steps:
> 1. `deploy-result.json` in session folder — if missing, read [`deploy-schemas.ts`](../deploy/references/deploy-schemas.ts) and write it NOW with status, endpoints, health, `createdResources[]`, `orphanedResourceGroups[]`
> 2. `deploy-result.json.createdResources[]` populated — the deterministic before/after `resources.list()` diff from `capture_deployment_inventory`. If empty/missing, run `capture_deployment_inventory` (`phase: "capture"`) NOW and write its output into `deploy-result.json` so the cleanup section below is derived from data, not memory.
> 3. Portal deployment link printed in chat — if missing, generate from `$resId` pattern (see deploy/instructions.md Step 6) and print now
> 4. `deployment-summary.md` in session folder — if missing, `create` it NOW with the same content you are about to present in chat (status, subscription, RG, region, services table, endpoints, cleanup commands). One `create` call — do NOT skip.
## Show the Deployment Results View
> ⛔ **After the artifact self-check passes, call `open_deploy_result_view` — exactly once, before presenting the chat handoff.** This opens the Deployment Results webview, which renders `deploy-result.json` as a structured report: status, endpoints with health, provisioned resources, recovery attempts, and the cleanup command.
>
> - Call it only after `deploy-result.json` is **finalized** (`status` is `succeeded` or `failed`, never `in-progress`). The view reads the file from disk, so calling it early shows a half-written report.
> - Call it on failure too — the view surfaces which resources and endpoints failed, which is exactly what the user needs to debug.
> - The view **supplements** the chat handoff; it does not replace it. Still present all four handoff sections below.
> - If the tool reports that no result file was found, the artifact was written somewhere unexpected — re-check the session folder path and fix the artifact, then call the tool again.
## Show the Deployment Results View
> ⛔ **After the artifact self-check passes, call `open_deploy_result_view` — exactly once, before presenting the chat handoff.** This opens the Deployment Results webview, which renders `deploy-result.json` as a structured report: status, endpoints with health, provisioned resources, recovery attempts, and the cleanup command.
>
> - Call it only after `deploy-result.json` is **finalized** (`status` is `succeeded` or `failed`, never `in-progress`). The view reads the file from disk, so calling it early shows a half-written report.
> - Call it on failure too — the view surfaces which resources and endpoints failed, which is exactly what the user needs to debug.
> - The view **supplements** the chat handoff; it does not replace it. Still present all four handoff sections below.
> - If the tool reports that no result file was found, the artifact was written somewhere unexpected — re-check the session folder path and fix the artifact, then call the tool again.
## Post-Deploy Recommendations
> ⛔ **`postDeployRecommendations[]` MUST be surfaced — not silently dropped.** Read `prepare-plan.json.postDeployRecommendations[]` (already merged with prereq findings by the prepare phase). Present EACH entry as a numbered recommendation with this format:
> ```
> 📋 Post-deploy recommendations:
> 1. **{title}** ({effort} effort) — {reason}. Services: {services[]}
> 2. ...
> ```
> This section MUST appear BEFORE the agent-based suggestions and AFTER the cleanup commands. If `postDeployRecommendations[]` is empty, print "No post-deploy recommendations." Do NOT skip this section — findings buried in JSON artifacts are easily dropped from the handoff if not explicitly surfaced here.
## Cleanup Commands
> ⛔ **Cleanup commands are MANDATORY — always print BOTH.** Every handoff must include these two cleanup blocks, regardless of whether healing occurred or orphans exist.
**Primary cleanup (always print):**
```
🗑️ Delete this deployment's resources:
→ az group delete -n {rg} --yes --no-wait
```
**Tag-based bulk cleanup (always print — catches orphans from healing):**
```
🏷️ Delete ALL resources from this AppOnboard session:
→ az group list --tag app-onboard-session-id={sessionId} --query "[].name" -o tsv | ForEach-Object { az group delete -n $_ --yes --no-wait }
```
This catches orphaned RGs from region fallback or naming conflict healing. For Terraform: `cd infra && terraform destroy`.
**Deterministic orphan cleanup (from `deploy-result.json.createdResources[]`):**
> ⛔ **Prefer the inventory over memory.** `deploy-result.json.createdResources[]` and `orphanedResourceGroups[]` are computed by `capture_deployment_inventory` from a before/after `resources.list()` diff, so they catch orphans the tag filter misses (imperative `az create` fallbacks and resources that never received the session tag). If they are empty/missing, run `capture_deployment_inventory` (`phase: "capture"`) and write its output into `deploy-result.json` before writing this section.
**If `deploy-result.json.orphanedResourceGroups[]` is non-empty**, list each explicitly with delete commands. For orphans with a `subscription` field, include `--subscription {subscription}`.
⛔ **Do not present unattributed resources as safe to delete.** The two classifications carry different confidence and MUST be printed as two separate lists:
- **`classification: "failed"`** — confirmed to belong to this deployment. Print the exact `az resource delete --ids {id}` command under a heading like `🗑️ Failed resources from this deployment (safe to delete):`.
- **`classification: "orphaned"`** — appeared during the deploy window but no tracked deployment reported it. It may be a healing stray, or it may belong to someone else on a shared subscription. Print it under `⚠️ Resources that appeared during this deploy but could not be matched to it — review before deleting:` with the resource ID and portal link, and **without** a ready-to-run delete command.
⛔ **If `deploy-result.json.inventoryUnverified` is `true`**, the deployment's ARM operations could not be read and nothing could be attributed. Print neither list. Instead say the created-resource inventory could not be verified (include `inventoryUnverifiedReason`; for `"forbidden"` explain the account lacks `Microsoft.Resources/deployments/operations/read`) and point the user at the resource group in the portal. Still print the primary and tag-based cleanup blocks above.
## Redeploy Command
> ⛔ **Redeploy command is MANDATORY.** The full AppOnboard pipeline (prereq → prepare → scaffold → deploy) is a one-time setup. After that, the user only needs to rebuild and push code. Without this command, they'd have to reverse-engineer the deploy steps from session artifacts or re-run the entire pipeline. Give them the shortcut.
**Derive from what you ran in deploy Step 6b (code deploy).** Do NOT hardcode per service type — echo back the actual command(s) you executed to deploy code. The command varies by compute type, registry name, Dockerfile path, image tag, app name, etc. — all of which you already know from this session.
```
🔄 Redeploy (after code changes):
→ {code deploy command(s) from Step 6b}
```
Include this in the chat handoff AND in `deployment-summary.md`.
**If `deploy-result.json.healingAttempts[]` is non-empty**, surface: "⚕️ Deployment required {N} healing attempts" with per-attempt error/action/outcome and planLevelChange details.
**Cross-session cleanup (optional — show if user asks):**
```
🔍 Find ALL AppOnboard resources across all sessions:
→ az group list --tag app-onboard-skill=true -o table
```
## Agent-Based Next Steps
> ⛔ **Agent-based next steps are MANDATORY.** Always suggest at minimum `azure-compliance` and `azure-resource-visualizer`. Evaluate every condition below.
> ⛔ **Suggest, don't self-execute.** Present these as optional suggestions. The deploy agent must NEVER perform post-deploy infra changes itself — zone redundancy, private endpoints, SKU upgrades, and resource re-creation are imperative mutations the deploy phase must not make. If the user explicitly opts into one, route to that agent as a new, scoped task; otherwise the pipeline ends at handoff.
| Condition | Suggest |
|-----------|--------|
| Always | **`azure-compliance`** — "Run a compliance scan on your deployed resources" |
| Always | **`azure-resource-visualizer`** — "Generate an architecture diagram of your resource group" |
| Reliability/HA gaps (single-zone, no failover, prod workload) or user asks to "harden" / "make production-ready" | **`azure-reliability`** — "Assess & improve reliability: zone redundancy, multi-region failover, health probes" |
| Cost-sensitive workload or user asks about spend | **`azure-cost`** — "Review and optimize your Azure spend" |
| Health check failed / `healthStatus: "degraded"/"unreachable"` | **`azure-diagnostics`** — "Troubleshoot your deployment" |
| Auth/OAuth/MSAL detected in intent or prereq | **`entra-app-registration`** — "Set up app registration for your auth flow" |
| `postDeployRecommendations[]` has upgrade suggestions | **`azure-upgrade`** — "Upgrade your runtime or framework" |
| Storage-heavy patterns | **`azure-storage`** — "Optimize your storage configuration" |
| `postDeployRecommendations[]` mentions RBAC/role | **`azure-rbac`** — "Configure least-privilege role assignments" |
## Completion
> ⛔ **End the handoff with an explicit completion line — the LAST thing you emit.** It marks the pipeline done (so you stop working) and tells the user the core task succeeded and anything more is their choice. Frame it as closure WITH an open door — never a hard stop.
>
> ⛔ **Use this EXACT sentence verbatim — do NOT paraphrase, reword, or reorder it:** "The onboarding pipeline is finished." Only `{primary endpoint URL}` varies. This exact phrase is a required completion marker.
```
✅ **Deployment complete — your app is live at {primary endpoint URL}.** The onboarding pipeline is
finished. The next steps above are optional — tell me which you'd like and I'll route you to the
right agent.
```
> ⛔ **The recommendations above are OPTIONAL — do NOT autonomously execute them.** Present them and let the user choose; if they pick one, route to the matching agent (it makes the change properly). Don't turn a general acknowledgment into a batch of unsolicited hardening. Beyond this point the session is the user's — they may continue, invoke another agent, or run their own commands; that's expected and no longer part of this pipeline.
@@ -1,45 +0,0 @@
<!-- 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.
# IaC Resources — Official Documentation & Tools
Look up when stuck after 3 tries, edge cases, or validating generated code against ground truth.
## Bicep
| Resource | URL | Use When |
|----------|-----|----------|
| Bicep Documentation | https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/ | Syntax, file structure, deployment scopes, install |
| Azure Resource Reference | https://learn.microsoft.com/en-us/azure/templates/ | Resource properties, API versions, schema per type |
## Terraform
| Resource | URL | Use When |
|----------|-----|----------|
| Terraform Registry — azurerm | https://registry.terraform.io/providers/hashicorp/azurerm/latest | Resource type properties, argument reference, import blocks |
| Terraform Registry — azapi | https://registry.terraform.io/providers/azure/azapi/latest | Preview resources not yet in azurerm; maps to ARM REST APIs |
## Validation Tools
| Tool | Format | Purpose |
|------|--------|---------|
| `bicep build` | Bicep | Syntax + schema validation |
| `az deployment group create --what-if` | Bicep | ARM-level dry run with change preview |
| `terraform validate` | Terraform | Syntax + schema validation |
| `terraform plan` | Terraform | Provider-level dry run |
## Deploy Troubleshooting
> ⛔ **Primary lookup path:** Call `mcp_azure_mcp_documentation` with the error message first. Use the table below as fallback when MCP is unavailable or returns no results.
>
> **On repeat failures (same error 2+ consecutive attempts):** `fetch_webpage` the matching URL below with the error message as query. Apply the documented fix — do not retry the same approach.
| Resource | URL | Use When |
|----------|-----|----------|
| Common ARM Deployment Errors | https://learn.microsoft.com/en-us/azure/azure-resource-manager/troubleshooting/common-deployment-errors | `InvalidTemplateDeployment`, `SkuNotAvailable`, `QuotaExceeded`, any ARM error code |
| App Service Troubleshooting | https://learn.microsoft.com/en-us/troubleshoot/azure/app-service/ | Startup crashes, Kudu/Oryx build failures, health probe issues |
| App Service Zip Deploy Guide | https://learn.microsoft.com/en-us/azure/app-service/deploy-zip | Zip deploy, SCM_DO_BUILD_DURING_DEPLOYMENT, Kudu publish API |
| Container Apps Troubleshooting | https://learn.microsoft.com/en-us/azure/container-apps/troubleshooting | Revision failures, ingress errors, secret resolution, image pull failures |
| Quota Increase Portal | https://portal.azure.com/#blade/Microsoft_Azure_Capacity/QuotaMenuBlade/myQuotas | Direct link for quota increase requests |
> **Source:** Official Microsoft Learn, HashiCorp Developer, and Azure documentation.
@@ -1,52 +0,0 @@
<!-- 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.
# Intent Gathering
## Scope Triage (Step 2 — before prereq)
> ⛔ **Scope triage — BEFORE prereq.** Check for azd template markers (`azure.yaml` + IaC in `infra/`).
>
> **Skip triage when:**
> - User explicitly asks for cost estimates, service recommendations, or code analysis
> - Empty workspace (prereq’s zero-code-path handles it)
> - Code exists but no infra files (no `.bicep`, `.tf`, `azure.yaml`, or `infra/` dir) — full pipeline is the only sensible path
> - Intentionally vulnerable app signals (🛑 detection) — proceed to prereq directly
>
> ### If azd template detected
>
> ⛔ **You MUST read [`azd-template-routing.md`](azd-template-routing.md)** for detection criteria, gate presentation, and routing protocol.
>
> ### If NO azd template BUT infra files present
>
> Ask ONE `ask_user` question:
> 1. **Yes — analyze and deploy end-to-end** (Recommended)
> 2. **Just scaffold Bicep/Terraform**
> 3. **Just deploy it** (I have IaC ready)
> 4. **Other**
>
> Option 1 / vague → full pipeline (Step 3). Any other → hand off to the `azure-prepare` agent.
## After Prereq Returns (Step 4 — scan-informed intent gathering)
Prereq has written `prereq-output.json` and `context.json.components[]` — this is the authoritative source for all downstream phases (prepare and scaffold consume `context.json`, not `prereq-output.json`).
Present the detected component boundaries before asking questions. Include each deployable component's path,
framework, and intended Azure compute model. For a SPA plus Azure Functions project, state both mappings
explicitly: the frontend remains its own hosting service and the backend remains a separate Function App.
Do not infer an SWA-managed API merely because the frontend uses `/api` routes.
Confirm the Azure target ("☁️ **Azure target**: {subscriptionName} ({subscriptionId})"). If the user wants a different subscription, write to `context.json.overrides[]`.
**Present scan results first, then ask only what prereq didn't answer** (≤2 if mostly covered, ≤4 if gaps):
| # | Topic | Ask if... |
|---|-------|----------|
| 1 | App purpose | Not obvious from `detectedStack` + `components[]` |
| 3 | Data/storage | Prereq didn't detect DB/compose |
| 4 | Auth approach | No MSAL/passport/auth library detected |
| 5 | Scale | Always — prereq doesn't know traffic expectations |
⛔ Do NOT ask about stack/language (always answered by prereq) or budget (only if user mentioned cost). User corrections → `context.json.overrides[]`. Stop when covered or user says "just go."
**Update intent:** Merge scan results into `context.json.intent`. Set `refinedFromScan: true` and populate `scanDiscoveredFacts[]`.
@@ -1,82 +0,0 @@
<!-- 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.
# MCP Tool Reference — Shared Index
Shared tools used across multiple AppOnboard phases, plus the cross-cutting Phase→Tool Map. For phase-specific tools with full parameter tables, see the per-phase references linked below.
> **Troubleshooting:** If a tool call fails with unknown parameter or missing command errors, consult the official docs for current parameter names and allowed values: <https://learn.microsoft.com/en-us/azure/developer/azure-mcp-server/tools/>
## Per-Phase Tool References
| Phase | File | Exclusive Tools |
|-------|------|-----------------|
| Prereq | *(shared tools only — see table below)* | *(shared only)* |
| Prepare | [mcp-tools.md](../prepare/references/mcp-tools.md) | pricing, quota, cloudarchitect, WAF, advisor, group_resource_list, policy |
| Scaffold | [mcp-tools.md](../scaffold/references/mcp-tools.md) | bicepschema, all mcp_bicep_*, deploy (iac_rules/pipeline/plan), terraform best practices |
| Deploy | [mcp-tools.md](../deploy/references/mcp-tools.md) | resourcehealth, monitor, appservice, deploy (app_logs/arch_diagram), role |
---
## Global Parameters (all tools)
Every Azure MCP tool accepts these optional parameters in addition to its own:
| Parameter | Description |
|-----------|-------------|
| `subscription` | Azure subscription ID or display name. Defaults to `az account show` default. |
| `tenant` | Entra ID tenant GUID or name. Uses default tenant if omitted. |
| `resource-group` | Resource group name. Required for most resource-specific operations. |
> **Additional global params** (not commonly needed by AppOnboard): `authentication-method` (credential\|key\|connectionString), `max-retries` (default 3), `retry-delay` (default 2s), `retry-delay-maximum` (default 10s), `retry-mode` (fixed\|exponential), `retry-network-timeout` (default 100s). See [official docs](https://learn.microsoft.com/en-us/azure/developer/azure-mcp-server/tools/) for details.
---
## Shared Tools (used by 2+ phases)
### `mcp_azure_mcp_subscription_list`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| *(none)* | `tenant` | ✅ |
Returns: `subscriptionId`, `displayName`, `state`, `tenantId`, `isDefault`. Use `isDefault: true` as default subscription.
Used by: **prepare** (Step 1), **deploy** (Step 1)
### `mcp_azure_mcp_group_list`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| *(none)* | `subscription`, `tenant` | ✅ |
Returns: resource group names and IDs as JSON array.
Used by: **prepare** (Step 7), **deploy** (Step 3)
### `mcp_azure_mcp_extension_cli_install`
| Required | Optional | Read-Only |
|----------|----------|-----------|
| `cli-type` (az\|azd\|func) | `tenant` | ✅ |
Returns: installation instructions for the specified CLI tool.
Used by: **prereq** (Step 2), **deploy** (Step 3)
### `mcp_azure_mcp_get_azure_bestpractices` (hierarchical)
| Sub-command | Required Params | Optional Params | Read-Only |
|-------------|----------------|-----------------|-----------|
| `get_azure_bestpractices_get` | `resource` (general\|azurefunctions\|static-web-app\|coding-agent), `action` (all\|code-generation\|deployment) | — | ✅ |
| `get_azure_bestpractices_ai_app` | *(none)* | — | ✅ |
**Usage:** `resource` + `action` are both required for `_get`. For `static-web-app` and `coding-agent`, only `action: "all"` is supported.
Used by: **prereq** (Step 3), **scaffold** (Step 5)
---
## Tool Pitfalls
- **`mcp_azure_mcp_subscription_list` is slow at scale:** Returns ALL subscriptions across ALL tenants (238+ in large orgs), causing lengthy picker detours. Use `az account show` for the active subscription (<1 second). Reserve `subscription_list` for prepare Step 1 when the user explicitly wants a different subscription.
@@ -1,32 +0,0 @@
<!-- 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.
# Pipeline Rules — Runtime Reference
Known platform bugs and deploy timing. Read before deploy completion or on scaffold/deploy error.
For core pipeline rules (approval gates, phase lifecycle, session artifacts, security baseline), see [pipeline-rules.md](pipeline-rules.md).
## Known Platform Bugs
| Bug | Symptom | Workaround |
|-----|---------|------------|
| SWA CLI Windows path with spaces | `StaticSitesClient.exe` fails when workspace path contains spaces | Use a short, space-free temp path on FIRST attempt (e.g., `C:\temp\swadeploy` on Windows, `/tmp/swa-deploy` on macOS/Linux) — do NOT wait for retry. Copy app content to the temp path before running `swa deploy`. |
| `az deployment sub validate` HTTP stream | "HTTP response stream consumed" error (Azure CLI bug, also affects `create` in CLI 2.75.0+) | `validate`: use `az deployment sub what-if` instead. `create`: use `az rest --method PUT` on deployment URI as fallback. |
| `az quota list` extension error | `PermissionError: [WinError 5]` blocks extension commands | Use `az rest` instead (see [sku-quota-validation.md](../prepare/references/sku-quota-validation.md)) |
| Managed identity sidecar OOM on free-tier Linux | `503 Service Unavailable` with `Microsoft.Azure.WebSites.DataProtection` or `/msi/token` timeout | This is exactly why free tier is not used. Managed identity is REQUIRED and works on B1+; the compute floor is B1. Never provision F1/D1 (the sidecar OOMs there). |
| `Compress-Archive` path flattening | PowerShell's `Compress-Archive -Path $files.FullName` uses absolute paths, flattening directory structure | Use `System.IO.Compression.ZipFile` with relative paths instead |
| AADSTS530084 (Terraform) | Token protection conditional access policy breaks `azurerm` provider auth; regular `az` CLI commands work fine | Re-scaffold as Bicep |
| Secret values with shell special chars | App-internal secrets containing `$`, `` ` ``, `!`, `'`, `"` break when passed as inline CLI args (`--parameters key=val`) | **Pass app-internal secrets via `main.parameters.json` / `terraform.tfvars`, or as an `@secure()` param from a variable reloaded from `deploy-secrets.env`** — never inline. Prefer URL-safe generation (no `/+=` and no shell-special chars) so the value is safe everywhere. No Key Vault. |
| `az acr task logs` encoding crash | `UnicodeEncodeError` on Windows from Unicode chars in build logs | Use `--no-format` + strip non-ASCII, or REST API `listLogSasUrl` |
| `create` tool nested path failure | `"Parent directory does not exist"` when creating files in `.copilot-azure/sessions/{id}/` | Run `New-Item -ItemType Directory -Path {parent} -Force` before `create`. Platform tool limitation — agent always recovers. |
| Windows PowerShell `az rest` 415 error | `az rest --method put --body '{json}'` returns `415 Unsupported Media Type` on Windows PowerShell | Add `--headers "Content-Type=application/json"` to every `az rest --method put` call |
## Deploy Timing
- **B1 App Service cold-start:** After IaC creation, a newly created B1 plan needs ~30–90 seconds before the Kudu sidecar is ready. Deploying code immediately after `az deployment group create` returns can cause a 504 Gateway Timeout. Wait for sidecar readiness before code deployment.
- **Identity tag resolution:** Resolve `deployed-by` once at session start via `az ad signed-in-user show`. Without this, resources may receive inconsistent tag values (display name, UPN, or hardcoded strings) across different runs.
## Shell Rules
⛔ Shell variables do NOT persist between tool calls. Generate secrets inline within the command that consumes them — never store in `$env:` for later use. Use synchronous shells for all deploy-phase operations.
@@ -1,91 +0,0 @@
<!-- 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.
# Pipeline Rules — Reference
Cross-cutting rules enforced across all workflow steps. Referenced from [instructions.md](../instructions.md) `## Pipeline Rules`.
## Approval gates
⛔ **Two separate approval gates are required — never merge them.**
1. **Scaffold gate (Step 6):** "✅ Ready to proceed with scaffolding? (Yes / Edit plan / Cancel)" — approves IaC generation only. ⛔ When the plan includes PostgreSQL/MySQL, add `Private access` as a selectable choice — see approval-gates.md for the exact variant.
2. **Deploy gate (Step 8):** "🚀 Ready to deploy? (Yes / Run manually / Edit plan / Cancel)" — approves resource provisioning.
The scaffold gate does NOT grant deploy permission. After scaffold completes, you MUST present the deploy gate as a SEPARATE response. Never go from scaffold approval directly to `az group create` or `az deployment`.
⛔ **BOTH gates MUST show Subscription (name + ID), Resource Group, and Region** as standalone lines above the service table — users must see WHERE resources will be created before approving.
⛔ **NEVER create, write, or modify infrastructure files before the user explicitly says "Yes" to the scaffold gate.** No exceptions — not for simple apps, trivial plans, free-tier deployments, or single-component repos.
⛔ **Modifying existing IaC files (not AppOnboard-generated) requires explicit user approval.** Present: "I need to modify {file}: {change description}. Approve? (Yes / Edit / Cancel)". This applies to repos with existing Bicep/Terraform where AppOnboard adjusts SKUs, regions, or settings.
Each gate is the LAST content in its response — do NOT continue past a gate in the same turn.
> ❌ BAD: Writes Bicep without approval · scaffolds and deploys in same response · skips deploy gate after scaffold approval
> ✅ GOOD: Shows plan → user says Yes → scaffold → show validation summary → deploy gate → user says Yes → deploy
## Phase lifecycle
Update `context.json` at phase boundaries — combine `completedPhases` update with `currentPhase` for the next phase in a single write. Write the phase artifact before marking complete. The orchestrator instructions.md specifies exact write points (after prereq, after scaffold, after deploy).
`currentPhase` must NEVER appear in `completedPhases` — if invariant violated, halt and report.
`context.json` is NOT write-once — each phase boundary MUST update it on completion:
- Write `intent` after Step 2
- `components` after Step 3
- `azure.resourceGroup` after Step 7 (also written to `deploy-result.json.resourceGroupName`)
- Push to `completedPhases` at phase boundaries
- Update `statusSummary` at every phase exit — 1-line description. Templates:
- prereq: `"{N} components, stack: {detectedStack}, health: {overallHealth}"`
- prepare: `"{N} services, ~${monthlyUsd}/mo, region: {region}"` (if `quotaValidation.checkedRegions` >1, append fallback reason: `"region: westus2 (eastus quota full)"`)
- scaffold: `"{N} files, self-review: {VERIFIED|FLAGGED count}"`
- deploy: `"{healthStatus}, RG: {resourceGroupName}"`
- cancel: `"Paused at {phase} — {reason}"`
## Session artifacts
**Session file writes: `New-Item -ItemType Directory` for directories, `create` tool for file content.** Create session directory via `New-Item -ItemType Directory -Path ".copilot-azure/sessions/{uuid}" -Force`, then `create` tool for all JSON/md content. Never use `Out-File`, `Set-Content`, or shell commands for file content.
## Phase transition rule
> ⛔ **Before executing the FIRST command of any new phase, re-read that phase instructions.md.** After prereq → read `prepare/instructions.md`. After prepare → `scaffold/instructions.md`. After scaffold gate → `deploy/instructions.md`. This applies at EVERY transition.
## Post-compaction recovery
> ⛔ After ANY compaction, re-read current phase instructions.md + this file. Check `scaffold-manifest.json` and `completedPhases` exist if mid-scaffold/deploy.
Begin responses with: "Started session at `.copilot-azure/sessions/{uuid}/`" or "Resuming session from [date] — {statusSummary}".
⛔ **Session immutability:** NEVER write to any session folder other than the active session (the one `.copilot-azure/sessions/active-session.json` points to). Old sessions are read-only — no updates, no backfills, no status changes.
**Session TTL:** 7 days. Non-active sessions where `context.json.lastModifiedUtc` is >7 days ago are deleted on next invocation. The active session is never pruned.
## fastTrackEligible
Set by prereq: (1) auto-approves readiness gate, (2) simplifies prepare Step 3 alternatives. Does NOT skip phases, reads, gates, self-review, validation, or preflight.
## Deploy as-is
⛔ Do NOT refactor or upgrade working application code. Deploy what works. Fixing broken code IS allowed (build errors, missing deps) through the approval gate. Upgrade suggestions → `prepare-plan.json.postDeployRecommendations[]`. Infrastructure changes = allowed; code rewrites = forbidden; Azure compatibility changes (TLS, SSL, port) = allowed when detected by prereq AND approved. Never prompt for secrets — databases/caches/storage use managed identity (no passwords exist); auto-generate only app-internal secrets (e.g. `SECRET_KEY`) and store them on the compute resource (App Service app settings / Container Apps native secrets) via an `@secure()` param — no Key Vault.
## Known Platform Bugs
See [`pipeline-rules-runtime.md`](pipeline-rules-runtime.md) § Known Platform Bugs for the full bug table and workarounds.
## No top-level agent invocation
⛔ **NEVER call external agents** (`azure-validate`, `azure-deploy`, `azure-prepare`, etc.) during the AppOnboard pipeline. Only `azure-app-onboard-prereq` and `azure-app-onboard` orchestrator are allowed. Use direct CLI commands for validation and deployment.
## Structured sub-agent delegation
⛔ Use ONLY `subagent-*.md` templates — no ad-hoc prompts. Pass template content verbatim. Destructive commands (`az deployment`, `az webapp deploy`, `az acr build`) execute in main thread only.
## Security baseline
See [iac-generation-rules.md](../scaffold/references/iac-generation-rules.md) § Security Patterns and [bicep-patterns-security.md](../scaffold/references/bicep-patterns-security.md). Flag `AllowAzureServices` firewall rule as a security warning.
## azure.yaml prohibition
⛔ **NEVER generate `azure.yaml`. NEVER use `azd up`/`azd provision`/`azd deploy`.** AppOnboard deploys via `az deployment sub create` (Bicep) or `terraform apply` (Terraform). Repos with existing `azure.yaml` → route per [`azd-template-routing.md`](azd-template-routing.md).
@@ -1,64 +0,0 @@
<!-- 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.
# Session Protocol — Step 1
## All Prompts Are Actionable
> ⛔ **ALL prompts that activate this agent are actionable — go directly to Step 1.** Do NOT answer the user's question, give an overview of capabilities, or describe what AppOnboard can do before starting the pipeline. "Can Azure figure out my app?" and "Deploy my app" are the same action: Step 1 → Step 2 → scan. The user's phrasing (question vs command) does NOT change the workflow.
## Session Check
Resolve active session via pointer file.
> ⛔ **YOU MUST CREATE A SESSION BEFORE DOING ANY WORK — INCLUDING SCANNING**
>
> 1. **STOP** — Do not answer the user's question, scan code, or plan architecture yet
> 2. **CHECK** — Read `.copilot-azure/sessions/active-session.json`.
> - ⛔ **First, ensure the repo's `.gitignore` contains `.copilot-azure/`** (append if missing, create the file if absent) — this runs on EVERY path below, BEFORE any branch writes a session file, since session artifacts may hold deploy secrets.
> - **Pointer exists** → ⛔ **You MUST read [`session-schemas.ts`](session-schemas.ts)** to get the exact field names and types for `AppOnboardContext`. Do not guess field names. Then read the pointed-to session's `context.json`. Display: "Found session from [lastModifiedUtc] — {statusSummary}." ⛔ **You MUST ask the user via `ask_user`: "Resume this session or start fresh?" Do NOT auto-resume.** This gate is mandatory — stale sessions from prior tests cause the agent to skip phase instructions.md reads and miss artifact writes.
> - Resume → ⛔ **Refresh the deployment inventory baseline before any more provisioning.** Preserve the current `deploy-result.json.createdResources[]` and `orphanedResourceGroups[]`, then call `capture_deployment_inventory` with `phase: "baseline"`, using `context.json.sessionId` and `context.json.azure.subscriptionId`. If the subscription is missing or auth must be refreshed, complete the Azure login gate below first, but do not run `az deployment`, `az group create`, `az webapp deploy`, `az acr build`, or any other command that can create resources until the baseline call succeeds. This new baseline starts a resumed inventory segment: union later capture output with the preserved inventory by normalized resource ID and case-insensitive resource-group name; never discard cleanup evidence from before the resume. Then ⛔ **read the phase instructions.md for the NEXT phase** (derive from `completedPhases`). E.g., prereq done → read `prepare/instructions.md`, then continue from that phase.
> - ⛔ **If `context.json.routeToSkill` is set:** The previous session was halted for migration (e.g., `azure-cloud-migrate`). The code has likely changed since then. **Do NOT skip prereq** — start fresh: clear `routeToSkill`, `routeReason`, remove `"prereq"` from `completedPhases`, and re-run from Step 2. This ensures the migrated codebase gets a clean 3-axis evaluation.
> - ⛔ **If `completedPhases` includes `"prereq"` (and no `routeToSkill`):** Prereq already wrote `prereq-output.json` and `context.json.components[]`. Proceed to Step 2 (scope triage) — prereq may have been invoked standalone, so the user still needs to confirm the full pipeline. Skip Step 3 (prereq invocation), then continue to Step 4 (scan-informed intent gathering).
> - Start fresh → generate a new UUID via `[guid]::NewGuid().ToString()`, create a new session folder, update `active-session.json` to point to the new session. Old session folder is never touched again.
> - **Pointer missing but session folders exist** → list folders under `.copilot-azure/sessions/`. If 1 folder: adopt it (read its `context.json`, write `active-session.json` pointing to it, show summary). If 2+: show a numbered list with `statusSummary` + `lastModifiedUtc` from each, ask user to pick one or start fresh. Write pointer for the chosen session.
> - **No sessions at all** → generate a UUID by running `[guid]::NewGuid().ToString()` in the terminal. ⛔ **You MUST generate the UUID via a terminal command — do NOT hardcode a placeholder like `a1b2c3d4-e5f6-7890-abcd-ef1234567890`.** Create the session directory: `New-Item -ItemType Directory -Path ".copilot-azure/sessions/{uuid}" -Force`. Then write a **minimal** `context.json` using the `create` tool — only these 3 fields are known immediately: `{ "sessionId": "{uuid}", "createdUtc": "{ISO 8601 now}", "intent": { "userPrompt": "{user's first message verbatim}" } }`. Write `active-session.json` with `activeSessionId: {uuid}` using the `create` tool.
> 3. **PRUNE** — After resolving the active session, check remaining session folders. Delete any where `context.json.lastModifiedUtc` is >7 days ago. **Never delete the active session** (the one `active-session.json` points to).
> 4. **VERIFY** — Confirm `context.json` exists and is valid JSON. If missing or malformed, halt and retry creation — do NOT continue to Step 2 without a verified session.
> 5. **CONFIRM** — Begin your first response with: "Started session at `.copilot-azure/sessions/{uuid}/`" (new) or "Resuming session from [date] — {statusSummary}" (existing)
> 6. **THEN** proceed to Step 2
>
> ⛔ **Ordering: session FIRST, scanning SECOND.** If you scan the workspace or read project files before writing `context.json`, you have violated the session-first rule. The session must exist before ANY code analysis.
>
> ⛔ **Shell fallback:** If PowerShell/terminal hangs on first attempt (no output after 10s), use the `create` tool directly for session directory and file writes. Do NOT retry shell commands more than once.
>
> ⛔ **Path scoping: ALL `create` tool calls for session artifacts MUST target `.copilot-azure/sessions/{active-session-id}/`.** Writing to any other session folder is forbidden.
## CLI Availability
Call `mcp_azure_mcp_extension_cli_install` with `cli-type: "az"` to verify Azure CLI is available. If missing, surface installation instructions before proceeding. Downstream phases (prepare, deploy) require it. Fallback: skip if MCP tool unavailable.
## Azure Login Gate
**Azure login gate (mandatory):** Run `az account show --query "{id:id, name:name, tenantId:tenantId}" -o json` with a **15-second timeout** (PowerShell: `Start-Process` with `-Wait` or inline timeout; if command hangs beyond 15s, treat as failure). Also run `az ad signed-in-user show --query displayName -o tsv` (15-second timeout). After BOTH commands complete, merge ALL azure fields into `context.json.azure` in a **SINGLE update** — `subscriptionId`, `subscriptionName`, `tenantId`, and `userDisplayName`. Do NOT write separate updates for subscription and identity.
> ⛔ **If `az account show` fails or hangs:** ⛔ **You MUST read [`subscription-resolution.md`](subscription-resolution.md)** and follow its fallback procedure. Do NOT proceed to Step 2 without a resolved subscription. Do NOT leave `context.json.azure` empty and continue. Every downstream phase (prepare, scaffold validation, deploy) requires Azure auth — proceeding without it produces incomplete results.
## User Identity Detection
**User identity detection (for `deployed-by` tag):** Run `az ad signed-in-user show --query displayName -o tsv` (15-second timeout) alongside `az account show`. Fallback if `az ad` fails: use `az account show --query user.name -o tsv` (returns UPN/email). If both fail, leave empty — prepare phase will resolve. This value becomes the `deployed-by` tag on ALL resources — resolving it once here prevents inconsistent tag values across resources. **Merge into the SAME `context.json` update as the azure login gate — do NOT write separately.**
## Subscription Detection Method
> ⛔ **`az account show` is the ONLY subscription detection method in Step 1.** Do NOT call `mcp_azure_mcp_subscription_list` here — that tool returns ALL subscriptions across ALL tenants and causes a lengthy picker detour. `az account show` returns the CLI's active subscription in <1 second. MCP subscription list is reserved for prepare Step 1 when the user explicitly wants a different subscription.
## Artifact Locations
| Location | Artifacts |
|----------|-----------|
| `.copilot-azure/sessions/{uuid}/` | `context.json`, `prereq-output.json`, `prepare-plan.json`, `scaffold-manifest.json`, `deploy-result.json` |
| `.copilot-azure/sessions/{uuid}/replaced-files/` | User files displaced by scaffold (existing IaC), stored at their original relative path (**mirror path** = same directory structure as the repo). Never overwritten or deleted — moved here so the original is preserved. |
## Phase-gated Reference Loading
> ⛔ **Phase-gated reference loading.** Do NOT pre-read reference files for downstream phases. Read each phase's references only when entering that step. Scaffold references (bicep-patterns, self-review) are irrelevant during deploy; prepare references (service-mapping, pricing-guide) are irrelevant during scaffold. Each phase instructions.md specifies its own required reads.
@@ -1,152 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Context + shared TypeScript interfaces for AppOnboard session artifacts:
* context.json, active-session.json, and shared types used across all phases.
*
* Per-phase schemas (each phase has its own in its references/ folder):
* - azure-app-onboard-prereq/references/prereq-schemas.ts — prereq-output.json
* - prepare/references/prepare-schemas.ts — prepare-plan.json
* - scaffold/references/scaffold-schemas.ts — scaffold-manifest.json
* - deploy/references/deploy-schemas.ts — deploy-result.json
*
* Source of truth for JSON artifacts in `.copilot-azure/sessions/{session-id}/`.
*/
// ─── shared types (used across all phases) ───────────────────────────────────
export interface AppOnboardComponentStack {
language: string;
framework: string;
version: string;
}
export type ReadinessStatus = "ready" | "fixesApplied" | "needsFixes" | "unknown";
export interface AppOnboardComponentReadiness {
status: ReadinessStatus;
fixes: string[];
}
export type VerdictLevel = "PASS" | "WARN" | "FAIL" | "SKIPPED";
export interface AppOnboardComponentVerdicts {
build: VerdictLevel;
completeness: Exclude<VerdictLevel, "SKIPPED">;
deployability: Exclude<VerdictLevel, "SKIPPED">;
}
export interface AppOnboardComponentFinding {
category: "build" | "completeness" | "deployability";
verdict: VerdictLevel;
summary: string;
fix: string | null;
}
export interface AppOnboardComponent {
name: string;
path: string;
stack: AppOnboardComponentStack;
readiness: AppOnboardComponentReadiness;
verdicts?: AppOnboardComponentVerdicts;
findings?: readonly AppOnboardComponentFinding[];
}
export interface AppOnboardAzureTarget {
subscriptionId: string;
/** Display name of the subscription (from `az account show --query name`).
* Shown at both approval gates so the user can verify the target. */
subscriptionName: string;
resourceGroup: string;
region: string;
}
export interface AppOnboardRepoInfo {
remote: string | null;
}
export interface AppOnboardOverride {
key: string;
value: string;
reason: string;
}
export interface AppOnboardAppInfo {
name: string;
}
export interface PostDeployRecommendation {
title: string;
reason: string;
effort: "low" | "medium" | "high";
services?: string[];
}
export interface DetectedService {
type: string;
version?: string;
source: "compose" | "config" | "code";
}
// ─── context.json ─────────────────────────────────────────────────────────────
export interface AppOnboardIntent {
userPrompt: string;
description: string;
users?: string;
auth?: string;
scale?: string;
budget?: string;
/** Set to true after prereq scan refines intent */
refinedFromScan?: boolean;
/** Facts discovered by the prereq scan */
scanDiscoveredFacts?: string[];
}
export type AppOnboardPhase = "info" | "prereq" | "prepare" | "scaffold" | "deploy" | "cicd" | "observe";
export interface AppOnboardContext {
sessionId: string;
createdUtc: string;
lastModifiedUtc: string;
currentPhase: AppOnboardPhase | null;
completedPhases: readonly AppOnboardPhase[];
/** Human-readable 1-line summary of where the session stands, updated at each phase exit.
* Displayed in the session picker when the user resumes or switches sessions. */
statusSummary?: string;
intent: AppOnboardIntent;
components: AppOnboardComponent[];
azure: AppOnboardAzureTarget;
repo: AppOnboardRepoInfo;
app?: AppOnboardAppInfo;
/** Infrastructure file types detected in repo: dockerfile, terraform, bicep, azure-yaml, github-actions */
detectedInfra: readonly string[];
/** Cloud provider targeted by detected IaC. Only populated when `.tf` or `.bicep` files found.
* Used by scaffold to distinguish "existing Azure IaC" (halt) from "non-Azure IaC" (generate Azure TF alongside). */
detectedInfraProvider?: {
terraform?: "azure" | "gcp" | "aws" | "multi" | "unknown";
};
/** Service dependencies parsed from docker-compose, config files, or code imports */
detectedServices: readonly DetectedService[];
overrides: AppOnboardOverride[];
/** Set when a phase routes the pipeline to another agent — e.g. "azure-cloud-migrate"
* (non-Azure cloud SDK deps) or "azure-prepare" (existing azd template). Presence halts
* the greenfield pipeline; the resume path in session-protocol.md clears it and re-runs prereq. */
routeToSkill?: string;
/** Human-readable reason paired with `routeToSkill` (e.g. "existing-azd-template"). */
routeReason?: string;
}
// ─── active-session.json ─────────────────────────────────────────────────────
/** Pointer file at `.copilot-azure/sessions/active-session.json`.
* Avoids scanning all session folders on startup — read this one file
* to find the active session, then read that session's context.json. */
export interface ActiveSessionPointer {
activeSessionId: string;
}
// PrereqOutput, BuildRequirements → see azure-app-onboard-prereq/references/prereq-schemas.ts
@@ -1,17 +0,0 @@
<!-- 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.
# Subscription Resolution — Defensive Fallback
The `azure-app-onboard` orchestrator resolves the subscription at Step 1 (login hard gate) and writes `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure` before any phase runs. In normal operation, `context.json.azure.subscriptionId` is always set by the time prepare runs.
At prepare phase entry, verify `context.json.azure.subscriptionId` is set. If it is (expected path), use it — done.
If `context.json.azure` is somehow empty, resolve now rather than halting the flow:
1. **Check env vars** — if `AZURE_SUBSCRIPTION_ID` is set, use it directly (with `AZURE_TENANT_ID` if set). Write `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure`, done.
2. **Run `az account show`** — `az account show --query "{id:id, name:name, tenantId:tenantId}" -o json`. If it succeeds, **auto-select** — write `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure`. Do NOT run `az account list` or present a picker.
3. **Fallback: `mcp_azure_mcp_subscription_list` + picker** — only if `az account show` fails. Call `mcp_azure_mcp_subscription_list` to retrieve all subscriptions (returns `subscriptionId`, `displayName`, `isDefault`).
- **1 subscription** → auto-select, no question. Write `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure`.
- **2+ subscriptions** → present a picker via `ask_user`: list each subscription as a choice `"{displayName} ({subscriptionId})"` with the default marked. The user selects one. Write `subscriptionId`, `subscriptionName`, `tenantId` to `context.json.azure`.
4. **MCP tool fails** → attempt `az login` (interactive browser login). If that fails (no browser, remote session), fall back to `az login --use-device-code`. On success, retry from step 2. **Cap login at 3 attempts total** — if login still fails after the 3rd attempt, **HALT once** with a clear, actionable message: the exact `az login --tenant <tenant>` command to run, and that re-invoking the agent resumes this session (completed phases are preserved). Do NOT retry past 3 attempts, and do NOT proceed without a resolved subscription.
@@ -1,193 +0,0 @@
<!-- 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.
# Azure App Onboard Scaffold — IaC Generation + Self-Review
Generate deployment-ready infrastructure code from an architecture plan, verify it with adversarial self-review, and bridge to validation — all without deploying.
## Quick Reference
| Property | Value |
|----------|-------|
| Parent | [azure-app-onboard](../instructions.md) |
| Best for | Turning `prepare-plan.json` service list into Bicep templates with secure-by-default patterns |
| Inputs | `prepare-plan.json` (services, naming, quotas), `context.json` (overrides, components, repo info) |
| Outputs | `scaffold-manifest.json`, generated IaC files in `infra/` |
| Pipeline position | Phase 3 of 4: prereq → prepare → **scaffold** → deploy |
| IaC format | Bicep (v1 default). Terraform when existing `.tf` detected or user override. |
## When to Use This Agent
Invoked by the `azure-app-onboard` orchestrator at Phase 3 when `prepare-plan.json` exists with `services[]`. Not directly user-routable in v1.
> **Return to orchestrator:** When complete, return control to `azure-app-onboard`. Do NOT directly invoke deploy — the orchestrator manages phase transitions.
## When NOT to Use
| Scenario | Use Instead |
|----------|-------------|
| User-triggered IaC (no `prepare-plan.json`) | `azure-prepare` |
| Subscription-scope landing zones | `azure-enterprise-infra-planner` |
| Execute deployment (`azd up`) | `azure-deploy` (do NOT invoke from AppOnboard pipeline) |
## MCP Tools
> See [shared tools](../references/mcp-tool-reference.md) for cross-phase tools and global parameters. See [scaffold tools](references/mcp-tools.md) for full parameter tables.
| Tool | Sub-command | Purpose | Parameters |
|------|-----------|---------|------------|
| `mcp_azure_mcp_bicepschema` | `bicepschema_get` | ARM resource type schemas | `resource_type` (Required), `api_version` (Optional) |
| `mcp_bicep_list_avm_metadata` | *(flat)* | AVM module catalog | None |
| `mcp_bicep_get_bicep_best_practices` | *(flat)* | Bicep best practices | None |
| `mcp_bicep_get_az_resource_type_schema` | *(flat)* | ARM resource type JSON schema | `azResourceType`, `apiVersion` (Required) |
| `mcp_bicep_build_bicep` | *(flat)* | Validate `.bicep` files (self-review L3) | `filePath` (Required) |
| `mcp_bicep_format_bicep_file` | *(flat)* | Format `.bicep` files (LF enforcement) | `filePath` (Required) |
| `mcp_azure_mcp_deploy` | `deploy_iac_rules_get` | IaC best practices and rules | `deployment-tool`, `iac-type`, `resource-types` |
| `mcp_azure_mcp_deploy` | `deploy_pipeline_guidance_get` | CI/CD pipeline config | `is-azd-project`, `pipeline-platform`, `deploy-option` |
| `mcp_azure_mcp_get_azure_bestpractices` | `get_azure_bestpractices_get` | SDK/Functions best practices | `resource`, `action` |
| `mcp_azure_mcp_azureterraformbestpractices` | *(flat)* | Terraform patterns (TF path only) | `resource_type` (Required) |
## Workflow
**Session folder:** `.copilot-azure/sessions/{uuid}/` — reads `prepare-plan.json` + `context.json`, writes `scaffold-manifest.json`.
### DETECT (Steps 1–4)
1. **Read `prepare-plan.json`** — verify `services[]` exists, read `naming` config (especially `naming.resourcePrefix`, `naming.suffix`, `naming.resources[]`). Read resource group name from `context.json.azure.resourceGroup`. ⛔ **Use EXACTLY these names in generated IaC — do NOT invent names, derive them from `environmentName`, or append your own suffixes.** ⛔ **Use EXACTLY the names from `prepare-plan.json.naming.resources[]` as Bicep parameters. Do NOT derive names with `take()`, `substring()`, or string manipulation. The plan is the source of truth.** Missing → trigger prepare backfill via `azure-app-onboard` orchestrator.
2. **Read `context.json`** — check `overrides[]` for `iacFormat` preference, `detectedInfra[]` for existing `.tf`, `detectedInfraProvider` for cloud provider classification.
3. **Check workspace for existing IaC** — ⛔ **Skip** if `context.json.overrides[]` contains `ignoreExistingInfra: true`. Otherwise:
- **Azure IaC** (`.bicep`, `azure.yaml`, `.tf` with `azurerm`): `ask_user` → "Start fresh" (rename `infra/` to `infra.bak/`) or "Use existing" (route to `azure-prepare`, stop pipeline).
- **Non-Azure IaC** (`.tf` with GCP/AWS): respect `context.json.overrides[].iacFormat` from prepare. Default: Bicep alongside existing TF.
- **Unknown TF** (`detectedInfraProvider.terraform` == `"unknown"`): ask user which provider before routing.
- **No IaC**: continue.
4. **Determine compute targets** — Check which compute targets are in the plan (App Service/Functions, Container Apps, or both) and whether PostgreSQL/Redis is present. Do NOT read any reference files — pass this info to the sub-agent at Step 5.
4b. **Pre-check API versions (main thread)** — MCP tool access is unreliable in `task` agents — call these in the main thread before dispatching. Call `mcp_bicep_list_az_resource_types_for_provider` (or `bicep-list_az_resource_types_for_provider`) once per provider namespace in `prepare-plan.json.services[]` (e.g., `Microsoft.Web`, `Microsoft.App`, `Microsoft.DBforPostgreSQL`, `Microsoft.Cache`, `Microsoft.ContainerRegistry`). Extract the latest GA API version (no `-preview`) for each resource type. Build an `apiVersions` map and pass it to the IaC gen sub-agent at Step 5. Fallback: if MCP unavailable, run `az provider show --namespace {ns} --query "resourceTypes[?resourceType=='{type}'].apiVersions[?!contains(@, 'preview')] | [0][0]" -o tsv` per resource type — this filters to GA-only and picks the latest. Pass `"MCP unavailable"` only if both MCP AND CLI fail. Sub-agent still validates generated Bicep via `az bicep build`.
### ACTION (Steps 5–12)
> ⛔ **File boundary:** NEVER modify files outside `infra/`, `.copilot-azure/`. Scaffold only writes files — no install/build commands.
> ⛔ **Sub-agent delegation is MANDATORY for Steps 5, 6–9, and 10–12.** Each step reads its `subagent-*.md` template, then dispatches a `task` call. Do NOT read any reference file not explicitly named in these steps.
>
> ⛔ **Dispatch type: `task` ONLY — NEVER `general-purpose`.** `general-purpose` leaks sub-agent context into the main thread, accelerating compaction and evicting the orchestrator workflow. `task` isolates sub-agent context.
>
> ⛔ **How to dispatch — VERBATIM COPY required:**
> 1. `view` the `subagent-*.md` template file
> 2. Your **NEXT action MUST be a `task` tool call** — not `view`, `powershell`, `create`, or ANY other tool
> 3. The task prompt MUST contain the **COMPLETE and UNMODIFIED** template text. Copy the template between `<<<TEMPLATE_START>>>` / `<<<TEMPLATE_END>>>` delimiters exactly as shown below. Do NOT summarize, paraphrase, reword, or omit ANY part of it — the sub-agent needs every "Read [file]" and "Do:" instruction to produce correct output
> 4. AFTER the template block, append the data sections (plan JSON, overrides, etc.)
>
> **Anti-pattern (causes regressions):** Writing your OWN prompt that lists workflow steps or describes what to generate. The template already contains the complete workflow — your job is to COPY it, not rewrite it.
5. **IaC generation** — ⛔ **You MUST dispatch [`subagent-iac-gen.md`](references/subagent-iac-gen.md) as a `task`.** ⛔ agent_type: `"task"` — NEVER `"general-purpose"`.
```
<<<TEMPLATE_START>>>
{paste the ENTIRE content of subagent-iac-gen.md here — unmodified}
<<<TEMPLATE_END>>>
## Data (appended by orchestrator)
### prepare-plan.json
{full JSON}
### context.json.overrides
{overrides array}
### prereq-output.json.buildRequirements
{buildRequirements object}
### prereq-output.json.warnings[]
{warnings array}
### Compute targets
{App Service/Functions, Container Apps, or both + whether PostgreSQL/Redis present}
### apiVersions
{map from Step 4b, e.g. {"Microsoft.Web/sites": "2023-12-01", ...} — or "MCP unavailable" if skipped}
### Working directory
{absolute path}
```
- **Expect:** IaC files written to `infra/`, file list returned for `scaffold-manifest.json.files[]`
- The tag `app-onboard-skill: 'true'` MUST appear verbatim in generated Bicep.
5b. **Deploy checklist (parallel with Step 5)** — Dispatch as a `task` **in parallel** with the IaC gen subagent above. ⛔ agent_type: `"task"` — NEVER `"general-purpose"`.
```
<<<TEMPLATE_START>>>
You are a deploy-checklist generator. Do NOT invoke any agents.
1. Read the deploy-checklist-template at: .github/agents/azure-deploy/deploy/references/deploy-checklist-template.md
2. Fill in {placeholders} with real values from prepare-plan.json (appName, rgName, subscriptionId, sessionId).
3. Delete sections that don't apply to this deployment's compute target (e.g., remove App Service section for Container Apps deploys). The template section headers indicate which to delete.
4. Write the result to the session folder using the `create` tool. This file survives conversation compaction — deploy re-reads it after every long-running command.
<<<TEMPLATE_END>>>
## Data (appended by orchestrator)
### prepare-plan.json
{full JSON}
### Session path
{.copilot-azure/sessions/{uuid}/}
### Compute targets
{App Service, Container Apps, Static Web Apps, or combination}
```
- **Expect:** `deploy-checklist.md` written to session folder. If this subagent fails, the validate subagent (Steps 10b–12.5) will catch the missing file.
6–9. **Self-review** — ⛔ **You MUST dispatch [`subagent-review.md`](references/subagent-review.md) as a `task`.** ⛔ agent_type: `"task"` — NEVER `"general-purpose"`.
```
<<<TEMPLATE_START>>>
{paste the ENTIRE content of subagent-review.md here — unmodified}
<<<TEMPLATE_END>>>
## Data (appended by orchestrator)
### Generated IaC files
{full content of every .bicep/.tf file}
### prepare-plan.json (services, naming, deploymentVariables)
{relevant sections}
### prereq-output.json.warnings[]
{warnings array}
```
- **Expect:** findings JSON → write to `scaffold-manifest.json.selfReview`
- FLAGGED at L1/L3 → fix IaC before proceeding
### VALIDATE → MANIFEST → APPROVE (Steps 10–12.5)
10a. **Format IaC (main thread)** — For each `.bicep` file in `infra/` (including `modules/`): call `mcp_bicep_format_bicep_file` (or `bicep-format_bicep_file`) with `{ filePath: "<absolute path>" }`.This enforces LF line endings via the `bicepconfig.json` written during IaC generation. Fallback: skip if unavailable.
10a-conf. **Conformance gate (main thread — MANDATORY for Bicep)** — ⛔ **Skip this entire step when the scaffold emitted Terraform** (`infra/main.bicep` absent) — these checks are Bicep-only (Terraform is syntax-validated via `terraform validate` in the validate subagent). Otherwise run the conformance script from this phase's `scripts/` dir; it deterministically catches ARM-rejected values and policy violations `az bicep build` can't (invalid Bicep values, wrong DB version, any Key Vault → `NO-KEYVAULT`, DB admin login/password → `DB-NO-LOCAL-AUTH`, free/shared SKUs → `NO-FREE-SKU`):
```
{scaffoldDir}/scripts/scaffold-conformance.ps1 -SessionPath ".copilot-azure/sessions/{uuid}" -InfraPath infra # pwsh (preferred)
bash {scaffoldDir}/scripts/scaffold-conformance.sh ".copilot-azure/sessions/{uuid}" infra # bash (only if pwsh unavailable; needs jq for the plan-dependent checks)
```
⛔ **Prefer the `.ps1` when `pwsh` is available** — it runs every check unconditionally. The `.sh` twin skips the plan-dependent checks (`DB-VERSION-MATCH`, `SERVICES-COMPLETE`, `DB-NAME-PRESENT`, `WARN-FIXED`) when `jq` is absent.
⛔ Any BLOCK failure → fix the IaC, re-run (max 3); never present the deploy gate with an open BLOCK. Run it here in the main thread — do NOT delegate to the validate subagent or hand-judge the result when a shell exists. Pass the JSON to the validate subagent for `scaffold-manifest.json.conformance`.
10b–12.5. **Validation + manifest** — ⛔ **You MUST dispatch [`subagent-validate.md`](references/subagent-validate.md) as a `task`.** ⛔ agent_type: `"task"` — NEVER `"general-purpose"`.
```
<<<TEMPLATE_START>>>
{paste the ENTIRE content of subagent-validate.md here — unmodified}
<<<TEMPLATE_END>>>
## Data (appended by orchestrator)
### IaC file paths
{list of generated files}
### Self-review findings (from Steps 6–9)
{findings JSON}
### prepare-plan.json
{full JSON}
### prereq-output.json.warnings[]
{warnings array}
### prereq-output.json.healthEndpoint
{detected health path string or null}
### Conformance result
{JSON from Step 10a-conf}
### Session path
{.copilot-azure/sessions/{uuid}/}
```
- **Expect:** `scaffold-manifest.json` with `validationResult`, deploy checklist generated
- Verify `deploy-checklist.md` exists (written at Step 5b) — if missing, create NOW from [`deploy-checklist-template.md`](../deploy/references/deploy-checklist-template.md). Verify `deploy-result.json` exists — if missing, create from [`deploy-schemas.ts`](../deploy/references/deploy-schemas.ts).
- ⛔ **Verify `context.json` update (main-thread — do NOT delegate).** Read `.copilot-azure/sessions/{uuid}/context.json`. If `completedPhases` does not include `"scaffold"` OR `currentPhase` is not `"deploy"`, write it yourself via `edit` / `create`: append `"scaffold"` to `completedPhases`, set `currentPhase` to `"deploy"`, update `lastModifiedUtc` to current UTC ISO 8601. This is a phase-boundary write required by [pipeline-rules.md](../references/pipeline-rules.md) — do not skip it.
- ⛔ **Return to orchestrator for Step 8 (Deploy Approval Gate).** YOUR NEXT ACTION MUST BE presenting the Deploy Gate per orchestrator instructions.md — do NOT write a "summary of generated files" message, do NOT emit a completion report. The Deploy Gate prompt (`🚀 Ready to deploy? ...`) is the ONLY correct next output.
## Self-Healing Loop
On validation failure → read [`scaffold-healing-rules.md`](references/scaffold-healing-rules.md) (healing cadence, PLAN_LEVEL_CHANGE, artifact consistency). Do NOT pre-read.
## Error Handling
- **Missing `prepare-plan.json`:** trigger backfill via orchestrator.
- **Existing IaC:** handled in DETECT Step 3.
- **MCP unavailable:** fall back to reference patterns, flag as "unverified."
- FLAGGED findings and healing exhaustion: see [scaffold-healing-rules.md](references/scaffold-healing-rules.md).
@@ -1,113 +0,0 @@
<!-- 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.
# Bicep — App Service Patterns
App Service-specific Bicep patterns. For shared patterns (skeleton, naming, tags, security defaults, data modules), see [bicep-patterns.md](bicep-patterns.md).
> ⛔ **Azure Functions on Flex Consumption (the default Functions floor) is NOT an App Service — do NOT use this module for it.** Flex uses a different resource shape (`functionAppConfig`) and a different deploy channel; see [bicep-functions-flex.md](bicep-functions-flex.md). This module applies to App Service web apps and to Functions only on **Premium (EP1)**.
## Module Template
Standard App Service module with managed identity and SCM/FTP auth disabled. Use this as the base for ALL App Service resources.
```bicep
param location string
param tags object
param appServicePlanId string
param appServiceName string
resource appService 'Microsoft.Web/sites@2023-12-01' = {
name: appServiceName
location: location
tags: tags
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: appServicePlanId
httpsOnly: true
siteConfig: {
minTlsVersion: '1.2'
ftpsState: 'Disabled'
// Only when this App Service is an API called by a browser frontend on a DIFFERENT hostname.
// Omit entirely for a full-stack app that serves its own UI — that is same-origin and needs no CORS.
// See bicep-patterns.md § Cross-Origin (CORS).
cors: {
allowedOrigins: [ 'https://${staticWebApp.properties.defaultHostname}' ]
supportCredentials: false
}
}
}
}
// SCM basic auth — enabled in IaC for deploy convenience. Deploy phase re-disables via REST API after code upload.
resource scmAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2023-12-01' = {
parent: appService
name: 'scm'
properties: {
allow: true
}
}
// ⛔ MANDATORY — disable FTP basic auth
resource ftpAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2023-12-01' = {
parent: appService
name: 'ftp'
properties: {
allow: false
}
}
output appServiceId string = appService.id
output principalId string = appService.identity.principalId
```
> ⛔ **Every App Service module MUST include:** (1) `identity: { type: 'SystemAssigned' }`, (2) `scm` basicPublishingCredentialsPolicies with `allow: true` (IaC sets enabled for deploy convenience — deploy phase re-disables via REST API after code upload), (3) `ftp` basicPublishingCredentialsPolicies with `allow: false`. Missing any of these → self-review L1 `FLAGGED`.
## Native Module Deploy Strategy
When `prepare-plan.json.deployStrategy` exists with `codeDeployPattern: "startup-install"`, apply these patterns to the App Service Bicep:
```bicep
resource appService 'Microsoft.Web/sites@2023-12-01' = {
name: appServiceName
location: location
tags: tags
properties: {
serverFarmId: appServicePlan.id
httpsOnly: true
siteConfig: {
linuxFxVersion: 'NODE|${nodeVersion}' // ⛔ Use exact version tag (18-lts, 20-lts) — NEVER tilde (~18). Tilde works for WEBSITE_NODE_DEFAULT_VERSION but NOT linuxFxVersion.
ftpsState: 'Disabled'
minTlsVersion: '1.2'
// Startup command: Oryx build is primary, this is the safety-net fallback.
// Runs npm install only if node_modules doesn't exist (Oryx missed it).
// MUST be inline — never a .sh file (Windows CRLF → bash exit code 2).
appCommandLine: '${deployStrategy.startupCommand}'
appSettings: [
// Primary: tell Oryx to run npm install during zip deploy
{ name: 'SCM_DO_BUILD_DURING_DEPLOYMENT', value: 'true' }
{ name: 'ENABLE_ORYX_BUILD', value: 'true' }
// Extended timeout for native module compilation (max 1800, default 230)
{ name: 'WEBSITES_CONTAINER_START_TIME_LIMIT', value: '1800' }
// App-specific settings
{ name: 'NODE_ENV', value: 'production' }
]
}
}
}
```
**Rules:**
- ⛔ **Inline `appCommandLine` only** — never generate a `.sh` startup script file. Files created on Windows have CRLF line endings → bash exit code 2 on Linux
- ⛔ **Entry point from manifest** — read `package.json.scripts.start` or `.main`, never hardcode `index.js`
- ⛔ **`WEBSITES_CONTAINER_START_TIME_LIMIT` = 1800** (the maximum). Native compilation takes 2-5 min; Python with scipy can take longer
- When `deployStrategy` is absent (no native modules), do NOT set `appCommandLine` — let Oryx use its default startup
- ⛔ **Never prefix startup with `cd /home/site/wwwroot`** — Oryx extracts build output to a temp directory and sets the working directory automatically. Hardcoding `cd /home/site/wwwroot` causes `MODULE_NOT_FOUND` / `Could not import` because the app files aren't there
**Self-review check (L2 Pattern):** If `hasNativeModules == true`, verify Bicep has BOTH `appCommandLine` and `WEBSITES_CONTAINER_START_TIME_LIMIT`. If `prereq-output.json.initCommands[]` has `required: true` entries, verify `appCommandLine` includes them before the app start command. **FLAGGED** if either check fails.
## Identity Output — SystemAssigned vs UserAssigned
> ⛔ **`appService.identity.principalId` only exists for `SystemAssigned` identity.** When using `UserAssigned`, output the managed identity MODULE's `principalId` instead — `identity.principalId` is undefined and causes `DeploymentOutputEvaluationFailed`. Every App Service has a managed identity (mandatory — the compute floor is B1, which supports MI; F1/D1/Free are never selected).
@@ -1,189 +0,0 @@
<!-- 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.
# Bicep — Container Apps Patterns
Container Apps-specific Bicep patterns. For shared patterns (skeleton, naming, tags, security defaults, data modules), see [bicep-patterns.md](bicep-patterns.md).
## Two-Phase Wiring
Container Apps + ACR requires two-phase deployment (circular dependency: CA needs ACR image, ACR needs CA identity for AcrPull):
1. **Phase 1:** Deploy Container App with placeholder image (`mcr.microsoft.com/azuredocs/containerapps-helloworld:latest`). ⛔ **No `registries` block.** The placeholder image is pulled from MCR (public). Use `registries: []`. Native `secrets` (literal values) MAY be set in Phase 1 — they have no RBAC dependency. **The AcrPull role assignment IS created in Phase 1** — it doesn't affect the placeholder deployment and needs 1–2 minutes to propagate before Phase 2.
2. **Phase 2:** Build + push app image to ACR, redeploy with real image + `registries`. AcrPull RBAC is already propagated from Phase 1.
> ⛔ **Placeholder image listens on port 80, not your app's port.** Set `targetPort` conditionally: `var effectivePort = containerImage == 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' ? 80 : appPort`. Mismatched ports cause "Operation expired" (health probe can't reach container).
> ⛔ **`containerImage` param must exist in BOTH `main.bicep` AND the container app module.** Phase 2 passes `--parameters containerImage='...'` via CLI — if `main.bicep` lacks the param, the override is silently ignored and the placeholder persists.
```bicep
// In main.bicep: thread containerImage to module
param containerImage string = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
module containerApp './modules/containerapp.bicep' = {
params: { containerImage: containerImage /* ...other params... */ }
}
// In containerapp.bicep:
param containerImage string = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
var isPlaceholder = containerImage == 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
resource containerApp 'Microsoft.App/containerApps@2024-03-01' = {
identity: { type: 'SystemAssigned' }
properties: {
configuration: {
ingress: {
external: true
targetPort: isPlaceholder ? 80 : appPort
allowInsecure: false // ⛔ MANDATORY
}
registries: isPlaceholder ? [] : [{ server: acr.properties.loginServer, identity: 'system' }]
secrets: [ { name: 'secret-key', value: secretKey } ] // native CA secrets (no RBAC dep) — NOT KV
}
template: {
containers: [{
image: containerImage
env: [{ name: 'PORT', value: string(isPlaceholder ? 80 : appPort) }]
}]
}
}
}
```
> ⛔ **Do NOT set `revisionSuffix`.** Omit it entirely — ARM auto-generates unique revision names. Hardcoding `revisionSuffix: 'v1'` causes Phase 2 redeploy to fail with "revision with suffix v1 already exists."
### AcrPull Role Assignment
> ⛔ **AcrPull role GUID: `7f951dda-4ed3-4680-a7ca-43fe172d538d`.** Copy verbatim — wrong GUIDs cause `RoleDefinitionDoesNotExist`.
```bicep
resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(acr.id, containerApp.id, '7f951dda-4ed3-4680-a7ca-43fe172d538d')
scope: acr
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
principalId: containerApp.identity.principalId
principalType: 'ServicePrincipal'
}
}
```
## Log Analytics Workspace Key
> ⛔ **Use `resource.listKeys()`, NOT `reference()`.** `reference()` does not expose `primarySharedKey`.
```bicep
// ✅ Correct
var laKey = logAnalyticsWorkspace.listKeys().primarySharedKey
// ❌ Wrong
var laKey = reference(logAnalyticsWorkspace.id, '2023-09-01').primarySharedKey
```
### Log Analytics customerId vs resource ID
> ⛔ **Output BOTH `id` and `customerId` from the log-analytics module.** Container Apps Environment needs the GUID `customerId`. App Insights needs the ARM resource ID. Do NOT use `split(workspaceId, '/')[8]` — that extracts the workspace name, not the GUID.
```bicep
// log-analytics.bicep outputs:
output id string = logAnalyticsWorkspace.id // ARM resource ID
output customerId string = logAnalyticsWorkspace.properties.customerId // GUID
output sharedKey string = logAnalyticsWorkspace.listKeys().primarySharedKey
// container-app-environment.bicep:
param workspaceCustomerId string // GUID, NOT resource ID
// ⛔ MUST nest under appLogsConfiguration.destination='log-analytics' — a bare top-level logAnalyticsConfiguration fails deploy (ManagedEnvironmentInvalidSchema). This nesting is the ONLY valid location at EVERY API version (the flat shape was never valid — NOT version drift, so do not chase API-version pins). This is the CA's only log path (no diagnostic-settings module).
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: workspaceCustomerId
sharedKey: workspaceSharedKey
}
}
}
// ❌ WRONG: customerId: split(workspaceId, '/')[8]
```
## Ingress & Port Mapping
> ⛔ **Container resource limits:** Use decimal format for memory: `'0.5Gi'`, `'1Gi'`, `'2Gi'` — NOT Kubernetes-style `'512Mi'`. CPU must be type `string`: `'0.25'`, `'0.5'`, `'1'`. Valid combos: `0.25/0.5Gi`, `0.5/1Gi`, `0.75/1.5Gi`, `1/2Gi`, `1.25/2.5Gi`, `1.5/3Gi`, `1.75/3.5Gi`, `2/4Gi`.
> ⛔ **ACR module:** `retentionPolicy` is **Premium-only**. For Basic/Standard ACR, omit `retentionPolicy` entirely — ARM rejects it.
## Container Apps Secrets (Native — No Key Vault)
> ⛔ **No Key Vault.** Container Apps store app-internal secrets in the app's **native** `secrets` array with a literal `value` supplied by an `@secure()` param at deploy time. NEVER use `keyVaultUrl`, `@Microsoft.KeyVault(...)`, or a KV `secrets` child resource.
> ⛔ **Only app-internal secrets use `secretRef`.** Database, cache, and storage access is managed-identity + token — wire their **connection parameters as plain `env` values** (host, db name, MI username, `sslmode=require`), never a `secretRef`. `secretRef` is for things like `SECRET_KEY`/JWT/third-party API keys.
> Native secrets have no RBAC dependency, so they don't need the `isPlaceholder` gate that KV secretRefs required (two-phase wiring is still needed for ACR registries + real image — see § Two-Phase Wiring).
```bicep
@secure()
param secretKey string // generated at deploy, passed via CLI --parameters, never committed
resource containerApp 'Microsoft.App/containerApps@2024-03-01' = {
identity: {
type: 'SystemAssigned'
}
properties: {
configuration: {
secrets: [
{ name: 'secret-key', value: secretKey } // ⛔ native CA secret — NOT keyVaultUrl
]
}
template: {
containers: [{
env: [
// App-internal secret via secretRef:
{ name: 'SECRET_KEY', secretRef: 'secret-key' }
// Database via managed identity — plain connection params, NO password/secretRef:
{ name: 'PGHOST', value: '${pgName}.postgres.database.azure.com' }
{ name: 'PGUSER', value: containerAppName } // the MI's DB principal name
{ name: 'PGSSLMODE', value: 'require' }
]
}]
}
}
}
```
> ⛔ **Never use conditional logic (`??`, ternary, `empty()`, `union()`) to mix plain and secret env vars in a single Bicep loop or array.** ARM evaluates ALL property paths in conditional expressions — `envVar.secretRef` errors on items that don't have that property, producing `InvalidTemplate`. Instead, define plain and secret env vars as separate arrays and concatenate:
>
> ```bicep
> env: concat(
> [
> { name: 'PORT', value: '8000' }
> { name: 'NODE_ENV', value: 'production' }
> // DB/cache via managed identity — plain connection params, no secretRef:
> { name: 'PGHOST', value: '${pgName}.postgres.database.azure.com' }
> { name: 'PGUSER', value: containerAppName }
> ],
> [
> { name: 'SECRET_KEY', secretRef: 'secret-key' } // app-internal secret only
> ]
> )
> ```
> ⛔ **`principalId: containerApp.identity.principalId`** (object ID, NOT clientId) for the AcrPull role assignment. See [rbac-roles.md](rbac-roles.md) for the AcrPull role GUID.
> ❌ **WRONG:** `principalId: .clientId` (not the object ID) or `identity: containerApp.id` in secrets[] (use `'system'` for system-assigned MI).
> App-internal secrets are native CA secrets (literal `value` from an `@secure()` param) — there is no Key Vault, no KV role assignment, and no secret seeding into KV.
## Multi-Container Internal DNS
Container Apps in the same environment communicate via internal DNS: `http://{container-app-name}`. Set via env vars:
```bicep
env: [
{ name: 'API_URL', value: 'http://${apiContainerApp.name}' }
{ name: 'WORKER_URL', value: 'http://${workerContainerApp.name}' }
]
```
No ingress needed for internal-only services — set `ingress.external: false` or omit ingress entirely.
## Networking
> ⛔ **Subnets MUST be defined inline** in VNet `properties.subnets[]`, NOT as separate `Microsoft.Network/virtualNetworks/subnets` child resources. Separate child resources cause `InUseSubnetCannotBeDeleted` on redeploy when NICs are attached.
@@ -1,161 +0,0 @@
<!-- 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.
# Bicep — Azure Functions (Flex Consumption)
Bicep patterns for **Azure Functions on the Flex Consumption plan** — the default Functions floor in this
pipeline (see [sku-matrix.md](../../prepare/references/sku-matrix.md)). For shared patterns (skeleton, naming,
tags, storage, RBAC), see [bicep-patterns.md](bicep-patterns.md) and
[bicep-patterns-data.md](bicep-patterns-data.md).
> ⛔ **A Flex Consumption function app is NOT an App Service.** Do **not** reuse
> [bicep-app-service.md](bicep-app-service.md) for it. Flex uses a different resource shape
> (`functionAppConfig`), a different deploy channel (a blob-container package — see
> [code-deployment-functions-flex.md](../../deploy/references/code-deployment-functions-flex.md)), and it
> rejects the App Service build/publish settings. Only **Functions on Premium (EP1)** use the App Service
> module + SCM/Kudu path.
## ⛔ Settings that are deprecated / moved on Flex Consumption — NEVER emit them
Flex Consumption ignores or is broken by these App Service / Consumption settings. Emitting them is the
single most common cause of a "provisioned but broken" Flex app. See
[Flex Consumption plan deprecations](https://learn.microsoft.com/azure/azure-functions/functions-app-settings#flex-consumption-plan-deprecations).
| Setting | Why it's wrong on Flex |
| --- | --- |
| `FUNCTIONS_WORKER_RUNTIME` | Conflicts with `functionAppConfig.runtime`. The runtime is declared **only** in `functionAppConfig.runtime`. |
| `FUNCTIONS_EXTENSION_VERSION` | Managed by the platform on Flex. |
| `SCM_DO_BUILD_DURING_DEPLOYMENT`, `ENABLE_ORYX_BUILD` | Oryx/Kudu build settings — unsupported. Flex builds via a **remote build** during package deploy (see the deploy reference), not Oryx-on-SCM. |
| `WEBSITE_RUN_FROM_PACKAGE` | Flex runs from its deployment blob container automatically. |
| `WEBSITE_CONTENTAZUREFILECONNECTIONSTRING`, `WEBSITE_CONTENTSHARE` | Flex has no content share. |
| `WEBSITES_CONTAINER_START_TIME_LIMIT`, `WEBSITE_TIME_ZONE`, `TZ` | Not supported on Flex. |
> ⛔ **Inline `siteConfig.appSettings` on a Flex site are dropped for any of the keys above.** Put runtime in
> `functionAppConfig.runtime`, scale/memory in `functionAppConfig.scaleAndConcurrency`, and only genuine
> **app** settings (connection info, `APPLICATIONINSIGHTS_CONNECTION_STRING`, `AzureWebJobsStorage__accountName`)
> in `siteConfig.appSettings`.
> ⛔ **Do NOT add `basicPublishingCredentialsPolicies` (`scm`/`ftp`) to a Flex function app.** That is an App
> Service / Premium publishing concept. Flex deploys through its deployment storage container, so there is no
> SCM basic-auth to enable or re-disable. Adding these does not harden anything and the deploy phase must not
> toggle them for Flex (see [code-deployment-functions-flex.md](../../deploy/references/code-deployment-functions-flex.md)).
## Module Template
```bicep
param location string
param tags object
param functionAppName string
param planName string
param storageAccountName string // host + deployment storage (Entra/MI — shared-key disabled)
param deploymentContainerName string // blob container that holds the package, e.g. 'app-package'
param appInsightsConnectionString string
@allowed(['dotnet-isolated', 'node', 'python', 'java', 'powershell', 'go'])
param runtimeName string
param runtimeVersion string // e.g. '8.0' (dotnet-isolated), '20' (node), '3.11' (python)
param instanceMemoryMB int = 2048 // 512 | 2048 | 4096
param maximumInstanceCount int = 100
resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' existing = {
name: storageAccountName
}
// Flex Consumption plan — FC1 / FlexConsumption, Linux (reserved).
resource flexPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: planName
location: location
tags: tags
kind: 'functionapp'
sku: {
tier: 'FlexConsumption'
name: 'FC1'
}
properties: {
reserved: true
}
}
resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
name: functionAppName
location: location
tags: tags
kind: 'functionapp,linux'
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: flexPlan.id
httpsOnly: true
siteConfig: {
minTlsVersion: '1.2'
ftpsState: 'Disabled'
// Only when a browser frontend on a DIFFERENT hostname calls this API. Omit the whole `cors`
// block when there is no separate frontend — `staticWebApp` must be a real symbolic name in
// THIS file, or use the `allowedOrigin` param form for a cross-module frontend.
// See bicep-patterns.md § Cross-Origin (CORS).
cors: {
allowedOrigins: [ 'https://${staticWebApp.properties.defaultHostname}' ]
supportCredentials: false
}
// ⛔ ONLY genuine app settings here — NEVER the deprecated keys listed above.
appSettings: [
// Identity-based host storage (shared-key access is disabled on the account).
{ name: 'AzureWebJobsStorage__accountName', value: storage.name }
{ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: appInsightsConnectionString }
]
}
functionAppConfig: {
deployment: {
storage: {
type: 'blobContainer'
value: '${storage.properties.primaryEndpoints.blob}${deploymentContainerName}'
authentication: {
// Managed identity — REQUIRED here because storage shared-key access is disabled.
type: 'SystemAssignedIdentity'
}
}
}
scaleAndConcurrency: {
instanceMemoryMB: instanceMemoryMB
maximumInstanceCount: maximumInstanceCount
}
runtime: {
name: runtimeName
version: runtimeVersion
}
}
}
}
output functionAppId string = functionApp.id
output functionAppName string = functionApp.name
output principalId string = functionApp.identity.principalId
```
## Required companions (emit these too)
- **Deployment blob container.** The container named by `deploymentContainerName` MUST exist on the storage
account (a `Microsoft.Storage/storageAccounts/blobServices/containers` resource). Flex writes the package
there on deploy and mounts it on startup.
- **Storage role for the app identity.** Because shared-key access is disabled, grant the function app's
managed identity **Storage Blob Data Owner** (or Contributor) on the storage account via a deterministic
`Microsoft.Authorization/roleAssignments` — see [rbac-roles.md](rbac-roles.md). Without it the app cannot
read its own deployment package and fails to start.
- **Application Insights** — same as other compute targets.
> ⛔ **Region gating.** Flex Consumption is not available in every region. The plan must target a
> Flex-supported region (`az functionapp list-flexconsumption-locations`). This is validated in
> [sku-quota-validation.md](../../prepare/references/sku-quota-validation.md); if the requested region has no
> Flex, surface the region fallback there — do not silently relocate.
## Self-review (Flex)
- `functionAppConfig.runtime` present; **no** `FUNCTIONS_WORKER_RUNTIME` anywhere → else `FLAGGED`.
- **No** `SCM_DO_BUILD_DURING_DEPLOYMENT` / `ENABLE_ORYX_BUILD` / `WEBSITE_RUN_FROM_PACKAGE` → else `FLAGGED`.
- **No** `basicPublishingCredentialsPolicies` on the Flex site → else `FLAGGED`.
- `deployment.storage.authentication.type` is an identity type (not a connection string) when shared-key is
disabled, and the app identity has a Storage Blob Data role on the account → else `FLAGGED`.
- If the plan includes a browser frontend on a different hostname, `siteConfig.cors.allowedOrigins` contains
that frontend's origin → else `FLAGGED`. A missing `cors` block deploys cleanly and then fails every
browser call with `No 'Access-Control-Allow-Origin' header is present`. See
[bicep-patterns.md § Cross-Origin (CORS)](bicep-patterns.md).
@@ -1,193 +0,0 @@
<!-- 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.
# Bicep Patterns — Data Service Modules
Bicep module templates for database and cache services. Read when the prepare plan includes PostgreSQL, MySQL, or Redis.
For core patterns (file structure, skeleton, naming, tagging), see [bicep-patterns.md](bicep-patterns.md). For security defaults, see [bicep-patterns-security.md](bicep-patterns-security.md).
## PostgreSQL Flexible Server Module — Entra-Only
> ⛔ **Password authentication is disabled.** No `administratorLogin` / `administratorLoginPassword`. The deploying principal is set as the Entra admin so migrations/seeding run token-based; the app's managed identity is granted a DB role as a post-deploy data-plane step (see [database-post-deploy.md](../../deploy/references/database-post-deploy.md)).
```bicep
param pgName string
param location string
param tags object
// Entra admin = the deploying principal (so migrations can run token-based). No password params.
param entraAdminObjectId string
param entraAdminName string
@allowed(['User', 'Group', 'ServicePrincipal'])
param entraAdminType string = 'User'
param allowedExtensions string = 'uuid-ossp,pgcrypto,pg_trgm'
resource pg 'Microsoft.DBforPostgreSQL/flexibleServers@2024-08-01' = {
name: pgName
location: location
tags: tags
sku: { name: 'Standard_B1ms', tier: 'Burstable' }
properties: {
version: '16' // ⛔ use prepare-plan.json.services[].version (capabilities-verified) — do not guess
// ⛔ Entra-only: no password auth, no administratorLogin/Password.
authConfig: {
activeDirectoryAuth: 'Enabled'
passwordAuth: 'Disabled'
tenantId: subscription().tenantId
}
storage: { storageSizeGB: 32 }
}
}
// Entra administrator (deploying principal) — required for token-based admin + migrations.
resource pgAdmin 'Microsoft.DBforPostgreSQL/flexibleServers/administrators@2024-08-01' = {
parent: pg
name: entraAdminObjectId
properties: {
principalType: entraAdminType
principalName: entraAdminName
tenantId: subscription().tenantId
}
}
// 0.0.0.0 = all Azure services (intentional) — broad access consented at the Scaffold Gate.
resource pgFirewall 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2024-08-01' = {
parent: pg
name: 'AllowAllAzureServicesAndResourcesWithinAzureIps'
properties: { startIpAddress: '0.0.0.0', endIpAddress: '0.0.0.0' }
}
// Allow PG extensions (uuid-ossp, pgcrypto, pg_trgm)
resource pgExtensions 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2024-08-01' = {
parent: pg
name: 'azure.extensions'
properties: { value: allowedExtensions, source: 'user-override' }
}
```
Wire connection **parameters** (host, db name, MI username, `sslmode=require`) as plain app settings — NOT a Key Vault secret. The driver fetches an Entra access token at runtime as the password. The app's managed identity is granted a DB role post-deploy via `pgaadauth_create_principal` (see [database-post-deploy.md](../../deploy/references/database-post-deploy.md) § Grant the app managed identity a DB role).
## MySQL Flexible Server Module — Entra-Only
> ⛔ **Password authentication is disabled.** No `administratorLogin` / `administratorLoginPassword`. The deploying principal is the Entra admin; the app's MI is mapped as an AAD login post-deploy.
> ⛔ **MySQL Entra admin REQUIRES a user-assigned managed identity (UAMI).** Unlike PostgreSQL, `Microsoft.DBforMySQL/flexibleServers/administrators` will not deploy without a UAMI: the `identityResourceId` property is mandatory (the portal and `az mysql flexible-server ad-admin create --identity` both require it), and the parent server must carry that UAMI in its `identity` block. `identityResourceId: null` fails to deploy. The UAMI is what the server uses to read Microsoft Entra — grant it the **Directory Readers** role (or `User.Read.All` / `GroupMember.Read.All` / `Application.Read.All` app permissions) in Entra ID; emit a `postDeployRecommendation` for that grant since it cannot be assigned from ARM.
```bicep
param mysqlName string
param location string
param tags object
param appDbName string
// Entra admin = the deploying principal. No password params.
param entraAdminObjectId string
param entraAdminName string
// UAMI required for MySQL Entra auth — the server uses it to read Entra.
resource mysqlAadIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: '${mysqlName}-aad-id'
location: location
tags: tags
}
resource mysql 'Microsoft.DBforMySQL/flexibleServers@2023-12-30' = {
name: mysqlName
location: location
tags: tags
sku: { name: 'Standard_B1ms', tier: 'Burstable' }
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${mysqlAadIdentity.id}': {}
}
}
properties: {
version: '8.0.21' // ⛔ use prepare-plan.json.services[].version (capabilities-verified) — major-only '8.0' is rejected by ARM
storage: { storageSizeGB: 32 }
}
}
// Entra administrator (deploying principal) — enables token-based admin + migrations.
resource mysqlAdmin 'Microsoft.DBforMySQL/flexibleServers/administrators@2023-12-30' = {
parent: mysql
name: 'ActiveDirectory'
properties: {
administratorType: 'ActiveDirectory'
login: entraAdminName
sid: entraAdminObjectId
tenantId: subscription().tenantId
identityResourceId: mysqlAadIdentity.id // ⛔ MANDATORY — MySQL AAD admin will not deploy without a UAMI
}
}
// Enforce Entra-only auth (disable native password auth)
resource mysqlAadOnly 'Microsoft.DBforMySQL/flexibleServers/configurations@2023-12-30' = {
parent: mysql
name: 'aad_auth_only'
properties: { value: 'ON', source: 'user-override' }
dependsOn: [ mysqlAdmin ]
}
// 0.0.0.0 = all Azure services (intentional) — broad access consented at the Scaffold Gate.
resource mysqlFirewall 'Microsoft.DBforMySQL/flexibleServers/firewallRules@2023-12-30' = {
parent: mysql
name: 'AllowAllAzureServicesAndResourcesWithinAzureIps'
properties: { startIpAddress: '0.0.0.0', endIpAddress: '0.0.0.0' }
}
// Enforce TLS
resource mysqlTls 'Microsoft.DBforMySQL/flexibleServers/configurations@2023-12-30' = {
parent: mysql
name: 'require_secure_transport'
properties: { value: 'ON', source: 'user-override' }
}
// App database from compose (e.g. MYSQLDB_DATABASE) — emit so it exists before first boot. Omit if only the default DB is used.
resource mysqlDb 'Microsoft.DBforMySQL/flexibleServers/databases@2023-12-30' = {
parent: mysql
name: appDbName
}
```
Wire connection **parameters** as plain app settings (host, db name, MI username, SSL required) — NOT a Key Vault secret. The driver fetches an Entra token at runtime. The app's MI is mapped as an AAD login post-deploy (see [database-post-deploy.md](../../deploy/references/database-post-deploy.md)).
## Redis Cache Module (Entra-Only)
> ⛔ **Access-key authentication is disabled** (`disableAccessKeyAuthentication: true`). The app's managed identity is granted data access via an access-policy assignment. Do NOT store a Redis access key in Key Vault. If the app's Redis client library cannot present an Entra token, surface it as a `FLAGGED` finding rather than re-enabling keys.
```bicep
param redisName string
param location string
param tags object
param appPrincipalId string // the app compute MI objectId
resource redis 'Microsoft.Cache/redis@2024-03-01' = {
name: redisName
location: location
tags: tags
properties: {
sku: { name: 'Basic', family: 'C', capacity: 0 }
enableNonSslPort: false
minimumTlsVersion: '1.2'
disableAccessKeyAuthentication: true // ⛔ Entra-only
redisConfiguration: {
'aad-enabled': 'true'
}
}
}
// Data-plane access for the app MI (Entra) — no keys.
resource redisAccess 'Microsoft.Cache/redis/accessPolicyAssignments@2024-03-01' = {
parent: redis
name: guid(redis.id, appPrincipalId, 'Data Owner')
properties: {
accessPolicyName: 'Data Owner'
objectId: appPrincipalId
objectIdAlias: appPrincipalId
}
}
```
Wire `redis.properties.hostName` (+ MI object id as the username) as plain app settings. The client obtains an Entra token at runtime — no key in Key Vault.
@@ -1,187 +0,0 @@
<!-- 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.
# Bicep Patterns — Security Defaults
Mandatory security configuration for all AppOnboard-generated Bicep. Read during IaC generation before writing resource definitions. Apply during scaffold — never defer to deploy.
For core patterns (file structure, skeleton, naming, tagging), see [bicep-patterns.md](bicep-patterns.md). For data module templates (PostgreSQL, Redis), see [subagent-iac-gen.md](subagent-iac-gen.md) Step 6.
## No Key Vault — App-Internal Secret Storage
> ⛔ **Do NOT create a Key Vault.** AppOnboard-generated IaC MUST NOT emit `Microsoft.KeyVault/vaults` (or any KV secret / KV RBAC resource). Azure resource auth (database, cache, storage, Cosmos, queues) is managed-identity + token — there is nothing to store. App-internal secrets that are NOT an Azure resource credential (e.g. `SECRET_KEY`, JWT signing key, third-party API keys) are stored **directly on the compute resource**, never in Key Vault:
- **App Service / Functions:** pass each secret as an `@secure()` Bicep param → `siteConfig.appSettings`. The platform encrypts app settings at rest. The value is generated at deploy time and passed via CLI (never committed); it does not appear in ARM deployment history because it's `@secure()`.
- **Container Apps:** pass each secret as an `@secure()` param → the Container App's **native** `secrets: [{ name, value }]` array → referenced by `secretRef`. This is the Container Apps secret store, NOT Key Vault (`keyVaultUrl` is never used).
> ⛔ **No `@Microsoft.KeyVault(...)` references, no `keyVaultUrl` secretRefs, no KV role assignments** (`Key Vault Secrets Officer`/`Secrets User`), no `deployerObjectId` KV param.
## Security Defaults
> **Source:** Adapted from Azure security best practices. See [Azure security baseline](https://learn.microsoft.com/en-us/security/benchmark/azure/overview) for updates.
### Identity — Managed Identity Everywhere (MANDATORY, NO EXCEPTIONS)
> ⛔ **Every compute resource MUST have a managed identity. There is NO SKU, tier, or service combination that is exempt.** Managed identity is the ONLY sanctioned way for app code to authenticate to any Azure resource. Shared keys, admin logins, and connection-string passwords are BLOCKED (see [`bicep-patterns-data.md`](bicep-patterns-data.md) and the conformance gate).
>
> | Condition | Include MI? |
> |-----------|-------------|
> | Any compute (App Service, Container Apps, Functions) | **YES — always** |
> | Any database, storage, queue, cache, Cosmos, or ACR access | **YES — always** |
> | None of the above | **YES** (default secure) |
- **System-assigned managed identity** for all services (default). User-assigned only when shared identity is explicitly needed.
- ⛔ **Compute floor is B1 (App Service) / Standard (Static Web Apps).** F1/D1/Free tiers are NOT offered — the free-tier MI sidecar OOMs and cannot host managed identity, and free SWA lacks the config (CORS, custom auth) these apps need. SKU selection MUST NOT emit F1, D1, or Free. See [sku-matrix.md](../../prepare/references/sku-matrix.md).
- ⛔ **Never generate `administratorLogin`, `administratorLoginPassword`, access keys, or connection-string passwords** for ANY data service (SQL, PostgreSQL, MySQL, Redis, Storage, Cosmos DB, Service Bus, Event Hubs) — including inside conditional branches. Use Entra/MI-only auth (see the data-service patterns below and in [`bicep-patterns-data.md`](bicep-patterns-data.md)).
- App-to-service auth: managed identity + RBAC role assignments (or data-plane role assignments for Cosmos/Redis). Zero shared-key or password auth to any Azure resource.
- **App-internal secrets** that are NOT an Azure resource credential (e.g. Django `SECRET_KEY`, JWT signing key, third-party API keys) are stored **directly on the compute resource** — App Service app settings or Container Apps native secrets, from an `@secure()` param generated at deploy. ⛔ **No Key Vault** (see § No Key Vault — App-Internal Secret Storage).
```bicep
identity: {
type: 'SystemAssigned'
}
```
### Data Services — Entra / Managed-Identity-Only Authentication
> ⛔ **Local/shared-key/password authentication MUST be disabled on every data service.** The app authenticates with its managed identity; the deploying principal is granted an Entra admin/data role so migrations and seeding can run token-based. Full module templates (auth config, Entra admin child resources, MI data-plane roles) live in [`bicep-patterns-data.md`](bicep-patterns-data.md).
| Service | How local auth is disabled | App/MI access |
|---------|----------------------------|---------------|
| Azure SQL | `azureADOnlyAuthentication: true` (pattern below) | Entra admin + `CREATE USER [<mi>] FROM EXTERNAL PROVIDER` |
| PostgreSQL Flexible | `authConfig: { passwordAuth: 'Disabled', activeDirectoryAuth: 'Enabled' }` | Entra admin child resource + MI role via `pgaadauth_create_principal` |
| MySQL Flexible | Entra admin child resource, `aad_auth_only` config | MI mapped as an AAD login |
| Redis | `disableAccessKeyAuthentication: true` + `accessPolicyAssignments` | Data Owner/Contributor access policy to the MI |
| Storage | `allowSharedKeyAccess: false` | Storage Blob/Queue Data role to the MI |
| Cosmos DB | `disableLocalAuth: true` | `sqlRoleAssignments` (data plane) — see [rbac-roles.md](rbac-roles.md) |
| Service Bus / Event Hubs | `disableLocalAuth: true` | Data Sender/Receiver role to the MI |
### SQL Server — Entra-Only Authentication
```bicep
param principalId string
param principalName string
@allowed(['User', 'Group', 'Application'])
param principalType string = 'User'
// Preview API required — azureADOnlyAuthentication via administrators block
// is not available in GA API versions (GA path uses a separate child resource).
resource sqlServer 'Microsoft.Sql/servers@2024-05-01-preview' = {
name: '${resourcePrefix}-sql-${uniqueHash}'
location: location
properties: {
administrators: {
administratorType: 'ActiveDirectory'
principalType: principalType
login: principalName
sid: principalId
tenantId: subscription().tenantId
azureADOnlyAuthentication: true
}
minimalTlsVersion: '1.2'
}
}
```
> ⚠️ If deploying from CI/CD with a service principal, set `principalType` to `'Application'`. The default `'User'` only works for interactive deployments.
### Secrets — App-Internal, Stored On-Compute (No Key Vault)
App-internal secrets (e.g. `SECRET_KEY`, `JWT_SECRET`, `API_KEY`, session secrets) are stored directly on the compute resource. ⛔ **No Key Vault**, no `@Microsoft.KeyVault(...)`, no `keyVaultUrl`.
> ⛔ **No plaintext/committed secrets.** Values MUST NOT be hardcoded in Bicep or committed to `main.parameters.json` — pass them as `@secure()` params generated at deploy time. Never use `uniqueString()` for secrets (deterministic/predictable). `@secure()` params do NOT appear in ARM deployment history.
>
> ❌ **NEVER:** `{ name: 'SECRET_KEY', value: 'hard-to-guess-string' }` or `value: 'change-me'`; any `@secure()` password param for a database/cache/storage account (those use managed identity — see § Data Services).
```bicep
// App Service / Functions
@secure()
param secretKey string // generated at deploy time, passed via CLI --parameters, never committed
appSettings: [
// Database via managed identity — plain connection params, NO password:
{ name: 'PGHOST', value: '${pgName}.postgres.database.azure.com' }
{ name: 'PGDATABASE', value: appDbName }
{ name: 'PGUSER', value: appServiceName } // the MI's DB principal name
{ name: 'PGSSLMODE', value: 'require' }
// App-internal secret stored directly as an app setting (platform-encrypted at rest):
{ name: 'SECRET_KEY', value: secretKey }
]
```
For Container Apps, put the secret in the Container App's **native** `secrets: [{ name, value: secretKey }]` array (value from an `@secure()` param) and reference it via `secretRef` — see [bicep-container-apps.md](bicep-container-apps.md). ⛔ **Never** emit a `Microsoft.KeyVault/vaults` resource.
### App Settings Are Non-Secret Endpoint Configuration Only
> ⛔ **App settings / container `env` MUST NOT contain any secret or connection string.** With managed identity the app authenticates by token — there is nothing secret to configure. App settings carry ONLY **non-secret endpoint configuration** that tells the app *where* and *as which identity* to connect:
> - **Endpoint / host** — e.g. `PGHOST=myserver.postgres.database.azure.com`, `AZURE_STORAGE_BLOB_ENDPOINT=https://myacct.blob.core.windows.net`
> - **Resource name** — e.g. database name, container/queue name
> - **Identity principal** — the MI's DB principal name as the connection `user` (Postgres/MySQL AAD auth)
> - **`AZURE_CLIENT_ID`** — ONLY for a user-assigned MI, so `DefaultAzureCredential` selects the right identity (omit for system-assigned)
> - **Non-secret app flags** — e.g. `NODE_ENV`, `PGSSLMODE=require`, CORS origins
>
> ❌ **NEVER put in app settings / `env`:** a full **connection string** (`DATABASE_URL=postgres://user:pass@host/db`, `AccountKey=...`, `SharedAccessKey=...`), a **password**, an **access key**, a **SAS token**, or any other credential. If a value would authenticate you, it does not belong here — use managed identity instead. The ONLY secret-like value permitted anywhere is a genuinely app-internal secret (e.g. `SECRET_KEY`) that is NOT an Azure resource credential, and even that is passed as an `@secure()` param, never as a plain app-setting literal.
### Transport — HTTPS Only
All web-facing resources:
```bicep
// App Service
httpsOnly: true
siteConfig: {
minTlsVersion: '1.2'
}
// Storage
supportsHttpsTrafficOnly: true
allowBlobPublicAccess: false
minimumTlsVersion: 'TLS1_2'
```
### App Service / Premium Functions — Publishing Credential Lockdown
> ⛔ **Every App Service and every Premium (EP1) Functions app MUST include both `basicPublishingCredentialsPolicies` child resources.** Missing these means deploy cannot toggle SCM auth post-deployment — the REST API call targets a resource that doesn't exist in ARM.
>
> ⛔ **Exception — Functions on Flex Consumption (the default Functions floor): do NOT add these policies.** Flex has no SCM basic-auth publishing model; it deploys through its deployment storage container. Adding `scm`/`ftp` `basicPublishingCredentialsPolicies` to a Flex site is wrong, and the deploy phase must not toggle them. See [bicep-functions-flex.md](bicep-functions-flex.md).
```bicep
// SCM — allow: true for deploy phase (deploy re-disables via REST API after code upload)
resource scmAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2023-12-01' = {
parent: appService
name: 'scm'
properties: {
allow: true
}
}
// FTP — always disabled
resource ftpAuth 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2023-12-01' = {
parent: appService
name: 'ftp'
properties: {
allow: false
}
}
```
> **Deploy lifecycle:** Scaffold sets `scm.allow: true` so `az webapp deploy` works. After code upload + health check, deploy phase runs `az rest --method put .../basicPublishingCredentialsPolicies/scm` with `allow: false` to re-harden. If scaffold omits these resources, deploy's Step 7 SCM re-disable REST API call fails silently.
### Cosmos DB — Data Plane RBAC
⛔ Cosmos DB uses its own role system — see [rbac-roles.md](rbac-roles.md) § Cosmos DB for role IDs and behavioral rules. Do NOT use `Microsoft.Authorization/roleAssignments` for Cosmos data access.
### RBAC — Deterministic Role Assignments
For the common roles GUID table, see [rbac-roles.md](rbac-roles.md).
```bicep
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(scopeResourceId, principalId, roleDefinitionId)
scope: targetResource
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
principalId: managedIdentity.properties.principalId
principalType: 'ServicePrincipal' // REQUIRED — prevents AAD graph lookup delays
}
}
@@ -1,226 +0,0 @@
<!-- 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.
# Bicep Patterns
Bicep default-path patterns for AppOnboard scaffold. Used as the primary IaC format. For the alternative Terraform path (existing `.tf` files or user override), scaffold uses `mcp_azure_mcp_azureterraformbestpractices` output patterns.
> **Source:** Adapted from Azure Bicep best practices. See [Bicep best practices](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/best-practices) for updates.
## File Structure
> ⛔ **Always use `targetScope = 'subscription'`.** Subscription-scope Bicep creates the resource group in IaC with all 5 AppOnboard tags (including `created-at`). Resource-group scope requires `az group create` via CLI, which consistently misses `created-at` because CLI-created resource groups don't receive IaC-managed tags. There are zero benefits to resource-group scope for AppOnboard.
```
infra/
├── main.bicep # Entry point (subscription scope)
├── main.parameters.json # ARM JSON parameter values (NOT .bicepparam)
└── modules/
├── container-app.bicep
├── app-service.bicep
├── sql-database.bicep
├── key-vault.bicep
├── log-analytics.bicep
└── ...
```
Each service gets its own module. `main.bicep` orchestrates resource group creation + module calls.
## main.bicep Skeleton
```bicep
targetScope = 'subscription'
@minLength(1)
@maxLength(64)
param environmentName string
@minLength(1)
param location string
param sessionId string
param deployedBy string // resolved via: az ad signed-in-user show --query displayName -o tsv
// ⛔ createdAt: passed in parameters.json, NOT utcNow() default (crashes Portal blade)
param createdAt string
var tags = {
'app-onboard-skill': 'true'
'app-onboard-session-id': sessionId
'created-at': createdAt
environment: environmentName
'deployed-by': deployedBy
}
resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = {
name: 'rg-${environmentName}'
location: location
tags: tags
}
// ⛔ scope: rg (symbolic) — creates implicit dependsOn. Do NOT use resourceGroup(name) — it races against RG creation.
module resources './modules/resources.bicep' = {
name: 'resources'
scope: rg
params: {
location: location
environmentName: environmentName
tags: tags
}
}
```
## main.parameters.json
> ⛔ **ARM JSON only.** Do NOT use `.bicepparam` syntax (`using`, `param`, `readEnvironmentVariable()`). AppOnboard deploys via `az deployment sub create` (subscription-scope default) — not `azd` — and `.bicepparam` requires azd or newer tooling. If the user lacks subscription-level permissions, the deploy phase falls back to `az deployment group create` automatically.
```json
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environmentName": { "value": "{project}-{env}" },
"location": { "value": "{region}" },
"sessionId": { "value": "{context.json.sessionId}" },
"deployedBy": { "value": "{context.json.azure.userDisplayName}" }
}
}
```
## Naming Convention (Bicep)
The prepare phase generates a logical resource prefix in `prepare-plan.json.naming.resourcePrefix` (e.g., `myapp-dev`). Scaffold MUST add a globally unique suffix using Bicep's `uniqueString()` function to prevent cross-deployment name collisions on globally unique Azure resources (App Service, Storage Account, ACR).
```bicep
// main.bicep — derive unique suffix from resource group
var nameSuffix = uniqueString(resourceGroup().id)
// Pass unique names to modules
param appName string = 'app-${resourcePrefix}-${take(nameSuffix, 4)}'
param storName string = 'st${replace(resourcePrefix, '-', '')}${take(nameSuffix, 4)}'
param acrName string = 'cr${replace(resourcePrefix, '-', '')}${take(nameSuffix, 4)}'
```
> ⛔ **Do NOT use `uniqueString()` for secrets** — it is deterministic and predictable. See [bicep-patterns-security.md](bicep-patterns-security.md) § Secrets for correct secret patterns.
If `prepare-plan.json.naming.resources[]` provides pre-computed names with suffixes, prefer those — but ALWAYS ensure globally unique resources include a `uniqueString()` or equivalent hash in main.bicep as a safety net.
## Log Analytics Module Output
> ⛔ **Output the resource ID (`.id`), NOT `.properties.customerId`.** Container Apps Environment requires `workspaceResourceId` (the full ARM resource ID). `.properties.customerId` is the GUID used for queries — passing it as `workspaceResourceId` causes an ARM deploy failure (`BadRequest`). Separate the two outputs:
> ```bicep
> output workspaceId string = logAnalyticsWorkspace.id // ARM resource ID — for CAE, App Insights
> output workspaceCustomerId string = logAnalyticsWorkspace.properties.customerId // GUID — for Log Analytics queries only
> ```
## Compute-Target Patterns
Read the file(s) matching the service mapping — load only what's needed:
- **Functions on Flex Consumption** (the default Functions floor): [bicep-functions-flex.md](bicep-functions-flex.md) — `functionAppConfig`, MI-based deployment storage, forbidden App Service settings. ⛔ Do NOT use the App Service module for a Flex function app.
- **App Service (and Functions on Premium/EP1):** [bicep-app-service.md](bicep-app-service.md) — module template, SCM/FTP auth, native module deploy strategy
- **Container Apps:** [bicep-container-apps.md](bicep-container-apps.md) — two-phase ACR wiring, ingress, secretRef, image parameter, multi-container DNS
- **Static Web Apps:** [bicep-swa.md](bicep-swa.md) — module template, detached deploy rule
Load multiple only if the plan includes multiple compute targets.
> ⛔ **F1/D1 SKU: do NOT generate a Dockerfile.** If `prepare-plan.json` specifies F1 or D1 (free/shared tier), use the platform's built-in runtime stack (e.g., `NODE|20-lts` for Node.js, `PYTHON|3.12` for Python). Dockerfiles are for B1+ or Container Apps only.
> ⛔ **Native module deploy strategy.** If `prepare-plan.json.deployStrategy` exists, read [bicep-app-service.md § Native Module Deploy Strategy](bicep-app-service.md) and apply the startup command + app settings. `deployStrategy.startupCommand` → `appCommandLine`, `deployStrategy.requiredAppSettings` → `appSettings[]`. When no `deployStrategy` exists, do NOT set `appCommandLine`.
## Cross-Origin (CORS) Between Frontend and Backend
> ⛔ **If the frontend is served from a different hostname than the API, the API MUST declare the frontend
> origin.** A Static Web App calling a Function App / App Service is *always* cross-origin — different
> hostname means different origin, so the browser sends a preflight `OPTIONS` and blocks the response
> unless the API returns `Access-Control-Allow-Origin`. A backend deployed with no `cors` block fails
> **every** browser call with:
> `No 'Access-Control-Allow-Origin' header is present on the requested resource`.
> The API works fine from `curl` and from the portal test console, so this surfaces only in the browser.
> ⛔ **Local success proves nothing here.** `local.settings.json` has `"Host": { "CORS": "*" }`, but that
> file configures the **local Functions emulator only** — it is git-ignored and never deployed. F5 working
> is not evidence that the deployed API allows the deployed frontend.
Emit `cors` on the backend's `siteConfig`, alongside the frontend resource in the same module:
```bicep
resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
properties: {
siteConfig: {
cors: {
// staticSites → `defaultHostname`; the value has no scheme, so add https://
allowedOrigins: [ 'https://${staticWebApp.properties.defaultHostname}' ]
supportCredentials: false
}
}
}
}
```
When the frontend and backend are in **different modules**, pass the origin instead of referencing across.
Guard with `empty()` so the same module still compiles for an API with no browser frontend:
```bicep
// api module
param allowedOrigin string = ''
...
siteConfig: {
cors: {
allowedOrigins: empty(allowedOrigin) ? [] : [ allowedOrigin ]
supportCredentials: false
}
}
// frontend module
output defaultHostname string = staticWebApp.properties.defaultHostname
// main.bicep
module api './modules/api.bicep' = {
params: { allowedOrigin: 'https://${frontend.outputs.defaultHostname}' }
}
```
> ⛔ **A symbolic reference only compiles inside the file that declares it.** `staticWebApp.properties...`
> fails `bicep build` with **`BCP057: The name "staticWebApp" does not exist in the current context`**
> whenever the SWA lives in another module — or when there is no frontend at all. Use the direct reference
> only in the file that declares both; otherwise use the `allowedOrigin` param form above. For an API with
> no browser frontend, leave `allowedOrigin` empty (or omit `cors` entirely).
> ⛔ **Property casing differs by resource type.** `Microsoft.Web/staticSites` exposes
> `properties.defaultHostname` (lowercase **n**); `Microsoft.Web/sites` exposes `properties.defaultHostName`
> (capital **N**). Using the wrong one fails at `bicep build` — verify against the resource you are actually
> referencing when the frontend is an App Service rather than a SWA.
> ⛔ **`allowedOrigins: ['*']` cannot be combined with `supportCredentials: true`** — Azure rejects the pair.
> Always emit the explicit origin; it is required anyway the moment cookie or bearer auth is added, and it
> avoids re-work. Include any custom domain as an additional entry when one is configured.
**No dependency cycle.** The frontend receives the API URL at **build** time (`VITE_API_BASE_URL` /
`NEXT_PUBLIC_API_URL`, baked into the bundle from `deploy-result.json.endpoints[]` — see
[deploy-checklist-template.md](../../deploy/references/deploy-checklist-template.md)), so the frontend
resource never references the backend in Bicep. The backend may therefore reference the frontend hostname
freely. Do **not** add the API URL to the SWA resource to "pair" them — that is what creates a cycle.
**Alternative that removes CORS entirely:** linking an existing Function App as an SWA backend makes the
frontend call `/api/*` on its own origin, so no preflight occurs. This is allowed only when
`context.json.overrides[]` records explicit user approval for `linkFunctionsToStaticWebApp`. The Function
App remains a separate resource and uses the Functions deployment channel. Never move or copy its source
into an SWA API directory, emit SWA `buildProperties.apiLocation`, or change the backend's runtime or
authentication provider to `azureStaticWebApps`.
**Self-review check:** If the plan has a browser frontend AND a separate HTTP API resource, verify the API's
`siteConfig.cors.allowedOrigins` contains the frontend origin. **FLAGGED** if absent — the app will deploy
successfully and then fail on first use.
## Service Tagging
> ⛔ **You MUST read [iac-generation-rules.md § Session Tags](iac-generation-rules.md).** All resources MUST include the 5 AppOnboard session tags. Pass `tags` object from `main.bicep` into every module.
## API Version Policy
Use the latest stable API version for each resource type. Never use preview APIs unless required for a feature with no GA alternative. Validate via `bicep build` — stale API versions produce warnings.
## App-Internal Secret Syntax (No Key Vault)
⛔ **No Key Vault is created.** App-internal secrets (e.g. `SECRET_KEY`) are stored on the compute resource from an `@secure()` param at deploy time. App Service → `siteConfig.appSettings`; Container Apps → native `secrets: [{ name, value }]` + `secretRef` (see [bicep-container-apps.md](bicep-container-apps.md)). Never use `@Microsoft.KeyVault(...)` / `keyVaultUrl`. See [bicep-patterns-security.md](bicep-patterns-security.md) § Secrets. For Terraform, see [terraform-patterns.md](terraform-patterns.md).
@@ -1,30 +0,0 @@
<!-- 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.
# Bicep — Static Web Apps Patterns
SWA-specific Bicep patterns. For shared patterns (skeleton, naming, tags, security defaults, data modules), see [bicep-patterns.md](bicep-patterns.md).
## Module Template
SWA modules for token-based deploys (no GitHub CI/CD):
```bicep
resource staticWebApp 'Microsoft.Web/staticSites@2023-12-01' = {
name: swaName
location: location
tags: tags
sku: { name: 'Standard', tier: 'Standard' }
properties: {} // ⛔ MUST be empty — no repositoryUrl, no branch, no buildProperties
}
```
> ⛔ **Standard SKU minimum — Free SWA is not offered.** Free Static Web Apps lack the configuration these apps need (custom authentication, configurable CORS, SLA, private endpoints, BYO API backends via managed identity). Always emit `sku: { name: 'Standard', tier: 'Standard' }`.
> ⛔ **The API must allow this SWA's origin.** A SWA calling a Function App or App Service is cross-origin, so
> the **backend** needs `siteConfig.cors.allowedOrigins` containing `https://${staticWebApp.properties.defaultHostname}`.
> Nothing on the SWA resource itself fixes this. Omitting it deploys cleanly and then fails every browser
> call with `No 'Access-Control-Allow-Origin' header is present`. See
> [bicep-patterns.md § Cross-Origin (CORS)](bicep-patterns.md).
> ⛔ **Detached SWA deploy:** Omit `repositoryUrl`, `branch`, and `buildProperties` entirely. These are only for GitHub Actions–connected deployments. Including `repositoryUrl: ''` causes `BadRequest: RepositoryUrl is invalid`.

Some files were not shown because too many files have changed in this diff Show More