From ba1470892e1f36b6fc8dc0e242753ab15adacc01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:54:35 +0000 Subject: [PATCH 1/3] Initial plan From b1c4370a287487907779e6f187456eb2572cb996 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:15:15 +0000 Subject: [PATCH 2/3] Apply PR #9259 changes: change AZD_SKIP_FIRST_RUN to default to skip first-run experience Co-authored-by: tg-msft <1179329+tg-msft@users.noreply.github.com> --- cli/azd/cmd/middleware/tool_first_run.go | 34 ++++++++++++++---- cli/azd/cmd/middleware/tool_first_run_test.go | 35 +++++++++++++------ cli/azd/cmd/middleware/tool_update_check.go | 3 ++ cli/azd/docs/environment-variables.md | 2 +- cli/azd/internal/tracing/fields/fields.go | 4 +-- 5 files changed, 59 insertions(+), 19 deletions(-) diff --git a/cli/azd/cmd/middleware/tool_first_run.go b/cli/azd/cmd/middleware/tool_first_run.go index 9123ce15e52..948542f615d 100644 --- a/cli/azd/cmd/middleware/tool_first_run.go +++ b/cli/azd/cmd/middleware/tool_first_run.go @@ -32,8 +32,19 @@ import ( // the timestamp of a completed first-run experience. const configKeyFirstRunCompleted = "tool.firstRunCompleted" -// envKeySkipFirstRun is the environment variable that, when set to -// "true", suppresses the first-run tool check entirely. +// envKeySkipFirstRun is the environment variable that controls the +// first-run tool check. +// +// The first-run experience is OFF by default: absent an explicit +// AZD_SKIP_FIRST_RUN=false, it is skipped, so we never interrupt a user's +// command with an unsolicited prompt. Setting it to "true" is still honoured +// (and still reported distinctly in telemetry) for users who had already +// opted out. +// +// Note the asymmetry: tool_update_check.go reads this same variable with the +// ordinary default-false semantics ("true" means skip, unset means run), +// because background update checks are not disabled by default. Don't +// "fix" that inconsistency — it's deliberate. const envKeySkipFirstRun = "AZD_SKIP_FIRST_RUN" // Skip reason constants emitted via fields.ToolFirstRunSkipReasonKey so @@ -54,7 +65,15 @@ const envKeySkipFirstRun = "AZD_SKIP_FIRST_RUN" // A skip reason is *not* emitted for the child-action path: child actions // (e.g. workflow steps) inherit the parent's first-run state and do not // contribute useful signal to first-run adoption analysis. +// +// `default_disabled` and `env_var` are likewise distinct: +// - `default_disabled` is the ordinary path — the first-run experience is +// off unless AZD_SKIP_FIRST_RUN is explicitly "false", so an unset (or +// unparsable) value lands here. This is the overwhelmingly common case. +// - `env_var` is a deliberate AZD_SKIP_FIRST_RUN=true opt-out, which is +// what that reason has always meant. const ( + skipReasonDefaultDisabled = "default_disabled" skipReasonEnvVar = "env_var" skipReasonNoPrompt = "no_prompt" skipReasonCICD = "ci_cd" @@ -129,8 +148,12 @@ func (m *ToolFirstRunMiddleware) Run(ctx context.Context, nextFn NextFn) (*actio // should be bypassed. The reasons are checked in order of cost (cheapest // first). func (m *ToolFirstRunMiddleware) shouldSkip(ctx context.Context) (string, bool) { - // 1. Env-var opt-out. - if skip, _ := strconv.ParseBool(os.Getenv(envKeySkipFirstRun)); skip { + // 1. Env-var gate. Opt-in: only an explicit AZD_SKIP_FIRST_RUN=false runs + // the experience; unset or unparsable means skip. + switch v, err := strconv.ParseBool(os.Getenv(envKeySkipFirstRun)); { + case err != nil: + return skipReasonDefaultDisabled, true + case v: return skipReasonEnvVar, true } @@ -182,8 +205,7 @@ func (m *ToolFirstRunMiddleware) runFirstRunExperience(ctx context.Context) erro "Discover and install Azure development tools such as Azure CLI, GitHub Copilot CLI, and Azure AI extensions.", ) m.console.Message(ctx, output.WithGrayFormat( - "To skip this check, set %s or run %s.", - output.WithHighLightFormat("AZD_SKIP_FIRST_RUN=true"), + "To skip this check, run %s.", output.WithHighLightFormat("azd config set tool.firstRunCompleted true"), )) m.console.Message(ctx, "") diff --git a/cli/azd/cmd/middleware/tool_first_run_test.go b/cli/azd/cmd/middleware/tool_first_run_test.go index fb6381aa9e8..b73c99b6b70 100644 --- a/cli/azd/cmd/middleware/tool_first_run_test.go +++ b/cli/azd/cmd/middleware/tool_first_run_test.go @@ -80,6 +80,14 @@ func TestToolFirstRunMiddleware_SkipConditions(t *testing.T) { childCtx bool // wrap context with WithChildAction wantSkip bool }{ + { + // The default for everyone who hasn't opted in. + name: "SkipWhenEnvVarUnset", + setup: func(t *testing.T, _ *mockinput.MockConsole, _ config.Config, _ *internal.GlobalCommandOptions) { + os.Unsetenv(envKeySkipFirstRun) + }, + wantSkip: true, + }, { name: "SkipWhenEnvVarSet", setup: func(t *testing.T, _ *mockinput.MockConsole, _ config.Config, _ *internal.GlobalCommandOptions) { @@ -144,8 +152,8 @@ func TestToolFirstRunMiddleware_SkipConditions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { clearCIVars(t) - t.Setenv(envKeySkipFirstRun, "") - os.Unsetenv(envKeySkipFirstRun) + // The experience is off unless explicitly opted in. + t.Setenv(envKeySkipFirstRun, "false") console := mockinput.NewMockConsole() cfg := config.NewEmptyConfig() @@ -190,8 +198,8 @@ func TestToolFirstRunMiddleware_SkipConditions(t *testing.T) { func TestToolFirstRunMiddleware_TriggersWhenNoSkip(t *testing.T) { // Ensure no CI env vars interfere. clearCIVars(t) - t.Setenv(envKeySkipFirstRun, "") - os.Unsetenv(envKeySkipFirstRun) + // The experience is off unless explicitly opted in. + t.Setenv(envKeySkipFirstRun, "false") console := mockinput.NewMockConsole() cfg := config.NewEmptyConfig() @@ -230,6 +238,13 @@ func TestToolFirstRunMiddleware_EmitsSkipReason(t *testing.T) { setup func(t *testing.T, console *mockinput.MockConsole, cfg config.Config, opts *internal.GlobalCommandOptions) wantReason string }{ + { + name: "default_disabled", + setup: func(t *testing.T, _ *mockinput.MockConsole, _ config.Config, _ *internal.GlobalCommandOptions) { + os.Unsetenv(envKeySkipFirstRun) + }, + wantReason: skipReasonDefaultDisabled, + }, { name: "env_var", setup: func(t *testing.T, _ *mockinput.MockConsole, _ config.Config, _ *internal.GlobalCommandOptions) { @@ -270,8 +285,8 @@ func TestToolFirstRunMiddleware_EmitsSkipReason(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { clearCIVars(t) - t.Setenv(envKeySkipFirstRun, "") - os.Unsetenv(envKeySkipFirstRun) + // The experience is off unless explicitly opted in. + t.Setenv(envKeySkipFirstRun, "false") // Reset usage attributes so we can verify the middleware // affirmatively writes tool.firstrun.skip_reason in this path. @@ -308,8 +323,8 @@ func TestToolFirstRunMiddleware_EmitsSkipReason(t *testing.T) { // to first-run adoption analysis. func TestToolFirstRunMiddleware_NoSkipReasonForChildAction(t *testing.T) { clearCIVars(t) - t.Setenv(envKeySkipFirstRun, "") - os.Unsetenv(envKeySkipFirstRun) + // The experience is off unless explicitly opted in. + t.Setenv(envKeySkipFirstRun, "false") tracing.ResetUsageAttributesForTest() @@ -364,8 +379,8 @@ func TestToolFirstRunMiddleware_ShouldSkip_FirstRunCompletedValue(t *testing.T) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { clearCIVars(t) - t.Setenv(envKeySkipFirstRun, "") - os.Unsetenv(envKeySkipFirstRun) + // The experience is off unless explicitly opted in. + t.Setenv(envKeySkipFirstRun, "false") cfg := config.NewEmptyConfig() require.NoError(t, cfg.Set(configKeyFirstRunCompleted, tt.value)) diff --git a/cli/azd/cmd/middleware/tool_update_check.go b/cli/azd/cmd/middleware/tool_update_check.go index 8e7dff4177e..ecec3e55e6e 100644 --- a/cli/azd/cmd/middleware/tool_update_check.go +++ b/cli/azd/cmd/middleware/tool_update_check.go @@ -147,6 +147,9 @@ func (m *ToolUpdateCheckMiddleware) isToolCommand() bool { // crashing the CLI. func (m *ToolUpdateCheckMiddleware) triggerBackgroundCheckIfNeeded(ctx context.Context) { // Honour the same opt-out signal used by the first-run experience. + // Note: unlike the first-run experience — which is off unless the variable + // is explicitly "false" — background update checks keep the ordinary + // default: unset means run, "true" means skip. See envKeySkipFirstRun. if skip, _ := strconv.ParseBool(os.Getenv(envKeySkipFirstRun)); skip { return } diff --git a/cli/azd/docs/environment-variables.md b/cli/azd/docs/environment-variables.md index ef337d5ffcc..4a904bc69a9 100644 --- a/cli/azd/docs/environment-variables.md +++ b/cli/azd/docs/environment-variables.md @@ -47,7 +47,7 @@ integration. | `AZD_FORCE_TTY` | If true, forces `azd` to write terminal-style output. | | `AZD_IN_CLOUDSHELL` | If true, `azd` runs with Azure Cloud Shell specific behavior. | | `AZD_SKIP_UPDATE_CHECK` | If true, skips the out-of-date update check output that is typically printed at the end of the command. | -| `AZD_SKIP_FIRST_RUN` | If true, skips the first-run tool setup experience and background tool update checks. Useful for CI/CD pipelines and automated environments. | +| `AZD_SKIP_FIRST_RUN` | Controls the first-run tool setup experience, which is **off by default**: set to `false` to opt in to it. If true, also skips background tool update checks, which are otherwise on by default. Useful for CI/CD pipelines and automated environments. | | `AZD_CONTAINER_RUNTIME` | The container runtime to use (e.g., `docker`, `podman`). | | `AZD_ALLOW_NON_EMPTY_FOLDER` | If set, allows `azd init` to run in a non-empty directory without prompting. | | `AZD_BUILDER_IMAGE` | The builder docker image used to perform Dockerfile-less builds. | diff --git a/cli/azd/internal/tracing/fields/fields.go b/cli/azd/internal/tracing/fields/fields.go index de360f23974..efbde2b2422 100644 --- a/cli/azd/internal/tracing/fields/fields.go +++ b/cli/azd/internal/tracing/fields/fields.go @@ -422,8 +422,8 @@ var ( var ( // ToolFirstRunSkipReasonKey records why the first-run experience was // bypassed for this invocation. - // Example: "env_var", "no_prompt", "ci_cd", "non_interactive", - // "already_completed", "config_error" + // Example: "default_disabled", "env_var", "no_prompt", "ci_cd", + // "non_interactive", "already_completed", "config_error" ToolFirstRunSkipReasonKey = AttributeKey{ Key: attribute.Key("tool.firstrun.skip_reason"), Classification: SystemMetadata, From 7c6497a713b1822b1ef7649174d9f0b5b761d126 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:17:16 +0000 Subject: [PATCH 3/3] Fix test setup: use t.Setenv+os.Unsetenv for consistent env var cleanup in unset test cases Co-authored-by: tg-msft <1179329+tg-msft@users.noreply.github.com> --- cli/azd/cmd/middleware/tool_first_run_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/azd/cmd/middleware/tool_first_run_test.go b/cli/azd/cmd/middleware/tool_first_run_test.go index b73c99b6b70..e0de85cf1a4 100644 --- a/cli/azd/cmd/middleware/tool_first_run_test.go +++ b/cli/azd/cmd/middleware/tool_first_run_test.go @@ -84,6 +84,7 @@ func TestToolFirstRunMiddleware_SkipConditions(t *testing.T) { // The default for everyone who hasn't opted in. name: "SkipWhenEnvVarUnset", setup: func(t *testing.T, _ *mockinput.MockConsole, _ config.Config, _ *internal.GlobalCommandOptions) { + t.Setenv(envKeySkipFirstRun, "") os.Unsetenv(envKeySkipFirstRun) }, wantSkip: true, @@ -241,6 +242,7 @@ func TestToolFirstRunMiddleware_EmitsSkipReason(t *testing.T) { { name: "default_disabled", setup: func(t *testing.T, _ *mockinput.MockConsole, _ config.Config, _ *internal.GlobalCommandOptions) { + t.Setenv(envKeySkipFirstRun, "") os.Unsetenv(envKeySkipFirstRun) }, wantReason: skipReasonDefaultDisabled,