Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 1 addition & 25 deletions cli/azd/cmd/auto_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/runcontext/agentdetect"
"github.com/azure/azure-dev/cli/azd/internal/tracing/resource"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/extensions"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
Expand Down Expand Up @@ -391,7 +390,7 @@ func ExecuteWithAutoInstall(ctx context.Context, rootContainer *ioc.NestedContai
// This also enables the global options to be set in the container for support during extension framework callbacks.
globalOpts := &internal.GlobalCommandOptions{}
if err := ParseGlobalFlags(os.Args[1:], globalOpts); err != nil {
return fmt.Errorf("failed to parse global flags: %w", err)
return fmt.Errorf("Warning: failed to parse global flags: %w", err)
}

// Register GlobalCommandOptions as a singleton in the container BEFORE building the command tree.
Expand Down Expand Up @@ -640,18 +639,6 @@ func CreateGlobalFlagSet() *pflag.FlagSet {
func ParseGlobalFlags(args []string, opts *internal.GlobalCommandOptions) error {
globalFlagSet := CreateGlobalFlagSet()

// Add the environment flag for early parsing. This is not in CreateGlobalFlagSet because
// it shouldn't be added to cobra's persistent flags (it's already registered per-command
// via EnvFlag.Bind). But we parse it here so GlobalCommandOptions.EnvironmentName is
// available for extension commands where DisableFlagParsing prevents cobra from parsing it.
// Guard against duplicate registration — pflag.StringP panics if the name already exists.
if globalFlagSet.Lookup(internal.EnvironmentNameFlagName) == nil {
globalFlagSet.StringP(
internal.EnvironmentNameFlagName, "e",
os.Getenv(environment.EnvNameEnvVarName), "",
)
}

// Set output to io.Discard to suppress any error messages from pflag
// Cobra will handle all user-facing output
globalFlagSet.SetOutput(io.Discard)
Expand Down Expand Up @@ -682,17 +669,6 @@ func ParseGlobalFlags(args []string, opts *internal.GlobalCommandOptions) error
opts.NoPrompt = boolVal
}

if strVal, err := globalFlagSet.GetString(internal.EnvironmentNameFlagName); err == nil {
if strVal != "" && !environment.IsValidEnvironmentName(strVal) {
return fmt.Errorf(
"invalid environment name '%s': must match %s",
strVal,
environment.EnvironmentNameRegexp.String(),
)
}
opts.EnvironmentName = strVal
}

// Agent Detection: If --no-prompt was not explicitly set and we detect an AI coding agent
// as the caller, automatically enable no-prompt mode for non-interactive execution.
noPromptFlag := globalFlagSet.Lookup("no-prompt")
Expand Down
126 changes: 0 additions & 126 deletions cli/azd/cmd/auto_install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (

"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/runcontext/agentdetect"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/extensions"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -422,128 +421,3 @@ func TestParseGlobalFlags_AgentDetection(t *testing.T) {
})
}
}

func TestParseGlobalFlags_EnvironmentName(t *testing.T) {
tests := []struct {
name string
args []string
envVar string
expected string
}{
{
name: "short flag -e", args: []string{"-e", "dev", "app", "run"},
envVar: "", expected: "dev",
},
{
name: "long flag --environment",
args: []string{"--environment", "staging", "app", "run"},
envVar: "", expected: "staging",
},
{
name: "long flag with equals",
args: []string{"--environment=prod", "app", "run"},
envVar: "", expected: "prod",
},
{
name: "no env flag", args: []string{"--debug", "app", "run"},
envVar: "", expected: "",
},
{
name: "env flag among other flags",
args: []string{"--debug", "-e", "dev", "--no-prompt"},
envVar: "", expected: "dev",
},
{
name: "env flag with unknown extension flags",
args: []string{"-e", "dev", "--foo", "bar"},
envVar: "", expected: "dev",
},
{
name: "AZURE_ENV_NAME fallback",
args: []string{"--debug", "app", "run"},
envVar: "from-env", expected: "from-env",
},
{
name: "-e flag overrides AZURE_ENV_NAME",
args: []string{"-e", "from-flag", "app", "run"},
envVar: "from-env",
expected: "from-flag",
},
{
name: "empty AZURE_ENV_NAME no effect",
args: []string{"app", "run"}, envVar: "", expected: "",
},
{
name: "concatenated short flag -edev",
args: []string{"-edev", "app", "run"},
envVar: "", expected: "dev",
},
{
name: "multiple -e flags last wins",
args: []string{"-e", "first", "-e", "second"},
envVar: "", expected: "second",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(environment.EnvNameEnvVarName, tt.envVar)

opts := &internal.GlobalCommandOptions{}
err := ParseGlobalFlags(tt.args, opts)
require.NoError(t, err)
assert.Equal(t, tt.expected, opts.EnvironmentName)
})
}

