Azure / Azure/azure-dev

Make azd work with git worktrees (share/pin environments across agent sessions)

Open
#8,841 2 comments 0 reactions 1 assignee Claimed by @vhvb1989 View on GitHub
area/core-cli area/environment developer-experience enhancement
Dominant language
Go
Stars
569
Forks
364
Avg merge
2d 19h
Merged PRs (30d)
136

Description

## Problem

`azd` anchors all environment state at `ProjectDirectory()/.azure` (the directory containing `azure.yaml`, found by walking up from the cwd). Because `.azure` is gitignored, **a newly created git worktree never has it**. As a result, every worktree looks like a fresh, un-inited template: `azd` prompts to `init` / create a new environment, losing the shared environment, remote state, and already-provisioned infrastructure.

This is increasingly painful because **one git worktree per AI agent session** (GitHub Copilot CLI, VS Code agent windows, etc.) has become a common way to run multiple concurrent sessions/experiments against the same template. Today each worktree is a dead end for `azd`.

### Desired behavior
- Detect when the current folder is a **linked worktree** and there is already an environment in the **main** repo.
- By default, **transparently reuse** the main repo's environment (the `.azure` folder lives only in the main worktree because it is gitignored).
- If the main repo has **no** `.azure`, prompt to create one and write it at the main repo path (shared across worktrees).
- Allow a worktree to **use/pin its own environment without changing the main app's default**, so another session in the main app is unaffected.

## Detection mechanics (verified)
```
git rev-parse --git-common-dir # /.git (shared)
git rev-parse --git-dir # /.git/worktrees/ (per-worktree)
git rev-parse --show-toplevel #
```
- Linked worktree iff `--git-dir` != `--git-common-dir`.
- Shared main-worktree root = `dirname(--git-common-dir)` (non-bare).
- Fallback without git CLI: `.git` is a *file* (`gitdir: …`) in a worktree vs a *dir* in main; `commondir` points to the shared git dir.
- Degrade to today's behavior when: not a git repo, git missing, or bare repo with no main working tree.

## Proposed model: split "selection" from "data"
Decouple the two responsibilities currently fused into `.azure`:

1. **Env data store** (env folders + remote cache): always the **shared** `.azure` in the main worktree, at the same project-relative subpath (monorepo-safe).
2. **Selection** (which env this context uses + the default pointer): resolved per context.

`AzdContext` gains a resolved `environmentDataRoot` distinct from `projectDirectory`. Everything already funnels through `EnvironmentDirectory()` / `EnvironmentRoot(name)` / `GetDefaultEnvironmentName()`, so the blast radius is small.

### Selected-environment precedence (high → low)
1. `--environment` flag
2. `AZURE_ENV_NAME` env var
3. **worktree-local selection** (linked worktree only)
4. **shared default** (`/.azure/config.json` `defaultEnvironment`)
5. interactive prompt / init

## Locked decisions
- **D1 — Selection storage:** worktree `.azure/config.json` acts as a **pointer** (`{ defaultEnvironment, sharedRoot }`) with **no env data dirs** inside the worktree. Self-contained, already gitignored, and removed automatically by `git worktree remove`. Git is the source of truth for `sharedRoot` and refreshes a stale pointer.
- **D2 — Scope from a worktree:** `azd env new` creates the env **data in the shared store** (folder + `.env` + `config.json`), but only **this worktree** selects it; `azd env select` switches **only this worktree**. Neither changes the shared/main default. (`--scope shared` may be added later as an explicit escape hatch.)
- **D3 — Same env in two worktrees:** **warn and allow** — show an "in use by another worktree" notice (derived from `git worktree list` + each worktree pointer), but let the user proceed.
- **D4 — First use in a worktree:** **silent + provenance** — no confirmation; always print `Using environment '' from main repository ()`.

## UX flows
- **Worktree, shared `.azure` exists, has default** → use silently, always print provenance.
- **Worktree, shared `.azure` missing** → prompt: "No azd environment found in the main repository. Create one shared across worktrees?" → create env data at the main worktree root.
- **`azd env new ` in a worktree** → create env data in shared store, set worktree-local selection, leave shared default untouched.
- **`azd env select ` in a worktree** → switch only this worktree.
- **`azd env list` in a worktree** → list shared envs; distinguish shared default (★) from worktree selection (→).

