fix: run interactive Azure context setup in unified azure.yaml adoption path#8933
fix: run interactive Azure context setup in unified azure.yaml adoption path#8933trangevi wants to merge 6 commits into
Conversation
…on path When 'azd ai agent init -m <azure.yaml>' points at a unified Foundry azure.yaml, the adoption path now runs subscription selection and Foundry project configuration (existing vs new) after scaffolding, matching the agent-manifest flow. Extracts the shared subscription + Foundry project selection logic from configureModelChoice's !hasModelResources branch into a standalone configureFoundryProject helper. Both the adoption path and the agent-manifest path now call this helper, eliminating duplication. Fixes #8922 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…project After selecting a Foundry project, the adoption path now checks each model deployment declared in the azure.yaml against existing deployments in the project. For each deployment, the user can: - Use an existing matching deployment (removes it from azure.yaml) - Deploy as specified (keeps it for provisioning) - Choose a different model (full catalog + deployment prompt) - Skip (removes from azure.yaml) The first selected deployment's name is persisted as AZURE_AI_MODEL_DEPLOYMENT_NAME (same env var as the manifest path). In --no-prompt mode, existing matches are auto-used and unmatched models are auto-deployed. Closes #8922 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tests cover: - foundryDeployments: single/multiple deployments, missing sections, non-project hosts, empty/malformed content, partial fields - stringField: correct type, wrong type, missing, empty - intField: int, float64, wrong type, missing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace map[string]any manual extraction with typed structs (azureYamlServices, azureYamlService) that unmarshal directly into project.Deployment. This gives strong typing at parse time and removes the stringField/intField helpers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Makes the YAML field mapping an explicit contract rather than relying on yaml.v3's lowercase-field-name fallback behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
📋 Prioritization NoteThanks for the contribution! The linked issue isn't in the current milestone yet. |
There was a problem hiding this comment.
Pull request overview
This PR fixes issue #8922, where azd ai agent init -m <azure.yaml> pointed at a unified Foundry azure.yaml scaffolded the project but returned early without running any interactive Azure setup (subscription selection, Foundry project selection, model verification), leaving an environment that could not provision. It wires the interactive setup into the unified adoption path (runInitFromAzureYaml) so it matches the agent-manifest flow.
Changes:
- Extracts the shared subscription + Foundry project selection logic out of
configureModelChoice's!hasModelResourcesbranch into a new reusableconfigureFoundryProjecthelper (returningCredential+FoundryProject), and delegates to it frominit.go. - Adds a model-deployment verification phase to the adoption path: parses deployments from
azure.yaml, compares them against the selected project's existing deployments, and lets the user use-existing / deploy / choose-different / skip (auto-resolved under--no-prompt), rewritingazure.yamland persistingAZURE_AI_MODEL_DEPLOYMENT_NAME. - Adds explicit
yamlstruct tags toproject.Deployment/DeploymentModel/DeploymentSkuand a table-driven test for the newfoundryDeploymentsparser.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
cli/azd/extensions/azure.ai.agents/internal/project/config.go |
Adds yaml tags to Deployment types (additive; matches yaml.v3 default lowercasing). |
cli/azd/extensions/azure.ai.agents/internal/cmd/init.go |
Refactors the !hasModelResources branch to delegate to configureFoundryProject. |
cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_project_setup.go |
New file; the extracted configureFoundryProject helper (project-id / deferred / no-prompt / interactive modes). |
cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go |
Wires subscription/Foundry setup and deployment verification into runInitFromAzureYaml; adds parsing, prompting, and azure.yaml rewrite logic. |
cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go |
Tests for foundryDeployments; adds two skipped placeholder stubs for removed helpers. |
Key findings:
- Critical: the "rewrite
azure.yaml" guard compares only list lengths (init_adopt.go:747). The "Choose a different model" flow replaces a deployment without changing the count, so a single-model manifest where the user swaps the model never gets rewritten — provisioning keeps the original model whileAZURE_AI_MODEL_DEPLOYMENT_NAMEpoints at the new one. - Nit: two always-skipped placeholder tests for removed helpers add no coverage and should be deleted.
| } | ||
|
|
||
| // Update the azure.yaml if the kept deployments differ from the original. | ||
| if len(deploymentsToKeep) != len(deploymentEntries) || len(deploymentsToKeep) == 0 { |
| func TestStringField(t *testing.T) { | ||
| // stringField was removed — this test is no longer needed. | ||
| t.Skip("stringField helper removed in favor of typed YAML unmarshal") | ||
| } | ||
|
|
||
| func TestIntField(t *testing.T) { | ||
| // intField was removed — this test is no longer needed. | ||
| t.Skip("intField helper removed in favor of typed YAML unmarshal") | ||
| } |
jongio
left a comment
There was a problem hiding this comment.
The extraction of configureFoundryProject is a clean refactor that removes real duplication. The deployment verification flow for the adoption path is well-structured and the typed YAML parsing is a good choice over map[string]any. Two issues to address before merging.
- The azure.yaml rewrite guard has a logic gap when the user swaps a model (count stays the same but content changes).
- Non-deterministic service assignment for replacement deployments that don't match any original entry.
| } | ||
|
|
||
| // Update the azure.yaml if the kept deployments differ from the original. | ||
| if len(deploymentsToKeep) != len(deploymentEntries) || len(deploymentsToKeep) == 0 { |
There was a problem hiding this comment.
This guard compares only list lengths:
if len(deploymentsToKeep) != len(deploymentEntries) || len(deploymentsToKeep) == 0 {When the user picks "Choose a different model", the replacement is appended to deploymentsToKeep (same count as original), so this condition is false and the rewrite is skipped. The azure.yaml keeps the old model while AZURE_AI_MODEL_DEPLOYMENT_NAME points to the new one.
A content-aware comparison would fix this. For example, track whether any deployment was actually modified:
| if len(deploymentsToKeep) != len(deploymentEntries) || len(deploymentsToKeep) == 0 { | |
| deploymentsChanged := len(deploymentsToKeep) != len(deploymentEntries) || len(deploymentsToKeep) == 0 | |
| if !deploymentsChanged { | |
| for i, kept := range deploymentsToKeep { | |
| if kept.Name != deploymentEntries[i].Deployment.Name || | |
| kept.Model.Name != deploymentEntries[i].Deployment.Model.Name { | |
| deploymentsChanged = true | |
| break | |
| } | |
| } | |
| } | |
| if deploymentsChanged { |
Note: the order in deploymentsToKeep may not match deploymentEntries order, so a set-based comparison (or a simple modified flag set in the "change" branch of verifyAzureYamlDeployments) might be more reliable.
| } | ||
| if !found { | ||
| // New model chosen — assign to the first service. | ||
| for svc := range byService { |
There was a problem hiding this comment.
When a replacement deployment from "Choose a different model" doesn't match any original entry by name or model name, it's assigned to whichever service the map iterator returns first:
for svc := range byService {
byService[svc] = append(byService[svc], dep)
break
}Go map iteration is non-deterministic. For a multi-service azure.yaml this could assign the replacement to the wrong service across runs. Consider tracking which service originally owned the replaced deployment (e.g., by pairing the original entry.ServiceName with each kept deployment in verifyAzureYamlDeployments) so the replacement inherits the correct service.
| func TestIntField(t *testing.T) { | ||
| // intField was removed — this test is no longer needed. | ||
| t.Skip("intField helper removed in favor of typed YAML unmarshal") | ||
| } |
There was a problem hiding this comment.
These always-skipped stubs (TestStringField, TestIntField) don't test anything. They'll show as passing in CI which masks the fact that no coverage exists for this area. Delete them entirely; the git history preserves the context of why they were removed.
Motivation
When
azd ai agent init -m <azure.yaml>points at a unified Foundry azure.yaml, the command scaffolds the project but returns early without running subscription selection, Foundry project setup, or model deployment verification. Users are left with an environment that cannot provision without manual configuration.Approach
This PR adds two capabilities to the unified azure.yaml adoption path (
runInitFromAzureYaml):Phase 1 -- Subscription and Foundry project setup:
Extracts the shared subscription + Foundry project selection logic from
configureModelChoice's!hasModelResourcesbranch into a standaloneconfigureFoundryProjecthelper. Both the agent-manifest path and the unified azure.yaml adoption path now call this helper, eliminating duplication and ensuring the adoption path runs the same interactive flow.Phase 2 -- Model deployment verification:
After project selection, parses model deployments from the azure.yaml and verifies each one against the selected Foundry project's existing deployments. For each declared deployment, the user can:
PromptAiDeploymentThe first selected deployment is persisted as
AZURE_AI_MODEL_DEPLOYMENT_NAME(same env var as the manifest path).In
--no-promptmode, existing matches are auto-used and unmatched models are auto-deployed.Key design decisions
configureFoundryProjectreturns afoundryProjectSetupResultwithCredentialandFoundryProjectInfo, allowing callers to chain dependent operations (like listing deployments).yamltags onproject.Deploymentrather thanmap[string]any, so field mapping is a compile-time contract.SetServiceConfigValuewith pathdeployments-- same mechanism used bysetServiceUsesfor theuses:field.Files changed
init_foundry_project_setup.go(new) -- extractedconfigureFoundryProjecthelperinit.go-- refactored!hasModelResourcesbranch to delegate to the helperinit_adopt.go-- wired both phases intorunInitFromAzureYaml; added deployment parsing + verification logicinit_adopt_test.go-- unit tests forfoundryDeploymentsparserproject/config.go-- addedyamlstruct tags toDeploymenttypesFixes: #8922