// Cross-field assertion: verify -e does not interfere with adjacent flags
t.Run("env flag does not interfere with other flags", func(t *testing.T) {
t.Setenv(environment.EnvNameEnvVarName, "")

opts := &internal.GlobalCommandOptions{}
err := ParseGlobalFlags(
[]string{"--debug", "-e", "dev", "--no-prompt"},
opts,
)
require.NoError(t, err)
assert.Equal(t, "dev", opts.EnvironmentName)
assert.True(t, opts.EnableDebugLogging,
"--debug should be true alongside -e")
assert.True(t, opts.NoPrompt,
"--no-prompt should be true alongside -e")
})

// Edge case: -e at end of args with no value — pflag returns an error
t.Run("-e without value returns error", func(t *testing.T) {
t.Setenv(environment.EnvNameEnvVarName, "")

opts := &internal.GlobalCommandOptions{}
err := ParseGlobalFlags([]string{"app", "-e"}, opts)
require.Error(t, err)
})
}

func TestParseGlobalFlags_InvalidEnvironmentName(t *testing.T) {
tests := []struct {
name string
args []string
}{
{
name: "invalid characters",
args: []string{"-e", "env name with spaces"},
},
{
name: "special characters",
args: []string{"-e", "env@#$%"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
opts := &internal.GlobalCommandOptions{}
err := ParseGlobalFlags(tt.args, opts)
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid environment name")
})
}
}
19 changes: 1 addition & 18 deletions cli/azd/cmd/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,7 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
return writer
})

container.MustRegisterScoped(func(
ctx context.Context,
cmd *cobra.Command,
globalOptions *internal.GlobalCommandOptions,
) internal.EnvFlag {
container.MustRegisterScoped(func(ctx context.Context, cmd *cobra.Command) internal.EnvFlag {
// The env flag `-e, --environment` is available on most azd commands but not all
// This is typically used to override the default environment and is used for bootstrapping other components
// such as the azd environment.
Expand All @@ -209,24 +205,11 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
// If no explicit environment flag was set, but one was provided
// in the context, use that instead.
// This is used in workflow execution (in `up`) to influence the environment used.
// Context takes precedence over globalOptions because azd up uses
// context.WithValue to propagate the environment to sub-commands.
if envFlag, ok := ctx.Value(envFlagCtxKey).(internal.EnvFlag); ok {
return envFlag
}
}

// Fall back to the pre-parsed global options value.
// This handles extension commands (DisableFlagParsing: true) where cobra
// doesn't parse persistent flags — the value was already parsed in ParseGlobalFlags.
if envValue == "" && globalOptions.EnvironmentName != "" {
log.Printf(
"using pre-parsed environment name '%s' from global options",
globalOptions.EnvironmentName,
)
envValue = globalOptions.EnvironmentName
}

return internal.EnvFlag{EnvironmentName: envValue}
})

Expand Down
14 changes: 8 additions & 6 deletions cli/azd/cmd/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,20 +244,22 @@ func (a *extensionAction) Run(ctx context.Context) (*actions.ActionResult, error
allEnv = append(allEnv, traceEnv...)
}

// Use pre-parsed global options for flag propagation.
// For extension commands (DisableFlagParsing: true), cobra doesn't parse persistent flags,
// but ParseGlobalFlags already parsed them into globalOptions before command execution.
// Read global flags for propagation via InvokeOptions
debugEnabled, _ := a.cmd.Flags().GetBool("debug")
cwd, _ := a.cmd.Flags().GetString("cwd")
envName, _ := a.cmd.Flags().GetString("environment")

options := &extensions.InvokeOptions{
Args: a.args,
Env: allEnv,
// cmd extensions are always interactive (connected to terminal)
Interactive: true,
Debug: a.globalOptions.EnableDebugLogging,
Debug: debugEnabled,
// Use globalOptions.NoPrompt which includes agent detection,
// not just the --no-prompt CLI flag
NoPrompt: a.globalOptions.NoPrompt,
Cwd: a.globalOptions.Cwd,
Environment: a.globalOptions.EnvironmentName,
Cwd: cwd,
Environment: envName,
}

_, invokeErr := a.extensionRunner.Invoke(ctx, extension, options)
Expand Down
5 changes: 0 additions & 5 deletions cli/azd/internal/global_command_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@ type GlobalCommandOptions struct {
// if there is no default value the prompt returns an error.
NoPrompt bool

// EnvironmentName is the name of the environment to use, set via `-e` or `--environment`.
// This is parsed early from raw args by ParseGlobalFlags so it is available even for
// commands with DisableFlagParsing: true (e.g. extension commands).
EnvironmentName string

// EnableTelemetry indicates if telemetry should be sent.
// The rootCmd will disable this based if the environment variable
// AZURE_DEV_COLLECT_TELEMETRY is set to 'no'.
Expand Down
Loading
Loading