Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
13 changes: 11 additions & 2 deletions cli/azd/cmd/middleware/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/azure/azure-dev/cli/azd/pkg/tools/pack"
"github.com/azure/azure-dev/cli/azd/pkg/update"
uxlib "github.com/azure/azure-dev/cli/azd/pkg/ux"
"go.opentelemetry.io/otel/codes"
)
Expand Down Expand Up @@ -120,8 +121,16 @@ func shouldSkipErrorAnalysis(err error) bool {
}

// Environment was already initialized
_, ok := errors.AsType[*environment.EnvironmentInitError](err)
return ok
if _, ok := errors.AsType[*environment.EnvironmentInitError](err); ok {
return true
}

// Update errors have their own user-facing messages and suggestions
if _, ok := errors.AsType[*update.UpdateError](err); ok {
return true
}

return false
}

func NewErrorMiddleware(
Expand Down
14 changes: 14 additions & 0 deletions cli/azd/cmd/middleware/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/azure/azure-dev/cli/azd/pkg/tools/github"
"github.com/azure/azure-dev/cli/azd/pkg/tools/pack"
"github.com/azure/azure-dev/cli/azd/pkg/update"
"github.com/azure/azure-dev/cli/azd/test/mocks"
"github.com/blang/semver/v4"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -399,6 +400,19 @@ func Test_ShouldSkipErrorAnalysis(t *testing.T) {
wrapped := fmt.Errorf("preflight declined: %w", internal.ErrAbortedByUser)
require.True(t, shouldSkipErrorAnalysis(wrapped))
})

t.Run("UpdateError is skipped", func(t *testing.T) {
t.Parallel()
err := &update.UpdateError{Code: update.CodeDownloadFailed, Err: errors.New("download failed")}
require.True(t, shouldSkipErrorAnalysis(err))
})

t.Run("Wrapped UpdateError is skipped", func(t *testing.T) {
t.Parallel()
inner := &update.UpdateError{Code: update.CodeReplaceFailed, Err: errors.New("replace failed")}
wrapped := fmt.Errorf("update error: %w", inner)
require.True(t, shouldSkipErrorAnalysis(wrapped))
})
}

func Test_TroubleshootCategory_Constants(t *testing.T) {
Expand Down
64 changes: 27 additions & 37 deletions cli/azd/cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"github.com/azure/azure-dev/cli/azd/internal/tracing"
"github.com/azure/azure-dev/cli/azd/internal/tracing/fields"
"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/exec"
"github.com/azure/azure-dev/cli/azd/pkg/input"
Expand Down Expand Up @@ -64,13 +63,12 @@ func newUpdateCmd() *cobra.Command {
}

type updateAction struct {
flags *updateFlags
console input.Console
formatter output.Formatter
writer io.Writer
configManager config.UserConfigManager
commandRunner exec.CommandRunner
alphaFeatureManager *alpha.FeatureManager
flags *updateFlags
console input.Console
formatter output.Formatter
writer io.Writer
configManager config.UserConfigManager
commandRunner exec.CommandRunner
}

func newUpdateAction(
Expand All @@ -80,16 +78,14 @@ func newUpdateAction(
writer io.Writer,
configManager config.UserConfigManager,
commandRunner exec.CommandRunner,
alphaFeatureManager *alpha.FeatureManager,
) actions.Action {
return &updateAction{
flags: flags,
console: console,
formatter: formatter,
writer: writer,
configManager: configManager,
commandRunner: commandRunner,
alphaFeatureManager: alphaFeatureManager,
flags: flags,
console: console,
formatter: formatter,
writer: writer,
configManager: configManager,
commandRunner: commandRunner,
}
}

Expand All @@ -102,27 +98,6 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) {
}
}

// Auto-enable the alpha feature if not already enabled.
// The user's intent is clear by running `azd update` directly.
if !a.alphaFeatureManager.IsEnabled(update.FeatureUpdate) {
userCfg, err := a.configManager.Load()
if err != nil {
userCfg = config.NewEmptyConfig()
}

if err := userCfg.Set(fmt.Sprintf("alpha.%s", update.FeatureUpdate), "on"); err != nil {
return nil, fmt.Errorf("failed to enable update feature: %w", err)
}

if err := a.configManager.Save(userCfg); err != nil {
return nil, fmt.Errorf("failed to save config: %w", err)
}

a.console.MessageUxItem(ctx, &ux.MessageTitle{
Title: "azd update is in alpha. Channel-aware version checks are now enabled.\n",
})
}

