──────
Build and Push Docker Images / detect-changes (push) Successful in 11s
Build and Push Docker Images / build-auth-api (push) Successful in 1m0s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 1m1s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m5s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m13s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m15s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s
Build and Push Docker Images / detect-changes (push) Successful in 11s
Build and Push Docker Images / build-auth-api (push) Successful in 1m0s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 1m1s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m5s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m13s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m15s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s
### Summary of Changes
1. Documentation Consolidation:
• Relocated the docs/ folder and all repository .md files (README.md, DEPLOYMENT.md, DOCKER_README.md,
DOMAINS_README.md, GETTING_STARTED.md, CI_CD_PORTAINER_GUIDE.md, ADMIN_DEPLOYMENT_SUMMARY.md,
SECURITY_PRIVATE_FILES.md, handoff.md, azure-project-plan.md, plus service-specific READMEs) into .
• Updated document links in README.md.
2. AI Agent Project Map:
• Created PROJECT_MAP.md in the project root containing complete architecture diagrams, service specs, ports, DB
schemas, environment variables, and developer workflows.
• Added PROJECT_MAP.md to .gitignore.
3. Monitoring UI & Infrastructure Integration:
• Added monitoring_ui service to docker-compose.yml (port 5175).
• Configured Gitea workflow docker-build.yml for monitoring_ui builds.
• Added CORS middleware headers in index.js.
• Added nginx.conf copy step in Dockerfile.
──────
### Recommended Commit Message
docs: consolidate project documentation into monitoring_ui and add agent project map
- Move all markdown documentation files and docs/ folder into monitoring_ui/docs/
- Update documentation hub index (monitoring_ui/docs/README.md) with relative paths
- Generate comprehensive AI agent reference map (PROJECT_MAP.md) in repository root
- Add PROJECT_MAP.md and monitoring_ui build output/env overrides to .gitignore
- Integrate monitoring_ui service into docker-compose.yml and Gitea CI workflow
- Configure CORS handling in employee_api for monitoring dashboard access
──────
### Alternative Short Commit Message
refactor(docs): move docs to monitoring_ui and create AI agent PROJECT_MAP.md
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "azure-deploy",
|
||||
"version": "0.0.1"
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,65 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Deploy — IaC Execution & Health Verification
|
||||
|
||||
> ⛔ **Read [`references/blocked-patterns.md`](references/blocked-patterns.md) before your first resource-creating command.**
|
||||
>
|
||||
> The trigger is **any** command that can create Azure resources — `az … create`, `az … up`, `azd up`, `azd provision`, `terraform apply` — not only `az deployment sub create`. Keying it to the declarative command alone made the rule unreachable on the path that most needs it: an agent that decided to provision imperatively never ran `az deployment sub create`, so it never reached the file explaining that imperative provisioning is forbidden.
|
||||
>
|
||||
> **This phase deploys a template. It does not create resources by hand.** If `infra/` holds no template, you are not ready to deploy — return to scaffold and generate one. Reaching for `az containerapp up` because the template is failing is the one fallback that is never available.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Best for | Executing validated IaC against Azure, health-checking deployed resources |
|
||||
| Inputs | `prepare-plan.json` + `scaffold-manifest.json` from `.copilot-azure/sessions/{id}/` |
|
||||
| Outputs | `deploy-result.json` written to session directory |
|
||||
| Parent | [azure-app-onboard](../instructions.md) |
|
||||
|
||||
## When to Use This Agent
|
||||
|
||||
Invoked by the `azure-app-onboard` orchestrator at Phase 4 when `scaffold-manifest.json` exists with `files[]` and `validationResult`. Not directly user-routable.
|
||||
|
||||
> **Return to orchestrator:** When complete, return control to `azure-app-onboard` for handoff (Step 10). Do NOT start new phases.
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
| Scenario | Use Instead |
|
||||
|----------|-------------|
|
||||
| Plan architecture, map services, estimate costs | [prepare](../prepare/instructions.md) |
|
||||
| Generate IaC files from a plan | `azure-app-onboard` Step 7 (scaffold) |
|
||||
| Run `azd up` or execute existing deployment templates | `azure-deploy` |
|
||||
| Debug a running app after deployment | `azure-diagnostics` |
|
||||
| Optimize existing Azure spending | `azure-cost` |
|
||||
|
||||
## Workflow
|
||||
|
||||
> ⛔ **Sub-agent delegation is MANDATORY for Step 0.** Read `subagent-preflight.md`, then dispatch as a `task` with the **COMPLETE and UNMODIFIED** template text between `<<<TEMPLATE_START>>>` / `<<<TEMPLATE_END>>>` delimiters. Do NOT summarize or rewrite the template — the sub-agent needs every "Read [file]" instruction to produce a correct `deploy-checklist.md`. Append session artifact data AFTER the template block. If your next action after reading the template is anything other than `task`, you are executing it inline instead of delegating.
|
||||
|
||||
> ⛔ **Healing loop:** ask user after 3 attempts, then every 5 (counter = `healingAttempts[].length`).
|
||||
|
||||
> ⛔ **Region lock:** Before `az deployment` retry, compare `--location` against `prepare-plan.json.deploymentVariables.location`. If changed → re-approval gate required. Update plan after approval.
|
||||
|
||||
> ⛔ **After compaction or any `az deployment`/`az webapp deploy`/`az acr build`/failed health check: re-read `deploy-checklist.md`.** If missing → fill from [`deploy-checklist-template.md`](references/deploy-checklist-template.md). On significant context loss: also re-read this instructions.md.
|
||||
|
||||
> ⛔ **Deterministic resource inventory (MANDATORY — do NOT rely on memory for what was created).** The `capture_deployment_inventory` MCP tool records exactly which Azure resources this session created by diffing a `resources.list()` snapshot taken **before** the first deployment against one taken **after** each attempt. Call it:
|
||||
> - **Step 5b** with `phase: "baseline"` — BEFORE the first `az deployment` (once).
|
||||
> - **Every time the user resumes this deployment** with `phase: "baseline"` — AFTER the user chooses Resume and the subscription is resolved, but BEFORE any further command that can provision resources. Preserve the inventory already in `deploy-result.json`; subsequent captures cover only the resumed segment and must be unioned into the preserved `createdResources[]`/`orphanedResourceGroups[]` by normalized resource ID/case-insensitive RG name.
|
||||
> - **After EVERY `az deployment` attempt** (success, failure, or healing retry) with `phase: "capture"`, passing `expectedResourceGroup`, all `deploymentNames[]`, and every `resourceGroups[]` touched (including abandoned healing RGs).
|
||||
> - **On the Step 9 failure path** before returning `status: "failed"`.
|
||||
> It holds the baseline snapshot **in memory** (no files are written to the workspace) and, on `phase: "capture"`, returns the created resources classified as `expected`/`failed`/`orphaned`/`unverified` plus the orphaned resource groups. Write that output into `deploy-result.json.createdResources[]` and `orphanedResourceGroups[]` — the durable record. ⛔ **Build ready-to-run cleanup commands from `failed` entries only.** `orphaned` means "appeared during the deploy window but no deployment claimed it" — it may be a healing stray, or it may belong to someone else on a shared subscription, so present it as "review before deleting" with no delete command. If the tool returns `inventoryUnverified`, present no cleanup list at all (see [`references/mcp-tools.md`](references/mcp-tools.md)). After a resume, merge the new segment into the durable record rather than replacing prior entries. The tool never deletes anything and never writes to disk; it only reports.
|
||||
|
||||
| # | Step | Action | Artifact | Reference |
|
||||
|---|------|--------|----------|-----------|
|
||||
| 0 | **Dispatch preflight sub-agent** | ⛔ **You MUST dispatch [`subagent-preflight.md`](references/subagent-preflight.md) as a `task`.** ⛔ agent_type: `"task"` — NEVER `"general-purpose"`. Read the template, then your NEXT action MUST be `task`. If after reading the template your next action is `powershell`, `view`, or anything other than `task`, STOP — you are executing inline instead of delegating. Writes `deploy-checklist.md`. **`view` it immediately after return.** | `deploy-checklist.md` | ⛔ **You MUST read [`subagent-preflight.md`](references/subagent-preflight.md)** |
|
||||
| 1 | **Read upstream artifacts** | Load `prepare-plan.json` + `scaffold-manifest.json`. Check `validationResult`. Resolve subscription + deployment variables. | — | — |
|
||||
| 3 | **Preflight checks** | Auth, **mandatory what-if preview**, RBAC, RG per `deploy-checklist.md` § Preflight. | — | ⛔ **You MUST read `deploy-checklist.md`** (re-read if compaction occurred) |
|
||||
| 4 | **Deploy approval gate** | Present cost + resource summary per `deploy-checklist.md` § Deploy approval gate format. | — | — |
|
||||
| 5b | **Write deploy-result.json skeleton + baseline inventory** | ⛔ Read [`deploy-schemas.ts`](references/deploy-schemas.ts), write skeleton (`status: "in-progress"`). Must exist BEFORE first `az` command. ⛔ **Then call `capture_deployment_inventory` with `phase: "baseline"`** (passing `sessionId`, `subscriptionId`) BEFORE the first `az deployment` — this snapshots pre-existing resources (held in memory) so the post-deploy diff is accurate. | `deploy-result.json` | ⛔ **You MUST read [`deploy-schemas.ts`](references/deploy-schemas.ts)** |
|
||||
| 6 | **Execute deployment** | ⛔ **BEFORE `az deployment sub create`:** Generate portal link — `$dn="{deploymentName}"; $r="/subscriptions/{subId}/providers/Microsoft.Resources/deployments/$dn"; $l="https://portal.azure.com/#view/Microsoft_Azure_Resources/DeploymentDetails.MenuView/~/overview/id/$($r.Replace('/','%2F'))"; Write-Output "LINK=$l"`. ⛔ **Do NOT auto-open the link** — never call `Start-Process` (or any browser launcher) on it. Print the bare URL in chat (ctrl-clickable) so the user can open it if they choose.<br>Auto-generate ALL app-internal `@secure()` params (`openssl rand -base64 32 \| tr -d '/+='`), NEVER `ask_user` for secrets (databases use managed identity — no passwords); on retry reuse from `deploy-secrets.env` — NEVER regenerate (see deploy-safety.md § Deploy Checklist). THEN deploy IaC. ⛔ **After the deployment command returns (success OR failure), call `capture_deployment_inventory` with `phase: "capture"`** (`expectedResourceGroup`, all `deploymentNames[]`, every `resourceGroups[]` used) to record what was actually created and surface any orphans early. | — | ⛔ **You MUST read `deploy-checklist.md`** § Execute deployment |
|
||||
| 6b | **Deploy application code** | ⛔ Deploy code for EVERY service in `prepare-plan.json.services[]`. Follow `deploy-checklist.md` § Code deploy. | — | ⛔ **You MUST read `deploy-checklist.md`** § Code deploy |
|
||||
| 7 | **Health-check + SCM re-disable** | HTTP GET per endpoint (max 3 iterations). ⛔ **Multi-service apps:** Also inspect the response body for error patterns (`connection refused`, `MODULE_NOT_FOUND`, `localhost`, `SET-IN-DEPLOY-PHASE`) — HTTP 200 alone does not mean functional when the app depends on another service or KV secrets. Then ⛔ for EVERY App Service and Premium/EP1 Functions app — but ⛔ **NOT** Flex Consumption function apps, which have no SCM basic-auth policy (see [code-deployment-functions-flex.md](references/code-deployment-functions-flex.md)) — run BOTH commands, no exceptions: `az rest --method put --url "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{app}/basicPublishingCredentialsPolicies/scm?api-version=2023-12-01" --headers "Content-Type=application/json" --body '{"properties":{"allow":false}}'` then verify: `az rest --method get --url "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{app}/basicPublishingCredentialsPolicies/scm?api-version=2023-12-01" --query properties.allow -o tsv` (must return `false`). | `deploy-result.json` full | ⛔ **You MUST read `deploy-checklist.md`** § Health check |
|
||||
| 8 | **Finalize artifacts** | ⛔ Read [`deploy-schemas.ts`](references/deploy-schemas.ts). ⛔ Re-read `deploy-checklist.md` § Artifact verification — follow ALL 5 checks. ⛔ **Run a final `capture_deployment_inventory` (`phase: "capture"`) and populate `deploy-result.json.createdResources[]` + `orphanedResourceGroups[]` FROM its returned output** — do NOT hand-author these fields. ⛔ **No "live"/handoff message until you overwrite the skeleton `deploy-result.json`** — flip `status` off `"in-progress"` (→ `succeeded`/`failed`) and fill healthStatus, endpoints, completedUtc, deploymentNames, healingAttempts. Write `deployment-summary.md` (status table + health + portal link(s) + cleanup commands — same content as your handoff message). Update `context.json` — add `"deploy"` to `completedPhases`, `currentPhase: null`, `lastModifiedUtc`. Read back to confirm `status != "in-progress"` and `"deploy"` ∈ `completedPhases`. ⛔ **Then STOP — return to orchestrator. No further CLI commands.** | `deploy-result.json` final + `deployment-summary.md` + `context.json` update | ⛔ **You MUST read [`deploy-schemas.ts`](references/deploy-schemas.ts)** + ⛔ **Re-read `deploy-checklist.md` § Artifact verification** |
|
||||
| 9 | **Error handling + healing** | ⛔ **Only if Steps 6/6b/7 returned nonzero exit code or health check failed.** Skip entirely on clean deploys. Classify errors, healing loop, PLAN_LEVEL_CHANGE re-approval per `deploy-checklist.md` § During healing. ⛔ **Even on unrecoverable failure:** first run `capture_deployment_inventory` (`phase: "capture"`) to record what was partially created, then write `deploy-result.json` with `status: "failed"`, `errorDetails`, and `createdResources[]`/`orphanedResourceGroups[]` from its returned output, and surface cleanup commands built from its `failed` entries only (list `orphaned` entries separately as "review before deleting", with no delete command) — before returning to orchestrator. `deploy-result.json` and the cleanup report must ALWAYS exist, especially on failure. | — | ⛔ **You MUST read [`error-classification.md`](references/error-classification.md)** |
|
||||
@@ -0,0 +1,59 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Approval Gate Template
|
||||
|
||||
Display template for the deploy approval gate. Present after preflight validation, before execution.
|
||||
|
||||
## Display Format
|
||||
|
||||
```
|
||||
## Deploy Approval
|
||||
|
||||
🏢 **Subscription:** {context.json.azure.subscriptionName} (`{context.json.azure.subscriptionId}`)
|
||||
📁 **Resource Group:** {context.json.azure.resourceGroup}
|
||||
🌍 **Region:** {context.json.azure.region}
|
||||
|
||||
**Services:**
|
||||
| Service | SKU | Region | Resource Name |
|
||||
|---------|-----|--------|---------------|
|
||||
{for each service in prepare-plan.json.services[]}
|
||||
|
||||
**💰 Estimated Monthly Cost:** ${costEstimate.monthlyUsd}/month
|
||||
| Service | SKU | Monthly |
|
||||
|---------|-----|---------|
|
||||
{for each item in costEstimate.breakdown[]}
|
||||
{costEstimate.disclaimer}
|
||||
|
||||
**🔒 Validation:** {scaffold-manifest.json.validationResult.status}
|
||||
{if FLAGGED findings from scaffold self-review → ⚠️ list each}
|
||||
|
||||
**📋 What-if preview:** {N} resources to create, {N} to modify, {N} unchanged
|
||||
{if any Delete operations → ⚠️ list each deleted resource with name + type}
|
||||
|
||||
**Generated Files:**
|
||||
{for each file in scaffold-manifest.json.files[]}
|
||||
|
||||
**📦 Deployment Summary:**
|
||||
{Display the "What's Being Deployed" table here, built from prepare-plan.json.services[] — showing which components map to which Azure services, SKUs, and estimated costs. (deployment-summary.md is not written until the deploy/handoff phase)}
|
||||
|
||||
---
|
||||
**🚀 Ready to deploy? (Yes / Run manually / Edit plan / Cancel)**
|
||||
```
|
||||
|
||||
## Response Handlers
|
||||
|
||||
| Response | Action |
|
||||
|----------|--------|
|
||||
| **Yes** | Execute deployment (Step 6 of deploy workflow). Do NOT re-confirm. |
|
||||
| **Run manually** | Show exact CLI commands based on `iacFormat`:<br>**Bicep (subscription-scope, default):** `az deployment sub create --subscription {subscriptionId} --location {location} --template-file infra/main.bicep --parameters @infra/main.parameters.json --query properties.provisioningState -o tsv`<br>**Bicep (resource-group scope, after 403 fallback):** `az deployment group create --resource-group {rg} --template-file infra/main.bicep --parameters @infra/main.parameters.json --query properties.provisioningState -o tsv`<br>**Terraform (alternative):** `cd infra && terraform init && terraform plan -out=tfplan && terraform apply tfplan`. Stop. |
|
||||
| **Edit plan** | Ask what to change → write to `context.json.overrides[]` → re-run prepare (Step 4) → re-scaffold → return to approval gate |
|
||||
| **Cancel** | Preserve all session artifacts. Say "💾 Session preserved — resume anytime." Stop. |
|
||||
|
||||
## Rules
|
||||
|
||||
- ⛔ Gate is the LAST content in the response — no continued execution until user replies
|
||||
- ⛔ SKU column must show exact Azure SKU code + tier name (e.g., "B1 Linux (Basic)", "P1v3 (Premium)") — not generic labels like "cheapest tier". F1/D1/Free are never selected.
|
||||
- Always show the monthly cost estimate — there is no $0 tier (B1 floor ~$13/mo)
|
||||
- Surface ALL `FLAGGED` self-review findings — user must see risks before approving
|
||||
- If validation failed, show failures and block Yes option until resolved
|
||||
@@ -0,0 +1,36 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Blocked Patterns
|
||||
|
||||
Commands the agent must NEVER execute. Block decisions are non-negotiable — user must run blocked commands manually outside AppOnboard.
|
||||
|
||||
| Pattern | Action | Reason |
|
||||
|---------|--------|--------|
|
||||
| `rm -rf` (any path outside a fresh temp dir) | ⛔ Block | Prevents accidental deletion of IaC, app code, or session artifacts — especially `infra/`, `.azure/`, `.copilot-azure/`. |
|
||||
| `git reset --hard`, `git checkout -- <path>`, `git restore`, `git clean` | ⛔ Block | Discards uncommitted work. During region-fallback healing the agent edits Bicep/app config; these wipe the user's unstaged changes irrecoverably. |
|
||||
| `git push --force` / `--force-with-lease` (any branch) | ⛔ Block | Prevents force-push of generated code over remote history. |
|
||||
| `--no-verify` (on `git commit` / `git push`) | ⛔ Block | Bypasses hooks (secret-scan, lint) that guard the commit. |
|
||||
| `DROP TABLE` / `DROP DATABASE` | ⛔ Block | Prevents data loss |
|
||||
| `terraform destroy` | ⛔ Block | Prevents accidental teardown (user must run manually) |
|
||||
| `az group delete` | ⛔ HARD BLOCK | **NEVER delete resource groups yourself.** During healing: if switching regions/RGs, add the old RG to your `orphanedResourceGroups[]` list (per `OrphanResourceGroup` in [`deploy-schemas.ts`](deploy-schemas.ts)) instead of deleting it. At handoff: emit `az group delete` commands in the handoff message for the USER to run — the agent never executes them. If you are about to type `az group delete` into a terminal command, STOP — you are violating this rule. Track it in `orphanedResourceGroups[]` instead. |
|
||||
| `az containerapp up --source` / `az containerapp create` | ⛔ Block | Creates ACR + CA Environment + Log Analytics imperatively — orphan resources invisible to `terraform destroy`, `az deployment sub delete`, and session tag-based bulk cleanup. State drift from IaC is unrecoverable. The Container App MUST be created via Bicep `az deployment sub create` — for code deploy on an existing CA use `az containerapp update --source` (Step 6d) |
|
||||
| `az appservice plan update` | ⛔ Block | Imperative SKU change — edit Bicep + redeploy |
|
||||
| `az webapp update` | ⛔ Block | Imperative resource modification — all changes via IaC |
|
||||
| `az functionapp update` | ⛔ Block | Imperative resource modification — all changes via IaC |
|
||||
| `az webapp deployment source config-zip` | ⛔ Block | Requires SCM basic auth — use `az webapp deploy` (Entra auth). ⛔ **This block is the `webapp` command only.** `az functionapp deployment source config-zip` is the **ALLOWED** deploy channel for Flex Consumption function apps (blob-container package, not SCM basic auth) — see [`code-deployment-functions-flex.md`](code-deployment-functions-flex.md). |
|
||||
| `az webapp deploy --track-status` | ⛔ Block | `--track-status` flag does not exist. Remove it. |
|
||||
| `az webapp up` / `az webapp create` / `az appservice plan create` | ⛔ Block | Creates App Service Plan + App imperatively — bypasses IaC entirely |
|
||||
| `az containerapp update` (config changes) | ⛔ Block | Imperative resource modification — all changes via IaC |
|
||||
| `az containerapp update --revision-suffix` (no config changes) | ⚠️ ALLOWED | KV secret rotation only — when KV secrets were updated post-deploy and a new revision is needed to pick up cached values |
|
||||
| `az webapp delete` | ⛔ Block | Imperative resource deletion — destroys resources outside IaC |
|
||||
| `az appservice plan delete` | ⛔ Block | Imperative plan deletion — remove from Bicep + redeploy instead |
|
||||
| `az containerapp update --image` | ⛔ Block (during healing) | Imperative image swap causes IaC drift — update Bicep + redeploy |
|
||||
| Inline secret values in CLI args | ⛔ Block | `--parameters password=MyP@ss$word!` breaks shell escaping and leaks secrets in terminal history. Pass app-internal secrets via `main.parameters.json`, `terraform.tfvars`, or an `@secure()` param from a variable reloaded from `deploy-secrets.env` (no Key Vault). |
|
||||
| Writing secrets to temp files on disk | ⛔ Block | ⛔ NEVER write rendered secret values to arbitrary temp files. App-internal secrets are passed as `@secure()` params and stored on the compute resource (App Service app settings / CA native secrets); the only persisted copy is the git-ignored `deploy-secrets.env` reload cache under `.copilot-azure/`. Temp files risk exposure in crash dumps, logs, and unprotected storage. |
|
||||
| `az group create` (during healing) | ⛔ HARD BLOCK — **one sanctioned exception** | **NEVER create resource groups imperatively during healing.** All RG creation must go through `az deployment sub create` with Bicep `targetScope = 'subscription'`. If you need a new RG for region fallback, update the Bicep region parameter and redeploy. **Sole exception:** the documented 403 scope-fallback in [`deploy-safety.md`](deploy-safety.md) § 403 Scope Fallback — when `az deployment sub create` returns 403, rescoping Bicep to RG-scope requires `az group create` (with all 5 AppOnboard tags). That path is allowed. |
|
||||
| `az rest --method put/patch` (for individual resource creation) | ⛔ HARD BLOCK | **NEVER create individual Azure resources via REST API as a fallback for Bicep failures.** After a deployment failure, the ONLY allowed remediation is: fix the Bicep parameters/template → re-run `az deployment sub create`. Compiling Bicep→ARM and deploying via REST is still imperative resource creation. |
|
||||
| Disabling a security control to unblock — `require_secure_transport`/TLS → OFF, HTTPS-only off, KV purge protection off, auth off (via `az ... parameter set` OR editing the Bicep) | ⛔ HARD BLOCK | **NEVER weaken a security control to make a failing deploy pass.** A DB TLS handshake failure means the *client* lacks SSL config — fix the client (prereq `W-MYSQL-SSL`/`W-PG-SSL`) or surface the tradeoff to the user. Downgrading the server control is forbidden. |
|
||||
| `Compress-Archive -Path $files.FullName` | ⛔ Block | Absolute paths flatten directory structure — app crashes on `./src/app` not found. Use `System.IO.Compression.ZipFile` with relative paths from workspace root. On Windows, normalize: `$entryName = $relativePath.Replace('\', '/')`. |
|
||||
|
||||
> **Repos with existing `azure.yaml`:** See [`pipeline-rules.md`](../../references/pipeline-rules.md) § azure.yaml prohibition. Deploy via `az deployment sub create` — do NOT run `azd up`.
|
||||
@@ -0,0 +1,84 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Code Deployment — App Service (and Premium/EP1 Functions)
|
||||
|
||||
After IaC deployment creates the Azure resources, deploy application code.
|
||||
|
||||
> ⛔ **Functions on Flex Consumption (the default Functions floor) do NOT deploy through this SCM/Kudu path.** They use a blob-container package channel with no `basicPublishingCredentialsPolicies` toggle. Use [code-deployment-functions-flex.md](code-deployment-functions-flex.md) instead. This file applies to App Service web apps and to Functions only on **Premium (EP1)**.
|
||||
|
||||
> ⛔ **`--subscription {subscriptionId}` on EVERY `az` command.**
|
||||
|
||||
> ⛔ **Verify `SCM_DO_BUILD_DURING_DEPLOYMENT=true` is active BEFORE deploying.** ARM timing can delay propagation. Check: `az webapp config appsettings list -g {rg} -n {app} --query "[?name=='SCM_DO_BUILD_DURING_DEPLOYMENT'].value" -o tsv`. If not `true`: `az webapp config appsettings set -g {rg} -n {app} --settings SCM_DO_BUILD_DURING_DEPLOYMENT=true ENABLE_ORYX_BUILD=true`. Wait 10s. If `az webapp deploy` reports "Build successful. Time: 0(s)", Oryx was skipped — use Kudu zipdeploy instead.
|
||||
|
||||
> ⛔ **`ORYX_DISABLE_COMPRESSION=true`** and **`WEBSITES_CONTAINER_START_TIME_LIMIT=1800`** must be in Bicep app settings (from `prepare-plan.json.deployStrategy.requiredAppSettings`).
|
||||
|
||||
## Pre-Deploy Verification (Step 6a)
|
||||
|
||||
> ⛔ **TypeScript projects:** Oryx with `NODE_ENV=production` skips devDependencies. If `typescript`, `@types/*`, or build tools are in `devDependencies`, move them to `dependencies` before zipdeploy. Alternative: set `NPM_CONFIG_PRODUCTION=false` as app setting so Oryx installs devDeps during build.
|
||||
|
||||
> ⛔ **Wait for App Service to stabilize** (B1: ~30-90s cold start on a freshly created plan). Poll `az webapp show -g {rg} -n {app} --query state` every 10s, max 2 min. If not `Running`, check logs.
|
||||
|
||||
When `deployStrategy.codeDeployPattern == "startup-install"`, surface: "⚠️ First cold start: 2-5 min (native module compilation)."
|
||||
|
||||
## Zip Deploy (Step 6b)
|
||||
|
||||
SCM lifecycle: enable → deploy → re-disable.
|
||||
|
||||
**Enable SCM:**
|
||||
```powershell
|
||||
az rest --method put --url "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{app}/basicPublishingCredentialsPolicies/scm?api-version=2023-12-01" --headers "Content-Type=application/json" --body '{"properties":{"allow":true}}'
|
||||
```
|
||||
|
||||
**Choose deploy method:**
|
||||
|
||||
| Runtime | Needs Oryx? | Method |
|
||||
|---|---|---|
|
||||
| Python, Node.js, Ruby, PHP | Yes | **Kudu zipdeploy** (`/api/zipdeploy`) |
|
||||
| .NET, Java, static front-end | No | `az webapp deploy --type zip` |
|
||||
|
||||
⛔ **OneDeploy NEVER triggers Oryx** — use Kudu zipdeploy for runtimes needing server-side install.
|
||||
⛔ **NEVER use `az webapp deployment source config-zip`** — deprecated.
|
||||
⛔ **`az webapp deploy` does NOT support `--track-status`.**
|
||||
|
||||
### Kudu Zipdeploy (Oryx-Dependent Runtimes)
|
||||
|
||||
For apps needing server-side package installation (Python, Node.js, Ruby, PHP), use Kudu zipdeploy directly:
|
||||
|
||||
```powershell
|
||||
# Get publishing credentials
|
||||
$creds = az webapp deployment list-publishing-credentials --subscription {sub} -g {rg} -n {app} --query "{user:publishingUserName, pass:publishingPassword}" -o json | ConvertFrom-Json
|
||||
$auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$($creds.user):$($creds.pass)"))
|
||||
|
||||
# Deploy via Kudu zipdeploy (triggers Oryx pip install)
|
||||
Invoke-WebRequest -Uri "https://{app}.scm.azurewebsites.net/api/zipdeploy?isAsync=true" -Method POST -InFile $zipPath -Headers @{Authorization="Basic $auth"} -ContentType "application/zip" -UseBasicParsing
|
||||
|
||||
# Poll until build completes
|
||||
for ($i = 1; $i -le 40; $i++) {
|
||||
Start-Sleep -Seconds 15
|
||||
$resp = Invoke-WebRequest -Uri "https://{app}.scm.azurewebsites.net/api/deployments/latest" -Headers @{Authorization="Basic $auth"} -UseBasicParsing
|
||||
$deploy = $resp.Content | ConvertFrom-Json
|
||||
if ($deploy.complete -eq $true) { break }
|
||||
}
|
||||
```
|
||||
|
||||
Prerequisites:
|
||||
- `SCM_DO_BUILD_DURING_DEPLOYMENT=true` must be set (verified in Step 6a)
|
||||
- Runtime manifest must be at the zip root — Oryx detects the runtime from it:
|
||||
- Python: `requirements.txt` (or `pyproject.toml`)
|
||||
- Node.js: `package.json` (+ lockfile)
|
||||
- Ruby: `Gemfile`
|
||||
- PHP: `composer.json`
|
||||
- Do NOT pre-install packages locally — let Oryx run the install remotely
|
||||
|
||||
> **SCM auth lifecycle (REST API toggle — no Bicep edits):**
|
||||
>
|
||||
> IaC has `scm.allow: true` (deploy convenience). Deploy phase:
|
||||
> Deploy code → health check → re-disable via REST API → verify `false`.
|
||||
> If re-disable fails, log but don't block — add postDeployRecommendation.
|
||||
|
||||
**Zip creation:** Use `System.IO.Compression.ZipFile` with relative paths from workspace root. **On Windows, normalize entry paths: `$entryName = $relativePath.Replace('\', '/')` — ZipFile preserves backslashes which Linux App Service cannot resolve.** Never use `Compress-Archive -Path $files.FullName` — absolute paths flatten the directory structure, causing app crashes (`./src/app` not found).
|
||||
|
||||
## Database Post-Deploy Verification
|
||||
|
||||
> ⛔ **You MUST read [`database-post-deploy.md`](database-post-deploy.md)** for migration discovery, execution via App Service SSH, error handling, and PostgreSQL-specific checks.
|
||||
@@ -0,0 +1,98 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Code Deployment — Container Apps
|
||||
|
||||
After IaC deployment creates the Container App with a placeholder image (`mcr.microsoft.com/azuredocs/containerapps-helloworld:latest`), deploy the actual application code. This is **Phase 2** of the two-phase Container Apps deployment pattern.
|
||||
|
||||
> ⛔ **`--subscription {subscriptionId}` on EVERY `az` command** (from `context.json.azure.subscriptionId`). Without it, the CLI uses whatever subscription is currently active — which may have changed since the prepare phase. This applies to ALL commands below.
|
||||
|
||||
> ⛔ **Phase 2 is NOT optional.** If IaC deployed a Container App with a placeholder image, you MUST execute Phase 2 before health checks. Do NOT leave a placeholder image and tell the user to deploy code manually.
|
||||
|
||||
**Determine the code deploy path:**
|
||||
|
||||
| Condition | Path | Steps |
|
||||
|-----------|------|-------|
|
||||
| ACR exists in IaC (`services[]` has Container Registry) | ACR build + image update | Steps 1–4 below |
|
||||
| No ACR in IaC, simple app (single Dockerfile or source) | Add ACR to IaC + redeploy, then ACR build | Step 0 + Steps 1–4 |
|
||||
| No ACR in IaC, no Dockerfile, Oryx-compatible (Node/Python/Go/.NET) | `az containerapp update --source .` | Step 5 (Oryx shortcut) |
|
||||
|
||||
## Step 0 — Add ACR to IaC (if not already present)
|
||||
|
||||
Add `container-registry.bicep` module (ACR Basic, `adminUserEnabled: false`) + AcrPull role assignment for CA's managed identity (role GUID: `7f951dda-4ed3-4680-a7ca-43fe172d538d`). Redeploy IaC, wait ~60s for AcrPull role propagation. Log as healing attempt.
|
||||
|
||||
⛔ Update `prepare-plan.json` (add ACR to `services[]`), `scaffold-manifest.json.files[]`, and `costEstimate`.
|
||||
|
||||
## Steps 1–4 — ACR Build + Image Update (IaC-compliant)
|
||||
|
||||
| Step | Command | Notes |
|
||||
|------|---------|-------|
|
||||
| 1. Build image | `az acr build --subscription {subscriptionId} -r {acrName} -t {appName}:latest . --no-logs` | Builds from workspace root. If `buildRequirements.hasBuildKitSyntax`, use `-f Dockerfile.azure` (see BuildKit handling below). ⛔ **Build-time env vars:** If Dockerfile has `ARG NEXT_PUBLIC_*` or `ARG VITE_*`, pass `--build-arg NEXT_PUBLIC_API_URL=https://{api-fqdn}` to inject the deployed API URL. These vars are baked into the JS bundle at build time — runtime env vars have no effect on client-side code. Get the API FQDN from Phase 1 output: `az containerapp show -g {rg} -n {apiApp} --query properties.configuration.ingress.fqdn -o tsv`. |
|
||||
| 2. Update Bicep image | Edit `infra/modules/{containerapp}.bicep`: replace placeholder with `image: '{acrLoginServer}/{appName}:latest'`. Add ACR registry config: `registries: [{ server: acrLoginServer, identity: 'system' }]` | IaC-only — no imperative `az containerapp update` |
|
||||
| 3. Redeploy | `az deployment sub create --subscription {subscriptionId} --location {location} --template-file infra/main.bicep --parameters @infra/main.parameters.json --parameters containerImage='{acrLoginServer}/{appName}:latest' --name app-onboard-code-deploy-{timestamp} --query properties.provisioningState -o tsv` | Emit new portal link. ⛔ No DB password param — databases are managed-identity only (see [Parameter Pass-Through](#parameter-pass-through-bicep-redeploy-safety)). |
|
||||
| 4. Verify revision | `az containerapp show --subscription {subscriptionId} -g {rg} -n {ca} --query "{revision:properties.latestReadyRevisionName, image:properties.template.containers[0].image}" -o json` | Confirm image is not the placeholder |
|
||||
|
||||
> ⛔ **`az acr build` failures count toward the `deploy-result.json.healingAttempts[]` counter.** After 3 failed builds (even with different root causes), pause and present a diagnosis to the user: "Web image build failed 3 times: [root causes]. Continue healing? (Yes / Cancel)." Each build fix attempt = 1 healing entry. Cross-reference deploy instructions.md healing loop rule.
|
||||
|
||||
> ⛔ **Windows: `az acr build` may fail with `UnicodeEncodeError`.** Azure CLI log streaming crashes on non-ASCII characters (✓, ✗) using Windows `charmap` codec. Append `--no-logs` to the build command. Build success/failure is still reported via exit code.
|
||||
|
||||
## Step 5 — Oryx Shortcut (no ACR needed, no Dockerfile)
|
||||
|
||||
```powershell
|
||||
az containerapp update --subscription {subscriptionId} -g {rg} -n {ca} --source . --set-env-vars NODE_ENV=production
|
||||
```
|
||||
|
||||
> ⛔ **`az containerapp update --source` is allowed for code deploy ONLY.** This is different from `az containerapp up --source` (which is ⛔ BLOCKED because it creates resources imperatively). `update --source` updates an EXISTING Container App — no state drift.
|
||||
|
||||
## App-Internal Secrets (native CA secrets — No Key Vault, no seeding step)
|
||||
|
||||
App-internal secrets are set as **native Container Apps secrets** directly in the Bicep, with the value supplied as an `@secure()` param on the deployment command. ⛔ **No Key Vault, no `az keyvault secret set` step.** Generate each secret ONCE and pass it on every `az deployment sub create`:
|
||||
|
||||
```powershell
|
||||
$secretKey = (openssl rand -base64 32) -replace '[/+=]','' # generate ONCE, persist to deploy-secrets.env
|
||||
az deployment sub create ... --parameters secretKey=$secretKey --query properties.provisioningState -o tsv
|
||||
```
|
||||
|
||||
> ⛔ Reload the SAME value from `deploy-secrets.env` on every retry/redeploy (shell variables don't persist between tool calls). Pass it to EVERY deployment so the desired-state apply keeps the CA secret stable.
|
||||
|
||||
## After Code Deploy (all paths)
|
||||
|
||||
- Wait 30–60s for the new revision to become ready
|
||||
- Proceed to Step 7 (Health-Check Endpoints)
|
||||
- If health check returns placeholder content → image update didn't take effect. Check `az containerapp revision list`
|
||||
|
||||
> ⛔ **`az containerapp revision restart` does NOT pick up a changed secret.** Container Apps bind secret values at revision *creation* time. To pick up an updated native secret, create a NEW revision by redeploying Bicep (preferred) or `az containerapp update --revision-suffix rev{timestamp}`.
|
||||
|
||||
> ⛔ **ACR registries require the CA managed identity + AcrPull to exist first.** Phase 1 of the two-phase deploy creates the Container App with a placeholder image and `registries: []` (native secrets MAY be set in Phase 1 — they have no RBAC dependency). ACR `registries` with `identity: 'system'` fails in Phase 1 because the CA's managed identity doesn't exist yet (no principalId → AcrPull fails → "Operation expired"). After Phase 1 completes and RBAC propagates (~60s), Phase 2 redeploys with ACR registries + real image + correct `targetPort`.
|
||||
|
||||
## Config-File Apps (Go/Viper, Spring Boot, etc.)
|
||||
|
||||
Creating Azure-specific config (e.g., `config-azure.yml`) is fine for non-secret values.
|
||||
|
||||
> ⛔ **NEVER bake secrets into config files COPY'd into Docker images.**
|
||||
|
||||
**Secret injection:** Config uses placeholders → `az containerapp secret set` → `az containerapp update --set-env-vars KEY=secretref:name` → framework env var override.
|
||||
|
||||
> **Go/Viper:** `AutomaticEnv()` maps `POSTGRES_PASSWORD` → `postgres_password` (underscores), NOT `postgres.password`. Without `SetEnvKeyReplacer(strings.NewReplacer(".", "_"))`, env vars can't override nested keys.
|
||||
|
||||
## BuildKit Dockerfile Handling
|
||||
|
||||
ACR's `az acr build` uses the classic Docker builder — it does NOT support BuildKit. When `buildRequirements.hasBuildKitSyntax == true`: build using the ACR-compatible copy instead: `az acr build -f Dockerfile.azure .`. The user's original Dockerfile stays untouched. If `Dockerfile.azure` doesn't exist yet, create it by stripping BuildKit syntax from the original Dockerfile per [`dockerfile-generation.md § ACR Build Compatibility`](../../scaffold/references/dockerfile-generation.md).
|
||||
|
||||
## Parameter Pass-Through (Bicep Redeploy Safety)
|
||||
|
||||
⛔ **On every redeploy, pass the SAME values you originally used** — a full desired-state apply, not a patch, so an omitted param reverts to its default. The one param that bites:
|
||||
|
||||
- **`containerImage`** — omitting it reverts the Container App to the placeholder image.
|
||||
|
||||
> ⛔ **No `administratorLoginPassword` (or any DB password/key) param exists** — databases are Entra/managed-identity only. There is nothing to re-pass and no `deploy-secrets.env` DB password. App-internal secrets (e.g. `SECRET_KEY`) live in Key Vault and are read via the app's MI — they are not passed as deploy params.
|
||||
|
||||
```powershell
|
||||
az deployment sub create ... `
|
||||
--parameters containerImage='{acrLoginServer}/{appName}:latest' `
|
||||
--query properties.provisioningState -o tsv
|
||||
```
|
||||
|
||||
## Database Post-Deploy Verification
|
||||
|
||||
> ⛔ **You MUST read [`database-post-deploy.md`](database-post-deploy.md)** for migration discovery, execution via Container Apps exec, error handling, and PostgreSQL-specific checks.
|
||||
@@ -0,0 +1,93 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Code Deployment — Azure Functions (Flex Consumption)
|
||||
|
||||
How to deploy application code to a **Flex Consumption** function app after its IaC is provisioned. This is a
|
||||
**different channel** from App Service — read this instead of
|
||||
[code-deployment-appservice.md](code-deployment-appservice.md) whenever the compute is Functions on Flex
|
||||
Consumption (the default Functions floor). Only **Functions on Premium (EP1)** use the App Service SCM/Kudu
|
||||
path.
|
||||
|
||||
> ⛔ **`--subscription {subscriptionId}` on EVERY `az` command.**
|
||||
|
||||
## The Flex deploy model (why the App Service path does not apply)
|
||||
|
||||
Flex Consumption has a **single deployment path**: you build and zip your project, the package is written to a
|
||||
**blob storage container**, and the app runs from that package on startup. There is **no** Kudu
|
||||
`SCM_DO_BUILD_DURING_DEPLOYMENT`/Oryx-on-SCM build, **no** `az webapp deploy --type zip`, and **no**
|
||||
`basicPublishingCredentialsPolicies` (`scm`/`ftp`) enable/re-disable dance. Do not run any of those against a
|
||||
Flex app — they either error or silently do nothing.
|
||||
|
||||
## Deploy the package
|
||||
|
||||
Build/zip the project, then deploy with the Functions channel. Prefer a **remote build** for interpreted
|
||||
runtimes so native wheels/modules are built on Linux, not on the local machine:
|
||||
|
||||
```powershell
|
||||
# Python / Node / TypeScript — zip the PROJECT ROOT (manifest at the zip root) and request a remote build.
|
||||
az functionapp deployment source config-zip `
|
||||
--subscription {sub} -g {rg} -n {app} `
|
||||
--src $zipPath --build-remote true
|
||||
|
||||
# .NET (isolated) / Java / Go — deploy a precompiled package (build output at the zip root); no remote build.
|
||||
az functionapp deployment source config-zip --subscription {sub} -g {rg} -n {app} --src $zipPath
|
||||
```
|
||||
|
||||
`func azure functionapp publish {app}` (Azure Functions Core Tools) is an equivalent channel and also performs
|
||||
a remote build — use it if Core Tools is already the project's workflow.
|
||||
|
||||
- The runtime is fixed by `functionAppConfig.runtime` in IaC — do **not** pass or set `FUNCTIONS_WORKER_RUNTIME`.
|
||||
- The deploying identity writes the package to the deployment container. With shared-key access disabled, that
|
||||
identity (the signed-in user for a local deploy, or the CI principal) needs **Storage Blob Data Contributor**
|
||||
on the deployment storage account, in addition to the function app's own identity role.
|
||||
- After deploy, sync is automatic; then run the health check below.
|
||||
|
||||
## ⛔ Local (non-CI) deploys: hardening can lock you out — do not deny-by-default before first deploy
|
||||
|
||||
This pipeline hardens network access, which is CI/CD-friendly but **can block a deploy run from a developer
|
||||
machine.** Two Flex-specific facts matter:
|
||||
|
||||
1. **Flex outbound is a platform-managed gateway pool, not a per-instance IP.** An SCM/site allow rule built
|
||||
from the deployer's observed public IP (e.g. via ipify) will **not** match the channel's real egress, so it
|
||||
does not unblock the deploy and often changes between attempts. Do **not** try to punch a `/32` SCM hole to
|
||||
fix a 403 — it is the wrong lever for Flex.
|
||||
2. **Model the main site and the deployment/SCM surface independently.** Restricting one must not implicitly
|
||||
deny the other.
|
||||
|
||||
**Rules for the deploy phase:**
|
||||
|
||||
- ⛔ **Do NOT set `publicNetworkAccess: 'Disabled'` or a deny-by-default rule on the function app or its
|
||||
deployment storage BEFORE the first successful code deploy + health check.** Provision with access open,
|
||||
deploy, verify, *then* harden.
|
||||
- Before hardening, **tell the user** (once, plainly): *"This deployment hardens network access for
|
||||
production. Publishing the code from your machine happens first, while access is still open; after the app
|
||||
is healthy I'll restrict it. If you need to redeploy later from your machine, you may need to temporarily
|
||||
re-open access or deploy from CI."*
|
||||
- If a package deploy returns **HTTP 403**, the cause is network hardening applied too early or a missing
|
||||
Storage Blob Data role — **not** a missing SCM allow rule. Re-open public access on the app + deployment
|
||||
storage, confirm the deployer's role assignment, redeploy, then re-harden. Never fall back to imperative
|
||||
provisioning.
|
||||
- ⛔ **Azure inserts a terminal "Deny all" rule as the last entry of any `ipSecurityRestrictions` /
|
||||
`scmIpSecurityRestrictions` list — this is the expected default sentinel, NOT an error.** Validation must
|
||||
treat a trailing deny-all with priority `2147483647` (name `Deny all`/`Explicit deny`) as normal. Only flag
|
||||
it if there are **no** preceding allow rules AND you intended the surface to be reachable.
|
||||
|
||||
## Hardening (only AFTER a healthy deploy)
|
||||
|
||||
If the plan calls for a locked-down app, apply `publicNetworkAccess`/IP restrictions as the **last** step,
|
||||
after the health check passes. Keep the deployment storage reachable by the app identity. Record any access
|
||||
you re-open for a local deploy so cleanup is idempotent (re-running the step must not error if the rule is
|
||||
already gone).
|
||||
|
||||
## Health check
|
||||
|
||||
HTTP GET the function endpoint(s) (max 3 iterations, honoring cold start). A Flex app can take up to ~30s to
|
||||
initialize on first start; `System.TimeoutException`/gRPC host messages during that window are startup noise,
|
||||
not deploy failures. Inspect the response body for error patterns (`connection refused`, `MODULE_NOT_FOUND`,
|
||||
`SET-IN-DEPLOY-PHASE`) — HTTP 200 alone is not proof of health when the app depends on another service.
|
||||
|
||||
## Database post-deploy
|
||||
|
||||
> ⛔ If the app uses a managed database, run [`database-post-deploy.md`](database-post-deploy.md) for migration
|
||||
> discovery and execution.
|
||||
@@ -0,0 +1,57 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Code Deployment — Static Web Apps
|
||||
|
||||
## Static Web Apps — Content Deployment (Step 6c)
|
||||
|
||||
Deploy content using the SWA CLI with deployment token.
|
||||
|
||||
**Pre-check:** Verify `swa` CLI is installed: `npx --yes @azure/static-web-apps-cli --version`. If not available, install: `npm install -g @azure/static-web-apps-cli`.
|
||||
|
||||
> ⛔ **Pre-deploy: Build the frontend if SPA source (not pre-built HTML).** Check if the SWA component directory has a `package.json` (or equivalent manifest) with a `build` script. If yes, detect the package manager from the lockfile (`package-lock.json` → npm, `yarn.lock` → yarn, `pnpm-lock.yaml` → pnpm, `bun.lock`/`bun.lockb` → bun; default to npm if no lockfile) and run:
|
||||
> ```powershell
|
||||
> cd {component-path} # e.g., web/
|
||||
> {pm} install # npm install, yarn install, pnpm install, etc.
|
||||
> {pm} run build # npm run build, yarn build, pnpm build, etc.
|
||||
> ```
|
||||
> The deploy path should point to the **build output directory** — read the framework config to determine the output dir (`vite.config.*` → `build.outDir`, `next.config.*` → `.next/` or `out/`, CRA → `build/`; default to `dist/`). If no `package.json` exists (plain HTML/CSS/JS), deploy the source directory directly — no build needed.
|
||||
>
|
||||
> ⛔ **This is NOT optional for SPA frameworks.** Raw JSX/TSX/Vue/Svelte source files cannot be served by SWA — the app must be compiled to static HTML/JS/CSS first. Skipping the build produces a broken deployment.
|
||||
|
||||
> ⛔ **Pre-deploy: Update frontend config with deployed backend URLs.** If `prereq-output.json.cloudSdkSwaps[]` mapped cloud SDK endpoints (AWS API Gateway, GCP Cloud Functions) to Azure equivalents, the frontend config file (`config.ts`, `.env`, `environment.ts`) still references the original cloud URLs. After IaC deploy (Step 6), read `deploy-result.json.endpoints[]` to get the deployed Azure backend URLs. Update the frontend config with these URLs before running `swa deploy`. This is a string replacement in the config file — NOT a code rewrite. Do NOT defer as a post-deploy step — the SWA will show a broken page if the frontend calls non-existent AWS/GCP endpoints.
|
||||
|
||||
| Step | Command | Notes |
|
||||
|------|---------|-------|
|
||||
| 1. Get token | `$token = az staticwebapp secrets list --name {swa} -g {rg} --query "properties.apiKey" -o tsv 2>$null` | Store in variable first — do NOT pass inline. ⛔ **On Windows, use `2>$null`** (not `2>&1`) — Azure CLI Python warnings corrupt `-o tsv` output |
|
||||
| 2. Set env var | `$env:SWA_CLI_DEPLOYMENT_TOKEN = $token` | ⛔ Use env var ONLY — do NOT pass `--deployment-token $token` as CLI arg (leaks token in process args / transcript) |
|
||||
| 3. Copy to temp | `$tempDir = "C:\temp\swa-deploy"; Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue; New-Item -ItemType Directory -Path $tempDir -Force \| Out-Null; Copy-Item -Path .\* -Destination $tempDir -Recurse -Exclude @('.git','.copilot-azure','node_modules','.azure','infra')` | ⛔ **MANDATORY on Windows** — `$env:TEMP` often contains spaces (e.g., `C:\Users\Jane Doe\AppData\Local\Temp`). Always use a short, space-free path on the FIRST attempt — do NOT use `$env:TEMP` and retry. On macOS/Linux, use `/tmp/swa-deploy` instead. **If a build step ran:** copy the build output directory (e.g., `{component}/dist/`) instead of the entire workspace — `Copy-Item -Path {component}\dist\* -Destination $tempDir -Recurse`. |
|
||||
| 4. Deploy | `swa deploy $tempDir --app-name {swaName} --env production` | ⛔ **`--app-name` is MANDATORY** — without it, the SWA CLI launches an interactive "create new project?" prompt that fails in automation. `{swaName}` = the SWA resource name from `prepare-plan.json.naming.resources[]`. SWA CLI reads `SWA_CLI_DEPLOYMENT_TOKEN` from env var automatically — do NOT pass `--deployment-token` flag (leaks token in command args) |
|
||||
| 5. Clean up | `Remove-Item -Recurse -Force $tempDir` | Clean temp dir after successful deploy |
|
||||
|
||||
⛔ **`az staticwebapp deploy` does NOT exist** — the correct CLI is `swa deploy` (from `@azure/static-web-apps-cli`).
|
||||
|
||||
## Fallback: Direct StaticSitesClient Upload
|
||||
|
||||
If `swa deploy` fails (binary crash, path errors on Windows), use the underlying `StaticSitesClient.exe` directly:
|
||||
|
||||
```powershell
|
||||
# Find the binary bundled with @azure/static-web-apps-cli
|
||||
$swaCliPath = (Get-Command swa).Source | Split-Path -Parent
|
||||
$client = Get-ChildItem -Path $swaCliPath -Recurse -Filter "StaticSitesClient*" | Select-Object -First 1
|
||||
|
||||
# Upload from PARENT directory with relative paths (avoids "identical to artifact folder" error)
|
||||
Push-Location (Split-Path $tempDir -Parent)
|
||||
& $client.FullName upload --app (Split-Path $tempDir -Leaf) --apiToken $token --skipAppBuild true
|
||||
Pop-Location
|
||||
```
|
||||
|
||||
> ⛔ **Run from a PARENT directory** with a relative `--app` path. Running from inside the app directory causes `StaticSitesClient` to error with "Current directory cannot be identical to or contained within artifact folders."
|
||||
|
||||
## Finalize deploy-result.json (after `swa deploy` succeeds)
|
||||
|
||||
⛔ **`swa deploy` succeeding is NOT the end of the deploy phase.** SWA finalizes through the **generic Step 8** (see [`deploy-checklist-template.md`](deploy-checklist-template.md) §"Before handoff (Step 8)") — overwrite the `deploy-result.json` skeleton IN PLACE with the full `DeployResult` contract, not a `status`+`subscriptionId` stub. Only the values Step 8 can't derive on the SWA path are below:
|
||||
|
||||
- **Hostname** — `swa deploy` produces no ARM endpoint output, so fetch it: `$swaHost = az staticwebapp show -n {swa} -g {rg} --query defaultHostname -o tsv`
|
||||
- `endpoints[]` — `[{ name, url: "https://$swaHost", healthStatus: "healthy" }]` (HTTP GET `https://$swaHost/` → 2xx confirms healthy)
|
||||
- `resourceIds[]` — include the `Microsoft.Web/staticSites` resource id
|
||||
@@ -0,0 +1,136 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Database Post-Deploy Verification (Managed-Identity / Entra-Only)
|
||||
|
||||
Run schema migrations on AppOnboard-created databases (listed in `prepare-plan.json.services[]`) before health checks. The app must be running first — if it's crashing, fix that before attempting migrations.
|
||||
|
||||
> ⛔ **No passwords anywhere.** Databases are provisioned Entra-only (no `administratorLogin`/`administratorLoginPassword`, no access keys). Admin operations (create DB, grant the app's managed identity a role, run migrations) authenticate with an **Entra access token** minted for the deploying principal, which the Bicep set as the server's Entra administrator. There is no `deploy-secrets.env` DB password and no `pgAdminPassword` parameter.
|
||||
|
||||
## 0. Mint an Entra token for the deploying principal
|
||||
|
||||
The deployer is the Entra admin on the server (set by the DB module's `administrators` child resource). Mint a short-lived token and use it as the "password" for CLI connections. `{entraAdminName}` = the deployer's UPN / app display name (the `login` set in the server's `administrators` block).
|
||||
|
||||
```powershell
|
||||
# PostgreSQL / MySQL Flexible Server (OSS RDBMS audience)
|
||||
$dbToken = az account get-access-token --resource-type oss-rdbms --query accessToken -o tsv
|
||||
# Azure SQL (SQL audience)
|
||||
$sqlToken = az account get-access-token --resource https://database.windows.net/ --query accessToken -o tsv
|
||||
```
|
||||
|
||||
## 1. Create the app database (if needed)
|
||||
|
||||
> ⛔ **Azure PostgreSQL/MySQL Flexible Server only creates the system `postgres`/`mysql` database by default.** If the app's config references a named database (e.g., `car_sale_db`, `myapp_production`), create it BEFORE the container starts — using token auth, never a password:
|
||||
>
|
||||
> ```powershell
|
||||
> az postgres flexible-server execute -n {pg} -g {rg} -u "{entraAdminName}" -p $dbToken `
|
||||
> -d postgres --querytext "CREATE DATABASE {dbName};"
|
||||
> ```
|
||||
>
|
||||
> Detect the database name from: (1) `prereq-output.json.initCommands[]` with `type: "db-migrate"`, (2) app config files (`config-docker.yml`, `.env`, `database.yml`), (3) compose `POSTGRES_DB` env var. If the container crashes with `database "X" does not exist`, this step was missed. (MySQL app DBs are emitted in Bicep as a `flexibleServers/databases` child — no CLI step needed.)
|
||||
|
||||
## 2. Grant the app's managed identity a database role
|
||||
|
||||
The app authenticates with its **managed identity**, which must exist as a database principal. Do this ONCE, as the Entra admin, before migrations. `{appMiName}` = the compute resource name (system-assigned MI display name); `{appMiObjectId}` = its object id.
|
||||
|
||||
**PostgreSQL** — two connections are required. The `pgaadauth_*` functions exist **only on the `postgres` database** (running them on the app DB fails with `No function matches the given name and argument types`), and for a **managed identity** you must use the `_with_oid` variant with `objectType 'service'` (the name-only `pgaadauth_create_principal` can't resolve a managed identity by name). Roles are cluster-wide, so create the principal on `postgres`, then GRANT on `{dbName}`:
|
||||
|
||||
```powershell
|
||||
# 2a. Create the Entra principal for the app MI — MUST run on the `postgres` database.
|
||||
az postgres flexible-server execute -n {pg} -g {rg} -u "{entraAdminName}" -p $dbToken -d postgres --querytext @"
|
||||
SELECT * FROM pgaadauth_create_principal_with_oid('{appMiName}', '{appMiObjectId}', 'service', false, false);
|
||||
"@
|
||||
|
||||
# 2b. Grant privileges — run on the app database {dbName} (the role already exists cluster-wide).
|
||||
# NOTE: a PowerShell here-string does NOT treat "" as an escape — use a single " around the identifier
|
||||
# (doubling it emits `TO ""myapp"";` → Postgres `zero-length delimited identifier`).
|
||||
az postgres flexible-server execute -n {pg} -g {rg} -u "{entraAdminName}" -p $dbToken -d {dbName} --querytext @"
|
||||
GRANT ALL PRIVILEGES ON DATABASE {dbName} TO "{appMiName}";
|
||||
GRANT ALL ON SCHEMA public TO "{appMiName}";
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "{appMiName}";
|
||||
"@
|
||||
```
|
||||
|
||||
**MySQL:** `CREATE AADUSER '{appMiName}' IDENTIFIED BY '{appMiObjectId}'; GRANT ALL PRIVILEGES ON `` `{dbName}`.* `` TO '{appMiName}'@'%'; FLUSH PRIVILEGES;`
|
||||
|
||||
**Azure SQL:** `CREATE USER [{appMiName}] FROM EXTERNAL PROVIDER;` then add to `db_datareader`, `db_datawriter`, and (if the app runs migrations) `db_ddladmin`.
|
||||
|
||||
**Discover the migration command** from the codebase (check in order):
|
||||
|
||||
| Signal | Command | Working directory |
|
||||
|--------|---------|-------------------|
|
||||
| `alembic.ini` exists | `alembic upgrade head` | Directory containing `alembic.ini` |
|
||||
| Django `manage.py` exists | `python manage.py migrate` | Directory containing `manage.py` |
|
||||
| `prisma/schema.prisma` exists | `npx prisma migrate deploy` | Project root |
|
||||
| EF `Migrations/` directory | `dotnet ef database update` | Project root |
|
||||
| Rails `db/migrate/` directory | `rails db:migrate` | Project root |
|
||||
| Sequelize `migrations/` + `.sequelizerc` | `npx sequelize-cli db:migrate` | Project root |
|
||||
|
||||
## Execute via the Deployed Environment
|
||||
|
||||
Migrations run where the managed identity lives — inside the app, not from your workstation.
|
||||
|
||||
> ⛔ **Authenticate with a token, never a password — and PREFER the app's own credential.**
|
||||
> - **(1) Preferred — `DefaultAzureCredential`:** if the app's DB config uses `DefaultAzureCredential` / `Authentication=Active Directory Default`, the migration command works **unchanged** and authenticates as the app MI. No token handling, no `curl`/`jq`, works on slim/distroless images. Use this whenever possible.
|
||||
> - **(2) Fallback — inject an MI token:** only if the app cannot self-authenticate. Fetch a token from the container's IMDS endpoint and pass it as the driver password env var (see below). ⛔ This requires `curl` + `jq` (or `wget`) **in the image** — absent on `slim`/`distroless` builds. If the tools aren't present, do NOT hand-roll it: surface a `FLAGGED` finding + `postDeployRecommendation` to add `DefaultAzureCredential` to the app's DB config.
|
||||
> - ⛔ Never re-enable password auth to unblock.
|
||||
|
||||
### App Service (Linux only)
|
||||
|
||||
```powershell
|
||||
az webapp ssh -n {app} -g {rg} --subscription {sub}
|
||||
```
|
||||
Then, **inside the SSH session** (the container's own shell — no PowerShell layer), for the token-injection fallback (requires `curl`+`jq` in the image):
|
||||
```bash
|
||||
export PGPASSWORD="$(curl -s "$IDENTITY_ENDPOINT?resource=https://ossrdbms-aad.database.windows.net&api-version=2019-08-01" -H "X-IDENTITY-HEADER: $IDENTITY_HEADER" | jq -r .access_token)"
|
||||
export PGUSER={appMiName} PGSSLMODE=require
|
||||
{migration_command}
|
||||
```
|
||||
|
||||
### Container Apps
|
||||
|
||||
**Preferred (DefaultAzureCredential — nothing to inject, works on distroless):**
|
||||
```powershell
|
||||
az containerapp exec -n {ca} -g {rg} --subscription {sub} --command '{migration_command}'
|
||||
```
|
||||
|
||||
**Fallback (token injection; image must have `curl`+`jq`):**
|
||||
```powershell
|
||||
# ⛔ SINGLE-QUOTE the whole --command so $(...) and $IDENTITY_* evaluate IN THE CONTAINER, not on the deployer.
|
||||
# (A double-quoted PowerShell string would run curl locally and expand $IDENTITY_* to empty → empty PGPASSWORD.)
|
||||
az containerapp exec -n {ca} -g {rg} --subscription {sub} --command 'sh -lc "export PGPASSWORD=$(curl -s \"$IDENTITY_ENDPOINT?resource=https://ossrdbms-aad.database.windows.net&api-version=2019-08-01\" -H \"X-IDENTITY-HEADER: $IDENTITY_HEADER\" | jq -r .access_token); export PGUSER={appMiName} PGSSLMODE=require; {migration_command}"'
|
||||
```
|
||||
|
||||
> Preferred over both: bake the migration into the app's startup (`initCommands[]` → `appCommandLine`) using the app's own `DefaultAzureCredential` config, so migrations run token-based on every cold start (idempotent) with no exec step and no `curl`/`jq` dependency.
|
||||
|
||||
## Error Handling
|
||||
|
||||
If the migration command fails, classify as `IAC_ERROR` and check based on the database type:
|
||||
- **AAD token / auth failure** (`password authentication failed`, `Login failed for token-identified principal`) → the app MI was not granted a DB role (§2), or Entra-admin/RBAC propagation hasn't completed. Verify the app MI principal exists, wait 60s, retry.
|
||||
- DB unreachable → check firewall rules (PostgreSQL: `AllowAllAzureServicesAndResourcesWithinAzureIps`, SQL: server firewall, MySQL: similar)
|
||||
- Extension/feature missing → check DB-specific config (PostgreSQL: `azure.extensions`, SQL: compatibility level, MySQL: `require_secure_transport`)
|
||||
- Module not found → verify the runtime includes the migration tool
|
||||
|
||||
> ⛔ **Never weaken auth to unblock.** Do NOT set `passwordAuth: 'Enabled'`, add an `administratorLoginPassword`, or re-enable access keys to make a failing migration pass. Fix the grant (§2) or the client token config.
|
||||
|
||||
## PostgreSQL-Specific Checks
|
||||
|
||||
Run BEFORE migrations when `services[]` includes PostgreSQL Flexible Server:
|
||||
|
||||
1. **Token connectivity:** `az postgres flexible-server execute -n {pg} -g {rg} -u "{entraAdminName}" -p $dbToken -d postgres --querytext "SELECT 1"` — if this fails, the firewall rule is missing, the deployer is not the Entra admin, or AAD propagation hasn't completed. Check `AllowAllAzureServicesAndResourcesWithinAzureIps` exists and the `administrators` child deployed, wait 60s, retry
|
||||
2. **Extension availability:** `az postgres flexible-server parameter show -g {rg} -n {pg} --name azure.extensions --query value -o tsv` — verify the extensions the app needs (e.g., `uuid-ossp` for Alembic/Django UUID fields) are in the allow-list. If missing, the Bicep module should have set them — check `infra/modules/postgresql.bicep`
|
||||
|
||||
## MySQL Flexible Server Checks
|
||||
|
||||
Run BEFORE migrations when `services[]` includes MySQL Flexible Server:
|
||||
|
||||
1. **Token connectivity:** `az mysql flexible-server execute -n {mysql} -u "{entraAdminName}" -p $dbToken -d mysql -q "SELECT 1"` — if this fails, check the firewall rule, that the Entra admin is set, and AAD propagation. Note: `execute` resolves by server name (no `-g` needed)
|
||||
2. **SSL enforcement:** `az mysql flexible-server parameter show -g {rg} -n {mysql} --name require_secure_transport --query value -o tsv` — verify matches the app's connection SSL mode
|
||||
|
||||
## Azure SQL Checks
|
||||
|
||||
Run BEFORE migrations when `services[]` includes Azure SQL:
|
||||
|
||||
1. **Database online:** `az sql db show -g {rg} -s {sqlServer} -n {dbName} --query status -o tsv` — verify the database is `Online`
|
||||
2. **Server firewall:** `az sql server firewall-rule list -g {rg} -s {sqlServer} -o table` — verify `AllowAllWindowsAzureIps` (0.0.0.0 → 0.0.0.0) exists for Azure-internal access
|
||||
3. **Entra-only auth:** verify `azureADOnlyAuthentication` is `true` and the app MI has a contained user (§2)
|
||||
@@ -0,0 +1,111 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Deploy Checklist Template (compaction-safe — generated at Step 5b)
|
||||
|
||||
Long-running deploy sessions lose rules when the conversation compacts. At Step 5b, generate a checklist file tailored to this deployment. Write it to disk so it survives compaction — re-reading costs ~100 tokens.
|
||||
|
||||
**Write** to `.copilot-azure/sessions/{id}/deploy-checklist.md` using the `create` tool at Step 5b.
|
||||
**Re-read** via `view` after every long-running command (`az deployment`, `az webapp deploy`, `az acr build`), after each failed health check, and after any conversation compaction.
|
||||
|
||||
## How to generate
|
||||
|
||||
Read `prepare-plan.json` to determine the service types, then build the checklist from the template below. **Replace `{placeholders}` with real values** and **delete sections that don't apply** (e.g., remove the App Service section for a Container Apps deploy).
|
||||
|
||||
```markdown
|
||||
# Deploy Checklist for {appName}
|
||||
# RG: {rgName} | Sub: {subscriptionId} | Session: {sessionId}
|
||||
|
||||
## ⛔ Secret generation (BEFORE first az deployment)
|
||||
- ⛔ **No database/cache/storage passwords or access keys exist** — those services are Entra/managed-identity only (no `@secure()` DB password params, no `administratorLoginPassword`). See [database-post-deploy.md](database-post-deploy.md).
|
||||
- Auto-generate ONLY app-internal `@secure()` params (e.g. `SECRET_KEY`, JWT key) before first `az deployment sub create` — NEVER `ask_user`
|
||||
- ⛔ On ANY retry OR redeploy (incl. after a conversation compaction): read the SAME `@secure()` value back from `deploy-secrets.env` (or `deploy-audit.log`) — NEVER regenerate. Pass it to EVERY deployment. An app-internal secret that's regenerated overwrites the live on-compute value → auth 500s while provisioning still reports success.
|
||||
|
||||
## ⛔ Read deploy/instructions.md
|
||||
- You MUST `view` deploy/instructions.md BEFORE running any `az deployment` command
|
||||
- Path: `.github/agents/azure-deploy/deploy/instructions.md`
|
||||
- If you have not read it in this conversation (or since the last compaction), read it NOW
|
||||
- It covers preflight checks, portal links, what-if, SCM lifecycle, deploy-result.json schema, audit logging, and health checks — skip it and none of these happen
|
||||
|
||||
## After every `az` command
|
||||
- Append 2 lines to `deploy-audit.log`: `{timestamp} | {command} | started` then `{timestamp} | {command} | succeeded/failed`
|
||||
|
||||
## After IaC deployment (Step 6)
|
||||
- ⛔ Call `capture_deployment_inventory` (`phase: "capture"`) after the deployment command returns — records created resources via a before/after `resources.list()` diff and flags orphans
|
||||
- Verify 5 tags: `az group show -n {rgName} --query tags`
|
||||
- ⛔ Do NOT set startup command or app settings via CLI — they are already in Bicep from scaffold. If `az webapp show` doesn't reflect them yet, wait 30s and re-check (ARM propagation delay). Do NOT run `az webapp config` imperatively.
|
||||
Required: app-onboard-skill, app-onboard-session-id, created-at, environment, deployed-by
|
||||
- Verify portal link is still correct if healing changed the deployment name
|
||||
|
||||
## Code deploy — Functions (Flex Consumption) (delete if not using Flex Functions)
|
||||
- ⛔ **Read [`code-deployment-functions-flex.md`](code-deployment-functions-flex.md).** Flex is NOT App Service: no SCM/Kudu, no `basicPublishingCredentialsPolicies`, no `SCM_DO_BUILD_DURING_DEPLOYMENT`/`ENABLE_ORYX_BUILD`.
|
||||
- Deploy the package: `az functionapp deployment source config-zip --subscription {sub} -g {rgName} -n {appName} --src {zip} [--build-remote true]` (remote build for Python/Node) — or `func azure functionapp publish {appName}`.
|
||||
- ⛔ **Local (non-CI) deploy warning — say it to the user:** hardening network access can block publishing from your machine. Deploy with access OPEN, verify health, THEN harden. An ipify-derived `/32` SCM allow rule will NOT match Flex egress (platform gateway pool) — do not try it.
|
||||
- ⛔ A trailing `Deny all` rule (priority `2147483647`) in `ipSecurityRestrictions`/`scmIpSecurityRestrictions` is the expected platform sentinel — NOT an error.
|
||||
- The app identity needs a Storage Blob Data role on the deployment storage (shared-key is disabled).
|
||||
|
||||
## Code deploy — App Service (delete if not using App Service)
|
||||
- ⛔ **Deploy command: `az webapp deploy --type zip`** (Entra-capable, supports `--async`). NEVER `az webapp deployment source config-zip` — it needs SCM basic auth and is disallowed.
|
||||
- Wait for stabilization: `az webapp show -g {rgName} -n {appName} --query state` → "Running"
|
||||
- Verify `SCM_DO_BUILD_DURING_DEPLOYMENT=true` is active before deploy (ARM timing can delay)
|
||||
- If build reports "0 seconds" but app needs deps: re-set the setting, wait 10s, retry
|
||||
- If 0s persists after 2 retries: fall back to Kudu `/api/zipdeploy`
|
||||
- Python: if no `antenv/` after deploy, use Kudu `/api/zipdeploy` immediately (OneDeploy may skip Oryx)
|
||||
- Windows zip paths: normalize with `.Replace('\', '/')` before creating zip entries
|
||||
- ⛔ Verify ORYX_DISABLE_COMPRESSION=true is set (prevents output.tar.zst extraction failures at startup — applies to ALL tiers, not just F1)
|
||||
- Set WEBSITES_CONTAINER_START_TIME_LIMIT=1800 for safety
|
||||
- TypeScript apps: verify `typescript` and `@types/*` are in `dependencies` (not devDependencies) — Oryx with NODE_ENV=production skips devDeps. Alternative: set app setting NPM_CONFIG_PRODUCTION=false
|
||||
- Enable SCM before zip deploy, re-disable after: `az rest --method put` → allow:false → verify
|
||||
- After deploy: check response body for Azure default page ("Your app service is up and running" = app didn't start)
|
||||
|
||||
## Code deploy — Container Apps (delete if not using Container Apps)
|
||||
- Phase 2 is NOT optional — deploy actual image, don't leave placeholder
|
||||
- Wait ~60s for RBAC propagation (AcrPull role) before code deploy
|
||||
- BuildKit Dockerfiles: create Dockerfile.azure without --mount syntax
|
||||
- Pass real image on EVERY Bicep redeploy: --parameters containerImage='{acr}/{app}:latest'
|
||||
- Native CA secrets: `revision restart` does NOT refresh — must create new revision
|
||||
- ACR build failures count toward healing counter
|
||||
- Windows: append `--no-logs` to `az acr build` to avoid UnicodeEncodeError
|
||||
- ⛔ After the revision is ready, run an explicit live HTTP probe against the ingress FQDN (`curl -sSfL`/`iwr` on `*.azurecontainerapps.io`) — this call IS the health check; capture the result into `deploy-result.json.endpoints[].healthStatus`.
|
||||
|
||||
## Code deploy — Static Web Apps (delete if not using SWA)
|
||||
- ⛔ **Build before deploy (SPA only):** If the SWA component has a manifest (`package.json`) with a `build` script: detect the package manager from the lockfile, run install + build, then deploy the build output directory (not raw source). Plain HTML repos (no manifest): skip build, deploy source directly.
|
||||
- ⛔ **Build-time env vars:** Before `npm run build`, set `VITE_API_BASE_URL` / `NEXT_PUBLIC_API_URL` / `REACT_APP_API_URL` to the deployed backend URL from `deploy-result.json.endpoints[]`. These are baked into the JS bundle at build time — runtime SWA env vars have no effect on client-side code.
|
||||
- If frontend config references cloud SDK endpoints (AWS API Gateway, GCP), update with deployed Azure backend URLs from `deploy-result.json.endpoints[]` before `swa deploy`
|
||||
- Use `swa deploy` (NOT `az staticwebapp deploy` — doesn't exist)
|
||||
- `--app-name {swaName}` is mandatory
|
||||
- Store token in $env:SWA_CLI_DEPLOYMENT_TOKEN — never as CLI arg
|
||||
|
||||
## During healing / retries
|
||||
- ⛔ REGION LOCK: Deploy region MUST match plan region ({region}). Any region change → RE-PRESENT deploy approval gate with old and new region. Do NOT silently switch. After approval: update `prepare-plan.json.services[].region`, `deploymentVariables.location`, AND append attempt number to `naming.suffix` (e.g., `edd6` → `edd602`). Recompute ALL resource names from the new suffix before redeploying — globally unique names (App Service, Storage, ACR) from the old region may be soft-deleted and unavailable.
|
||||
- ⛔ IaC-only: NEVER use `az containerapp update --image`, `az webapp update`, `az appservice plan delete`, or `az group create` — fix the Bicep and redeploy via `az deployment sub create`
|
||||
- ⛔ IaC-only for app-managed roles: NEVER `az role assignment create` for AcrPull or KV Secrets User — Bicep-managed (deterministic GUID), so an imperative grant collides on redeploy (`RoleAssignmentExists`). Missing app role = fix the Bicep module and redeploy. (Deployer/subscription-scope 403s are the ONLY exception — see [`error-classification.md`](error-classification.md).)
|
||||
- ⛔ **On error: read [`error-classification.md`](error-classification.md)** to classify the failure and follow the prescribed remediation. Do NOT ad-hoc heal without reading the classification.
|
||||
- ⛔ **Never weaken a security control to unblock** — do NOT flip `require_secure_transport`/TLS, HTTPS-only, KV purge protection, or auth OFF to make a failing deploy pass. A DB TLS handshake failure = fix the client SSL config (prereq `W-MYSQL-SSL`) or ask the user; never downgrade the server.
|
||||
- Count ALL attempts in deploy-result.json.healingAttempts[]
|
||||
After 3: STOP and ask user ("Yes / I have a suggestion / Stop")
|
||||
- NEVER run `az group delete` — track in orphanedResourceGroups[] (run `capture_deployment_inventory` after the healing attempt so the abandoned RG's resources are recorded deterministically)
|
||||
- ⛔ **RG deletion timeout:** If you ran `az group delete --no-wait`, wait max 2 minutes then `ask_user`: "Resource group deletion is slow. Wait longer / Proceed without cleanup / Cancel." Do NOT poll indefinitely.
|
||||
- Region/SKU/service changes require re-approval gate
|
||||
|
||||
## Before handoff (Step 8)
|
||||
- ⛔ Read [`deploy-schemas.ts`](deploy-schemas.ts) for exact DeployResult field names
|
||||
- ⛔ Run a final `capture_deployment_inventory` (`phase: "capture"`) — then populate `createdResources[]` and `orphanedResourceGroups[]` from its returned output (do NOT hand-author them)
|
||||
- Finalize `deploy-result.json` — overwrite skeleton IN PLACE (keep exact field names, do NOT rename): status (lowercase `succeeded`/`failed`), resourceGroupName, subscriptionId, deploymentNames (all used), resourceIds, endpoints, healthStatus (worst across endpoints), duration.completedUtc, resourceResults from `az deployment operation list`, createdResources + orphanedResourceGroups from the inventory. Read back to verify.
|
||||
- ⛔ `deployment-summary.md` — generate from `deploy-result.json` fields (Status, Health, Portal Links, Cleanup). NOT a separate data source.
|
||||
- ⛔ `context.json` — add "deploy" to completedPhases, set currentPhase to null, update lastModifiedUtc. VERIFY by reading back.
|
||||
- SCM re-disabled (App Service) or image param set (Container Apps)
|
||||
- If prereq found migration frameworks: run migrations before declaring healthy
|
||||
|
||||
## Artifact verification (Step 8 — MANDATORY)
|
||||
⛔ Before returning to orchestrator, verify ALL artifacts exist by reading each one back:
|
||||
1. `deploy-result.json` — MUST contain (exact names): `status` (lowercase `succeeded`/`failed`), `resourceGroupName`, `subscriptionId`, `deploymentNames[]`, `resourceIds[]`, `endpoints[]`, `healthStatus`, `duration.completedUtc`, `resourceResults[]`, `createdResources[]`, `orphanedResourceGroups[]`. `createdResources[]`/`orphanedResourceGroups[]` come from a final `capture_deployment_inventory` (`phase: "capture"`) — if empty or missing, run it NOW and write its output. Missing/renamed fields → rewrite with real values NOW
|
||||
2. `deploy-audit.log` — MUST exist with ≥2 entries (started + result for at least 1 command). Missing → reconstruct from memory
|
||||
3. `deployment-summary.md` — MUST contain Status, Health, Portal Links sections. Missing → generate from deploy-result.json
|
||||
4. `context.json` — MUST have `"deploy"` in `completedPhases`, `currentPhase: null`, updated `lastModifiedUtc`
|
||||
5. ⛔ **Endpoint completeness** — EVERY service in `prepare-plan.json.services[]` that hosts application code MUST have a corresponding entry in `deploy-result.json.endpoints[]` with code deployed and a valid `healthStatus` (`healthy`, `degraded`, `unreachable`, `unknown`). If ANY compute endpoint is missing or has code not deployed, set `partial: true` and `status: "failed"`. A deployment with undeployed user components is NOT `"succeeded"`.
|
||||
|
||||
If ANY artifact is missing or incomplete, write it NOW — do NOT return to orchestrator without all 5 checks passing.
|
||||
|
||||
⛔ **Then STOP — return to orchestrator. No further CLI commands or agent invocations.**
|
||||
```
|
||||
@@ -0,0 +1,66 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Deploy Safety
|
||||
|
||||
Hook-based safety rules for the deploy phase. Block destructive operations.
|
||||
|
||||
## deploy-result.json Skeleton
|
||||
|
||||
Created by scaffold validate sub-agent. If missing at deploy Step 5b, create from [`deploy-schemas.ts`](deploy-schemas.ts) with `status: "in-progress"`. Append to `deploymentNames[]` on each healing retry.
|
||||
|
||||
## Deploy Checklist (compaction-safe — generated at Step 5b)
|
||||
|
||||
> ⛔ **You MUST read [`deploy-checklist-template.md`](deploy-checklist-template.md)** at Step 5b to generate the checklist. Write the result to `.copilot-azure/sessions/{id}/deploy-checklist.md`. Re-read the generated checklist after every long-running command, failed health check, and conversation compaction.
|
||||
|
||||
## Finalize deploy-result.json (Step 8)
|
||||
|
||||
Overwrite the skeleton with real values:
|
||||
- `status` → `"succeeded"` or `"failed"`
|
||||
- `deploymentNames` → ALL names used (initial + retries)
|
||||
- `healthStatus` → worst across endpoints
|
||||
- `duration.completedUtc` → now
|
||||
- `resourceResults` → one entry per resource from `az deployment operation list`
|
||||
|
||||
## Blocked Patterns
|
||||
|
||||
> ⛔ **You MUST read [`blocked-patterns.md`](blocked-patterns.md)** before running ANY `az` command during the deploy phase. This file contains every command the agent is forbidden from executing. Block decisions are non-negotiable — user must run blocked commands manually outside AppOnboard.
|
||||
|
||||
## 403 Scope Fallback
|
||||
|
||||
When `az deployment sub create` returns 403 (insufficient subscription-scope permissions), do NOT halt immediately:
|
||||
|
||||
1. **Restructure Bicep to RG-scope** — change `targetScope = 'subscription'` to resource-group scope, remove the `Microsoft.Resources/resourceGroups` resource.
|
||||
2. **Create the RG via CLI** — `az group create -n {rg} -l {region} --tags app-onboard-skill=true app-onboard-session-id={sessionId} created-at={createdAt} environment={environmentName} deployed-by={deployedBy}`. All 5 AppOnboard tags MUST be included.
|
||||
3. **Retry with `az deployment group create`** — use `--resource-group {rg}` instead of subscription scope.
|
||||
4. **Regenerate portal link** for RG-scope — `$resId` must include `/resourceGroups/{rg}`: `$resId = "/subscriptions/{subscriptionId}/resourceGroups/{rg}/providers/Microsoft.Resources/deployments/$deploymentName"`. Re-run `Write-Output "LINK=$l"` and print the new bare URL. ⛔ Do NOT auto-open it (no `Start-Process`).
|
||||
5. **If retry ALSO fails with 403** → classify as `ENVIRONMENT_BLOCKING`. Surface required role: `az role assignment create --role Contributor --assignee {user} --scope /subscriptions/{sub}/resourceGroups/{rg}`.
|
||||
|
||||
## Deploy Checklist
|
||||
|
||||
> ⛔ **Use sync shells** so state persists. **App-internal secrets only** (e.g. `SECRET_KEY`, JWT signing key, third-party API keys) are generated at deploy and passed as `@secure()` params → stored on the compute resource (App Service app settings / Container Apps native secrets). ⛔ **No Key Vault.** ⛔ **There are NO database/cache/storage passwords or access keys** — those services are managed-identity + token (see [database-post-deploy.md](database-post-deploy.md)). Persist any app-internal secret to `.copilot-azure/sessions/{id}/deploy-secrets.env` — generate each ONCE (URL-safe, no `/+=`), reload in every later shell, and pass to EVERY deployment. Never regenerate an existing key (the desired-state apply would overwrite the live value). The file is a git-ignored cross-shell reload cache. NEVER echo or log rendered secret values.
|
||||
>
|
||||
> ⛔ **URL-safe app-internal secrets** when embedded in URLs. Forbidden chars: `# @ / ? % : & = + ;`.
|
||||
> ⛔ **`az webapp deploy` does NOT support `--track-status`.**
|
||||
> ⛔ **`az rest` on Windows PowerShell:** ALWAYS include `--headers "Content-Type=application/json"`.
|
||||
> ⛔ **Suppress deployment output:** Add `--query properties.provisioningState -o tsv` to deployment commands. For `az acr build`, append `--no-logs`.
|
||||
|
||||
## Post-Deploy Tag Verification
|
||||
|
||||
After deployment, verify all 5 AppOnboard tags: `az group show -n {rg} --query tags -o json`. Re-apply missing via `az tag update`.
|
||||
|
||||
## Deployment Operation Polling
|
||||
|
||||
For deployments with >5 resources, poll every 30s: `az deployment operation list --name {name} --subscription {sub} --query "[?properties.provisioningState=='Failed']" -o table`. Wait for FULL completion before healing — collect ALL errors in one pass.
|
||||
|
||||
## Re-Approval Gates
|
||||
|
||||
Region, service type, or SKU changes from user-approved values → re-present approval gate. Resource name changes → informational only. Same-deployment retries → no re-approval.
|
||||
|
||||
## Antipatterns
|
||||
|
||||
⛔ Do NOT `az group delete --no-wait` then `az group create` same name — background deletion takes 5-15 min and destroys the new RG. Use a different name or wait: `az group wait --name {rg} --deleted --timeout 900`.
|
||||
|
||||
## Artifact Reconciliation After Healing
|
||||
|
||||
After ANY healing that changes deployed resources, update `prepare-plan.json`, `scaffold-manifest.json`, and `context.json` to reflect actual state. Track orphaned RGs in `deploy-result.json.orphanedResourceGroups[]` immediately when switching to a new RG.
|
||||
@@ -0,0 +1,150 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Deploy artifact schema — deploy-result.json.
|
||||
* Read by deploy instructions.md Step 8 (finalize artifacts).
|
||||
*/
|
||||
|
||||
export type HealthStatus = "healthy" | "degraded" | "unreachable" | "unknown";
|
||||
|
||||
// ─── Deploy healing types ────────────────────────────────────────────────────
|
||||
|
||||
export type DeployErrorClassification = "IAC_ERROR" | "INFRA_TRANSIENT" | "ENVIRONMENT_BLOCKING";
|
||||
export type DeployHealingPhase = "validation" | "deployment";
|
||||
|
||||
/** Tracks a resource group created during a healing attempt that is no longer
|
||||
* the final deployment target. Surfaced at handoff for manual cleanup. */
|
||||
export interface OrphanResourceGroup {
|
||||
/** Azure resource group name */
|
||||
name: string;
|
||||
/** Region where the RG was created. Omitted when derived by the deterministic
|
||||
* `capture_deployment_inventory` diff, which doesn't track a region. */
|
||||
region?: string;
|
||||
/** Which healing attempt created or targeted this RG. Omitted when derived by the
|
||||
* deterministic `capture_deployment_inventory` diff rather than a tracked healing attempt. */
|
||||
healingAttempt?: number;
|
||||
/** Why this RG was abandoned (e.g., "region fallback to westus2") */
|
||||
reason: string;
|
||||
}
|
||||
export type DeployHealingAction = "routed-to-scaffold" | "retried" | "surfaced-to-user";
|
||||
export type DeployHealingResult = "fixed" | "still-failing" | "blocked";
|
||||
|
||||
// ─── Deterministic resource inventory (capture_deployment_inventory) ─────────
|
||||
|
||||
/** How a resource created during this session relates to the tracked deployment(s).
|
||||
* Computed deterministically by the `capture_deployment_inventory` MCP tool from a
|
||||
* before/after `resources.list()` diff — NOT inferred from chat history. */
|
||||
export type CreatedResourceClassification =
|
||||
/** Reported `Succeeded` by a tracked deployment and located in the final target RG.
|
||||
* Part of the working deployment — never a cleanup candidate. */
|
||||
| "expected"
|
||||
/** Reported by a tracked deployment but with a non-succeeded provisioning state.
|
||||
* Confirmed to belong to this deployment, so a delete command may be offered. */
|
||||
| "failed"
|
||||
/** Appeared during the deploy window but no tracked deployment reported it, or it landed
|
||||
* outside the final target RG. Probably a healing retry or an imperative `az` fallback —
|
||||
* but on a shared subscription it may belong to someone else entirely. Surface for the
|
||||
* user to REVIEW; never describe it as safe to delete. */
|
||||
| "orphaned"
|
||||
/** The tracked deployment's ARM operations could not be read (permissions, throttling,
|
||||
* transient failure), so nothing could be attributed. Never a cleanup candidate. */
|
||||
| "unverified";
|
||||
|
||||
/** A resource that exists now because of this session (post − baseline diff). */
|
||||
export interface CreatedResource {
|
||||
/** Full ARM resource ID. */
|
||||
id: string;
|
||||
name?: string;
|
||||
type?: string;
|
||||
/** Resource group parsed from the ARM ID. */
|
||||
resourceGroup?: string;
|
||||
/** Provisioning state reported by the deployment operations, when known. */
|
||||
provisioningState?: string;
|
||||
classification: CreatedResourceClassification;
|
||||
}
|
||||
|
||||
export interface DeployHealingError {
|
||||
source: string;
|
||||
detail: string;
|
||||
classification: DeployErrorClassification;
|
||||
}
|
||||
|
||||
export interface DeployHealingAttempt {
|
||||
attempt: number;
|
||||
phase: DeployHealingPhase;
|
||||
errors: DeployHealingError[];
|
||||
action: DeployHealingAction;
|
||||
result: DeployHealingResult;
|
||||
/** Resource group targeted by this attempt — useful for audit and debugging */
|
||||
resourceGroupName?: string;
|
||||
/** True when this healing attempt changed the service type or region — requires re-approval */
|
||||
planLevelChange?: boolean;
|
||||
/** What changed: "service-type", "region", "sku" */
|
||||
changeType?: "service-type" | "region" | "sku";
|
||||
/** Original value before the change (e.g., "App Service B1 eastus") */
|
||||
originalValue?: string;
|
||||
/** New value after the change (e.g., "Container Apps Consumption eastus") */
|
||||
newValue?: string;
|
||||
}
|
||||
|
||||
// ─── deploy-result.json ──────────────────────────────────────────────────────
|
||||
|
||||
export interface DeployEndpoint {
|
||||
name: string;
|
||||
url: string;
|
||||
healthStatus: HealthStatus;
|
||||
}
|
||||
|
||||
export interface DeployDuration {
|
||||
startedUtc: string;
|
||||
completedUtc: string;
|
||||
}
|
||||
|
||||
export type DeployStatus = "in-progress" | "succeeded" | "failed";
|
||||
|
||||
export type ResourceDeployStatus = "succeeded" | "failed" | "skipped";
|
||||
|
||||
export interface ResourceResult {
|
||||
resourceId: string;
|
||||
type: string;
|
||||
status: ResourceDeployStatus;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface DeployResult {
|
||||
sessionId: string;
|
||||
/** Azure subscription ID from context.json.azure.subscriptionId */
|
||||
subscriptionId: string;
|
||||
/** All ARM deployment names used during this session (initial + healing retries).
|
||||
* First entry is the initial deployment; subsequent entries are from scope/RG changes. */
|
||||
deploymentNames: string[];
|
||||
resourceGroupName: string;
|
||||
status: DeployStatus;
|
||||
resourceIds: string[];
|
||||
endpoints: DeployEndpoint[];
|
||||
healthStatus: HealthStatus;
|
||||
duration: DeployDuration;
|
||||
warnings: string[];
|
||||
partial: boolean;
|
||||
resourceResults: readonly ResourceResult[];
|
||||
/** Resources that exist now because of this session, computed deterministically by
|
||||
* `capture_deployment_inventory` (before/after `resources.list()` diff). Includes
|
||||
* expected, failed, and orphaned resources — the source of truth for the handoff
|
||||
* cleanup section. Populated at Step 8 and on the Step 9 failure path. */
|
||||
createdResources: readonly CreatedResource[];
|
||||
/** RGs created during healing that are not the final deployment target.
|
||||
* Derived from `createdResources` (resources whose RG != the final target RG).
|
||||
* Surfaced at handoff (Step 9) with manual cleanup commands. */
|
||||
orphanedResourceGroups: readonly OrphanResourceGroup[];
|
||||
/** Set when `capture_deployment_inventory` could not read the deployment's ARM operations,
|
||||
* so `createdResources` could not be attributed. When true, present NO cleanup list and
|
||||
* tell the user to review the resource group in the portal. */
|
||||
inventoryUnverified?: boolean;
|
||||
/** Why verification failed: `"forbidden"` (missing
|
||||
* `Microsoft.Resources/deployments/operations/read`), `"throttled"`, or `"error"`. */
|
||||
inventoryUnverifiedReason?: string;
|
||||
healingAttempts?: readonly DeployHealingAttempt[];
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Error Classification
|
||||
|
||||
Three error categories for deploy-time failures.
|
||||
|
||||
## Multi-Error Triage
|
||||
|
||||
> ⛔ When 3+ errors, classify ALL before fixing ANY. Group by root cause. Present: "Deploy failed with {N} errors from {M} root causes."
|
||||
|
||||
## Categories
|
||||
|
||||
### `IAC_ERROR` — Route Back to Scaffold
|
||||
|
||||
| Example | Fix |
|
||||
|---------|-----|
|
||||
| Invalid property name | Call `mcp_bicep_build_bicep` with `{ filePath: "infra/main.bicep" }` for structured error details, then fix. Fallback: `az bicep build` |
|
||||
| Wrong SKU for region | Substitute from `rejectedAlternatives[]` |
|
||||
| Missing required field | Call `mcp_bicep_build_bicep` for structured error, fix from diagnostics. Fallback: `az bicep build` |
|
||||
| Policy violation | Substitute per policy |
|
||||
| API version not found | Call `mcp_bicep_list_az_resource_types_for_provider` with `{ providerNamespace: "..." }`. Use latest GA — no `-preview`. Fallback: `az provider show` |
|
||||
| `listKeys()` in output | ⛔ Security risk — replace with managed-identity access (no shared keys, no Key Vault) |
|
||||
| Redis `InvalidRequestBody` for `properties.sku.name` | Bicep type issue — create via `az redis create --sku Basic --vm-size c0`, switch Bicep to `existing` keyword |
|
||||
|
||||
**Flow:** Deploy → classifies IAC_ERROR → scaffold self-healing → re-validate → retry.
|
||||
|
||||
### `INFRA_TRANSIENT` — Retry with Backoff
|
||||
|
||||
| Example | Strategy |
|
||||
|---------|----------|
|
||||
| ARM 429, 409 conflict, RBAC delay, network timeout | 30s → 60s → 120s (3 max) |
|
||||
| Container Apps `Operation expired` | Check root cause first: image pull failure, port mismatch, health probe timeout, crash loop. Port mismatch → IAC_ERROR. Image pull → retry. Crash → ENVIRONMENT_BLOCKING. |
|
||||
|
||||
After 3 failures → escalate to user.
|
||||
|
||||
### `ENVIRONMENT_BLOCKING` — Surface to User
|
||||
|
||||
| Example | Action |
|
||||
|---------|--------|
|
||||
| 403 on sub-scope deploy | ⛔ Try [403 Scope Fallback](deploy-safety.md) first (RG-scope). Only block if retry also fails |
|
||||
| 403 on RG-scope | Surface: `az role assignment create --role Contributor --assignee {objectId} --scope {rg}` |
|
||||
| AuthorizationFailed / deny assignment | Get user objectId, suggest role assignment command. Deny assignments → contact admin |
|
||||
| Quota exhausted | ⛔ PLAN_LEVEL_CHANGE — HALT, present region fallback with re-approval |
|
||||
| Region doesn't support resource | ⛔ PLAN_LEVEL_CHANGE — same as quota |
|
||||
| `LocationIsOfferRestricted` | ⛔ PLAN_LEVEL_CHANGE — read `quotaValidation.offerRestrictions[]` for unblocked regions |
|
||||
| MI sidecar OOM on F1/B1 | Upgrade SKU, remove MI, or switch to Container Apps |
|
||||
| AADSTS530084 / TF auth failure | Re-scaffold as Bicep + `az deployment group create`. Never fall back to imperative CLI |
|
||||
|
||||
> ⛔ **During ALL healing: NEVER run `az group delete`, `az postgres flexible-server delete`, `az redis delete`, `az webapp delete`, or any destructive resource deletion.** Track failed RGs in `deploy-result.json.orphanedResourceGroups[]` instead. The user deletes at handoff. If you need a clean region retry, use a NEW RG name (append `-2`) — do NOT delete-and-recreate. ⛔ **After each failed/healing `az deployment`, call `capture_deployment_inventory` (`phase: "capture"`, passing every `resourceGroups[]` including the abandoned one)** so orphaned resources from the healing retry are recorded deterministically rather than left for you to remember.
|
||||
|
||||
## Healing Trace
|
||||
|
||||
Log to `deploy-result.json.healingAttempts[]`: `{ attempt, phase, errors: [{ source, detail, classification }], action, result, planLevelChange, changeType, originalValue, newValue }`.
|
||||
|
||||
> ⛔ **Repeat failure (same error 2+):** Read [iac-resources.md](../../references/iac-resources.md) § Deploy Troubleshooting and `fetch_webpage` the matching URL.
|
||||
|
||||
After 3 failed cycles: write `partial: true`, surface remaining errors. Do NOT auto-rollback.
|
||||
|
||||
## Known Platform Bugs / Deploy Timing
|
||||
|
||||
See [`pipeline-rules-runtime.md`](../../references/pipeline-rules-runtime.md).
|
||||
@@ -0,0 +1,98 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Health Check Patterns
|
||||
|
||||
Post-deployment health verification for AppOnboard-deployed resources.
|
||||
|
||||
## HTTP Endpoints
|
||||
|
||||
For each endpoint: HTTPS GET, 30s timeout, 3 retries (10s/20s/40s backoff).
|
||||
|
||||
| Status | Health | Note |
|
||||
|--------|--------|------|
|
||||
| 2xx | `healthy` | **Verify not a placeholder page (see below)** |
|
||||
| 401/403 | `healthy` | Auth working, app running |
|
||||
| 5xx ×3 | `degraded` | |
|
||||
| Timeout/DNS ×3 | `unreachable` | |
|
||||
|
||||
> ⛔ **DB-backed apps: a 200 on `/` is NOT healthy.** When `prepare-plan.json.services[]` includes a database, probing only `/` (or any non-DB route) just proves the web server booted. Probe at least one **data-backed route** (derive from the app's detected routes, e.g. a REST resource path) and inspect the body for DB errors (`insecure transport`, `Access denied`, `connection refused`, `Unknown database`, `doesn't exist`) → mark `degraded`, not `healthy`.
|
||||
|
||||
### HTTP Redirect Handling (Container Apps)
|
||||
|
||||
> ⛔ **ACA health probes do NOT follow HTTP redirects.** A 301/302 response from the probe path causes `ActivationFailed` — the probe treats it as a failure, not a redirect.
|
||||
|
||||
If the first health check returns **301 or 302**:
|
||||
1. Read the `Location` header: `curl -sI "https://{fqdn}{probePath}" | Select-String "^location:" -CaseSensitive:$false`
|
||||
2. Update `probePath` in Bicep to the redirect target (e.g., `/wetty/` → `/wetty`)
|
||||
3. Redeploy: `az deployment sub create` with updated Bicep
|
||||
4. Re-check health after new revision activates
|
||||
|
||||
Common redirect patterns: Express trailing-slash normalization (`/app/` → `/app`), framework-level path canonicalization, HTTPS redirects on mixed-content paths.
|
||||
|
||||
### App Service Default Page Detection
|
||||
|
||||
> ⛔ **HTTP 200 ≠ app started.** Azure serves its own default page with 200 when the app fails to start — false positive.
|
||||
|
||||
After HTTP 200 from App Service, check first 2KB of body:
|
||||
|
||||
| Body contains | Meaning |
|
||||
|---------------|--------|
|
||||
| `"Your app service is up and running"` | Default page — app didn't start |
|
||||
| `"Time to take the next step and deploy your code"` | Default page — no code or app failed |
|
||||
| `"Hey, Python developers!"` / `"Hey, Node.js developers!"` | Runtime default — app didn't start |
|
||||
| `"Error 503"` / `"Application Error"` | App crashed on startup |
|
||||
|
||||
If detected → `healthStatus: "degraded"` + warning: `"App Service default page detected — check logs: az webapp log tail -g {rg} -n {app}"`.
|
||||
|
||||
## Non-HTTP Resources
|
||||
|
||||
```bash
|
||||
az resource show --ids {resourceId} --query "properties.provisioningState" -o tsv
|
||||
```
|
||||
|
||||
`Succeeded` → `healthy`. `Failed` → `degraded`. Other → `unknown`.
|
||||
|
||||
| Service | Health Signal |
|
||||
|---------|---------------|
|
||||
| Container Apps | `latestReadyRevisionName` not empty + HTTP on ingress FQDN |
|
||||
| App Service | HTTP GET `https://{name}.azurewebsites.net/` + `/health` |
|
||||
| Static Web Apps | HTTP GET `https://{defaultHostname}/` → 2xx = `healthy` (hostname: `az staticwebapp show -n {swa} -g {rg} --query defaultHostname -o tsv`) |
|
||||
| Azure SQL | `provisioningState` + `az sql db show` |
|
||||
| Cosmos DB | `provisioningState` |
|
||||
| Storage | `provisioningState` + `statusOfPrimary` |
|
||||
| Functions | HTTP trigger URL + HTTP check |
|
||||
|
||||
> ⛔ **Container Apps — run an explicit live HTTP probe (the pipeline status is NOT sufficient).** After `latestReadyRevisionName` is set, run an observable request against the ingress FQDN and capture the result into `deploy-result.json.endpoints[].healthStatus`:
|
||||
> ```powershell
|
||||
> iwr "https://{ingressFqdn}/{probePath}" -UseBasicParsing # PowerShell
|
||||
> curl -sSfL "https://{ingressFqdn}/{probePath}" # bash
|
||||
> ```
|
||||
> This live HTTP call against `*.azurecontainerapps.io` IS the health verification — do NOT infer health from the revision's internal status alone.
|
||||
|
||||
## Output
|
||||
|
||||
Write to `deploy-result.json.endpoints[]`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "api",
|
||||
"url": "https://myapp-ca-dev-a1b2.azurecontainerapps.io",
|
||||
"healthStatus": "healthy" // healthy | degraded | unreachable | unknown
|
||||
}
|
||||
```
|
||||
|
||||
Overall `healthStatus` = worst status across all endpoints. If any `unreachable` → overall `unreachable`.
|
||||
|
||||
## Functional Endpoint Verification
|
||||
|
||||
> ⛔ **A 200 on `/` only proves the web server booted — not that the app works.** After the HTTP check, confirm the app actually functions against the services the plan provisioned (database, cache), not just that it responds.
|
||||
|
||||
Health checks only confirm the web server is responding. Exercise a route that depends on the backing services — for example:
|
||||
|
||||
| Pattern | Functional Check |
|
||||
|---------|-----------------|
|
||||
| Database in the plan (MySQL/PostgreSQL/SQL/Cosmos) | Probe a route that reads/writes the DB (a detected app route, NOT `/` — root often serves a static page with no DB access, so 200 on `/` masks broken DB connectivity). A 5xx or a DB error in the body (`connection refused`, `does not exist`, token/auth failure) → `degraded` — usually the app MI wasn't granted a DB role (see [database-post-deploy.md](database-post-deploy.md) § 2). |
|
||||
| `FIRST_SUPERUSER` env var or `prestart.sh`/`init_db()` | After health passes, attempt login endpoint. If 401/500 → startup scripts may have failed. Trigger `az containerapp revision restart` to re-run startup. |
|
||||
| Migration frameworks (Alembic, Django, Prisma, EF) | After health passes, check `prereq-output.json` for migration signals. If found, run migrations per [database-post-deploy.md](database-post-deploy.md). |
|
||||
| Two-phase Container Apps (managed identity) | Wait 60s after Phase 2 for AcrPull RBAC propagation. If the DB fails with auth/token errors, the app MI may not yet have its DB role — grant it (see [database-post-deploy.md](database-post-deploy.md)) and create a new revision. |
|
||||
@@ -0,0 +1,104 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Deploy Phase — MCP Tools
|
||||
|
||||
Phase-exclusive tool parameters for the deploy phase. For shared tools (`subscription_list`, `group_list`, `extension_cli_install`, `get_azure_bestpractices`), see [mcp-tool-reference.md](../../references/mcp-tool-reference.md).
|
||||
|
||||
> **Troubleshooting:** If a tool call fails with unknown parameter or missing command errors, consult the official docs: <https://learn.microsoft.com/en-us/azure/developer/azure-mcp-server/tools/>
|
||||
|
||||
---
|
||||
|
||||
## `capture_deployment_inventory` (Copilot on Rails extension tool)
|
||||
|
||||
Deterministic resource-inventory capture provided by the Azure Resources extension (in-proc MCP, `copilot-azure-resources-extension-tools/*`). Snapshots the subscription's resources via ARM and diffs before/after to record exactly what this session created. Holds the baseline in memory (no workspace files). **Report-only — never deletes.**
|
||||
|
||||
| Param | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `sessionId` | ✅ | App Onboard session id; locates `.copilot-azure/sessions/{id}/`. |
|
||||
| `subscriptionId` | ✅ | Target subscription. |
|
||||
| `phase` | ✅ | `"baseline"` (before first deployment) or `"capture"` (after each attempt / on failure / at finalize). |
|
||||
| `expectedResourceGroup` | — | Final target RG; created resources outside it are flagged `orphaned`. |
|
||||
| `deploymentNames` | — | All ARM deployment names (initial + healing) used to classify created resources. |
|
||||
| `resourceGroups` | — | RGs touched (incl. abandoned healing RGs) so RG-scoped deployment operations are read. |
|
||||
|
||||
Holds the baseline snapshot in memory (no files written). On `phase: "capture"` returns the created resources classified `expected`/`failed`/`orphaned`/`unverified` plus orphaned resource groups; write those into `deploy-result.json.createdResources[]`/`orphanedResourceGroups[]`.
|
||||
|
||||
⛔ **Classification confidence differs, and your wording to the user MUST reflect it:**
|
||||
|
||||
| Classification | Meaning | How to present it |
|
||||
|---|---|---|
|
||||
| `expected` | Tracked deployment reported it as `Succeeded` in the target RG. | Part of the working deployment. Never suggest deleting it. |
|
||||
| `failed` | Tracked deployment reported it with a non-succeeded state. | Confirmed to belong to this deployment. Safe to offer a delete command. |
|
||||
| `orphaned` | Appeared during the deploy window but **no tracked deployment reported it**. | It *may* be a healing/imperative stray — or another person's resource on a shared subscription. Present as "review before deleting", never as "safe to delete" or "created by this deployment". |
|
||||
| `unverified` | Deployment operations could not be read at all, so nothing could be attributed. | Say the inventory could not be verified and point the user at the portal. Do **not** produce any cleanup list or delete command. |
|
||||
|
||||
When the tool returns `inventoryUnverified: true`, record it in `deploy-result.json` as `inventoryUnverified` + `inventoryUnverifiedReason` and skip the cleanup section entirely.
|
||||
|
||||
See the [Phase 4 Tool Map](#phase-4-tool-map) below and [`../instructions.md`](../instructions.md) Steps 5b/6/8/9.
|
||||
|
||||
---
|
||||
|
||||
## `mcp_azure_mcp_deploy` (hierarchical — deploy-phase sub-commands)
|
||||
|
||||
| Sub-command | Required Params | Optional Params | Read-Only |
|
||||
|-------------|----------------|-----------------|-----------|
|
||||
| `deploy_app_logs_get` | `workspace-folder`, `azd-env-name` | `limit` (default 200) | ✅ |
|
||||
| `deploy_architecture_diagram_generate` | `workspaceFolder`, `services[]` (complex array with name, path, language, port, azureComputeHost, dependencies, settings) | `projectName` | ✅ |
|
||||
|
||||
## `mcp_azure_mcp_resourcehealth` (hierarchical)
|
||||
|
||||
| Sub-command | Required Params | Optional Params | Read-Only |
|
||||
|-------------|----------------|-----------------|-----------|
|
||||
| `resourcehealth_availability-status_get` | *(none)* | `subscription`, `resourceId` (full ARM ID), `resource-group` | ✅ |
|
||||
| `resourcehealth_health-events_list` | *(none)* | `subscription`, `event-type` (ServiceIssue\|PlannedMaintenance\|HealthAdvisory\|Security), `status` (Active\|Resolved), `tracking-id`, `filter`, `query-start-time`, `query-end-time` | ✅ |
|
||||
|
||||
## `mcp_azure_mcp_monitor` (hierarchical)
|
||||
|
||||
| Sub-command | Required Params | Optional Params | Read-Only |
|
||||
|-------------|----------------|-----------------|-----------|
|
||||
| `monitor_activitylog_list` | `resource-name` | `resource-group`, `resource-type`, `hours`, `event-level` (Critical\|Error\|Informational\|Verbose\|Warning), `top`, `subscription` | ✅ |
|
||||
| `monitor_metrics_query` | `resource`, `metric-names` (comma-sep), `metric-namespace` | `resource-group`, `resource-type`, `start-time`, `end-time`, `interval`, `aggregation` (Average\|Maximum\|Minimum\|Total\|Count), `filter`, `max-buckets` | ✅ |
|
||||
| `monitor_metrics_definitions` | `resource` | `resource-group`, `resource-type`, `metric-namespace`, `search-string`, `limit` | ✅ |
|
||||
| `monitor_workspace_list` | *(none)* | `subscription` | ✅ |
|
||||
| `monitor_workspace_log_query` | `resource-group`, `workspace`, `table`, `query` (KQL or "recent"\|"errors") | `hours`, `limit`, `subscription` | ✅ |
|
||||
| `monitor_resource_log_query` | `resource-id` (full ARM ID), `table`, `query` | `hours`, `limit`, `subscription` | ✅ |
|
||||
|
||||
## `mcp_azure_mcp_appservice` (hierarchical)
|
||||
|
||||
| Sub-command | Required Params | Optional Params | Read-Only |
|
||||
|-------------|----------------|-----------------|-----------|
|
||||
| `appservice_webapp_get` | *(none)* | `app`, `resource-group`, `subscription` | ✅ |
|
||||
| `appservice_webapp_deployment_get` | `resource-group`, `app` | `deployment-id`, `subscription` | ✅ |
|
||||
| `appservice_webapp_diagnostic_diagnose` | `resource-group`, `app`, `detector-name` (Availability\|CpuAnalysis\|MemoryAnalysis) | `start-time`, `end-time`, `interval`, `subscription` | ✅ |
|
||||
| `appservice_webapp_diagnostic_list` | `resource-group`, `app` | `subscription` | ✅ |
|
||||
| `appservice_webapp_settings_get-appsettings` | `resource-group`, `app` | `subscription` | ✅ (⚠️ secret) |
|
||||
| `appservice_database_add` | `resource-group`, `app`, `database-type` (SqlServer\|MySQL\|PostgreSQL\|CosmosDB), `database-server`, `database` | `connection-string`, `subscription` | ❌ |
|
||||
| `appservice_webapp_settings_update-appsettings` | `resource-group`, `app`, `setting-name`, `setting-update-type` (add\|set\|delete) | `setting-value`, `subscription` | ❌ (destructive) |
|
||||
|
||||
## `mcp_azure_mcp_role` (hierarchical)
|
||||
|
||||
| Sub-command | Required Params | Optional Params | Read-Only |
|
||||
|-------------|----------------|-----------------|-----------|
|
||||
| `role_assignment_list` | `scope` (ARM scope path) | `subscription` | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 Tool Map
|
||||
|
||||
| Tool | Sub-command | AppOnboard Step | Purpose |
|
||||
|------|-----------|----------|---------|
|
||||
| `capture_deployment_inventory` | *(baseline)* | Step 5b | Snapshot pre-existing resources before the first deployment |
|
||||
| `capture_deployment_inventory` | *(capture)* | Steps 6, 8, 9 | Diff resources.list() to record created/orphaned/failed resources; build cleanup commands from `failed` entries only |
|
||||
| `mcp_azure_mcp_resourcehealth` | `resourcehealth_availability-status_get` | Step 7 | Post-deploy health. Call with `resourceId` from `deploy-result.json.resourceIds[]` |
|
||||
| `mcp_azure_mcp_resourcehealth` | `resourcehealth_health-events_list` | Step 3 | Pre-deploy outage check. `event-type: "ServiceIssue"`, `status: "Active"` |
|
||||
| `mcp_azure_mcp_monitor` | `monitor_activitylog_list` | Step 7+ | Failed deployment analysis. `resource-name` from deploy output, `event-level: "Error"`, `hours: 1` |
|
||||
| `mcp_azure_mcp_monitor` | `monitor_metrics_query` | Step 7 | Performance validation. Requires `metric-namespace` from `monitor_metrics_definitions` first |
|
||||
| `mcp_azure_mcp_appservice` | `appservice_webapp_get` | Step 7 | Verify App Service state, hostnames, runtime |
|
||||
| `mcp_azure_mcp_appservice` | `appservice_webapp_deployment_get` | Step 7 | Verify deployment completed successfully |
|
||||
| `mcp_azure_mcp_appservice` | `appservice_webapp_diagnostic_diagnose` | Step 7 | Run `Availability` detector on deployed app |
|
||||
| `mcp_azure_mcp_deploy` | `deploy_app_logs_get` | Step 7 | App logs for azd-deployed apps only. Requires `workspace-folder` + `azd-env-name` |
|
||||
| `mcp_azure_mcp_role` | `role_assignment_list` | Step 3 | Preflight RBAC check. `scope: "/subscriptions/{sub}/resourceGroups/{rg}"` |
|
||||
| `mcp_azure_mcp_subscription_list` | *(flat)* | Step 1 | Resolve target subscription |
|
||||
| `mcp_azure_mcp_group_list` | *(flat)* | Step 3 | Verify RG exists before deploy |
|
||||
| `mcp_azure_mcp_extension_cli_install` | *(flat)* | Step 3 | Ensure az/azd/func CLI installed |
|
||||
@@ -0,0 +1,54 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Portal Monitoring Links
|
||||
|
||||
Generate the portal link BEFORE deploying — the deployment name is deterministic, so the link works before the deployment even starts. The user wants to watch resources being created in real time.
|
||||
|
||||
## Generate via PowerShell — NEVER construct manually
|
||||
|
||||
The `%2F` encoding is critical and models consistently decode it back to `/` during text generation, producing broken links. Always use `.Replace('/', '%2F')`.
|
||||
|
||||
```powershell
|
||||
# Subscription-scope deployment
|
||||
$deploymentName = "app-onboard-deploy-$("{sessionId}".Substring(0,8))"
|
||||
$resId = "/subscriptions/{subscriptionId}/providers/Microsoft.Resources/deployments/$deploymentName"
|
||||
$link = "https://portal.azure.com/#view/HubsExtension/DeploymentDetailsBlade/~/overview/id/$($resId.Replace('/', '%2F'))"
|
||||
Write-Output "LINK=$link"
|
||||
```
|
||||
|
||||
Read `LINK=` from terminal output and print the URL in chat on its own bare line — no backticks, no markdown. Then deploy:
|
||||
```powershell
|
||||
az deployment sub create --name $deploymentName --subscription {subscriptionId} --location {location} --template-file infra/main.bicep --parameters @infra/main.parameters.json
|
||||
```
|
||||
|
||||
### RG-scope (403 fallback)
|
||||
|
||||
Use `az deployment group create --name $deploymentName --resource-group {rg}` and adjust `$resId` to include `/resourceGroups/{rg}`:
|
||||
```powershell
|
||||
$resId = "/subscriptions/{subscriptionId}/resourceGroups/{rg}/providers/Microsoft.Resources/deployments/$deploymentName"
|
||||
$link = "https://portal.azure.com/#view/HubsExtension/DeploymentDetailsBlade/~/overview/id/$($resId.Replace('/', '%2F'))"
|
||||
Write-Output "LINK=$link"
|
||||
```
|
||||
|
||||
For Terraform (no single ARM deployment): `https://portal.azure.com/#@{tenantId}/resource/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/activitylog`
|
||||
|
||||
> 💡 Resolve `{subscriptionId}` and `{resourceGroup}` from `context.json`. For Terraform, resolve `{tenantId}` via `az account show --query tenantId -o tsv`.
|
||||
|
||||
## Same-Scope Retries vs New Names
|
||||
|
||||
The portal link stays valid for same-scope retries — ARM overwrites in-place. Generate a new name (e.g., `$deploymentName = "app-onboard-deploy-{first8}-2"`) **only** when scope or RG changes.
|
||||
|
||||
## Chat Output Rules
|
||||
|
||||
1. **Terminal command:** the PowerShell snippet above (outputs the bare URL via `Write-Output "LINK=..."`). ⛔ **Do NOT auto-open the link in a browser** — never call `Start-Process` (or any browser-launching command) on the portal URL. Just print it so the user can open it if they choose.
|
||||
2. **Read the terminal output** — find the line starting with `LINK=` and extract the URL
|
||||
3. **Chat output:** paste the bare URL on its own line — no backticks, no markdown, no emoji on the same line
|
||||
|
||||
⛔ **Link must be ctrl+clickable.** The URL MUST be the ONLY content on its line — no emoji, no text, no backticks, no markdown formatting, no markdown link syntax on the same line. Terminals auto-linkify bare URLs but ONLY when the URL is alone on the line.
|
||||
|
||||
⛔ **Emit a NEW link whenever deployment name changes.** When healing causes a redeploy with a different `--name`, you MUST re-run the PowerShell snippet with the new name and print the new link:
|
||||
```
|
||||
⚠️ Previous deployment link is stale — use this one:
|
||||
https://portal.azure.com/.../{newDeploymentName}
|
||||
```
|
||||
@@ -0,0 +1,129 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Preflight Checks
|
||||
|
||||
Pre-deployment validation steps. Run after user approval, before deployment execution.
|
||||
|
||||
> AppOnboard runs direct deployment (no `azd`).
|
||||
|
||||
## Check Sequence
|
||||
|
||||
Branch on `scaffold-manifest.json.iacFormat`:
|
||||
|
||||
### 0. Auth Token Verification
|
||||
|
||||
```bash
|
||||
az account show
|
||||
```
|
||||
|
||||
- Success → proceed. Active subscription + tenant confirmed.
|
||||
- Failure → `ENVIRONMENT_BLOCKING`. Suggest `az login` (plain, no scope).
|
||||
- ⛔ NEVER suggest `az login --scope https://graph.microsoft.com/.default` — Graph scope is irrelevant for ARM deployments.
|
||||
|
||||
### 0b. Resource Name Availability
|
||||
|
||||
Check globally-unique names before deploy: `az acr check-name`, `az storage account check-name`, `az webapp show`, `az keyvault show`. Name taken → suggest alternate from `prepare-plan.json.naming.suffix`: "Name `{name}` taken. Use `{altName}`?"
|
||||
|
||||
### 0c. F1/Free Tier Warning
|
||||
|
||||
If plan includes F1/D1/free SKUs, surface at deploy gate (do NOT block):
|
||||
> ⚠️ Free tier: no custom domains, no SSL, no always-on, 60 min/day compute (F1). Dev/test only.
|
||||
|
||||
### 0d. RBAC Scope Pre-Check
|
||||
|
||||
```bash
|
||||
az role assignment list --assignee {userId} --scope /subscriptions/{sub} --query "[].roleDefinitionName" -o tsv
|
||||
```
|
||||
|
||||
Subscription-scope deploy requires `Contributor`/`Owner` on subscription. Missing → `ENVIRONMENT_BLOCKING` with `az role assignment create` command.
|
||||
|
||||
### 1. Deployment Preview
|
||||
|
||||
⛔ **MANDATORY — do NOT skip.** What-if validates + previews in one call. Use `what-if` exclusively — `az deployment sub/group validate` hits a known CLI bug (HTTP stream consumed error). If what-if fails, log + warn user — do not skip to execution.
|
||||
|
||||
#### Bicep (subscription scope)
|
||||
|
||||
```bash
|
||||
az deployment sub what-if \
|
||||
--name "{deploymentName}" \
|
||||
--location {location} \
|
||||
--template-file infra/main.bicep \
|
||||
--parameters @infra/main.parameters.json \
|
||||
--subscription {subscriptionId} \
|
||||
--what-if-result-format FullResourcePayloads
|
||||
```
|
||||
|
||||
#### Bicep (resource-group scope)
|
||||
|
||||
```bash
|
||||
az deployment group create \
|
||||
--resource-group {rg} \
|
||||
--template-file infra/main.bicep \
|
||||
--parameters @infra/main.parameters.json \
|
||||
--subscription {subscriptionId} \
|
||||
--what-if \
|
||||
--what-if-result-format FullResourcePayloads
|
||||
```
|
||||
|
||||
- Review changes: `Create`, `Modify`, `Delete`, `NoChange`. Surface `Delete` as warnings — user must acknowledge.
|
||||
- Auth error → `ENVIRONMENT_BLOCKING`.
|
||||
|
||||
#### Terraform
|
||||
|
||||
```bash
|
||||
terraform plan -out=tfplan -detailed-exitcode
|
||||
```
|
||||
|
||||
- Exit 0 → no changes. Exit 2 → changes (normal). Exit 1 → error.
|
||||
- Surface `destroy` as warnings — user must acknowledge. Auth error → `ENVIRONMENT_BLOCKING`.
|
||||
|
||||
### 3. RBAC Permission Check
|
||||
|
||||
```bash
|
||||
az role assignment list \
|
||||
--assignee {currentUserObjectId} \
|
||||
--scope /subscriptions/{sub}/resourceGroups/{rg} \
|
||||
--query "[].roleDefinitionName" -o tsv
|
||||
```
|
||||
|
||||
Required: `Contributor` or `Owner` on the target resource group. If missing → `ENVIRONMENT_BLOCKING` with remediation command.
|
||||
|
||||
### 4. SKU Quota Verification
|
||||
|
||||
⛔ **`what-if` does NOT catch quota errors.** It returns `Succeeded` even when target SKU has limit=0.
|
||||
|
||||
If `prepare-plan.json.quotaValidation.verified == true` → proceed.
|
||||
|
||||
Otherwise → **read [sku-quota-validation.md](../../prepare/references/sku-quota-validation.md)** and run direct quota checks NOW (per-provider API patterns, offer restrictions). If limit=0 → HALT, present region fallback. Skip regions in `quotaValidation.checkedRegions` with zero availability.
|
||||
|
||||
⛔ `SubscriptionIsOverQuotaForSku` or `LocationIsOfferRestricted` in deploy output → HALT. See [error-classification.md](error-classification.md).
|
||||
|
||||
### 5. Resource Group Existence
|
||||
|
||||
```bash
|
||||
az group show --name {rg} --query "location" -o tsv 2>/dev/null
|
||||
```
|
||||
|
||||
- Exists → verify location matches `prepare-plan.json` region. Mismatch → warn.
|
||||
- Not exists → will be created by deployment (if `main.bicep` has subscription scope).
|
||||
|
||||
## Error Handling
|
||||
|
||||
Each check runs independently. Collect all results, then present structured report.
|
||||
|
||||
| Check | Fail Behavior |
|
||||
|-------|---------------|
|
||||
| Deployment preview | Warn, don't block (can fail on unsupported types) |
|
||||
| RBAC | Block. Surface `az role assignment create`. |
|
||||
| RG check | Warn on location mismatch. Don't block. |
|
||||
|
||||
## Report Format
|
||||
|
||||
```
|
||||
## Preflight Results
|
||||
✅ IaC syntax: valid (terraform validate / bicep build)
|
||||
⚠️ Deployment preview: 3 creates, 0 destroys, 1 update
|
||||
✅ RBAC: Contributor role confirmed
|
||||
✅ Resource group: rg-myapp-dev (eastus2)
|
||||
```
|
||||
@@ -0,0 +1,81 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Subagent Template — Deploy Preflight & Checklist Generation
|
||||
|
||||
Read deploy reference files and distill deployment-specific rules into `deploy-checklist.md`. This is the main agent's ONLY source of deploy rules.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- ⛔ Do NOT invoke any agents, run `az` commands, or modify IaC/app code. Read-only sub-agent.
|
||||
- ⛔ Do NOT read `deploy-schemas.ts` or `error-classification.md` — main agent reads those on-demand.
|
||||
|
||||
## Input (provided by caller)
|
||||
|
||||
| Field | Source |
|
||||
|-------|--------|
|
||||
| `prepare-plan.json` content | services[], naming, region, costEstimate, deploymentVariables |
|
||||
| `scaffold-manifest.json` content | deployCommand, validationResult, files[] |
|
||||
| `context.json` content | azure.subscriptionId, subscriptionName, resourceGroup, sessionId, intent |
|
||||
| `prereq-output.json` content | buildRequirements, warnings[], components[] |
|
||||
| Session folder path + working directory | Required |
|
||||
|
||||
## Output
|
||||
|
||||
| Artifact | Location |
|
||||
|----------|----------|
|
||||
| `deploy-checklist.md` | Session folder |
|
||||
| `deploy-result.json` skeleton (if missing) | Session folder |
|
||||
| Summary (≤300 tokens) | Return to caller |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1 — Read session artifacts + write skeleton
|
||||
|
||||
Read all 4 session artifacts. Extract: services[], naming, costEstimate, deployCommand, validationResult, deploymentVariables, subscriptionId, sessionId, buildRequirements, warnings[], quotaValidation.
|
||||
|
||||
If `deploy-result.json` missing, write skeleton with these EXACT field names (do NOT rename): `{ sessionId, subscriptionId, resourceGroupName, deploymentNames: ["app-onboard-deploy-{first 8 of sessionId}"], status: "in-progress", startedUtc, resourceIds: [], endpoints: [], healthStatus: "unknown", resourceResults: [], healingAttempts: [] }`.
|
||||
|
||||
### Step 2 — Read safety + blocked patterns refs
|
||||
|
||||
Read [deploy-safety.md](deploy-safety.md) and [blocked-patterns.md](blocked-patterns.md). Bake into checklist sections: deploy-result.json rules, blocked commands table, shell rules (sync shells, secrets persistence, no --track-status, az rest headers on Windows), 403 scope fallback (4-step procedure with real values), post-deploy tag verification, deployment operation polling (conditional: >5 resources), re-approval gates, antipatterns, artifact reconciliation.
|
||||
|
||||
### Step 3 — Read preflight + approval gate refs
|
||||
|
||||
Read [preflight-checks.md](preflight-checks.md) and [approval-gate-template.md](approval-gate-template.md).
|
||||
|
||||
Bake preflight into checklist: auth token check, resource name availability (real names), RBAC scope pre-check, ⛔ MANDATORY what-if command (pre-filled with real deploymentName, region, subscriptionId), RG existence check, offer restriction check (conditional: if `offerRestrictionsVerified` false AND DB services in plan).
|
||||
|
||||
Bake approval gate VERBATIM with real values: subscription, RG, region, service table, cost table, validation status, files list, response handlers with exact CLI commands. If F1/D1 detected, append warning.
|
||||
|
||||
### Step 4 — Read code-deployment + health refs
|
||||
|
||||
Read ONLY the code-deployment ref(s) matching the compute types in `prepare-plan.json.services[]`. **Do NOT read** references for compute types absent from the plan:
|
||||
- If `App Service` or `Functions` in plan → read [code-deployment-appservice.md](code-deployment-appservice.md)
|
||||
- If `Container Apps` in plan → read [code-deployment-container-apps.md](code-deployment-container-apps.md)
|
||||
- If `Static Web Apps` in plan → read [code-deployment-swa.md](code-deployment-swa.md)
|
||||
|
||||
Bake per-service-type `## Code deploy` sections (one per compute service — do NOT merge).
|
||||
|
||||
Read [health-check-patterns.md](health-check-patterns.md). Bake health check section: HTTP checks (timeout 30s, 3 retries), status interpretation, Azure default page detection strings, non-HTTP resource checks, functional verification (conditional).
|
||||
|
||||
### Step 5 — Read conditional refs + handoff + write checklist
|
||||
|
||||
**Database (conditional):** If DB services in plan → read [database-post-deploy.md](database-post-deploy.md). Bake migration discovery, execution commands, PG-specific checks.
|
||||
|
||||
Read [../../references/handoff-protocol.md](../../references/handoff-protocol.md). Bake cleanup commands (with real rg, sessionId), orphan listing, healing summary, post-deploy recommendations, agent-based next steps, auth-aware handoff.
|
||||
|
||||
**Write `deploy-checklist.md`** to session folder. If it already exists, replace all content via `edit`; if not, use `create`:
|
||||
```
|
||||
# Deploy Checklist for {appName}
|
||||
# RG: {rgName} | Sub: {subscriptionId} | Session: {sessionId}
|
||||
# ⚠️ If compaction recently occurred: re-read deploy/instructions.md Steps 6-8
|
||||
```
|
||||
|
||||
Delete inapplicable sections (e.g., remove App Service section for Container Apps deploys).
|
||||
|
||||
> ⛔ **Copy `## Before handoff (Step 8)` and `## Artifact verification (Step 8 — MANDATORY)` from the template VERBATIM.** Do NOT paraphrase, merge, or weaken `⛔` markers. These sections are the compaction-safe finalization anchor — diluting them causes artifact writes to be skipped after compaction.
|
||||
|
||||
### Step 6 — Return summary
|
||||
|
||||
Return ≤300 tokens: preflight warnings, deploy command, service types, confirmation of checklist write, database migration note if applicable.
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
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)
|
||||
@@ -0,0 +1,74 @@
|
||||
<!-- 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 |
|
||||
@@ -0,0 +1,124 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,87 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,64 @@
|
||||
<!-- 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
|
||||
@@ -0,0 +1,201 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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[];
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<!-- 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).
|
||||
@@ -0,0 +1,73 @@
|
||||
<!-- 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."
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,46 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,108 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,56 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,74 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,26 @@
|
||||
<!-- 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
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
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) |
|
||||
@@ -0,0 +1,97 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,32 @@
|
||||
<!-- 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 |
|
||||
@@ -0,0 +1,99 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,100 @@
|
||||
<!-- 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}"] }`.
|
||||
@@ -0,0 +1,143 @@
|
||||
<!-- 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 |
|
||||
@@ -0,0 +1,48 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,28 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,87 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 }[];
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,47 @@
|
||||
<!-- 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`.
|
||||
@@ -0,0 +1,63 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,152 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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
|
||||
@@ -0,0 +1,70 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,17 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,31 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,87 @@
|
||||
<!-- 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). |
|
||||
@@ -0,0 +1,140 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,45 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,52 @@
|
||||
<!-- 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[]`.
|
||||
@@ -0,0 +1,82 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,32 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,91 @@
|
||||
<!-- 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).
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,152 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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
|
||||
@@ -0,0 +1,17 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,193 @@
|
||||
<!-- 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).
|
||||
@@ -0,0 +1,113 @@
|
||||
<!-- 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).
|
||||
@@ -0,0 +1,189 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,161 @@
|
||||
<!-- 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).
|
||||
@@ -0,0 +1,193 @@
|
||||
<!-- 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.
|
||||
@@ -0,0 +1,187 @@
|
||||
<!-- 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
<!-- 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).
|
||||
@@ -0,0 +1,30 @@
|
||||
<!-- 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`.
|
||||
@@ -0,0 +1,8 @@
|
||||
<!-- 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.
|
||||
|
||||
# CI/CD Pipeline Patterns
|
||||
|
||||
CI/CD is deferred to v2. Do NOT auto-generate workflow files.
|
||||
|
||||
If the user requests CI/CD guidance, call `mcp_azure_mcp_deploy` → `deploy_pipeline_guidance_get` with `is-azd-project: false`, `pipeline-platform: 'github-actions'`, `deploy-option: 'provision-and-deploy'` and present the guidance.
|
||||
@@ -0,0 +1,77 @@
|
||||
<!-- 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.
|
||||
|
||||
# Dockerfile Generation
|
||||
|
||||
Generate Dockerfiles for components targeting Container Apps (or B1+ App Service) that have no existing Dockerfile. Read this when `iac-generation-rules.md` Step 6b applies.
|
||||
|
||||
## When to Generate
|
||||
|
||||
- Component targets Container Apps and has NO Dockerfile (and no `Dockerfile.azure`)
|
||||
- Component targets App Service B1+ with `deployStrategy.containerized: true`
|
||||
- ⛔ Do NOT generate for F1/D1 SKUs — use platform runtime stack instead
|
||||
- ⛔ Do NOT overwrite an existing Dockerfile — create `Dockerfile.azure` only if BuildKit stripping is needed
|
||||
|
||||
## Principles
|
||||
|
||||
### Layer ordering
|
||||
|
||||
Copy dependency manifests and install BEFORE copying source (preserves layer cache):
|
||||
|
||||
```
|
||||
COPY {manifest files} ./
|
||||
RUN {install command}
|
||||
COPY . .
|
||||
```
|
||||
|
||||
### Base image selection
|
||||
|
||||
| Principle | Rule |
|
||||
|-----------|------|
|
||||
| Pin version | `node:{major}-slim`, NOT `node:latest` |
|
||||
| Slim variants | `-slim` or `-alpine` for smaller images |
|
||||
| Multi-stage | Go, .NET, Java, Rust: build in SDK image, copy binary to runtime |
|
||||
| Match runtime | Read `engines`, `python_requires`, `go.mod`, `<TargetFramework>` |
|
||||
|
||||
### Port alignment
|
||||
|
||||
`EXPOSE` port, app's listening port, and Container App `targetPort` must all match. Mismatch = silent health probe failure. Read app config for listening port, set `EXPOSE {port}`, add `ENV PORT={port}` if app reads `PORT` from env.
|
||||
|
||||
### Security defaults
|
||||
|
||||
- Non-root user — Debian/`-slim` base: `RUN groupadd -r app && useradd -r -g app app`; Alpine base: `RUN addgroup -S app && adduser -S app -G app`. Then `USER app`
|
||||
- Never `COPY .env` or secrets — use `.dockerignore`
|
||||
- Direct exec: `CMD ["node", "server.js"]` not `CMD ["npm", "start"]`
|
||||
|
||||
### .dockerignore
|
||||
|
||||
Always generate alongside Dockerfile. Exclude: `.git`, `node_modules`, `__pycache__`, `*.pyc`, `.env`, `.env.*`, `.azure`, `.copilot-azure`, `infra`, `*.md`.
|
||||
|
||||
### Common pitfalls
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| `COPY . .` before deps | Copy manifests first, install, then source |
|
||||
| `npm install` in prod | `npm ci --omit=dev` |
|
||||
| Wrong EXPOSE port | Read actual listening port from app source |
|
||||
|
||||
### Next.js multi-container build args
|
||||
|
||||
`NEXT_PUBLIC_*` env vars are embedded in the client JS bundle at `npm run build` time — runtime Container App env vars have zero effect on client-side code. For multi-container deploys where a Next.js frontend references another component's API:
|
||||
|
||||
1. Dockerfile MUST include `ARG NEXT_PUBLIC_API_URL` before the `RUN npm run build` step
|
||||
2. Deploy phase passes `--build-arg NEXT_PUBLIC_API_URL=https://{api-fqdn}` to `az acr build`
|
||||
|
||||
Detect from `.env*` files containing `NEXT_PUBLIC_*` pointing to another service (e.g., `NEXT_PUBLIC_API_URL=http://localhost:3001`).
|
||||
|
||||
### ACR Build Compatibility — `Dockerfile.azure` Generation
|
||||
|
||||
⛔ ACR `az acr build` uses the **classic Docker builder — NOT BuildKit**. Do NOT assume ACR supports BuildKit.
|
||||
|
||||
**When to generate `Dockerfile.azure`:** If `buildRequirements.hasBuildKitSyntax == true` OR the existing Dockerfile contains any BuildKit-only syntax, create `{component}/Dockerfile.azure` with all BuildKit syntax removed.
|
||||
|
||||
**BuildKit-only syntax** (strip all): `# syntax=` directives, `RUN --mount=...` (all types: cache, secret, bind, tmpfs), `RUN --network=...`, `RUN --security=...`, `COPY --link`, `COPY --chmod=...`, heredoc syntax (`RUN <<EOF`).
|
||||
|
||||
⛔ **Package manager pinning applies here too.** When stripping BuildKit from an existing Dockerfile, also replace `npm install -g {pm}@latest` with the exact version from the project's `packageManager` field in `package.json` (e.g., `pnpm@9.4.0`). The upstream Dockerfile's `@latest` may pull a version incompatible with the base image's Node.js version.
|
||||
|
||||
⛔ Handle multi-line continuations (`\`) — remove the BuildKit flag but preserve the actual command across all continuation lines. Never leave a bare `RUN` with no command.
|
||||
@@ -0,0 +1,53 @@
|
||||
<!-- 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.
|
||||
|
||||
# Environment Variables & Secrets — Cross-Cutting Rules
|
||||
|
||||
Applies to ALL compute targets (App Service, Container Apps, Functions). For Container Apps-specific Bicep patterns (secretRef, identity), see [bicep-container-apps.md](bicep-container-apps.md).
|
||||
|
||||
## Environment Variable Value Derivation
|
||||
|
||||
> ⛔ **Never invent env var values — derive from the app's config class.** Cross-reference `.env.example`, `.env.sample`, `docker-compose.yml`, and the app's config module. Verify each value:
|
||||
> 1. **Type validation:** URL-typed fields need valid URLs — not `*` or placeholders
|
||||
> 2. **Defaults:** Use app defaults unless overriding with deployed URL
|
||||
> 3. **Required:** Fields without defaults must be provided
|
||||
>
|
||||
> **Pitfalls:** `CORS_ORIGINS=["*"]` → invalid for strict validators (use actual URLs). `DATABASE_URL=changethis` → use a managed-identity connection (no password; wire plain connection params, driver fetches a token). JSON array env vars need Bicep variable escaping:
|
||||
> ```bicep
|
||||
> var corsOrigins = '["https://${containerApp.properties.configuration.ingress.fqdn}"]'
|
||||
> { name: 'CORS_ORIGINS', value: corsOrigins }
|
||||
> ```
|
||||
|
||||
## App-Internal Secret Storage (No Key Vault)
|
||||
|
||||
> ⛔ **No Key Vault is created.** Database/cache/storage access is managed-identity + token — there is NO connection-string password or access key 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** from an `@secure()` param generated at deploy time.
|
||||
|
||||
**Where app-internal secrets go:**
|
||||
- **App Service / Functions:** `@secure()` param → `siteConfig.appSettings` (platform-encrypted at rest), or set at deploy via `az webapp config appsettings set`.
|
||||
- **Container Apps:** `@secure()` param → the CA's **native** `secrets: [{ name, value }]` → `secretRef`. No `keyVaultUrl`. Native secrets have no RBAC dependency, so they need no `isPlaceholder` gating (two-phase wiring is still needed for ACR registries + real image only).
|
||||
|
||||
```bicep
|
||||
// Container Apps — native secret (NOT keyVaultUrl)
|
||||
secrets: [
|
||||
{ name: 'secret-key', value: secretKey } // value from an @secure() param
|
||||
]
|
||||
```
|
||||
|
||||
> ⛔ **Do NOT hardcode secrets in committed files** — not in `main.parameters.json`, `terraform.tfvars`, env vars, or any generated file. (`@secure()` Bicep params ARE the correct way to pass an **app-internal** secret at deploy time — the ban is on committing the value, not on the parameter. Databases/caches/storage have NO secret param — they use managed identity.) The deploy phase generates each app-internal secret ONCE and passes it as an `@secure()` param — see [code-deployment-appservice.md](../../deploy/references/code-deployment-appservice.md) or [code-deployment-container-apps.md](../../deploy/references/code-deployment-container-apps.md).
|
||||
|
||||
## Azure Managed Service SSL/TLS Requirements
|
||||
|
||||
> ⛔ **Azure managed databases and caches enforce TLS. Local docker-compose configs typically don't.** This mismatch causes container crashes post-deploy.
|
||||
|
||||
Check `prereq-output.json.warnings[]` for warnings with `fixPhase: "scaffold"`. Each warning's `fix` field describes the required IaC change. Read the app's config loader for the actual env var name.
|
||||
|
||||
> ⛔ **Prefer env var override over code change.** Only modify source if no env override path exists AND user approves.
|
||||
> ⛔ **Self-review:** If any `fixPhase: "scaffold"` warning exists and IaC lacks the fix → flag as FLAGGED.
|
||||
|
||||
## App-Internal Secret Naming
|
||||
|
||||
> ⛔ **Container Apps native secret names allow only lowercase alphanumeric characters and hyphens.** Map env var names: `SECRET_KEY` → `secret-key`, `JWT_SECRET` → `jwt-secret`. Do NOT use underscores or uppercase. (App Service app settings keep the original env var name, e.g. `SECRET_KEY`.)
|
||||
|
||||
## Compose → Azure PaaS Credential Mapping (Entra-only)
|
||||
|
||||
> ⛔ **Azure managed databases are provisioned Entra-only — there is no username/password to map.** Docker-compose `POSTGRES_USER`/`POSTGRES_PASSWORD` / `MYSQL_USER`/`MYSQL_PASSWORD` are DROPPED, not translated. The app connects with its **managed identity**: the DB username is the app MI's principal name and the "password" is an Entra token fetched at runtime. Map the compose DB **name** (`POSTGRES_DB`/`MYSQL_DATABASE`) to the app DB; do NOT emit any password env var. See [database-post-deploy.md](../../deploy/references/database-post-deploy.md) for granting the MI a DB role.
|
||||
@@ -0,0 +1,15 @@
|
||||
<!-- 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 Handling — Scaffold Phase
|
||||
|
||||
| Error | Remediation |
|
||||
|-------|-------------|
|
||||
| `prepare-plan.json` missing | Trigger prepare backfill via `azure-app-onboard` orchestrator. Do not generate IaC without a plan. |
|
||||
| Existing Azure IaC (`.bicep`, `azure.yaml`, or `.tf` with `azurerm` provider) | ⛔ Never delete/overwrite; move to `.copilot-azure/sessions/<id>/replaced-files/` (mirror path), tell the user their original was preserved at that backup location, then scaffold. |
|
||||
| Existing non-Azure IaC (`.tf` with GCP/AWS provider) | Generate Azure TF alongside — see [terraform-patterns.md § Non-Azure IaC coexistence](terraform-patterns.md). Do NOT halt. |
|
||||
| MCP tool unavailable | Fall back to reference patterns. Flag generated IaC as "unverified against best practices." |
|
||||
| Self-review finds FLAGGED items | Include in `scaffold-manifest.json.selfReview.findings[]`. Surface at approval gate. |
|
||||
| Self-healing exhausted (3 attempts) | Pause auto-healing. Present diagnosis: (1) explain error pattern, (2) propose specific next fix, (3) ask user: "Yes, try that" / "I have a suggestion" / "Stop." If user continues, auto-heal for 5 more, then ask every 5 thereafter. If user stops, write `validationResult` with `status: "Failed"` and all errors. Do NOT proceed to deploy. |
|
||||
| Schema summary exceeds token limit | Use sub-agent pattern: compress each schema to ≤500 tokens. |
|
||||
| `context.json` malformed | Halt. Report: "Session state corrupted — consider starting a fresh session." |
|
||||
@@ -0,0 +1,70 @@
|
||||
<!-- 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 Generation Rules — Steps 5–8
|
||||
|
||||
Rules for generating infrastructure code, Dockerfiles, security verification, and telemetry wiring.
|
||||
|
||||
## Step 5 — Generate IaC
|
||||
|
||||
For each service in `services[]`:
|
||||
|
||||
> **Sub-agent delegation:** Use [subagent-iac-gen.md](subagent-iac-gen.md) template verbatim. Include full `prepare-plan.json`, `ScaffoldManifest` interface, and compute-target patterns. Self-review (Step 9) remains mandatory after sub-agent returns.
|
||||
|
||||
> **PostgreSQL wiring:** Include firewall rule + extension allow-list (see [subagent-iac-gen.md](subagent-iac-gen.md) Step 6 if PostgreSQL in plan). ⛔ **PostgreSQL config resources** (e.g., `require_secure_transport`) must use `source: 'user-override'` — `'system-default'` is read-only and ARM rejects it. ⛔ **Do NOT create `databases/postgres` child resource** — it exists by default and ARM rejects duplicate creation. **BuildKit Dockerfiles:** Generate `Dockerfile.azure` for ACR compatibility — see [code-deployment-container-apps.md § BuildKit](../../deploy/references/code-deployment-container-apps.md).
|
||||
|
||||
> **Env var completeness:** Read `.env.example` (or `.env.sample`, `config.example`) + config/settings files (Pydantic `Settings`, `@t3-oss/env-nextjs`, Django `settings.py`) for each component to enumerate required env vars before generating IaC. Every env var with a placeholder value (not `localhost`) should map to either: (1) a Bicep parameter, (2) a **managed-identity KV reference** for an app-internal secret (NOT an Azure resource credential), or (3) a value derived from other resources. ⛔ **Database/cache/storage access is managed-identity + token — wire connection *parameters* (host, db name, MI username, `sslmode=require`) as plain app settings, never a connection-string password or access key.** ⛔ **Container Apps:** KV `secretRef` entries must be gated behind `isPlaceholder` — Phase 1 = `secrets: []`, Phase 2 activates KV refs. See [bicep-container-apps.md](bicep-container-apps.md). Flag unmapped vars in selfReview as ⚠️ WARN. Missing vars cause container crash loops at deploy time.
|
||||
|
||||
> ⛔ **Set `targetScope = 'subscription'` in `main.bicep`.** Subscription-scope Bicep creates the resource group in IaC with all 5 AppOnboard tags (including `created-at`). Do NOT use default resource-group scope — it requires imperative `az group create` which consistently misses tags. If the user lacks subscription-level permissions, the deploy phase handles fallback to RG-scope automatically.
|
||||
|
||||
> ⛔ **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 to the App Service Bicep. The `deployStrategy.startupCommand` goes into `appCommandLine`, and `deployStrategy.requiredAppSettings` goes into `appSettings[]`. When no `deployStrategy` exists, do NOT set `appCommandLine` — let Oryx use its default startup.
|
||||
|
||||
### Session Tags — Mandatory on ALL Resources
|
||||
|
||||
⛔ Include the 5 AppOnboard tags on every resource and module. See [bicep-patterns.md § tags](bicep-patterns.md) for Bicep code block or [terraform-patterns.md § tags](terraform-patterns.md) for HCL code block.
|
||||
|
||||
| Tag key | Value source |
|
||||
|---------|-------------|
|
||||
| `app-onboard-skill` | `'true'` |
|
||||
| `app-onboard-session-id` | `sessionId` param |
|
||||
| `created-at` | ISO timestamp (scaffold populates, deploy may override) |
|
||||
| `environment` | `naming.resourcePrefix` |
|
||||
| `deployed-by` | `context.json.azure.userDisplayName` |
|
||||
|
||||
If extending existing IaC, MERGE with existing tags using `union()` / `merge()`.
|
||||
|
||||
> ⛔ **Do NOT generate `azure.yaml`.** Deploy via `az deployment sub create`. See [pipeline-rules.md](../../references/pipeline-rules.md) § azure.yaml prohibition.
|
||||
|
||||
### Verification
|
||||
|
||||
⛔ **API version verification:** Use versions from `apiVersions` input map. If a type is missing from the map, use the latest GA version from reference file examples — no `-preview` suffix. `az bicep build` catches invalid versions at compile time.
|
||||
|
||||
⛔ **Resource property verification:** Training data references deprecated properties. Known traps: `Microsoft.CognitiveServices/accounts/deployments` uses `sku` (name + capacity), NOT `scaleSettings` (deprecated) — omit `raiPolicyName`. ⛔ **Do NOT emit a `Microsoft.KeyVault/vaults` resource at all** (no Key Vault). Fallback: `az bicep build` + `what-if`.
|
||||
|
||||
### Output
|
||||
|
||||
`infra/main.bicep`, `main.parameters.json`, `modules/{service}.bicep` per service.
|
||||
|
||||
### Platform Compatibility
|
||||
|
||||
- **Line endings:** Generated `.bicep` files need LF, not CRLF — Bicep triple-quoted strings pass content literally to ARM, and `\r` bytes crash `/bin/sh` in containers. The validate subagent runs `mcp_bicep_format_bicep_file` (or `bicep-format_bicep_file`) post-generation to enforce this. If the formatter is unavailable, ensure files use LF manually.
|
||||
- **Shell compatibility:** Startup scripts in Bicep multiline strings MUST use `set -eu` (POSIX). Do NOT use `set -euo pipefail` — Container Apps base images use `/bin/sh` (dash), not bash.
|
||||
- **Package manager pinning:** When generating or modifying Dockerfiles, pin package manager versions from the project's `packageManager` field (e.g., `pnpm@9.4.0`). Never use `@latest` — major version drift breaks builds on older Node.js base images.
|
||||
|
||||
### Security Patterns — Apply During Generation
|
||||
|
||||
⛔ Apply ALL patterns from [`bicep-patterns-security.md`](bicep-patterns-security.md) during generation — managed identity, SCM/FTP auth policies, on-compute app-internal secrets (NO Key Vault), least-privilege RBAC.
|
||||
|
||||
## Step 6b — Dockerfile Generation (conditional)
|
||||
|
||||
If `prepare-plan.json.services[]` has any entry with `name` containing "Container Apps" AND that component has no existing Dockerfile → ⛔ **You MUST read [`dockerfile-generation.md`](dockerfile-generation.md)** and generate one. Skip if all Container Apps components already have Dockerfiles, or if no service targets Container Apps.
|
||||
|
||||
## Step 7 — Secure-by-Default Verification
|
||||
|
||||
⛔ Read [`bicep-patterns-security.md`](bicep-patterns-security.md) (or `terraform-patterns.md` § Security Defaults) and verify ALL security patterns from Step 5. If deployment includes compute with MI ↔ resource RBAC, ⛔ read [rbac-roles.md](rbac-roles.md) for GUID table. **If a role is NOT in the table, check [Azure built-in roles docs](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles).** Never guess GUIDs. Skip RBAC for SWA-only.
|
||||
|
||||
Tell the user which patterns were applied: managed identity, KV secrets, least-privilege RBAC, SCM/FTP auth policies, private endpoints.
|
||||
|
||||
## Step 8 — Wire Telemetry
|
||||
|
||||
If `prepare-plan.json.instrumentation.appInsightsEnabled` is `true`, add `APPLICATIONINSIGHTS_CONNECTION_STRING` as a plain app setting wired from the App Insights module output. The connection string is not a secret — no KV storage needed. For Container Apps: use a plain env var (not `secretRef`).
|
||||
@@ -0,0 +1,139 @@
|
||||
<!-- 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.
|
||||
|
||||
# Scaffold Phase — MCP Tools
|
||||
|
||||
Phase-exclusive tool parameters for the scaffold phase. For shared tools (`get_azure_bestpractices`, `subscription_list`, `group_list`, `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/>
|
||||
|
||||
---
|
||||
|
||||
## Azure MCP Tools — `mcp_azure_mcp_*`
|
||||
|
||||
### `mcp_azure_mcp_deploy` (hierarchical)
|
||||
|
||||
| Sub-command | Required Params | Optional Params | Read-Only |
|
||||
|-------------|----------------|-----------------|-----------|
|
||||
| `deploy_plan_get` | `workspace-folder`, `project-name`, `target-app-service` (ContainerApp\|WebApp\|FunctionApp\|AKS), `provisioning-tool` (AzCli\|AZD), `source-type` (from-project\|from-azure\|from-context), `deploy-option` (provision-and-deploy\|deploy-only\|provision-only) | `iac-options` (bicep\|terraform), `resource-group` | ✅ |
|
||||
| `deploy_iac_rules_get` | `deployment-tool` (AzCli\|AZD) | `iac-type` (bicep\|terraform), `resource-types` (comma-sep: appservice, containerapp, function, aks, azuredatabaseforpostgresql, azuredatabaseformysql, azuresqldatabase, azurecosmosdb, azurestorageaccount, azurekeyvault) | ✅ |
|
||||
| `deploy_pipeline_guidance_get` | `is-azd-project` (bool), `pipeline-platform` (github-actions\|azure-devops), `deploy-option` (deploy-only\|provision-and-deploy) | subscription, tenant | ✅ |
|
||||
|
||||
### `mcp_azure_mcp_bicepschema` (hierarchical)
|
||||
|
||||
| Sub-command | Required Params | Optional Params | Read-Only |
|
||||
|-------------|----------------|-----------------|-----------|
|
||||
| `bicepschema_get` | `resource_type` (e.g. Microsoft.Web/sites) | `api_version` | ✅ |
|
||||
|
||||
**Usage:** ARM resource type schemas. Primary tool for scaffold — returns resource properties, required fields, and constraints. Prefer over `mcp_bicep_get_az_resource_type_schema` when you need a quick schema lookup without specifying an exact API version.
|
||||
|
||||
### `mcp_azure_mcp_azureterraformbestpractices` (flat)
|
||||
|
||||
| Required | Optional | Read-Only |
|
||||
|----------|----------|-----------|
|
||||
| *(none)* | `resource_type` (e.g. "azurerm_linux_web_app") | ✅ |
|
||||
|
||||
**Usage:** Returns Terraform best practices for Azure resources. Omit `resource_type` for general guidance. Provide `resource_type` to get resource-specific `azurerm` configuration patterns, recommended properties, and security settings. Used during scaffold Step 5 for per-resource HCL generation (Terraform path only).
|
||||
|
||||
---
|
||||
|
||||
## Bicep MCP Tools — `mcp_bicep_*`
|
||||
|
||||
### `mcp_bicep_get_bicep_best_practices`
|
||||
|
||||
| Required | Optional | Read-Only |
|
||||
|----------|----------|-----------|
|
||||
| *(none)* | — | ✅ |
|
||||
|
||||
Returns: comprehensive best practices for Bicep authoring (naming, organization, parameters, security, testing).
|
||||
|
||||
### `mcp_bicep_get_az_resource_type_schema`
|
||||
|
||||
| Required | Optional | Read-Only |
|
||||
|----------|----------|-----------|
|
||||
| `azResourceType` (e.g. Microsoft.Web/sites), `apiVersion` (e.g. 2024-11-01) | — | ✅ |
|
||||
|
||||
Returns: complete JSON schema for the resource type including all properties, nested types, constraints.
|
||||
|
||||
### `mcp_bicep_list_avm_metadata`
|
||||
|
||||
| Required | Optional | Read-Only |
|
||||
|----------|----------|-----------|
|
||||
| *(none)* | — | ✅ |
|
||||
|
||||
Returns: metadata for all Azure Verified Modules (AVM) including versions and docs.
|
||||
|
||||
### `mcp_bicep_list_az_resource_types_for_provider`
|
||||
|
||||
| Required | Optional | Read-Only |
|
||||
|----------|----------|-----------|
|
||||
| `providerNamespace` (e.g. Microsoft.Compute) | — | ✅ |
|
||||
|
||||
Returns: all resource types and API versions for the provider.
|
||||
|
||||
### `mcp_bicep_build_bicep`
|
||||
|
||||
| Required | Optional | Read-Only |
|
||||
|----------|----------|-----------|
|
||||
| `filePath` (absolute) | — | ✅ |
|
||||
|
||||
Returns: compiled ARM template JSON + compilation errors, warnings, info for a `.bicep` file.
|
||||
|
||||
### `mcp_bicep_format_bicep_file`
|
||||
|
||||
| Required | Optional | Read-Only |
|
||||
|----------|----------|-----------|
|
||||
| `filePath` (absolute) | — | ❌ |
|
||||
|
||||
Formats the Bicep file per official standards. Respects `bicepconfig.json`.
|
||||
|
||||
### `mcp_bicep_get_deployment_snapshot`
|
||||
|
||||
| Required | Optional | Read-Only |
|
||||
|----------|----------|-----------|
|
||||
| `filePath` (absolute, `.bicepparam`) | — | ✅ |
|
||||
|
||||
Returns: deployment snapshot from a Bicep parameters file — resolves all parameter values and template references into a single deployable payload. Useful for pre-deploy validation.
|
||||
|
||||
### `mcp_bicep_get_file_references`
|
||||
|
||||
| Required | Optional | Read-Only |
|
||||
|----------|----------|-----------|
|
||||
| `filePath` (absolute, `.bicep` or `.bicepparam`) | — | ✅ |
|
||||
|
||||
Returns: all file references (modules, parameters, imports) from a Bicep file. Useful for dependency analysis and verifying scaffold output completeness.
|
||||
|
||||
### `mcp_bicep_decompile_arm_template_file`
|
||||
|
||||
| Required | Optional | Read-Only |
|
||||
|----------|----------|-----------|
|
||||
| `filePath` (absolute, .json/.jsonc/.arm) | — | ❌ |
|
||||
|
||||
Converts ARM JSON → Bicep. Best-effort; may need manual review.
|
||||
|
||||
### `mcp_bicep_decompile_arm_parameters_file`
|
||||
|
||||
| Required | Optional | Read-Only |
|
||||
|----------|----------|-----------|
|
||||
| `filePath` (absolute, .json/.jsonc/.arm) | — | ❌ |
|
||||
|
||||
Converts ARM parameters JSON → `.bicepparam`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 Tool Map
|
||||
|
||||
| Tool | Sub-command | AppOnboard Step | Purpose |
|
||||
|------|-----------|----------|---------|
|
||||
| `mcp_bicep_get_bicep_best_practices` | *(flat)* | Step 5 | **Primary** — Bicep conventions, naming, module patterns for IaC generation |
|
||||
| `mcp_bicep_get_az_resource_type_schema` | *(flat)* | Step 5 | **Primary** — ARM resource type JSON schema per service for property validation |
|
||||
| `mcp_bicep_list_avm_metadata` | *(flat)* | Step 5 | **Primary** — AVM module catalog; prefer verified modules over raw resource definitions |
|
||||
| `mcp_azure_mcp_bicepschema` | `bicepschema_get` | Step 5 | **Primary** — ARM resource type schemas without requiring exact API version |
|
||||
| `mcp_azure_mcp_deploy` | `deploy_iac_rules_get` | Step 5 | IaC best practices and rules. `--iac-type bicep` (default) or `--iac-type terraform` |
|
||||
| `mcp_azure_mcp_get_azure_bestpractices` | `get_azure_bestpractices_get` | Step 5 | SDK patterns during IaC gen. `resource: "general"`, `action: "code-generation"` |
|
||||
| `mcp_bicep_build_bicep` | *(flat)* | Step 9 | **Primary** — validate generated `.bicep` files in self-review L3 (syntax, properties, API versions) |
|
||||
| `mcp_bicep_format_bicep_file` | *(flat)* | Step 9 | Format generated `.bicep` files per official standards after self-review fixes |
|
||||
| `mcp_bicep_get_file_references` | *(flat)* | Step 11 | Verify scaffold output completeness — all module references resolve |
|
||||
| `mcp_azure_mcp_deploy` | `deploy_pipeline_guidance_get` | Step 10 | CI/CD pipeline config. `is-azd-project: false`, `pipeline-platform: "github-actions"`, `deploy-option: "provision-and-deploy"` |
|
||||
| `mcp_azure_mcp_deploy` | `deploy_plan_get` | Step 12 | Structured deployment plan as validation input. Set `source-type: "from-project"`, `target-app-service` from plan |
|
||||
| `mcp_azure_mcp_azureterraformbestpractices` | *(flat)* | Step 5 | Terraform path only — patterns and conventions for HCL generation |
|
||||
@@ -0,0 +1,58 @@
|
||||
<!-- 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.
|
||||
|
||||
# RBAC — Common Roles Reference
|
||||
|
||||
Shared reference for role assignment GUIDs used in both Bicep and Terraform. Loaded by both pattern files.
|
||||
|
||||
## Deterministic Role Assignments
|
||||
|
||||
Use `guid()` (Bicep) or deterministic naming (Terraform) for reproducible assignment names. Always set `principalType` to `'ServicePrincipal'` — prevents AAD graph lookup delays.
|
||||
|
||||
If the required role is NOT in the table below, call `azure__documentation` with the target resource type to look up the correct built-in role definition ID before generating the role assignment.
|
||||
|
||||
| Role | ID | Use |
|
||||
|------|-----|-----|
|
||||
| AcrPull | `7f951dda-4ed3-4680-a7ca-43fe172d538d` | Container Apps → ACR |
|
||||
| Storage Blob Data Contributor | `ba92f5b4-2d11-453d-a403-e96b0029c9fe` | App → Storage blobs (read/write/delete) |
|
||||
| Storage Blob Data Reader | `2a2b9908-6ea1-4ae2-8e65-a410df84e7d1` | App → Storage blobs (read-only) |
|
||||
| Storage Queue Data Contributor | `974c5e8b-45b9-4653-ba55-5f855dd0fb88` | App → Storage queues |
|
||||
| Storage Table Data Contributor | `0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3` | App → Storage tables |
|
||||
| Azure Service Bus Data Sender | `69a216fc-b8fb-44d8-bc22-1f3c2cd27a39` | App → Service Bus (send) |
|
||||
| Azure Service Bus Data Receiver | `4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0` | App → Service Bus (receive) |
|
||||
| Azure Event Hubs Data Sender | `2b629674-e913-4c01-ae53-ef4638d8f975` | App → Event Hubs (send) |
|
||||
| Azure Event Hubs Data Receiver | `a638d3c7-ab3a-418d-83e6-5f17a39d4fde` | App → Event Hubs (receive) |
|
||||
| App Configuration Data Reader | `516239f1-63e1-4d78-a4de-a74fb236a071` | App → App Configuration (read) |
|
||||
| SignalR Service Owner | `7e4f1700-ea5a-4f59-8f37-079cfe29dce3` | App → SignalR |
|
||||
| Cognitive Services OpenAI User | `5e0bd9bd-7b93-4f28-af87-19fc36ad61bd` | App → Azure OpenAI (inference) |
|
||||
| Search Index Data Reader | `1407120a-92aa-4202-b7e9-c0e197c71c8f` | App → AI Search (query) |
|
||||
| Search Index Data Contributor | `8ebe5a00-799e-43f5-93ac-243d3dce84a7` | App → AI Search (read/write index) |
|
||||
| Monitoring Metrics Publisher | `3913510d-42f4-4e42-8a64-420c390055eb` | App → Azure Monitor (custom metrics) |
|
||||
|
||||
## Cosmos DB — Data Plane RBAC (NOT ARM RBAC)
|
||||
|
||||
⛔ **Cosmos DB uses its OWN role system** — do NOT use `Microsoft.Authorization/roleAssignments` for Cosmos DB data access. ARM RBAC roles (Contributor, Reader) grant control-plane access only. For data access (read/write documents), use `Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments`.
|
||||
|
||||
| System | Resource type | Use for |
|
||||
|--------|--------------|--------|
|
||||
| ARM RBAC | `Microsoft.Authorization/roleAssignments` | Subscription/RG-level permissions (Contributor, Reader, Storage/DB data roles) |
|
||||
| Cosmos DB data plane | `Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments` | Document read/write access (Data Reader, Data Contributor) |
|
||||
|
||||
Built-in Cosmos DB data roles:
|
||||
- Data Reader: `00000000-0000-0000-0000-000000000001`
|
||||
- Data Contributor: `00000000-0000-0000-0000-000000000002`
|
||||
|
||||
## Delegation
|
||||
|
||||
For complex RBAC scenarios (custom roles, cross-subscription, conditional access), AppOnboard does not generate custom role definitions inline. Instead, append an entry to `prepare-plan.json → postDeployRecommendations[]` (schema: `PostDeployRecommendation` in [`prepare-schemas.ts`](../../prepare/references/prepare-schemas.ts)):
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Configure custom RBAC role for <service>",
|
||||
"reason": "<why the built-in roles above are insufficient>",
|
||||
"effort": "medium",
|
||||
"services": ["<affected-service>"]
|
||||
}
|
||||
```
|
||||
|
||||
At handoff, the agent will detect this recommendation and offer to call `mcp_azure_mcp_role` to list existing role assignments and create custom roles for the deployed resource group. See `handoff-protocol.md` Agent-Based Next Steps — the RBAC condition row triggers this automatically.
|
||||
@@ -0,0 +1,49 @@
|
||||
<!-- 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.
|
||||
|
||||
# Scaffold Healing Rules
|
||||
|
||||
Self-healing loop for scaffold validation failures. Contains error classification (FIXABLE vs BLOCKING) and scaffold-specific escalation and plan-change rules.
|
||||
|
||||
## Error Classification
|
||||
|
||||
| Error Type | Class | Auto-Fix Strategy |
|
||||
|------------|-------|-------------------|
|
||||
| Invalid property name | FIXABLE | Fix from schema |
|
||||
| Syntax error (HCL/Bicep) | FIXABLE | Regenerate from references |
|
||||
| Missing required property | FIXABLE | Call `mcp_bicep_build_bicep` for structured error, fix from diagnostics output |
|
||||
| Wrong API version | FIXABLE | Call `mcp_bicep_list_az_resource_types_for_provider` with `{ providerNamespace: "..." }` for latest GA. Fallback: reference file examples |
|
||||
| Provider version conflict | FIXABLE | Update `required_providers` |
|
||||
| Undeclared variable | FIXABLE | Add to `variables.tf` |
|
||||
| Policy-blocked SKU | FIXABLE | Next-best from `rejectedAlternatives[]` |
|
||||
| Circular dependency | FIXABLE | Break cycle — refactor module refs |
|
||||
| Permission/RBAC insufficient | BLOCKING | Surface role + `az role assignment create` |
|
||||
| State backend inaccessible | BLOCKING | Surface `az storage account create` |
|
||||
| Region unsupported | BLOCKING | Suggest alternate regions (user decision) |
|
||||
| Quota exhaustion (all tiers+regions) | PLAN_LEVEL_CHANGE | ⛔ See § PLAN_LEVEL_CHANGE |
|
||||
| Quota exhaustion (single region) | PLAN_LEVEL_CHANGE | ⛔ Region pivot — skip `quotaValidation.checkedRegions` failures |
|
||||
| Policy blocks service entirely | PLAN_LEVEL_CHANGE | ⛔ Map to `rejectedAlternatives[]` |
|
||||
|
||||
## Healing Escalation Cadence
|
||||
|
||||
Classify Step 11 failures using the table above. **FIXABLE:** edit IaC → re-validate (max 3 before asking). Each attempt must be a *different* fix. **BLOCKING:** surface to user, halt.
|
||||
|
||||
> ⛔ **After each FIXABLE auto-fix:** call `mcp_bicep_build_bicep` with `{ filePath: "infra/main.bicep" }` to quick-check before re-dispatching the validate sub-agent. If errors remain, fix again — saves a sub-agent dispatch. Fallback: `az bicep build`.
|
||||
|
||||
> ⛔ **After 3 attempts:** summarize error pattern, propose specific next fix (never "I'll try again" without naming the change), present options: **"Yes, try that"** (5 more) | **"I have a suggestion"** (apply user fix) | **"Stop"** (write `validationResult.status: "Failed"`). Ask again every 5 thereafter.
|
||||
|
||||
> ⛔ **Same error 3×:** read [iac-resources.md](../../references/iac-resources.md) for docs. Present to user; stop auto-healing that pattern.
|
||||
|
||||
## PLAN_LEVEL_CHANGE
|
||||
|
||||
> ⛔ Service type or region changes require user re-approval — never silently rewrite IaC.
|
||||
|
||||
| Step | Action |
|
||||
|------|--------|
|
||||
| 1. STOP | Never silently change service/region |
|
||||
| 2. Update plan | `prepare-plan.json`: `services[]`, `naming.resources[]`, `costEstimate`, `deployStrategy` per [`prepare-schemas.ts`](../../prepare/references/prepare-schemas.ts) |
|
||||
| 3. Re-approve | "⚠️ Plan change: {old} → {new} (~${new}/mo). Approve? (Yes / Edit / Cancel)" |
|
||||
| 4. Regenerate | New module files with correct names, delete stale, re-run self-review + validation |
|
||||
| 5. Log | [`ScaffoldHealingAttempt`](scaffold-schemas.ts) with `planLevelChange: true`. Counts toward healing cap |
|
||||
|
||||
> ⛔ **Artifact consistency:** After any service/region/SKU change, update ALL upstream artifacts (`prepare-plan.json`, `scaffold-manifest.json`, `context.json`, IaC files) — the plan MUST match deployed reality.
|
||||
@@ -0,0 +1,115 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Scaffold artifact schema — scaffold-manifest.json.
|
||||
* Read by scaffold refs (validation-and-manifest.md, scaffold-healing-rules.md, iac-generation-rules.md).
|
||||
*/
|
||||
|
||||
// ─── Scaffold healing types ──────────────────────────────────────────────────
|
||||
|
||||
export type ScaffoldErrorClassification = "FIXABLE" | "BLOCKING";
|
||||
export type ScaffoldHealingResult = "fixed" | "still-failing" | "blocked";
|
||||
|
||||
export interface ScaffoldHealingError {
|
||||
check: string;
|
||||
detail: string;
|
||||
classification: ScaffoldErrorClassification;
|
||||
}
|
||||
|
||||
export interface ScaffoldHealingFix {
|
||||
file: string;
|
||||
change: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface ScaffoldHealingAttempt {
|
||||
attempt: number;
|
||||
errors: ScaffoldHealingError[];
|
||||
fixes: ScaffoldHealingFix[];
|
||||
result: ScaffoldHealingResult;
|
||||
/** True when this healing attempt changed the service type or region — requires re-approval */
|
||||
planLevelChange?: boolean;
|
||||
/** Original service before the pivot (e.g., "App Service B1") */
|
||||
originalService?: string;
|
||||
/** New service after the pivot (e.g., "Container Apps Consumption") */
|
||||
newService?: string;
|
||||
}
|
||||
|
||||
// ─── scaffold-manifest.json ──────────────────────────────────────────────────
|
||||
|
||||
export type SelfReviewRating = "VERIFIED" | "PLAUSIBLE" | "FLAGGED";
|
||||
|
||||
export interface ScaffoldFile {
|
||||
path: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface SelfReviewFinding {
|
||||
layer: string; // "L1" | "L2" | "L3" | "L4"
|
||||
claim: string;
|
||||
rating: SelfReviewRating;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export type IacFormat = "bicep" | "terraform";
|
||||
|
||||
export interface ValidationCheck {
|
||||
name: string;
|
||||
passed: boolean;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of Step 11 IaC validation. This is what `ScaffoldManifest.validationResult` holds.
|
||||
* Distinct from `ConformanceResult` below — do not substitute one for the other.
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
status: "Validated" | "Partial" | "Failed";
|
||||
checks: ValidationCheck[];
|
||||
proof?: string;
|
||||
}
|
||||
|
||||
/** One BLOCK failure emitted by scaffold-conformance.{ps1,sh} (Step 3c plan-conformance gate). */
|
||||
export interface ConformanceFailure {
|
||||
id: string; // e.g. "TAGS-NO-CAMEL", "NO-PLAINTEXT-SECRET", "DB-TLS-ON"
|
||||
detail: string;
|
||||
file: string;
|
||||
}
|
||||
|
||||
/** Result of the deterministic plan-conformance gate (subagent-validate.md Step 3c). */
|
||||
export interface ConformanceResult {
|
||||
passed: boolean;
|
||||
failures: ConformanceFailure[];
|
||||
/** "script" when scaffold-conformance.{ps1,sh} ran; "manual" when the fallback assertion table was used. */
|
||||
source: "script" | "manual";
|
||||
}
|
||||
|
||||
export interface ScaffoldManifest {
|
||||
/** Session id this manifest belongs to. */
|
||||
sessionId: string;
|
||||
/** UTC timestamp when scaffold completed. */
|
||||
scaffoldCompletedUtc: string;
|
||||
iacFormat: IacFormat;
|
||||
/** Deployment scope for the generated IaC (deploy preflight branches on this). */
|
||||
targetScope: "subscription" | "resourceGroup";
|
||||
files: ScaffoldFile[];
|
||||
/** Entry-point IaC file, e.g. "infra/main.bicep". */
|
||||
entryPoint?: string;
|
||||
/** Parameters file, e.g. "infra/main.parameters.json". */
|
||||
parametersFile?: string;
|
||||
/** Exact command the deploy phase runs to provision (read by deploy preflight). */
|
||||
deployCommand: string;
|
||||
/** Container Apps two-phase (infra, then image) wiring, when applicable. */
|
||||
twoPhaseWiring?: boolean;
|
||||
/** Ordered phase-2 steps (e.g. image build/push) for two-phase deploys. */
|
||||
phase2Steps?: readonly string[];
|
||||
selfReview: {
|
||||
findings: SelfReviewFinding[];
|
||||
healingAttempts?: readonly ScaffoldHealingAttempt[];
|
||||
};
|
||||
validationResult?: ValidationResult;
|
||||
conformance?: ConformanceResult;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<!-- 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.
|
||||
|
||||
# Self-Healing Loop — Error Classification & Auto-Fix
|
||||
|
||||
Step 11 runs validation against the generated IaC via CLI commands (`az bicep build` + `az deployment sub what-if`). On failure, classify each error and apply the strategy below. Max 3 attempts before pausing to present a diagnosis (explain pattern → propose fix → ask user). After user approves, 5 more attempts before asking again — then every 5 thereafter. See scaffold instructions.md § Self-Healing Loop for the full escalation protocol.
|
||||
|
||||
| Error Type | Class | Auto-Fix Strategy |
|
||||
|------------|-------|-------------------|
|
||||
| Invalid property name | FIXABLE | Replace with correct property from schema summary |
|
||||
| Syntax error (HCL/Bicep) | FIXABLE | Re-generate affected module from reference patterns |
|
||||
| Missing required property | FIXABLE | Add with default value from MCP best practices |
|
||||
| Wrong API version | FIXABLE | Update to version from schema result |
|
||||
| Provider version conflict | FIXABLE | Update `required_providers` block |
|
||||
| Undeclared variable | FIXABLE | Add declaration to `variables.tf` |
|
||||
| Policy-blocked SKU | FIXABLE | Substitute with next-best from `rejectedAlternatives[]` |
|
||||
| Circular dependency | FIXABLE | Refactor module references — break cycle |
|
||||
| Permission/RBAC insufficient | BLOCKING | Surface required role + `az role assignment create` command |
|
||||
| State backend inaccessible | BLOCKING | Surface `az storage account create` instructions |
|
||||
| Region unsupported for resource | BLOCKING | Suggest alternate regions — requires user decision |
|
||||
| Quota exhaustion (ALL tiers in ALL regions) | PLAN_LEVEL_CHANGE | ⛔ Service type pivot required — see scaffold instructions.md § Self-Healing Loop. Update `prepare-plan.json` → present re-approval gate → regenerate IaC. Counts as 1 healing attempt |
|
||||
| Quota exhaustion (single region) | PLAN_LEVEL_CHANGE | ⛔ Region pivot required — read `prepare-plan.json.quotaValidation.checkedRegions` and `failedResources` to skip already-failed regions. After checking new regions, append results back to these fields. See scaffold instructions.md § Self-Healing Loop. Update plan region → present re-approval → regenerate IaC |
|
||||
| Policy blocks planned service entirely | PLAN_LEVEL_CHANGE | ⛔ Alternative service required — see scaffold instructions.md § Self-Healing Loop. Map to next-best from `rejectedAlternatives[]` → present re-approval → regenerate IaC |
|
||||
@@ -0,0 +1,132 @@
|
||||
<!-- 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.
|
||||
|
||||
# Self-Review Checklist
|
||||
|
||||
4-layer adversarial review of generated IaC. Run after scaffold generates all files, before `scaffold-manifest.json`.
|
||||
|
||||
## Rating System
|
||||
|
||||
- **VERIFIED** — confirmed correct by inspecting the generated code
|
||||
- **PLAUSIBLE** — likely correct but cannot fully verify (e.g., API version exists but not checked against registry)
|
||||
- **FLAGGED** — incorrect, missing, or contradicts the plan/patterns
|
||||
|
||||
**Consume results:** If any finding is FLAGGED → fix the IaC, then re-run validation. If all VERIFIED/PLAUSIBLE → proceed. Write findings to `scaffold-manifest.json.selfReview`.
|
||||
|
||||
> ⛔ **Halt on critical failures** — if any finding is FLAGGED at L1 (Security) or L3 (Hallucination), do NOT proceed to deploy. Present findings and ask: **"Fix / Continue with risks / Cancel"**.
|
||||
|
||||
> ⛔ **Step 12 validation remains mandatory** regardless of self-review results — IaC may change during FLAGGED fixes.
|
||||
|
||||
## Layer 1 — Security Claims Extraction
|
||||
|
||||
Extract every security claim from the generated IaC and check for internal contradictions.
|
||||
|
||||
| Check | Example |
|
||||
|-------|---------|
|
||||
| Managed identity declared but secret hardcoded | `identity: { type: 'SystemAssigned' }` but `password: 'hardcoded'` in same file |
|
||||
| HTTPS enforced but HTTP endpoint exposed | `httpsOnly: true` but ingress allows HTTP |
|
||||
| Resource accessed via managed identity but matching role not granted | ACR image pull without AcrPull, Storage access without a Storage Data role, or any MI→resource dependency missing its `roleAssignment` — access fails at runtime → `FLAGGED` |
|
||||
| ⛔ Role assignment scope targets wrong resource | `scope: resourceGroup()` on resource-specific roles → `FLAGGED`. Must scope to specific resource. |
|
||||
| `principalType` missing on role assignments | Causes intermittent 30s+ delays |
|
||||
| ⛔ Identity block missing on compute resource | ⛔ **MANDATORY FAIL** — ALL compute MUST have `identity: { type: 'SystemAssigned' }`. There is NO exception: managed identity is required on every SKU (the compute floor is B1, which supports MI; F1/D1/Free are never generated). Missing identity → `FLAGGED`. |
|
||||
| SQL firewall `0.0.0.0/0` without private endpoint | Prefer MI + private endpoint. AllowAzureServices genuinely needed → `PLAUSIBLE`. |
|
||||
| ⛔ SCM/FTP auth policy missing on App Service | ALL App Service (and Premium/EP1 Functions) MUST have `basicPublishingCredentialsPolicies`: `scm.allow: true`, `ftp.allow: false`. Missing → `FLAGGED`. ⛔ **Functions on Flex Consumption must NOT have these policies** — flag their presence instead. |
|
||||
| ⛔ Any `Microsoft.KeyVault/vaults`, `@Microsoft.KeyVault(...)`, or `keyVaultUrl` present | ⛔ **MANDATORY FAIL** — no Key Vault is created; app-internal secrets live on the compute resource. → ⛔ **FLAGGED** |
|
||||
|
||||
**Rating:** Claims that contradict each other → `FLAGGED`. Consistent claims → `VERIFIED`.
|
||||
|
||||
## Layer 2 — Pattern Validation
|
||||
|
||||
Validate against pattern files loaded at Steps 3–5 and [rbac-roles.md](rbac-roles.md).
|
||||
|
||||
### Bicep
|
||||
|
||||
| Check | Source |
|
||||
|-------|--------|
|
||||
| File structure: `main.bicep` → `modules/*.bicep` | `bicep-patterns.md` |
|
||||
| `main.parameters.json` uses ARM JSON (not `.bicepparam`) | `bicep-patterns.md` |
|
||||
| Naming: `{prefix}{name}{token}` ≤32 chars | `bicep-patterns.md` |
|
||||
| System-assigned managed identity on all services | `bicep-patterns-security.md` |
|
||||
| No `administratorLogin`/`administratorLoginPassword`/access key on ANY data service (SQL, PG, MySQL, Redis, Storage, Cosmos) — Entra/MI-only | `bicep-patterns-security.md` § Data Services |
|
||||
| PG/MySQL set an Entra admin child resource; PG `authConfig.passwordAuth: 'Disabled'` | `bicep-patterns-data.md` |
|
||||
| No F1/D1 App Service SKU, no Free Static Web Apps tier | `sku-matrix.md` |
|
||||
| ⛔ No `Microsoft.KeyVault/vaults` resource | No Key Vault is created — app-internal secrets are stored on the compute resource → any KV resource is ⛔ **FLAGGED** | `bicep-patterns-security.md` § No Key Vault |
|
||||
| App-internal secrets passed as `@secure()` params → App Service `appSettings` / CA native `secrets` | `bicep-patterns-security.md` § Secrets |
|
||||
| Prereq `warnings[]` each have a corresponding IaC fix | Read [`env-var-secrets.md`](env-var-secrets.md) for SSL/TLS fixes |
|
||||
| Detected Azure Functions topology preserved | Every Azure Functions component in `context.json.components[]` has a planned Azure Functions service and a `Microsoft.Web/sites` Function App resource. If an SWA frontend is also present, no `buildProperties.apiLocation`, SWA-managed API source copy, or `azureStaticWebApps` backend auth/runtime setting appears. Missing or merged Function App → `FLAGGED`. |
|
||||
| Container Apps: two-phase ACR wiring, `registries` populated when ACR in plan, port alignment | `bicep-container-apps.md` |
|
||||
| ⛔ BuildKit Dockerfile without `Dockerfile.azure` | `hasBuildKitSyntax == true` but no `Dockerfile.azure` in `files[]` → ⛔ **MANDATORY FAIL**. ACR does not support BuildKit. |
|
||||
| ⛔ Role assignment `scope` targets specific resource, not `resourceGroup()` | `rbac-roles.md` |
|
||||
| ⛔ No `azure.yaml` in `scaffold-manifest.json.files[]` | `pipeline-rules.md` |
|
||||
| Non-Azure TF in separate dir | `terraform-patterns.md` |
|
||||
|
||||
### Cross-Module Reference Validation
|
||||
|
||||
Trace references BETWEEN modules — per-file checks miss broken cross-module wiring.
|
||||
|
||||
| Check | Rating |
|
||||
|-------|--------|
|
||||
| **Param wiring** — every `module` call in `main.bicep`: verify every param without `= default` is passed | Missing param → `FLAGGED` |
|
||||
| **Secret ref completeness** — every CA `secretRef` has a matching entry in the CA's native `secrets[]` (name + `value`); ⛔ NO `keyVaultUrl` / Key Vault | Missing native secret or any `keyVaultUrl` → `FLAGGED` |
|
||||
| **Output ref validity** — every `moduleRef.outputs.X` is declared in the referenced module | Missing output → `FLAGGED` |
|
||||
|
||||
### Terraform
|
||||
|
||||
| Check | Source |
|
||||
|-------|--------|
|
||||
| File structure: `main.tf`, `variables.tf`, `outputs.tf`, `backend.tf`, `modules/` | `mcp_azure_mcp_azureterraformbestpractices` |
|
||||
| Provider: `azurerm ~> 4.0` | Terraform registry |
|
||||
| System-assigned managed identity, no `administrator_login`, no Key Vault (`shared_access_key_enabled=false` etc.) | `terraform-patterns.md` |
|
||||
| Container Apps: two-phase ACR wiring | Same pattern as Bicep |
|
||||
|
||||
**Rating:** Matches → `VERIFIED`. Reasonable deviation → `PLAUSIBLE`. Violates → `FLAGGED`.
|
||||
|
||||
## Layer 3 — Hallucination Detection
|
||||
|
||||
Catch fabricated resource types, API versions, SKU names, or properties.
|
||||
|
||||
### Bicep (default path)
|
||||
|
||||
| Check | How |
|
||||
|-------|-----|
|
||||
| API versions, resource types, property names valid | `bicep build` — errors = `FLAGGED` |
|
||||
| ⛔ Deploy-time value validity (`bicep build` blind spot) | The compiler accepts any schema-valid string, but ARM rejects wrong enum-like values (engine versions, region-restricted SKUs), wrong resource `scope`, and empty resource-ID properties. Any such value the generator chose from memory — not traceable to `prepare-plan.json` or a pattern file — MUST be confirmed against the provider capabilities API; unconfirmed → `FLAGGED`. |
|
||||
| SKU names match `prepare-plan.json` | Cross-reference `services[].sku` |
|
||||
| OpenAI deployment uses `scaleSettings` instead of `sku` | `scaleSettings` deprecated → `FLAGGED` |
|
||||
| Any resource uses `-preview` API version | Use latest GA from MCP tool → `FLAGGED` |
|
||||
| VNet subnets as separate child resources | Must be inline in `properties.subnets[]` → `FLAGGED` |
|
||||
|
||||
Run `bicep build main.bicep --stdout > /dev/null` as syntax + schema validation. Parse errors = `FLAGGED`.
|
||||
|
||||
> ⛔ **`az bicep build` is MANDATORY for L3.** If unavailable, write `FLAGGED` with "bicep build unavailable."
|
||||
|
||||
> ⛔ **Verify `main.bicep` has `targetScope = 'subscription'`.** Missing → FLAGGED (FIXABLE — add targetScope, RG resource with tags, `scope: rg` on modules).
|
||||
|
||||
> `mcp_bicep_build_bicep` + `az deployment sub what-if` also appropriate at L3. ⛔ Do NOT use `az deployment sub validate` (known bug).
|
||||
|
||||
**Rating:** Passes `bicep build` → `VERIFIED`. Build warning → `PLAUSIBLE`. Build error → `FLAGGED`.
|
||||
|
||||
### Terraform (alternative path)
|
||||
|
||||
Run `terraform init -backend=false && terraform validate` + `terraform plan -detailed-exitcode`. Same rating criteria as Bicep.
|
||||
|
||||
**Rating:** Passes validate + plan → `VERIFIED`. Plan warning → `PLAUSIBLE`. Validate/plan error → `FLAGGED`.
|
||||
|
||||
## Layer 4 — WAF Alignment
|
||||
|
||||
Per-pillar spot check against [Azure Well-Architected Framework](https://learn.microsoft.com/en-us/azure/well-architected/).
|
||||
|
||||
| Pillar | Check |
|
||||
|--------|-------|
|
||||
| Reliability | Health probes configured. ⛔ Verify probe path matches `prereq-output.json.healthEndpoint` — non-existent or mismatched path = `FLAGGED`. ⛔ ACA probes do NOT follow HTTP redirects — if app has trailing-slash normalization (Express `redirect`), use path WITHOUT trailing `/` (e.g., `/app` not `/app/`). |
|
||||
| Security | No public blob access, TLS 1.2+, managed identity |
|
||||
| Cost | SKU matches budget tier from `prepare-plan.json` |
|
||||
| Ops | App Insights present + connected for APM, 5 AppOnboard tags on resources, all values parameterized. Present = `VERIFIED`. App Insights absent = `PLAUSIBLE`. ⛔ `diagnostic-settings` is not part of the plan — if the generator added one it MUST be gated (`if (enableDiagnostics)`, default `false`) or absent; wired UNCONDITIONALLY in `main.bicep` = `FLAGGED` (blocks first deploy). |
|
||||
| Performance | Autoscale rules present for production SKUs |
|
||||
| Reliability | ⛔ Env var values compatible with app config validation (Pydantic `Settings`, Django `settings.py`). Typed fields reject wrong formats → `FLAGGED` |
|
||||
|
||||
**Rating:** Pillar addressed → `VERIFIED`. Not applicable for SKU → `PLAUSIBLE`. Missing for production SKU → `FLAGGED`.
|
||||
|
||||
## Output
|
||||
|
||||
Write findings to `scaffold-manifest.json.selfReview.findings[]`: `{ "layer": "L1", "claim": "...", "rating": "FLAGGED", "detail": "..." }` (layer is one of `"L1"`|`"L2"`|`"L3"`|`"L4"`). All `FLAGGED` must be resolved or surfaced at deploy gate. `PLAUSIBLE` = informational.
|
||||
@@ -0,0 +1,28 @@
|
||||
<!-- 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.
|
||||
|
||||
# Self-Review Procedure — Step 9
|
||||
|
||||
Adversarial self-review using a sub-agent to perform L1–L4 review of generated IaC.
|
||||
|
||||
## Sub-Agent Setup
|
||||
|
||||
Use a sub-agent to perform the review. Provide:
|
||||
- All generated IaC file contents (every .bicep or .tf file from Step 5)
|
||||
- The `prepare-plan.json` services/naming/deploymentVariables sections
|
||||
- The `scaffold-manifest.json.files[]` list
|
||||
- The full content of [self-review-checklist.md](self-review-checklist.md) AND [waf-checklist.md](waf-checklist.md) verbatim
|
||||
|
||||
## Sub-Agent Prompt
|
||||
|
||||
> "Follow the self-review-checklist.md procedures for EACH of L1–L4. Rate each finding as VERIFIED | PLAUSIBLE | FLAGGED. Check: L1 Security (RBAC scope, network rules, managed identity, Key Vault — check contradictions between IaC and plan), L2 Pattern (anti-patterns, missing supporting resources — verify every file in scaffold-manifest.json.files[] exists on disk and is non-empty, FLAGGED if any missing or empty), L3 Hallucination (resource names match prepare-plan.json.naming exactly, API versions are real, SKU names match plan, no invented resource types), L4 WAF (use waf-checklist.md — Reliability, Security, Cost, Ops, Performance per-service checks). Do not fabricate results — check each claim against the actual IaC content provided. Return: { findings: [{ layer: 'L1'|'L2'|'L3'|'L4', claim: '...', rating: 'VERIFIED'|'PLAUSIBLE'|'FLAGGED', detail: '...' }], summary: 'N/N VERIFIED, N PLAUSIBLE, N FLAGGED' }. ≤1000 tokens."
|
||||
|
||||
## Consume Results
|
||||
|
||||
- If any finding is FLAGGED → fix the IaC, then re-run validation (`az bicep build`, `az deployment sub what-if`)
|
||||
- If all VERIFIED/PLAUSIBLE → proceed to Step 10
|
||||
- Write findings to `scaffold-manifest.json.selfReview`
|
||||
|
||||
> ⛔ **Self-review is COMPLETE after L1–L4.** L3 may use `mcp_bicep_get_bicep_file_diagnostics`, `az bicep build`, or `az deployment sub what-if` — all are appropriate for catching errors early. **Step 12 remains mandatory regardless of what self-review found** — IaC may change between Steps 9–12 (FLAGGED fixes), and Step 12 writes the contractual `validationResult` to the manifest.
|
||||
|
||||
> ⛔ **Halt on critical self-review failures** — if any selfReview finding is FLAGGED at L1 (Security) or L3 (Hallucination), do NOT proceed to deploy. Present findings and ask: **"Fix / Continue with risks / Cancel"**.
|
||||
@@ -0,0 +1,103 @@
|
||||
<!-- 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 — IaC Generation (Steps 5–8)
|
||||
|
||||
Generate deployment-ready IaC from `prepare-plan.json`. Follow the workflow below — each step specifies which reference to read and what to do with it.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- ⛔ **Do NOT invoke any other agents or hand off** — no external agent calls of any kind. Use the procedures in THIS file only.
|
||||
- ⛔ **Do NOT generate `azure.yaml`**
|
||||
- ⛔ **Do NOT modify app source code** — only write files under `infra/` (and `Dockerfile.azure` if needed)
|
||||
- ⛔ **Do NOT run app build/test/lint commands** (`npm test`, `npm run build`, `pnpm build`, `python -m pytest`, `dotnet build`, etc.). Only validate generated IaC via `az bicep build`.
|
||||
|
||||
## Input (provided by caller)
|
||||
|
||||
| Field | Source | Required |
|
||||
|-------|--------|----------|
|
||||
| `prepare-plan.json` content | Session folder — services, naming, quotas, cost, deploymentVariables | YES (verbatim) |
|
||||
| `context.json.overrides` | `iacFormat`, `detectedInfraProvider` | YES |
|
||||
| `buildRequirements` | From `prereq-output.json` — runtime, deps, Dockerfiles | YES |
|
||||
| `warnings[]` | From `prereq-output.json` — prereq warnings requiring IaC fixes (env var overrides, config changes). Applied during Steps 3–4. | YES |
|
||||
| Compute targets | App Service/Functions, Container Apps, or both + whether PostgreSQL/Redis present | YES |
|
||||
| `apiVersions` | Map of `resourceType → latestGAVersion` from main-thread MCP lookup. Use these versions in generated Bicep — do NOT use versions from training data. If `"MCP unavailable"` — see Step 1 for fallback. | YES |
|
||||
|
||||
## Output
|
||||
|
||||
| Artifact | Location |
|
||||
|----------|----------|
|
||||
| `infra/main.bicep` (or `main.tf`) | Workspace `infra/` |
|
||||
| `infra/main.parameters.json` (or `variables.tf`) | Workspace `infra/` |
|
||||
| `infra/modules/{service}.bicep` per service | Workspace `infra/modules/` |
|
||||
| File list | Return to caller for `scaffold-manifest.json.files[]` |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1 — Read skeleton + tag patterns
|
||||
|
||||
Read [bicep-patterns.md](bicep-patterns.md) (Bicep) OR [terraform-patterns.md](terraform-patterns.md) (Terraform) — NOT both.
|
||||
|
||||
**Do:** Extract the `main.bicep` skeleton structure (targetScope, parameters, variables, resource group, module calls). Extract the 5-tag block definition. Use `prepare-plan.json.naming` for all resource names — never derive names with `take()`, `substring()`, `uniqueString()`, or string manipulation. The 4-char session suffix in the plan names already provides uniqueness. ⛔ For each `resource 'Type@Version'` declaration, use the version from `apiVersions` input. If type missing from map, use version from reference file examples.
|
||||
|
||||
> ⛔ **If `apiVersions` is `"MCP unavailable"` or missing a resource type:** run `az provider show --namespace {ns} --query "resourceTypes[?resourceType=='{type}'].apiVersions[?!contains(@, 'preview')] | [0][0]" -o tsv` for each missing provider — this filters to GA-only and picks the latest. NEVER fall back to training data — hallucinated API versions cause multiple deploy healing cycles.
|
||||
|
||||
### Step 2 — Read compute-target patterns
|
||||
|
||||
Read ONLY the compute-target reference(s) matching the plan, if the plan has multiple compute targets, read each matching file.:
|
||||
- If plan has App Service/Functions → read [bicep-app-service.md](bicep-app-service.md).
|
||||
- If plan has Container Apps → read [bicep-container-apps.md](bicep-container-apps.md).
|
||||
- If plan has Static Web Apps → read [bicep-swa.md](bicep-swa.md).
|
||||
- If plan has BOTH → read both.
|
||||
|
||||
**Do:** Generate compute module(s) using the patterns from each reference file. F1/D1 App Service: do NOT generate Dockerfile, do NOT add managed identity (OOM). App Service health probe: if `prereq-output.json.healthEndpoint` is non-null, set `siteConfig.healthCheckPath` to that value; otherwise omit (do NOT default to `/`).
|
||||
|
||||
### Step 3 — Read security patterns
|
||||
|
||||
⛔ **You MUST read [bicep-patterns-security.md](bicep-patterns-security.md).** It contains Key Vault config, managed identity, HTTPS/TLS, and credential hygiene rules. Apply to every generated module.
|
||||
|
||||
### Step 4 — Read generation rules
|
||||
|
||||
Read [iac-generation-rules.md](iac-generation-rules.md).
|
||||
|
||||
**Do:** Apply ALL rules from the reference file to every generated module. The file contains mandatory tag definitions, naming constraints, security patterns, env var completeness checks, and Dockerfile generation rules. Do NOT skip any section — every rule applies.
|
||||
|
||||
### Step 5 — Read env var + secrets wiring
|
||||
|
||||
Read [env-var-secrets.md](env-var-secrets.md).
|
||||
|
||||
**Do:** For each component in the plan, read `.env.example` (or `.env.sample`, config files like Pydantic `Settings`, Django `settings.py`) from the workspace. Map every env var to either: (1) a Bicep parameter, (2) an **`@secure()` param stored on the compute resource** (App Service `appSettings` / Container Apps native `secrets`) for app-internal secrets that are NOT Azure resource credentials (e.g. `SECRET_KEY`, JWT key), or (3) a value derived from other resources. ⛔ **No Key Vault.** ⛔ **Database/cache/storage access is managed-identity + token — never a connection-string password or access key.** Wire DB/cache **connection parameters** (host, db name, MI username, `sslmode=require`) as plain `appSettings`/`env` values. See [bicep-patterns-security.md](bicep-patterns-security.md) § Data Services and § No Key Vault.
|
||||
|
||||
### Step 6 — Generate data modules (if needed)
|
||||
|
||||
ONLY if PostgreSQL, MySQL, or Redis is in the plan. Skip if none are present.
|
||||
|
||||
**PostgreSQL Flexible Server module** — **Entra-only** (`authConfig: { activeDirectoryAuth: 'Enabled', passwordAuth: 'Disabled', tenantId }`). NO `administratorLogin`/`administratorLoginPassword`. Add an `administrators` child resource setting the **deploying principal** as Entra admin (pass `entraAdminObjectId`/`entraAdminName`/`entraAdminType`). Include the `AllowAllAzureServicesAndResourcesWithinAzureIps` (`0.0.0.0`) firewall rule, extension allow-list (`azure.extensions` config: `uuid-ossp,pgcrypto,pg_trgm`), storage config (default 32 GB). Set the server `version` from `prepare-plan.json.services[].version` (capabilities-verified) — do NOT hardcode or guess. The app MI is granted a DB role post-deploy (see [database-post-deploy.md](../../deploy/references/database-post-deploy.md)).
|
||||
|
||||
**MySQL Flexible Server module** — **Entra-only**: a **user-assigned managed identity (REQUIRED for MySQL AAD auth)** + `identity: { type: 'UserAssigned', … }` on the server + an `administrators` child resource (deploying principal) with `identityResourceId` set to that UAMI (⛔ `null` fails to deploy) + `aad_auth_only: ON` config. NO password params. Emit a `postDeployRecommendation` to grant the UAMI **Directory Readers** in Entra. Include the MySQL-only deltas: `require_secure_transport: ON` config, the server `version` from `prepare-plan.json.services[].version` (ARM rejects major-only strings like `'8.0'` — needs an exact patch such as `'8.0.21'`), and a `flexibleServers/databases` child resource for the compose-declared DB name (e.g. `MYSQLDB_DATABASE`) so the app's schema DB exists in IaC before the container starts. See [bicep-patterns-data.md § MySQL Flexible Server Module](bicep-patterns-data.md).
|
||||
|
||||
**Redis Cache module** — Basic SKU, `enableNonSslPort: false`, `minimumTlsVersion: '1.2'`, **`disableAccessKeyAuthentication: true`** + `redisConfiguration: { 'aad-enabled': 'true' }` + an `accessPolicyAssignments` child granting the app MI Data Owner. ⛔ **No access key in Key Vault.** ⛔ Known Bicep type issue: `sku` property may cause BCP035/BCP187 warnings — these are false positives. If deploy fails with `InvalidRequestBody` for `properties.sku.name`, create via `az redis create --sku Basic --vm-size c0` then reference with `existing` keyword in Bicep.
|
||||
|
||||
Wire DB/cache **connection parameters** (host, db name, MI username, SSL required) as plain `appSettings`/`env` values — NOT secrets. The driver fetches an Entra token at runtime. App-internal (non-Azure) secrets are stored on the compute resource via an `@secure()` param (App Service `appSettings` / Container Apps native `secrets`) — ⛔ **no Key Vault**.
|
||||
|
||||
### Step 7 — Generate all files
|
||||
|
||||
> ⛔ **Before writing ANY file, verify:** (1) ⛔ **NO `Microsoft.KeyVault/vaults` resource anywhere** — app-internal secrets live on the compute resource. (2) No secrets in module outputs — `@secure()` params flow in at deploy, never out. (3) API versions from `apiVersions` input, not memory. (4) Container Apps: no `revisionSuffix`, placeholder image as default, `isPlaceholder` conditionals on registries only (native secrets need no gating).
|
||||
|
||||
**Do:** Create the `infra/` directory and write all files:
|
||||
1. `infra/bicepconfig.json` — write `{ "formatting": { "newlineKind": "LF" } }` if it doesn't already exist (user's repo may have one). LF is critical because Bicep triple-quoted strings pass content literally to ARM, and `\r` bytes crash `/bin/sh` in containers.
|
||||
2. `infra/main.bicep` — subscription scope, RG creation with tags, module calls for all services + `role-assignments` module (AcrPull + app-to-resource data-plane RBAC — NO Key Vault roles), all unconditional.
|
||||
3. `infra/main.parameters.json` — ARM JSON format (NOT `.bicepparam`). Include `environmentName`, `location`, `sessionId`, `deployedBy`, `createdAt`, and `entraAdminObjectId`/`entraAdminName` (Entra DB admin = deployer). ⛔ **`createdAt` value:** run `Get-Date -Format "o"` in terminal to get the current ISO 8601 timestamp — NEVER use a hardcoded or placeholder date. Do NOT include `@secure()` app-internal secret params (passed at deploy time). Deploy phase passes the deployer object id via `az ad signed-in-user show --query id -o tsv`.
|
||||
4. `infra/modules/{service}.bicep` — one module per service from the plan, PLUS `role-assignments.bicep` (AcrPull for Container Apps → ACR, and app-MI → resource data-plane roles per [rbac-roles.md](rbac-roles.md)). ⛔ **No Key Vault module and no Key Vault role assignments.**
|
||||
5. If `buildRequirements.hasBuildKitSyntax == true`: ⛔ create `{component}/Dockerfile.azure` per [dockerfile-generation.md § ACR Build Compatibility](dockerfile-generation.md).
|
||||
6. If Container Apps and component has NO Dockerfile: read [dockerfile-generation.md](dockerfile-generation.md) and generate one. Follow the layer ordering, port alignment, and security defaults from that reference — do NOT generate from memory.
|
||||
|
||||
> ⛔ **Health probes for Container Apps:** Probe path priority: (1) `prereq-output.json.healthEndpoint` if non-null, (2) first detected GET route from the app, (3) `/` only if the app has a root handler. Do NOT default to `/` for APIs that only serve sub-paths — returns 404, blocks activation. For DB apps: use `/healthz` not `/readyz` (DB not wired in Phase 1).
|
||||
|
||||
### Step 8 — Validate syntax
|
||||
|
||||
**Do:** Run `az bicep build --file infra/main.bicep --stdout > $null`. If errors, fix and re-run (max 2 attempts). Do NOT use the `azure-validate` agent.
|
||||
|
||||
### Step 9 — Return results
|
||||
|
||||
**Do:** Return the list of generated files and any validation notes to the caller. Keep status report ≤1500 tokens.
|
||||
@@ -0,0 +1,59 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Subagent Template — Security + Adversarial Review (Steps 6–9)
|
||||
|
||||
Review generated IaC for security compliance and correctness. Follow the workflow below — each step specifies which reference to read and what to check.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- ⛔ **Do NOT invoke any other agents or hand off** — no external agent calls of any kind. Use the procedures in THIS file only.
|
||||
- ⛔ **Do NOT run `az deployment` commands** — review is read-only analysis of generated files.
|
||||
- ⛔ **Do NOT modify IaC files** — report findings only. The caller fixes issues.
|
||||
|
||||
## Input (provided by caller)
|
||||
|
||||
| Field | Required |
|
||||
|-------|----------|
|
||||
| All generated IaC file contents (every `.bicep` or `.tf` file) | YES |
|
||||
| `prepare-plan.json` — services (service types, SKUs), naming, deploymentVariables sections | YES |
|
||||
| `scaffold-manifest.json.files[]` list | YES |
|
||||
| `prereq-output.json.warnings[]` — all prereq warnings that require IaC fixes | YES |
|
||||
|
||||
## Output
|
||||
|
||||
Return JSON (≤1000 tokens):
|
||||
```json
|
||||
{
|
||||
"findings": [
|
||||
{ "layer": "L1|L2|L3|L4", "file": "modules/app.bicep", "claim": "...", "rating": "VERIFIED|PLAUSIBLE|FLAGGED", "detail": "..." }
|
||||
],
|
||||
"summary": "N/N VERIFIED, N PLAUSIBLE, N FLAGGED"
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1 — Read security patterns + run L1 security baseline
|
||||
|
||||
Read [bicep-patterns-security.md](bicep-patterns-security.md) and [rbac-roles.md](rbac-roles.md).
|
||||
|
||||
**Do:** Check every generated IaC file against ALL security checks defined in the reference file. The file contains the complete check table with FLAGGED conditions, edge cases, and Bicep code patterns. Do NOT guess checks from memory — use the reference file as the checklist.
|
||||
|
||||
### Step 2 — Read checklist + run L2–L4 adversarial review
|
||||
|
||||
Read [self-review-checklist.md](self-review-checklist.md).
|
||||
|
||||
**Do:** First run the **cross-module reference trace** from the checklist's § Cross-Module Reference Validation: parse every `module` call in `main.bicep`, read each target module's `param`/`output` declarations and `secrets[]` entries, then verify every reference resolves (params passed match params declared, outputs referenced exist, every CA `secretRef` has a matching native `secrets[]` entry — ⛔ NO `keyVaultUrl` / Key Vault). Then run L2–L4:
|
||||
- **L2 (Pattern Validation):** File structure matches `main.bicep` → `modules/*.bicep`, naming follows plan, Container Apps uses two-phase wiring, every `files[]` entry exists on disk, no `azure.yaml`, cross-module references all resolve
|
||||
- **L3 (Hallucination Detection):** Resource names match `naming.resources[]` exactly, API versions are real (verify via `az bicep build`), SKU names match plan, no invented resource types
|
||||
- **L4 (WAF Alignment):** Check per-pillar:
|
||||
- Reliability: zone redundancy (prod SKUs), health probes, GRS storage, min replicas ≥1
|
||||
- Security: managed identity, on-compute app-internal secrets (no Key Vault), HTTPS+TLS 1.2, no public blob, no `administratorLogin`
|
||||
- Cost: SKU matches budget, scale-to-zero for dev/test CA, free grants applied
|
||||
- Ops: App Insights, 5 AppOnboard tags, all values parameterized
|
||||
- Performance: autoscale (prod), CDN for SPA, connection pooling, cache tier
|
||||
|
||||
### Step 3 — Compile findings + return
|
||||
|
||||
**Do:** Merge L1–L4 results into the findings JSON. Apply rating per [self-review-checklist.md](self-review-checklist.md) § Rating System: VERIFIED (evidence confirms claim), PLAUSIBLE (no counter-evidence but unverified), FLAGGED (evidence contradicts or missing critical pattern). ⛔ FLAGGED at L1 (Security) or L3 (Hallucination) → caller must fix before deploy. Return to caller.
|
||||
@@ -0,0 +1,104 @@
|
||||
<!-- azure-cor-disclaimer -->
|
||||
> **Important:** This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
|
||||
|
||||
# Subagent Template — Validation & Manifest (Steps 10–12)
|
||||
|
||||
Validate IaC syntax, write `scaffold-manifest.json`, and generate deploy checklist. Follow the workflow below.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
- ⛔ **Do NOT invoke any other agents or hand off** — no external agent calls of any kind. Use the procedures in THIS file only.
|
||||
- ⛔ **Do NOT create or modify Azure resources** — validation is syntax-only (`bicep build` / `terraform validate`), never `az deployment sub create`.
|
||||
- ⛔ **Do NOT run `what-if` or `terraform plan`** — deploy runs the mandatory what-if with real secret params. Scaffold validates syntax only.
|
||||
|
||||
## Input (provided by caller)
|
||||
|
||||
| Field | Required |
|
||||
|-------|----------|
|
||||
| IaC file paths (all generated `.bicep` or `.tf` files) | YES |
|
||||
| Self-review findings from Steps 6–9 | YES |
|
||||
| `prepare-plan.json` — services, naming, region, subscriptionId | YES |
|
||||
| `prereq-output.json.warnings[]` — prereq warnings with `fixPhase` | YES |
|
||||
| `prereq-output.json.healthEndpoint` — detected health path (or `null`) | YES |
|
||||
| Conformance result JSON (from main-thread Step 10a-conf) | YES |
|
||||
|
||||
## Output
|
||||
|
||||
| Artifact | Location |
|
||||
|----------|----------|
|
||||
| `scaffold-manifest.json` | Session folder |
|
||||
| `deploy-result.json` skeleton | Session folder |
|
||||
| Validation result status | Return to caller: `Validated` or `Failed` |
|
||||
| Deploy checklist | `.copilot-azure/sessions/{id}/deploy-checklist.md` |
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1 — Read validation + manifest rules
|
||||
|
||||
Read [validation-and-manifest.md](validation-and-manifest.md) and [scaffold-schemas.ts](scaffold-schemas.ts).
|
||||
|
||||
**Do:** Understand the `ScaffoldManifest` interface (field names, types, required fields) and the validation sequence.
|
||||
|
||||
### Step 2 — Format and validate IaC
|
||||
|
||||
**Do:**
|
||||
1. Run `az bicep build --file infra/main.bicep --stdout > $null` (Bicep) or `terraform validate` (TF). Process output for BCP errors and warnings. Record pass/fail.
|
||||
|
||||
### Step 3 — Check RBAC completeness
|
||||
|
||||
**Do:** Verify every managed identity ↔ resource pair in the IaC has a corresponding `Microsoft.Authorization/roleAssignments` resource with the correct role GUID. Cross-reference with the review findings from Steps 6–9 input.
|
||||
|
||||
### Step 3b — Azure runtime constraint check (Container Apps)
|
||||
|
||||
> Skip if no Container Apps in the plan.
|
||||
|
||||
For each Container App resource in the generated Bicep:
|
||||
|
||||
1. **cpu/memory combo** — verify `cpu` (must be type `string`) + `memory` is one of: `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`. FIXABLE: adjust to nearest valid combo (use smallest valid combo for sidecars/companions).
|
||||
2. **secretRef coverage** — every `secretRef` in container env vars must have a matching entry in `configuration.secrets[]` (native `name` + `value`). ⛔ **No `keyVaultUrl` / Key Vault** — a `keyVaultUrl` or any `Microsoft.KeyVault/vaults` resource is a BLOCK. Missing native secret entry → FIXABLE: add `{ name, value: <secureParam> }` to the CA's `secrets[]`.
|
||||
3. **probe path** — if `prereq-output.json.healthEndpoint` is non-null, verify `probePath` matches it. If null AND any `plainEnvVars` entry is named `BASE` or `PATH_PREFIX`, verify `probePath` starts with that value (not bare `/`). If null AND no BASE var: verify the app has a route handler for the probe path (check source entry point for `app.get('/')` or framework root handler) — bare `/` on a REST API with only sub-path routes (e.g., `/users`, `/messages`) returns 404 and blocks revision activation. FIXABLE: update `probePath` to a known GET endpoint from the app.
|
||||
|
||||
FIXABLE errors: fix the Bicep → re-run `az bicep build` → proceed to Step 3c.
|
||||
|
||||
### Step 3b2 — App Service security constraint check
|
||||
|
||||
> Skip if no App Service or Functions in the plan.
|
||||
|
||||
For each App Service resource, and each **Premium (EP1)** Functions resource, in the generated Bicep:
|
||||
|
||||
1. **basicPublishingCredentialsPolicies** — verify both child resources exist: `basicPublishingCredentialsPolicies/scm` (with `allow: true`) and `basicPublishingCredentialsPolicies/ftp` (with `allow: false`). Missing → FIXABLE: add the child resources per [bicep-patterns-security.md](bicep-patterns-security.md) § Publishing Credential Lockdown. ⛔ **Skip Flex Consumption function apps** (`kind: 'functionapp,linux'` with `functionAppConfig`) — they must NOT have these policies; if present, FIXABLE: remove them (see [bicep-functions-flex.md](bicep-functions-flex.md)).
|
||||
2. **uniqueString in naming** — verify the App Service name uses `uniqueString()` or a unique suffix (not a hardcoded literal). Hardcoded names cause global collisions. Missing → FIXABLE: wrap name with `uniqueString(resourceGroup().id)`.
|
||||
|
||||
FIXABLE errors: fix the Bicep → re-run `az bicep build` → proceed to Step 3c.
|
||||
|
||||
### Step 3c — Record plan conformance result
|
||||
|
||||
The main thread (instructions.md Step 10a-conf) already ran the conformance script and passed you its JSON. Record it in `scaffold-manifest.json.conformance` = `{ passed, failures, source: "script" }`. ⛔ Do NOT set `validationResult.status: "Validated"` while any BLOCK failure is unresolved.
|
||||
|
||||
**Fallback** (only if the caller passed NO result AND `infra/main.bicep` exists — the gate is Bicep-only, skip for Terraform): run `{scaffoldDir}/scripts/scaffold-conformance.ps1 -SessionPath "{sessionPath}" -InfraPath infra` (or `.sh` on bash) yourself, then record with `source: "script"`. Never hand-judge when a shell is available.
|
||||
|
||||
### Step 4 — Write scaffold-manifest.json
|
||||
|
||||
Read [scaffold-schemas.ts](scaffold-schemas.ts) for exact field names.
|
||||
|
||||
**Do:** Write `scaffold-manifest.json` to the session folder with: `sessionId`, `scaffoldCompletedUtc`, `iacFormat`, `targetScope`, `entryPoint`, `parametersFile`, `files[]`, `deployCommand`, `twoPhaseWiring` (if Container Apps), `phase2Steps` (if applicable), `selfReview` (from caller input), `validationResult` (from Steps 2–3). Use the exact field names from [scaffold-schemas.ts](scaffold-schemas.ts) § `ScaffoldManifest`.
|
||||
|
||||
### Step 5 — Handle failures (if any)
|
||||
|
||||
Read [scaffold-healing-rules.md](scaffold-healing-rules.md) ONLY if validation failed.
|
||||
|
||||
**Do:** Classify errors as FIXABLE or BLOCKING. FIXABLE: auto-fix IaC → re-validate (max 3 attempts before asking user). BLOCKING: surface to user and halt. PLAN_LEVEL_CHANGE: requires re-approval — do NOT auto-fix.
|
||||
|
||||
### Step 6 — Verify deploy checklist exists
|
||||
|
||||
**Do:** Check that `.copilot-azure/sessions/{id}/deploy-checklist.md` exists (written by the parallel checklist subagent at scaffold Step 5b). If missing, read [`deploy-checklist-template.md`](../../deploy/references/deploy-checklist-template.md) and write it now as a fallback — fill `{placeholders}` from `prepare-plan.json`, delete non-applicable sections. This file survives conversation compaction
|
||||
|
||||
### Step 7 — Create deploy-result.json skeleton (MANDATORY)
|
||||
|
||||
Read [`deploy-schemas.ts`](../../deploy/references/deploy-schemas.ts) — specifically the `DeployResult` interface.
|
||||
|
||||
**Do:** Create `.copilot-azure/sessions/{id}/deploy-result.json` conforming to the `DeployResult` interface. Populate fields from all session artifacts already written (`prepare-plan.json`, `context.json`, `scaffold-manifest.json`, `prereq-output.json`). Use sensible defaults for fields the deploy phase will fill later. The deploy main agent updates this file in-place at Step 8 with real values.
|
||||
|
||||
### Step 8 — Return results
|
||||
|
||||
**Do:** Return validation status (`Validated` or `Failed`) to the caller. Confirm deploy-checklist.md exists. Keep status report ≤500 tokens.
|
||||
@@ -0,0 +1,186 @@
|
||||
<!-- 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.
|
||||
|
||||
# Terraform Patterns
|
||||
|
||||
Alternative-path patterns for AppOnboard scaffold. Used when `.tf` files detected or user overrides `iacFormat`. Per-resource config comes from `mcp_azure_mcp_azureterraformbestpractices` at runtime — this file covers layout, provider, naming, state, tagging, and wiring.
|
||||
|
||||
## File Structure
|
||||
|
||||
### Default (greenfield or user override)
|
||||
|
||||
```
|
||||
infra/
|
||||
├── main.tf # Root module — provider, resource group, module calls
|
||||
├── variables.tf # Input variables (all configurable values)
|
||||
├── outputs.tf # Exported values (endpoints, resource IDs)
|
||||
├── backend.tf # State backend config (local default, Azure Storage for prod)
|
||||
├── terraform.tfvars # Default variable values (from prepare-plan.json)
|
||||
└── modules/
|
||||
├── app-service/
|
||||
│ ├── main.tf
|
||||
│ ├── variables.tf
|
||||
│ └── outputs.tf
|
||||
├── container-app/
|
||||
├── sql-database/
|
||||
├── log-analytics/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Non-Azure IaC coexistence (GCP/AWS TF already in repo)
|
||||
|
||||
When `detectedInfraProvider.terraform` is `"gcp"`, `"aws"`, or `"multi"` (without `azurerm`), write Azure TF to a **separate directory** from existing non-Azure TF. Never overwrite or modify existing IaC files.
|
||||
|
||||
**Output directory rule:** If existing TF is NOT at `infra/`, write to `infra/`. If existing TF IS at `infra/` (or any path containing `infra`), write to `infra-azure/`. Same module structure as default layout.
|
||||
|
||||
Each Azure service gets its own module. `main.tf` orchestrates resource group + module calls.
|
||||
|
||||
## Provider Configuration
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
required_version = ">= 1.5"
|
||||
|
||||
required_providers {
|
||||
azurerm = {
|
||||
source = "hashicorp/azurerm"
|
||||
version = "~> 4.0"
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = "~> 3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "azurerm" {
|
||||
features {}
|
||||
subscription_id = var.subscription_id
|
||||
resource_provider_registrations = "none"
|
||||
}
|
||||
```
|
||||
|
||||
> ⛔ Never pin to exact patch versions (e.g., `= 4.1.0`). Use `~> 4.0` to allow minor/patch updates. `azurerm` manages API versions internally — if a resource isn't available in `azurerm`, use `azapi_resource` with the latest stable ARM API version.
|
||||
|
||||
> **Conditional access (AADSTS530084):** `azurerm` provider re-requests tokens that violate device-binding policies. Fix: (1) switch to Bicep, or (2) use service principal auth (`ARM_CLIENT_ID` + `ARM_CLIENT_SECRET` + `ARM_TENANT_ID`).
|
||||
|
||||
## variables.tf
|
||||
|
||||
Required variables: `environment_name` (string, default "dev"), `location` (string, default "eastus"), `subscription_id` (string), `session_id` (string), `deployed_by` (string). All configurable values MUST be variables — no hardcoded regions, names, or SKUs.
|
||||
|
||||
## terraform.tfvars
|
||||
|
||||
Populate from `prepare-plan.json`: `environment_name`, `location`, `subscription_id`, `session_id`. See naming-patterns.md for naming convention.
|
||||
|
||||
## Backend
|
||||
|
||||
Local backend by default: `backend "local" { path = "terraform.tfstate" }`. Recommend Azure Storage backend in `postDeployRecommendations[]` for production.
|
||||
|
||||
## Resource Group
|
||||
|
||||
Use `rg-${var.project_name}-${var.environment_name}-${random_string.suffix.result}` with `tags = local.tags`. Suffix prevents collisions across AppOnboard sessions.
|
||||
|
||||
## Naming Convention
|
||||
|
||||
```hcl
|
||||
resource "random_string" "suffix" {
|
||||
length = 4
|
||||
special = false
|
||||
upper = false
|
||||
}
|
||||
|
||||
locals {
|
||||
# Pattern: {type}-{appname}-{env}-{suffix}
|
||||
app_name = "app-${var.environment_name}-${random_string.suffix.result}"
|
||||
sql_name = "sql-${var.environment_name}-${random_string.suffix.result}"
|
||||
# Storage/ACR: alphanumeric only, no hyphens
|
||||
storage_name = "st${replace(var.environment_name, "-", "")}${random_string.suffix.result}"
|
||||
acr_name = "cr${replace(var.environment_name, "-", "")}${random_string.suffix.result}"
|
||||
}
|
||||
```
|
||||
|
||||
Cross-reference naming with [prepare/references/naming-patterns.md](../../prepare/references/naming-patterns.md) — Terraform names must match `prepare-plan.json.naming.resources[]`.
|
||||
|
||||
## Resource Tags — Mandatory
|
||||
|
||||
Apply all 5 AppOnboard tags via `local.tags` — see [iac-generation-rules.md § Session Tags](iac-generation-rules.md) for tag names and values.
|
||||
|
||||
```hcl
|
||||
locals {
|
||||
tags = {
|
||||
"app-onboard-skill" = "true"
|
||||
"app-onboard-session-id" = var.session_id
|
||||
"created-at" = timestamp()
|
||||
"environment" = var.environment_name
|
||||
"deployed-by" = var.deployed_by
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ `timestamp()` changes on every plan. Add `lifecycle { ignore_changes = [tags["created-at"]] }` on every resource.
|
||||
|
||||
## Secrets — App-Internal Only, Stored On-Compute (No Key Vault)
|
||||
|
||||
Use `random_password` ONLY for app-internal secrets that are NOT an Azure resource credential (e.g. a Django `SECRET_KEY`, JWT signing key), and store the value **directly on the compute resource** as an app setting / container secret. ⛔ **No Key Vault.** Azure resource auth (database, cache, storage, Cosmos) is **managed-identity + token — never a generated password**.
|
||||
|
||||
```hcl
|
||||
resource "random_password" "app_secret_key" {
|
||||
length = 50
|
||||
special = true
|
||||
lifecycle { ignore_changes = [result] }
|
||||
}
|
||||
|
||||
# App Service — store directly as an app setting (no Key Vault):
|
||||
resource "azurerm_linux_web_app" "app" {
|
||||
# ...
|
||||
app_settings = {
|
||||
SECRET_KEY = random_password.app_secret_key.result
|
||||
}
|
||||
}
|
||||
# Container Apps — use a native `secret { name, value }` block + `env { secret_name = ... }`.
|
||||
```
|
||||
|
||||
> ⛔ NEVER use `random_string` for secrets — it is not marked `sensitive` in state. Always use `random_password`.
|
||||
> ⛔ **NEVER generate a database/cache/storage password or access key** (no `administrator_login_password`, no `random_password` for a DB). Those services use `azuread_authentication_only`/`password_auth_enabled = false`/`shared_access_key_enabled = false` + managed identity. See [bicep-patterns-security.md](bicep-patterns-security.md) § Data Services.
|
||||
|
||||
## Container Apps — Two-Phase Wiring
|
||||
|
||||
Same circular dependency as Bicep — see [bicep-container-apps.md](bicep-container-apps.md). Phase 1: placeholder image, no ACR refs. Phase 2: build + push, assign AcrPull, update via `az containerapp update --image` outside Terraform.
|
||||
|
||||
```hcl
|
||||
# Phase 1: Placeholder image
|
||||
resource "azurerm_container_app" "app" {
|
||||
template {
|
||||
container {
|
||||
name = "app"
|
||||
image = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"
|
||||
cpu = 0.25
|
||||
memory = "0.5Gi"
|
||||
}
|
||||
}
|
||||
identity {
|
||||
type = "SystemAssigned"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## outputs.tf
|
||||
|
||||
Export: `resource_group_name`, `app_url` (https://${hostname}), `resource_ids` (list of all deployed resource IDs for deploy-result.json).
|
||||
|
||||
## Security Defaults
|
||||
|
||||
Apply same security rules as Bicep — see [bicep-patterns-security.md](bicep-patterns-security.md). Terraform-specific syntax:
|
||||
|
||||
| Rule | Terraform HCL |
|
||||
|------|---------------|
|
||||
| Managed identity (MANDATORY — all compute) | `identity { type = "SystemAssigned" }` |
|
||||
| ⛔ No DB admin password (SQL/PG/MySQL) | SQL: `azuread_authentication_only = true`; PostgreSQL: `authentication { password_auth_enabled = false, active_directory_auth_enabled = true, tenant_id = ... }` + `azurerm_postgresql_flexible_server_active_directory_administrator`; MySQL: `azurerm_mysql_flexible_server_active_directory_administrator` + `aad_auth_only`. NEVER `administrator_login_password`. |
|
||||
| ⛔ No storage/cache keys | Storage: `shared_access_key_enabled = false`; Redis: `azurerm_redis_cache` with `access_keys_authentication_enabled = false` + `azurerm_redis_cache_access_policy_assignment` for the app MI |
|
||||
| ⛔ No Key Vault | Do NOT create `azurerm_key_vault` / `azurerm_key_vault_secret`. App-internal secrets go directly into `app_settings` (App Service) or a native container `secret` block |
|
||||
| App-internal secret (on-compute) | `app_settings = { SECRET_KEY = random_password.app_secret_key.result }` — no `@Microsoft.KeyVault(...)` |
|
||||
| HTTPS only | `https_only = true`, `minimum_tls_version = "1.2"` |
|
||||
| Storage | `https_traffic_only_enabled = true`, `allow_nested_items_to_be_public = false`, `min_tls_version = "TLS1_2"` |
|
||||
| ⛔ Cosmos DB RBAC | `azurerm_cosmosdb_sql_role_assignment`, NOT `azurerm_role_assignment` + `local_authentication_disabled = true` — see [rbac-roles.md](rbac-roles.md) |
|
||||
| RBAC assignments | `principal_type = "ServicePrincipal"` REQUIRED — see [rbac-roles.md](rbac-roles.md) |
|
||||
| SCM/FTP auth | `scm.allow: true` (scaffold), `ftp.allow: false` (always) — use `azapi_resource` |
|
||||
@@ -0,0 +1,89 @@
|
||||
<!-- 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, Manifest & Approval — Steps 10–12.5
|
||||
|
||||
## Step 10 — CI/CD
|
||||
|
||||
Do NOT auto-generate workflow files or create branches/PRs. Scaffold only writes IaC files. This step activates only when `context.json.repo.remote` is non-null AND user explicitly requests branch/PR creation. When `repo.remote` is absent or user declines, write IaC directly to working tree. If the user asks for CI/CD, or after deploy completes, suggest it as a follow-up: call `mcp_azure_mcp_deploy` → `deploy_pipeline_guidance_get` with `is-azd-project: false`, `pipeline-platform: "github-actions"`, `deploy-option: "provision-and-deploy"` and present the guidance for the user to apply.
|
||||
|
||||
## Step 11 — Validate Generated IaC
|
||||
|
||||
> ⛔ **Validation MUST happen BEFORE the manifest is written.** The manifest requires `validationResult` — you cannot write it without completing validation first. Do NOT write `scaffold-manifest.json` until validation has run.
|
||||
|
||||
> ⛔ Do NOT call other agents during scaffold/deploy — see [pipeline-rules.md](../../references/pipeline-rules.md).
|
||||
|
||||
Run these checks directly. All must pass.
|
||||
|
||||
**11a. Bicep compilation:**
|
||||
```powershell
|
||||
az bicep build --file infra/main.bicep --stdout > $null
|
||||
```
|
||||
(Bash: redirect to `/dev/null` instead of `$null`.) This discards the compiled ARM JSON; diagnostics still arrive on **stderr**, so read stderr — do not just check the exit code.
|
||||
|
||||
> ⛔ **Exit 0 does NOT mean the template is valid.** `az bicep build` exits 0 on `Warning BCP*` diagnostics, and those warnings describe a template that will not do what it says. Measured on bicep 0.46.1: a resource missing its required `location`, `sku` and `kind` compiles with **exit 0** and only `Warning BCP035`. An agent that trusts the exit code ships a resource with no SKU and reports it as validated.
|
||||
|
||||
**Pass criteria — all three must hold:**
|
||||
|
||||
| Signal | Verdict |
|
||||
|---|---|
|
||||
| Non-zero exit, or any `Error BCP*` | **Fail** — fix and retry |
|
||||
| `Warning BCP*` (e.g. `BCP035` missing required property, `BCP036` wrong property type) | **Fail** — fix and retry |
|
||||
| `BCP081` "does not have types available" | **Advisory only** — see below |
|
||||
| Linter warnings (`no-unused-params`, `prefer-interpolation`, …) | Pass — do not block |
|
||||
|
||||
`BCP081` is carved out because bicep ships a bundled type index that lags ARM, so it also fires on API versions that are real and current. It cannot distinguish an invented resource type from a merely newer one. Do not treat it as a failure, but do read it — if the resource type is genuinely misspelled, that is a real bug.
|
||||
|
||||
Fail: fix errors and retry.
|
||||
|
||||
**11b. Static RBAC review** — review generated Bicep for correct role assignments per [rbac-roles.md](rbac-roles.md). Every managed identity ↔ resource pair must have a `Microsoft.Authorization/roleAssignments` resource with the correct role GUID.
|
||||
|
||||
**11c. Write `validationResult`** — after all checks pass, write:
|
||||
```json
|
||||
{
|
||||
"validationResult": {
|
||||
"status": "Validated",
|
||||
"checks": [
|
||||
{ "name": "bicep build", "passed": true, "detail": "<the command you ran and what it reported>" },
|
||||
{ "name": "RBAC review", "passed": true, "detail": "<which identity/resource pairs you checked>" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> ⛔ **Field names are fixed — copy them exactly.** `validationResult` MUST match `ValidationResult` in [`scaffold-schemas.ts`](scaffold-schemas.ts): `status` + `checks[]`, where each check is `{ name, passed: boolean, detail?: string }`.
|
||||
> - `passed` is a **boolean** — not `"result": "PASS"`, not `"status"`.
|
||||
> - The array is `checks` — not `notes`, `warnings`, or `errors`.
|
||||
> - Do NOT use `ConformanceResult` (`{ passed, failures[], source }`) here. That is a **different type** for a different gate (the Step 3c plan-conformance script). `validationResult` is never shaped like it.
|
||||
>
|
||||
> ⛔ **`detail` must describe what you actually observed** — the command you ran and its real output. Do not copy the placeholder text above, and never write a `detail` claiming a command succeeded unless you ran it and it did.
|
||||
|
||||
**Terraform path:** Replace 11a with `terraform init -backend=false && terraform validate`.
|
||||
|
||||
- If validation finds FIXABLE errors: edit IaC → re-run self-review (L1–L4) → re-validate (max 3 attempts). ⛔ **You MUST read [scaffold-healing-rules.md](scaffold-healing-rules.md) before entering the healing loop** — it defines error classification (FIXABLE vs BLOCKING) and auto-fix strategies.
|
||||
- If BLOCKING errors remain after 3 attempts: surface to user and halt.
|
||||
|
||||
## Step 12 — Write `scaffold-manifest.json`
|
||||
|
||||
⛔ **You MUST read [`scaffold-schemas.ts`](scaffold-schemas.ts)** to get the exact `ScaffoldManifest` interface. Write to the session folder with ALL fields populated: `files[]`, `selfReview.findings[]`, AND `validationResult` (from Step 11). This is a single write — validation is already complete.
|
||||
|
||||
> ⛔ **Phase exit gate: `scaffold-manifest.json.validationResult` MUST NOT be null.** If validation ran: `{ status: 'Validated'/'Partial'/'Failed', checks: [{ name, passed, detail? }] }` (per `ValidationResult` in [`scaffold-schemas.ts`](scaffold-schemas.ts)). Null = incomplete scaffold.
|
||||
|
||||
**You MUST also update `context.json`** per `AppOnboardContext` in [`session-schemas.ts`](../../references/session-schemas.ts): append `"scaffold"` to `completedPhases`, set `currentPhase` to `"deploy"`, update `lastModifiedUtc`.
|
||||
|
||||
## Step 12.5 — Deploy Approval Gate
|
||||
|
||||
Present the user with: files generated, selfReview findings, **validation results** (pass/fail per check from Step 11), services + SKUs, secure-defaults applied. End with: **"Ready to deploy? (Yes / Run manually / Edit plan / Cancel)"** — do not continue until the user approves.
|
||||
|
||||
> ⛔ **Self-check before presenting the deploy gate.** Does `scaffold-manifest.json` contain a `validationResult` field with `status` set? If NO → you skipped Step 11. Go back and run validation. Do NOT present the deploy gate with `validationResult: null`.
|
||||
|
||||
> ⛔ **Quota gate — MANDATORY.** Read `prepare-plan.json.quotaValidation`. If `verified == false`, `method == "unverifiable"`, or `method` is not `"cli"` for quota-constrained services: read [`sku-quota-validation.md`](../../prepare/references/sku-quota-validation.md) § Deploy Gate Re-Validation and follow the procedure.
|
||||
> ⛔ Do NOT use `az vm list-usage`, `az appservice list-locations`, or `mcp_azure_mcp_quota` for quota checks — see Anti-Patterns in [`sku-quota-validation.md`](../../prepare/references/sku-quota-validation.md).
|
||||
|
||||
|
||||
> ⛔ **Azure service compatibility warnings.** Read `prereq-output.json.warnings[]` for any warnings with `fixPhase: "deploy-gate"`. Surface EACH at the deploy gate: "⚠️ Azure compatibility: {warning.summary}. Fix: {warning.fix}. Approve? (Yes / Skip / Cancel)". If the user skips, add to `postDeployRecommendations[]`.
|
||||
|
||||
> ⛔ **Phase exit — NOT complete until ALL done:**
|
||||
> 1. `scaffold-manifest.json` written with `files[]`, `selfReview.findings[]`, AND `validationResult`
|
||||
> 2. `context.json`: `"scaffold"` appended to `completedPhases`, `currentPhase` → `"deploy"`, `lastModifiedUtc` updated
|
||||
> 3. `deploy-checklist.md` exists in session folder — written by the parallel checklist subagent at scaffold Step 5b (before IaC gen completes). Verify it exists. If missing (subagent failed), write it now from [`deploy-checklist-template.md`](../../deploy/references/deploy-checklist-template.md) — fill in real values from `prepare-plan.json`, delete unrelated compute sections.
|
||||
@@ -0,0 +1,47 @@
|
||||
<!-- 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.
|
||||
|
||||
# WAF Checklist
|
||||
|
||||
Per-pillar Well-Architected Framework alignment for AppOnboard-generated infrastructure. Use during scaffold self-review (Layer 4) and prepare validation (WAF Alignment dimension).
|
||||
|
||||
> **Reference:** [Azure Well-Architected Framework](https://learn.microsoft.com/en-us/azure/well-architected/) — link, don't duplicate. See [WAF Service Guides](https://learn.microsoft.com/en-us/azure/well-architected/service-guides/) for per-service checklists.
|
||||
|
||||
## Reliability
|
||||
|
||||
- Zone redundancy enabled for production SKUs (App Service P1v3+, SQL Premium, Redis Premium)
|
||||
- Health probes configured (Container Apps liveness/readiness, App Service `/health`)
|
||||
- GRS storage for production data (Standard_GRS or RA-GRS)
|
||||
- Retry policies in application code for transient failures
|
||||
- Min replicas ≥ 1 for production Container Apps (no cold-start)
|
||||
|
||||
## Security
|
||||
|
||||
- System-assigned managed identity on all services
|
||||
- App-internal secrets stored on-compute (App Service app settings / CA native secrets) — ⛔ NO Key Vault; no inline/committed values
|
||||
- HTTPS-only + TLS 1.2+ on all endpoints
|
||||
- No public blob access on storage accounts
|
||||
- Private endpoints where budget allows (balanced/performance tiers)
|
||||
- No `administratorLogin`/password or access key on ANY data service (SQL, PostgreSQL, MySQL, Redis, Storage, Cosmos) — Entra / managed-identity-only auth
|
||||
- Compute floor B1 / SWA Standard — no F1/D1/Free (managed identity is mandatory)
|
||||
|
||||
## Cost Optimization
|
||||
|
||||
- SKU matches `prepare-plan.json` budget tier — no over-provisioning
|
||||
- Scale-to-zero enabled for dev/test Container Apps
|
||||
- Free tier grants applied in cost estimate (see [pricing-guide.md](../../prepare/references/pricing-guide.md) § Free Grants Summary)
|
||||
- Reserved instances noted as option for production (don't auto-apply)
|
||||
|
||||
## Operational Excellence
|
||||
|
||||
- `diagnostic-settings` is not scaffolded; if the generator added one it MUST be gated behind `enableDiagnostics` (default `false`) or absent — never wired unconditionally (that blocks the first deploy).
|
||||
- Application Insights connected for APM
|
||||
- Resource tagging: `app-onboard-skill`, `app-onboard-session-id`, `created-at` (see `bicep-patterns.md` § Service Tagging or `terraform-patterns.md` § Resource Tags)
|
||||
- All configurable values parameterized (no hardcoded regions, names, SKUs)
|
||||
|
||||
## Performance Efficiency
|
||||
|
||||
- Autoscale rules for production SKUs (Container Apps max replicas, App Service auto-scale)
|
||||
- CDN for static assets when SPA frontend detected
|
||||
- Connection pooling for database access
|
||||
- Appropriate cache tier (Redis) when session/cache pattern detected
|
||||
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See LICENSE.md in the project root for license information.
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
|
||||
# Scaffold -> Deploy conformance gate.
|
||||
# Checks generated IaC against the plan for the semantic defects `az bicep build`
|
||||
# cannot catch (valid Bicep, wrong values) — the class that causes deploy healing.
|
||||
#
|
||||
# Usage: scaffold-conformance.ps1 -SessionPath <.copilot-azure/sessions/{id}> -InfraPath <infra>
|
||||
# Output: JSON { passed, failures:[{id,detail,file}] } to stdout.
|
||||
# Exit: 0 = pass, 1 = one or more BLOCK failures.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)] [string]$SessionPath,
|
||||
[string]$InfraPath = 'infra'
|
||||
)
|
||||
|
||||
$failures = New-Object System.Collections.Generic.List[object]
|
||||
function Add-Fail($id, $detail, $file) {
|
||||
$failures.Add([ordered]@{ id = $id; detail = $detail; file = $file })
|
||||
}
|
||||
function Read-Json($p) {
|
||||
if (Test-Path $p) { try { return (Get-Content $p -Raw | ConvertFrom-Json) } catch { return $null } }
|
||||
return $null
|
||||
}
|
||||
|
||||
# --- Load artifacts (a missing input skips only the checks that need it) ---
|
||||
$plan = Read-Json (Join-Path $SessionPath 'prepare-plan.json')
|
||||
|
||||
$mainPath = Join-Path $InfraPath 'main.bicep'
|
||||
$paramsPath = Join-Path $InfraPath 'main.parameters.json'
|
||||
$mainBicep = if (Test-Path $mainPath) { Get-Content $mainPath -Raw } else { '' }
|
||||
$paramsRaw = if (Test-Path $paramsPath) { Get-Content $paramsPath -Raw } else { '' }
|
||||
$iac = ''
|
||||
if (Test-Path $InfraPath) {
|
||||
$iac = (Get-ChildItem $InfraPath -Recurse -Filter *.bicep -ErrorAction SilentlyContinue |
|
||||
ForEach-Object { Get-Content $_.FullName -Raw }) -join "`n"
|
||||
}
|
||||
|
||||
$svcNames = @()
|
||||
if ($plan -and $plan.services) { $svcNames = @($plan.services | ForEach-Object { $_.name }) }
|
||||
function Has-Service($re) { return [bool]($svcNames | Where-Object { $_ -match $re }) }
|
||||
|
||||
# 1. TAGS-NO-CAMEL — tag keys must be hyphenated, not Bicep camelCase identifiers.
|
||||
if ($mainBicep -match 'appOnboard(Skill|SessionId)|createdAt:|deployedBy:') {
|
||||
Add-Fail 'TAGS-NO-CAMEL' "camelCase tag keys found — use hyphenated ('app-onboard-skill' ...)" 'infra/main.bicep'
|
||||
}
|
||||
|
||||
# 12. DIAG-GATED — we don't scaffold diagnostic-settings; if the generator added one it must be gated behind
|
||||
# enableDiagnostics (default false) or removed. Unconditional wiring blocks the first deploy (target/workspace not ready).
|
||||
# Match an actual diagnostic MODULE/RESOURCE declaration, not a prose comment mentioning 'diagnostic'.
|
||||
if ($mainBicep -match 'module\s+\w*[Dd]iagnostic|Microsoft\.Insights/diagnosticSettings' -and $mainBicep -notmatch 'enableDiagnostics') {
|
||||
Add-Fail 'DIAG-GATED' 'diagnostic-settings module wired without an enableDiagnostics gate — gate it (module ... = if (enableDiagnostics), default false) or remove it; unconditional wiring blocks the first deploy' 'infra/main.bicep'
|
||||
}
|
||||
|
||||
# 2. NO-PLAINTEXT-SECRET — secrets are @secure() params passed at deploy, never literals.
|
||||
if ($paramsRaw) {
|
||||
try {
|
||||
$pj = $paramsRaw | ConvertFrom-Json
|
||||
foreach ($k in $pj.parameters.PSObject.Properties.Name) {
|
||||
if ($k -match '(?i)password|secret|connstring|connection') {
|
||||
$val = $pj.parameters.$k.value
|
||||
if ($null -ne $val -and "$val".Trim().Length -gt 0) {
|
||||
Add-Fail 'NO-PLAINTEXT-SECRET' "parameter '$k' has a literal value — must be @secure(), passed at deploy" 'infra/main.parameters.json'
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
# 2b. NO-BICEP-LITERAL-SECRET — secret values must be @secure() params, never quoted literals in Bicep.
|
||||
# Property name must end in 'password'/'connectionString' right before ':' (so 'secretName' etc. never match).
|
||||
if ($iac) {
|
||||
foreach ($m in [regex]::Matches($iac, "(?im)\b([A-Za-z]*(?:password|connectionstring))\s*:\s*'([^']+)'")) {
|
||||
Add-Fail 'NO-BICEP-LITERAL-SECRET' "property '$($m.Groups[1].Value)' assigned a literal secret in Bicep — use an @secure() param passed at deploy" 'infra/'
|
||||
}
|
||||
if ($iac -match "(?im):\s*'[^']*(?:Password=|AccountKey=|SharedAccessKey=|://[^:@/\s]+:[^:@/\s]+@)[^']*'") {
|
||||
Add-Fail 'NO-BICEP-LITERAL-SECRET' 'quoted literal contains an embedded credential (Password=/AccountKey=/user:pass@) in Bicep — use an @secure() param' 'infra/'
|
||||
}
|
||||
}
|
||||
|
||||
# 3. SERVICES-COMPLETE — every planned service maps to a resource type present in the IaC.
|
||||
$typeMap = [ordered]@{
|
||||
'container apps environment' = 'Microsoft\.App/managedEnvironments'
|
||||
'container app' = 'Microsoft\.App/containerApps'
|
||||
'container registry' = 'Microsoft\.ContainerRegistry/registries'
|
||||
'mysql' = 'Microsoft\.DBforMySQL/flexibleServers'
|
||||
'postgres' = 'Microsoft\.DBforPostgreSQL/flexibleServers'
|
||||
'log analytics' = 'Microsoft\.OperationalInsights/workspaces'
|
||||
'application insights' = 'Microsoft\.Insights/components'
|
||||
'static web app' = 'Microsoft\.Web/staticSites'
|
||||
'app service' = 'Microsoft\.Web/sites'
|
||||
'functions' = 'Microsoft\.Web/sites'
|
||||
'sql' = 'Microsoft\.Sql/servers'
|
||||
'cosmos' = 'Microsoft\.DocumentDB/databaseAccounts'
|
||||
'redis' = 'Microsoft\.Cache/redis'
|
||||
'storage' = 'Microsoft\.Storage/storageAccounts'
|
||||
'service bus' = 'Microsoft\.ServiceBus/namespaces'
|
||||
'event hub' = 'Microsoft\.EventHub/namespaces'
|
||||
}
|
||||
if ($iac) {
|
||||
foreach ($name in $svcNames) {
|
||||
$lc = "$name".ToLower(); $expected = $null
|
||||
foreach ($k in $typeMap.Keys) { if ($lc.Contains($k)) { $expected = $typeMap[$k]; break } }
|
||||
if ($expected -and ($iac -notmatch $expected)) {
|
||||
Add-Fail 'SERVICES-COMPLETE' "planned service '$name' has no matching resource ($expected) in IaC" 'infra/'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Managed database checks ---
|
||||
$hasMysql = Has-Service 'MySQL'
|
||||
$hasPg = Has-Service 'PostgreSQL|Postgres'
|
||||
if (($hasMysql -or $hasPg) -and $iac) {
|
||||
# 4. DB-VERSION-MATCH — each server's version must equal its capabilities-verified plan value.
|
||||
# Scope the match to the DB resource block so an unrelated 'version:' elsewhere isn't picked up.
|
||||
$dbTypeRe = @{ 'MySQL' = 'Microsoft\.DBforMySQL/flexibleServers'; 'PostgreSQL' = 'Microsoft\.DBforPostgreSQL/flexibleServers' }
|
||||
foreach ($svc in ($plan.services | Where-Object { $_.name -match 'MySQL|PostgreSQL' -and $_.version })) {
|
||||
$kind = if ($svc.name -match 'MySQL') { 'MySQL' } else { 'PostgreSQL' }
|
||||
$m = [regex]::Match($iac, "$($dbTypeRe[$kind])[\s\S]{0,800}?version:\s*'([^']+)'")
|
||||
if ($m.Success -and $m.Groups[1].Value -ne $svc.version) {
|
||||
Add-Fail 'DB-VERSION-MATCH' "$kind IaC version '$($m.Groups[1].Value)' != plan '$($svc.version)'" 'infra/modules'
|
||||
}
|
||||
}
|
||||
|
||||
# 5. DB-TLS-ON (MySQL) — require_secure_transport must be enforced.
|
||||
if ($hasMysql -and ($iac -notmatch 'require_secure_transport')) {
|
||||
Add-Fail 'DB-TLS-ON' 'MySQL module missing require_secure_transport config' 'infra/modules'
|
||||
}
|
||||
|
||||
# 6. DB-NAME-PRESENT — the app's named DB must exist in IaC (from prepare-plan.appDbName).
|
||||
if ($plan.appDbName -and ($iac -notmatch 'flexibleServers/databases')) {
|
||||
Add-Fail 'DB-NAME-PRESENT' "app DB '$($plan.appDbName)' declared but no flexibleServers/databases resource" 'infra/modules'
|
||||
}
|
||||
|
||||
# 10. MYSQL-NO-NETWORK-BLOCK (MySQL) — the public-access flow must OMIT the network block.
|
||||
# An empty delegatedSubnetResourceId/privateDnsZoneResourceId is rejected by ARM (LinkedInvalidPropertyId).
|
||||
if ($hasMysql -and ($iac -match 'delegatedSubnetResourceId|privateDnsZoneResourceId')) {
|
||||
Add-Fail 'MYSQL-NO-NETWORK-BLOCK' 'MySQL module includes a network block (delegatedSubnetResourceId) — omit it for public access; an empty value is rejected by ARM' 'infra/modules'
|
||||
}
|
||||
|
||||
# 11. DB-NO-LOCAL-AUTH — data services MUST be Entra / managed-identity only. No admin login/password.
|
||||
if ($iac -match "administratorLogin\s*:") {
|
||||
Add-Fail 'DB-NO-LOCAL-AUTH' "administratorLogin/administratorLoginPassword present — databases must be Entra-only (authConfig passwordAuth Disabled + Entra admin). Remove ALL admin login/password params." 'infra/modules'
|
||||
}
|
||||
if ($iac -match "passwordAuth\s*:\s*'Enabled'") {
|
||||
Add-Fail 'DB-NO-LOCAL-AUTH' "PostgreSQL authConfig sets passwordAuth: 'Enabled' — must be 'Disabled' (Entra-only)" 'infra/modules'
|
||||
}
|
||||
# 11b. DB-ENTRA-ADMIN — PG/MySQL must set an Entra administrator so migrations run token-based.
|
||||
if (($hasPg -or $hasMysql) -and ($iac -notmatch 'flexibleServers/administrators')) {
|
||||
Add-Fail 'DB-ENTRA-ADMIN' 'PostgreSQL/MySQL module missing an administrators (Entra admin) child resource — required for Entra-only auth + token-based migrations' 'infra/modules'
|
||||
}
|
||||
# 11c. PG-ENTRA-ONLY — PostgreSQL must explicitly disable password auth.
|
||||
if ($hasPg -and ($iac -notmatch "passwordAuth\s*:\s*'Disabled'")) {
|
||||
Add-Fail 'PG-ENTRA-ONLY' "PostgreSQL module missing authConfig { passwordAuth: 'Disabled', activeDirectoryAuth: 'Enabled' }" 'infra/modules'
|
||||
}
|
||||
# 11c2. MYSQL-ENTRA-UAMI — MySQL AAD admin requires a user-assigned MI; identityResourceId: null fails to deploy.
|
||||
if ($hasMysql) {
|
||||
if ($iac -match 'identityResourceId\s*:\s*null') {
|
||||
Add-Fail 'MYSQL-ENTRA-UAMI' "MySQL administrators sets identityResourceId: null — will not deploy. Set it to a user-assigned managed identity resourceId and add that UAMI to the server's identity block." 'infra/modules'
|
||||
} elseif ($iac -notmatch 'identityResourceId') {
|
||||
Add-Fail 'MYSQL-ENTRA-UAMI' "MySQL administrators missing identityResourceId — MySQL Entra admin REQUIRES a user-assigned managed identity (unlike PostgreSQL)." 'infra/modules'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- No shared-key / access-key / local-auth on any data service (managed-identity only) ---
|
||||
if ($iac) {
|
||||
# 11d. STORAGE-NO-SHARED-KEY
|
||||
if (($iac -match 'Microsoft\.Storage/storageAccounts') -and ($iac -notmatch 'allowSharedKeyAccess\s*:\s*false')) {
|
||||
Add-Fail 'STORAGE-NO-SHARED-KEY' 'Storage account must set allowSharedKeyAccess: false (managed-identity only)' 'infra/modules'
|
||||
}
|
||||
# 11e. REDIS-NO-KEYS
|
||||
if (($iac -match 'Microsoft\.Cache/redis') -and ($iac -notmatch 'disableAccessKeyAuthentication\s*:\s*true')) {
|
||||
Add-Fail 'REDIS-NO-KEYS' 'Redis must set disableAccessKeyAuthentication: true + accessPolicyAssignments (Entra only)' 'infra/modules'
|
||||
}
|
||||
# 11f. COSMOS-NO-LOCAL-AUTH
|
||||
if (($iac -match 'Microsoft\.DocumentDB/databaseAccounts') -and ($iac -notmatch 'disableLocalAuth\s*:\s*true')) {
|
||||
Add-Fail 'COSMOS-NO-LOCAL-AUTH' 'Cosmos DB must set disableLocalAuth: true (data-plane RBAC only)' 'infra/modules'
|
||||
}
|
||||
# 11g. SBUS-EHUB-NO-LOCAL-AUTH
|
||||
if (($iac -match 'Microsoft\.ServiceBus/namespaces|Microsoft\.EventHub/namespaces') -and ($iac -notmatch 'disableLocalAuth\s*:\s*true')) {
|
||||
Add-Fail 'MSG-NO-LOCAL-AUTH' 'Service Bus / Event Hubs namespace must set disableLocalAuth: true (managed-identity only)' 'infra/modules'
|
||||
}
|
||||
# 11h. NO-FREE-SKU — free/shared compute tiers are not allowed (managed identity is mandatory).
|
||||
if ($iac -match "name\s*:\s*'(F1|D1)'") {
|
||||
Add-Fail 'NO-FREE-SKU' "App Service plan uses a free/shared SKU ('$($Matches[1])') — floor is B1 (managed identity is mandatory; the free-tier MI sidecar OOMs)" 'infra/modules'
|
||||
}
|
||||
if (($iac -match 'Microsoft\.Web/staticSites') -and ($iac -match "tier\s*:\s*'Free'")) {
|
||||
Add-Fail 'NO-FREE-SKU' "Static Web App uses the Free tier — floor is Standard" 'infra/modules'
|
||||
}
|
||||
}
|
||||
|
||||
# --- Key Vault checks ---
|
||||
# --- No Key Vault (app-internal secrets live on the compute resource) ---
|
||||
if (($iac -match 'Microsoft\.KeyVault/vaults') -or ($iac -match '@Microsoft\.KeyVault') -or ($iac -match 'keyVaultUrl')) {
|
||||
Add-Fail 'NO-KEYVAULT' 'Key Vault detected (Microsoft.KeyVault/vaults, @Microsoft.KeyVault(...), or keyVaultUrl) — no Key Vault is allowed. Store app-internal secrets on the compute resource: @secure() param -> App Service appSettings / Container Apps native secrets[].' 'infra/'
|
||||
}
|
||||
|
||||
# --- Container Apps / ACR checks (deterministic ARM-failure invariants) ---
|
||||
$hasCA = $iac -match 'Microsoft\.App/containerApps'
|
||||
$hasCAE = $iac -match 'Microsoft\.App/managedEnvironments'
|
||||
$hasAcr = $iac -match 'Microsoft\.ContainerRegistry/registries'
|
||||
if ($iac) {
|
||||
# 13. CAE-APPLOGS — managedEnvironments log config MUST nest under appLogsConfiguration; a bare
|
||||
# top-level logAnalyticsConfiguration fails deploy with ManagedEnvironmentInvalidSchema.
|
||||
if ($hasCAE -and ($iac -match 'logAnalyticsConfiguration') -and ($iac -notmatch 'appLogsConfiguration')) {
|
||||
Add-Fail 'CAE-APPLOGS' "Container Apps Environment uses a bare logAnalyticsConfiguration — nest it under appLogsConfiguration.destination='log-analytics' (else ManagedEnvironmentInvalidSchema at deploy)" 'infra/modules'
|
||||
}
|
||||
# 14. CA-NO-REVISION-SUFFIX — a hardcoded revisionSuffix fails Phase 2 redeploy ('revision already exists').
|
||||
if ($hasCA -and ($iac -match 'revisionSuffix\s*:')) {
|
||||
Add-Fail 'CA-NO-REVISION-SUFFIX' 'Container App sets revisionSuffix — omit it (ARM auto-generates); a hardcoded value fails Phase 2 redeploy' 'infra/modules'
|
||||
}
|
||||
# 15. CA-IMAGE-PARAM — two-phase deploy needs `param containerImage` in main.bicep, else the Phase 2 `--parameters containerImage=` override is silently ignored and the placeholder image persists.
|
||||
if ($hasCA -and $hasAcr -and ($mainBicep -notmatch 'param\s+containerImage')) {
|
||||
Add-Fail 'CA-IMAGE-PARAM' 'main.bicep lacks `param containerImage` — the Phase 2 image override is silently ignored and the placeholder persists (MANIFEST_UNKNOWN)' 'infra/main.bicep'
|
||||
}
|
||||
# 16. ACRPULL-GUID — near-miss detector: the fixed AcrPull prefix present without the canonical full GUID = wrong last segment (hallucinated) → RoleDefinitionDoesNotExist. Cannot false-positive.
|
||||
if (($iac -match '7f951dda-4ed3-4680-a7ca-') -and ($iac -notmatch '7f951dda-4ed3-4680-a7ca-43fe172d538d')) {
|
||||
Add-Fail 'ACRPULL-GUID' 'AcrPull role GUID is wrong — canonical value is 7f951dda-4ed3-4680-a7ca-43fe172d538d (RoleDefinitionDoesNotExist otherwise)' 'infra/modules/role-assignments.bicep'
|
||||
}
|
||||
# 17. ACR-NO-PREMIUM-POLICY — retention/trust/quarantine are Premium-only; on Basic/Standard ACR they fail SkuNotSupported. FP-safe: skipped if any 'Premium' SKU appears in the IaC.
|
||||
if ($hasAcr -and ($iac -match '(retentionPolicy|trustPolicy|quarantinePolicy)\s*:') -and ($iac -notmatch "'Premium'")) {
|
||||
Add-Fail 'ACR-NO-PREMIUM-POLICY' 'ACR has a Premium-only policy (retention/trust/quarantine) but is not Premium — omit these for Basic/Standard (SkuNotSupported at deploy)' 'infra/modules'
|
||||
}
|
||||
}
|
||||
|
||||
# 9. WARN-FIXED — prereq warnings flagged fixPhase:"scaffold" must land in the generated IaC.
|
||||
# Only warnings with a provable signal are enforced; unverifiable ones are skipped (no false BLOCK).
|
||||
# Extend $warnSignal as new provable scaffold-phase warning IDs are added.
|
||||
$prereq = Read-Json (Join-Path $SessionPath 'prereq-output.json')
|
||||
if ($prereq -and $prereq.warnings) {
|
||||
$warnSignal = @{
|
||||
'W-PG-SSL' = @{ kind = 'iac'; pattern = 'PGSSLMODE|sslmode' } # SSL-mode env var emitted in IaC
|
||||
'W-BUILDKIT' = @{ kind = 'file'; pattern = 'Dockerfile.azure' } # ACR-compatible Dockerfile emitted
|
||||
}
|
||||
foreach ($w in $prereq.warnings) {
|
||||
if ($w.fixPhase -ne 'scaffold') { continue }
|
||||
$sig = $warnSignal[$w.id]
|
||||
if (-not $sig) { continue } # no deterministic signal — cannot verify, do not block
|
||||
$present = if ($sig.kind -eq 'file') {
|
||||
[bool](Get-ChildItem -Recurse -Filter $sig.pattern -ErrorAction SilentlyContinue | Select-Object -First 1)
|
||||
} else {
|
||||
[bool]($iac -match $sig.pattern)
|
||||
}
|
||||
if (-not $present) {
|
||||
Add-Fail 'WARN-FIXED' "prereq warning '$($w.id)' (fixPhase:scaffold) fix not found in IaC — $($w.fix)" 'infra/'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result = [ordered]@{ passed = ($failures.Count -eq 0); failures = $failures }
|
||||
$result | ConvertTo-Json -Depth 5 -Compress
|
||||
if ($failures.Count -gt 0) { exit 1 } else { exit 0 }
|
||||
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env bash
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See LICENSE.md in the project root for license information.
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
|
||||
# Scaffold -> Deploy conformance gate (bash twin of scaffold-conformance.ps1).
|
||||
# Checks generated IaC against the plan for semantic defects `az bicep build` cannot catch.
|
||||
#
|
||||
# Usage: scaffold-conformance.sh <sessionPath> [infraPath]
|
||||
# Output: JSON { passed, failures:[{id,detail,file}] } to stdout.
|
||||
# Exit: 0 = pass, 1 = one or more BLOCK failures.
|
||||
#
|
||||
# Pure-text checks (tags, TLS, purge, deployer role, plaintext secret) always run.
|
||||
# Plan-dependent checks (version, services-complete, db-name, warn-fixed) run only when `jq` is present.
|
||||
set -u
|
||||
SESSION_PATH="${1:?usage: scaffold-conformance.sh <sessionPath> [infraPath]}"
|
||||
INFRA_PATH="${2:-infra}"
|
||||
|
||||
PLAN="$SESSION_PATH/prepare-plan.json"
|
||||
MAIN="$INFRA_PATH/main.bicep"
|
||||
PARAMS="$INFRA_PATH/main.parameters.json"
|
||||
|
||||
main_bicep="$( [ -f "$MAIN" ] && cat "$MAIN" || true )"
|
||||
params_raw="$( [ -f "$PARAMS" ] && cat "$PARAMS" || true )"
|
||||
iac="$( find "$INFRA_PATH" -name '*.bicep' -type f 2>/dev/null -exec cat {} + || true )"
|
||||
|
||||
HAVE_JQ=0; command -v jq >/dev/null 2>&1 && HAVE_JQ=1
|
||||
|
||||
# failures accumulator (JSON objects, newline-separated)
|
||||
fails=""
|
||||
add_fail() { # id detail file
|
||||
local obj; obj="$(printf '{"id":"%s","detail":"%s","file":"%s"}' "$1" "$2" "$3")"
|
||||
fails="${fails:+$fails,}$obj"
|
||||
}
|
||||
iac_has() { printf '%s' "$iac" | grep -qE "$1"; }
|
||||
|
||||
# 1. TAGS-NO-CAMEL
|
||||
if printf '%s' "$main_bicep" | grep -qE 'appOnboard(Skill|SessionId)|createdAt:|deployedBy:'; then
|
||||
add_fail "TAGS-NO-CAMEL" "camelCase tag keys found — use hyphenated ('app-onboard-skill' ...)" "infra/main.bicep"
|
||||
fi
|
||||
|
||||
# 12. DIAG-GATED — we don't scaffold diagnostic-settings; if present it must be gated behind enableDiagnostics (default false) or removed. Unconditional wiring blocks the first deploy.
|
||||
# Match an actual diagnostic module/resource declaration, not a prose comment mentioning 'diagnostic'.
|
||||
if printf '%s' "$main_bicep" | grep -qE 'module[[:space:]]+[A-Za-z]*[Dd]iagnostic|Microsoft\.Insights/diagnosticSettings' && ! printf '%s' "$main_bicep" | grep -q 'enableDiagnostics'; then
|
||||
add_fail "DIAG-GATED" "diagnostic-settings module wired without an enableDiagnostics gate — gate it (module ... = if (enableDiagnostics), default false) or remove it; unconditional wiring blocks the first deploy" "infra/main.bicep"
|
||||
fi
|
||||
|
||||
# 2. NO-PLAINTEXT-SECRET (jq-free: awk over the params file so it runs on hosts without jq)
|
||||
if [ -n "$params_raw" ]; then
|
||||
bad_keys="$(printf '%s' "$params_raw" | awk '
|
||||
{ line=tolower($0) }
|
||||
line ~ /"[a-z0-9_]*(password|secret|connstring|connection)[a-z0-9_]*"[ \t]*:/ {
|
||||
n=split($0, parts, "\"")
|
||||
for (i=1;i<=n;i++) { if (tolower(parts[i]) ~ /(password|secret|connstring|connection)/) { pend=parts[i]; break } }
|
||||
}
|
||||
pend != "" && line ~ /"value"[ \t]*:[ \t]*"[^"]+"/ { print pend; pend="" }
|
||||
/}/ { pend="" }
|
||||
')"
|
||||
if [ -n "$bad_keys" ]; then
|
||||
while IFS= read -r k; do
|
||||
[ -z "$k" ] && continue
|
||||
add_fail "NO-PLAINTEXT-SECRET" "parameter '$k' has a literal value — must be @secure(), passed at deploy" "infra/main.parameters.json"
|
||||
done <<< "$bad_keys"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2b. NO-BICEP-LITERAL-SECRET (pure-text) — secret values must be @secure() params, never quoted literals.
|
||||
# Property name must end in 'password'/'connectionString' right before ':' (so 'secretName' etc. never match).
|
||||
if [ -n "$iac" ]; then
|
||||
if printf '%s' "$iac" | grep -qiE "[A-Za-z]*(password|connectionstring)[[:space:]]*:[[:space:]]*'[^']+'"; then
|
||||
k="$(printf '%s' "$iac" | grep -oiE "[A-Za-z]*(password|connectionstring)[[:space:]]*:[[:space:]]*'[^']+'" | head -n1 | sed -E "s/[[:space:]]*:.*//")"
|
||||
add_fail "NO-BICEP-LITERAL-SECRET" "property '$k' assigned a literal secret in Bicep — use an @secure() param passed at deploy" "infra/"
|
||||
fi
|
||||
if printf '%s' "$iac" | grep -qE ":[[:space:]]*'[^']*(Password=|AccountKey=|SharedAccessKey=|://[^:@/ ]+:[^:@/ ]+@)[^']*'"; then
|
||||
add_fail "NO-BICEP-LITERAL-SECRET" "quoted literal contains an embedded credential (Password=/AccountKey=/user:pass@) in Bicep — use an @secure() param" "infra/"
|
||||
fi
|
||||
fi
|
||||
|
||||
# service-name -> resource-type map (longest keys first so 'container apps environment' wins)
|
||||
declare -a MAP_KEYS=( "container apps environment" "container app" "container registry" "mysql" "postgres" \
|
||||
"log analytics" "application insights" "static web app" "app service" "functions" \
|
||||
"sql" "cosmos" "redis" "storage" "service bus" "event hub" )
|
||||
map_type() {
|
||||
case "$1" in
|
||||
*"container apps environment"*) echo 'Microsoft\.App/managedEnvironments';;
|
||||
*"container app"*) echo 'Microsoft\.App/containerApps';;
|
||||
*"container registry"*) echo 'Microsoft\.ContainerRegistry/registries';;
|
||||
*mysql*) echo 'Microsoft\.DBforMySQL/flexibleServers';;
|
||||
*postgres*) echo 'Microsoft\.DBforPostgreSQL/flexibleServers';;
|
||||
*"log analytics"*) echo 'Microsoft\.OperationalInsights/workspaces';;
|
||||
*"application insights"*) echo 'Microsoft\.Insights/components';;
|
||||
*"static web app"*) echo 'Microsoft\.Web/staticSites';;
|
||||
*"app service"*|*functions*) echo 'Microsoft\.Web/sites';;
|
||||
*sql*) echo 'Microsoft\.Sql/servers';;
|
||||
*cosmos*) echo 'Microsoft\.DocumentDB/databaseAccounts';;
|
||||
*redis*) echo 'Microsoft\.Cache/redis';;
|
||||
*storage*) echo 'Microsoft\.Storage/storageAccounts';;
|
||||
*"service bus"*) echo 'Microsoft\.ServiceBus/namespaces';;
|
||||
*"event hub"*) echo 'Microsoft\.EventHub/namespaces';;
|
||||
*) echo '';;
|
||||
esac
|
||||
}
|
||||
|
||||
svc_names=""; db_version=""; app_db_name=""; has_mysql=0; has_pg=0
|
||||
if [ "$HAVE_JQ" = 1 ] && [ -f "$PLAN" ]; then
|
||||
svc_names="$(jq -r '.services[].name' "$PLAN" 2>/dev/null)"
|
||||
app_db_name="$(jq -r '.appDbName // empty' "$PLAN" 2>/dev/null)"
|
||||
printf '%s' "$svc_names" | grep -qiE 'mysql' && has_mysql=1
|
||||
printf '%s' "$svc_names" | grep -qiE 'postgres' && has_pg=1
|
||||
|
||||
# 3. SERVICES-COMPLETE
|
||||
while IFS= read -r name; do
|
||||
[ -z "$name" ] && continue
|
||||
lc="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')"
|
||||
expected="$(map_type "$lc")"
|
||||
if [ -n "$expected" ] && ! iac_has "$expected"; then
|
||||
add_fail "SERVICES-COMPLETE" "planned service '$name' has no matching resource ($expected) in IaC" "infra/"
|
||||
fi
|
||||
done < <(printf '%s\n' "$svc_names")
|
||||
else
|
||||
# jq absent: fall back to grep for the DB-type presence (drives TLS/name checks below)
|
||||
printf '%s' "$iac" | grep -qE 'Microsoft\.DBforMySQL/flexibleServers' && has_mysql=1
|
||||
printf '%s' "$iac" | grep -qE 'Microsoft\.DBforPostgreSQL/flexibleServers' && has_pg=1
|
||||
fi
|
||||
|
||||
# --- Managed database checks ---
|
||||
if [ "$has_mysql" = 1 ] || [ "$has_pg" = 1 ]; then
|
||||
# 4. DB-VERSION-MATCH — each server's version must equal its capabilities-verified plan value.
|
||||
# Scope the match to the DB resource block so an unrelated 'version:' elsewhere isn't picked up.
|
||||
if [ "$HAVE_JQ" = 1 ] && [ -f "$PLAN" ]; then
|
||||
while IFS=$'\t' read -r dbkind dbver; do
|
||||
[ -z "$dbver" ] && continue
|
||||
case "$dbkind" in
|
||||
*MySQL*) marker='Microsoft[.]DBforMySQL/flexibleServers';;
|
||||
*) marker='Microsoft[.]DBforPostgreSQL/flexibleServers';;
|
||||
esac
|
||||
iac_ver="$(printf '%s\n' "$iac" | awk -v marker="$marker" '
|
||||
$0 ~ marker { armed=1 }
|
||||
armed==1 && match($0, /version:[ \t]*'"'"'[^'"'"']+'"'"'/) {
|
||||
v=substr($0,RSTART,RLENGTH); sub(/version:[ \t]*'"'"'/,"",v); sub(/'"'"'.*/,"",v); print v; exit
|
||||
}')"
|
||||
if [ -n "$iac_ver" ] && [ "$iac_ver" != "$dbver" ]; then
|
||||
add_fail "DB-VERSION-MATCH" "$dbkind IaC version '$iac_ver' != plan '$dbver'" "infra/modules"
|
||||
fi
|
||||
done < <(jq -r '.services[]? | select(.name|test("MySQL|PostgreSQL")) | [.name, (.version // "")] | @tsv' "$PLAN" 2>/dev/null)
|
||||
fi
|
||||
# 5. DB-TLS-ON (MySQL, pure-text)
|
||||
if [ "$has_mysql" = 1 ] && ! iac_has 'require_secure_transport'; then
|
||||
add_fail "DB-TLS-ON" "MySQL module missing require_secure_transport config" "infra/modules"
|
||||
fi
|
||||
# 6. DB-NAME-PRESENT (needs plan.appDbName from jq)
|
||||
if [ -n "$app_db_name" ] && ! iac_has 'flexibleServers/databases'; then
|
||||
add_fail "DB-NAME-PRESENT" "app DB '$app_db_name' declared but no flexibleServers/databases resource" "infra/modules"
|
||||
fi
|
||||
# 10. MYSQL-NO-NETWORK-BLOCK (MySQL, pure-text) — public flow must omit the network block;
|
||||
# an empty delegatedSubnetResourceId is rejected by ARM (LinkedInvalidPropertyId).
|
||||
if [ "$has_mysql" = 1 ] && iac_has 'delegatedSubnetResourceId|privateDnsZoneResourceId'; then
|
||||
add_fail "MYSQL-NO-NETWORK-BLOCK" "MySQL module includes a network block (delegatedSubnetResourceId) — omit it for public access; an empty value is rejected by ARM" "infra/modules"
|
||||
fi
|
||||
# 11. DB-NO-LOCAL-AUTH (pure-text) — databases MUST be Entra / managed-identity only.
|
||||
if iac_has "administratorLogin[[:space:]]*:"; then
|
||||
add_fail "DB-NO-LOCAL-AUTH" "administratorLogin/administratorLoginPassword present — databases must be Entra-only (authConfig passwordAuth Disabled + Entra admin). Remove ALL admin login/password params." "infra/modules"
|
||||
fi
|
||||
if iac_has "passwordAuth[[:space:]]*:[[:space:]]*'Enabled'"; then
|
||||
add_fail "DB-NO-LOCAL-AUTH" "PostgreSQL authConfig sets passwordAuth: 'Enabled' — must be 'Disabled' (Entra-only)" "infra/modules"
|
||||
fi
|
||||
# 11b. DB-ENTRA-ADMIN — PG/MySQL must set an Entra administrator for token-based migrations.
|
||||
if ! iac_has 'flexibleServers/administrators'; then
|
||||
add_fail "DB-ENTRA-ADMIN" "PostgreSQL/MySQL module missing an administrators (Entra admin) child resource — required for Entra-only auth + token-based migrations" "infra/modules"
|
||||
fi
|
||||
# 11c. PG-ENTRA-ONLY — PostgreSQL must explicitly disable password auth.
|
||||
if [ "$has_pg" = 1 ] && ! iac_has "passwordAuth[[:space:]]*:[[:space:]]*'Disabled'"; then
|
||||
add_fail "PG-ENTRA-ONLY" "PostgreSQL module missing authConfig { passwordAuth: 'Disabled', activeDirectoryAuth: 'Enabled' }" "infra/modules"
|
||||
fi
|
||||
# 11c2. MYSQL-ENTRA-UAMI — MySQL AAD admin requires a user-assigned MI; identityResourceId: null fails to deploy.
|
||||
if [ "$has_mysql" = 1 ]; then
|
||||
if iac_has "identityResourceId[[:space:]]*:[[:space:]]*null"; then
|
||||
add_fail "MYSQL-ENTRA-UAMI" "MySQL administrators sets identityResourceId: null — will not deploy. Set it to a user-assigned managed identity resourceId and add that UAMI to the server's identity block." "infra/modules"
|
||||
elif ! iac_has 'identityResourceId'; then
|
||||
add_fail "MYSQL-ENTRA-UAMI" "MySQL administrators missing identityResourceId — MySQL Entra admin REQUIRES a user-assigned managed identity (unlike PostgreSQL)." "infra/modules"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- No shared-key / access-key / local-auth on any data service (managed-identity only) ---
|
||||
# 11d. STORAGE-NO-SHARED-KEY
|
||||
if iac_has 'Microsoft\.Storage/storageAccounts' && ! iac_has "allowSharedKeyAccess[[:space:]]*:[[:space:]]*false"; then
|
||||
add_fail "STORAGE-NO-SHARED-KEY" "Storage account must set allowSharedKeyAccess: false (managed-identity only)" "infra/modules"
|
||||
fi
|
||||
# 11e. REDIS-NO-KEYS
|
||||
if iac_has 'Microsoft\.Cache/redis' && ! iac_has "disableAccessKeyAuthentication[[:space:]]*:[[:space:]]*true"; then
|
||||
add_fail "REDIS-NO-KEYS" "Redis must set disableAccessKeyAuthentication: true + accessPolicyAssignments (Entra only)" "infra/modules"
|
||||
fi
|
||||
# 11f. COSMOS-NO-LOCAL-AUTH
|
||||
if iac_has 'Microsoft\.DocumentDB/databaseAccounts' && ! iac_has "disableLocalAuth[[:space:]]*:[[:space:]]*true"; then
|
||||
add_fail "COSMOS-NO-LOCAL-AUTH" "Cosmos DB must set disableLocalAuth: true (data-plane RBAC only)" "infra/modules"
|
||||
fi
|
||||
# 11g. MSG-NO-LOCAL-AUTH
|
||||
if iac_has 'Microsoft\.ServiceBus/namespaces|Microsoft\.EventHub/namespaces' && ! iac_has "disableLocalAuth[[:space:]]*:[[:space:]]*true"; then
|
||||
add_fail "MSG-NO-LOCAL-AUTH" "Service Bus / Event Hubs namespace must set disableLocalAuth: true (managed-identity only)" "infra/modules"
|
||||
fi
|
||||
# 11h. NO-FREE-SKU — free/shared compute tiers are not allowed (managed identity is mandatory).
|
||||
if iac_has "name[[:space:]]*:[[:space:]]*'(F1|D1)'"; then
|
||||
sku="$(printf '%s' "$iac" | grep -oE "name[[:space:]]*:[[:space:]]*'(F1|D1)'" | grep -oE '(F1|D1)' | head -n1)"
|
||||
add_fail "NO-FREE-SKU" "App Service plan uses a free/shared SKU ('${sku}') — floor is B1 (managed identity is mandatory; the free-tier MI sidecar OOMs)" "infra/modules"
|
||||
fi
|
||||
if iac_has 'Microsoft\.Web/staticSites' && iac_has "tier[[:space:]]*:[[:space:]]*'Free'"; then
|
||||
add_fail "NO-FREE-SKU" "Static Web App uses the Free tier — floor is Standard" "infra/modules"
|
||||
fi
|
||||
|
||||
# --- No Key Vault (app-internal secrets live on the compute resource) ---
|
||||
if iac_has 'Microsoft\.KeyVault/vaults' || iac_has '@Microsoft\.KeyVault' || iac_has 'keyVaultUrl'; then
|
||||
add_fail "NO-KEYVAULT" "Key Vault detected (Microsoft.KeyVault/vaults, @Microsoft.KeyVault(...), or keyVaultUrl) — no Key Vault is allowed. Store app-internal secrets on the compute resource: @secure() param -> App Service appSettings / Container Apps native secrets[]." "infra/"
|
||||
fi
|
||||
|
||||
# --- Container Apps / ACR checks (deterministic ARM-failure invariants) ---
|
||||
if [ -n "$iac" ]; then
|
||||
has_ca=0; iac_has 'Microsoft\.App/containerApps' && has_ca=1
|
||||
has_cae=0; iac_has 'Microsoft\.App/managedEnvironments' && has_cae=1
|
||||
has_acr=0; iac_has 'Microsoft\.ContainerRegistry/registries' && has_acr=1
|
||||
# 13. CAE-APPLOGS — managedEnvironments log config MUST nest under appLogsConfiguration; a bare
|
||||
# top-level logAnalyticsConfiguration fails deploy with ManagedEnvironmentInvalidSchema.
|
||||
if [ "$has_cae" = 1 ] && iac_has 'logAnalyticsConfiguration' && ! iac_has 'appLogsConfiguration'; then
|
||||
add_fail "CAE-APPLOGS" "Container Apps Environment uses a bare logAnalyticsConfiguration — nest it under appLogsConfiguration.destination='log-analytics' (else ManagedEnvironmentInvalidSchema at deploy)" "infra/modules"
|
||||
fi
|
||||
# 14. CA-NO-REVISION-SUFFIX — a hardcoded revisionSuffix fails Phase 2 redeploy ('revision already exists').
|
||||
if [ "$has_ca" = 1 ] && iac_has 'revisionSuffix[[:space:]]*:'; then
|
||||
add_fail "CA-NO-REVISION-SUFFIX" "Container App sets revisionSuffix — omit it (ARM auto-generates); a hardcoded value fails Phase 2 redeploy" "infra/modules"
|
||||
fi
|
||||
# 15. CA-IMAGE-PARAM — two-phase deploy needs 'param containerImage' in main.bicep, else the Phase 2 override is silently ignored and the placeholder image persists.
|
||||
if [ "$has_ca" = 1 ] && [ "$has_acr" = 1 ] && ! printf '%s' "$main_bicep" | grep -qE 'param[[:space:]]+containerImage'; then
|
||||
add_fail "CA-IMAGE-PARAM" "main.bicep lacks 'param containerImage' — the Phase 2 image override is silently ignored and the placeholder persists (MANIFEST_UNKNOWN)" "infra/main.bicep"
|
||||
fi
|
||||
# 16. ACRPULL-GUID — near-miss detector: fixed AcrPull prefix present without the canonical full GUID = hallucinated last segment → RoleDefinitionDoesNotExist. Cannot false-positive.
|
||||
if iac_has '7f951dda-4ed3-4680-a7ca-' && ! iac_has '7f951dda-4ed3-4680-a7ca-43fe172d538d'; then
|
||||
add_fail "ACRPULL-GUID" "AcrPull role GUID is wrong — canonical value is 7f951dda-4ed3-4680-a7ca-43fe172d538d (RoleDefinitionDoesNotExist otherwise)" "infra/modules/role-assignments.bicep"
|
||||
fi
|
||||
# 17. ACR-NO-PREMIUM-POLICY — retention/trust/quarantine are Premium-only; on Basic/Standard ACR they fail SkuNotSupported. FP-safe: skipped if any 'Premium' SKU appears.
|
||||
if [ "$has_acr" = 1 ] && iac_has '(retentionPolicy|trustPolicy|quarantinePolicy)[[:space:]]*:' && ! iac_has "'Premium'"; then
|
||||
add_fail "ACR-NO-PREMIUM-POLICY" "ACR has a Premium-only policy (retention/trust/quarantine) but is not Premium — omit these for Basic/Standard (SkuNotSupported at deploy)" "infra/modules"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 9. WARN-FIXED (needs jq to read prereq-output.json warnings[]) — prereq warnings flagged
|
||||
# fixPhase:"scaffold" must land in the IaC. Only warnings with a provable signal are enforced;
|
||||
# unverifiable ones are skipped (no false BLOCK). Extend the case map for new provable warning IDs.
|
||||
PREREQ="$SESSION_PATH/prereq-output.json"
|
||||
if [ "$HAVE_JQ" = 1 ] && [ -f "$PREREQ" ]; then
|
||||
while IFS=$'\t' read -r wid wfix; do
|
||||
[ -z "$wid" ] && continue
|
||||
present=0
|
||||
case "$wid" in
|
||||
W-PG-SSL) printf '%s' "$iac" | grep -qiE 'PGSSLMODE|sslmode' && present=1 ;;
|
||||
W-BUILDKIT) [ -n "$(find . -name 'Dockerfile.azure' -type f 2>/dev/null | head -n1)" ] && present=1 ;;
|
||||
*) continue ;; # no deterministic signal — cannot verify, do not block
|
||||
esac
|
||||
if [ "$present" != 1 ]; then
|
||||
add_fail "WARN-FIXED" "prereq warning '$wid' (fixPhase:scaffold) fix not found in IaC — $wfix" "infra/"
|
||||
fi
|
||||
done < <(jq -r '.warnings[]? | select(.fixPhase=="scaffold") | [.id, .fix] | @tsv' "$PREREQ" 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [ -z "$fails" ]; then
|
||||
printf '{"passed":true,"failures":[]}\n'
|
||||
exit 0
|
||||
else
|
||||
printf '{"passed":false,"failures":[%s]}\n' "$fails"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user