From a4093ea0c2d7b632eb5f5cc31bf2dfeaedac764b Mon Sep 17 00:00:00 2001 From: trangevi Date: Wed, 1 Jul 2026 10:14:00 -0700 Subject: [PATCH 1/5] fix: apply --deploy-mode flags to adopted azure.yaml (#8923) 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> --- .../internal/cmd/init_adopt.go | 200 ++++++++++++++++++ .../internal/cmd/init_adopt_test.go | 51 +++++ 2 files changed, 251 insertions(+) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go index b3805be515d..b66b2d3e3dc 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go @@ -25,6 +25,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/azure/azure-dev/cli/azd/pkg/tools/github" + "google.golang.org/protobuf/types/known/structpb" "gopkg.in/yaml.v3" ) @@ -206,6 +207,14 @@ func runInitFromAzureYaml( return err } + // Apply deploy-mode configuration to the adopted agent service (#8923). + // When the user passes --deploy-mode (and optionally --runtime / + // --entry-point), or when the service doesn't already specify its deploy + // mode, resolve code vs container configuration and update the service. + if err := applyDeployModeToAdoptedProject(ctx, flags, azdClient); err != nil { + return err + } + fmt.Printf( "\nAdopted the sample's azure.yaml as the project manifest at %s.\n", output.WithHighLightFormat("azure.yaml"), @@ -522,3 +531,194 @@ func printAdoptionNextSteps(ctx context.Context, azdClient *azdext.AzdClient, fo state, _ := nextstep.AssembleState(ctx, azdClient, stateOpts...) _ = printAllNextIfTerminal(os.Stdout, nextstep.ResolveAfterInit(state, readmeExistsForProject(ctx, azdClient))) } + +// applyDeployModeToAdoptedProject locates the azure.ai.agent service in the +// adopted project and applies deploy-mode configuration (code or container) +// based on the --deploy-mode, --runtime, and --entry-point flags. When no +// explicit flag is passed and the service already has a code_configuration or +// docker property, the service is left unchanged (the sample is pre-configured). +func applyDeployModeToAdoptedProject( + ctx context.Context, + flags *initFlags, + azdClient *azdext.AzdClient, +) error { + resp, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) + if err != nil { + return fmt.Errorf("reading adopted project: %w", err) + } + + // Find the agent service in the adopted project. + var agentServiceName string + var agentSvc *azdext.ServiceConfig + for name, svc := range resp.GetProject().GetServices() { + if svc.GetHost() == AiAgentHost { + agentServiceName = name + agentSvc = svc + break + } + } + if agentSvc == nil { + // No agent service found — nothing to configure. + return nil + } + + // Check whether the service already specifies its deploy mode. + hasCodeConfig := adoptedServiceHasCodeConfig(agentSvc) + hasDocker := agentSvc.GetDocker() != nil + + // When no explicit --deploy-mode flag is passed and the service is + // already configured, respect the sample's existing configuration. + if flags.deployMode == "" && (hasCodeConfig || hasDocker) { + return nil + } + + // Resolve deploy mode via the same logic the manifest path uses. + // The target directory (project root after adoption) is used for + // language detection. + targetDir := "." + showCodeDeploy := isPythonProject(targetDir) || isDotnetProject(targetDir) + // userProvidedManifest is true: -m was explicitly provided. + deployMode, err := promptDeployMode(ctx, azdClient, flags.noPrompt, showCodeDeploy, flags.deployMode, true) + if err != nil { + return fmt.Errorf("resolving deploy mode for adopted project: %w", err) + } + + if deployMode == "code" { + return applyCodeDeployToService(ctx, flags, azdClient, agentServiceName, targetDir) + } + return applyContainerDeployToService(ctx, azdClient, agentServiceName) +} + +// adoptedServiceHasCodeConfig checks whether the adopted agent service already +// declares a code_configuration in its properties. +func adoptedServiceHasCodeConfig(svc *azdext.ServiceConfig) bool { + props := svc.GetAdditionalProperties() + if props == nil { + return false + } + fields := props.GetFields() + if fields == nil { + return false + } + v, ok := fields["code_configuration"] + if !ok { + return false + } + // A null value doesn't count as having a code_configuration. + return v != nil && v.GetStructValue() != nil +} + +// applyCodeDeployToService writes code_configuration onto the adopted agent +// service and updates the service language from "docker" to the appropriate +// language for the selected runtime. +func applyCodeDeployToService( + ctx context.Context, + flags *initFlags, + azdClient *azdext.AzdClient, + serviceName string, + targetDir string, +) error { + codeConfig, err := promptCodeConfig(ctx, azdClient, targetDir, flags.noPrompt, codeDeployOptions{ + runtime: flags.runtime, + entryPoint: flags.entryPoint, + depResolution: flags.depResolution, + }, true) // userProvidedManifest=true since -m was provided + if err != nil { + return fmt.Errorf("resolving code configuration for adopted project: %w", err) + } + + // Write code_configuration onto the service. + 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", + Value: codeConfigValue, + }); err != nil { + return fmt.Errorf("writing code_configuration to agent service: %w", err) + } + + // Update the service language to match the runtime. + language := "python" + if strings.HasPrefix(codeConfig.Runtime, "dotnet_") { + language = "csharp" + } + langValue, _ := structpb.NewValue(language) + if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ + ServiceName: serviceName, + Path: "language", + Value: langValue, + }); err != nil { + return fmt.Errorf("updating service language to %s: %w", language, err) + } + + // Remove docker property if it was previously set (switching from container to code). + if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ + ServiceName: serviceName, + Path: "docker", + Value: structpb.NewNullValue(), + }); err != nil { + log.Printf("warning: could not clear docker property on service %q: %v", serviceName, err) + } + + log.Printf("Applied code deploy configuration (runtime=%s, entry_point=%s) to service %q", + codeConfig.Runtime, codeConfig.EntryPoint, serviceName) + return nil +} + +// applyContainerDeployToService sets the docker property on the adopted agent +// service and ensures the language is "docker". Removes any code_configuration +// if present. +func applyContainerDeployToService( + ctx context.Context, + azdClient *azdext.AzdClient, + serviceName string, +) error { + // Set docker property with remote build enabled. + dockerMap := map[string]any{"remoteBuild": true} + dockerValue, err := structpb.NewValue(dockerMap) + if err != nil { + return fmt.Errorf("encoding docker configuration: %w", err) + } + + if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ + ServiceName: serviceName, + Path: "docker", + Value: dockerValue, + }); err != nil { + return fmt.Errorf("writing docker property to agent service: %w", err) + } + + // Set language to docker. + langValue, _ := structpb.NewValue("docker") + if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ + ServiceName: serviceName, + Path: "language", + Value: langValue, + }); err != nil { + return fmt.Errorf("updating service language to docker: %w", err) + } + + // Remove code_configuration if present (switching from code to container). + if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ + ServiceName: serviceName, + Path: "code_configuration", + Value: structpb.NewNullValue(), + }); err != nil { + log.Printf("warning: could not clear code_configuration on service %q: %v", serviceName, err) + } + + log.Printf("Applied container deploy configuration to service %q", serviceName) + return nil +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go index a16e4d436b4..65831b3a433 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go @@ -8,7 +8,9 @@ import ( "path/filepath" "testing" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" ) func TestLooksLikeFoundryAzureYaml(t *testing.T) { @@ -274,3 +276,52 @@ func TestStageAzureYamlTemplate_LocalRenamesToAzureYaml(t *testing.T) { // Sibling files are carried into the staging directory. require.True(t, fileExists(filepath.Join(staging, "agents", "main.py"))) } + +func TestAdoptedServiceHasCodeConfig(t *testing.T) { + tests := []struct { + name string + svc *azdext.ServiceConfig + want bool + }{ + { + name: "nil additional properties", + svc: &azdext.ServiceConfig{}, + want: false, + }, + { + name: "empty additional properties", + svc: &azdext.ServiceConfig{ + AdditionalProperties: &structpb.Struct{Fields: map[string]*structpb.Value{}}, + }, + want: false, + }, + { + name: "code_configuration present with struct value", + svc: &azdext.ServiceConfig{ + 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"), + }, + }), + }}, + }, + want: true, + }, + { + name: "code_configuration present but null", + svc: &azdext.ServiceConfig{ + AdditionalProperties: &structpb.Struct{Fields: map[string]*structpb.Value{ + "code_configuration": structpb.NewNullValue(), + }}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, adoptedServiceHasCodeConfig(tt.svc)) + }) + } +} From 511d165002b71dea321e46fa922502b5194ad1b9 Mon Sep 17 00:00:00 2001 From: trangevi Date: Wed, 1 Jul 2026 15:35:37 -0700 Subject: [PATCH 2/5] fix: handle --image flag in azure.yaml adoption path When --image is passed with -m , write it onto the adopted agent service's image field and force container deploy mode. Validate early that --image + --deploy-mode code is incompatible (same as the manifest path). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../internal/cmd/init_adopt.go | 21 +++++++++++++ .../internal/cmd/init_adopt_test.go | 30 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go index b66b2d3e3dc..266b22a5017 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go @@ -542,6 +542,11 @@ func applyDeployModeToAdoptedProject( flags *initFlags, azdClient *azdext.AzdClient, ) error { + // Validate --image flag early (incompatible with --deploy-mode code). + if err := validateImageFlag(flags.image, flags.deployMode); err != nil { + return err + } + resp, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{}) if err != nil { return fmt.Errorf("reading adopted project: %w", err) @@ -562,6 +567,22 @@ func applyDeployModeToAdoptedProject( return nil } + // Apply --image override to the agent service when provided. + if flags.image != "" { + imageValue, _ := structpb.NewValue(flags.image) + if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ + ServiceName: agentServiceName, + Path: "image", + Value: imageValue, + }); err != nil { + return fmt.Errorf("writing image to agent service: %w", err) + } + log.Printf("Applied --image %q to agent service %q", flags.image, agentServiceName) + + // --image implies container deploy; apply container config and return. + return applyContainerDeployToService(ctx, azdClient, agentServiceName) + } + // Check whether the service already specifies its deploy mode. hasCodeConfig := adoptedServiceHasCodeConfig(agentSvc) hasDocker := agentSvc.GetDocker() != nil diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go index 65831b3a433..55d1655ef71 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go @@ -325,3 +325,33 @@ func TestAdoptedServiceHasCodeConfig(t *testing.T) { }) } } + +func TestValidateImageFlagInAdoptionPath(t *testing.T) { + t.Run("image with deploy-mode code is rejected", func(t *testing.T) { + err := validateImageFlag("myacr.azurecr.io/agent:v1", "code") + require.Error(t, err) + require.Contains(t, err.Error(), "--image cannot be used with --deploy-mode code") + }) + + t.Run("image with deploy-mode container is allowed", func(t *testing.T) { + err := validateImageFlag("myacr.azurecr.io/agent:v1", "container") + require.NoError(t, err) + }) + + t.Run("image with no deploy-mode is allowed", func(t *testing.T) { + err := validateImageFlag("myacr.azurecr.io/agent:v1", "") + require.NoError(t, err) + }) + + t.Run("no image is always valid", func(t *testing.T) { + require.NoError(t, validateImageFlag("", "code")) + require.NoError(t, validateImageFlag("", "container")) + require.NoError(t, validateImageFlag("", "")) + }) + + t.Run("invalid image format is rejected", func(t *testing.T) { + err := validateImageFlag("not-a-valid-image", "") + require.Error(t, err) + require.Contains(t, err.Error(), "invalid image URL") + }) +} From 2016077e6882b36765fd653b4f58cdb141e86849 Mon Sep 17 00:00:00 2001 From: trangevi Date: Wed, 1 Jul 2026 16:05:10 -0700 Subject: [PATCH 3/5] fix: address PR review - camelCase keys, multi-service, targetDir, errors - Use camelCase keys (codeConfiguration, entryPoint, dependencyResolution) in SetServiceConfigValue paths to match the azure.yaml inline format that the deploy-time read path expects via JSON unmarshal. - Apply deploy-mode configuration to ALL azure.ai.agent services in the adopted project, not just the first one found (fixes non-deterministic map iteration issue with multi-agent samples). - Use svc.GetRelativePath() for language detection instead of '.' so isPythonProject/isDotnetProject scan the service subdirectory. - Handle structpb.NewValue errors instead of discarding with _. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../internal/cmd/init_adopt.go | 109 +++++++++++------- .../internal/cmd/init_adopt_test.go | 12 +- 2 files changed, 75 insertions(+), 46 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go index 266b22a5017..8a561988c18 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go @@ -535,7 +535,7 @@ func printAdoptionNextSteps(ctx context.Context, azdClient *azdext.AzdClient, fo // applyDeployModeToAdoptedProject locates the azure.ai.agent service in the // adopted project and applies deploy-mode configuration (code or container) // based on the --deploy-mode, --runtime, and --entry-point flags. When no -// explicit flag is passed and the service already has a code_configuration or +// explicit flag is passed and the service already has a codeConfiguration or // docker property, the service is left unchanged (the sample is pre-configured). func applyDeployModeToAdoptedProject( ctx context.Context, @@ -552,40 +552,61 @@ func applyDeployModeToAdoptedProject( return fmt.Errorf("reading adopted project: %w", err) } - // Find the agent service in the adopted project. - var agentServiceName string - var agentSvc *azdext.ServiceConfig + // Collect all agent services in the adopted project. + type agentEntry struct { + name string + svc *azdext.ServiceConfig + } + var agentServices []agentEntry for name, svc := range resp.GetProject().GetServices() { if svc.GetHost() == AiAgentHost { - agentServiceName = name - agentSvc = svc - break + agentServices = append(agentServices, agentEntry{name: name, svc: svc}) } } - if agentSvc == nil { - // No agent service found — nothing to configure. + if len(agentServices) == 0 { + // No agent service found -- nothing to configure. return nil } + // Apply configuration to each agent service. + for _, agent := range agentServices { + if err := applyDeployModeToService(ctx, flags, azdClient, agent.name, agent.svc); err != nil { + return err + } + } + return nil +} + +// applyDeployModeToService applies deploy-mode configuration to a single agent service. +func applyDeployModeToService( + ctx context.Context, + flags *initFlags, + azdClient *azdext.AzdClient, + serviceName string, + svc *azdext.ServiceConfig, +) error { // Apply --image override to the agent service when provided. if flags.image != "" { - imageValue, _ := structpb.NewValue(flags.image) + imageValue, err := structpb.NewValue(flags.image) + if err != nil { + return fmt.Errorf("encoding image value: %w", err) + } if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ - ServiceName: agentServiceName, + ServiceName: serviceName, Path: "image", Value: imageValue, }); err != nil { - return fmt.Errorf("writing image to agent service: %w", err) + return fmt.Errorf("writing image to agent service %q: %w", serviceName, err) } - log.Printf("Applied --image %q to agent service %q", flags.image, agentServiceName) + log.Printf("Applied --image %q to agent service %q", flags.image, serviceName) // --image implies container deploy; apply container config and return. - return applyContainerDeployToService(ctx, azdClient, agentServiceName) + return applyContainerDeployToService(ctx, azdClient, serviceName) } // Check whether the service already specifies its deploy mode. - hasCodeConfig := adoptedServiceHasCodeConfig(agentSvc) - hasDocker := agentSvc.GetDocker() != nil + hasCodeConfig := adoptedServiceHasCodeConfig(svc) + hasDocker := svc.GetDocker() != nil // When no explicit --deploy-mode flag is passed and the service is // already configured, respect the sample's existing configuration. @@ -593,10 +614,11 @@ func applyDeployModeToAdoptedProject( return nil } - // Resolve deploy mode via the same logic the manifest path uses. - // The target directory (project root after adoption) is used for - // language detection. - targetDir := "." + // Use the service's subdirectory for language detection (not project root). + targetDir := svc.GetRelativePath() + if targetDir == "" { + targetDir = "." + } showCodeDeploy := isPythonProject(targetDir) || isDotnetProject(targetDir) // userProvidedManifest is true: -m was explicitly provided. deployMode, err := promptDeployMode(ctx, azdClient, flags.noPrompt, showCodeDeploy, flags.deployMode, true) @@ -605,13 +627,13 @@ func applyDeployModeToAdoptedProject( } if deployMode == "code" { - return applyCodeDeployToService(ctx, flags, azdClient, agentServiceName, targetDir) + return applyCodeDeployToService(ctx, flags, azdClient, serviceName, targetDir) } - return applyContainerDeployToService(ctx, azdClient, agentServiceName) + return applyContainerDeployToService(ctx, azdClient, serviceName) } // adoptedServiceHasCodeConfig checks whether the adopted agent service already -// declares a code_configuration in its properties. +// declares a codeConfiguration in its properties. func adoptedServiceHasCodeConfig(svc *azdext.ServiceConfig) bool { props := svc.GetAdditionalProperties() if props == nil { @@ -621,15 +643,15 @@ func adoptedServiceHasCodeConfig(svc *azdext.ServiceConfig) bool { if fields == nil { return false } - v, ok := fields["code_configuration"] + v, ok := fields["codeConfiguration"] if !ok { return false } - // A null value doesn't count as having a code_configuration. + // A null value doesn't count as having a codeConfiguration. return v != nil && v.GetStructValue() != nil } -// applyCodeDeployToService writes code_configuration onto the adopted agent +// applyCodeDeployToService writes codeConfiguration onto the adopted agent // service and updates the service language from "docker" to the appropriate // language for the selected runtime. func applyCodeDeployToService( @@ -648,26 +670,27 @@ func applyCodeDeployToService( return fmt.Errorf("resolving code configuration for adopted project: %w", err) } - // Write code_configuration onto the service. + // Write codeConfiguration onto the service (camelCase keys match the + // azure.yaml inline format read by the deploy path via JSON unmarshal). codeConfigMap := map[string]any{ - "runtime": codeConfig.Runtime, - "entry_point": codeConfig.EntryPoint, + "runtime": codeConfig.Runtime, + "entryPoint": codeConfig.EntryPoint, } if codeConfig.DependencyResolution != nil { - codeConfigMap["dependency_resolution"] = *codeConfig.DependencyResolution + codeConfigMap["dependencyResolution"] = *codeConfig.DependencyResolution } codeConfigValue, err := structpb.NewValue(codeConfigMap) if err != nil { - return fmt.Errorf("encoding code_configuration: %w", err) + return fmt.Errorf("encoding codeConfiguration: %w", err) } if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ ServiceName: serviceName, - Path: "code_configuration", + Path: "codeConfiguration", Value: codeConfigValue, }); err != nil { - return fmt.Errorf("writing code_configuration to agent service: %w", err) + return fmt.Errorf("writing codeConfiguration to agent service: %w", err) } // Update the service language to match the runtime. @@ -675,7 +698,10 @@ func applyCodeDeployToService( if strings.HasPrefix(codeConfig.Runtime, "dotnet_") { language = "csharp" } - langValue, _ := structpb.NewValue(language) + langValue, err := structpb.NewValue(language) + if err != nil { + return fmt.Errorf("encoding language value: %w", err) + } if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ ServiceName: serviceName, Path: "language", @@ -693,13 +719,13 @@ func applyCodeDeployToService( log.Printf("warning: could not clear docker property on service %q: %v", serviceName, err) } - log.Printf("Applied code deploy configuration (runtime=%s, entry_point=%s) to service %q", + log.Printf("Applied code deploy configuration (runtime=%s, entryPoint=%s) to service %q", codeConfig.Runtime, codeConfig.EntryPoint, serviceName) return nil } // applyContainerDeployToService sets the docker property on the adopted agent -// service and ensures the language is "docker". Removes any code_configuration +// service and ensures the language is "docker". Removes any codeConfiguration // if present. func applyContainerDeployToService( ctx context.Context, @@ -722,7 +748,10 @@ func applyContainerDeployToService( } // Set language to docker. - langValue, _ := structpb.NewValue("docker") + langValue, err := structpb.NewValue("docker") + if err != nil { + return fmt.Errorf("encoding language value: %w", err) + } if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ ServiceName: serviceName, Path: "language", @@ -731,13 +760,13 @@ func applyContainerDeployToService( return fmt.Errorf("updating service language to docker: %w", err) } - // Remove code_configuration if present (switching from code to container). + // Remove codeConfiguration if present (switching from code to container). if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ ServiceName: serviceName, - Path: "code_configuration", + Path: "codeConfiguration", Value: structpb.NewNullValue(), }); err != nil { - log.Printf("warning: could not clear code_configuration on service %q: %v", serviceName, err) + log.Printf("warning: could not clear codeConfiguration on service %q: %v", serviceName, err) } log.Printf("Applied container deploy configuration to service %q", serviceName) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go index 55d1655ef71..516eb2a2425 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go @@ -296,13 +296,13 @@ func TestAdoptedServiceHasCodeConfig(t *testing.T) { want: false, }, { - name: "code_configuration present with struct value", + name: "codeConfiguration present with struct value", svc: &azdext.ServiceConfig{ AdditionalProperties: &structpb.Struct{Fields: map[string]*structpb.Value{ - "code_configuration": structpb.NewStructValue(&structpb.Struct{ + "codeConfiguration": structpb.NewStructValue(&structpb.Struct{ Fields: map[string]*structpb.Value{ - "runtime": structpb.NewStringValue("python_3_13"), - "entry_point": structpb.NewStringValue("app.py"), + "runtime": structpb.NewStringValue("python_3_13"), + "entryPoint": structpb.NewStringValue("app.py"), }, }), }}, @@ -310,10 +310,10 @@ func TestAdoptedServiceHasCodeConfig(t *testing.T) { want: true, }, { - name: "code_configuration present but null", + name: "codeConfiguration present but null", svc: &azdext.ServiceConfig{ AdditionalProperties: &structpb.Struct{Fields: map[string]*structpb.Value{ - "code_configuration": structpb.NewNullValue(), + "codeConfiguration": structpb.NewNullValue(), }}, }, want: false, From 1c8c9f1d432523d3d236ee43097da56e56399017 Mon Sep 17 00:00:00 2001 From: trangevi Date: Thu, 2 Jul 2026 10:12:34 -0700 Subject: [PATCH 4/5] fix: check additionalProperties for docker instead of GetDocker() svc.GetDocker() is always non-nil after the gRPC round-trip because the mapper unconditionally converts the zero-value DockerProjectOptions to a non-nil pointer. This caused the no-flag adoption path to always skip deploy-mode configuration. Replace with adoptedServiceHasDocker() that checks additionalProperties for a 'docker' key, matching how codeConfiguration is detected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../internal/cmd/init_adopt.go | 23 +++++++- .../internal/cmd/init_adopt_test.go | 56 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go index 8a561988c18..bbb20d9231f 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go @@ -606,7 +606,7 @@ func applyDeployModeToService( // Check whether the service already specifies its deploy mode. hasCodeConfig := adoptedServiceHasCodeConfig(svc) - hasDocker := svc.GetDocker() != nil + hasDocker := adoptedServiceHasDocker(svc) // When no explicit --deploy-mode flag is passed and the service is // already configured, respect the sample's existing configuration. @@ -651,6 +651,27 @@ func adoptedServiceHasCodeConfig(svc *azdext.ServiceConfig) bool { return v != nil && v.GetStructValue() != nil } +// adoptedServiceHasDocker checks whether the adopted agent service already +// declares a docker configuration in its properties. We check +// additionalProperties rather than svc.GetDocker() because the gRPC mapper +// always returns a non-nil Docker pointer (even for the zero-value struct). +func adoptedServiceHasDocker(svc *azdext.ServiceConfig) bool { + props := svc.GetAdditionalProperties() + if props == nil { + return false + } + fields := props.GetFields() + if fields == nil { + return false + } + v, ok := fields["docker"] + if !ok { + return false + } + // A null value doesn't count as having docker configured. + return v != nil && v.GetStructValue() != nil +} + // applyCodeDeployToService writes codeConfiguration onto the adopted agent // service and updates the service language from "docker" to the appropriate // language for the selected runtime. diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go index 516eb2a2425..82fcd8e5cb5 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_test.go @@ -326,6 +326,62 @@ func TestAdoptedServiceHasCodeConfig(t *testing.T) { } } +func TestAdoptedServiceHasDocker(t *testing.T) { + tests := []struct { + name string + svc *azdext.ServiceConfig + want bool + }{ + { + name: "nil additional properties", + svc: &azdext.ServiceConfig{}, + want: false, + }, + { + name: "empty additional properties", + svc: &azdext.ServiceConfig{ + AdditionalProperties: &structpb.Struct{Fields: map[string]*structpb.Value{}}, + }, + want: false, + }, + { + name: "docker present with struct value", + svc: &azdext.ServiceConfig{ + AdditionalProperties: &structpb.Struct{Fields: map[string]*structpb.Value{ + "docker": structpb.NewStructValue(&structpb.Struct{ + Fields: map[string]*structpb.Value{ + "remoteBuild": structpb.NewBoolValue(true), + }, + }), + }}, + }, + want: true, + }, + { + name: "docker present but null", + svc: &azdext.ServiceConfig{ + AdditionalProperties: &structpb.Struct{Fields: map[string]*structpb.Value{ + "docker": structpb.NewNullValue(), + }}, + }, + want: false, + }, + { + name: "non-nil GetDocker but no docker in additionalProperties", + svc: &azdext.ServiceConfig{ + Docker: &azdext.DockerProjectOptions{}, + AdditionalProperties: &structpb.Struct{Fields: map[string]*structpb.Value{}}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, adoptedServiceHasDocker(tt.svc)) + }) + } +} + func TestValidateImageFlagInAdoptionPath(t *testing.T) { t.Run("image with deploy-mode code is rejected", func(t *testing.T) { err := validateImageFlag("myacr.azurecr.io/agent:v1", "code") From 9e6b66527d9b28f81c1d5d8e0f6fae1638d8185a Mon Sep 17 00:00:00 2001 From: trangevi Date: Thu, 2 Jul 2026 11:47:26 -0700 Subject: [PATCH 5/5] fix: use UnsetServiceConfig to remove keys instead of writing null When switching deploy modes, use UnsetServiceConfig (which deletes the map key) instead of SetServiceConfigValue with a null protobuf value. The null approach left 'codeConfiguration: null' or 'docker: null' in the azure.yaml because nil values in map[string]any are marshaled as YAML null rather than being omitted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../internal/cmd/init_adopt.go | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go index bbb20d9231f..39ca1d6d281 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go @@ -601,7 +601,7 @@ func applyDeployModeToService( log.Printf("Applied --image %q to agent service %q", flags.image, serviceName) // --image implies container deploy; apply container config and return. - return applyContainerDeployToService(ctx, azdClient, serviceName) + return applyContainerDeployToService(ctx, azdClient, serviceName, svc) } // Check whether the service already specifies its deploy mode. @@ -627,9 +627,9 @@ func applyDeployModeToService( } if deployMode == "code" { - return applyCodeDeployToService(ctx, flags, azdClient, serviceName, targetDir) + return applyCodeDeployToService(ctx, flags, azdClient, serviceName, targetDir, svc) } - return applyContainerDeployToService(ctx, azdClient, serviceName) + return applyContainerDeployToService(ctx, azdClient, serviceName, svc) } // adoptedServiceHasCodeConfig checks whether the adopted agent service already @@ -681,6 +681,7 @@ func applyCodeDeployToService( azdClient *azdext.AzdClient, serviceName string, targetDir string, + svc *azdext.ServiceConfig, ) error { codeConfig, err := promptCodeConfig(ctx, azdClient, targetDir, flags.noPrompt, codeDeployOptions{ runtime: flags.runtime, @@ -732,12 +733,13 @@ func applyCodeDeployToService( } // Remove docker property if it was previously set (switching from container to code). - if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ - ServiceName: serviceName, - Path: "docker", - Value: structpb.NewNullValue(), - }); err != nil { - log.Printf("warning: could not clear docker property on service %q: %v", serviceName, err) + if adoptedServiceHasDocker(svc) { + if _, err := azdClient.Project().UnsetServiceConfig(ctx, &azdext.UnsetServiceConfigRequest{ + ServiceName: serviceName, + Path: "docker", + }); err != nil { + log.Printf("warning: could not clear docker property on service %q: %v", serviceName, err) + } } log.Printf("Applied code deploy configuration (runtime=%s, entryPoint=%s) to service %q", @@ -752,6 +754,7 @@ func applyContainerDeployToService( ctx context.Context, azdClient *azdext.AzdClient, serviceName string, + svc *azdext.ServiceConfig, ) error { // Set docker property with remote build enabled. dockerMap := map[string]any{"remoteBuild": true} @@ -782,12 +785,13 @@ func applyContainerDeployToService( } // Remove codeConfiguration if present (switching from code to container). - if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ - ServiceName: serviceName, - Path: "codeConfiguration", - Value: structpb.NewNullValue(), - }); err != nil { - log.Printf("warning: could not clear codeConfiguration on service %q: %v", serviceName, err) + if adoptedServiceHasCodeConfig(svc) { + if _, err := azdClient.Project().UnsetServiceConfig(ctx, &azdext.UnsetServiceConfigRequest{ + ServiceName: serviceName, + Path: "codeConfiguration", + }); err != nil { + log.Printf("warning: could not clear codeConfiguration on service %q: %v", serviceName, err) + } } log.Printf("Applied container deploy configuration to service %q", serviceName)