Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 3 additions & 22 deletions cli/azd/cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (

type updateFlags struct {
channel string
autoUpdate string
checkIntervalHours int
global *internal.GlobalCommandOptions
}
Expand All @@ -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",
Expand Down Expand Up @@ -126,7 +119,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",
})
}

Expand Down Expand Up @@ -315,22 +308,11 @@ 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
}

if a.flags.checkIntervalHours > 0 {
if err := update.SaveCheckIntervalHours(cfg, a.flags.checkIntervalHours); err != nil {
return false, err
Expand All @@ -343,6 +325,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
}
156 changes: 71 additions & 85 deletions cli/azd/docs/design/azd-update.md
Original file line number Diff line number Diff line change
@@ -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)

---

Expand All @@ -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.

Expand All @@ -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

---

Expand Down Expand Up @@ -114,10 +113,11 @@ 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"
azd config set updates.checkIntervalHours 4
```

Channel is set via `azd update --channel <stable|daily>` (which persists the choice to `updates.channel` config). Default channel is `stable`.
Expand Down Expand Up @@ -159,19 +159,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`.
Expand All @@ -180,14 +177,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)
Expand All @@ -198,90 +197,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 <path> /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 <binary>` — 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 <path> /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 <channel> --install-folder <current-install-dir> --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)
Expand Down Expand Up @@ -311,6 +297,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
Expand Down Expand Up @@ -365,15 +353,13 @@ 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 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.

### 9. Update Banner Suppression

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.

---
Loading
Loading