Skip to content

Commit dd75998

Browse files
wbrezaCopilot
andauthored
feat: gate azd tool command group behind alpha feature flag (#8111)
* Gate 'azd tool' command group and middleware behind alpha feature flag Register a new 'tool' alpha feature in alpha_features.yaml and gate: - Command registration in toolActions() (tool.go/root.go) - ToolFirstRunMiddleware (tool_first_run.go) - ToolUpdateCheckMiddleware (tool_update_check.go) Users enable via: azd config set alpha.tool on Or env var: AZD_ALPHA_ENABLE_TOOL=true Tests updated to set AZD_ALPHA_ENABLE_TOOL=true where needed, with new test cases verifying the feature is hidden when the flag is off. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address code review findings for alpha feature flag - Add debug logging when user config fails to load for alpha gating - Move alpha check to call site in root.go for clearer nil-safety - Verify YAML description formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 832a81c commit dd75998

12 files changed

Lines changed: 173 additions & 48 deletions

.github-pr-body.md

Lines changed: 0 additions & 47 deletions
This file was deleted.

cli/azd/cmd/figspec_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func TestFigSpec(t *testing.T) {
2828
t.Setenv("AZD_CONFIG_DIR", configDir)
2929
t.Setenv("AZURE_DEV_COLLECT_TELEMETRY", "no")
3030
t.Setenv("AZD_SKIP_FIRST_RUN", "true")
31+
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")
3132

3233
cli := azdcli.NewCLI(t)
3334

cli/azd/cmd/middleware/tool_first_run.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/azure/azure-dev/cli/azd/cmd/actions"
1616
"github.com/azure/azure-dev/cli/azd/internal"
1717
"github.com/azure/azure-dev/cli/azd/internal/tracing/resource"
18+
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
1819
"github.com/azure/azure-dev/cli/azd/pkg/config"
1920
"github.com/azure/azure-dev/cli/azd/pkg/input"
2021
"github.com/azure/azure-dev/cli/azd/pkg/output"
@@ -35,6 +36,7 @@ const envKeySkipFirstRun = "AZD_SKIP_FIRST_RUN"
3536
// installed Azure development tools and optionally offers to
3637
// install any missing recommended tools.
3738
type ToolFirstRunMiddleware struct {
39+
alphaManager *alpha.FeatureManager
3840
configManager config.UserConfigManager
3941
console input.Console
4042
manager *tool.Manager
@@ -43,12 +45,14 @@ type ToolFirstRunMiddleware struct {
4345

4446
// NewToolFirstRunMiddleware creates a new [ToolFirstRunMiddleware].
4547
func NewToolFirstRunMiddleware(
48+
alphaManager *alpha.FeatureManager,
4649
configManager config.UserConfigManager,
4750
console input.Console,
4851
manager *tool.Manager,
4952
options *internal.GlobalCommandOptions,
5053
) Middleware {
5154
return &ToolFirstRunMiddleware{
55+
alphaManager: alphaManager,
5256
configManager: configManager,
5357
console: console,
5458
manager: manager,
@@ -61,6 +65,11 @@ func NewToolFirstRunMiddleware(
6165
// always delegates to nextFn so the user's intended command is
6266
// never blocked.
6367
func (m *ToolFirstRunMiddleware) Run(ctx context.Context, nextFn NextFn) (*actions.ActionResult, error) {
68+
// Skip when the tool alpha feature is not enabled.
69+
if !m.alphaManager.IsEnabled(tool.FeatureAlphaTool) {
70+
return nextFn(ctx)
71+
}
72+
6473
// Skip for child actions (e.g. workflow steps).
6574
if IsChildAction(ctx) {
6675
return nextFn(ctx)

cli/azd/cmd/middleware/tool_first_run_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

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

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

143145
m := &ToolFirstRunMiddleware{
146+
alphaManager: alpha.NewFeaturesManagerWithConfig(config.NewEmptyConfig()),
144147
configManager: ucm,
145148
console: console,
146149
manager: nil, // not needed for skip paths
@@ -177,13 +180,15 @@ func TestToolFirstRunMiddleware_TriggersWhenNoSkip(t *testing.T) {
177180
clearCIVars(t)
178181
t.Setenv(envKeySkipFirstRun, "")
179182
os.Unsetenv(envKeySkipFirstRun)
183+
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")
180184

181185
console := mockinput.NewMockConsole()
182186
cfg := config.NewEmptyConfig()
183187
opts := &internal.GlobalCommandOptions{}
184188
ucm := &mockUserConfigManager{cfg: cfg}
185189

186190
m := &ToolFirstRunMiddleware{
191+
alphaManager: alpha.NewFeaturesManagerWithConfig(config.NewEmptyConfig()),
187192
configManager: ucm,
188193
console: console,
189194
manager: nil, // runFirstRunExperience will fail at prompt
@@ -205,3 +210,35 @@ func TestToolFirstRunMiddleware_TriggersWhenNoSkip(t *testing.T) {
205210
assert.NotEmpty(t, console.Output(),
206211
"triggered first-run experience should produce console output")
207212
}
213+
214+
// TestToolFirstRunMiddleware_SkipsWhenAlphaDisabled verifies that the
215+
// middleware is a no-op when the tool alpha feature is not enabled.
216+
func TestToolFirstRunMiddleware_SkipsWhenAlphaDisabled(t *testing.T) {
217+
clearCIVars(t)
218+
t.Setenv(envKeySkipFirstRun, "")
219+
os.Unsetenv(envKeySkipFirstRun)
220+
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "false")
221+
222+
console := mockinput.NewMockConsole()
223+
cfg := config.NewEmptyConfig()
224+
opts := &internal.GlobalCommandOptions{}
225+
ucm := &mockUserConfigManager{cfg: cfg}
226+
227+
m := &ToolFirstRunMiddleware{
228+
alphaManager: alpha.NewFeaturesManagerWithConfig(config.NewEmptyConfig()),
229+
configManager: ucm,
230+
console: console,
231+
manager: nil,
232+
options: opts,
233+
}
234+
235+
nextCalled := false
236+
result, err := m.Run(t.Context(), passthroughNext(&nextCalled))
237+
238+
require.NoError(t, err)
239+
require.NotNil(t, result)
240+
assert.True(t, nextCalled,
241+
"nextFn must always be called even when alpha is disabled")
242+
assert.Empty(t, console.Output(),
243+
"alpha-disabled should skip the first-run experience entirely")
244+
}

cli/azd/cmd/middleware/tool_update_check.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/azure/azure-dev/cli/azd/cmd/actions"
1515
"github.com/azure/azure-dev/cli/azd/internal"
1616
"github.com/azure/azure-dev/cli/azd/internal/tracing/resource"
17+
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
1718
"github.com/azure/azure-dev/cli/azd/pkg/input"
1819
"github.com/azure/azure-dev/cli/azd/pkg/output"
1920
"github.com/azure/azure-dev/cli/azd/pkg/tool"
@@ -25,6 +26,7 @@ import (
2526
// interactive and the current command is not a tool-management
2627
// subcommand.
2728
type ToolUpdateCheckMiddleware struct {
29+
alphaManager *alpha.FeatureManager
2830
manager *tool.Manager
2931
console input.Console
3032
options *Options
@@ -34,12 +36,14 @@ type ToolUpdateCheckMiddleware struct {
3436
// NewToolUpdateCheckMiddleware creates a new [ToolUpdateCheckMiddleware].
3537
// All dependencies are resolved by the IoC container.
3638
func NewToolUpdateCheckMiddleware(
39+
alphaManager *alpha.FeatureManager,
3740
manager *tool.Manager,
3841
console input.Console,
3942
options *Options,
4043
globalOptions *internal.GlobalCommandOptions,
4144
) Middleware {
4245
return &ToolUpdateCheckMiddleware{
46+
alphaManager: alphaManager,
4347
manager: manager,
4448
console: console,
4549
options: options,
@@ -52,6 +56,11 @@ func NewToolUpdateCheckMiddleware(
5256
// completes it triggers a background update check when the configured
5357
// check interval has elapsed.
5458
func (m *ToolUpdateCheckMiddleware) Run(ctx context.Context, nextFn NextFn) (*actions.ActionResult, error) {
59+
// Skip when the tool alpha feature is not enabled.
60+
if !m.alphaManager.IsEnabled(tool.FeatureAlphaTool) {
61+
return nextFn(ctx)
62+
}
63+
5564
// Skip all notification and background-check logic for child
5665
// actions (e.g. workflow steps invoked by a parent command).
5766
if !IsChildAction(ctx) {

cli/azd/cmd/middleware/tool_update_check_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/azure/azure-dev/cli/azd/cmd/actions"
1313
"github.com/azure/azure-dev/cli/azd/internal"
14+
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
1415
"github.com/azure/azure-dev/cli/azd/pkg/config"
1516
"github.com/azure/azure-dev/cli/azd/pkg/tool"
1617
"github.com/azure/azure-dev/cli/azd/test/mocks/mockinput"
@@ -39,6 +40,7 @@ func newUpdateCheckMiddleware(
3940
}
4041

4142
return &ToolUpdateCheckMiddleware{
43+
alphaManager: alpha.NewFeaturesManagerWithConfig(config.NewEmptyConfig()),
4244
manager: manager,
4345
console: console,
4446
options: opts,
@@ -51,6 +53,8 @@ func newUpdateCheckMiddleware(
5153
// ---------------------------------------------------------------------------
5254

5355
func TestToolUpdateCheckMiddleware_SkipNotification(t *testing.T) {
56+
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")
57+
5458
tests := []struct {
5559
name string
5660
setup func(
@@ -147,6 +151,7 @@ func TestToolUpdateCheckMiddleware_SkipNotification(t *testing.T) {
147151

148152
func TestToolUpdateCheckMiddleware_ChildAction(t *testing.T) {
149153
clearCIVars(t)
154+
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")
150155

151156
console := mockinput.NewMockConsole()
152157
m := newUpdateCheckMiddleware(
@@ -175,6 +180,7 @@ func TestToolUpdateCheckMiddleware_NotificationGating(t *testing.T) {
175180
clearCIVars(t)
176181
t.Setenv(envKeySkipFirstRun, "")
177182
os.Unsetenv(envKeySkipFirstRun)
183+
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")
178184

179185
console := mockinput.NewMockConsole()
180186
cfg := config.NewEmptyConfig()
@@ -229,6 +235,7 @@ func TestToolUpdateCheckMiddleware_BackgroundCheckSkippedWhenCI(
229235
// early and never calls manager.ShouldCheckForUpdates.
230236
clearCIVars(t)
231237
t.Setenv("CI", "1")
238+
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")
232239

233240
console := mockinput.NewMockConsole()
234241

@@ -259,6 +266,7 @@ func TestToolUpdateCheckMiddleware_BackgroundCheckSkippedByEnvVar(
259266
) {
260267
clearCIVars(t)
261268
t.Setenv(envKeySkipFirstRun, "true")
269+
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")
262270

263271
console := mockinput.NewMockConsole()
264272

@@ -312,6 +320,32 @@ func TestToolUpdateCheckMiddleware_IsToolCommand(t *testing.T) {
312320
}
313321
}
314322

323+
// TestToolUpdateCheckMiddleware_SkipsWhenAlphaDisabled verifies that
324+
// the middleware is a no-op when the tool alpha feature is not enabled.
325+
func TestToolUpdateCheckMiddleware_SkipsWhenAlphaDisabled(t *testing.T) {
326+
clearCIVars(t)
327+
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "false")
328+
t.Setenv(envKeySkipFirstRun, "")
329+
os.Unsetenv(envKeySkipFirstRun)
330+
331+
console := mockinput.NewMockConsole()
332+
m := newUpdateCheckMiddleware(
333+
nil, console,
334+
&Options{CommandPath: "azd provision"},
335+
&internal.GlobalCommandOptions{},
336+
)
337+
338+
nextCalled := false
339+
result, err := m.Run(t.Context(), passthroughNext(&nextCalled))
340+
341+
require.NoError(t, err)
342+
require.NotNil(t, result)
343+
assert.True(t, nextCalled,
344+
"nextFn must always be called even when alpha is disabled")
345+
assert.Empty(t, console.Output(),
346+
"alpha-disabled should skip update check entirely")
347+
}
348+
315349
// ---------------------------------------------------------------------------
316350
// TestToolUpdateCheckMiddleware_AlwaysCallsNext
317351
// ---------------------------------------------------------------------------
@@ -320,6 +354,7 @@ func TestToolUpdateCheckMiddleware_AlwaysCallsNext(t *testing.T) {
320354
clearCIVars(t)
321355
// Skip background check so the nil manager is not accessed.
322356
t.Setenv(envKeySkipFirstRun, "true")
357+
t.Setenv("AZD_ALPHA_ENABLE_TOOL", "true")
323358

324359
console := mockinput.NewMockConsole()
325360
console.SetNoPromptMode(true) // triggers notification skip

cli/azd/cmd/root.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ import (
1717

1818
// Importing for infrastructure provider plugin registrations
1919

20+
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
2021
"github.com/azure/azure-dev/cli/azd/pkg/azd"
22+
"github.com/azure/azure-dev/cli/azd/pkg/config"
2123
"github.com/azure/azure-dev/cli/azd/pkg/extensions"
2224
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
2325
"github.com/azure/azure-dev/cli/azd/pkg/platform"
26+
"github.com/azure/azure-dev/cli/azd/pkg/tool"
2427

2528
"github.com/azure/azure-dev/cli/azd/internal"
2629
"github.com/azure/azure-dev/cli/azd/internal/cmd"
@@ -196,7 +199,13 @@ func newRootCmd(
196199
hooksActions(root)
197200
mcpActions(root)
198201
copilotActions(root)
199-
toolActions(root)
202+
203+
// Create a FeatureManager for command-tree gating.
204+
// User config is loaded best-effort; env vars (AZD_ALPHA_ENABLE_TOOL) always work.
205+
alphaManager := newAlphaManagerForCommandTree()
206+
if alphaManager.IsEnabled(tool.FeatureAlphaTool) {
207+
toolActions(root)
208+
}
200209

201210
root.Add("version", &actions.ActionDescriptorOptions{
202211
Command: &cobra.Command{
@@ -563,6 +572,20 @@ var workflowCommands = map[string]struct{}{
563572
"restore": {},
564573
}
565574

575+
// newAlphaManagerForCommandTree creates a FeatureManager for use during
576+
// command-tree construction (before DI is available). It loads the
577+
// user config best-effort so that `azd config set alpha.tool on` works;
578+
// env-var overrides (AZD_ALPHA_ENABLE_TOOL) always work regardless.
579+
func newAlphaManagerForCommandTree() *alpha.FeatureManager {
580+
ucm := config.NewUserConfigManager(config.NewFileConfigManager(config.NewManager()))
581+
cfg, err := ucm.Load()
582+
if err != nil {
583+
log.Printf("warning: failed to load user config for alpha feature gating: %v", err)
584+
cfg = config.NewEmptyConfig()
585+
}
586+
return alpha.NewFeaturesManagerWithConfig(cfg)
587+
}
588+
566589
// isWorkflowCommand reports whether the command is a primary workflow
567590
// command where a first-run tool check adds value.
568591
func isWorkflowCommand(descriptor *actions.ActionDescriptor) bool {

cli/azd/cmd/tool.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
)
2424

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

0 commit comments

Comments
 (0)