// Track install method for telemetry
installedBy := installer.InstalledBy()
tracing.SetUsageAttributes(
Expand All @@ -134,13 +109,24 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) {
userConfig = config.NewEmptyConfig()
}

// Show preview notice on first use
if !update.HasUpdateConfig(userConfig) {
Comment thread
hemarina marked this conversation as resolved.
a.console.MessageUxItem(ctx, &ux.MessageTitle{
Title: fmt.Sprintf(
"azd update is currently in preview. "+
"To learn more about feature stages, visit %s.",
output.WithLinkFormat("https://aka.ms/azd-feature-stages")),
Comment thread
hemarina marked this conversation as resolved.
})
}
Comment thread
hemarina marked this conversation as resolved.
Outdated
Comment thread
hemarina marked this conversation as resolved.

// Determine current channel BEFORE persisting any flags
currentCfg := update.LoadUpdateConfig(userConfig)
switchingChannels := a.flags.channel != "" && update.Channel(a.flags.channel) != currentCfg.Channel

// Persist non-channel config flags immediately (auto-update, check-interval)
configChanged, err := a.persistNonChannelFlags(userConfig)
if err != nil {
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeConfigFailed))
return nil, err
}
Comment thread
hemarina marked this conversation as resolved.
Outdated

Expand All @@ -149,13 +135,15 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) {
if switchingChannels {
newChannel, err := update.ParseChannel(a.flags.channel)
if err != nil {
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeInvalidInput))
return nil, err
}
_ = update.SaveChannel(userConfig, newChannel)
configChanged = true
} else if a.flags.channel != "" {
// Same channel explicitly set — just persist it
if err := update.SaveChannel(userConfig, update.Channel(a.flags.channel)); err != nil {
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeConfigFailed))
return nil, err
}
configChanged = true
Expand Down Expand Up @@ -205,6 +193,7 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) {
if a.onlyConfigFlagsSet() {
if configChanged {
if err := a.configManager.Save(userConfig); err != nil {
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeConfigFailed))
return nil, fmt.Errorf("failed to save config: %w", err)
}
}
Expand Down Expand Up @@ -273,6 +262,7 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) {
// Now persist all config changes (including channel) after confirmation
if configChanged {
if err := a.configManager.Save(userConfig); err != nil {
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeConfigFailed))
return nil, fmt.Errorf("failed to save config: %w", err)
}
}
Expand Down
23 changes: 23 additions & 0 deletions cli/azd/cmd/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,26 @@ func TestPersistNonChannelFlags(t *testing.T) {
assert.Equal(t, 12, updateCfg.CheckIntervalHours)
})
}

func TestUpdateErrorCodes(t *testing.T) {
Comment thread
hemarina marked this conversation as resolved.
t.Parallel()

// Verify telemetry result codes used in updateAction.Run() are non-empty
// and follow the expected "update." prefix convention.
codes := []string{
update.CodeSuccess,
update.CodeAlreadyUpToDate,
update.CodeVersionCheckFailed,
update.CodeSkippedCI,
update.CodePackageManagerFailed,
update.CodeChannelSwitchDecline,
update.CodeReplaceFailed,
update.CodeConfigFailed,
update.CodeInvalidInput,
}

for _, code := range codes {
assert.NotEmpty(t, code)
assert.Contains(t, code, "update.")
}
Comment thread
hemarina marked this conversation as resolved.
}
25 changes: 8 additions & 17 deletions cli/azd/cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"github.com/azure/azure-dev/cli/azd/pkg/contracts"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
Expand All @@ -35,26 +34,23 @@ func newVersionFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions)
}

type versionAction struct {
flags *versionFlags
formatter output.Formatter
writer io.Writer
console input.Console
alphaFeatureManager *alpha.FeatureManager
flags *versionFlags
formatter output.Formatter
writer io.Writer
console input.Console
}

func newVersionAction(
flags *versionFlags,
formatter output.Formatter,
writer io.Writer,
console input.Console,
alphaFeatureManager *alpha.FeatureManager,
) actions.Action {
return &versionAction{
flags: flags,
formatter: formatter,
writer: writer,
console: console,
alphaFeatureManager: alphaFeatureManager,
flags: flags,
formatter: formatter,
writer: writer,
console: console,
}
}

Expand All @@ -81,12 +77,7 @@ func (v *versionAction) Run(ctx context.Context) (*actions.ActionResult, error)

