diff --git a/cli/azd/cmd/container.go b/cli/azd/cmd/container.go index 5af2152826f..4d73091fe3d 100644 --- a/cli/azd/cmd/container.go +++ b/cli/azd/cmd/container.go @@ -1007,7 +1007,9 @@ func extractGlobalArgs() []string { var result []string globalFlagSet.VisitAll(func(f *pflag.Flag) { if f.Changed { - result = append(result, fmt.Sprintf("--%s", f.Name), f.Value.String()) + // Use --flag=value syntax to avoid ambiguity. The two-arg form (--flag value) + // doesn't work for boolean flags, where the value is treated as a positional arg. + result = append(result, fmt.Sprintf("--%s=%s", f.Name, f.Value.String())) } }) return result diff --git a/cli/azd/cmd/container_test.go b/cli/azd/cmd/container_test.go index 15d63ada4d4..9cfdb081eff 100644 --- a/cli/azd/cmd/container_test.go +++ b/cli/azd/cmd/container_test.go @@ -5,6 +5,7 @@ package cmd import ( "context" + "os" "testing" "github.com/azure/azure-dev/cli/azd/cmd/middleware" @@ -312,6 +313,56 @@ func Test_WorkflowCmdAdapter_ContextPropagation(t *testing.T) { "Each execution should use a distinct command tree instance") }) + t.Run("GlobalBoolFlagsRemainSingleTokenWhenMerged", func(t *testing.T) { + originalArgs := os.Args + os.Args = []string{"azd", "--debug", "up"} + t.Cleanup(func() { + os.Args = originalArgs + }) + + globalArgs := extractGlobalArgs() + require.Equal(t, []string{"--debug=true"}, globalArgs) + + var ( + capturedPositionalArgs []string + debugEnabled bool + ) + + newCommand := func() *cobra.Command { + rootCmd := &cobra.Command{Use: "root"} + rootCmd.PersistentFlags().AddFlagSet(CreateGlobalFlagSet()) + + packageCmd := &cobra.Command{ + Use: "package", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + capturedPositionalArgs = append([]string(nil), args...) + + var err error + debugEnabled, err = cmd.Flags().GetBool("debug") + require.NoError(t, err) + + return nil + }, + } + packageCmd.Flags().Bool("all", false, "") + rootCmd.AddCommand(packageCmd) + + return rootCmd + } + + adapter := &workflowCmdAdapter{ + newCommand: newCommand, + globalArgs: globalArgs, + } + + err := adapter.ExecuteContext(context.WithoutCancel(context.Background()), []string{"package", "--all"}) + require.NoError(t, err) + require.True(t, debugEnabled, "global --debug flag should still be parsed on the rebuilt tree") + require.Empty(t, capturedPositionalArgs, + "boolean global flag value should not leak into workflow step positional args") + }) + t.Run("NewRootCmdPreservesMiddlewareChain", func(t *testing.T) { // Verify that building a real command tree via NewRootCmd preserves // the full middleware chain (debug, ux, telemetry, error, loginGuard, etc.)