From dc33b4c2264aec102f7b668ee6920111ece39f02 Mon Sep 17 00:00:00 2001 From: Rajesh Kamal Date: Thu, 26 Feb 2026 11:34:13 -0800 Subject: [PATCH 1/6] Design doc for azd update and auto-update Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/docs/design/azd-update.md | 321 ++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 cli/azd/docs/design/azd-update.md diff --git a/cli/azd/docs/design/azd-update.md b/cli/azd/docs/design/azd-update.md new file mode 100644 index 00000000000..ec36d2282e0 --- /dev/null +++ b/cli/azd/docs/design/azd-update.md @@ -0,0 +1,321 @@ +# Design: `azd update`, Auto-Update & Channel Management + +**Epic**: [#6721](https://github.com/Azure/azure-dev/issues/6721) +**Status**: Draft + +--- + +## Overview + +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 + +The feature will ship as a hidden command initially for internal testing before being advertised publicly. + +--- + +## 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) +- Avoid disruption to CI/CD pipelines +- Respect platform install methods (Homebrew, winget, choco, scripts) + +--- + +## Existing Infrastructure + +### Install Method Tracking + +azd already tracks how it was installed via `.installed-by.txt` placed alongside the binary: + +| Installer | Value | Default Location | +|-----------|-------|------------------| +| Bash script | `install-azd.sh` | `/opt/microsoft/azd/` | +| PowerShell script | `install-azd.ps1` | `C:\Program Files\Azure Dev CLI\` (customizable via `-InstallFolder`) | +| Homebrew | `brew` | Homebrew prefix (e.g., `/usr/local/Cellar/azd/`) | +| Chocolatey | `choco` | `C:\Program Files\Azure Dev CLI\` (via MSI) | +| Winget | `winget` | `C:\Program Files\Azure Dev CLI\` (via MSI) | +| Debian pkg | `deb` | `/opt/microsoft/azd/` | +| RPM pkg | `rpm` | `/opt/microsoft/azd/` | +| MSI | `msi` | `C:\Program Files\Azure Dev CLI\` | + +`.installed-by.txt` is always placed in the same directory as the azd binary. At runtime, azd locates itself via `os.Executable()` and reads `.installed-by.txt` from that directory. + +**Code**: `cli/azd/pkg/installer/installed_by.go` + +### Version Check + +- **Endpoint**: `https://aka.ms/azure-dev/versions/cli/latest` — returns latest stable semver (plaintext) +- **Logic**: `main.go` → `fetchLatestVersion()` — async check at startup, cached in `~/.azd/update-check.json` +- **Skip**: `AZD_SKIP_UPDATE_CHECK=true` disables the check +- Already shows platform-specific upgrade instructions based on install method + +**Current cache format** (`~/.azd/update-check.json`): +```json +{"version":"1.23.6","expiresOn":"2026-02-26T01:24:50Z"} +``` + +**New cache format** (extended for channel + daily support): +```json +{ + "channel": "daily", + "version": "1.24.0-beta.1", + "buildNumber": 98770, + "expiresOn": "2026-02-26T08:00:00Z" +} +``` + +- `channel`: `"stable"` or `"daily"`. Missing field defaults to `"stable"` (backward compatible with existing cache files). +- `buildNumber`: Only used for daily builds (semver alone can't distinguish dailies). Missing field triggers a fresh check for daily users. +- `expiresOn`: Channel-dependent TTL — defaults to 24h for stable, 4h for daily. Configurable via `azd config set updates.checkIntervalHours `. + +### Build Artifacts + +- **Stable**: Published to GitHub Releases + Azure Blob Storage (`release/stable/`, `release/{VERSION}/`) + package managers (brew, winget, choco, apt, dnf) +- **Daily**: Published to Azure Blob Storage only (`release/daily/`), overwritten each build. Archived at `daily/archive/{BuildId}-{CommitSHA}/` +- **Base URL**: `https://azuresdkartifacts.z5.web.core.windows.net/azd/standalone/` + +### Versioning Scheme + +| State | Version Format | Example | +|-------|---------------|---------| +| Stable release | `X.Y.Z` | `1.23.6` | +| Development (daily) | `X.Y.Z-beta.1` | `1.24.0-beta.1` | + +After each stable release, `cli/version.txt` is immediately bumped to the next beta. Daily builds all carry this beta version until the next release. + +### Reusable Existing Patterns + +The extension manager (`pkg/extensions/manager.go`) already implements a nearly identical download-verify-install flow. Reuse these existing utilities rather than building new ones: + +| Capability | Existing Code | Notes | +|-----------|---------------|-------| +| **HTTP download + progress** | `pkg/input/progress_log.go`, `pkg/async/progress.go` | Terminal-based progress display | +| **Checksum verification** | `pkg/extensions/manager.go` → `validateChecksum()` | Supports SHA256/SHA512 | +| **Staging + temp file mgmt** | `pkg/extensions/manager.go` → `downloadFromRemote()` | Downloads to `os.TempDir()`, cleanup via `defer os.Remove()` | +| **Shelling out to tools** | `pkg/exec/command_runner.go` → `CommandRunner` interface | Wraps `os/exec` with context support, I/O routing | +| **Config nested keys** | `pkg/config/config.go` → `Get(path)`, `GetString(path)` | Dotted path traversal (e.g., `updates.channel`) | +| **Hidden commands** | `cmd/build.go`, `cmd/auth_token.go` | `Hidden: true` on `cobra.Command` | +| **Semver comparison** | `blang/semver/v4` (main.go), `Masterminds/semver/v3` (extensions) | Already used for version check | +| **User confirmation** | `pkg/ux/confirm.go` → `NewConfirm()` | Standard `[y/N]` prompt pattern | +| **Binary self-location** | `pkg/installer/installed_by.go` → `os.Executable()` | Resolves symlinks, finds install dir | +| **Background goroutine** | `main.go` → `fetchLatestVersion()` | Non-blocking startup check pattern | +| **CI detection** | `internal/tracing/resource/ci.go` → `IsRunningOnCI()` | Detects GitHub Actions, Azure Pipelines, Jenkins, etc. | + +--- + +## Design + +### 1. Configuration + +Two new config keys via `azd config`: + +```bash +azd config set updates.autoUpdate on # or "off" (default: off) +``` + +Channel is set via `azd update --channel ` (which persists the choice to `updates.channel` config). Default channel is `stable`. + +These follow the existing convention of `"on"/"off"` for boolean-like config values (consistent with alpha features). + +### 2. Daily Build Version Tracking + +**Problem**: Daily builds all share the same semver (e.g., `1.24.0-beta.1`), so version comparison alone can't determine if a newer daily exists. + +**Solution**: Upload a `version.txt` to `release/daily/` containing the Azure DevOps `$(Build.BuildId)` (monotonically increasing integer): + +``` +1.24.0-beta.1 +98770 +``` + +**Pipeline change**: Add a step in the daily publish pipeline to write and upload this file alongside the binaries. Same overwrite behavior — zero storage impact. + +**Client comparison**: Cache the build number locally (extend existing `~/.azd/update-check.json`). Compare local build number against remote — higher number means update available. + +### 3. `azd update` Command + +A new command (initially hidden) that updates the azd binary. + +**Usage**: +```bash +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` + +**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`. + +**Update strategy based on install method**: + +| Install Method | Strategy | +|----------------|----------| +| `brew` | Shell out: `brew upgrade azd` | +| `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 | + +> **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) + +Follows the same download-verify-install pattern as the extension manager (`pkg/extensions/manager.go`): + +``` +1. Check current channel config (stable or daily) +2. Fetch remote version info (always fresh — ignores cache for manual update) + - Stable: GET https://aka.ms/azure-dev/versions/cli/latest + - Daily: GET release/daily/version.txt (semver + build number) +3. Compare with local version (using existing blang/semver library) + - Stable: semver comparison + - Daily: build number comparison +4. If no update available → "You're up to date" +5. Download new binary to ~/.azd/staging/azd-new (with progress bar via pkg/input/progress_log.go) +6. Verify integrity (reuse pkg/extensions/manager.go validateChecksum()) +7. Replace binary at install location + - If install location is user-writable → direct move + - If install location needs elevation → `sudo mv` (user sees OS password prompt) +8. Done — new version takes effect on next invocation +``` + +#### Elevation Handling + +Most install methods write to system directories requiring elevation: + +| Location | Needs Elevation | +|----------|----------------| +| `/opt/microsoft/azd/` (Linux script) | Yes — `sudo mv` | +| `C:\Program Files\` (Windows MSI) | Yes — UAC | +| Homebrew prefix | No — user-writable | +| User home dirs | No | + +For `sudo`, azd passes through stdin/stdout so the user sees the standard OS password prompt. Use existing `CommandRunner` (`pkg/exec/command_runner.go`) for exec: + +```go +cmd := exec.Command("sudo", "mv", stagedBinary, currentBinaryPath) +cmd.Stdin = os.Stdin +cmd.Stdout = os.Stdout +cmd.Stderr = os.Stderr +``` + +One password prompt total (download to staging is in user space, only the final move needs elevation). + +### 4. Auto-Update + +When `updates.autoUpdate` is set to `on`: + +**Cache TTL** (channel-dependent): +- Stable: 24h (releases are infrequent) +- Daily: 4h (builds land frequently) + +The check is a cheap HTTP GET; downloads only happen when a newer version exists. + +**Flow (download + swap on next startup)**: + +``` +Startup (every azd invocation): +1. Check AZD_SKIP_UPDATE_CHECK / CI env vars → skip if set +2. Check non-interactive terminal → skip if detected +3. Background goroutine: check version (respecting channel-dependent cache TTL) +4. If newer version available → download to ~/.azd/staging/ +5. On NEXT startup: detect staged binary → swap → continue execution +6. Display banner: "azd has been updated to version X.Y.Z" +``` + +This approach is used universally across platforms for consistency (avoids the Windows binary-locking issue). + +**CI/Non-Interactive Detection**: Auto-update is disabled when running in CI/CD or non-interactive environments. Use the existing `resource.IsRunningOnCI()` detection (supports GitHub Actions, Azure Pipelines, Jenkins, GitLab CI, CircleCI, and others) and `--no-prompt` flag. This is consistent with how azd already disables interactive prompts in these environments. + +Skip auto-update when: +- `resource.IsRunningOnCI()` returns true +- `--no-prompt` flag is set +- `AZD_SKIP_UPDATE_CHECK=true` + +### 5. Channel Switching + +#### Same Install Method (Script/MSI) + +Switching channels is just changing the download source: + +```bash +azd update --channel daily +# Persists channel config and updates from release/daily/ instead of release/stable/ +``` + +**Daily → Stable downgrade warning** (using existing `pkg/ux/confirm.go` → `NewConfirm()`): +``` +⚠ You're currently on daily build 1.24.0-beta.1 (build 98770). + Switching to stable will downgrade you to 1.23.6. + Continue? [y/N] +``` + +#### Cross Install Method + +Switching between a package manager and direct installs is **not supported** via `azd update`. Users must manually uninstall and reinstall: + +| Scenario | Guidance | +|----------|----------| +| Package manager → daily | Show: "Daily builds aren't available via {brew/winget/choco}. Uninstall with `{uninstall command}`, then install daily with `curl -fsSL https://aka.ms/install-azd.sh \| bash -s -- --version daily`" | +| Script/daily → package manager | Show: "To switch to {brew/winget/choco}, first uninstall the current version, then install via your package manager." | + +This avoids the silent symlink overwrite problem that exists today with conflicting install methods. + +**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 + +Extend `azd version` to show channel and update info: + +``` +azd version 1.23.6 (stable) +``` + +``` +azd version 1.24.0-beta.1 (daily, build 98770) +``` + +### 7. Telemetry + +Leverage existing azd telemetry infrastructure (OpenTelemetry) — new commands and flags are automatically tracked. Additionally track: +- Update success/failure outcomes +- Update method used (package manager vs direct binary download) +- Channel distribution (stable vs daily) +- Auto-update opt-in rate + +**Result/Error Codes**: + +| Code | Meaning | +|------|---------| +| `update.success` | Update completed successfully | +| `update.alreadyUpToDate` | No update available, already on latest | +| `update.downloadFailed` | Failed to download binary from remote | +| `update.checksumMismatch` | Downloaded binary failed integrity verification | +| `update.elevationRequired` | Update requires elevation and user declined | +| `update.elevationFailed` | Elevation prompt (sudo/UAC) failed | +| `update.replaceFailed` | Failed to replace binary at install location | +| `update.packageManagerFailed` | Package manager command (brew/winget/choco) failed | +| `update.versionCheckFailed` | Failed to fetch remote version info | +| `update.unsupportedInstallMethod` | Unknown or unsupported install method | +| `update.channelSwitchDowngrade` | User declined downgrade when switching channels | +| `update.skippedCI` | Skipped due to CI/non-interactive environment | + +--- + From f519deadcc4bbfb87ccb82a4ce71feee7c0cc407 Mon Sep 17 00:00:00 2001 From: Rajesh Kamal Date: Thu, 26 Feb 2026 22:52:46 -0800 Subject: [PATCH 2/6] Update design doc with implementation details - Daily version format: single-line 1.24.0-beta.1-daily.5935787 - Code signing verification (macOS codesign, Windows Authenticode) - Elevation-aware auto-update with graceful fallback - Alpha feature toggle (alpha.update) for safe rollout - Telemetry fields and error codes including codeSignatureInvalid - Two-phase auto-update flow (stage + apply with re-exec) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/docs/design/azd-update.md | 133 ++++++++++++++++++++---------- 1 file changed, 89 insertions(+), 44 deletions(-) diff --git a/cli/azd/docs/design/azd-update.md b/cli/azd/docs/design/azd-update.md index ec36d2282e0..96dc8853e7c 100644 --- a/cli/azd/docs/design/azd-update.md +++ b/cli/azd/docs/design/azd-update.md @@ -13,7 +13,7 @@ Today, when a new version of `azd` is available, users see a warning message wit 2. **Auto-update** — opt-in background updates applied at next startup 3. **Channel management** — ability to switch between `stable` and `daily` builds -The feature will ship as a hidden command initially for internal testing before being advertised publicly. +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. --- @@ -24,6 +24,7 @@ The feature will ship as a hidden command initially for internal testing before - Preserve user control (opt-out, 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 --- @@ -64,14 +65,14 @@ azd already tracks how it was installed via `.installed-by.txt` placed alongside ```json { "channel": "daily", - "version": "1.24.0-beta.1", - "buildNumber": 98770, - "expiresOn": "2026-02-26T08:00:00Z" + "version": "1.24.0-beta.1-daily.5935787", + "buildNumber": 5935787, + "expiresOn": "2026-02-27T08:00:00Z" } ``` - `channel`: `"stable"` or `"daily"`. Missing field defaults to `"stable"` (backward compatible with existing cache files). -- `buildNumber`: Only used for daily builds (semver alone can't distinguish dailies). Missing field triggers a fresh check for daily users. +- `buildNumber`: Extracted from the daily version string's `daily.N` suffix. Used to compare daily builds since they share a base semver. - `expiresOn`: Channel-dependent TTL — defaults to 24h for stable, 4h for daily. Configurable via `azd config set updates.checkIntervalHours `. ### Build Artifacts @@ -85,9 +86,9 @@ azd already tracks how it was installed via `.installed-by.txt` placed alongside | State | Version Format | Example | |-------|---------------|---------| | Stable release | `X.Y.Z` | `1.23.6` | -| Development (daily) | `X.Y.Z-beta.1` | `1.24.0-beta.1` | +| Daily build | `X.Y.Z-beta.1-daily.{BuildId}` | `1.24.0-beta.1-daily.5935787` | -After each stable release, `cli/version.txt` is immediately bumped to the next beta. Daily builds all carry this beta version until the next release. +After each stable release, `cli/version.txt` is immediately bumped to the next beta (e.g. `1.24.0-beta.1`). The CI pipeline appends `-daily.{BuildId}` for daily builds, where `BuildId` is the Azure DevOps `$(Build.BuildId)` — a monotonically increasing integer that lets us tell daily builds apart even though they share the same base semver. ### Reusable Existing Patterns @@ -125,18 +126,29 @@ These follow the existing convention of `"on"/"off"` for boolean-like config val ### 2. Daily Build Version Tracking -**Problem**: Daily builds all share the same semver (e.g., `1.24.0-beta.1`), so version comparison alone can't determine if a newer daily exists. +**Problem**: Daily builds share a base semver (e.g., `1.24.0-beta.1`), so version comparison alone can't tell if a newer daily exists. -**Solution**: Upload a `version.txt` to `release/daily/` containing the Azure DevOps `$(Build.BuildId)` (monotonically increasing integer): +**Solution**: The CI pipeline publishes a `version.txt` to `release/daily/` containing the full daily version string: ``` -1.24.0-beta.1 -98770 +1.24.0-beta.1-daily.5935787 ``` -**Pipeline change**: Add a step in the daily publish pipeline to write and upload this file alongside the binaries. Same overwrite behavior — zero storage impact. +This is the same version string baked into the binary at build time. The build number (`5935787`) is the Azure DevOps `$(Build.BuildId)` — monotonically increasing, so a higher number always means a newer build. -**Client comparison**: Cache the build number locally (extend existing `~/.azd/update-check.json`). Compare local build number against remote — higher number means update available. +**Pipeline change**: Add a step in the daily publish pipeline (`Publish_Continuous_Deployment`) to write `$(CLI_VERSION)` to `version.txt` and upload alongside the binaries. + +**Client comparison**: Parse the build number from the `daily.N` suffix. Compare local build number (from the running binary's version string) against remote — higher number means update available. + +**Cache format** (`~/.azd/update-check.json`): +```json +{ + "channel": "daily", + "version": "1.24.0-beta.1-daily.5935787", + "buildNumber": 5935787, + "expiresOn": "2026-02-27T08:00:00Z" +} +``` ### 3. `azd update` Command @@ -177,25 +189,33 @@ All flags persist their values to config, which can also be set directly via `az #### Direct Binary Update Flow (Script/MSI Users) -Follows the same download-verify-install pattern as the extension manager (`pkg/extensions/manager.go`): - ``` 1. Check current channel config (stable or daily) 2. Fetch remote version info (always fresh — ignores cache for manual update) - Stable: GET https://aka.ms/azure-dev/versions/cli/latest - - Daily: GET release/daily/version.txt (semver + build number) -3. Compare with local version (using existing blang/semver library) - - Stable: semver comparison - - Daily: build number comparison + - Daily: GET release/daily/version.txt (full version string, e.g. 1.24.0-beta.1-daily.5935787) +3. Compare with local version + - 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 new binary to ~/.azd/staging/azd-new (with progress bar via pkg/input/progress_log.go) -6. Verify integrity (reuse pkg/extensions/manager.go validateChecksum()) -7. Replace binary at install location - - If install location is user-writable → direct move - - If install location needs elevation → `sudo mv` (user sees OS password prompt) -8. Done — new version takes effect on next invocation +5. Download new binary to temp dir (with progress bar) +6. Verify SHA256 checksum (downloaded from {binaryURL}.sha256) +7. Verify code signature (macOS: codesign, Windows: Get-AuthenticodeSignature) +8. Replace binary at install location + - If install location is user-writable → direct copy + - If install location needs elevation → sudo cp (user sees OS password prompt) +9. Done — new version takes effect on next invocation ``` +#### Code Signing Verification + +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) + +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: @@ -228,25 +248,32 @@ When `updates.autoUpdate` is set to `on`: The check is a cheap HTTP GET; downloads only happen when a newer version exists. -**Flow (download + swap on next startup)**: +**Flow (two-phase: stage in background, apply on next startup)**: ``` -Startup (every azd invocation): +Phase 1 — Stage (background goroutine during any azd invocation): 1. Check AZD_SKIP_UPDATE_CHECK / CI env vars → skip if set -2. Check non-interactive terminal → skip if detected -3. Background goroutine: check version (respecting channel-dependent cache TTL) -4. If newer version available → download to ~/.azd/staging/ -5. On NEXT startup: detect staged binary → swap → continue execution -6. Display banner: "azd has been updated to version X.Y.Z" +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. Try to copy over current binary + - If writable (user home, homebrew prefix) → swap, re-exec, show success banner + - If permission denied (system dir like /opt/microsoft/azd/) → skip, show warning +3. On success: write marker file, re-exec with same args, display banner +4. On permission denied: show "WARNING: New update downloaded. Run 'azd update' to apply." ``` -This approach is used universally across platforms for consistency (avoids the Windows binary-locking issue). +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-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). -**CI/Non-Interactive Detection**: Auto-update is disabled when running in CI/CD or non-interactive environments. Use the existing `resource.IsRunningOnCI()` detection (supports GitHub Actions, Azure Pipelines, Jenkins, GitLab CI, CircleCI, and others) and `--no-prompt` flag. This is consistent with how azd already disables interactive prompts in these environments. +**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`. Skip auto-update when: - `resource.IsRunningOnCI()` returns true -- `--no-prompt` flag is set - `AZD_SKIP_UPDATE_CHECK=true` ### 5. Channel Switching @@ -260,9 +287,9 @@ azd update --channel daily # Persists channel config and updates from release/daily/ instead of release/stable/ ``` -**Daily → Stable downgrade warning** (using existing `pkg/ux/confirm.go` → `NewConfirm()`): +**Daily → Stable downgrade warning**: ``` -⚠ You're currently on daily build 1.24.0-beta.1 (build 98770). +⚠ You're currently on version 1.24.0-beta.1-daily.5935787. Switching to stable will downgrade you to 1.23.6. Continue? [y/N] ``` @@ -282,23 +309,29 @@ This avoids the silent symlink overwrite problem that exists today with conflict ### 6. `azd version` Output -Extend `azd version` to show channel and update info: +When the update feature is enabled, `azd version` shows the channel: ``` azd version 1.23.6 (stable) ``` ``` -azd version 1.24.0-beta.1 (daily, build 98770) +azd version 1.24.0-beta.1-daily.5935787 (daily, build 5935787) ``` +When the feature toggle is off, `azd version` output stays unchanged — no suffix, no channel info. + ### 7. Telemetry -Leverage existing azd telemetry infrastructure (OpenTelemetry) — new commands and flags are automatically tracked. Additionally track: -- Update success/failure outcomes -- Update method used (package manager vs direct binary download) -- Channel distribution (stable vs daily) -- Auto-update opt-in rate +Uses the existing azd telemetry infrastructure (OpenTelemetry). New telemetry fields tracked on every update operation: + +| Field | Description | +|-------|-------------| +| `update.from_version` | Version before update | +| `update.to_version` | Target version | +| `update.channel` | `stable` or `daily` | +| `update.method` | How the update was performed (e.g. `brew`, `direct`, `winget`) | +| `update.result` | Result code (see below) | **Result/Error Codes**: @@ -308,6 +341,7 @@ Leverage existing azd telemetry infrastructure (OpenTelemetry) — new commands | `update.alreadyUpToDate` | No update available, already on latest | | `update.downloadFailed` | Failed to download binary from remote | | `update.checksumMismatch` | Downloaded binary failed integrity verification | +| `update.codeSignatureInvalid` | Code signature verification failed | | `update.elevationRequired` | Update requires elevation and user declined | | `update.elevationFailed` | Elevation prompt (sudo/UAC) failed | | `update.replaceFailed` | Failed to replace binary at install location | @@ -317,5 +351,16 @@ Leverage existing azd telemetry infrastructure (OpenTelemetry) — new commands | `update.channelSwitchDowngrade` | User declined downgrade when switching channels | | `update.skippedCI` | Skipped due to CI/non-interactive environment | +These codes are integrated into azd's `MapError` pipeline, so update failures show up properly in telemetry dashboards alongside other command errors. + +### 8. Feature Toggle (Alpha Gate) + +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`." + +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. + --- From 8aec21ae70399cb25bb80e8fb582fdc06baa56e7 Mon Sep 17 00:00:00 2001 From: Rajesh Kamal Date: Thu, 26 Feb 2026 23:00:46 -0800 Subject: [PATCH 3/6] Add codesign, Authenticode, syscall to cspell dictionary Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/.vscode/cspell.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 3e2685ef4a0..4c535ea1d57 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -1,6 +1,7 @@ import: ../../../.vscode/cspell.global.yaml words: - agentdetect + - Authenticode - azcloud - azdext - azurefd @@ -12,6 +13,7 @@ words: - cmds - Codespace - Codespaces + - codesign - cooldown - customtype - devcontainers @@ -43,6 +45,7 @@ words: - protoreflect - SNAPPROCESS - structpb + - syscall - Retryable - runcontext - surveyterm From a2021dbe086aff023a3089c21f1cb38bee60b83b Mon Sep 17 00:00:00 2001 From: Rajesh Kamal Date: Fri, 27 Feb 2026 13:01:49 -0800 Subject: [PATCH 4/6] Update design doc to match implementation - Channel switch: bidirectional confirmation prompt (not just downgrade) - Cancellation UX: plain message instead of SUCCESS banner - Staged binary verification: codesign check before apply - copyFile fsync: flush data to disk to prevent corruption - Duplicate WARNING suppression: elevation warning suppresses out-of-date banner - Banner suppression for azd update and azd config commands - Fix telemetry code: update.signatureInvalid (not codeSignatureInvalid) - Version output: shows (daily) not (daily, build N), derived from binary Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/docs/design/azd-update.md | 35 +++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/cli/azd/docs/design/azd-update.md b/cli/azd/docs/design/azd-update.md index 96dc8853e7c..3dcd66cf834 100644 --- a/cli/azd/docs/design/azd-update.md +++ b/cli/azd/docs/design/azd-update.md @@ -259,15 +259,20 @@ Phase 1 — Stage (background goroutine during any azd invocation): Phase 2 — Apply (on NEXT startup, before command execution): 1. Detect staged binary at ~/.azd/staging/azd -2. Try to copy over current binary +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 -3. On success: write marker file, re-exec with same args, display banner -4. On permission denied: show "WARNING: New update downloaded. Run 'azd update' to apply." + - 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.) ``` 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. +**Staged binary verification**: Before applying, `verifyStagedBinary()` checks the staged binary's integrity. On macOS, it runs `codesign -v --strict`. Unsigned binaries (dev builds) are allowed ("code object is not signed at all" is OK), but corrupted/truncated binaries with invalid signatures are rejected and cleaned up. This prevents crashes from partially-downloaded files left behind when a background download goroutine is interrupted. + **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). **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`. @@ -287,13 +292,13 @@ azd update --channel daily # Persists channel config and updates from release/daily/ instead of release/stable/ ``` -**Daily → Stable downgrade warning**: +**Channel switch confirmation** (any direction — daily↔stable): ``` -⚠ You're currently on version 1.24.0-beta.1-daily.5935787. - Switching to stable will downgrade you to 1.23.6. - Continue? [y/N] +? Switch from daily channel (1.24.0-beta.1-daily.5935787) to stable channel (1.23.6)? [Y/n] ``` +If the user declines, the command prints "Channel switch cancelled." (no SUCCESS banner) and exits without modifying config or downloading anything. The channel config is only persisted after confirmation. + #### Cross Install Method Switching between a package manager and direct installs is **not supported** via `azd update`. Users must manually uninstall and reinstall: @@ -312,13 +317,15 @@ This avoids the silent symlink overwrite problem that exists today with conflict When the update feature is enabled, `azd version` shows the channel: ``` -azd version 1.23.6 (stable) +azd version 1.23.6 (commit abc1234) (stable) ``` ``` -azd version 1.24.0-beta.1-daily.5935787 (daily, build 5935787) +azd version 1.24.0-beta.1-daily.5935787 (commit abc1234) (daily) ``` +The channel suffix is derived from the running binary's version string (presence of `daily.` pattern), not the configured channel. This means the output always reflects what the binary actually is. + When the feature toggle is off, `azd version` output stays unchanged — no suffix, no channel info. ### 7. Telemetry @@ -341,14 +348,14 @@ Uses the existing azd telemetry infrastructure (OpenTelemetry). New telemetry fi | `update.alreadyUpToDate` | No update available, already on latest | | `update.downloadFailed` | Failed to download binary from remote | | `update.checksumMismatch` | Downloaded binary failed integrity verification | -| `update.codeSignatureInvalid` | Code signature verification failed | +| `update.signatureInvalid` | Code signature verification failed | | `update.elevationRequired` | Update requires elevation and user declined | | `update.elevationFailed` | Elevation prompt (sudo/UAC) failed | | `update.replaceFailed` | Failed to replace binary at install location | | `update.packageManagerFailed` | Package manager command (brew/winget/choco) failed | | `update.versionCheckFailed` | Failed to fetch remote version info | | `update.unsupportedInstallMethod` | Unknown or unsupported install method | -| `update.channelSwitchDowngrade` | User declined downgrade when switching channels | +| `update.channelSwitchDowngrade` | User declined when switching channels | | `update.skippedCI` | Skipped due to CI/non-interactive environment | These codes are integrated into azd's `MapError` pipeline, so update failures show up properly in telemetry dashboards alongside other command errors. @@ -362,5 +369,11 @@ The entire update feature ships behind `alpha.update` (default: off). This means 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. + --- From a066d6b70c4c120417390fa7e01dd4ee340d6498 Mon Sep 17 00:00:00 2001 From: Rajesh Kamal Date: Fri, 27 Feb 2026 19:17:14 -0800 Subject: [PATCH 5/6] Address review feedback: remove checksum, add Windows MSI and brew guidance - Remove SHA256 checksum step (HTTPS provides integrity) - Windows: use MSI installer instead of direct binary replacement - Brew: document delegation to brew commands, no direct overwrite - Add update method column to elevation table Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/docs/design/azd-update.md | 39 ++++++++++++++----------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/cli/azd/docs/design/azd-update.md b/cli/azd/docs/design/azd-update.md index 3dcd66cf834..c599fd803d6 100644 --- a/cli/azd/docs/design/azd-update.md +++ b/cli/azd/docs/design/azd-update.md @@ -198,13 +198,14 @@ 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 new binary to temp dir (with progress bar) -6. Verify SHA256 checksum (downloaded from {binaryURL}.sha256) -7. Verify code signature (macOS: codesign, Windows: Get-AuthenticodeSignature) -8. Replace binary at install location - - If install location is user-writable → direct copy - - If install location needs elevation → sudo cp (user sees OS password prompt) -9. Done — new version takes effect on next invocation +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 ``` #### Code Signing Verification @@ -220,23 +221,19 @@ The check is fail-safe: if `codesign` or PowerShell isn't available (unlikely), Most install methods write to system directories requiring elevation: -| Location | Needs Elevation | -|----------|----------------| -| `/opt/microsoft/azd/` (Linux script) | Yes — `sudo mv` | -| `C:\Program Files\` (Windows MSI) | Yes — UAC | -| Homebrew prefix | No — user-writable | -| User home dirs | No | +| 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 azd` | +| User home dirs | No | Direct binary replacement | -For `sudo`, azd passes through stdin/stdout so the user sees the standard OS password prompt. Use existing `CommandRunner` (`pkg/exec/command_runner.go`) for exec: +**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. -```go -cmd := exec.Command("sudo", "mv", stagedBinary, currentBinaryPath) -cmd.Stdin = os.Stdin -cmd.Stdout = os.Stdout -cmd.Stderr = os.Stderr -``` +**macOS/Linux (brew)**: Homebrew tracks installed assets, so azd never overwrites brew-managed binaries directly. Same-channel updates delegate to `brew upgrade 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. -One password prompt total (download to staging is in user space, only the final move needs elevation). +**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 From b94bcde6c98790256adf13f023e10d5e9306fcd6 Mon Sep 17 00:00:00 2001 From: Rajesh Kamal Date: Fri, 27 Feb 2026 19:23:55 -0800 Subject: [PATCH 6/6] Add msiexec to cspell dictionary Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/.vscode/cspell.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 4c535ea1d57..d9418a205d4 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -28,6 +28,7 @@ words: - OPENCODE - opencode - grpcbroker + - msiexec - nosec - oneof - idxs