From d1edaae884a84517cff97b081bcdd3bb73348f14 Mon Sep 17 00:00:00 2001 From: hemarina Date: Mon, 23 Mar 2026 17:50:47 -0700 Subject: [PATCH 1/7] update design docs and hide auto update from help --- cli/azd/cmd/update.go | 20 +--- cli/azd/docs/design/azd-update.md | 153 ++++++++++++-------------- cli/azd/resources/alpha_features.yaml | 2 +- 3 files changed, 74 insertions(+), 101 deletions(-) diff --git a/cli/azd/cmd/update.go b/cli/azd/cmd/update.go index 974200a0667..090eaa47ab5 100644 --- a/cli/azd/cmd/update.go +++ b/cli/azd/cmd/update.go @@ -48,12 +48,6 @@ func (f *updateFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandO "", "Update channel: stable or daily.", ) - local.StringVar( - &f.autoUpdate, - "auto-update", - "", - "Enable or disable auto-update: on or off.", - ) local.IntVar( &f.checkIntervalHours, "check-interval-hours", @@ -126,7 +120,7 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) { } a.console.MessageUxItem(ctx, &ux.MessageTitle{ - Title: "azd update is in alpha. Auto-update and channel-aware version checks are now enabled.\n", + Title: "azd update is in alpha. Channel-aware version checks are now enabled.\n", }) } @@ -315,20 +309,14 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) { }, nil } -// persistNonChannelFlags saves auto-update and check-interval flags to config. +// persistNonChannelFlags saves check-interval flags to config. // Channel is handled separately to allow confirmation before persisting. func (a *updateAction) persistNonChannelFlags(cfg config.Config) (bool, error) { changed := false if a.flags.autoUpdate != "" { - enabled := a.flags.autoUpdate == "on" - if a.flags.autoUpdate != "on" && a.flags.autoUpdate != "off" { - return false, fmt.Errorf("invalid auto-update value %q, must be \"on\" or \"off\"", a.flags.autoUpdate) - } - if err := update.SaveAutoUpdate(cfg, enabled); err != nil { - return false, err - } - changed = true + return false, fmt.Errorf( + "the --auto-update flag is work in progress. Run 'azd update' to update manually") } if a.flags.checkIntervalHours > 0 { diff --git a/cli/azd/docs/design/azd-update.md b/cli/azd/docs/design/azd-update.md index 3eedd750303..e5348200b39 100644 --- a/cli/azd/docs/design/azd-update.md +++ b/cli/azd/docs/design/azd-update.md @@ -1,7 +1,8 @@ -# Design: `azd update`, Auto-Update & Channel Management +# Design: `azd update` & Channel Management **Epic**: [#6721](https://github.com/Azure/azure-dev/issues/6721) -**Status**: Draft +**Status**: In Progress (once confirmed with team, will update to Final) +**Decisions**: [#7002](https://github.com/Azure/azure-dev/issues/7002) --- @@ -10,8 +11,7 @@ Today, when a new version of `azd` is available, users see a warning message with copy/paste instructions to update manually. This design introduces: 1. **`azd update`** — a command that performs the update for the user -2. **Auto-update** — opt-in background updates applied at next startup -3. **Channel management** — ability to switch between `stable` and `daily` builds +2. **Channel management** — ability to switch between `stable` and `daily` builds The feature ships as a hidden command behind an alpha feature toggle (`alpha.update`) for safe rollout. When the toggle is off, there are zero changes to existing behavior — `azd version`, update notifications, everything stays exactly as it is today. @@ -20,11 +20,10 @@ The feature ships as a hidden command behind an alpha feature toggle (`alpha.upd ## Goals - Make it easy for users to update `azd` intentionally -- Support opt-in auto-update for both stable and daily channels -- Preserve user control (opt-out, channel selection, check interval) +- Preserve user control (channel selection, check interval) - Avoid disruption to CI/CD pipelines -- Respect platform install methods (Homebrew, winget, choco, scripts) -- Ship safely behind a feature flag with zero impact when off +- Respect platform install methods (MSI, Install Scripts, Homebrew, winget, choco) +- Ship safely behind an alpha feature flag with zero impact when off --- @@ -114,10 +113,10 @@ The extension manager (`pkg/extensions/manager.go`) already implements a nearly ### 1. Configuration -Three config keys via `azd config`: +Two config keys via `azd config`: ```bash -azd config set updates.autoUpdate on # or "off" (default: off) +azd config set updates.channel daily # "stable" (default) or "daily" ``` Channel is set via `azd update --channel ` (which persists the choice to `updates.channel` config). Default channel is `stable`. @@ -159,19 +158,16 @@ A new command (initially hidden) that updates the azd binary. azd update # Update to latest version on current channel azd update --channel daily # Switch channel to daily and update now azd update --channel stable # Switch channel to stable and update now -azd update --auto-update on # Enable auto-update -azd update --auto-update off # Disable auto-update azd update --check-interval-hours 4 # Override check interval ``` -Flags can be combined: `azd update --channel daily --auto-update on --check-interval-hours 2` +Flags can be combined: `azd update --channel daily --check-interval-hours 2` **Defaults**: | Flag | Config Key | Default | Values | |------|-----------|---------|--------| | `--channel` | `updates.channel` | `stable` | `stable`, `daily` | -| `--auto-update` | `updates.autoUpdate` | `off` | `on`, `off` | | `--check-interval-hours` | `updates.checkIntervalHours` | `24` (stable), `4` (daily) | Any positive integer | All flags persist their values to config, which can also be set directly via `azd config set`. @@ -180,14 +176,16 @@ All flags persist their values to config, which can also be set directly via `az | Install Method | Strategy | |----------------|----------| -| `brew` | Shell out: `brew upgrade azure/azd/azd` | +| `brew` | Homebrew cask: `brew install/upgrade --cask azure/azd/azd` (stable) or `azure/azd/azd@daily` (daily). Handles channel switching by uninstalling the current formula or cask and installing the target. | | `winget` | Shell out: `winget upgrade Microsoft.Azd` | | `choco` | Shell out: `choco upgrade azd` | -| `install-azd.sh`, `install-azd.ps1`, `msi`, `deb`, `rpm` | Direct binary download + replace | +| `install-azd.ps1`, `msi` (Windows) | Shell out: `install-azd.ps1` with backup/restore of running executable | +| `install-azd.sh` (Linux/macOS) | Shell out: `install-azd.sh` with channel and install folder arguments | +| `deb`, `rpm` | Direct binary download + replace | > **Note**: Linux `deb`/`rpm` packages are standalone files from GitHub Releases — there is no managed apt/dnf repository. These users are treated the same as script-installed users for update purposes. -#### Direct Binary Update Flow (Script/MSI Users) +#### Update Flow ``` 1. Check current channel config (stable or daily) @@ -198,90 +196,77 @@ All flags persist their values to config, which can also be set directly via `az - Stable: semver comparison (blang/semver) - Daily: build number comparison (extracted from the daily.N suffix) 4. If no update available → "You're up to date" -5. Download update (with progress bar) - - macOS/Linux: download archive to temp dir, extract binary - - Windows: download MSI to temp dir -6. Verify code signature (macOS: codesign, Windows: Get-AuthenticodeSignature) -7. Install update - - macOS/Linux: replace binary at install location (sudo if needed) - - Windows: run MSI silently via `msiexec /i /qn` -8. Done — new version takes effect on next invocation +5. Dispatch to the appropriate update method based on install type (see below) ``` -#### Code Signing Verification +#### Windows Update Flow (MSI via `install-azd.ps1`) -Before installing, the downloaded binary's code signature is verified: -- **macOS**: `codesign -v --strict ` — checks Apple notarization -- **Windows**: `Get-AuthenticodeSignature` via PowerShell — checks Authenticode signature -- **Linux**: Skipped (no standard code signing mechanism) +Windows updates (for `install-azd.ps1`, `msi`, and other Windows install types) use the official PowerShell install script: -The check is fail-safe: if `codesign` or PowerShell isn't available (unlikely), the update proceeds. But if the tool runs and the signature is explicitly invalid, the update is blocked. - -#### Elevation Handling - -Most install methods write to system directories requiring elevation: - -| Location | Needs Elevation | Update Method | -|----------|----------------|---------------| -| `/opt/microsoft/azd/` (Linux script) | Yes — `sudo cp` | Direct binary replacement | -| `C:\Program Files\` (Windows MSI) | Yes — handled by MSI installer | MSI via `msiexec /i` | -| `~/.azd/bin/` (Windows PowerShell script) | No — user-writable | MSI via `msiexec /i` | -| Homebrew prefix | No — user-writable | Delegates to `brew upgrade azure/azd/azd` | -| User home dirs | No | Direct binary replacement | - -**Windows**: Updates always use the MSI installer (`msiexec /i /qn`), which handles UAC elevation when installing to protected locations like `C:\Program Files\`. Downgrades between GA versions are not supported via MSI. - -**macOS/Linux (brew)**: Homebrew tracks installed assets, so azd never overwrites brew-managed binaries directly. Same-channel updates delegate to `brew upgrade azure/azd/azd`. Channel switching (stable ↔ daily) currently requires uninstalling brew and reinstalling via script. A future brew pre-release formula could enable `brew` to handle daily builds natively. - -**macOS/Linux (script)**: For `sudo`, azd passes through stdin/stdout so the user sees the standard OS password prompt. Uses `CommandRunner` (`pkg/exec/command_runner.go`) for exec. - -### 4. Auto-Update - -**Auto-update is off by default.** Background downloading and staged apply only happen when the user explicitly opts in via `azd config set updates.autoUpdate on` or `azd update --auto-update on`. Without opt-in, the only background activity is the existing version check (cheap HTTP GET) that shows a banner — no downloads occur. +``` +1. Verify standard per-user MSI install location + - install-azd.ps1 installs with ALLUSERS=2 to %LOCALAPPDATA%\Programs\Azure Dev CLI + - If non-standard install detected → abort with guidance to reinstall +2. Backup running executable (rename + safety copy) + - Rename running azd.exe to a temp backup location (frees the path; process continues via OS handle) + - Copy the backup back as an unlocked safety net at the original path + - If update fails at any point → restore original from backup automatically +3. Run install-azd.ps1 with channel-specific arguments + - Script handles MSI download, verification, and installation +4. Verify the binary was actually replaced (SHA-256 hash comparison pre vs post) + - If hashes match → MSI failed silently → abort and restore backup +5. Clean up backup on success +``` -Additionally, auto-update is gated behind the `update` alpha feature flag during the rollout phase. +#### Linux/macOS Update Flow (via `install-azd.sh`) -When `updates.autoUpdate` is set to `on`: +For script-based installs (`install-azd.sh`) on Linux/macOS: -**Cache TTL** (channel-dependent): -- Stable: 24h (releases are infrequent) -- Daily: 4h (builds land frequently) +``` +1. Download install-azd.sh to a temp directory with restrictive permissions (0700) +2. Make the script executable (0500) +3. Run: bash install-azd.sh --version --install-folder --symlink-folder "" + - The script handles download, checksum verification, and binary placement + - Passes through stdin/stdout for sudo prompts if elevation is needed +4. Done — new version takes effect on next invocation +``` -Downloads only happen when a newer version exists. +#### Homebrew Update Flow (via cask) -**Flow (two-phase: stage in background, apply on next startup)**: +Homebrew updates use cask operations for both stable and daily channels: ``` -Phase 1 — Stage (background goroutine during any azd invocation): -1. Check AZD_SKIP_UPDATE_CHECK / CI env vars → skip if set -2. Check version (respecting channel-dependent cache TTL) -3. If newer version available → download to ~/.azd/staging/azd -4. Verify checksum + code signature on the staged binary - -Phase 2 — Apply (on NEXT startup, before command execution): -1. Detect staged binary at ~/.azd/staging/azd -2. Verify staged binary integrity (macOS: codesign check — unsigned is OK, corrupted/truncated is rejected) -3. Try to copy over current binary (with fsync to flush data to disk) - - If writable (user home, homebrew prefix) → swap, re-exec, show success banner - - If permission denied (system dir like /opt/microsoft/azd/) → skip, show warning - - If staged binary is invalid (e.g. truncated download) → clean up, skip silently -4. On success: write marker file, re-exec with same args, display banner -5. On permission denied: show "WARNING: azd version X.Y.Z has been downloaded. Run 'azd update' to apply it." - (The "out of date" banner is suppressed when this elevation warning is shown, to avoid duplicate warnings.) +1. Check which cask is currently installed via `brew list --cask` + - `azd` = stable cask, `azd@daily` = daily cask +2. If no cask installed (formula or other install) → uninstall and reinstall as correct cask +3. If switching channels → uninstall current cask, install target cask + - daily→stable: `brew uninstall --cask azd@daily` then `brew install --cask azure/azd/azd` + - stable→daily: `brew uninstall --cask azd` then `brew install --cask azure/azd/azd@daily` +4. If same channel → `brew upgrade --cask azure/azd/azd` (or `azure/azd/azd@daily`) ``` -The re-exec approach (`syscall.Exec` on Unix, spawn-and-exit on Windows) means the user's command runs seamlessly on the new binary — they just see a one-line success banner before their normal output. +#### Elevation Handling -**Staged binary verification**: Before applying, `verifyStagedBinary()` checks the staged binary's integrity. On macOS, it runs `codesign -v --strict`. On Windows, it checks Authenticode signature via `Get-AuthenticodeSignature`. **Unsigned or invalid binaries are rejected** — verification hard-fails with `CodeSignatureInvalid` error and the staged binary is cleaned up. A minimum file size check (1MB) catches truncated downloads on all platforms. +| Location | Needs Elevation | Update Method | +|----------|----------------|---------------| +| `/opt/microsoft/azd/` (Linux script) | Yes — handled by `install-azd.sh` via `sudo` | Install script | +| `%LOCALAPPDATA%\Programs\Azure Dev CLI\` (Windows MSI) | No — per-user install | `install-azd.ps1` | +| Homebrew prefix | No — user-writable | `brew install/upgrade --cask` | -**Elevation-aware behavior**: Auto-update doesn't prompt for passwords. If the install location requires elevation, it gracefully falls back to a warning and the staged binary stays around for `azd update` to apply (which has the sudo fallback with an interactive prompt). +**Windows**: The default per-user MSI install (`install-azd.ps1`) writes to `%LOCALAPPDATA%\Programs\Azure Dev CLI\` which does not require elevation. Non-standard installs (e.g., `C:\Program Files\`) are detected and rejected with guidance to reinstall using the standard method. -**CI/Non-Interactive Detection**: Auto-update staging is skipped when running in CI/CD. Uses `resource.IsRunningOnCI()` (supports GitHub Actions, Azure Pipelines, Jenkins, GitLab CI, CircleCI, etc.) and `AZD_SKIP_UPDATE_CHECK`. +**macOS/Linux (brew)**: Homebrew manages its own assets. Channel switching is fully supported via cask uninstall/install operations. -Skip auto-update when: +**macOS/Linux (script)**: The install script handles elevation internally. azd passes through stdin/stdout via `CommandRunner` (`pkg/exec/command_runner.go`) so the user sees the standard OS password prompt when `sudo` is needed. + +**CI/Non-Interactive Detection**: The startup version check is skipped in CI/CD environments. Uses `resource.IsRunningOnCI()` (supports GitHub Actions, Azure Pipelines, Jenkins, GitLab CI, CircleCI, etc.) and `AZD_SKIP_UPDATE_CHECK`. + +Skip update check when: - `resource.IsRunningOnCI()` returns true - `AZD_SKIP_UPDATE_CHECK=true` +> **Note on auto-update**: Background download and auto-apply (stage in background, apply on next startup) is deferred to a future iteration. Currently, users must run `azd update` manually. See [#7002 decision](https://github.com/Azure/azure-dev/issues/7002#issuecomment-4114386136). + ### 5. Channel Switching #### Same Install Method (Script/MSI) @@ -311,6 +296,8 @@ Switching between a package manager and direct installs is **not supported** via This avoids the silent symlink overwrite problem that exists today with conflicting install methods. +**Homebrew users**: Channel switching between stable and daily is fully supported via cask operations (see Homebrew Update Flow above). Homebrew is not treated as an external package manager — azd handles cask operations directly. + **Package manager users on stable**: `azd update` delegates to the package manager. No channel switching complexity — daily isn't available through package managers. ### 6. `azd version` Output @@ -366,7 +353,7 @@ These codes are integrated into azd's `MapError` pipeline, so update failures sh The entire update feature ships behind `alpha.update` (default: off). This means: - **Toggle off** (default): Zero behavior changes. `azd version` output is the same. Update notification shows the existing platform-specific install instructions. `azd update` returns an error telling the user to enable the feature. -- **Toggle on** (`azd config set alpha.update on`): All update features are active — `azd update` works, auto-update stages/applies, `azd version` shows the channel suffix, notifications say "run `azd update`." +- **Toggle on** (`azd config set alpha.update on`): All update features are active — `azd update` works, `azd version` shows the channel suffix, notifications say "run `azd update`." This lets us roll out to internal users first, gather feedback, and fix issues before broader availability. Once stable, the toggle can be removed and the feature enabled by default. @@ -374,6 +361,4 @@ This lets us roll out to internal users first, gather feedback, and fix issues b The startup "out of date" warning banner is suppressed during `azd update` (stale version is in-process and about to be replaced) and `azd config` (user is managing settings — showing a warning alongside config changes is noise). This is handled by `suppressUpdateBanner()` in `main.go`. -When the auto-update elevation warning is shown ("azd version X.Y.Z has been downloaded. Run 'azd update' to apply it."), the "out of date" warning is also suppressed to avoid showing two redundant warnings about the same condition. - --- diff --git a/cli/azd/resources/alpha_features.yaml b/cli/azd/resources/alpha_features.yaml index e0e622a7d7d..84b84e4e0f5 100644 --- a/cli/azd/resources/alpha_features.yaml +++ b/cli/azd/resources/alpha_features.yaml @@ -15,4 +15,4 @@ - id: language.custom description: "Enables support for services to use custom language." - id: update - description: "Enables the azd update command for self-updating azd, including channel management and auto-update." + description: "Enables the azd update command for self-updating azd and channel management." From 693488e013c152e4ee3375900b4828507fa3d79d Mon Sep 17 00:00:00 2001 From: hemarina Date: Mon, 23 Mar 2026 17:56:23 -0700 Subject: [PATCH 2/7] update message --- cli/azd/cmd/update.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/azd/cmd/update.go b/cli/azd/cmd/update.go index 090eaa47ab5..663318edd68 100644 --- a/cli/azd/cmd/update.go +++ b/cli/azd/cmd/update.go @@ -316,7 +316,8 @@ func (a *updateAction) persistNonChannelFlags(cfg config.Config) (bool, error) { if a.flags.autoUpdate != "" { return false, fmt.Errorf( - "the --auto-update flag is work in progress. Run 'azd update' to update manually") + "the --auto-update flag is work in progress. Run 'azd config unset updates.autoUpdate' and" + + " then 'azd update' to update manually") } if a.flags.checkIntervalHours > 0 { From 9e64bc0accd3e5e8e07b9b020db67d9789314ebd Mon Sep 17 00:00:00 2001 From: hemarina Date: Tue, 24 Mar 2026 11:49:25 -0700 Subject: [PATCH 3/7] address copilot feedback --- cli/azd/cmd/update.go | 24 ++++-- cli/azd/docs/design/azd-update.md | 3 +- cli/azd/main.go | 116 +++++++++++++------------- cli/azd/resources/alpha_features.yaml | 2 +- 4 files changed, 78 insertions(+), 67 deletions(-) diff --git a/cli/azd/cmd/update.go b/cli/azd/cmd/update.go index 663318edd68..54be6c7a825 100644 --- a/cli/azd/cmd/update.go +++ b/cli/azd/cmd/update.go @@ -48,6 +48,12 @@ func (f *updateFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandO "", "Update channel: stable or daily.", ) + // local.StringVar( + // &f.autoUpdate, + // "auto-update", + // "", + // "Enable or disable auto-update: on or off.", + // ) local.IntVar( &f.checkIntervalHours, "check-interval-hours", @@ -314,11 +320,16 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) { func (a *updateAction) persistNonChannelFlags(cfg config.Config) (bool, error) { changed := false - if a.flags.autoUpdate != "" { - return false, fmt.Errorf( - "the --auto-update flag is work in progress. Run 'azd config unset updates.autoUpdate' and" + - " then 'azd update' to update manually") - } + // if a.flags.autoUpdate != "" { + // enabled := a.flags.autoUpdate == "on" + // if a.flags.autoUpdate != "on" && a.flags.autoUpdate != "off" { + // return false, fmt.Errorf("invalid auto-update value %q, must be \"on\" or \"off\"", a.flags.autoUpdate) + // } + // if err := update.SaveAutoUpdate(cfg, enabled); err != nil { + // return false, err + // } + // changed = true + // } if a.flags.checkIntervalHours > 0 { if err := update.SaveCheckIntervalHours(cfg, a.flags.checkIntervalHours); err != nil { @@ -332,6 +343,5 @@ func (a *updateAction) persistNonChannelFlags(cfg config.Config) (bool, error) { // onlyConfigFlagsSet returns true if only config flags were provided (no channel that requires an update). func (a *updateAction) onlyConfigFlagsSet() bool { - return a.flags.channel == "" && - (a.flags.autoUpdate != "" || a.flags.checkIntervalHours > 0) + return a.flags.channel == "" && a.flags.checkIntervalHours > 0 } diff --git a/cli/azd/docs/design/azd-update.md b/cli/azd/docs/design/azd-update.md index e5348200b39..fd014732d15 100644 --- a/cli/azd/docs/design/azd-update.md +++ b/cli/azd/docs/design/azd-update.md @@ -117,6 +117,7 @@ Two config keys via `azd config`: ```bash azd config set updates.channel daily # "stable" (default) or "daily" +azd config set updates.checkIntervalHours 4 ``` Channel is set via `azd update --channel ` (which persists the choice to `updates.channel` config). Default channel is `stable`. @@ -352,7 +353,7 @@ These codes are integrated into azd's `MapError` pipeline, so update failures sh The entire update feature ships behind `alpha.update` (default: off). This means: -- **Toggle off** (default): Zero behavior changes. `azd version` output is the same. Update notification shows the existing platform-specific install instructions. `azd update` returns an error telling the user to enable the feature. +- **Toggle off** (default): Zero behavior changes. `azd version` output is the same. Update notification shows the existing platform-specific install instructions. Running `azd update` auto-enables the feature. - **Toggle on** (`azd config set alpha.update on`): All update features are active — `azd update` works, `azd version` shows the channel suffix, notifications say "run `azd update`." This lets us roll out to internal users first, gather feedback, and fix issues before broader availability. Once stable, the toggle can be removed and the feature enabled by default. diff --git a/cli/azd/main.go b/cli/azd/main.go index d22e4b3b961..fbcb8aabad4 100644 --- a/cli/azd/main.go +++ b/cli/azd/main.go @@ -24,7 +24,6 @@ import ( "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/internal/telemetry" "github.com/azure/azure-dev/cli/azd/internal/tracing" - "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/installer" @@ -63,56 +62,56 @@ func main() { } // Auto-update: check for applied update marker and display banner - if !internal.IsNonProdVersion() { - if fromVersion, err := update.ReadAppliedMarker(); err == nil && fromVersion != "" { - update.RemoveAppliedMarker() - fmt.Fprintln( - os.Stderr, - output.WithSuccessFormat( - "azd has been auto-updated from %s to %s", fromVersion, internal.Version)) - } - } + // if !internal.IsNonProdVersion() { + // if fromVersion, err := update.ReadAppliedMarker(); err == nil && fromVersion != "" { + // update.RemoveAppliedMarker() + // fmt.Fprintln( + // os.Stderr, + // output.WithSuccessFormat( + // "azd has been auto-updated from %s to %s", fromVersion, internal.Version)) + // } + // } // Auto-update: apply staged binary if one exists (before anything else) showedElevationWarning := false - if !internal.IsNonProdVersion() && update.HasStagedUpdate() { - applyConfigMgr := config.NewUserConfigManager(config.NewFileConfigManager(config.NewManager())) - applyCfg, cfgErr := applyConfigMgr.Load() - if cfgErr != nil { - applyCfg = config.NewEmptyConfig() - } - - applyFeatures := alpha.NewFeaturesManagerWithConfig(applyCfg) - updateCfg := update.LoadUpdateConfig(applyCfg) - - if applyFeatures.IsEnabled(update.FeatureUpdate) && updateCfg.AutoUpdate { - appliedPath, err := update.ApplyStagedUpdate() - if errors.Is(err, update.ErrNeedsElevation) { - versionStr := "a new version" - if cache, cacheErr := update.LoadCache(); cacheErr == nil && cache != nil && cache.Version != "" { - versionStr = "version " + cache.Version - } - fmt.Fprintln( - os.Stderr, - output.WithWarningFormat( - "WARNING: azd %s has been downloaded. "+ - "Run 'azd update' to apply it.", versionStr)) - showedElevationWarning = true - } else if err != nil { - log.Printf("failed to apply staged update: %v", err) - } else if appliedPath != "" { - log.Printf("applied staged update, re-executing: %s", appliedPath) - update.WriteAppliedMarker(internal.Version) - if err := reExec(appliedPath); err != nil { - log.Printf("re-exec failed: %v, continuing with current binary", err) - } - // reExec replaces the process; if we get here it failed - } - } else { - // Feature or auto-update was disabled after staging — clean up - update.CleanStagedUpdate() - } - } + // if !internal.IsNonProdVersion() && update.HasStagedUpdate() { + // applyConfigMgr := config.NewUserConfigManager(config.NewFileConfigManager(config.NewManager())) + // applyCfg, cfgErr := applyConfigMgr.Load() + // if cfgErr != nil { + // applyCfg = config.NewEmptyConfig() + // } + + // applyFeatures := alpha.NewFeaturesManagerWithConfig(applyCfg) + // updateCfg := update.LoadUpdateConfig(applyCfg) + + // if applyFeatures.IsEnabled(update.FeatureUpdate) && updateCfg.AutoUpdate { + // appliedPath, err := update.ApplyStagedUpdate() + // if errors.Is(err, update.ErrNeedsElevation) { + // versionStr := "a new version" + // if cache, cacheErr := update.LoadCache(); cacheErr == nil && cache != nil && cache.Version != "" { + // versionStr = "version " + cache.Version + // } + // fmt.Fprintln( + // os.Stderr, + // output.WithWarningFormat( + // "WARNING: azd %s has been downloaded. "+ + // "Run 'azd update' to apply it.", versionStr)) + // showedElevationWarning = true + // } else if err != nil { + // log.Printf("failed to apply staged update: %v", err) + // } else if appliedPath != "" { + // log.Printf("applied staged update, re-executing: %s", appliedPath) + // update.WriteAppliedMarker(internal.Version) + // if err := reExec(appliedPath); err != nil { + // log.Printf("re-exec failed: %v, continuing with current binary", err) + // } + // // reExec replaces the process; if we get here it failed + // } + // } else { + // // Feature or auto-update was disabled after staging — clean up + // update.CleanStagedUpdate() + // } + // } latest := make(chan *update.VersionInfo) bgCtx, bgCancel := context.WithTimeout(context.Background(), 60*time.Second) @@ -239,16 +238,17 @@ func fetchLatestVersion(ctx context.Context, result chan<- *update.VersionInfo) // Auto-update: if enabled and an update is available, stage the new binary in the background. // Skip in CI environments and package manager installs. - if cfg.AutoUpdate && versionInfo.HasUpdate && !update.IsPackageManagerInstall() && - !resource.IsRunningOnCI() { - featureManager := alpha.NewFeaturesManagerWithConfig(userConfig) - if featureManager.IsEnabled(update.FeatureUpdate) { - log.Printf("auto-update: staging update to %s", versionInfo.Version) - if stageErr := mgr.StageUpdate(ctx, cfg); stageErr != nil { - log.Printf("auto-update: staging failed: %v", stageErr) - } - } - } + // Comment it out for now until we support auto update + // if cfg.AutoUpdate && versionInfo.HasUpdate && !update.IsPackageManagerInstall() && + // !resource.IsRunningOnCI() { + // featureManager := alpha.NewFeaturesManagerWithConfig(userConfig) + // if featureManager.IsEnabled(update.FeatureUpdate) { + // log.Printf("auto-update: staging update to %s", versionInfo.Version) + // if stageErr := mgr.StageUpdate(ctx, cfg); stageErr != nil { + // log.Printf("auto-update: staging failed: %v", stageErr) + // } + // } + // } result <- versionInfo } diff --git a/cli/azd/resources/alpha_features.yaml b/cli/azd/resources/alpha_features.yaml index 84b84e4e0f5..d6141b01ebd 100644 --- a/cli/azd/resources/alpha_features.yaml +++ b/cli/azd/resources/alpha_features.yaml @@ -15,4 +15,4 @@ - id: language.custom description: "Enables support for services to use custom language." - id: update - description: "Enables the azd update command for self-updating azd and channel management." + description: "Enables the azd update command for channel management." From a293a880283b8c975a3fec9008207f8ad91677ed Mon Sep 17 00:00:00 2001 From: hemarina Date: Tue, 24 Mar 2026 13:19:37 -0700 Subject: [PATCH 4/7] golangci-lint --- cli/azd/cmd/update.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/azd/cmd/update.go b/cli/azd/cmd/update.go index 54be6c7a825..0539ed71ef0 100644 --- a/cli/azd/cmd/update.go +++ b/cli/azd/cmd/update.go @@ -27,8 +27,8 @@ import ( ) type updateFlags struct { - channel string - autoUpdate string + channel string + // autoUpdate string checkIntervalHours int global *internal.GlobalCommandOptions } From 439a4f8681f144a44b8d35111d611d9d9569b5ed Mon Sep 17 00:00:00 2001 From: hemarina Date: Tue, 24 Mar 2026 13:30:50 -0700 Subject: [PATCH 5/7] golangci --- cli/azd/main.go | 50 ++++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/cli/azd/main.go b/cli/azd/main.go index fbcb8aabad4..e5217ec6b79 100644 --- a/cli/azd/main.go +++ b/cli/azd/main.go @@ -7,7 +7,6 @@ package main import ( "context" - "errors" "fmt" "io" "log" @@ -16,7 +15,6 @@ import ( "path/filepath" "runtime" "strconv" - "syscall" "time" azcorelog "github.com/Azure/azure-sdk-for-go/sdk/azcore/log" @@ -411,30 +409,30 @@ func platformUpgradeText() string { // reExec replaces the current process with the binary at the given path, // passing the same arguments. On Unix, this uses syscall.Exec to replace // the process in-place. On Windows, it spawns a new process and exits. -func reExec(binaryPath string) error { - args := os.Args - args[0] = binaryPath - - if runtime.GOOS == "windows" { - // Windows doesn't support exec-style process replacement. - // Spawn the new binary and exit. - // #nosec G204 -- binaryPath is the staged azd binary we just verified - cmd := exec.Command(binaryPath, args[1:]...) //nolint:gosec - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - os.Exit(exitErr.ExitCode()) - } - return err - } - os.Exit(0) - } - - return syscall.Exec(binaryPath, args, os.Environ()) //nolint:gosec -} +// func reExec(binaryPath string) error { +// args := os.Args +// args[0] = binaryPath + +// if runtime.GOOS == "windows" { +// // Windows doesn't support exec-style process replacement. +// // Spawn the new binary and exit. +// // #nosec G204 -- binaryPath is the staged azd binary we just verified +// cmd := exec.Command(binaryPath, args[1:]...) //nolint:gosec +// cmd.Stdin = os.Stdin +// cmd.Stdout = os.Stdout +// cmd.Stderr = os.Stderr +// if err := cmd.Run(); err != nil { +// var exitErr *exec.ExitError +// if errors.As(err, &exitErr) { +// os.Exit(exitErr.ExitCode()) +// } +// return err +// } +// os.Exit(0) +// } + +// return syscall.Exec(binaryPath, args, os.Environ()) //nolint:gosec +// } func startBackgroundUploadProcess() error { // The background upload process executable is ourself From 19014c5733f6456bef97cffd4b1d791df7734c95 Mon Sep 17 00:00:00 2001 From: hemarina Date: Tue, 24 Mar 2026 17:43:05 -0700 Subject: [PATCH 6/7] remove auto update part instead of comment out --- cli/azd/cmd/update.go | 18 --------- cli/azd/main.go | 92 ------------------------------------------- 2 files changed, 110 deletions(-) diff --git a/cli/azd/cmd/update.go b/cli/azd/cmd/update.go index 0539ed71ef0..fe08e0c6051 100644 --- a/cli/azd/cmd/update.go +++ b/cli/azd/cmd/update.go @@ -28,7 +28,6 @@ import ( type updateFlags struct { channel string - // autoUpdate string checkIntervalHours int global *internal.GlobalCommandOptions } @@ -48,12 +47,6 @@ func (f *updateFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandO "", "Update channel: stable or daily.", ) - // local.StringVar( - // &f.autoUpdate, - // "auto-update", - // "", - // "Enable or disable auto-update: on or off.", - // ) local.IntVar( &f.checkIntervalHours, "check-interval-hours", @@ -320,17 +313,6 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) { func (a *updateAction) persistNonChannelFlags(cfg config.Config) (bool, error) { changed := false - // if a.flags.autoUpdate != "" { - // enabled := a.flags.autoUpdate == "on" - // if a.flags.autoUpdate != "on" && a.flags.autoUpdate != "off" { - // return false, fmt.Errorf("invalid auto-update value %q, must be \"on\" or \"off\"", a.flags.autoUpdate) - // } - // if err := update.SaveAutoUpdate(cfg, enabled); err != nil { - // return false, err - // } - // changed = true - // } - if a.flags.checkIntervalHours > 0 { if err := update.SaveCheckIntervalHours(cfg, a.flags.checkIntervalHours); err != nil { return false, err diff --git a/cli/azd/main.go b/cli/azd/main.go index e5217ec6b79..88baff5a054 100644 --- a/cli/azd/main.go +++ b/cli/azd/main.go @@ -59,57 +59,7 @@ func main() { ctx = tracing.ContextFromEnv(ctx) } - // Auto-update: check for applied update marker and display banner - // if !internal.IsNonProdVersion() { - // if fromVersion, err := update.ReadAppliedMarker(); err == nil && fromVersion != "" { - // update.RemoveAppliedMarker() - // fmt.Fprintln( - // os.Stderr, - // output.WithSuccessFormat( - // "azd has been auto-updated from %s to %s", fromVersion, internal.Version)) - // } - // } - - // Auto-update: apply staged binary if one exists (before anything else) showedElevationWarning := false - // if !internal.IsNonProdVersion() && update.HasStagedUpdate() { - // applyConfigMgr := config.NewUserConfigManager(config.NewFileConfigManager(config.NewManager())) - // applyCfg, cfgErr := applyConfigMgr.Load() - // if cfgErr != nil { - // applyCfg = config.NewEmptyConfig() - // } - - // applyFeatures := alpha.NewFeaturesManagerWithConfig(applyCfg) - // updateCfg := update.LoadUpdateConfig(applyCfg) - - // if applyFeatures.IsEnabled(update.FeatureUpdate) && updateCfg.AutoUpdate { - // appliedPath, err := update.ApplyStagedUpdate() - // if errors.Is(err, update.ErrNeedsElevation) { - // versionStr := "a new version" - // if cache, cacheErr := update.LoadCache(); cacheErr == nil && cache != nil && cache.Version != "" { - // versionStr = "version " + cache.Version - // } - // fmt.Fprintln( - // os.Stderr, - // output.WithWarningFormat( - // "WARNING: azd %s has been downloaded. "+ - // "Run 'azd update' to apply it.", versionStr)) - // showedElevationWarning = true - // } else if err != nil { - // log.Printf("failed to apply staged update: %v", err) - // } else if appliedPath != "" { - // log.Printf("applied staged update, re-executing: %s", appliedPath) - // update.WriteAppliedMarker(internal.Version) - // if err := reExec(appliedPath); err != nil { - // log.Printf("re-exec failed: %v, continuing with current binary", err) - // } - // // reExec replaces the process; if we get here it failed - // } - // } else { - // // Feature or auto-update was disabled after staging — clean up - // update.CleanStagedUpdate() - // } - // } latest := make(chan *update.VersionInfo) bgCtx, bgCancel := context.WithTimeout(context.Background(), 60*time.Second) @@ -234,20 +184,6 @@ func fetchLatestVersion(ctx context.Context, result chan<- *update.VersionInfo) return } - // Auto-update: if enabled and an update is available, stage the new binary in the background. - // Skip in CI environments and package manager installs. - // Comment it out for now until we support auto update - // if cfg.AutoUpdate && versionInfo.HasUpdate && !update.IsPackageManagerInstall() && - // !resource.IsRunningOnCI() { - // featureManager := alpha.NewFeaturesManagerWithConfig(userConfig) - // if featureManager.IsEnabled(update.FeatureUpdate) { - // log.Printf("auto-update: staging update to %s", versionInfo.Version) - // if stageErr := mgr.StageUpdate(ctx, cfg); stageErr != nil { - // log.Printf("auto-update: staging failed: %v", stageErr) - // } - // } - // } - result <- versionInfo } @@ -406,34 +342,6 @@ func platformUpgradeText() string { return "visit https://aka.ms/azd/upgrade" } -// reExec replaces the current process with the binary at the given path, -// passing the same arguments. On Unix, this uses syscall.Exec to replace -// the process in-place. On Windows, it spawns a new process and exits. -// func reExec(binaryPath string) error { -// args := os.Args -// args[0] = binaryPath - -// if runtime.GOOS == "windows" { -// // Windows doesn't support exec-style process replacement. -// // Spawn the new binary and exit. -// // #nosec G204 -- binaryPath is the staged azd binary we just verified -// cmd := exec.Command(binaryPath, args[1:]...) //nolint:gosec -// cmd.Stdin = os.Stdin -// cmd.Stdout = os.Stdout -// cmd.Stderr = os.Stderr -// if err := cmd.Run(); err != nil { -// var exitErr *exec.ExitError -// if errors.As(err, &exitErr) { -// os.Exit(exitErr.ExitCode()) -// } -// return err -// } -// os.Exit(0) -// } - -// return syscall.Exec(binaryPath, args, os.Environ()) //nolint:gosec -// } - func startBackgroundUploadProcess() error { // The background upload process executable is ourself execPath, err := os.Executable() From 09d838c88176c062b0e9fbed40fa92e3540e9d9e Mon Sep 17 00:00:00 2001 From: hemarina Date: Tue, 24 Mar 2026 18:27:01 -0700 Subject: [PATCH 7/7] gofmt --- cli/azd/cmd/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/cmd/update.go b/cli/azd/cmd/update.go index fe08e0c6051..cdfcf3134e2 100644 --- a/cli/azd/cmd/update.go +++ b/cli/azd/cmd/update.go @@ -27,7 +27,7 @@ import ( ) type updateFlags struct { - channel string + channel string checkIntervalHours int global *internal.GlobalCommandOptions }