fix: make azd init idempotent with respect to the environment#8561
Conversation
Re-running `azd init -t <template> --environment <name>` previously hard-failed with "environment already initialized to <name>" whenever a default environment existed, forcing destructive workarounds (azd env delete or deleting .azure/). Replace the unconditional failure in initializeEnv with a reuse-vs-create-new decision that defaults to reuse: - Exact match (requested == existing default): silently reuse, no prompt. - Interactive, ambiguous: prompt (Reuse [default] / Create new). - Non-interactive: reuse the default; only error when an explicit, different, still-valid env name is requested (preserves CI safety and the typed error). - Recover partial state: orphaned env folder with no recorded default, and a stale default whose folder is missing. - On reuse, apply template metadata and location/subscription flags set-if-absent so user edits between runs are not clobbered, while repairing missing values. Add unit tests (TestInitializeEnv, 13 cases) and a functional test (Test_CLI_Init_Idempotent_Environment).
There was a problem hiding this comment.
Pull request overview
This PR updates azd init so that re-running initialization in an already-initialized project becomes environment-idempotent: it can reuse the existing default environment (or prompt to reuse vs create new in interactive scenarios) instead of hard-failing.
Changes:
- Refactors
initAction.initializeEnvto resolve whether to reuse an existing environment vs create a new one (including recovery from stale/default-orphaned state). - When reusing an environment, applies template metadata and flag-derived values in a “set-if-absent” way to avoid clobbering user-modified values between runs.
- Adds a new unit test suite for environment init resolution behavior and a functional test covering the idempotent CLI scenario.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| cli/azd/test/functional/init_test.go | Adds a functional test asserting azd init reuse on same -e and error-on-different-name in --no-prompt. |
| cli/azd/cmd/init.go | Implements reuse vs create decision logic (resolveInitEnv / helpers) and set-if-absent behavior on reuse. |
| cli/azd/cmd/init_env_test.go | Adds end-to-end unit tests for environment resolution and non-clobbering behavior. |
📋 Prioritization NoteThanks for the contribution! The linked issue isn't in the current milestone yet. |
… ErrNotFound In no-prompt mode with an explicit -e value that differs from the recorded default: - Validate the requested name before checking the default env, so an invalid name yields a clear 'invalid environment name' error rather than a misleading 'already initialized' one. - Replace the any-error bypass with an errors.Is(err, ErrNotFound) check so that real I/O or config errors loading the default env are surfaced instead of silently falling through to create the requested environment. Add two new test cases covering both code paths.
There was a problem hiding this comment.
- Deliberate behavior change for --no-prompt with no -e. Previously this errored; now it silently reuses the recorded default. Which is a change for CI/scripts. This should get a CHANGELOG.md entry and ideally a one-liner in the azd init docs.
- "Create new" + explicit name that already exists → reports "Reusing". In the interactive path, if the user picks Create a new environment but -e names an existing (non-default) env, getOrCreateEnv does a Get, succeeds, returns reused=true, and prints "Reusing existing environment…" while switching the default to it. Behavior is defensible (it switches + reuses), but the message contradicts the user's "create new" choice. Minor UX wrinkle — consider re-prompting for a name on collision, or adjusting the message.
JeffreyCA
left a comment
There was a problem hiding this comment.
Thanks for the fixes! Wondering if something like this would feel better:
-e given? |
Requested env exists? | Default exists? | Suggested behaviour |
|---|---|---|---|
| Yes | Yes | Yes — same as requested | Confirm "Reuse existing environment ''?" (default Yes) → Yes: reuse, no extra note (the confirm is the acknowledgment). No: gracefully cancel init (nothing mutated) |
| Yes | Yes | No / different default | Confirm "Reuse existing environment ''?" (default Yes) → Yes: reuse + promote to default, emit info note that the default was switched. No: gracefully cancel init (nothing mutated) |
| Yes | No (new name) | No | No prompt — create <name>, set as default |
| Yes | No (new name) | Yes (different) | No prompt — create <name>, set as default; emit info note that the previous default is being switched |
| No | — | Yes | Prompt "Reuse existing '' / Create new" (default Reuse) — the genuinely ambiguous case |
| No | — | No | Create / derive a name as today (prompt for name if interactive) |
|
My read: Re-running What's compelling: The resolution logic in Concerns:
Counter-proposals:
// Flags represent explicit user intent - always apply.
if i.flags.location != "" {
env.DotenvSet(environment.LocationEnvVarName, i.flags.location)
}
if i.flags.subscription != "" {
env.DotenvSet(environment.SubscriptionIdEnvVarName, i.flags.subscription)
}This preserves the Recommended phasing: @glharper's CHANGELOG point is a quick add. The flag semantics fix above is small and makes reuse behavior consistent with standard CLI expectations. @JeffreyCA's decision table refinements (skip prompt when |
… on reuse Rework resolveInitEnv into an explicit decision tree so re-running azd init in an initialized project is idempotent: an existing -e environment is reused (interactive confirms, declining cancels cleanly), a new -e environment is created and promoted to the default without prompting, and --no-prompt without -e reuses the recorded default instead of erroring. Centralize outcome messaging via initEnvOutcome so exactly one Switching/Reusing line prints after a successful save, apply explicit --location/--subscription flags unconditionally so they win on reuse, and add graceful cancellation (errInitEnvCancelled) handling at the init call sites.
|
@JeffreyCA Love this, much clearer. Implemented the full table in |
|
@glharper Thanks! Both addressed:
|
|
@jongio Good call on the flag semantics! Fixed: |
5752845 to
02c3272
Compare
Azure Dev CLI Install InstructionsInstall scriptsMacOS/Linux
bash: pwsh: WindowsPowerShell install MSI install Standalone Binary
MSI
Documentationlearn.microsoft.com documentationtitle: Azure Developer CLI reference
|
Fixes #8562
Problem
Re-running
azd init -t <template> --environment <name>in a directory that already has an azd environment hard-failed with:This forced users (and agent/extension flows that drive
azd init) into destructive workarounds —azd env delete <name>or deleting the.azure/folder — just to re-run init. The failure happened ininitializeEnv, which errored whenever a default environment existed, without ever checking whether the requested--environmentmatched the existing one.Change
Replace the unconditional failure with a reuse-vs-create-new decision that defaults to reuse:
-e== existing default) → silently reuse, no prompt. This is the common case and fixes agent/extension flows that have no--no-promptflag.EnvironmentInitErrorused by the error middleware).--location/--subscriptionflags are applied set-if-absent, so user edits between runs aren't clobbered while missing values still get repaired.Tests
cli/azd/cmd/init_env_test.go(new) —TestInitializeEnv, 13 cases covering fresh create, interactive reuse/create-new prompts, exact-match silent reuse, non-interactive reuse/error/recovery, orphan/stale recovery, metadata-not-clobbered (with disk reload), and flag set-if-absent.cli/azd/test/functional/init_test.go—Test_CLI_Init_Idempotent_Environment:init -e TESTENV→init -e TESTENV(reuses, succeeds) →init -e OTHERENV --no-prompt(still errors).Validation
go buildclean ·golangci-lint0 issues ·gofmt -sclean ·cspell(project config) 0 issues · copyright headers present.Known limitation
In
--no-promptmode with an empty requested name where the auto-derived name collides with an orphan, recovery isn't attempted — the derived name lives insideenvManager.Createand isn't retrievable by the caller. Callers that pass an explicit--environment(the agent/extension path) are unaffected.