// channelSuffix returns a display suffix like " (stable)" or " (daily)".
// Based on the running binary's version string, not the configured channel.
Comment thread
hemarina marked this conversation as resolved.
// Only shown when the update alpha feature is enabled.
func (v *versionAction) channelSuffix() string {
if !v.alphaFeatureManager.IsEnabled(update.FeatureUpdate) {
return ""
}

// Detect from the binary itself: if the version contains "daily.", it's a daily build.
if _, err := update.ParseDailyBuildNumber(internal.Version); err == nil {
return " (daily)"
Expand Down
48 changes: 9 additions & 39 deletions cli/azd/cmd/version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import (
"testing"

"github.com/azure/azure-dev/cli/azd/internal"
"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/contracts"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/test/mocks"
Expand All @@ -27,7 +25,6 @@ func TestVersionAction_NoneFormat(t *testing.T) {
&output.NoneFormatter{},
&bytes.Buffer{},
mockContext.Console,
mockContext.AlphaFeaturesManager,
)

result, err := action.Run(t.Context())
Expand All @@ -45,7 +42,6 @@ func TestVersionAction_JsonFormat(t *testing.T) {
&output.JsonFormatter{},
buf,
mockContext.Console,
mockContext.AlphaFeaturesManager,
)

result, err := action.Run(t.Context())
Expand All @@ -64,40 +60,14 @@ func TestVersionAction_JsonFormat(t *testing.T) {
func TestVersionAction_ChannelSuffix(t *testing.T) {
t.Parallel()

t.Run("update_feature_disabled", func(t *testing.T) {
t.Parallel()
mockContext := mocks.NewMockContext(context.Background())
va := &versionAction{
flags: &versionFlags{},
formatter: &output.NoneFormatter{},
writer: &bytes.Buffer{},
}

va := &versionAction{
flags: &versionFlags{},
formatter: &output.NoneFormatter{},
writer: &bytes.Buffer{},
console: mockContext.Console,
alphaFeatureManager: mockContext.AlphaFeaturesManager,
}

suffix := va.channelSuffix()
require.Equal(t, "", suffix)
})

t.Run("update_feature_enabled_stable", func(t *testing.T) {
t.Parallel()

cfg := config.NewEmptyConfig()
_ = cfg.Set("alpha.update", "on")
fm := alpha.NewFeaturesManagerWithConfig(cfg)

va := &versionAction{
flags: &versionFlags{},
formatter: &output.NoneFormatter{},
writer: &bytes.Buffer{},
console: nil, // not needed for channelSuffix
alphaFeatureManager: fm,
}

suffix := va.channelSuffix()
// In test builds, internal.Version is "0.0.0-dev.0" (not daily format)
// so it will either return " (stable)" or " (daily)" depending on version
require.NotEqual(t, "", suffix)
})
suffix := va.channelSuffix()
// In test builds, internal.Version is "0.0.0-dev.0" (not daily format)
// so it will return " (stable)"
require.Equal(t, " (stable)", suffix)
}
16 changes: 6 additions & 10 deletions cli/azd/docs/design/azd-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Today, when a new version of `azd` is available, users see a warning message wit
1. **`azd update`** — a command that performs the update for the user
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.
The feature ships as a command currently in preview. On first use, a preview notice is displayed. The `azd update` command is always available.

---

Expand All @@ -23,7 +23,7 @@ The feature ships as a hidden command behind an alpha feature toggle (`alpha.upd
- Preserve user control (channel selection, check interval)
- Avoid disruption to CI/CD pipelines
- Respect platform install methods (MSI, Install Scripts, Homebrew, winget, choco)
- Ship safely behind an alpha feature flag with zero impact when off
- Ship safely as a preview feature while gathering feedback

---

Expand Down Expand Up @@ -122,8 +122,6 @@ 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`.

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 share a base semver (e.g., `1.24.0-beta.1`), so version comparison alone can't tell if a newer daily exists.
Expand Down Expand Up @@ -349,14 +347,12 @@ Uses the existing azd telemetry infrastructure (OpenTelemetry). New telemetry fi

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:
### 8. Feature Stage (Preview)

- **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`."
The update feature is currently in preview:

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.
- `azd update` works without needing any config toggle
- On first use, when no `updates.*` configuration exists, a preview notice is displayed.

Comment thread
hemarina marked this conversation as resolved.
Outdated
### 9. Update Banner Suppression

Expand Down
Loading
Loading