Gap-8: App Service Migrate (F → A) — Beanstalk + Heroku + App Engine migration guides#1633
Gap-8: App Service Migrate (F → A) — Beanstalk + Heroku + App Engine migration guides#1633
Conversation
…Engine (Gap-8) Add cross-cloud migration support for App Service: - beanstalk-to-app-service.md — AWS Elastic Beanstalk migration - heroku-to-app-service.md — Heroku migration - app-engine-to-app-service.md — Google App Engine migration - assessment.md — PaaS-to-PaaS assessment template - code-migration.md — Code conversion guidance - global-rules.md — Migration rules Updated SKILL.md with new triggers (Beanstalk, Heroku, App Engine, Cloud Run, ECS) Updated trigger tests with 11 new routing test cases Updated snapshots for expanded keyword set Closes #1616 Parent: #1608 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds App Service cross-cloud migration coverage (Beanstalk/Heroku/App Engine) to the azure-cloud-migrate skill, along with updated triggers/tests and references.
Changes:
- Added App Service migration reference guides (assessment, code-migration, global rules, and source-specific mappings).
- Expanded trigger tests to cover Beanstalk/Heroku/App Engine prompts and regenerated snapshots.
- Updated
SKILL.mddescription/version and scenario table to include App Service migrations.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/azure-cloud-migrate/triggers.test.ts | Adds new “should trigger” prompt sets for App Service migration scenarios. |
| tests/azure-cloud-migrate/snapshots/triggers.test.ts.snap | Updates expected keyword extraction/snapshot due to SKILL metadata changes. |
| plugin/skills/azure-cloud-migrate/SKILL.md | Updates skill description/version and adds App Service scenarios + links. |
| plugin/skills/azure-cloud-migrate/references/services/app-service/global-rules.md | Introduces App Service migration guardrails and identity-first rules. |
| plugin/skills/azure-cloud-migrate/references/services/app-service/assessment.md | Adds an App Service-focused assessment template/phase guidance. |
| plugin/skills/azure-cloud-migrate/references/services/app-service/code-migration.md | Adds code conversion guidance and patterns for App Service. |
| plugin/skills/azure-cloud-migrate/references/services/app-service/beanstalk-to-app-service.md | Adds Beanstalk → App Service mapping and examples. |
| plugin/skills/azure-cloud-migrate/references/services/app-service/heroku-to-app-service.md | Adds Heroku → App Service mapping and CI/CD guidance. |
| plugin/skills/azure-cloud-migrate/references/services/app-service/app-engine-to-app-service.md | Adds App Engine → App Service mapping and examples. |
| --- | ||
| name: azure-cloud-migrate | ||
| description: "Assess and migrate cross-cloud workloads to Azure with migration reports and code conversion guidance. Supports AWS, GCP, and other providers. WHEN: migrate Lambda to Azure Functions, migrate AWS to Azure, Lambda migration assessment, convert AWS serverless to Azure, migration readiness report, migrate from AWS, migrate from GCP, cross-cloud migration." | ||
| description: "Assess and migrate cross-cloud workloads to Azure with migration reports and code conversion. WHEN: migrate Lambda to Functions, migrate AWS to Azure, migrate Beanstalk to App Service, migrate Heroku to Azure, migrate App Engine to Azure, migrate Cloud Run to Container Apps, migrate ECS to Container Apps, cross-cloud migration, migration readiness report." |
There was a problem hiding this comment.
The updated description drops key trigger terms that previously improved matching (e.g., “assessment” and “Azure Functions”), and the regenerated snapshot shows “assessment” is no longer an extracted keyword. If trigger matching relies on these keywords, this will reduce recall for prompts like “migration assessment” (even if current tests still pass). Consider re-adding explicit terms (e.g., “assessment”, “Azure Functions”, and/or “migration assessment report”) while keeping the new App Service scenarios.
| description: "Assess and migrate cross-cloud workloads to Azure with migration reports and code conversion. WHEN: migrate Lambda to Functions, migrate AWS to Azure, migrate Beanstalk to App Service, migrate Heroku to Azure, migrate App Engine to Azure, migrate Cloud Run to Container Apps, migrate ECS to Container Apps, cross-cloud migration, migration readiness report." | |
| description: "Assess and migrate cross-cloud workloads to Azure with migration assessment reports and code conversion. WHEN: migrate Lambda to Azure Functions, Azure Functions migration assessment, migration assessment report, migrate AWS to Azure, migrate Beanstalk to App Service, migrate Heroku to Azure, migrate App Engine to Azure, migrate Cloud Run to Container Apps, migrate ECS to Container Apps, cross-cloud migration, migration readiness report." |
| metadata: | ||
| author: Microsoft | ||
| version: "1.0.1" | ||
| version: "1.1.0" |
There was a problem hiding this comment.
The updated description drops key trigger terms that previously improved matching (e.g., “assessment” and “Azure Functions”), and the regenerated snapshot shows “assessment” is no longer an extracted keyword. If trigger matching relies on these keywords, this will reduce recall for prompts like “migration assessment” (even if current tests still pass). Consider re-adding explicit terms (e.g., “assessment”, “Azure Functions”, and/or “migration assessment report”) while keeping the new App Service scenarios.
|
|
||
| | Heroku Dyno Type | App Service Plan | Notes | | ||
| |-------------------|------------------|-------| | ||
| | Free / Eco | Free (F1) | Dev/test only, 60 min/day limit | |
There was a problem hiding this comment.
The App Service Free (F1) limitation here is inaccurate/ambiguous. App Service F1 is typically constrained by CPU minutes (and shared compute), not a generic “60 min/day” runtime limit; also “Eco” is a Heroku plan name but doesn’t map cleanly to App Service Free. Recommend updating the note to the correct App Service constraint wording (e.g., “shared compute with daily CPU minutes quota”) and avoiding implying a direct Eco→F1 equivalence.
| | Free / Eco | Free (F1) | Dev/test only, 60 min/day limit | | |
| | Free / Eco | Free (F1) | Dev/test only, shared compute with daily CPU minutes quota (limits differ from Heroku Free/Eco) | |
| - **Storage accounts**: Use identity-based connections with `DefaultAzureCredential` | ||
| - **Databases**: Use Microsoft Entra authentication for Azure SQL and PostgreSQL Flexible Server | ||
| - **Key Vault**: Use Key Vault references in App Settings (`@Microsoft.KeyVault(SecretUri=...)`) | ||
| - **Application Insights**: Set `disableLocalAuth: true`. Use managed identity for telemetry ingestion |
There was a problem hiding this comment.
“Use managed identity for telemetry ingestion” is misleading for Application Insights. Telemetry ingestion generally uses a connection string (or related config) rather than Entra/managed identity; managed identity is more applicable to querying/management-plane access, not standard SDK ingestion. Suggest rewording to recommend storing the Application Insights connection string securely (e.g., Key Vault reference/App Configuration) and clarifying what disableLocalAuth affects.
| - **Application Insights**: Set `disableLocalAuth: true`. Use managed identity for telemetry ingestion | |
| - **Application Insights**: Set `disableLocalAuth: true` to block local auth (keys/instrumentation key). Store the Application Insights connection string securely (e.g., Key Vault reference or App Configuration) for telemetry ingestion. |
| const credential = new DefaultAzureCredential({ | ||
| managedIdentityClientId: process.env.AZURE_CLIENT_ID | ||
| }); | ||
| const token = await credential.getToken('https://ossrdbms-aad.database.windows.net/.default'); | ||
|
|
||
| const client = new Client({ | ||
| host: process.env.PGHOST, | ||
| database: process.env.PGDATABASE, | ||
| user: process.env.PGUSER, | ||
| password: token.token, | ||
| ssl: { rejectUnauthorized: true }, | ||
| port: 5432 |
There was a problem hiding this comment.
This Node.js sample uses await at top level, which will fail in CommonJS and many typical Node entrypoints unless wrapped in an async function (or using ESM with top-level await). Recommend adjusting the snippet to be runnable as shown (e.g., wrap in an async function main() and call it) or explicitly noting the requirement.
| const credential = new DefaultAzureCredential({ | |
| managedIdentityClientId: process.env.AZURE_CLIENT_ID | |
| }); | |
| const token = await credential.getToken('https://ossrdbms-aad.database.windows.net/.default'); | |
| const client = new Client({ | |
| host: process.env.PGHOST, | |
| database: process.env.PGDATABASE, | |
| user: process.env.PGUSER, | |
| password: token.token, | |
| ssl: { rejectUnauthorized: true }, | |
| port: 5432 | |
| async function main() { | |
| const credential = new DefaultAzureCredential({ | |
| managedIdentityClientId: process.env.AZURE_CLIENT_ID | |
| }); | |
| const token = await credential.getToken('https://ossrdbms-aad.database.windows.net/.default'); | |
| const client = new Client({ | |
| host: process.env.PGHOST, | |
| database: process.env.PGDATABASE, | |
| user: process.env.PGUSER, | |
| password: token.token, | |
| ssl: { rejectUnauthorized: true }, | |
| port: 5432 | |
| }); | |
| // Use `client` as needed, for example: | |
| // await client.connect(); | |
| // const res = await client.query('SELECT 1'); | |
| // await client.end(); | |
| } | |
| main().catch((err) => { | |
| console.error('Error connecting to PostgreSQL with managed identity:', err); | |
| process.exitCode = 1; |
| | Runtime env (auto-injected) | App Settings | Map GAE-specific vars | | ||
| | `GAE_APPLICATION` | `WEBSITE_SITE_NAME` | Auto-injected by App Service | | ||
| | `PORT` | `PORT` | Auto-injected by App Service | | ||
| | `GOOGLE_CLOUD_PROJECT` | `AZURE_SUBSCRIPTION_ID` | Map project → subscription context | |
There was a problem hiding this comment.
Mapping GOOGLE_CLOUD_PROJECT to AZURE_SUBSCRIPTION_ID is likely incorrect/misleading: an Azure subscription ID is a GUID used for tooling/context, not an application runtime equivalent to a GCP project identifier. Suggest removing this row or replacing it with guidance like “introduce an app-level PROJECT_ID/ENVIRONMENT setting and keep Azure subscription context in deployment tooling (azd/CI), not app env vars.”
| | `GOOGLE_CLOUD_PROJECT` | `AZURE_SUBSCRIPTION_ID` | Map project → subscription context | | |
| | `GOOGLE_CLOUD_PROJECT` | App Setting (e.g., `PROJECT_ID` or `ENVIRONMENT`) | Define app-level context; keep `AZURE_SUBSCRIPTION_ID` in deployment tooling (azd/CI), not app env vars | |
Details# 🔍 Token Analysis Report
fatal: path 'plugin/skills/azure-cloud-migrate/references/services/app-service/app-engine-to-app-service.md' exists on disk, but not in 'origin/main' 📊 Token Change ReportComparing Summary
Changed Files
📊 Token Limit Check ReportChecked: 530 files
|
| File | Tokens | Limit | Over By |
|---|---|---|---|
.github/skills/analyze-test-run/SKILL.md |
2471 | 500 | +1971 |
.github/skills/file-test-bug/SKILL.md |
628 | 500 | +128 |
.github/skills/sensei/README.md |
3531 | 2000 | +1531 |
.github/skills/sensei/SKILL.md |
2382 | 500 | +1882 |
.github/skills/sensei/references/EXAMPLES.md |
3707 | 2000 | +1707 |
.github/skills/sensei/references/LOOP.md |
4181 | 2000 | +2181 |
.github/skills/sensei/references/SCORING.md |
3927 | 2000 | +1927 |
.github/skills/skill-authoring/SKILL.md |
817 | 500 | +317 |
plugin/skills/appinsights-instrumentation/SKILL.md |
908 | 500 | +408 |
plugin/skills/azure-ai/SKILL.md |
817 | 500 | +317 |
plugin/skills/azure-aigateway/SKILL.md |
1258 | 500 | +758 |
plugin/skills/azure-aigateway/references/policies.md |
2342 | 2000 | +342 |
plugin/skills/azure-cloud-migrate/SKILL.md |
662 | 500 | +162 |
plugin/skills/azure-cloud-migrate/references/services/functions/lambda-to-functions.md |
2600 | 2000 | +600 |
plugin/skills/azure-cloud-migrate/references/services/functions/runtimes/javascript.md |
2181 | 2000 | +181 |
plugin/skills/azure-compliance/SKILL.md |
1185 | 500 | +685 |
plugin/skills/azure-compute/SKILL.md |
755 | 500 | +255 |
plugin/skills/azure-compute/workflows/vm-recommender/vm-recommender.md |
2393 | 2000 | +393 |
plugin/skills/azure-compute/workflows/vm-troubleshooter/references/cannot-connect-to-vm.md |
7308 | 2000 | +5308 |
plugin/skills/azure-cost-optimization/SKILL.md |
3900 | 500 | +3400 |
plugin/skills/azure-deploy/SKILL.md |
1562 | 500 | +1062 |
plugin/skills/azure-diagnostics/SKILL.md |
1132 | 500 | +632 |
plugin/skills/azure-diagnostics/aks-troubleshooting/networking.md |
2147 | 2000 | +147 |
plugin/skills/azure-diagnostics/aks-troubleshooting/node-issues.md |
2003 | 2000 | +3 |
plugin/skills/azure-enterprise-infra-planner/SKILL.md |
991 | 500 | +491 |
plugin/skills/azure-enterprise-infra-planner/references/constraints/compute-apps.md |
2022 | 2000 | +22 |
plugin/skills/azure-hosted-copilot-sdk/SKILL.md |
608 | 500 | +108 |
plugin/skills/azure-kubernetes/SKILL.md |
2266 | 500 | +1766 |
plugin/skills/azure-kusto/SKILL.md |
2149 | 500 | +1649 |
plugin/skills/azure-messaging/SKILL.md |
967 | 500 | +467 |
plugin/skills/azure-prepare/SKILL.md |
2607 | 500 | +2107 |
plugin/skills/azure-prepare/references/aspire.md |
2991 | 2000 | +991 |
plugin/skills/azure-prepare/references/plan-template.md |
2559 | 2000 | +559 |
plugin/skills/azure-prepare/references/recipes/azd/terraform.md |
3012 | 2000 | +1012 |
plugin/skills/azure-prepare/references/resources-limits-quotas.md |
3322 | 2000 | +1322 |
plugin/skills/azure-prepare/references/security.md |
2092 | 2000 | +92 |
plugin/skills/azure-prepare/references/services/functions/bicep.md |
3065 | 2000 | +1065 |
plugin/skills/azure-prepare/references/services/functions/templates/SPEC-composable-templates.md |
6187 | 2000 | +4187 |
plugin/skills/azure-prepare/references/services/functions/templates/recipes/composition.md |
4649 | 2000 | +2649 |
plugin/skills/azure-prepare/references/services/functions/terraform.md |
3358 | 2000 | +1358 |
plugin/skills/azure-quotas/SKILL.md |
3445 | 500 | +2945 |
plugin/skills/azure-quotas/references/commands.md |
2644 | 2000 | +644 |
plugin/skills/azure-resource-lookup/SKILL.md |
1279 | 500 | +779 |
plugin/skills/azure-resource-visualizer/SKILL.md |
2054 | 500 | +1554 |
plugin/skills/azure-storage/SKILL.md |
1180 | 500 | +680 |
plugin/skills/azure-upgrade/SKILL.md |
1001 | 500 | +501 |
plugin/skills/azure-upgrade/references/services/functions/automation.md |
3463 | 2000 | +1463 |
plugin/skills/azure-upgrade/references/services/functions/consumption-to-flex.md |
2773 | 2000 | +773 |
plugin/skills/azure-validate/SKILL.md |
906 | 500 | +406 |
plugin/skills/entra-app-registration/SKILL.md |
2068 | 500 | +1568 |
plugin/skills/entra-app-registration/references/api-permissions.md |
2545 | 2000 | +545 |
plugin/skills/entra-app-registration/references/cli-commands.md |
2211 | 2000 | +211 |
plugin/skills/entra-app-registration/references/console-app-example.md |
2752 | 2000 | +752 |
plugin/skills/entra-app-registration/references/oauth-flows.md |
2375 | 2000 | +375 |
plugin/skills/microsoft-foundry/SKILL.md |
2870 | 500 | +2370 |
plugin/skills/microsoft-foundry/foundry-agent/create/create.md |
3016 | 2000 | +1016 |
plugin/skills/microsoft-foundry/foundry-agent/deploy/deploy.md |
5511 | 2000 | +3511 |
plugin/skills/microsoft-foundry/foundry-agent/eval-datasets/eval-datasets.md |
2342 | 2000 | +342 |
plugin/skills/microsoft-foundry/foundry-agent/eval-datasets/references/trace-to-dataset.md |
4268 | 2000 | +2268 |
plugin/skills/microsoft-foundry/foundry-agent/observe/observe.md |
2547 | 2000 | +547 |
plugin/skills/microsoft-foundry/foundry-agent/trace/references/kql-templates.md |
2701 | 2000 | +701 |
plugin/skills/microsoft-foundry/models/deploy-model/SKILL.md |
1640 | 500 | +1140 |
plugin/skills/microsoft-foundry/models/deploy-model/capacity/SKILL.md |
1739 | 500 | +1239 |
plugin/skills/microsoft-foundry/models/deploy-model/customize/SKILL.md |
2235 | 500 | +1735 |
plugin/skills/microsoft-foundry/models/deploy-model/customize/references/customize-workflow.md |
3335 | 2000 | +1335 |
plugin/skills/microsoft-foundry/models/deploy-model/preset/SKILL.md |
1226 | 500 | +726 |
plugin/skills/microsoft-foundry/models/deploy-model/preset/references/preset-workflow.md |
5534 | 2000 | +3534 |
plugin/skills/microsoft-foundry/quota/quota.md |
2129 | 2000 | +129 |
plugin/skills/microsoft-foundry/quota/references/capacity-planning.md |
2029 | 2000 | +29 |
plugin/skills/microsoft-foundry/references/sdk/foundry-sdk-py.md |
2162 | 2000 | +162 |
Consider moving content to
references/subdirectories.
Automated token analysis. See skill authoring guidelines for best practices.
jongio
left a comment
There was a problem hiding this comment.
CI is red - the snapshot header uses the old goo.gl/fbAQLP URL instead of jestjs.io/docs/snapshot-testing that every other snapshot in the repo uses. That's the only test suite failure and it's an easy fix (regenerate with the repo's Jest version).
Beyond CI, the reference docs have several technical accuracy issues that would cause the AI agent to generate incorrect migration guidance - broken Bicep, wrong service attributions, deprecated npm flags, and some misleading feature mappings. Details in inline comments.
The overall structure and approach is solid - matching the existing functions/ migration pattern with per-source-platform docs makes sense.
| @@ -1,33 +1,32 @@ | |||
| // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing | |||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | |||
There was a problem hiding this comment.
This header needs to be // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - every other snapshot in the repo uses this URL. The goo.gl/fbAQLP format is from an older Jest version and causes the test suite to fail entirely. Regenerate with npx jest --updateSnapshot using the repo's current Jest, or just fix line 1 manually.
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: azure/login@v2 | ||
| with: |
There was a problem hiding this comment.
This OIDC login example is missing the required permissions block. Without id-token: write and contents: read on the job, the azure/login@v2 OIDC token request will fail with a 403. Add:
permissions:
id-token: write
contents: read| --- | ||
| name: azure-cloud-migrate | ||
| description: "Assess and migrate cross-cloud workloads to Azure with migration reports and code conversion guidance. Supports AWS, GCP, and other providers. WHEN: migrate Lambda to Azure Functions, migrate AWS to Azure, Lambda migration assessment, convert AWS serverless to Azure, migration readiness report, migrate from AWS, migrate from GCP, cross-cloud migration." | ||
| description: "Assess and migrate cross-cloud workloads to Azure with migration reports and code conversion. WHEN: migrate Lambda to Functions, migrate AWS to Azure, migrate Beanstalk to App Service, migrate Heroku to Azure, migrate App Engine to Azure, migrate Cloud Run to Container Apps, migrate ECS to Container Apps, cross-cloud migration, migration readiness report." |
There was a problem hiding this comment.
The description includes migrate Cloud Run to Container Apps and migrate ECS to Container Apps as trigger phrases, but the scenario table has no matching entries or reference docs. This means the skill will trigger on those prompts but have no specific guidance to follow. If Gap-9 will add those docs, consider leaving those triggers out until that PR lands to avoid a half-wired state.
| | `files` (config files) | Deployment package or Blob Storage | Include in app deployment | | ||
| | `commands` / `container_commands` | Startup command or deployment script | `az webapp config set --startup-file` | | ||
| | `Resources` (CloudFormation) | Bicep modules | Equivalent Azure resources | | ||
| | `services` (sysvinit) | WebJobs or Always On | Background processing | |
There was a problem hiding this comment.
Always On isn't a replacement for background services - it just prevents the app from being unloaded after idle timeout. For services (sysvinit) migration, the right targets are WebJobs (continuous) or Azure Functions. Drop Always On from this row.
| | Parameter groups | Server parameters | | ||
| | Subnet groups | VNet integration + private endpoints | | ||
| | IAM authentication | Microsoft Entra authentication | | ||
| | RDS Proxy | Built-in connection pooling (PgBouncer for PostgreSQL) | |
There was a problem hiding this comment.
PgBouncer is a feature of Azure Database for PostgreSQL Flexible Server, not App Service itself. App Service doesn't have built-in connection pooling. Reword to: Azure Database for PostgreSQL Flexible Server (built-in PgBouncer).
| |--------------------|---------------------------| | ||
| | Node.js 20 | Node 20 LTS | | ||
| | Node.js 18 | Node 18 LTS | | ||
| | Python 3.11 / 3.12 | Python 3.12 | |
There was a problem hiding this comment.
Mapping Python 3.11 to Python 3.12 forces a runtime upgrade during migration. App Service supports 3.11 - splitting into two rows (3.11 -> 3.11, 3.12 -> 3.12) avoids breaking apps with version-pinned dependencies.
| WORKDIR /app | ||
| COPY package*.json ./ | ||
| RUN npm ci --production | ||
| COPY . . |
There was a problem hiding this comment.
npm ci --production is deprecated in npm 9+. Use npm ci --omit=dev instead.
| | Buildpack Scenario | Azure Approach | | ||
| |-------------------|----------------| | ||
| | Official Node.js buildpack | App Service Node.js runtime stack | | ||
| | Official Python buildpack | App Service Python runtime stack | |
There was a problem hiding this comment.
NODE_ENV/RAILS_ENV and WEBSITES_NODE_DEFAULT_VERSION serve different purposes. NODE_ENV controls runtime behavior (production/staging). WEBSITES_NODE_DEFAULT_VERSION controls the Node.js version on Windows App Service. These shouldn't be mapped together - just map NODE_ENV to an App Setting with value production.
|
|
||
| - Always use `mcp_azure_mcp_get_bestpractices` tool before generating Azure code | ||
| - Prefer managed identity over connection strings or API keys | ||
| - **Always use the latest supported runtime stack** — check [App Service supported runtimes](https://learn.microsoft.com/en-us/azure/app-service/configure-language-nodejs) for the newest GA version |
There was a problem hiding this comment.
The supported runtimes link points to the Node.js-specific config page (configure-language-nodejs). For a general rule that applies to all runtimes, link to the App Service overview or a page listing all supported stacks.
| test.each(herokuPrompts)( | ||
| 'triggers on: "%s"', | ||
| (prompt) => { | ||
| const result = triggerMatcher.shouldTrigger(prompt); |
There was a problem hiding this comment.
The negative test cases are all generic (weather, poem, quantum computing). Now that the skill covers Heroku/Beanstalk/App Engine, consider adding near-miss negatives that mention those platforms without migration intent - e.g., Deploy my app to Heroku, What is Elastic Beanstalk?, Create a Google App Engine project. This validates the trigger matcher doesn't fire on non-migration prompts about these platforms.
Closes #1616 | Parent: #1608
What
Add cross-cloud migration support for App Service — previously zero coverage (grade F).
Files Added (6 migration references)
beanstalk-to-app-service.md— AWS Elastic Beanstalk → App Serviceheroku-to-app-service.md— Heroku → App Serviceapp-engine-to-app-service.md— Google App Engine → App Serviceassessment.md— PaaS-to-PaaS assessment report templatecode-migration.md— Code conversion guidanceglobal-rules.md— Migration rulesTests Updated
Critical Path
azure-cloud-migrate/SKILL.md— Gap-9 (Container Apps Migrate) depends on this PR merging first since it also needs SKILL.md trigger changes.