fix: apply --deploy-mode flags to adopted azure.yaml (#8923)#8926
fix: apply --deploy-mode flags to adopted azure.yaml (#8923)#8926trangevi wants to merge 1 commit into
Conversation
The unified azure.yaml adoption path (runInitFromAzureYaml) now processes --deploy-mode, --runtime, and --entry-point flags after scaffolding the project. For code deploy, code_configuration is written onto the agent service and the language is updated. For container deploy, the docker property is set. When no flags are provided and the service already has code_configuration or a docker property configured, the service is left unchanged (the sample's pre-configured state is respected). 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 #8923, where azd ai agent init -m <azure.yaml> (the unified Foundry adoption path added in #8885) silently ignored --deploy-mode, --runtime, and --entry-point. After the sample's azure.yaml is adopted, a new applyDeployModeToAdoptedProject hook locates the azure.ai.agent service and applies code- or container-deploy configuration, reusing the same promptDeployMode/promptCodeConfig helpers as the manifest path. Configuration is written back to the service via SetServiceConfigValue.
Changes:
- Add
applyDeployModeToAdoptedProjectand theapplyCodeDeployToService/applyContainerDeployToService/adoptedServiceHasCodeConfighelpers, invoked after scaffolding/provider stamping inrunInitFromAzureYaml. - Resolve deploy mode with explicit-flag precedence, no-op when the sample is already configured, and prompt/auto-select otherwise; write
code_configuration/docker/languageonto the agent service. - Add
TestAdoptedServiceHasCodeConfigcovering nil/empty/struct/null property cases.
The core issue with this PR is a key-casing mismatch: the adopted path reads and writes snake_case (code_configuration, entry_point, dependency_resolution), but the unified azure.yaml inline agent shape and the extension's deploy/read path use camelCase (codeConfiguration, entryPoint, dependencyResolution, per schemas/azure.ai.agent.json and AgentDefinitionInline). Because SetServiceConfigValue writes the path verbatim into azure.yaml, the code configuration this PR writes will not be recognized at deploy time, so --deploy-mode code still has no effect — the bug the PR intends to fix. Detection of already-configured samples and the code→container clear path share the same casing problem, and language/runtime detection runs against . instead of the agent's source subdirectory.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go |
Adds the deploy-mode application hook and helpers for the adopted unified azure.yaml; uses snake_case config keys and . for language detection (both incorrect). |
cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go |
Adds TestAdoptedServiceHasCodeConfig, whose fixtures encode the same snake_case key as the code under test. |
| codeConfigMap := map[string]any{ | ||
| "runtime": codeConfig.Runtime, | ||
| "entry_point": codeConfig.EntryPoint, | ||
| } | ||
| if codeConfig.DependencyResolution != nil { | ||
| codeConfigMap["dependency_resolution"] = *codeConfig.DependencyResolution | ||
| } | ||
|
|
||
| codeConfigValue, err := structpb.NewValue(codeConfigMap) | ||
| if err != nil { | ||
| return fmt.Errorf("encoding code_configuration: %w", err) | ||
| } | ||
|
|
||
| if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ | ||
| ServiceName: serviceName, | ||
| Path: "code_configuration", |
| if fields == nil { | ||
| return false | ||
| } | ||
| v, ok := fields["code_configuration"] |
| // Remove code_configuration if present (switching from code to container). | ||
| if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ | ||
| ServiceName: serviceName, | ||
| Path: "code_configuration", |
| // The target directory (project root after adoption) is used for | ||
| // language detection. | ||
| targetDir := "." | ||
| showCodeDeploy := isPythonProject(targetDir) || isDotnetProject(targetDir) |
| for name, svc := range resp.GetProject().GetServices() { | ||
| if svc.GetHost() == AiAgentHost { | ||
| agentServiceName = name | ||
| agentSvc = svc | ||
| break | ||
| } | ||
| } |
| AdditionalProperties: &structpb.Struct{Fields: map[string]*structpb.Value{ | ||
| "code_configuration": structpb.NewStructValue(&structpb.Struct{ | ||
| Fields: map[string]*structpb.Value{ | ||
| "runtime": structpb.NewStringValue("python_3_13"), | ||
| "entry_point": structpb.NewStringValue("app.py"), | ||
| }, | ||
| }), | ||
| }}, |
jongio
left a comment
There was a problem hiding this comment.
Two verified issues that affect correctness, plus a robustness concern:
1. Language detection uses project root instead of service directory (HIGH)
\ argetDir := "."\ means \isPythonProject/\isDotnetProject\ scan the project root, but adopted samples declare agent services in subdirectories (e.g., \project: ./agents/assistant). The manifest path uses the downloaded service directory (\init.go:1388). Here, \�gentSvc.GetRelativePath()\ should be used so auto-detection finds the correct files.
2. Non-deterministic map iteration picks an arbitrary service (MEDIUM)
esp.GetProject().GetServices()\ is a map. With multi-agent samples (researcher, writer, triage), the \�reak\ on line 559 applies configuration to whichever service the runtime iterates first. Either apply to all matching services or collect them and let the user choose.
3. Silently discarded \structpb.NewValue\ errors (LOW)
Lines 665 and 697 discard the error from \structpb.NewValue(language)\ with _. While unlikely to fail for string literals, ignoring errors deviates from the extension's error handling conventions.
Note on the existing bot comments about snake_case vs camelCase: those comments cite \internal/project/agent_definition.go\ and \AgentDefinitionInline, neither of which exist in the codebase. The agent_yaml struct uses YAML tag \code_configuration\ and the service target detection (\service_target_agent.go:1122) also reads \code_configuration. The PR's use of snake_case appears internally consistent. I'd recommend verifying which key the deploy path actually reads from the azure.yaml inline properties to confirm end-to-end correctness.
| // The target directory (project root after adoption) is used for | ||
| // language detection. | ||
| targetDir := "." | ||
| showCodeDeploy := isPythonProject(targetDir) || isDotnetProject(targetDir) |
There was a problem hiding this comment.
targetDir := "." scans the project root, but the sample's agent code lives in the service subdirectory (e.g., ./agents/assistant). The manifest path at init.go:1388 passes the service's source directory to isPythonProject/isDotnetProject. Use agentSvc.GetRelativePath() here so language detection finds the correct files. Without this, showCodeDeploy will be false for most adopted samples, and promptCodeConfig will auto-detect the wrong runtime/entry-point.
| agentSvc = svc | ||
| break | ||
| } | ||
| } |
There was a problem hiding this comment.
Map iteration order is non-deterministic in Go. If the adopted sample declares multiple azure.ai.agent services (e.g., researcher, writer, triage), this picks an arbitrary one and leaves the others unconfigured. Consider iterating all services and applying the deploy mode to each, or detecting the multi-service case and prompting the user.
| }); err != nil { | ||
| return fmt.Errorf("updating service language to %s: %w", language, err) | ||
| } | ||
|
|
There was a problem hiding this comment.
| langValue, err := structpb.NewValue(language) | |
| if err != nil { | |
| return fmt.Errorf("encoding language value: %%w", err) | |
| } |
|
Corrected review summary (the review body above has formatting issues from a shell escape bug): Two verified issues that affect correctness, plus a robustness concern: 1. Language detection uses project root instead of service directory (HIGH)
2. Non-deterministic map iteration picks an arbitrary service (MEDIUM)
3. Silently discarded Lines 665 and 697 discard the error from Note on the existing bot comments about snake_case vs camelCase: those comments cite |
Why
When
azd ai agent init -m <azure.yaml>routes to the unified Foundry adoption path, the--deploy-mode,--runtime, and--entry-pointflags are silently ignored. The adopted azure.yaml is written as-is with nocode_configurationapplied to the agent service. This means users cannot select code deploy when adopting a sample.Approach
After
scaffoldProjectsucceeds, the newapplyDeployModeToAdoptedProjectfunction locates theazure.ai.agentservice in the adopted project and applies deploy-mode configuration using the samepromptDeployMode/promptCodeConfigfunctions the manifest path already uses.Decision logic:
--deploy-modeflag -- always apply (user intent overrides sample)code_configurationordockerproperty -- no-op (respect pre-configured sample)For code deploy: writes
code_configuration(runtime, entry_point, dependency_resolution) onto the service, updates language topython/csharp, clears any docker property.For container deploy: sets the
dockerproperty (remoteBuild: true), updates language todocker, clears any code_configuration.Testing
TestAdoptedServiceHasCodeConfigcovering nil props, empty props, valid struct value, and null value casesgo test ./internal/cmd/... -shortpassesFixes: #8923