From e1588e4b43195ec56d4510703a5ad411d9473af0 Mon Sep 17 00:00:00 2001 From: Jian Wu Date: Fri, 22 May 2026 15:53:48 +0800 Subject: [PATCH 1/5] feat: support code deploy in --no-prompt mode via CLI flags Add --deploy-mode, --runtime, --entry-point, and --dep-resolution flags to enable non-interactive code deploy for CI/CD pipelines. - --deploy-mode code|container (default: container, backward compatible) - --runtime python_3_13|python_3_14|dotnet_10 (required for code deploy) - --entry-point (required for code deploy) - --dep-resolution remote_build|bundled (default: remote_build) These flags produce the same output as interactive mode (agent.yaml code_configuration + azure.yaml language field + SKIP_ACR_CREATION env var). No changes to deploy-time behavior. --- .../azure.ai.agents/internal/cmd/init.go | 43 +++++++++- .../internal/cmd/init_from_code.go | 78 +++++++++++++++---- 2 files changed, 105 insertions(+), 16 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 8a8a70c8e1a..77fb176fe6c 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -53,6 +53,11 @@ type initFlags struct { src string env string protocols []string + // deploy mode flags for non-interactive code deploy support + deployMode string // "container" or "code"; empty = prompt interactively + runtime string // e.g. "python_3_13", "python_3_14", "dotnet_10" + entryPoint string // e.g. "app.py", "MyAgent.dll" + depResolution string // "remote_build" or "bundled"; defaults to "remote_build" // force, when true, lets headless callers (--no-prompt) pre-consent to // overwrite prompts that would otherwise return a structured error. It // mirrors the `--force` convention used by `azd down`, `azd env remove`, @@ -852,6 +857,18 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, cmd.Flags().StringSliceVar(&flags.protocols, "protocol", nil, "Protocols supported by the agent (e.g., 'responses', 'invocations'). Can be specified multiple times.") + cmd.Flags().StringVar(&flags.deployMode, "deploy-mode", "", + "Deployment mode: 'container' (Docker image) or 'code' (ZIP upload). Defaults to 'container' in --no-prompt.") + + cmd.Flags().StringVar(&flags.runtime, "runtime", "", + "Runtime for code deploy (e.g., 'python_3_13', 'python_3_14', 'dotnet_10'). Required with --deploy-mode code --no-prompt.") + + cmd.Flags().StringVar(&flags.entryPoint, "entry-point", "", + "Entry point file for code deploy (e.g., 'app.py', 'MyAgent.dll'). Required with --deploy-mode code --no-prompt.") + + cmd.Flags().StringVar(&flags.depResolution, "dep-resolution", "", + "Dependency resolution for code deploy: 'remote_build' or 'bundled'. Defaults to 'remote_build'.") + cmd.Flags().BoolVar(&flags.force, "force", false, "Overwrite an input manifest that already lives inside the generated src tree without prompting. "+ "Required together with --no-prompt when init would otherwise need confirmation.") @@ -875,6 +892,24 @@ func (a *InitAction) Run(ctx context.Context) error { a.flags.src = relPath } + // Validate code deploy flags + if a.flags.noPrompt && a.flags.deployMode == "code" { + if a.flags.runtime == "" { + return exterrors.Validation( + exterrors.CodeInvalidParameter, + "--runtime is required when using --deploy-mode code with --no-prompt", + "Specify --runtime (e.g., python_3_13, python_3_14, dotnet_10)", + ) + } + if a.flags.entryPoint == "" { + return exterrors.Validation( + exterrors.CodeInvalidParameter, + "--entry-point is required when using --deploy-mode code with --no-prompt", + "Specify --entry-point (e.g., app.py, main.py, MyAgent.dll)", + ) + } + } + // If --manifest is given if a.flags.manifestPointer != "" { // Validate that the manifest pointer is either a valid URL or existing file path @@ -910,7 +945,7 @@ func (a *InitAction) Run(ctx context.Context) error { // Code deploy is supported for Python and .NET projects. if _, ok := agentManifest.Template.(agent_yaml.ContainerAgent); ok { showCodeDeploy := isPythonProject(targetDir) || isDotnetProject(targetDir) - deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, showCodeDeploy) + deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, showCodeDeploy, a.flags.deployMode) if err != nil { return fmt.Errorf("prompting for deploy mode: %w", err) } @@ -918,7 +953,11 @@ func (a *InitAction) Run(ctx context.Context) error { if a.isCodeDeploy { // Prompt for code configuration and update the manifest - codeConfig, err := promptCodeConfig(ctx, a.azdClient, targetDir, a.flags.noPrompt) + codeConfig, err := promptCodeConfig(ctx, a.azdClient, targetDir, a.flags.noPrompt, codeDeployOptions{ + runtime: a.flags.runtime, + entryPoint: a.flags.entryPoint, + depResolution: a.flags.depResolution, + }) if err != nil { return fmt.Errorf("prompting for code configuration: %w", err) } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go index 1464b1eb709..d96e282eb10 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go @@ -74,6 +74,24 @@ func (a *InitFromCodeAction) Run(ctx context.Context) error { a.flags.src = relPath } + // Validate code deploy flags + if a.flags.noPrompt && a.flags.deployMode == "code" { + if a.flags.runtime == "" { + return exterrors.Validation( + exterrors.CodeInvalidParameter, + "--runtime is required when using --deploy-mode code with --no-prompt", + "Specify --runtime (e.g., python_3_13, python_3_14, dotnet_10)", + ) + } + if a.flags.entryPoint == "" { + return exterrors.Validation( + exterrors.CodeInvalidParameter, + "--entry-point is required when using --deploy-mode code with --no-prompt", + "Specify --entry-point (e.g., app.py, main.py, MyAgent.dll)", + ) + } + } + // Default src to current directory when not specified srcDir := a.flags.src if srcDir == "" { @@ -487,7 +505,7 @@ func (a *InitFromCodeAction) createDefinitionFromLocalAgent(ctx context.Context) srcDir, _ = os.Getwd() } showCodeDeploy := isPythonProject(srcDir) || isDotnetProject(srcDir) - deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, showCodeDeploy) + deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, showCodeDeploy, a.flags.deployMode) if err != nil { return nil, err } @@ -1008,7 +1026,11 @@ func (a *InitFromCodeAction) addToProject(ctx context.Context, targetDir string, // promptCodeConfiguration prompts the user for code deploy configuration settings. func (a *InitFromCodeAction) promptCodeConfiguration(ctx context.Context, srcDir string) (*agent_yaml.CodeConfiguration, error) { - return promptCodeConfig(ctx, a.azdClient, srcDir, a.flags.noPrompt) + return promptCodeConfig(ctx, a.azdClient, srcDir, a.flags.noPrompt, codeDeployOptions{ + runtime: a.flags.runtime, + entryPoint: a.flags.entryPoint, + depResolution: a.flags.depResolution, + }) } // protocolInfo pairs a protocol name with the default version used when generating agent.yaml. @@ -1135,22 +1157,37 @@ func knownProtocolNames() string { } // promptDeployMode asks the user to choose between code deploy and container deploy. -// When noPrompt is true, defaults to "container" for backward compatibility. -// When showCodeDeploy is false, code deploy is not offered (e.g. for non-Python languages). -func promptDeployMode(ctx context.Context, azdClient *azdext.AzdClient, noPrompt bool, showCodeDeploy bool) (string, error) { +// When deployModeFlag is set, it is used directly (for --no-prompt with explicit flag). +// When noPrompt is true and no flag is provided, defaults to "container" for backward compatibility. +// When showCodeDeploy is false and no explicit flag overrides, code deploy is not offered. +func promptDeployMode(ctx context.Context, azdClient *azdext.AzdClient, noPrompt bool, showCodeDeploy bool, deployModeFlag string) (string, error) { + // Explicit flag takes precedence + if deployModeFlag != "" { + switch deployModeFlag { + case "container", "code": + return deployModeFlag, nil + default: + return "", exterrors.Validation( + exterrors.CodeInvalidParameter, + fmt.Sprintf("invalid --deploy-mode value %q; must be 'container' or 'code'", deployModeFlag), + "Use --deploy-mode container or --deploy-mode code", + ) + } + } + if !showCodeDeploy { return "container", nil } + if noPrompt { + return "container", nil + } + deployModeChoices := []*azdext.SelectChoice{ {Label: "Container Image (Docker)", Value: "container"}, {Label: "Source Code (ZIP upload)", Value: "code"}, } - if noPrompt { - return "container", nil - } - defaultIdx := int32(0) // Container is the default for backward compatibility deployModeResp, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ Options: &azdext.SelectOptions{ @@ -1221,9 +1258,16 @@ func extractAssemblyName(csprojContent string) string { return name } +// codeDeployOptions holds optional flag overrides for code deploy configuration. +type codeDeployOptions struct { + runtime string + entryPoint string + depResolution string +} + // promptCodeConfig prompts for code deploy configuration (runtime, entry point, -// dependency resolution). When noPrompt is true, defaults are used without prompting. -func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir string, noPrompt bool) (*agent_yaml.CodeConfiguration, error) { +// dependency resolution). When noPrompt is true, flags or defaults are used without prompting. +func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir string, noPrompt bool, opts codeDeployOptions) (*agent_yaml.CodeConfiguration, error) { if srcDir == "" { srcDir = "." } @@ -1252,7 +1296,9 @@ func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir s } var runtime string - if noPrompt { + if opts.runtime != "" { + runtime = opts.runtime + } else if noPrompt { if isDotnet && !isPython { runtime = "dotnet_10" } else { @@ -1280,7 +1326,9 @@ func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir s defaultEntryPoint := detectDefaultEntryPoint(srcDir, runtime) var entryPoint string - if noPrompt { + if opts.entryPoint != "" { + entryPoint = opts.entryPoint + } else if noPrompt { entryPoint = defaultEntryPoint } else { entryPointResp, err := azdClient.Prompt().Prompt(ctx, &azdext.PromptRequest{ @@ -1305,7 +1353,9 @@ func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir s } var depResolution string - if noPrompt { + if opts.depResolution != "" { + depResolution = opts.depResolution + } else if noPrompt { depResolution = "remote_build" } else { depDefaultIdx := int32(0) From 0b06f775b1c05afbfb7f3801e520dbeffab25532 Mon Sep 17 00:00:00 2001 From: Jian Wu Date: Fri, 22 May 2026 16:14:20 +0800 Subject: [PATCH 2/5] Add unit tests for code deploy flag validation, promptDeployMode, and promptCodeConfig Extract validateCodeDeployFlags into a testable method and add 16 test cases covering flag precedence, noPrompt defaults, and error conditions. --- .../azure.ai.agents/internal/cmd/init.go | 39 ++-- .../internal/cmd/init_from_code_test.go | 168 ++++++++++++++++++ .../azure.ai.agents/internal/cmd/init_test.go | 59 ++++++ 3 files changed, 251 insertions(+), 15 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 77fb176fe6c..5de0bfd56a7 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -893,21 +893,8 @@ func (a *InitAction) Run(ctx context.Context) error { } // Validate code deploy flags - if a.flags.noPrompt && a.flags.deployMode == "code" { - if a.flags.runtime == "" { - return exterrors.Validation( - exterrors.CodeInvalidParameter, - "--runtime is required when using --deploy-mode code with --no-prompt", - "Specify --runtime (e.g., python_3_13, python_3_14, dotnet_10)", - ) - } - if a.flags.entryPoint == "" { - return exterrors.Validation( - exterrors.CodeInvalidParameter, - "--entry-point is required when using --deploy-mode code with --no-prompt", - "Specify --entry-point (e.g., app.py, main.py, MyAgent.dll)", - ) - } + if err := a.validateCodeDeployFlags(); err != nil { + return err } // If --manifest is given @@ -2986,3 +2973,25 @@ func extractConnectionConfigs( return connections, credentialEnvVars, nil } + +// validateCodeDeployFlags checks that required flags are present when using +// --deploy-mode code in --no-prompt mode. +func (a *InitAction) validateCodeDeployFlags() error { + if a.flags.noPrompt && a.flags.deployMode == "code" { + if a.flags.runtime == "" { + return exterrors.Validation( + exterrors.CodeInvalidParameter, + "--runtime is required when using --deploy-mode code with --no-prompt", + "Specify --runtime (e.g., python_3_13, python_3_14, dotnet_10)", + ) + } + if a.flags.entryPoint == "" { + return exterrors.Validation( + exterrors.CodeInvalidParameter, + "--entry-point is required when using --deploy-mode code with --no-prompt", + "Specify --entry-point (e.g., app.py, main.py, MyAgent.dll)", + ) + } + } + return nil +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go index b6bd1cc9aa6..84e6d151669 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go @@ -821,6 +821,174 @@ func TestPromptProtocols_Interactive(t *testing.T) { } } +func TestPromptDeployMode_FlagOverride(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + noPrompt bool + showCodeDeploy bool + flag string + want string + wantErr bool + wantErrContain string + }{ + { + name: "flag=container returns container", + noPrompt: true, + showCodeDeploy: true, + flag: "container", + want: "container", + }, + { + name: "flag=code returns code", + noPrompt: true, + showCodeDeploy: true, + flag: "code", + want: "code", + }, + { + name: "flag=code works even when showCodeDeploy=false", + noPrompt: true, + showCodeDeploy: false, + flag: "code", + want: "code", + }, + { + name: "invalid flag value returns error", + noPrompt: true, + showCodeDeploy: true, + flag: "invalid", + wantErr: true, + wantErrContain: "invalid --deploy-mode value", + }, + { + name: "no flag + noPrompt defaults to container", + noPrompt: true, + showCodeDeploy: true, + flag: "", + want: "container", + }, + { + name: "no flag + showCodeDeploy=false defaults to container", + noPrompt: false, + showCodeDeploy: false, + flag: "", + want: "container", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := promptDeployMode(t.Context(), nil, tt.noPrompt, tt.showCodeDeploy, tt.flag) + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + if tt.wantErrContain != "" && !strings.Contains(err.Error(), tt.wantErrContain) { + t.Errorf("error = %q, want containing %q", err.Error(), tt.wantErrContain) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("promptDeployMode() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestPromptCodeConfig_FlagOverrides(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + files []string // files to create in temp dir + noPrompt bool + opts codeDeployOptions + wantRuntime string + wantEntry string + wantDepRes string + }{ + { + name: "all opts provided", + noPrompt: true, + opts: codeDeployOptions{runtime: "python_3_14", entryPoint: "bot.py", depResolution: "bundled"}, + wantRuntime: "python_3_14", + wantEntry: "bot.py", + wantDepRes: "bundled", + }, + { + name: "noPrompt defaults for python project", + files: []string{"requirements.txt", "app.py"}, + noPrompt: true, + opts: codeDeployOptions{}, + wantRuntime: "python_3_13", + wantEntry: "app.py", + wantDepRes: "remote_build", + }, + { + name: "noPrompt defaults for dotnet project", + files: []string{"MyBot.csproj", "Program.cs"}, + noPrompt: true, + opts: codeDeployOptions{}, + wantRuntime: "dotnet_10", + wantEntry: "MyBot.dll", + wantDepRes: "remote_build", + }, + { + name: "opts override noPrompt defaults", + files: []string{"requirements.txt", "app.py"}, + noPrompt: true, + opts: codeDeployOptions{runtime: "python_3_14", entryPoint: "serve.py", depResolution: "bundled"}, + wantRuntime: "python_3_14", + wantEntry: "serve.py", + wantDepRes: "bundled", + }, + { + name: "partial opts — runtime from flag, rest from defaults", + files: []string{"app.py"}, + noPrompt: true, + opts: codeDeployOptions{runtime: "python_3_14"}, + wantRuntime: "python_3_14", + wantEntry: "app.py", + wantDepRes: "remote_build", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + for _, f := range tt.files { + if err := os.WriteFile(filepath.Join(dir, f), []byte(""), 0600); err != nil { + t.Fatal(err) + } + } + + got, err := promptCodeConfig(t.Context(), nil, dir, tt.noPrompt, tt.opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.Runtime != tt.wantRuntime { + t.Errorf("Runtime = %q, want %q", got.Runtime, tt.wantRuntime) + } + if got.EntryPoint != tt.wantEntry { + t.Errorf("EntryPoint = %q, want %q", got.EntryPoint, tt.wantEntry) + } + if got.DependencyResolution == nil { + t.Fatal("DependencyResolution is nil") + } + if *got.DependencyResolution != tt.wantDepRes { + t.Errorf("DependencyResolution = %q, want %q", *got.DependencyResolution, tt.wantDepRes) + } + }) + } +} + func TestDetectDefaultEntryPoint(t *testing.T) { tests := []struct { name string diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go index c37af2b2eb2..e133e3dc2e2 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go @@ -2282,3 +2282,62 @@ func TestDownloadAgentYaml_NoPromptManifestInSrcWithoutForce(t *testing.T) { t.Errorf("suggestion should mention --force, got: %s", localErr.Suggestion) } } + +func TestCodeDeployFlagValidation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + flags initFlags + wantErr bool + wantErrContain string + }{ + { + name: "code deploy with all required flags passes validation", + flags: initFlags{noPrompt: true, deployMode: "code", runtime: "python_3_13", entryPoint: "app.py"}, + wantErr: false, + }, + { + name: "code deploy without runtime fails", + flags: initFlags{noPrompt: true, deployMode: "code", entryPoint: "app.py"}, + wantErr: true, + wantErrContain: "--runtime is required", + }, + { + name: "code deploy without entry-point fails", + flags: initFlags{noPrompt: true, deployMode: "code", runtime: "python_3_13"}, + wantErr: true, + wantErrContain: "--entry-point is required", + }, + { + name: "container deploy without runtime/entry-point passes", + flags: initFlags{noPrompt: true, deployMode: "container"}, + wantErr: false, + }, + { + name: "code deploy without noPrompt skips validation", + flags: initFlags{noPrompt: false, deployMode: "code"}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + a := &InitAction{flags: &tt.flags} + err := a.validateCodeDeployFlags() + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), tt.wantErrContain) { + t.Errorf("error = %q, want containing %q", err.Error(), tt.wantErrContain) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} From 9d82b9ead167d2270de57da5268c4ce1d0363b57 Mon Sep 17 00:00:00 2001 From: Jian Wu Date: Fri, 22 May 2026 16:18:56 +0800 Subject: [PATCH 3/5] refactor: extract shared validateCodeDeployInput to eliminate duplication --- .../azure.ai.agents/internal/cmd/init.go | 12 +++++++++--- .../internal/cmd/init_from_code.go | 17 ++--------------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 5de0bfd56a7..30a17e92795 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -2977,15 +2977,21 @@ func extractConnectionConfigs( // validateCodeDeployFlags checks that required flags are present when using // --deploy-mode code in --no-prompt mode. func (a *InitAction) validateCodeDeployFlags() error { - if a.flags.noPrompt && a.flags.deployMode == "code" { - if a.flags.runtime == "" { + return validateCodeDeployInput(a.flags.noPrompt, a.flags.deployMode, a.flags.runtime, a.flags.entryPoint) +} + +// validateCodeDeployInput is the shared validation logic for code deploy flags. +// Used by both InitAction and InitFromCodeAction. +func validateCodeDeployInput(noPrompt bool, deployMode, runtime, entryPoint string) error { + if noPrompt && deployMode == "code" { + if runtime == "" { return exterrors.Validation( exterrors.CodeInvalidParameter, "--runtime is required when using --deploy-mode code with --no-prompt", "Specify --runtime (e.g., python_3_13, python_3_14, dotnet_10)", ) } - if a.flags.entryPoint == "" { + if entryPoint == "" { return exterrors.Validation( exterrors.CodeInvalidParameter, "--entry-point is required when using --deploy-mode code with --no-prompt", diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go index d96e282eb10..74de1d8fc1c 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go @@ -75,21 +75,8 @@ func (a *InitFromCodeAction) Run(ctx context.Context) error { } // Validate code deploy flags - if a.flags.noPrompt && a.flags.deployMode == "code" { - if a.flags.runtime == "" { - return exterrors.Validation( - exterrors.CodeInvalidParameter, - "--runtime is required when using --deploy-mode code with --no-prompt", - "Specify --runtime (e.g., python_3_13, python_3_14, dotnet_10)", - ) - } - if a.flags.entryPoint == "" { - return exterrors.Validation( - exterrors.CodeInvalidParameter, - "--entry-point is required when using --deploy-mode code with --no-prompt", - "Specify --entry-point (e.g., app.py, main.py, MyAgent.dll)", - ) - } + if err := validateCodeDeployInput(a.flags.noPrompt, a.flags.deployMode, a.flags.runtime, a.flags.entryPoint); err != nil { + return err } // Default src to current directory when not specified From 3524f85d82c475c987567c009f789421c29791ff Mon Sep 17 00:00:00 2001 From: Jian Wu Date: Fri, 22 May 2026 16:27:03 +0800 Subject: [PATCH 4/5] docs: add non-interactive code deploy example to init --help --- cli/azd/extensions/azure.ai.agents/internal/cmd/init.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 30a17e92795..9a413b69462 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -618,7 +618,12 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, azd ai agent init -m ./agent.manifest.yaml --agent-name my-unique-agent # Initialize from local agent code - azd ai agent init --src ./src/my-agent --agent-name my-unique-agent`, + azd ai agent init --src ./src/my-agent --agent-name my-unique-agent + + # Non-interactive code deploy (CI/CD) + azd ai agent init --no-prompt \ + --project-id "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/" \ + --deploy-mode code --runtime python_3_13 --entry-point app.py`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { flags.noPrompt = extCtx.NoPrompt From 163bd3279707f131eaa34cc89fc490d865b81d58 Mon Sep 17 00:00:00 2001 From: Jian Wu Date: Fri, 22 May 2026 16:47:52 +0800 Subject: [PATCH 5/5] fix: validate --runtime, --deploy-mode, and --dep-resolution flag values --- .../azure.ai.agents/internal/cmd/init.go | 36 ++++++++++++++++--- .../internal/cmd/init_from_code.go | 4 ++- .../azure.ai.agents/internal/cmd/init_test.go | 18 ++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 9a413b69462..12ad85334c9 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -621,8 +621,7 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`, azd ai agent init --src ./src/my-agent --agent-name my-unique-agent # Non-interactive code deploy (CI/CD) - azd ai agent init --no-prompt \ - --project-id "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/" \ + azd ai agent init --no-prompt --project-id "" \ --deploy-mode code --runtime python_3_13 --entry-point app.py`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -2982,12 +2981,41 @@ func extractConnectionConfigs( // validateCodeDeployFlags checks that required flags are present when using // --deploy-mode code in --no-prompt mode. func (a *InitAction) validateCodeDeployFlags() error { - return validateCodeDeployInput(a.flags.noPrompt, a.flags.deployMode, a.flags.runtime, a.flags.entryPoint) + return validateCodeDeployInput( + a.flags.noPrompt, a.flags.deployMode, a.flags.runtime, a.flags.entryPoint, a.flags.depResolution) } // validateCodeDeployInput is the shared validation logic for code deploy flags. // Used by both InitAction and InitFromCodeAction. -func validateCodeDeployInput(noPrompt bool, deployMode, runtime, entryPoint string) error { +func validateCodeDeployInput(noPrompt bool, deployMode, runtime, entryPoint, depResolution string) error { + if deployMode != "" && deployMode != "container" && deployMode != "code" { + return exterrors.Validation( + exterrors.CodeInvalidParameter, + "--deploy-mode must be 'container' or 'code'", + "Specify --deploy-mode container or --deploy-mode code", + ) + } + if runtime != "" { + validRuntimes := map[string]bool{ + "python_3_13": true, + "python_3_14": true, + "dotnet_10": true, + } + if !validRuntimes[runtime] { + return exterrors.Validation( + exterrors.CodeInvalidParameter, + "--runtime must be one of: python_3_13, python_3_14, dotnet_10", + "Specify a valid runtime value", + ) + } + } + if depResolution != "" && depResolution != "remote_build" && depResolution != "bundled" { + return exterrors.Validation( + exterrors.CodeInvalidParameter, + "--dep-resolution must be 'remote_build' or 'bundled'", + "Specify --dep-resolution remote_build or --dep-resolution bundled", + ) + } if noPrompt && deployMode == "code" { if runtime == "" { return exterrors.Validation( diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go index 74de1d8fc1c..b02e24127c7 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go @@ -75,7 +75,9 @@ func (a *InitFromCodeAction) Run(ctx context.Context) error { } // Validate code deploy flags - if err := validateCodeDeployInput(a.flags.noPrompt, a.flags.deployMode, a.flags.runtime, a.flags.entryPoint); err != nil { + if err := validateCodeDeployInput( + a.flags.noPrompt, a.flags.deployMode, a.flags.runtime, a.flags.entryPoint, a.flags.depResolution, + ); err != nil { return err } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go index e133e3dc2e2..e75b39434fc 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go @@ -2319,6 +2319,24 @@ func TestCodeDeployFlagValidation(t *testing.T) { flags: initFlags{noPrompt: false, deployMode: "code"}, wantErr: false, }, + { + name: "invalid deploy-mode value fails", + flags: initFlags{noPrompt: true, deployMode: "invalid"}, + wantErr: true, + wantErrContain: "--deploy-mode must be", + }, + { + name: "invalid runtime value fails", + flags: initFlags{noPrompt: true, deployMode: "code", runtime: "node_20", entryPoint: "app.js"}, + wantErr: true, + wantErrContain: "--runtime must be one of", + }, + { + name: "invalid dep-resolution value fails", + flags: initFlags{noPrompt: true, deployMode: "code", runtime: "python_3_13", entryPoint: "app.py", depResolution: "invalid"}, + wantErr: true, + wantErrContain: "--dep-resolution must be", + }, } for _, tt := range tests {