Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
69 changes: 31 additions & 38 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,27 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) {
userConfig = config.NewEmptyConfig()
}

// Show 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 Beta. "+
"To learn more about feature stages, visit %s.",
output.WithLinkFormat("https://aka.ms/azd-feature-stages")),
Comment thread
hemarina marked this conversation as resolved.
})

// Write a default channel so HasUpdateConfig returns true next time.
Comment thread
hemarina marked this conversation as resolved.
_ = update.SaveChannel(userConfig, update.LoadUpdateConfig(userConfig).Channel)
}
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)
// Persist non-channel config flags immediately (check-interval)
configChanged, err := a.persistNonChannelFlags(userConfig)
if err != nil {
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeConfigFailed))
return nil, err
}

Expand All @@ -149,13 +138,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 +196,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 +265,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
25 changes: 25 additions & 0 deletions cli/azd/cmd/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package cmd

import (
"strings"
"testing"

"github.com/azure/azure-dev/cli/azd/pkg/config"
Expand Down Expand Up @@ -101,3 +102,27 @@ 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.True(t, strings.HasPrefix(code, "update."),
"code %q should have prefix %q", 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)
}
Loading
Loading