From f631c053b6acd3d13cbb86937afa33964977035d Mon Sep 17 00:00:00 2001 From: "Tom Meschter (from Dev Box)" Date: Fri, 5 Jun 2026 14:45:36 -0700 Subject: [PATCH 1/6] feat: replace azure-prepare Aspire detection sequence with a script Add cross-platform detect-aspire.sh/.ps1 scripts that run the .NET Aspire detection sequence in one pass and emit key=value fields plus an informational summary. Update aspire.md, generate.md, and scan.md to invoke the script instead of re-deriving find/grep commands, keeping decision-making in the reference files. Fixes #2494 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../skills/azure-prepare/references/aspire.md | 75 +++++----- .../azure-prepare/references/generate.md | 14 +- .../skills/azure-prepare/references/scan.md | 19 ++- .../references/scripts/detect-aspire.ps1 | 130 ++++++++++++++++++ .../references/scripts/detect-aspire.sh | 110 +++++++++++++++ 5 files changed, 304 insertions(+), 44 deletions(-) create mode 100644 plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 create mode 100644 plugin/skills/azure-prepare/references/scripts/detect-aspire.sh diff --git a/plugin/skills/azure-prepare/references/aspire.md b/plugin/skills/azure-prepare/references/aspire.md index 96361b131..335692e14 100644 --- a/plugin/skills/azure-prepare/references/aspire.md +++ b/plugin/skills/azure-prepare/references/aspire.md @@ -40,36 +40,49 @@ orleans-voting/ ### Step 1: Detection -When scanning the codebase (per [scan.md](scan.md)), detect Aspire by: +When scanning the codebase (per [scan.md](scan.md)), run the detection script ([detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1)). It performs the full deterministic detection sequence in one pass and prints `key=value` lines plus a human-readable summary, so you can branch on the result instead of parsing raw `find`/`grep` output. +**bash:** ```bash -# Check for AppHost project -find . -name "*.AppHost.csproj" +./scripts/detect-aspire.sh [workspace-root] +``` -# Or check for Aspire.Hosting package reference -grep -r "Aspire.Hosting" . --include="*.csproj" +**PowerShell:** +```powershell +./scripts/detect-aspire.ps1 -WorkspaceRoot ``` -### ⛔ Step 1a: Pre-Check for Custom/Non-Deployable Resources (MANDATORY) +`workspace-root` defaults to the current directory. The script reports these fields: -**Before running `azd init --from-code`, scan the AppHost source code to understand whether the app may contain local-only custom resources.** +| Field | Meaning | +|-------|---------| +| `isAspire` | `true` if a `*.AppHost.csproj` or `Aspire.Hosting` package reference was found | +| `appHostPath` | Path to the AppHost project file (empty if none) | +| `appHostDir` | AppHost source directory derived from `appHostPath` | +| `hasExcludeFromManifest` | `true` if `ExcludeFromManifest` appears in AppHost `*.cs` (informational — see Step 1a) | +| `hasFunctions` | `true` if `AddAzureFunctionsProject` appears in AppHost `*.cs` (see Step 4b) | +| `secretStorageConfigured` | `true` if `AzureWebJobsSecretStorageType` is already configured (see Step 4b) | -```bash -# Find the AppHost project and scan only its source directory -APPHOST_PROJECT=$(find . -name "*.AppHost.csproj" | head -1) -APPHOST_DIR=$(dirname "$APPHOST_PROJECT") -grep -r "ExcludeFromManifest" "$APPHOST_DIR" --include="*.cs" | head -20 +**Example output:** ``` - -**PowerShell:** -```powershell -# Find the AppHost project and scan only its source directory -$appHostProject = Get-ChildItem -Recurse -Filter "*.AppHost.csproj" | Select-Object -First 1 -$appHostDir = $appHostProject.DirectoryName -Get-ChildItem -Path $appHostDir -Recurse -Filter "*.cs" | Select-String "ExcludeFromManifest" | Select-Object -First 20 +isAspire=true +appHostPath=./OrleansVoting.AppHost/OrleansVoting.AppHost.csproj +appHostDir=./OrleansVoting.AppHost +hasExcludeFromManifest=false +hasFunctions=true +secretStorageConfigured=false ``` -This scan is informational. `.ExcludeFromManifest()` can appear alongside deployable resources, so a positive match does **not** immediately block deployment. What matters is the final `azure.yaml` output after `azd init --from-code` completes: +If `isAspire=false`, this is not an Aspire app — continue with the normal recipe selection. + +### ⛔ Step 1a: Pre-Check for Custom/Non-Deployable Resources (MANDATORY) + +**Before running `azd init --from-code`, understand whether the app may contain local-only custom resources.** The detection script from Step 1 already reports this as the `hasExcludeFromManifest` field — no separate scan is needed. + +- `hasExcludeFromManifest=true` → the AppHost source uses `.ExcludeFromManifest()`. +- `hasExcludeFromManifest=false` → no such usage was found. + +This signal is informational. `.ExcludeFromManifest()` can appear alongside deployable resources, so a positive match does **not** immediately block deployment. What matters is the final `azure.yaml` output after `azd init --from-code` completes: - If `azd init` **fails** with `unsupported resource type` → see Step 2 error guidance below. - If `azd init` **succeeds** but `azure.yaml` has an empty or missing `services` section → see Step 4a below. @@ -185,31 +198,25 @@ This step **MUST** run BEFORE `azd up` or `azd provision`. Skipping it causes a **1. Detect Azure Functions in the AppHost:** +Use the `hasFunctions` field from the Step 1 detection script. If you have not run it yet (or the workspace changed), re-run it: + +**bash:** ```bash -APPHOST_DIR=$(dirname "$(find . -name '*.AppHost.csproj' | head -1)") -grep -n "AddAzureFunctionsProject" "$APPHOST_DIR"/*.cs +./scripts/detect-aspire.sh [workspace-root] ``` **PowerShell:** ```powershell -$appHostDir = (Get-ChildItem -Recurse -Filter "*.AppHost.csproj" | Select-Object -First 1).DirectoryName -Get-ChildItem -Path $appHostDir -Filter "*.cs" | Select-String "AddAzureFunctionsProject" +./scripts/detect-aspire.ps1 -WorkspaceRoot ``` -**If `AddAzureFunctionsProject` is NOT found → skip this step.** +**If `hasFunctions=false` → skip this step.** **2. Check if `AzureWebJobsSecretStorageType` is already configured:** -```bash -grep -n "AzureWebJobsSecretStorageType" "$APPHOST_DIR"/*.cs -``` - -**PowerShell:** -```powershell -Get-ChildItem -Path $appHostDir -Filter "*.cs" | Select-String "AzureWebJobsSecretStorageType" -``` +The same script reports this as the `secretStorageConfigured` field. -**If already present → skip this step.** +**If `secretStorageConfigured=true` → skip this step.** **3. Add the environment variable to the Functions builder chain:** diff --git a/plugin/skills/azure-prepare/references/generate.md b/plugin/skills/azure-prepare/references/generate.md index fb2917fb5..526ce0602 100644 --- a/plugin/skills/azure-prepare/references/generate.md +++ b/plugin/skills/azure-prepare/references/generate.md @@ -4,16 +4,20 @@ Generate infrastructure and configuration files based on selected recipe. ## ⛔ CRITICAL: Check for .NET Aspire Projects FIRST -**MANDATORY: Before generating any files, detect .NET Aspire projects:** +**MANDATORY: Before generating any files, detect .NET Aspire projects** using the detection script ([detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1)). It runs the full detection sequence and prints `key=value` fields plus a summary: +**bash:** ```bash -# Method 1: Find AppHost project files -find . -name "*.AppHost.csproj" -o -name "*AppHost.csproj" +./scripts/detect-aspire.sh [workspace-root] +``` -# Method 2: Search for Aspire packages -grep -r "Aspire\.Hosting\|Aspire\.AppHost\.Sdk" . --include="*.csproj" +**PowerShell:** +```powershell +./scripts/detect-aspire.ps1 -WorkspaceRoot ``` +If the result includes `isAspire=true`, treat the project as Aspire. See [aspire.md](aspire.md) Step 1 for the full list of fields the script reports. + **If Aspire is detected:** 1. ⛔ **STOP** - Do NOT manually create `azure.yaml` 2. ⛔ **STOP** - Do NOT manually create `infra/` files diff --git a/plugin/skills/azure-prepare/references/scan.md b/plugin/skills/azure-prepare/references/scan.md index 8f36c5af2..361e958d8 100644 --- a/plugin/skills/azure-prepare/references/scan.md +++ b/plugin/skills/azure-prepare/references/scan.md @@ -56,16 +56,25 @@ Before classifying components, grep dependency files for SDKs that require a spe ### .NET Aspire Detection -**.NET Aspire projects** are identified by: -- A project ending with `.AppHost.csproj` (e.g., `OrleansVoting.AppHost.csproj`) -- Reference to `Aspire.Hosting` or `Aspire.Hosting.AppHost` package in .csproj files -- Multiple .NET projects in a solution, typically including an AppHost orchestrator +Detect **.NET Aspire projects** by running the detection script ([detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1)), which reports `isAspire` along with AppHost path, `ExcludeFromManifest`, and Azure Functions signals: + +**bash:** +```bash +./scripts/detect-aspire.sh [workspace-root] +``` + +**PowerShell:** +```powershell +./scripts/detect-aspire.ps1 -WorkspaceRoot +``` + +A project is Aspire when the script returns `isAspire=true` (a `*.AppHost.csproj` or an `Aspire.Hosting` / `Aspire.Hosting.AppHost` package reference was found). See [aspire.md](aspire.md) Step 1 for the full field list. **When Aspire is detected:** - Use `azd init --from-code -e ` instead of manual azure.yaml creation - The `--from-code` flag automatically detects the AppHost and generates appropriate configuration - The `-e` flag is **required** for non-interactive environments (agents, CI/CD) -- ⚠️ **CRITICAL:** If the AppHost contains `AddAzureFunctionsProject`, you **MUST** add `.WithEnvironment("AzureWebJobsSecretStorageType", "Files")` to the Functions builder chain BEFORE deployment. Without this, Functions will fail at startup with `Secret initialization from Blob storage failed`. See [aspire.md](aspire.md) Step 4b for the complete detection and fix procedure. +- ⚠️ **CRITICAL:** If the script reports `hasFunctions=true` and `secretStorageConfigured=false`, you **MUST** add `.WithEnvironment("AzureWebJobsSecretStorageType", "Files")` to the Functions builder chain BEFORE deployment. Without this, Functions will fail at startup with `Secret initialization from Blob storage failed`. See [aspire.md](aspire.md) Step 4b for the complete fix procedure. - See [aspire.md](aspire.md) for detailed Aspire-specific guidance ## Output diff --git a/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 b/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 new file mode 100644 index 000000000..c4f800125 --- /dev/null +++ b/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 @@ -0,0 +1,130 @@ +<# +.SYNOPSIS + Detects whether a workspace is a .NET Aspire application and gathers the + facts the azure-prepare skill needs to plan deployment. + +.DESCRIPTION + Runs the full deterministic detection sequence in one pass: + 1. Find the AppHost project (*.AppHost.csproj) + 2. Confirm Aspire.Hosting package references + 3. Derive the AppHost source directory + 4. Scan the AppHost *.cs for ExcludeFromManifest (informational) + 5. Scan for AddAzureFunctionsProject, and if present, check whether + AzureWebJobsSecretStorageType is already configured + + Output: key=value lines the agent can branch on, followed by a + human-readable summary. The remediation decision (whether/how to add + .WithEnvironment("AzureWebJobsSecretStorageType", "Files")) stays with the agent. + +.PARAMETER WorkspaceRoot + Workspace root directory to scan. Defaults to the current directory. + +.EXAMPLE + ./detect-aspire.ps1 + Scan the current directory. + +.EXAMPLE + ./detect-aspire.ps1 -WorkspaceRoot ./src/MyApp + Scan a specific workspace root. +#> + +param( + [string]$WorkspaceRoot = "." +) + +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path -LiteralPath $WorkspaceRoot -PathType Container)) { + Write-Error "workspace root '$WorkspaceRoot' is not a directory" + exit 1 +} + +# Defaults (emitted when the workspace is not an Aspire app) +$isAspire = $false +$appHostPath = "" +$appHostDir = "" +$hasExcludeFromManifest = $false +$hasFunctions = $false +$secretStorageConfigured = $false + +# Step 1: Find the AppHost project +$appHostProject = Get-ChildItem -Path $WorkspaceRoot -Recurse -Filter "*.AppHost.csproj" -File -ErrorAction SilentlyContinue | + Select-Object -First 1 + +# Step 2: Confirm Aspire.Hosting package references anywhere in the workspace +$hasAspirePackage = $false +$csprojFiles = Get-ChildItem -Path $WorkspaceRoot -Recurse -Filter "*.csproj" -File -ErrorAction SilentlyContinue +if ($csprojFiles) { + if ($csprojFiles | Select-String -Pattern "Aspire.Hosting" -SimpleMatch -List -ErrorAction SilentlyContinue) { + $hasAspirePackage = $true + } +} + +if ($appHostProject -or $hasAspirePackage) { + $isAspire = $true +} + +if ($appHostProject) { + $appHostPath = $appHostProject.FullName + + # Step 3: Derive the AppHost source directory + $appHostDir = $appHostProject.DirectoryName + + $appHostCs = Get-ChildItem -Path $appHostDir -Recurse -Filter "*.cs" -File -ErrorAction SilentlyContinue + + # Step 4: Scan the AppHost source for ExcludeFromManifest (informational) + if ($appHostCs -and ($appHostCs | Select-String -Pattern "ExcludeFromManifest" -SimpleMatch -List -ErrorAction SilentlyContinue)) { + $hasExcludeFromManifest = $true + } + + # Step 5: Detect Azure Functions and secret-storage configuration + if ($appHostCs -and ($appHostCs | Select-String -Pattern "AddAzureFunctionsProject" -SimpleMatch -List -ErrorAction SilentlyContinue)) { + $hasFunctions = $true + if ($appHostCs | Select-String -Pattern "AzureWebJobsSecretStorageType" -SimpleMatch -List -ErrorAction SilentlyContinue) { + $secretStorageConfigured = $true + } + } +} + +function ConvertTo-Lower([bool]$value) { + if ($value) { "true" } else { "false" } +} + +# Machine-readable result +Write-Output "isAspire=$(ConvertTo-Lower $isAspire)" +Write-Output "appHostPath=$appHostPath" +Write-Output "appHostDir=$appHostDir" +Write-Output "hasExcludeFromManifest=$(ConvertTo-Lower $hasExcludeFromManifest)" +Write-Output "hasFunctions=$(ConvertTo-Lower $hasFunctions)" +Write-Output "secretStorageConfigured=$(ConvertTo-Lower $secretStorageConfigured)" + +# Human-readable summary +Write-Output "" +Write-Output "Summary:" +if (-not $isAspire) { + Write-Output "- No .NET Aspire app detected in '$WorkspaceRoot' (no *.AppHost.csproj or Aspire.Hosting package reference)." + exit 0 +} + +if ($appHostPath) { + Write-Output "- .NET Aspire app detected. AppHost project: $appHostPath" + Write-Output "- AppHost source directory: $appHostDir" +} else { + Write-Output "- Aspire.Hosting package reference found, but no *.AppHost.csproj was located." +} + +if ($hasExcludeFromManifest) { + Write-Output "- ExcludeFromManifest found in AppHost source (informational): the app may contain local-only resources." +} else { + Write-Output "- No ExcludeFromManifest usage found in AppHost source." +} + +if ($hasFunctions) { + if ($secretStorageConfigured) { + Write-Output "- AddAzureFunctionsProject found; AzureWebJobsSecretStorageType is configured in the AppHost source." + } else { + Write-Output "- AddAzureFunctionsProject found; AzureWebJobsSecretStorageType is not configured in the AppHost source." + } +} else { + Write-Output "- AddAzureFunctionsProject not found in AppHost source." +} diff --git a/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh b/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh new file mode 100644 index 000000000..e976d25ba --- /dev/null +++ b/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# detect-aspire.sh +# Detects whether a workspace is a .NET Aspire application and gathers the +# facts the azure-prepare skill needs to plan deployment. +# +# It runs the full deterministic detection sequence in one pass: +# 1. Find the AppHost project (*.AppHost.csproj) +# 2. Confirm Aspire.Hosting package references +# 3. Derive the AppHost source directory +# 4. Scan the AppHost *.cs for ExcludeFromManifest (informational) +# 5. Scan for AddAzureFunctionsProject, and if present, check whether +# AzureWebJobsSecretStorageType is already configured +# +# Output: key=value lines the agent can branch on, followed by a human-readable +# summary. The remediation decision (whether/how to add +# .WithEnvironment("AzureWebJobsSecretStorageType", "Files")) stays with the agent. +# +# Usage: +# ./detect-aspire.sh [workspace-root] +# +# Examples: +# ./detect-aspire.sh # Scan the current directory +# ./detect-aspire.sh ./src/MyApp # Scan a specific workspace root + +set -euo pipefail + +WORKSPACE_ROOT="${1:-.}" + +if [ ! -d "$WORKSPACE_ROOT" ]; then + echo "Error: workspace root '$WORKSPACE_ROOT' is not a directory" >&2 + exit 1 +fi + +# Defaults (emitted when the workspace is not an Aspire app) +IS_ASPIRE="false" +APPHOST_PATH="" +APPHOST_DIR="" +HAS_EXCLUDE_FROM_MANIFEST="false" +HAS_FUNCTIONS="false" +SECRET_STORAGE_CONFIGURED="false" + +# Step 1: Find the AppHost project +APPHOST_PATH=$(find "$WORKSPACE_ROOT" -name "*.AppHost.csproj" 2>/dev/null | head -1 || true) + +# Step 2: Confirm Aspire.Hosting package references anywhere in the workspace +HAS_ASPIRE_PACKAGE="false" +if grep -rql "Aspire.Hosting" "$WORKSPACE_ROOT" --include="*.csproj" 2>/dev/null; then + HAS_ASPIRE_PACKAGE="true" +fi + +if [ -n "$APPHOST_PATH" ] || [ "$HAS_ASPIRE_PACKAGE" = "true" ]; then + IS_ASPIRE="true" +fi + +if [ -n "$APPHOST_PATH" ]; then + # Step 3: Derive the AppHost source directory + APPHOST_DIR=$(dirname "$APPHOST_PATH") + + # Step 4: Scan the AppHost source for ExcludeFromManifest (informational) + if grep -rq "ExcludeFromManifest" "$APPHOST_DIR" --include="*.cs" 2>/dev/null; then + HAS_EXCLUDE_FROM_MANIFEST="true" + fi + + # Step 5: Detect Azure Functions and secret-storage configuration + if grep -rq "AddAzureFunctionsProject" "$APPHOST_DIR" --include="*.cs" 2>/dev/null; then + HAS_FUNCTIONS="true" + if grep -rq "AzureWebJobsSecretStorageType" "$APPHOST_DIR" --include="*.cs" 2>/dev/null; then + SECRET_STORAGE_CONFIGURED="true" + fi + fi +fi + +# Machine-readable result +echo "isAspire=$IS_ASPIRE" +echo "appHostPath=$APPHOST_PATH" +echo "appHostDir=$APPHOST_DIR" +echo "hasExcludeFromManifest=$HAS_EXCLUDE_FROM_MANIFEST" +echo "hasFunctions=$HAS_FUNCTIONS" +echo "secretStorageConfigured=$SECRET_STORAGE_CONFIGURED" + +# Human-readable summary +echo "" +echo "Summary:" +if [ "$IS_ASPIRE" != "true" ]; then + echo "- No .NET Aspire app detected in '$WORKSPACE_ROOT' (no *.AppHost.csproj or Aspire.Hosting package reference)." + exit 0 +fi + +if [ -n "$APPHOST_PATH" ]; then + echo "- .NET Aspire app detected. AppHost project: $APPHOST_PATH" + echo "- AppHost source directory: $APPHOST_DIR" +else + echo "- Aspire.Hosting package reference found, but no *.AppHost.csproj was located." +fi + +if [ "$HAS_EXCLUDE_FROM_MANIFEST" = "true" ]; then + echo "- ExcludeFromManifest found in AppHost source (informational): the app may contain local-only resources." +else + echo "- No ExcludeFromManifest usage found in AppHost source." +fi + +if [ "$HAS_FUNCTIONS" = "true" ]; then + if [ "$SECRET_STORAGE_CONFIGURED" = "true" ]; then + echo "- AddAzureFunctionsProject found; AzureWebJobsSecretStorageType is configured in the AppHost source." + else + echo "- AddAzureFunctionsProject found; AzureWebJobsSecretStorageType is not configured in the AppHost source." + fi +else + echo "- AddAzureFunctionsProject not found in AppHost source." +fi From c4c63a64c64a558ca0fc681775eeed5d54501254 Mon Sep 17 00:00:00 2001 From: "Tom Meschter (from Dev Box)" Date: Fri, 5 Jun 2026 14:56:38 -0700 Subject: [PATCH 2/6] test: verify detect-aspire script runs in azure-prepare aspire-brownfield tests Assert via matchesCommand that the agent invokes detect-aspire.sh/.ps1 rather than re-deriving the find/grep detection sequence inline, in the two aspire-brownfield integration tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/azure-prepare/integration.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/azure-prepare/integration.test.ts b/tests/azure-prepare/integration.test.ts index 3aa21c8b8..52807577f 100644 --- a/tests/azure-prepare/integration.test.ts +++ b/tests/azure-prepare/integration.test.ts @@ -17,7 +17,7 @@ import { import { hasValidationCommand } from "../azure-validate/utils"; import { hasPlanReadyForValidation, hasServicesSection, getServiceProject } from "./utils"; import { cloneRepo } from "../utils/git-clone"; -import { doesWorkspaceFileIncludePattern, expectFiles, softCheckSkill, isSkillInvoked, shouldEarlyTerminateForSkillInvocation, withTestResult } from "../utils/evaluate"; +import { doesWorkspaceFileIncludePattern, expectFiles, softCheckSkill, isSkillInvoked, matchesCommand, shouldEarlyTerminateForSkillInvocation, withTestResult } from "../utils/evaluate"; const SKILL_NAME = "azure-prepare"; const RUNS_PER_PROMPT = 1; @@ -855,6 +855,10 @@ describeIntegration(`${SKILL_NAME}_ - Integration Tests`, () => { expect(workspacePath).toBeDefined(); expect(isSkillInvoked(agentMetadata, SKILL_NAME)).toBe(true); + // Verify the agent ran the detect-aspire detection script rather than + // re-deriving the find/grep detection sequence inline. + expect(matchesCommand(agentMetadata, /detect-aspire\.(sh|ps1)/)).toBe(true); + // Verify azure.yaml exists expectFiles(workspacePath!, [/azure\.yaml$/], []); @@ -900,6 +904,11 @@ describeIntegration(`${SKILL_NAME}_ - Integration Tests`, () => { expect(workspacePath).toBeDefined(); expect(isSkillInvoked(agentMetadata, SKILL_NAME)).toBe(true); + + // Verify the agent ran the detect-aspire detection script rather than + // re-deriving the find/grep detection sequence inline. + expect(matchesCommand(agentMetadata, /detect-aspire\.(sh|ps1)/)).toBe(true); + expectFiles(workspacePath!, [/azure\.yaml$/], []); // For Aspire projects, azd init --from-code generates a single "app" service From 399550b8b8d9ee085b76546f68bdc9a13375f3eb Mon Sep 17 00:00:00 2001 From: "Tom Meschter (from Dev Box)" Date: Mon, 8 Jun 2026 09:58:08 -0700 Subject: [PATCH 3/6] fix: address PR #2576 review feedback for detect-aspire scripts - Normalize appHostPath/appHostDir to workspace-relative forward-slash paths in both shells - Replace GNU-only grep --include with portable find -exec grep (macOS/BSD safe) - Use -LiteralPath in PowerShell Get-ChildItem to avoid wildcard interpretation - Prune bin/obj from *.cs scans - Sort AppHost candidates (case-insensitive) for deterministic, cross-shell-consistent selection - Treat Aspire.AppHost.Sdk as an Aspire indicator; update summaries and scan.md/aspire.md docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../skills/azure-prepare/references/aspire.md | 2 +- .../skills/azure-prepare/references/scan.md | 2 +- .../references/scripts/detect-aspire.ps1 | 33 +++++++---- .../references/scripts/detect-aspire.sh | 56 ++++++++++++++----- 4 files changed, 66 insertions(+), 27 deletions(-) diff --git a/plugin/skills/azure-prepare/references/aspire.md b/plugin/skills/azure-prepare/references/aspire.md index 335692e14..e057a53cb 100644 --- a/plugin/skills/azure-prepare/references/aspire.md +++ b/plugin/skills/azure-prepare/references/aspire.md @@ -56,7 +56,7 @@ When scanning the codebase (per [scan.md](scan.md)), run the detection script ([ | Field | Meaning | |-------|---------| -| `isAspire` | `true` if a `*.AppHost.csproj` or `Aspire.Hosting` package reference was found | +| `isAspire` | `true` if a `*.AppHost.csproj` or an `Aspire.Hosting` / `Aspire.AppHost.Sdk` package reference was found | | `appHostPath` | Path to the AppHost project file (empty if none) | | `appHostDir` | AppHost source directory derived from `appHostPath` | | `hasExcludeFromManifest` | `true` if `ExcludeFromManifest` appears in AppHost `*.cs` (informational — see Step 1a) | diff --git a/plugin/skills/azure-prepare/references/scan.md b/plugin/skills/azure-prepare/references/scan.md index 361e958d8..e5fa20e75 100644 --- a/plugin/skills/azure-prepare/references/scan.md +++ b/plugin/skills/azure-prepare/references/scan.md @@ -68,7 +68,7 @@ Detect **.NET Aspire projects** by running the detection script ([detect-aspire. ./scripts/detect-aspire.ps1 -WorkspaceRoot ``` -A project is Aspire when the script returns `isAspire=true` (a `*.AppHost.csproj` or an `Aspire.Hosting` / `Aspire.Hosting.AppHost` package reference was found). See [aspire.md](aspire.md) Step 1 for the full field list. +A project is Aspire when the script returns `isAspire=true` (a `*.AppHost.csproj` or an `Aspire.Hosting` / `Aspire.Hosting.AppHost` / `Aspire.AppHost.Sdk` package reference was found). See [aspire.md](aspire.md) Step 1 for the full field list. **When Aspire is detected:** - Use `azd init --from-code -e ` instead of manual azure.yaml creation diff --git a/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 b/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 index c4f800125..90620c3d5 100644 --- a/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 +++ b/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 @@ -6,7 +6,7 @@ .DESCRIPTION Runs the full deterministic detection sequence in one pass: 1. Find the AppHost project (*.AppHost.csproj) - 2. Confirm Aspire.Hosting package references + 2. Confirm Aspire.Hosting or Aspire.AppHost.Sdk package references 3. Derive the AppHost source directory 4. Scan the AppHost *.cs for ExcludeFromManifest (informational) 5. Scan for AddAzureFunctionsProject, and if present, check whether @@ -47,15 +47,15 @@ $hasExcludeFromManifest = $false $hasFunctions = $false $secretStorageConfigured = $false -# Step 1: Find the AppHost project -$appHostProject = Get-ChildItem -Path $WorkspaceRoot -Recurse -Filter "*.AppHost.csproj" -File -ErrorAction SilentlyContinue | - Select-Object -First 1 +# Step 1: Find the AppHost project (sorted for deterministic selection) +$appHostProject = @(Get-ChildItem -LiteralPath $WorkspaceRoot -Recurse -Filter "*.AppHost.csproj" -File -ErrorAction SilentlyContinue | + Sort-Object FullName) | Select-Object -First 1 -# Step 2: Confirm Aspire.Hosting package references anywhere in the workspace +# Step 2: Confirm Aspire package references anywhere in the workspace $hasAspirePackage = $false -$csprojFiles = Get-ChildItem -Path $WorkspaceRoot -Recurse -Filter "*.csproj" -File -ErrorAction SilentlyContinue +$csprojFiles = Get-ChildItem -LiteralPath $WorkspaceRoot -Recurse -Filter "*.csproj" -File -ErrorAction SilentlyContinue if ($csprojFiles) { - if ($csprojFiles | Select-String -Pattern "Aspire.Hosting" -SimpleMatch -List -ErrorAction SilentlyContinue) { + if ($csprojFiles | Select-String -Pattern "Aspire\.Hosting|Aspire\.AppHost\.Sdk" -List -ErrorAction SilentlyContinue) { $hasAspirePackage = $true } } @@ -65,12 +65,21 @@ if ($appHostProject -or $hasAspirePackage) { } if ($appHostProject) { - $appHostPath = $appHostProject.FullName + # Emit a workspace-relative, forward-slash path (matches the bash output contract) + Push-Location -LiteralPath $WorkspaceRoot + try { + $appHostPath = (Resolve-Path -LiteralPath $appHostProject.FullName -Relative) -replace '\\', '/' + } finally { + Pop-Location + } # Step 3: Derive the AppHost source directory - $appHostDir = $appHostProject.DirectoryName + $appHostDir = $appHostPath -replace '/[^/]+$', '' + $appHostDirFull = $appHostProject.DirectoryName - $appHostCs = Get-ChildItem -Path $appHostDir -Recurse -Filter "*.cs" -File -ErrorAction SilentlyContinue + # Scan AppHost *.cs, excluding bin/ and obj/ build output + $appHostCs = Get-ChildItem -LiteralPath $appHostDirFull -Recurse -Filter "*.cs" -File -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch '[\\/](bin|obj)[\\/]' } # Step 4: Scan the AppHost source for ExcludeFromManifest (informational) if ($appHostCs -and ($appHostCs | Select-String -Pattern "ExcludeFromManifest" -SimpleMatch -List -ErrorAction SilentlyContinue)) { @@ -102,7 +111,7 @@ Write-Output "secretStorageConfigured=$(ConvertTo-Lower $secretStorageConfigured Write-Output "" Write-Output "Summary:" if (-not $isAspire) { - Write-Output "- No .NET Aspire app detected in '$WorkspaceRoot' (no *.AppHost.csproj or Aspire.Hosting package reference)." + Write-Output "- No .NET Aspire app detected in '$WorkspaceRoot' (no *.AppHost.csproj or Aspire.Hosting / Aspire.AppHost.Sdk package reference)." exit 0 } @@ -110,7 +119,7 @@ if ($appHostPath) { Write-Output "- .NET Aspire app detected. AppHost project: $appHostPath" Write-Output "- AppHost source directory: $appHostDir" } else { - Write-Output "- Aspire.Hosting package reference found, but no *.AppHost.csproj was located." + Write-Output "- Aspire.Hosting / Aspire.AppHost.Sdk package reference found, but no *.AppHost.csproj was located." } if ($hasExcludeFromManifest) { diff --git a/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh b/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh index e976d25ba..0e7741875 100644 --- a/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh +++ b/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh @@ -5,7 +5,7 @@ # # It runs the full deterministic detection sequence in one pass: # 1. Find the AppHost project (*.AppHost.csproj) -# 2. Confirm Aspire.Hosting package references +# 2. Confirm Aspire.Hosting or Aspire.AppHost.Sdk package references # 3. Derive the AppHost source directory # 4. Scan the AppHost *.cs for ExcludeFromManifest (informational) # 5. Scan for AddAzureFunctionsProject, and if present, check whether @@ -31,6 +31,32 @@ if [ ! -d "$WORKSPACE_ROOT" ]; then exit 1 fi +# Strip the workspace-root prefix and emit a "./"-prefixed, forward-slash +# workspace-relative path. Keeps output identical across shells/platforms. +to_relative() { + local p="$1" + case "$p" in + "$WORKSPACE_ROOT"/*) p="${p#"$WORKSPACE_ROOT"/}" ;; + ./*) p="${p#./}" ;; + esac + printf './%s' "$p" +} + +# Portable recursive match over *.csproj files (BSD/macOS-safe: no `grep --include`). +# Returns 0 if the extended-regex pattern is found in any *.csproj under the directory. +csproj_match() { + local dir="$1" pattern="$2" + [ -n "$(find "$dir" -type f -name '*.csproj' -exec grep -lE "$pattern" {} + 2>/dev/null)" ] +} + +# Portable recursive match over *.cs files, pruning bin/ and obj/ build output. +# Returns 0 if the extended-regex pattern is found in any *.cs under the directory. +cs_match() { + local dir="$1" pattern="$2" + [ -n "$(find "$dir" -type d \( -name bin -o -name obj \) -prune -o \ + -type f -name '*.cs' -exec grep -lE "$pattern" {} + 2>/dev/null)" ] +} + # Defaults (emitted when the workspace is not an Aspire app) IS_ASPIRE="false" APPHOST_PATH="" @@ -39,32 +65,36 @@ HAS_EXCLUDE_FROM_MANIFEST="false" HAS_FUNCTIONS="false" SECRET_STORAGE_CONFIGURED="false" -# Step 1: Find the AppHost project -APPHOST_PATH=$(find "$WORKSPACE_ROOT" -name "*.AppHost.csproj" 2>/dev/null | head -1 || true) +# Step 1: Find the AppHost project (case-insensitive sort for deterministic, +# cross-shell-consistent selection) +APPHOST_RAW=$(find "$WORKSPACE_ROOT" -type f -name "*.AppHost.csproj" 2>/dev/null | sort -f | head -1 || true) -# Step 2: Confirm Aspire.Hosting package references anywhere in the workspace +# Step 2: Confirm Aspire package references anywhere in the workspace HAS_ASPIRE_PACKAGE="false" -if grep -rql "Aspire.Hosting" "$WORKSPACE_ROOT" --include="*.csproj" 2>/dev/null; then +if csproj_match "$WORKSPACE_ROOT" "Aspire\.Hosting|Aspire\.AppHost\.Sdk"; then HAS_ASPIRE_PACKAGE="true" fi -if [ -n "$APPHOST_PATH" ] || [ "$HAS_ASPIRE_PACKAGE" = "true" ]; then +if [ -n "$APPHOST_RAW" ] || [ "$HAS_ASPIRE_PACKAGE" = "true" ]; then IS_ASPIRE="true" fi -if [ -n "$APPHOST_PATH" ]; then - # Step 3: Derive the AppHost source directory +if [ -n "$APPHOST_RAW" ]; then + APPHOST_PATH=$(to_relative "$APPHOST_RAW") + + # Step 3: Derive the AppHost source directory (scan the real path on disk) APPHOST_DIR=$(dirname "$APPHOST_PATH") + APPHOST_DIR_RAW=$(dirname "$APPHOST_RAW") # Step 4: Scan the AppHost source for ExcludeFromManifest (informational) - if grep -rq "ExcludeFromManifest" "$APPHOST_DIR" --include="*.cs" 2>/dev/null; then + if cs_match "$APPHOST_DIR_RAW" "ExcludeFromManifest"; then HAS_EXCLUDE_FROM_MANIFEST="true" fi # Step 5: Detect Azure Functions and secret-storage configuration - if grep -rq "AddAzureFunctionsProject" "$APPHOST_DIR" --include="*.cs" 2>/dev/null; then + if cs_match "$APPHOST_DIR_RAW" "AddAzureFunctionsProject"; then HAS_FUNCTIONS="true" - if grep -rq "AzureWebJobsSecretStorageType" "$APPHOST_DIR" --include="*.cs" 2>/dev/null; then + if cs_match "$APPHOST_DIR_RAW" "AzureWebJobsSecretStorageType"; then SECRET_STORAGE_CONFIGURED="true" fi fi @@ -82,7 +112,7 @@ echo "secretStorageConfigured=$SECRET_STORAGE_CONFIGURED" echo "" echo "Summary:" if [ "$IS_ASPIRE" != "true" ]; then - echo "- No .NET Aspire app detected in '$WORKSPACE_ROOT' (no *.AppHost.csproj or Aspire.Hosting package reference)." + echo "- No .NET Aspire app detected in '$WORKSPACE_ROOT' (no *.AppHost.csproj or Aspire.Hosting / Aspire.AppHost.Sdk package reference)." exit 0 fi @@ -90,7 +120,7 @@ if [ -n "$APPHOST_PATH" ]; then echo "- .NET Aspire app detected. AppHost project: $APPHOST_PATH" echo "- AppHost source directory: $APPHOST_DIR" else - echo "- Aspire.Hosting package reference found, but no *.AppHost.csproj was located." + echo "- Aspire.Hosting / Aspire.AppHost.Sdk package reference found, but no *.AppHost.csproj was located." fi if [ "$HAS_EXCLUDE_FROM_MANIFEST" = "true" ]; then From 41f3ea23fbeb55454f7664f4a425028aea3ae638 Mon Sep 17 00:00:00 2001 From: "Tom Meschter (from Dev Box)" Date: Tue, 9 Jun 2026 14:41:48 -0700 Subject: [PATCH 4/6] docs: encourage use of the detect-aspire detection scripts Update azure-prepare reference docs to consistently point to the bundled detect-aspire.sh/.ps1 scripts for .NET Aspire detection, and fix the relative script links in recipes/azd/README.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../skills/azure-prepare/references/aspire.md | 21 ------------------- .../azure-prepare/references/generate.md | 2 +- .../references/recipe-selection.md | 12 ++++++++++- .../references/recipes/azd/README.md | 2 +- .../references/recipes/azd/aspire.md | 8 ------- .../skills/azure-prepare/references/scan.md | 2 +- 6 files changed, 14 insertions(+), 33 deletions(-) diff --git a/plugin/skills/azure-prepare/references/aspire.md b/plugin/skills/azure-prepare/references/aspire.md index e057a53cb..1bd769f45 100644 --- a/plugin/skills/azure-prepare/references/aspire.md +++ b/plugin/skills/azure-prepare/references/aspire.md @@ -15,27 +15,6 @@ Guidance for preparing .NET Aspire applications for Azure deployment. .NET Aspire is an opinionated, cloud-ready stack for building observable, production-ready distributed applications. Aspire projects use an AppHost orchestrator to define and configure the application's components, services, and dependencies. -## Detection - -A .NET Aspire project is identified by: - -| Indicator | Description | -|-----------|-------------| -| `*.AppHost.csproj` | AppHost orchestrator project file | -| `Aspire.Hosting` package | Core Aspire hosting package reference | -| `Aspire.Hosting.AppHost` | Alternative Aspire hosting package | - -**Example project structure:** -``` -orleans-voting/ -├── OrleansVoting.sln -├── OrleansVoting.AppHost/ -│ └── OrleansVoting.AppHost.csproj ← AppHost indicator -├── OrleansVoting.Web/ -├── OrleansVoting.Api/ -└── OrleansVoting.Grains/ -``` - ## Azure Preparation Workflow ### Step 1: Detection diff --git a/plugin/skills/azure-prepare/references/generate.md b/plugin/skills/azure-prepare/references/generate.md index 526ce0602..d8b15becc 100644 --- a/plugin/skills/azure-prepare/references/generate.md +++ b/plugin/skills/azure-prepare/references/generate.md @@ -4,7 +4,7 @@ Generate infrastructure and configuration files based on selected recipe. ## ⛔ CRITICAL: Check for .NET Aspire Projects FIRST -**MANDATORY: Before generating any files, detect .NET Aspire projects** using the detection script ([detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1)). It runs the full detection sequence and prints `key=value` fields plus a summary: +**MANDATORY: Before generating any files, always check for .NET Aspire projects** using the detection script ([detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1)). It runs the full detection sequence and prints `key=value` fields plus a summary: **bash:** ```bash diff --git a/plugin/skills/azure-prepare/references/recipe-selection.md b/plugin/skills/azure-prepare/references/recipe-selection.md index 2168591a5..a056127c7 100644 --- a/plugin/skills/azure-prepare/references/recipe-selection.md +++ b/plugin/skills/azure-prepare/references/recipe-selection.md @@ -8,7 +8,17 @@ Choose the deployment recipe based on project needs and existing tooling. | Project Type | Detection | Recipe Selection | |--------------|-----------|------------------| -| **.NET Aspire** | `*.AppHost.csproj` or `Aspire.Hosting` package | **AZD (auto via `azd init --from-code`)** → [aspire.md](aspire.md) | +| **.NET Aspire** | run [detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1) | **AZD (auto via `azd init --from-code`)** → [aspire.md](aspire.md) | + +**bash:** +```bash +./scripts/detect-aspire.sh [workspace-root] +``` + +**PowerShell:** +```powershell +./scripts/detect-aspire.ps1 -WorkspaceRoot +``` > 💡 **Tip:** .NET Aspire projects always use AZD recipe with auto-generated configuration. Do not manually select recipe or create artifacts. diff --git a/plugin/skills/azure-prepare/references/recipes/azd/README.md b/plugin/skills/azure-prepare/references/recipes/azd/README.md index 98a7a5ad5..b13d27a80 100644 --- a/plugin/skills/azure-prepare/references/recipes/azd/README.md +++ b/plugin/skills/azure-prepare/references/recipes/azd/README.md @@ -29,7 +29,7 @@ Azure Developer CLI workflow for preparing Azure deployments. | Pattern | Detection | Action | |---------|-----------|--------| -| **.NET Aspire** | `*.AppHost.csproj` or `Aspire.Hosting` package | Use `azd init --from-code -e ` → [aspire.md](../../aspire.md) | +| **.NET Aspire** | run [detect-aspire.sh](../../scripts/detect-aspire.sh) / [detect-aspire.ps1](../../scripts/detect-aspire.ps1) | Use `azd init --from-code -e ` → [aspire.md](../../aspire.md) | | **Existing azure.yaml** | `azure.yaml` present | MODIFY mode - update existing config | | **New project** | No azure.yaml, no special patterns | Manual generation (steps below) | diff --git a/plugin/skills/azure-prepare/references/recipes/azd/aspire.md b/plugin/skills/azure-prepare/references/recipes/azd/aspire.md index caed8ee2c..ee4f89b35 100644 --- a/plugin/skills/azure-prepare/references/recipes/azd/aspire.md +++ b/plugin/skills/azure-prepare/references/recipes/azd/aspire.md @@ -2,14 +2,6 @@ **⛔ MANDATORY: For .NET Aspire projects, NEVER manually create azure.yaml. Use `azd init --from-code` instead.** -## Detection - -| Indicator | How to Detect | -|-----------|---------------| -| `*.AppHost.csproj` | `find . -name "*.AppHost.csproj"` | -| `Aspire.Hosting` package | `grep -r "Aspire\.Hosting" . --include="*.csproj"` | -| `Aspire.AppHost.Sdk` | `grep -r "Aspire\.AppHost\.Sdk" . --include="*.csproj"` | - ## Workflow ### ⛔ DO NOT (Wrong Approach) diff --git a/plugin/skills/azure-prepare/references/scan.md b/plugin/skills/azure-prepare/references/scan.md index e5fa20e75..bc03f823d 100644 --- a/plugin/skills/azure-prepare/references/scan.md +++ b/plugin/skills/azure-prepare/references/scan.md @@ -56,7 +56,7 @@ Before classifying components, grep dependency files for SDKs that require a spe ### .NET Aspire Detection -Detect **.NET Aspire projects** by running the detection script ([detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1)), which reports `isAspire` along with AppHost path, `ExcludeFromManifest`, and Azure Functions signals: +Always check **.NET Aspire projects** by running the detection script ([detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1)), which reports `isAspire` along with AppHost path, `ExcludeFromManifest`, and Azure Functions signals: **bash:** ```bash From 54839bc7098735e2af9f5ca9d47874996d2ccd24 Mon Sep 17 00:00:00 2001 From: "Tom Meschter (from Dev Box)" Date: Wed, 10 Jun 2026 12:16:15 -0700 Subject: [PATCH 5/6] refac: split detect-aspire into presence and gather-info scripts Separate Aspire detection into two purpose-built scripts: - detect-aspire.{sh,ps1}: presence-only (isAspire + appHostPath) for detection/routing points (scan.md, recipe-selection.md, generate.md, recipes/azd/README.md) - gather-aspire-info.{sh,ps1}: full facts (appHostDir, hasExcludeFromManifest, hasFunctions, secretStorageConfigured) for Aspire-specific reference files (aspire.md, recipes/azd/aspire.md, services/functions/aspire-containerapps.md) Add gather-aspire-info invocations to the two Aspire-specific files that previously referenced no detection script. Update the integration test regex to accept either script name. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../skills/azure-prepare/references/aspire.md | 14 +- .../azure-prepare/references/generate.md | 4 +- .../references/recipes/azd/aspire.md | 2 + .../skills/azure-prepare/references/scan.md | 6 +- .../references/scripts/detect-aspire.ps1 | 64 +------- .../references/scripts/detect-aspire.sh | 68 +-------- .../references/scripts/gather-aspire-info.ps1 | 140 +++++++++++++++++ .../references/scripts/gather-aspire-info.sh | 141 ++++++++++++++++++ .../functions/aspire-containerapps.md | 2 + tests/azure-prepare/integration.test.ts | 12 +- 10 files changed, 319 insertions(+), 134 deletions(-) create mode 100644 plugin/skills/azure-prepare/references/scripts/gather-aspire-info.ps1 create mode 100644 plugin/skills/azure-prepare/references/scripts/gather-aspire-info.sh diff --git a/plugin/skills/azure-prepare/references/aspire.md b/plugin/skills/azure-prepare/references/aspire.md index 1bd769f45..d3e6708ea 100644 --- a/plugin/skills/azure-prepare/references/aspire.md +++ b/plugin/skills/azure-prepare/references/aspire.md @@ -19,16 +19,16 @@ Guidance for preparing .NET Aspire applications for Azure deployment. ### Step 1: Detection -When scanning the codebase (per [scan.md](scan.md)), run the detection script ([detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1)). It performs the full deterministic detection sequence in one pass and prints `key=value` lines plus a human-readable summary, so you can branch on the result instead of parsing raw `find`/`grep` output. +When scanning the codebase (per [scan.md](scan.md)), Aspire presence is established with `detect-aspire`. Once confirmed, gather the full set of facts by running [gather-aspire-info.sh](scripts/gather-aspire-info.sh) / [gather-aspire-info.ps1](scripts/gather-aspire-info.ps1). It performs the full deterministic detection sequence in one pass and prints `key=value` lines plus a human-readable summary, so you can branch on the result instead of parsing raw `find`/`grep` output. **bash:** ```bash -./scripts/detect-aspire.sh [workspace-root] +./scripts/gather-aspire-info.sh [workspace-root] ``` **PowerShell:** ```powershell -./scripts/detect-aspire.ps1 -WorkspaceRoot +./scripts/gather-aspire-info.ps1 -WorkspaceRoot ``` `workspace-root` defaults to the current directory. The script reports these fields: @@ -56,7 +56,7 @@ If `isAspire=false`, this is not an Aspire app — continue with the normal reci ### ⛔ Step 1a: Pre-Check for Custom/Non-Deployable Resources (MANDATORY) -**Before running `azd init --from-code`, understand whether the app may contain local-only custom resources.** The detection script from Step 1 already reports this as the `hasExcludeFromManifest` field — no separate scan is needed. +**Before running `azd init --from-code`, understand whether the app may contain local-only custom resources.** The `gather-aspire-info` run from Step 1 already reports this as the `hasExcludeFromManifest` field — no separate scan is needed. - `hasExcludeFromManifest=true` → the AppHost source uses `.ExcludeFromManifest()`. - `hasExcludeFromManifest=false` → no such usage was found. @@ -177,16 +177,16 @@ This step **MUST** run BEFORE `azd up` or `azd provision`. Skipping it causes a **1. Detect Azure Functions in the AppHost:** -Use the `hasFunctions` field from the Step 1 detection script. If you have not run it yet (or the workspace changed), re-run it: +Use the `hasFunctions` field from the Step 1 `gather-aspire-info` run. If you have not run it yet (or the workspace changed), re-run it: **bash:** ```bash -./scripts/detect-aspire.sh [workspace-root] +./scripts/gather-aspire-info.sh [workspace-root] ``` **PowerShell:** ```powershell -./scripts/detect-aspire.ps1 -WorkspaceRoot +./scripts/gather-aspire-info.ps1 -WorkspaceRoot ``` **If `hasFunctions=false` → skip this step.** diff --git a/plugin/skills/azure-prepare/references/generate.md b/plugin/skills/azure-prepare/references/generate.md index d8b15becc..f5bfb48fe 100644 --- a/plugin/skills/azure-prepare/references/generate.md +++ b/plugin/skills/azure-prepare/references/generate.md @@ -4,7 +4,7 @@ Generate infrastructure and configuration files based on selected recipe. ## ⛔ CRITICAL: Check for .NET Aspire Projects FIRST -**MANDATORY: Before generating any files, always check for .NET Aspire projects** using the detection script ([detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1)). It runs the full detection sequence and prints `key=value` fields plus a summary: +**MANDATORY: Before generating any files, always check for .NET Aspire projects** using the presence script ([detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1)). It reports `isAspire` and `appHostPath`: **bash:** ```bash @@ -16,7 +16,7 @@ Generate infrastructure and configuration files based on selected recipe. ./scripts/detect-aspire.ps1 -WorkspaceRoot ``` -If the result includes `isAspire=true`, treat the project as Aspire. See [aspire.md](aspire.md) Step 1 for the full list of fields the script reports. +If the result includes `isAspire=true`, treat the project as Aspire. See [aspire.md](aspire.md) Step 1 for gathering the full set of Aspire facts with `gather-aspire-info`. **If Aspire is detected:** 1. ⛔ **STOP** - Do NOT manually create `azure.yaml` diff --git a/plugin/skills/azure-prepare/references/recipes/azd/aspire.md b/plugin/skills/azure-prepare/references/recipes/azd/aspire.md index ee4f89b35..26962fb4b 100644 --- a/plugin/skills/azure-prepare/references/recipes/azd/aspire.md +++ b/plugin/skills/azure-prepare/references/recipes/azd/aspire.md @@ -2,6 +2,8 @@ **⛔ MANDATORY: For .NET Aspire projects, NEVER manually create azure.yaml. Use `azd init --from-code` instead.** +> 📋 **Gather facts first:** Run [gather-aspire-info.sh](../../scripts/gather-aspire-info.sh) / [gather-aspire-info.ps1](../../scripts/gather-aspire-info.ps1) to capture the `appHostPath`, `hasExcludeFromManifest`, and Azure Functions signals before generating anything. See [aspire.md](../../aspire.md) Step 1 for the full field list. + ## Workflow ### ⛔ DO NOT (Wrong Approach) diff --git a/plugin/skills/azure-prepare/references/scan.md b/plugin/skills/azure-prepare/references/scan.md index bc03f823d..4cf8569a0 100644 --- a/plugin/skills/azure-prepare/references/scan.md +++ b/plugin/skills/azure-prepare/references/scan.md @@ -56,7 +56,7 @@ Before classifying components, grep dependency files for SDKs that require a spe ### .NET Aspire Detection -Always check **.NET Aspire projects** by running the detection script ([detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1)), which reports `isAspire` along with AppHost path, `ExcludeFromManifest`, and Azure Functions signals: +Always check **.NET Aspire projects** by running the presence script ([detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1)), which reports just `isAspire` and `appHostPath` so you can branch quickly. Once Aspire is confirmed, [aspire.md](aspire.md) gathers the deeper facts (`ExcludeFromManifest`, Azure Functions signals) via `gather-aspire-info`: **bash:** ```bash @@ -68,13 +68,13 @@ Always check **.NET Aspire projects** by running the detection script ([detect-a ./scripts/detect-aspire.ps1 -WorkspaceRoot ``` -A project is Aspire when the script returns `isAspire=true` (a `*.AppHost.csproj` or an `Aspire.Hosting` / `Aspire.Hosting.AppHost` / `Aspire.AppHost.Sdk` package reference was found). See [aspire.md](aspire.md) Step 1 for the full field list. +A project is Aspire when the script returns `isAspire=true` (a `*.AppHost.csproj` or an `Aspire.Hosting` / `Aspire.Hosting.AppHost` / `Aspire.AppHost.Sdk` package reference was found). See [aspire.md](aspire.md) Step 1 for the full field list gathered by `gather-aspire-info`. **When Aspire is detected:** - Use `azd init --from-code -e ` instead of manual azure.yaml creation - The `--from-code` flag automatically detects the AppHost and generates appropriate configuration - The `-e` flag is **required** for non-interactive environments (agents, CI/CD) -- ⚠️ **CRITICAL:** If the script reports `hasFunctions=true` and `secretStorageConfigured=false`, you **MUST** add `.WithEnvironment("AzureWebJobsSecretStorageType", "Files")` to the Functions builder chain BEFORE deployment. Without this, Functions will fail at startup with `Secret initialization from Blob storage failed`. See [aspire.md](aspire.md) Step 4b for the complete fix procedure. +- ⚠️ **CRITICAL:** Run `gather-aspire-info` (per [aspire.md](aspire.md) Step 1). If it reports `hasFunctions=true` and `secretStorageConfigured=false`, you **MUST** add `.WithEnvironment("AzureWebJobsSecretStorageType", "Files")` to the Functions builder chain BEFORE deployment. Without this, Functions will fail at startup with `Secret initialization from Blob storage failed`. See [aspire.md](aspire.md) Step 4b for the complete fix procedure. - See [aspire.md](aspire.md) for detailed Aspire-specific guidance ## Output diff --git a/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 b/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 index 90620c3d5..50a3c5de5 100644 --- a/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 +++ b/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 @@ -1,20 +1,17 @@ <# .SYNOPSIS - Detects whether a workspace is a .NET Aspire application and gathers the - facts the azure-prepare skill needs to plan deployment. + Presence check: determines whether a workspace is a .NET Aspire application. + Use this at detection/routing points where you only need a yes/no answer. + To gather the deeper deployment facts (ExcludeFromManifest, Azure Functions, + secret storage, AppHost source dir), use gather-aspire-info.ps1 instead. .DESCRIPTION - Runs the full deterministic detection sequence in one pass: + Runs the minimal deterministic presence sequence: 1. Find the AppHost project (*.AppHost.csproj) 2. Confirm Aspire.Hosting or Aspire.AppHost.Sdk package references - 3. Derive the AppHost source directory - 4. Scan the AppHost *.cs for ExcludeFromManifest (informational) - 5. Scan for AddAzureFunctionsProject, and if present, check whether - AzureWebJobsSecretStorageType is already configured - Output: key=value lines the agent can branch on, followed by a - human-readable summary. The remediation decision (whether/how to add - .WithEnvironment("AzureWebJobsSecretStorageType", "Files")) stays with the agent. + Output: key=value lines (isAspire, appHostPath) followed by a short + human-readable summary. .PARAMETER WorkspaceRoot Workspace root directory to scan. Defaults to the current directory. @@ -42,10 +39,6 @@ if (-not (Test-Path -LiteralPath $WorkspaceRoot -PathType Container)) { # Defaults (emitted when the workspace is not an Aspire app) $isAspire = $false $appHostPath = "" -$appHostDir = "" -$hasExcludeFromManifest = $false -$hasFunctions = $false -$secretStorageConfigured = $false # Step 1: Find the AppHost project (sorted for deterministic selection) $appHostProject = @(Get-ChildItem -LiteralPath $WorkspaceRoot -Recurse -Filter "*.AppHost.csproj" -File -ErrorAction SilentlyContinue | @@ -72,27 +65,6 @@ if ($appHostProject) { } finally { Pop-Location } - - # Step 3: Derive the AppHost source directory - $appHostDir = $appHostPath -replace '/[^/]+$', '' - $appHostDirFull = $appHostProject.DirectoryName - - # Scan AppHost *.cs, excluding bin/ and obj/ build output - $appHostCs = Get-ChildItem -LiteralPath $appHostDirFull -Recurse -Filter "*.cs" -File -ErrorAction SilentlyContinue | - Where-Object { $_.FullName -notmatch '[\\/](bin|obj)[\\/]' } - - # Step 4: Scan the AppHost source for ExcludeFromManifest (informational) - if ($appHostCs -and ($appHostCs | Select-String -Pattern "ExcludeFromManifest" -SimpleMatch -List -ErrorAction SilentlyContinue)) { - $hasExcludeFromManifest = $true - } - - # Step 5: Detect Azure Functions and secret-storage configuration - if ($appHostCs -and ($appHostCs | Select-String -Pattern "AddAzureFunctionsProject" -SimpleMatch -List -ErrorAction SilentlyContinue)) { - $hasFunctions = $true - if ($appHostCs | Select-String -Pattern "AzureWebJobsSecretStorageType" -SimpleMatch -List -ErrorAction SilentlyContinue) { - $secretStorageConfigured = $true - } - } } function ConvertTo-Lower([bool]$value) { @@ -102,10 +74,6 @@ function ConvertTo-Lower([bool]$value) { # Machine-readable result Write-Output "isAspire=$(ConvertTo-Lower $isAspire)" Write-Output "appHostPath=$appHostPath" -Write-Output "appHostDir=$appHostDir" -Write-Output "hasExcludeFromManifest=$(ConvertTo-Lower $hasExcludeFromManifest)" -Write-Output "hasFunctions=$(ConvertTo-Lower $hasFunctions)" -Write-Output "secretStorageConfigured=$(ConvertTo-Lower $secretStorageConfigured)" # Human-readable summary Write-Output "" @@ -117,23 +85,7 @@ if (-not $isAspire) { if ($appHostPath) { Write-Output "- .NET Aspire app detected. AppHost project: $appHostPath" - Write-Output "- AppHost source directory: $appHostDir" } else { Write-Output "- Aspire.Hosting / Aspire.AppHost.Sdk package reference found, but no *.AppHost.csproj was located." } - -if ($hasExcludeFromManifest) { - Write-Output "- ExcludeFromManifest found in AppHost source (informational): the app may contain local-only resources." -} else { - Write-Output "- No ExcludeFromManifest usage found in AppHost source." -} - -if ($hasFunctions) { - if ($secretStorageConfigured) { - Write-Output "- AddAzureFunctionsProject found; AzureWebJobsSecretStorageType is configured in the AppHost source." - } else { - Write-Output "- AddAzureFunctionsProject found; AzureWebJobsSecretStorageType is not configured in the AppHost source." - } -} else { - Write-Output "- AddAzureFunctionsProject not found in AppHost source." -} +Write-Output "- Run gather-aspire-info.ps1 for AppHost source dir, ExcludeFromManifest, and Azure Functions details." diff --git a/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh b/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh index 0e7741875..fdc0b17a8 100644 --- a/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh +++ b/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh @@ -1,19 +1,16 @@ #!/usr/bin/env bash # detect-aspire.sh -# Detects whether a workspace is a .NET Aspire application and gathers the -# facts the azure-prepare skill needs to plan deployment. +# Presence check: determines whether a workspace is a .NET Aspire application. +# Use this at detection/routing points where you only need a yes/no answer. +# To gather the deeper deployment facts (ExcludeFromManifest, Azure Functions, +# secret storage, AppHost source dir), use gather-aspire-info.sh instead. # -# It runs the full deterministic detection sequence in one pass: +# It runs the minimal deterministic presence sequence: # 1. Find the AppHost project (*.AppHost.csproj) # 2. Confirm Aspire.Hosting or Aspire.AppHost.Sdk package references -# 3. Derive the AppHost source directory -# 4. Scan the AppHost *.cs for ExcludeFromManifest (informational) -# 5. Scan for AddAzureFunctionsProject, and if present, check whether -# AzureWebJobsSecretStorageType is already configured # -# Output: key=value lines the agent can branch on, followed by a human-readable -# summary. The remediation decision (whether/how to add -# .WithEnvironment("AzureWebJobsSecretStorageType", "Files")) stays with the agent. +# Output: key=value lines (isAspire, appHostPath) followed by a short +# human-readable summary. # # Usage: # ./detect-aspire.sh [workspace-root] @@ -49,21 +46,9 @@ csproj_match() { [ -n "$(find "$dir" -type f -name '*.csproj' -exec grep -lE "$pattern" {} + 2>/dev/null)" ] } -# Portable recursive match over *.cs files, pruning bin/ and obj/ build output. -# Returns 0 if the extended-regex pattern is found in any *.cs under the directory. -cs_match() { - local dir="$1" pattern="$2" - [ -n "$(find "$dir" -type d \( -name bin -o -name obj \) -prune -o \ - -type f -name '*.cs' -exec grep -lE "$pattern" {} + 2>/dev/null)" ] -} - # Defaults (emitted when the workspace is not an Aspire app) IS_ASPIRE="false" APPHOST_PATH="" -APPHOST_DIR="" -HAS_EXCLUDE_FROM_MANIFEST="false" -HAS_FUNCTIONS="false" -SECRET_STORAGE_CONFIGURED="false" # Step 1: Find the AppHost project (case-insensitive sort for deterministic, # cross-shell-consistent selection) @@ -81,32 +66,11 @@ fi if [ -n "$APPHOST_RAW" ]; then APPHOST_PATH=$(to_relative "$APPHOST_RAW") - - # Step 3: Derive the AppHost source directory (scan the real path on disk) - APPHOST_DIR=$(dirname "$APPHOST_PATH") - APPHOST_DIR_RAW=$(dirname "$APPHOST_RAW") - - # Step 4: Scan the AppHost source for ExcludeFromManifest (informational) - if cs_match "$APPHOST_DIR_RAW" "ExcludeFromManifest"; then - HAS_EXCLUDE_FROM_MANIFEST="true" - fi - - # Step 5: Detect Azure Functions and secret-storage configuration - if cs_match "$APPHOST_DIR_RAW" "AddAzureFunctionsProject"; then - HAS_FUNCTIONS="true" - if cs_match "$APPHOST_DIR_RAW" "AzureWebJobsSecretStorageType"; then - SECRET_STORAGE_CONFIGURED="true" - fi - fi fi # Machine-readable result echo "isAspire=$IS_ASPIRE" echo "appHostPath=$APPHOST_PATH" -echo "appHostDir=$APPHOST_DIR" -echo "hasExcludeFromManifest=$HAS_EXCLUDE_FROM_MANIFEST" -echo "hasFunctions=$HAS_FUNCTIONS" -echo "secretStorageConfigured=$SECRET_STORAGE_CONFIGURED" # Human-readable summary echo "" @@ -118,23 +82,7 @@ fi if [ -n "$APPHOST_PATH" ]; then echo "- .NET Aspire app detected. AppHost project: $APPHOST_PATH" - echo "- AppHost source directory: $APPHOST_DIR" else echo "- Aspire.Hosting / Aspire.AppHost.Sdk package reference found, but no *.AppHost.csproj was located." fi - -if [ "$HAS_EXCLUDE_FROM_MANIFEST" = "true" ]; then - echo "- ExcludeFromManifest found in AppHost source (informational): the app may contain local-only resources." -else - echo "- No ExcludeFromManifest usage found in AppHost source." -fi - -if [ "$HAS_FUNCTIONS" = "true" ]; then - if [ "$SECRET_STORAGE_CONFIGURED" = "true" ]; then - echo "- AddAzureFunctionsProject found; AzureWebJobsSecretStorageType is configured in the AppHost source." - else - echo "- AddAzureFunctionsProject found; AzureWebJobsSecretStorageType is not configured in the AppHost source." - fi -else - echo "- AddAzureFunctionsProject not found in AppHost source." -fi +echo "- Run gather-aspire-info.sh for AppHost source dir, ExcludeFromManifest, and Azure Functions details." diff --git a/plugin/skills/azure-prepare/references/scripts/gather-aspire-info.ps1 b/plugin/skills/azure-prepare/references/scripts/gather-aspire-info.ps1 new file mode 100644 index 000000000..740f64dd1 --- /dev/null +++ b/plugin/skills/azure-prepare/references/scripts/gather-aspire-info.ps1 @@ -0,0 +1,140 @@ +<# +.SYNOPSIS + Gathers the detailed facts the azure-prepare skill needs to plan deployment + of a .NET Aspire application. Use this in Aspire-specific reference files, + AFTER presence has been established (see detect-aspire.ps1). + +.DESCRIPTION + Runs the full deterministic detection sequence in one pass: + 1. Find the AppHost project (*.AppHost.csproj) + 2. Confirm Aspire.Hosting or Aspire.AppHost.Sdk package references + 3. Derive the AppHost source directory + 4. Scan the AppHost *.cs for ExcludeFromManifest (informational) + 5. Scan for AddAzureFunctionsProject, and if present, check whether + AzureWebJobsSecretStorageType is already configured + + Output: key=value lines the agent can branch on, followed by a + human-readable summary. The remediation decision (whether/how to add + .WithEnvironment("AzureWebJobsSecretStorageType", "Files")) stays with the agent. + +.PARAMETER WorkspaceRoot + Workspace root directory to scan. Defaults to the current directory. + +.EXAMPLE + ./gather-aspire-info.ps1 + Scan the current directory. + +.EXAMPLE + ./gather-aspire-info.ps1 -WorkspaceRoot ./src/MyApp + Scan a specific workspace root. +#> + +param( + [string]$WorkspaceRoot = "." +) + +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path -LiteralPath $WorkspaceRoot -PathType Container)) { + Write-Error "workspace root '$WorkspaceRoot' is not a directory" + exit 1 +} + +# Defaults (emitted when the workspace is not an Aspire app) +$isAspire = $false +$appHostPath = "" +$appHostDir = "" +$hasExcludeFromManifest = $false +$hasFunctions = $false +$secretStorageConfigured = $false + +# Step 1: Find the AppHost project (sorted for deterministic selection) +$appHostProject = @(Get-ChildItem -LiteralPath $WorkspaceRoot -Recurse -Filter "*.AppHost.csproj" -File -ErrorAction SilentlyContinue | + Sort-Object FullName) | Select-Object -First 1 + +# Step 2: Confirm Aspire package references anywhere in the workspace +$hasAspirePackage = $false +$csprojFiles = Get-ChildItem -LiteralPath $WorkspaceRoot -Recurse -Filter "*.csproj" -File -ErrorAction SilentlyContinue +if ($csprojFiles) { + if ($csprojFiles | Select-String -Pattern "Aspire\.Hosting|Aspire\.AppHost\.Sdk" -List -ErrorAction SilentlyContinue) { + $hasAspirePackage = $true + } +} + +if ($appHostProject -or $hasAspirePackage) { + $isAspire = $true +} + +if ($appHostProject) { + # Emit a workspace-relative, forward-slash path (matches the bash output contract) + Push-Location -LiteralPath $WorkspaceRoot + try { + $appHostPath = (Resolve-Path -LiteralPath $appHostProject.FullName -Relative) -replace '\\', '/' + } finally { + Pop-Location + } + + # Step 3: Derive the AppHost source directory + $appHostDir = $appHostPath -replace '/[^/]+$', '' + $appHostDirFull = $appHostProject.DirectoryName + + # Scan AppHost *.cs, excluding bin/ and obj/ build output + $appHostCs = Get-ChildItem -LiteralPath $appHostDirFull -Recurse -Filter "*.cs" -File -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch '[\\/](bin|obj)[\\/]' } + + # Step 4: Scan the AppHost source for ExcludeFromManifest (informational) + if ($appHostCs -and ($appHostCs | Select-String -Pattern "ExcludeFromManifest" -SimpleMatch -List -ErrorAction SilentlyContinue)) { + $hasExcludeFromManifest = $true + } + + # Step 5: Detect Azure Functions and secret-storage configuration + if ($appHostCs -and ($appHostCs | Select-String -Pattern "AddAzureFunctionsProject" -SimpleMatch -List -ErrorAction SilentlyContinue)) { + $hasFunctions = $true + if ($appHostCs | Select-String -Pattern "AzureWebJobsSecretStorageType" -SimpleMatch -List -ErrorAction SilentlyContinue) { + $secretStorageConfigured = $true + } + } +} + +function ConvertTo-Lower([bool]$value) { + if ($value) { "true" } else { "false" } +} + +# Machine-readable result +Write-Output "isAspire=$(ConvertTo-Lower $isAspire)" +Write-Output "appHostPath=$appHostPath" +Write-Output "appHostDir=$appHostDir" +Write-Output "hasExcludeFromManifest=$(ConvertTo-Lower $hasExcludeFromManifest)" +Write-Output "hasFunctions=$(ConvertTo-Lower $hasFunctions)" +Write-Output "secretStorageConfigured=$(ConvertTo-Lower $secretStorageConfigured)" + +# Human-readable summary +Write-Output "" +Write-Output "Summary:" +if (-not $isAspire) { + Write-Output "- No .NET Aspire app detected in '$WorkspaceRoot' (no *.AppHost.csproj or Aspire.Hosting / Aspire.AppHost.Sdk package reference)." + exit 0 +} + +if ($appHostPath) { + Write-Output "- .NET Aspire app detected. AppHost project: $appHostPath" + Write-Output "- AppHost source directory: $appHostDir" +} else { + Write-Output "- Aspire.Hosting / Aspire.AppHost.Sdk package reference found, but no *.AppHost.csproj was located." +} + +if ($hasExcludeFromManifest) { + Write-Output "- ExcludeFromManifest found in AppHost source (informational): the app may contain local-only resources." +} else { + Write-Output "- No ExcludeFromManifest usage found in AppHost source." +} + +if ($hasFunctions) { + if ($secretStorageConfigured) { + Write-Output "- AddAzureFunctionsProject found; AzureWebJobsSecretStorageType is configured in the AppHost source." + } else { + Write-Output "- AddAzureFunctionsProject found; AzureWebJobsSecretStorageType is not configured in the AppHost source." + } +} else { + Write-Output "- AddAzureFunctionsProject not found in AppHost source." +} diff --git a/plugin/skills/azure-prepare/references/scripts/gather-aspire-info.sh b/plugin/skills/azure-prepare/references/scripts/gather-aspire-info.sh new file mode 100644 index 000000000..5a5bfdd46 --- /dev/null +++ b/plugin/skills/azure-prepare/references/scripts/gather-aspire-info.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# gather-aspire-info.sh +# Gathers the detailed facts the azure-prepare skill needs to plan deployment of +# a .NET Aspire application. Use this in Aspire-specific reference files, AFTER +# presence has been established (see detect-aspire.sh). +# +# It runs the full deterministic detection sequence in one pass: +# 1. Find the AppHost project (*.AppHost.csproj) +# 2. Confirm Aspire.Hosting or Aspire.AppHost.Sdk package references +# 3. Derive the AppHost source directory +# 4. Scan the AppHost *.cs for ExcludeFromManifest (informational) +# 5. Scan for AddAzureFunctionsProject, and if present, check whether +# AzureWebJobsSecretStorageType is already configured +# +# Output: key=value lines the agent can branch on, followed by a human-readable +# summary. The remediation decision (whether/how to add +# .WithEnvironment("AzureWebJobsSecretStorageType", "Files")) stays with the agent. +# +# Usage: +# ./gather-aspire-info.sh [workspace-root] +# +# Examples: +# ./gather-aspire-info.sh # Scan the current directory +# ./gather-aspire-info.sh ./src/MyApp # Scan a specific workspace root + +set -euo pipefail + +WORKSPACE_ROOT="${1:-.}" + +if [ ! -d "$WORKSPACE_ROOT" ]; then + echo "Error: workspace root '$WORKSPACE_ROOT' is not a directory" >&2 + exit 1 +fi + +# Strip the workspace-root prefix and emit a "./"-prefixed, forward-slash +# workspace-relative path. Keeps output identical across shells/platforms. +to_relative() { + local p="$1" + case "$p" in + "$WORKSPACE_ROOT"/*) p="${p#"$WORKSPACE_ROOT"/}" ;; + ./*) p="${p#./}" ;; + esac + printf './%s' "$p" +} + +# Portable recursive match over *.csproj files (BSD/macOS-safe: no `grep --include`). +# Returns 0 if the extended-regex pattern is found in any *.csproj under the directory. +csproj_match() { + local dir="$1" pattern="$2" + [ -n "$(find "$dir" -type f -name '*.csproj' -exec grep -lE "$pattern" {} + 2>/dev/null)" ] +} + +# Portable recursive match over *.cs files, pruning bin/ and obj/ build output. +# Returns 0 if the extended-regex pattern is found in any *.cs under the directory. +cs_match() { + local dir="$1" pattern="$2" + [ -n "$(find "$dir" -type d \( -name bin -o -name obj \) -prune -o \ + -type f -name '*.cs' -exec grep -lE "$pattern" {} + 2>/dev/null)" ] +} + +# Defaults (emitted when the workspace is not an Aspire app) +IS_ASPIRE="false" +APPHOST_PATH="" +APPHOST_DIR="" +HAS_EXCLUDE_FROM_MANIFEST="false" +HAS_FUNCTIONS="false" +SECRET_STORAGE_CONFIGURED="false" + +# Step 1: Find the AppHost project (case-insensitive sort for deterministic, +# cross-shell-consistent selection) +APPHOST_RAW=$(find "$WORKSPACE_ROOT" -type f -name "*.AppHost.csproj" 2>/dev/null | sort -f | head -1 || true) + +# Step 2: Confirm Aspire package references anywhere in the workspace +HAS_ASPIRE_PACKAGE="false" +if csproj_match "$WORKSPACE_ROOT" "Aspire\.Hosting|Aspire\.AppHost\.Sdk"; then + HAS_ASPIRE_PACKAGE="true" +fi + +if [ -n "$APPHOST_RAW" ] || [ "$HAS_ASPIRE_PACKAGE" = "true" ]; then + IS_ASPIRE="true" +fi + +if [ -n "$APPHOST_RAW" ]; then + APPHOST_PATH=$(to_relative "$APPHOST_RAW") + + # Step 3: Derive the AppHost source directory (scan the real path on disk) + APPHOST_DIR=$(dirname "$APPHOST_PATH") + APPHOST_DIR_RAW=$(dirname "$APPHOST_RAW") + + # Step 4: Scan the AppHost source for ExcludeFromManifest (informational) + if cs_match "$APPHOST_DIR_RAW" "ExcludeFromManifest"; then + HAS_EXCLUDE_FROM_MANIFEST="true" + fi + + # Step 5: Detect Azure Functions and secret-storage configuration + if cs_match "$APPHOST_DIR_RAW" "AddAzureFunctionsProject"; then + HAS_FUNCTIONS="true" + if cs_match "$APPHOST_DIR_RAW" "AzureWebJobsSecretStorageType"; then + SECRET_STORAGE_CONFIGURED="true" + fi + fi +fi + +# Machine-readable result +echo "isAspire=$IS_ASPIRE" +echo "appHostPath=$APPHOST_PATH" +echo "appHostDir=$APPHOST_DIR" +echo "hasExcludeFromManifest=$HAS_EXCLUDE_FROM_MANIFEST" +echo "hasFunctions=$HAS_FUNCTIONS" +echo "secretStorageConfigured=$SECRET_STORAGE_CONFIGURED" + +# Human-readable summary +echo "" +echo "Summary:" +if [ "$IS_ASPIRE" != "true" ]; then + echo "- No .NET Aspire app detected in '$WORKSPACE_ROOT' (no *.AppHost.csproj or Aspire.Hosting / Aspire.AppHost.Sdk package reference)." + exit 0 +fi + +if [ -n "$APPHOST_PATH" ]; then + echo "- .NET Aspire app detected. AppHost project: $APPHOST_PATH" + echo "- AppHost source directory: $APPHOST_DIR" +else + echo "- Aspire.Hosting / Aspire.AppHost.Sdk package reference found, but no *.AppHost.csproj was located." +fi + +if [ "$HAS_EXCLUDE_FROM_MANIFEST" = "true" ]; then + echo "- ExcludeFromManifest found in AppHost source (informational): the app may contain local-only resources." +else + echo "- No ExcludeFromManifest usage found in AppHost source." +fi + +if [ "$HAS_FUNCTIONS" = "true" ]; then + if [ "$SECRET_STORAGE_CONFIGURED" = "true" ]; then + echo "- AddAzureFunctionsProject found; AzureWebJobsSecretStorageType is configured in the AppHost source." + else + echo "- AddAzureFunctionsProject found; AzureWebJobsSecretStorageType is not configured in the AppHost source." + fi +else + echo "- AddAzureFunctionsProject not found in AppHost source." +fi diff --git a/plugin/skills/azure-prepare/references/services/functions/aspire-containerapps.md b/plugin/skills/azure-prepare/references/services/functions/aspire-containerapps.md index 3eef3daa1..2005f7153 100644 --- a/plugin/skills/azure-prepare/references/services/functions/aspire-containerapps.md +++ b/plugin/skills/azure-prepare/references/services/functions/aspire-containerapps.md @@ -4,6 +4,8 @@ When .NET Aspire deploys Azure Functions via `azd`, Functions run as containeriz > ⚠️ **Critical:** When Azure Functions use identity-based storage (e.g., `AzureWebJobsStorage__blobServiceUri`), you **must** set `AzureWebJobsSecretStorageType=Files`. +> 📋 **Confirm with `gather-aspire-info`:** Run [gather-aspire-info.sh](../../scripts/gather-aspire-info.sh) / [gather-aspire-info.ps1](../../scripts/gather-aspire-info.ps1). If it reports `hasFunctions=true` and `secretStorageConfigured=false`, apply the configuration below before `azd up`. See [aspire.md](../../aspire.md) Step 4b for the complete procedure. + ## Proactive Configuration in AppHost **Best Practice:** Add this setting in your AppHost BEFORE running `azd up`: diff --git a/tests/azure-prepare/integration.test.ts b/tests/azure-prepare/integration.test.ts index 52807577f..23b69bba6 100644 --- a/tests/azure-prepare/integration.test.ts +++ b/tests/azure-prepare/integration.test.ts @@ -855,9 +855,9 @@ describeIntegration(`${SKILL_NAME}_ - Integration Tests`, () => { expect(workspacePath).toBeDefined(); expect(isSkillInvoked(agentMetadata, SKILL_NAME)).toBe(true); - // Verify the agent ran the detect-aspire detection script rather than - // re-deriving the find/grep detection sequence inline. - expect(matchesCommand(agentMetadata, /detect-aspire\.(sh|ps1)/)).toBe(true); + // Verify the agent ran the detect-aspire / gather-aspire-info script + // rather than re-deriving the find/grep detection sequence inline. + expect(matchesCommand(agentMetadata, /(detect-aspire|gather-aspire-info)\.(sh|ps1)/)).toBe(true); // Verify azure.yaml exists expectFiles(workspacePath!, [/azure\.yaml$/], []); @@ -905,9 +905,9 @@ describeIntegration(`${SKILL_NAME}_ - Integration Tests`, () => { expect(workspacePath).toBeDefined(); expect(isSkillInvoked(agentMetadata, SKILL_NAME)).toBe(true); - // Verify the agent ran the detect-aspire detection script rather than - // re-deriving the find/grep detection sequence inline. - expect(matchesCommand(agentMetadata, /detect-aspire\.(sh|ps1)/)).toBe(true); + // Verify the agent ran the detect-aspire / gather-aspire-info script + // rather than re-deriving the find/grep detection sequence inline. + expect(matchesCommand(agentMetadata, /(detect-aspire|gather-aspire-info)\.(sh|ps1)/)).toBe(true); expectFiles(workspacePath!, [/azure\.yaml$/], []); From 44b9473b5e4f1b65ab6ade947aa7eb04c8b679f2 Mon Sep 17 00:00:00 2001 From: "Tom Meschter (from Dev Box)" Date: Thu, 11 Jun 2026 10:46:24 -0700 Subject: [PATCH 6/6] Further changes Make some further updates to encourage the LLM to call the detect-aspire scripts. --- plugin/skills/azure-prepare/references/aspire.md | 2 +- plugin/skills/azure-prepare/references/recipes/azd/aspire.md | 4 +++- plugin/skills/azure-prepare/references/scan.md | 2 +- .../skills/azure-prepare/references/scripts/detect-aspire.ps1 | 2 +- .../skills/azure-prepare/references/scripts/detect-aspire.sh | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/plugin/skills/azure-prepare/references/aspire.md b/plugin/skills/azure-prepare/references/aspire.md index d3e6708ea..af6c57529 100644 --- a/plugin/skills/azure-prepare/references/aspire.md +++ b/plugin/skills/azure-prepare/references/aspire.md @@ -19,7 +19,7 @@ Guidance for preparing .NET Aspire applications for Azure deployment. ### Step 1: Detection -When scanning the codebase (per [scan.md](scan.md)), Aspire presence is established with `detect-aspire`. Once confirmed, gather the full set of facts by running [gather-aspire-info.sh](scripts/gather-aspire-info.sh) / [gather-aspire-info.ps1](scripts/gather-aspire-info.ps1). It performs the full deterministic detection sequence in one pass and prints `key=value` lines plus a human-readable summary, so you can branch on the result instead of parsing raw `find`/`grep` output. +When scanning the codebase (per [scan.md](scan.md)), run [detect-aspire.sh](scripts/detect-aspire.sh)/[detect-aspire.ps1](scripts/detect-aspire.ps1) to determine if this is an Aspire application. Once confirmed, gather the full set of facts by running [gather-aspire-info.sh](scripts/gather-aspire-info.sh)/[gather-aspire-info.ps1](scripts/gather-aspire-info.ps1). It performs the full deterministic detection sequence in one pass and prints `key=value` lines plus a human-readable summary, so you can branch on the result instead of parsing raw `find`/`grep` output. **bash:** ```bash diff --git a/plugin/skills/azure-prepare/references/recipes/azd/aspire.md b/plugin/skills/azure-prepare/references/recipes/azd/aspire.md index 26962fb4b..b5e6747d2 100644 --- a/plugin/skills/azure-prepare/references/recipes/azd/aspire.md +++ b/plugin/skills/azure-prepare/references/recipes/azd/aspire.md @@ -2,7 +2,9 @@ **⛔ MANDATORY: For .NET Aspire projects, NEVER manually create azure.yaml. Use `azd init --from-code` instead.** -> 📋 **Gather facts first:** Run [gather-aspire-info.sh](../../scripts/gather-aspire-info.sh) / [gather-aspire-info.ps1](../../scripts/gather-aspire-info.ps1) to capture the `appHostPath`, `hasExcludeFromManifest`, and Azure Functions signals before generating anything. See [aspire.md](../../aspire.md) Step 1 for the full field list. +## Detection + +Run [detect-aspire.sh](../../scripts/detect-aspire.sh)/[detect-aspire.ps1](../../scripts/detect-aspire.ps1) to definitively determine if this is an Aspire app. If it is, run [gather-aspire-info.sh](../../scripts/gather-aspire-info.sh)/[gather-aspire-info.ps1](../../scripts/gather-aspire-info.ps1) to gather further information that will be needed in this workflow. ## Workflow diff --git a/plugin/skills/azure-prepare/references/scan.md b/plugin/skills/azure-prepare/references/scan.md index 4cf8569a0..effc74570 100644 --- a/plugin/skills/azure-prepare/references/scan.md +++ b/plugin/skills/azure-prepare/references/scan.md @@ -56,7 +56,7 @@ Before classifying components, grep dependency files for SDKs that require a spe ### .NET Aspire Detection -Always check **.NET Aspire projects** by running the presence script ([detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1)), which reports just `isAspire` and `appHostPath` so you can branch quickly. Once Aspire is confirmed, [aspire.md](aspire.md) gathers the deeper facts (`ExcludeFromManifest`, Azure Functions signals) via `gather-aspire-info`: +_Always_ check **.NET Aspire projects** by running ([detect-aspire.sh](scripts/detect-aspire.sh) / [detect-aspire.ps1](scripts/detect-aspire.ps1)), which reports just `isAspire` and `appHostPath` so you can branch quickly. Once Aspire is confirmed, [aspire.md](aspire.md) gathers the deeper facts (`ExcludeFromManifest`, Azure Functions signals) via `gather-aspire-info`: **bash:** ```bash diff --git a/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 b/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 index 50a3c5de5..2a020bbeb 100644 --- a/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 +++ b/plugin/skills/azure-prepare/references/scripts/detect-aspire.ps1 @@ -88,4 +88,4 @@ if ($appHostPath) { } else { Write-Output "- Aspire.Hosting / Aspire.AppHost.Sdk package reference found, but no *.AppHost.csproj was located." } -Write-Output "- Run gather-aspire-info.ps1 for AppHost source dir, ExcludeFromManifest, and Azure Functions details." +Write-Output "- Run gather-aspire-info.ps1 to gather other essential deployment information." diff --git a/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh b/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh index fdc0b17a8..b30046ff6 100644 --- a/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh +++ b/plugin/skills/azure-prepare/references/scripts/detect-aspire.sh @@ -85,4 +85,4 @@ if [ -n "$APPHOST_PATH" ]; then else echo "- Aspire.Hosting / Aspire.AppHost.Sdk package reference found, but no *.AppHost.csproj was located." fi -echo "- Run gather-aspire-info.sh for AppHost source dir, ExcludeFromManifest, and Azure Functions details." +echo "- Run gather-aspire-info.sh to gather other essential deployment information."