## Corner cases to handle
1. Shared `.azure` missing / never inited → create at main worktree root; deterministic under `--no-prompt`.
2. Bare repo / no main working tree → fall back to standalone; document.
3. Monorepo / project in a subdir → key shared `.azure` by project path relative to worktree root; a branch that adds a NEW project not present in main → standalone.
4. `azure.yaml` moved/renamed on the worktree branch → relative path differs; no match in main → standalone.
5. **Two worktrees select the same env** (biggest hazard) → concurrent provision/deploy can corrupt state / race Azure. flock guards local files only → warn-and-allow per D3.
6. Surprise deployments → always surface resolved env + provenance.
7. Worktree already has REAL `.azure` env data (manually copied / older azd) → treat as standalone (back-compat); only redirect when the worktree has no `.azure` or only a pointer.
8. `AZURE_ENV_NAME` / `--environment` must still win over worktree selection and shared default; unknown name → offer to create in shared store.
9. `git worktree remove` → pointer removed with the dir (no orphan cleanup needed under D1).
10. No git / git not installed / non-git dir → exactly today's behavior.
11. Path normalization / symlinks (macOS `/var`→`/private/var`) → abs + EvalSymlinks before compare / relpath.
12. Worktree pointer files must be gitignored (the `.azure` dir already is).
13. Remote env backend (`m.remote`) → keep the shared local cache at the shared `.azure` so both worktrees share it.
14. `azd init` inside a worktree → `azure.yaml` already tracked; re-init OK, but env creation targets shared store.
15. CI / `--no-prompt` → deterministic: shared default if present; else error-with-suggestion (never hang).
16. Two "defaults" in UX → clearly distinguish shared default vs worktree selection everywhere.
17. Paths with spaces → quote in all user-facing messages.

## Implementation surface
- New `internal/gitrepo` helper: `IsWorktree()`, `CommonDir()`, `MainWorktreeRoot()`, `WorktreeID()`, with git CLI + manual `.git`-file fallback; no hard fail when git is absent.
- `AzdContext`: resolve `environmentDataRoot` separately from `projectDirectory`; `EnvironmentDirectory()`/`EnvironmentRoot()` return the shared data root; add a selection/pointer dir; worktree-aware `GetDefaultEnvironmentName()` and a scoped `SetProjectState(state, scope)`.
- `env` commands (`new`, `select`, `list`): add `--scope worktree|shared`; provenance messaging; "in use by another worktree" warning.
- Telemetry (per `cli/azd/AGENTS.md` checklist): bool "in worktree" + enum "env source" (flag/envvar/worktree/shared/prompt). Enums/bools only — nothing hashed. Update all telemetry docs + coverage test.
- Roll out behind an **alpha feature flag** (`alpha.worktrees`) to contain surprise-deploy risk.

## Suggested phasing
1. **Phase 1 — Detection + transparent reuse.** `alpha.worktrees` feature flag + `internal/gitrepo` detection helper; `AzdContext` data-root/selection split + worktree-aware default resolution (silent reuse + provenance); worktree pointer (D1); `env new`/`env select` scope behavior (D2).
2. **Phase 2 — `azd env list` worktree awareness.** When the repo has at least one worktree, enrich `azd env list` so users can see how environments map to worktrees (see below). Surface the "in use by another worktree" signal here (D3).
3. **Phase 3 — Telemetry + docs** (`environment-variables.md`, feature-status, design doc under `cli/azd/docs/design/`).

### Phase 2 detail: `azd env list` worktree info
Output is unchanged unless `git worktree list` reports **more than one** worktree.
- **Table mode:** add a `WORKTREE(S)` column showing which worktree(s) currently select each env (worktree name/relative path). Keep distinguishing the shared default (★) from each worktree's selection (→); highlight the env selected by the current worktree; mark envs "in use by another worktree" (ties into D3).
- **JSON mode** (`--output json`): add per-env fields, e.g. `selectedByWorktrees: ["", …]`, `isSharedDefault: bool`, `isCurrentWorktreeSelection: bool`. Match existing JSON field/type conventions; populate only when worktrees exist.
- **Data source:** `git worktree list --porcelain` cross-referenced with each worktree's `.azure/config.json` pointer (`defaultEnvironment`). Normalize paths (abs + EvalSymlinks); tolerate stale/removed worktrees.
- **Corner cases:** worktree with no pointer yet (falls back to shared default — show as such); paths with spaces (quote); bare repo / no main working tree (degrade to today's output); many worktrees (comma-join/count in table, full detail in `--output json`).

## Open follow-ups
- Whether/when to expose `--scope shared` as an explicit override.
- Exact shape of the "in use by another worktree" detection and message.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.