Skip to content
96 changes: 41 additions & 55 deletions plugin/skills/azure-prepare/references/aspire.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,61 +15,53 @@ 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

When scanning the codebase (per [scan.md](scan.md)), detect Aspire by:
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
# Check for AppHost project
find . -name "*.AppHost.csproj"
./scripts/gather-aspire-info.sh [workspace-root]
```

# Or check for Aspire.Hosting package reference
grep -r "Aspire.Hosting" . --include="*.csproj"
**PowerShell:**
```powershell
./scripts/gather-aspire-info.ps1 -WorkspaceRoot <workspace-root>
```

### ⛔ 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 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) |
| `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 `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.

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.
Expand Down Expand Up @@ -185,31 +177,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 `gather-aspire-info` run. 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/gather-aspire-info.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/gather-aspire-info.ps1 -WorkspaceRoot <workspace-root>
```

**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:**

Expand Down
14 changes: 9 additions & 5 deletions plugin/skills/azure-prepare/references/generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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
# 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 <workspace-root>
```

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`
2. ⛔ **STOP** - Do NOT manually create `infra/` files
Expand Down
12 changes: 11 additions & 1 deletion plugin/skills/azure-prepare/references/recipe-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <workspace-root>
```

> 💡 **Tip:** .NET Aspire projects always use AZD recipe with auto-generated configuration. Do not manually select recipe or create artifacts.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <environment-name>` → [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 <environment-name>` → [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) |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@

## 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"` |
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

Expand Down
19 changes: 14 additions & 5 deletions plugin/skills/azure-prepare/references/scan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
_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
./scripts/detect-aspire.sh [workspace-root]
```

**PowerShell:**
```powershell
./scripts/detect-aspire.ps1 -WorkspaceRoot <workspace-root>
```

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 <environment-name>` 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:** 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<#
.SYNOPSIS
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 minimal deterministic presence sequence:
1. Find the AppHost project (*.AppHost.csproj)
2. Confirm Aspire.Hosting or Aspire.AppHost.Sdk package references

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.

.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 = ""

# 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) {
Comment thread
tmeschter marked this conversation as resolved.
# 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
}
}

function ConvertTo-Lower([bool]$value) {
if ($value) { "true" } else { "false" }
}

# Machine-readable result
Write-Output "isAspire=$(ConvertTo-Lower $isAspire)"
Write-Output "appHostPath=$appHostPath"

# 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
Comment thread
tmeschter marked this conversation as resolved.
}

if ($appHostPath) {
Write-Output "- .NET Aspire app detected. AppHost project: $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 to gather other essential deployment information."
Loading
Loading