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
47 changes: 0 additions & 47 deletions .github-pr-body.md

This file was deleted.

1 change: 1 addition & 0 deletions cli/azd/cmd/figspec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func TestFigSpec(t *testing.T) {
t.Setenv("AZD_CONFIG_DIR", configDir)
t.Setenv("AZURE_DEV_COLLECT_TELEMETRY", "no")
t.Setenv("AZD_SKIP_FIRST_RUN", "true")
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")

cli := azdcli.NewCLI(t)

Expand Down
9 changes: 9 additions & 0 deletions cli/azd/cmd/middleware/tool_first_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/tracing/resource"
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"github.com/azure/azure-dev/cli/azd/pkg/config"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
Expand All @@ -35,6 +36,7 @@ const envKeySkipFirstRun = "AZD_SKIP_FIRST_RUN"
// installed Azure development tools and optionally offers to
// install any missing recommended tools.
type ToolFirstRunMiddleware struct {
alphaManager *alpha.FeatureManager
configManager config.UserConfigManager
console input.Console
manager *tool.Manager
Expand All @@ -43,12 +45,14 @@ type ToolFirstRunMiddleware struct {

// NewToolFirstRunMiddleware creates a new [ToolFirstRunMiddleware].
func NewToolFirstRunMiddleware(
alphaManager *alpha.FeatureManager,
configManager config.UserConfigManager,
console input.Console,
manager *tool.Manager,
options *internal.GlobalCommandOptions,
) Middleware {
return &ToolFirstRunMiddleware{
alphaManager: alphaManager,
configManager: configManager,
console: console,
manager: manager,
Expand All @@ -61,6 +65,11 @@ func NewToolFirstRunMiddleware(
// always delegates to nextFn so the user's intended command is
// never blocked.
func (m *ToolFirstRunMiddleware) Run(ctx context.Context, nextFn NextFn) (*actions.ActionResult, error) {
// Skip when the tool alpha feature is not enabled.
if !m.alphaManager.IsEnabled(tool.FeatureAlphaTool) {
return nextFn(ctx)
}
Comment thread
wbreza marked this conversation as resolved.

// Skip for child actions (e.g. workflow steps).
if IsChildAction(ctx) {
return nextFn(ctx)
Expand Down
37 changes: 37 additions & 0 deletions cli/azd/cmd/middleware/tool_first_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"github.com/azure/azure-dev/cli/azd/pkg/config"
"github.com/azure/azure-dev/cli/azd/test/mocks/mockinput"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -58,6 +59,7 @@ func passthroughNext(called *bool) NextFn {
func TestToolFirstRunMiddleware_SkipConditions(t *testing.T) {
// These tests modify env vars via t.Setenv so they cannot be
// parallel with each other or with tests that read the same vars.
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")

tests := []struct {
name string
Expand Down Expand Up @@ -141,6 +143,7 @@ func TestToolFirstRunMiddleware_SkipConditions(t *testing.T) {
ucm := &mockUserConfigManager{cfg: cfg}

m := &ToolFirstRunMiddleware{
alphaManager: alpha.NewFeaturesManagerWithConfig(config.NewEmptyConfig()),
configManager: ucm,
console: console,
manager: nil, // not needed for skip paths
Expand Down Expand Up @@ -177,13 +180,15 @@ func TestToolFirstRunMiddleware_TriggersWhenNoSkip(t *testing.T) {
clearCIVars(t)
t.Setenv(envKeySkipFirstRun, "")
os.Unsetenv(envKeySkipFirstRun)
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")

console := mockinput.NewMockConsole()
cfg := config.NewEmptyConfig()
opts := &internal.GlobalCommandOptions{}
ucm := &mockUserConfigManager{cfg: cfg}

m := &ToolFirstRunMiddleware{
alphaManager: alpha.NewFeaturesManagerWithConfig(config.NewEmptyConfig()),
configManager: ucm,
console: console,
manager: nil, // runFirstRunExperience will fail at prompt
Expand All @@ -205,3 +210,35 @@ func TestToolFirstRunMiddleware_TriggersWhenNoSkip(t *testing.T) {
assert.NotEmpty(t, console.Output(),
"triggered first-run experience should produce console output")
}

// TestToolFirstRunMiddleware_SkipsWhenAlphaDisabled verifies that the
// middleware is a no-op when the tool alpha feature is not enabled.
func TestToolFirstRunMiddleware_SkipsWhenAlphaDisabled(t *testing.T) {
clearCIVars(t)
t.Setenv(envKeySkipFirstRun, "")
os.Unsetenv(envKeySkipFirstRun)
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "false")

console := mockinput.NewMockConsole()
cfg := config.NewEmptyConfig()
opts := &internal.GlobalCommandOptions{}
ucm := &mockUserConfigManager{cfg: cfg}

m := &ToolFirstRunMiddleware{
alphaManager: alpha.NewFeaturesManagerWithConfig(config.NewEmptyConfig()),
configManager: ucm,
console: console,
manager: nil,
options: opts,
}

nextCalled := false
result, err := m.Run(t.Context(), passthroughNext(&nextCalled))

require.NoError(t, err)
require.NotNil(t, result)
assert.True(t, nextCalled,
"nextFn must always be called even when alpha is disabled")
assert.Empty(t, console.Output(),
"alpha-disabled should skip the first-run experience entirely")
}
9 changes: 9 additions & 0 deletions cli/azd/cmd/middleware/tool_update_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/tracing/resource"
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/tool"
Expand All @@ -25,6 +26,7 @@ import (
// interactive and the current command is not a tool-management
// subcommand.
type ToolUpdateCheckMiddleware struct {
alphaManager *alpha.FeatureManager
manager *tool.Manager
console input.Console
options *Options
Expand All @@ -34,12 +36,14 @@ type ToolUpdateCheckMiddleware struct {
// NewToolUpdateCheckMiddleware creates a new [ToolUpdateCheckMiddleware].
// All dependencies are resolved by the IoC container.
func NewToolUpdateCheckMiddleware(
alphaManager *alpha.FeatureManager,
manager *tool.Manager,
console input.Console,
options *Options,
globalOptions *internal.GlobalCommandOptions,
) Middleware {
return &ToolUpdateCheckMiddleware{
alphaManager: alphaManager,
manager: manager,
console: console,
options: options,
Expand All @@ -52,6 +56,11 @@ func NewToolUpdateCheckMiddleware(
// completes it triggers a background update check when the configured
// check interval has elapsed.
func (m *ToolUpdateCheckMiddleware) Run(ctx context.Context, nextFn NextFn) (*actions.ActionResult, error) {
// Skip when the tool alpha feature is not enabled.
if !m.alphaManager.IsEnabled(tool.FeatureAlphaTool) {
return nextFn(ctx)
}
Comment thread
wbreza marked this conversation as resolved.

// Skip all notification and background-check logic for child
// actions (e.g. workflow steps invoked by a parent command).
if !IsChildAction(ctx) {
Expand Down
35 changes: 35 additions & 0 deletions cli/azd/cmd/middleware/tool_update_check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"github.com/azure/azure-dev/cli/azd/pkg/config"
"github.com/azure/azure-dev/cli/azd/pkg/tool"
"github.com/azure/azure-dev/cli/azd/test/mocks/mockinput"
Expand Down Expand Up @@ -39,6 +40,7 @@ func newUpdateCheckMiddleware(
}

return &ToolUpdateCheckMiddleware{
alphaManager: alpha.NewFeaturesManagerWithConfig(config.NewEmptyConfig()),
manager: manager,
console: console,
options: opts,
Expand All @@ -51,6 +53,8 @@ func newUpdateCheckMiddleware(
// ---------------------------------------------------------------------------

func TestToolUpdateCheckMiddleware_SkipNotification(t *testing.T) {
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")

tests := []struct {
name string
setup func(
Expand Down Expand Up @@ -147,6 +151,7 @@ func TestToolUpdateCheckMiddleware_SkipNotification(t *testing.T) {

func TestToolUpdateCheckMiddleware_ChildAction(t *testing.T) {
clearCIVars(t)
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")

console := mockinput.NewMockConsole()
m := newUpdateCheckMiddleware(
Expand Down Expand Up @@ -175,6 +180,7 @@ func TestToolUpdateCheckMiddleware_NotificationGating(t *testing.T) {
clearCIVars(t)
t.Setenv(envKeySkipFirstRun, "")
os.Unsetenv(envKeySkipFirstRun)
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")

console := mockinput.NewMockConsole()
cfg := config.NewEmptyConfig()
Expand Down Expand Up @@ -229,6 +235,7 @@ func TestToolUpdateCheckMiddleware_BackgroundCheckSkippedWhenCI(
// early and never calls manager.ShouldCheckForUpdates.
clearCIVars(t)
t.Setenv("CI", "1")
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")

console := mockinput.NewMockConsole()

Expand Down Expand Up @@ -259,6 +266,7 @@ func TestToolUpdateCheckMiddleware_BackgroundCheckSkippedByEnvVar(
) {
clearCIVars(t)
t.Setenv(envKeySkipFirstRun, "true")
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")

console := mockinput.NewMockConsole()

Expand Down Expand Up @@ -312,6 +320,32 @@ func TestToolUpdateCheckMiddleware_IsToolCommand(t *testing.T) {
}
}

// TestToolUpdateCheckMiddleware_SkipsWhenAlphaDisabled verifies that
// the middleware is a no-op when the tool alpha feature is not enabled.
func TestToolUpdateCheckMiddleware_SkipsWhenAlphaDisabled(t *testing.T) {
clearCIVars(t)
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "false")
t.Setenv(envKeySkipFirstRun, "")
os.Unsetenv(envKeySkipFirstRun)

console := mockinput.NewMockConsole()
m := newUpdateCheckMiddleware(
nil, console,
&Options{CommandPath: "azd provision"},
&internal.GlobalCommandOptions{},
)

nextCalled := false
result, err := m.Run(t.Context(), passthroughNext(&nextCalled))

require.NoError(t, err)
require.NotNil(t, result)
assert.True(t, nextCalled,
"nextFn must always be called even when alpha is disabled")
assert.Empty(t, console.Output(),
"alpha-disabled should skip update check entirely")
}

// ---------------------------------------------------------------------------
// TestToolUpdateCheckMiddleware_AlwaysCallsNext
// ---------------------------------------------------------------------------
Expand All @@ -320,6 +354,7 @@ func TestToolUpdateCheckMiddleware_AlwaysCallsNext(t *testing.T) {
clearCIVars(t)
// Skip background check so the nil manager is not accessed.
t.Setenv(envKeySkipFirstRun, "true")
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")

console := mockinput.NewMockConsole()
console.SetNoPromptMode(true) // triggers notification skip
Expand Down
25 changes: 24 additions & 1 deletion cli/azd/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ import (

// Importing for infrastructure provider plugin registrations

"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"github.com/azure/azure-dev/cli/azd/pkg/azd"
"github.com/azure/azure-dev/cli/azd/pkg/config"
"github.com/azure/azure-dev/cli/azd/pkg/extensions"
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
"github.com/azure/azure-dev/cli/azd/pkg/platform"
"github.com/azure/azure-dev/cli/azd/pkg/tool"

"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/cmd"
Expand Down Expand Up @@ -196,7 +199,13 @@ func newRootCmd(
hooksActions(root)
mcpActions(root)
copilotActions(root)
toolActions(root)

// Create a FeatureManager for command-tree gating.
// User config is loaded best-effort; env vars (AZD_ALPHA_ENABLE_TOOL) always work.
alphaManager := newAlphaManagerForCommandTree()
if alphaManager.IsEnabled(tool.FeatureAlphaTool) {
toolActions(root)
Comment thread
wbreza marked this conversation as resolved.
}
Comment thread
wbreza marked this conversation as resolved.

root.Add("version", &actions.ActionDescriptorOptions{
Command: &cobra.Command{
Expand Down Expand Up @@ -563,6 +572,20 @@ var workflowCommands = map[string]struct{}{
"restore": {},
}

// newAlphaManagerForCommandTree creates a FeatureManager for use during
// command-tree construction (before DI is available). It loads the
// user config best-effort so that `azd config set alpha.tool on` works;
// env-var overrides (AZD_ALPHA_ENABLE_TOOL) always work regardless.
func newAlphaManagerForCommandTree() *alpha.FeatureManager {
ucm := config.NewUserConfigManager(config.NewFileConfigManager(config.NewManager()))
cfg, err := ucm.Load()
if err != nil {
log.Printf("warning: failed to load user config for alpha feature gating: %v", err)
cfg = config.NewEmptyConfig()
}
return alpha.NewFeaturesManagerWithConfig(cfg)
Comment thread
wbreza marked this conversation as resolved.
}

// isWorkflowCommand reports whether the command is a primary workflow
// command where a first-run tool check adds value.
func isWorkflowCommand(descriptor *actions.ActionDescriptor) bool {
Expand Down
1 change: 1 addition & 0 deletions cli/azd/cmd/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
)

// toolActions registers the "azd tool" command group and all of its subcommands.
// The caller is responsible for gating on the "tool" alpha feature flag.
func toolActions(root *actions.ActionDescriptor) *actions.ActionDescriptor {
toolCmd := &cobra.Command{
Use: "tool",
Expand Down
Loading
Loading