Skip to content

Commit d6cf42b

Browse files
v1212Jian Wu
andauthored
feat: support code deploy in --no-prompt mode via CLI flags (#8324)
* 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 <file> (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. * 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. * refactor: extract shared validateCodeDeployInput to eliminate duplication * docs: add non-interactive code deploy example to init --help * fix: validate --runtime, --deploy-mode, and --dep-resolution flag values --------- Co-authored-by: Jian Wu <wujia@microsoft.com>
1 parent 508fd69 commit d6cf42b

4 files changed

Lines changed: 388 additions & 17 deletions

File tree

cli/azd/extensions/azure.ai.agents/internal/cmd/init.go

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ type initFlags struct {
5353
src string
5454
env string
5555
protocols []string
56+
// deploy mode flags for non-interactive code deploy support
57+
deployMode string // "container" or "code"; empty = prompt interactively
58+
runtime string // e.g. "python_3_13", "python_3_14", "dotnet_10"
59+
entryPoint string // e.g. "app.py", "MyAgent.dll"
60+
depResolution string // "remote_build" or "bundled"; defaults to "remote_build"
5661
// force, when true, lets headless callers (--no-prompt) pre-consent to
5762
// overwrite prompts that would otherwise return a structured error. It
5863
// mirrors the `--force` convention used by `azd down`, `azd env remove`,
@@ -613,7 +618,11 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`,
613618
azd ai agent init -m ./agent.manifest.yaml --agent-name my-unique-agent
614619
615620
# Initialize from local agent code
616-
azd ai agent init --src ./src/my-agent --agent-name my-unique-agent`,
621+
azd ai agent init --src ./src/my-agent --agent-name my-unique-agent
622+
623+
# Non-interactive code deploy (CI/CD)
624+
azd ai agent init --no-prompt --project-id "<resource-id>" \
625+
--deploy-mode code --runtime python_3_13 --entry-point app.py`,
617626
Args: cobra.MaximumNArgs(1),
618627
RunE: func(cmd *cobra.Command, args []string) error {
619628
flags.noPrompt = extCtx.NoPrompt
@@ -852,6 +861,18 @@ from code-deploy ZIP packaging (uses .gitignore syntax).`,
852861
cmd.Flags().StringSliceVar(&flags.protocols, "protocol", nil,
853862
"Protocols supported by the agent (e.g., 'responses', 'invocations'). Can be specified multiple times.")
854863

864+
cmd.Flags().StringVar(&flags.deployMode, "deploy-mode", "",
865+
"Deployment mode: 'container' (Docker image) or 'code' (ZIP upload). Defaults to 'container' in --no-prompt.")
866+
867+
cmd.Flags().StringVar(&flags.runtime, "runtime", "",
868+
"Runtime for code deploy (e.g., 'python_3_13', 'python_3_14', 'dotnet_10'). Required with --deploy-mode code --no-prompt.")
869+
870+
cmd.Flags().StringVar(&flags.entryPoint, "entry-point", "",
871+
"Entry point file for code deploy (e.g., 'app.py', 'MyAgent.dll'). Required with --deploy-mode code --no-prompt.")
872+
873+
cmd.Flags().StringVar(&flags.depResolution, "dep-resolution", "",
874+
"Dependency resolution for code deploy: 'remote_build' or 'bundled'. Defaults to 'remote_build'.")
875+
855876
cmd.Flags().BoolVar(&flags.force, "force", false,
856877
"Overwrite an input manifest that already lives inside the generated src tree without prompting. "+
857878
"Required together with --no-prompt when init would otherwise need confirmation.")
@@ -875,6 +896,11 @@ func (a *InitAction) Run(ctx context.Context) error {
875896
a.flags.src = relPath
876897
}
877898

899+
// Validate code deploy flags
900+
if err := a.validateCodeDeployFlags(); err != nil {
901+
return err
902+
}
903+
878904
// If --manifest is given
879905
if a.flags.manifestPointer != "" {
880906
// Validate that the manifest pointer is either a valid URL or existing file path
@@ -910,15 +936,19 @@ func (a *InitAction) Run(ctx context.Context) error {
910936
// Code deploy is supported for Python and .NET projects.
911937
if _, ok := agentManifest.Template.(agent_yaml.ContainerAgent); ok {
912938
showCodeDeploy := isPythonProject(targetDir) || isDotnetProject(targetDir)
913-
deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, showCodeDeploy)
939+
deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, showCodeDeploy, a.flags.deployMode)
914940
if err != nil {
915941
return fmt.Errorf("prompting for deploy mode: %w", err)
916942
}
917943
a.isCodeDeploy = (deployMode == "code")
918944

919945
if a.isCodeDeploy {
920946
// Prompt for code configuration and update the manifest
921-
codeConfig, err := promptCodeConfig(ctx, a.azdClient, targetDir, a.flags.noPrompt)
947+
codeConfig, err := promptCodeConfig(ctx, a.azdClient, targetDir, a.flags.noPrompt, codeDeployOptions{
948+
runtime: a.flags.runtime,
949+
entryPoint: a.flags.entryPoint,
950+
depResolution: a.flags.depResolution,
951+
})
922952
if err != nil {
923953
return fmt.Errorf("prompting for code configuration: %w", err)
924954
}
@@ -2947,3 +2977,60 @@ func extractConnectionConfigs(
29472977

29482978
return connections, credentialEnvVars, nil
29492979
}
2980+
2981+
// validateCodeDeployFlags checks that required flags are present when using
2982+
// --deploy-mode code in --no-prompt mode.
2983+
func (a *InitAction) validateCodeDeployFlags() error {
2984+
return validateCodeDeployInput(
2985+
a.flags.noPrompt, a.flags.deployMode, a.flags.runtime, a.flags.entryPoint, a.flags.depResolution)
2986+
}
2987+
2988+
// validateCodeDeployInput is the shared validation logic for code deploy flags.
2989+
// Used by both InitAction and InitFromCodeAction.
2990+
func validateCodeDeployInput(noPrompt bool, deployMode, runtime, entryPoint, depResolution string) error {
2991+
if deployMode != "" && deployMode != "container" && deployMode != "code" {
2992+
return exterrors.Validation(
2993+
exterrors.CodeInvalidParameter,
2994+
"--deploy-mode must be 'container' or 'code'",
2995+
"Specify --deploy-mode container or --deploy-mode code",
2996+
)
2997+
}
2998+
if runtime != "" {
2999+
validRuntimes := map[string]bool{
3000+
"python_3_13": true,
3001+
"python_3_14": true,
3002+
"dotnet_10": true,
3003+
}
3004+
if !validRuntimes[runtime] {
3005+
return exterrors.Validation(
3006+
exterrors.CodeInvalidParameter,
3007+
"--runtime must be one of: python_3_13, python_3_14, dotnet_10",
3008+
"Specify a valid runtime value",
3009+
)
3010+
}
3011+
}
3012+
if depResolution != "" && depResolution != "remote_build" && depResolution != "bundled" {
3013+
return exterrors.Validation(
3014+
exterrors.CodeInvalidParameter,
3015+
"--dep-resolution must be 'remote_build' or 'bundled'",
3016+
"Specify --dep-resolution remote_build or --dep-resolution bundled",
3017+
)
3018+
}
3019+
if noPrompt && deployMode == "code" {
3020+
if runtime == "" {
3021+
return exterrors.Validation(
3022+
exterrors.CodeInvalidParameter,
3023+
"--runtime is required when using --deploy-mode code with --no-prompt",
3024+
"Specify --runtime (e.g., python_3_13, python_3_14, dotnet_10)",
3025+
)
3026+
}
3027+
if entryPoint == "" {
3028+
return exterrors.Validation(
3029+
exterrors.CodeInvalidParameter,
3030+
"--entry-point is required when using --deploy-mode code with --no-prompt",
3031+
"Specify --entry-point (e.g., app.py, main.py, MyAgent.dll)",
3032+
)
3033+
}
3034+
}
3035+
return nil
3036+
}

cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ func (a *InitFromCodeAction) Run(ctx context.Context) error {
7474
a.flags.src = relPath
7575
}
7676

77+
// Validate code deploy flags
78+
if err := validateCodeDeployInput(
79+
a.flags.noPrompt, a.flags.deployMode, a.flags.runtime, a.flags.entryPoint, a.flags.depResolution,
80+
); err != nil {
81+
return err
82+
}
83+
7784
// Default src to current directory when not specified
7885
srcDir := a.flags.src
7986
if srcDir == "" {
@@ -487,7 +494,7 @@ func (a *InitFromCodeAction) createDefinitionFromLocalAgent(ctx context.Context)
487494
srcDir, _ = os.Getwd()
488495
}
489496
showCodeDeploy := isPythonProject(srcDir) || isDotnetProject(srcDir)
490-
deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, showCodeDeploy)
497+
deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, showCodeDeploy, a.flags.deployMode)
491498
if err != nil {
492499
return nil, err
493500
}
@@ -1008,7 +1015,11 @@ func (a *InitFromCodeAction) addToProject(ctx context.Context, targetDir string,
10081015

10091016
// promptCodeConfiguration prompts the user for code deploy configuration settings.
10101017
func (a *InitFromCodeAction) promptCodeConfiguration(ctx context.Context, srcDir string) (*agent_yaml.CodeConfiguration, error) {
1011-
return promptCodeConfig(ctx, a.azdClient, srcDir, a.flags.noPrompt)
1018+
return promptCodeConfig(ctx, a.azdClient, srcDir, a.flags.noPrompt, codeDeployOptions{
1019+
runtime: a.flags.runtime,
1020+
entryPoint: a.flags.entryPoint,
1021+
depResolution: a.flags.depResolution,
1022+
})
10121023
}
10131024

10141025
// protocolInfo pairs a protocol name with the default version used when generating agent.yaml.
@@ -1135,22 +1146,37 @@ func knownProtocolNames() string {
11351146
}
11361147

11371148
// promptDeployMode asks the user to choose between code deploy and container deploy.
1138-
// When noPrompt is true, defaults to "container" for backward compatibility.
1139-
// When showCodeDeploy is false, code deploy is not offered (e.g. for non-Python languages).
1140-
func promptDeployMode(ctx context.Context, azdClient *azdext.AzdClient, noPrompt bool, showCodeDeploy bool) (string, error) {
1149+
// When deployModeFlag is set, it is used directly (for --no-prompt with explicit flag).
1150+
// When noPrompt is true and no flag is provided, defaults to "container" for backward compatibility.
1151+
// When showCodeDeploy is false and no explicit flag overrides, code deploy is not offered.
1152+
func promptDeployMode(ctx context.Context, azdClient *azdext.AzdClient, noPrompt bool, showCodeDeploy bool, deployModeFlag string) (string, error) {
1153+
// Explicit flag takes precedence
1154+
if deployModeFlag != "" {
1155+
switch deployModeFlag {
1156+
case "container", "code":
1157+
return deployModeFlag, nil
1158+
default:
1159+
return "", exterrors.Validation(
1160+
exterrors.CodeInvalidParameter,
1161+
fmt.Sprintf("invalid --deploy-mode value %q; must be 'container' or 'code'", deployModeFlag),
1162+
"Use --deploy-mode container or --deploy-mode code",
1163+
)
1164+
}
1165+
}
1166+
11411167
if !showCodeDeploy {
11421168
return "container", nil
11431169
}
11441170

1171+
if noPrompt {
1172+
return "container", nil
1173+
}
1174+
11451175
deployModeChoices := []*azdext.SelectChoice{
11461176
{Label: "Container Image (Docker)", Value: "container"},
11471177
{Label: "Source Code (ZIP upload)", Value: "code"},
11481178
}
11491179

1150-
if noPrompt {
1151-
return "container", nil
1152-
}
1153-
11541180
defaultIdx := int32(0) // Container is the default for backward compatibility
11551181
deployModeResp, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{
11561182
Options: &azdext.SelectOptions{
@@ -1221,9 +1247,16 @@ func extractAssemblyName(csprojContent string) string {
12211247
return name
12221248
}
12231249

1250+
// codeDeployOptions holds optional flag overrides for code deploy configuration.
1251+
type codeDeployOptions struct {
1252+
runtime string
1253+
entryPoint string
1254+
depResolution string
1255+
}
1256+
12241257
// promptCodeConfig prompts for code deploy configuration (runtime, entry point,
1225-
// dependency resolution). When noPrompt is true, defaults are used without prompting.
1226-
func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir string, noPrompt bool) (*agent_yaml.CodeConfiguration, error) {
1258+
// dependency resolution). When noPrompt is true, flags or defaults are used without prompting.
1259+
func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir string, noPrompt bool, opts codeDeployOptions) (*agent_yaml.CodeConfiguration, error) {
12271260
if srcDir == "" {
12281261
srcDir = "."
12291262
}
@@ -1252,7 +1285,9 @@ func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir s
12521285
}
12531286

12541287
var runtime string
1255-
if noPrompt {
1288+
if opts.runtime != "" {
1289+
runtime = opts.runtime
1290+
} else if noPrompt {
12561291
if isDotnet && !isPython {
12571292
runtime = "dotnet_10"
12581293
} else {
@@ -1280,7 +1315,9 @@ func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir s
12801315
defaultEntryPoint := detectDefaultEntryPoint(srcDir, runtime)
12811316

12821317
var entryPoint string
1283-
if noPrompt {
1318+
if opts.entryPoint != "" {
1319+
entryPoint = opts.entryPoint
1320+
} else if noPrompt {
12841321
entryPoint = defaultEntryPoint
12851322
} else {
12861323
entryPointResp, err := azdClient.Prompt().Prompt(ctx, &azdext.PromptRequest{
@@ -1305,7 +1342,9 @@ func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir s
13051342
}
13061343

13071344
var depResolution string
1308-
if noPrompt {
1345+
if opts.depResolution != "" {
1346+
depResolution = opts.depResolution
1347+
} else if noPrompt {
13091348
depResolution = "remote_build"
13101349
} else {
13111350
depDefaultIdx := int32(0)

0 commit comments

Comments
 (0)