Skip to content
Merged
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
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
3 changes: 3 additions & 0 deletions cli/azd/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ func newRootCmd(
ActionResolver: newUpdateAction,
OutputFormats: []output.Format{output.NoneFormat},
DefaultFormat: output.NoneFormat,
GroupingOptions: actions.CommandGroupOptions{
RootLevelHelp: actions.CmdGroupBeta,
},
})

root.Add("vs-server", &actions.ActionDescriptorOptions{
Expand Down
28 changes: 28 additions & 0 deletions cli/azd/cmd/testdata/TestFigSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2828,6 +2828,30 @@ const completionSpec: Fig.Spec = {
},
],
},
{
name: ['update'],
description: 'Updates azd to the latest version.',
options: [
{
name: ['--channel'],
description: 'Update channel: stable or daily.',
args: [
{
name: 'channel',
},
],
},
{
name: ['--check-interval-hours'],
description: 'Override the update check interval in hours.',
args: [
{
name: 'check-interval-hours',
},
],
},
],
},
{
name: ['version'],
description: 'Print the version number of Azure Developer CLI.',
Expand Down Expand Up @@ -3623,6 +3647,10 @@ const completionSpec: Fig.Spec = {
name: ['up'],
description: 'Provision and deploy your project to Azure with a single command.',
},
{
name: ['update'],
description: 'Updates azd to the latest version.',
},
{
name: ['version'],
description: 'Print the version number of Azure Developer CLI.',
Expand Down
21 changes: 21 additions & 0 deletions cli/azd/cmd/testdata/TestUsage-azd-update.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

Updates azd to the latest version.

Usage
azd update [flags]

Flags
--channel string : Update channel: stable or daily.
--check-interval-hours int : Override the update check interval in hours.

Global Flags
-C, --cwd string : Sets the current working directory.
--debug : Enables debugging and diagnostics logging.
--docs : Opens the documentation for azd update in your web browser.
-e, --environment string : The name of the environment to use.
-h, --help : Gets help for update.
--no-prompt : Accepts the default value instead of prompting, or it fails if there is no default.

Find a bug? Want to let us know how we're doing? Fill out this brief survey: https://aka.ms/azure-dev/hats.


1 change: 1 addition & 0 deletions cli/azd/cmd/testdata/TestUsage-azd.snap
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Commands
pipeline : Manage and configure your deployment pipelines.
restore : Restores the project's dependencies.
template : Find and view template details.
update : Updates azd to the latest version.

Enabled alpha commands
copilot : Manage GitHub Copilot agent settings. (Preview)
Expand Down
109 changes: 53 additions & 56 deletions cli/azd/cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import (
"errors"
"fmt"
"io"
"log"

"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"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 @@ -57,20 +57,18 @@ func (f *updateFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandO

func newUpdateCmd() *cobra.Command {
return &cobra.Command{
Use: "update",
Short: "Updates azd to the latest version.",
Hidden: true,
Use: "update",
Short: "Updates azd to the latest version.",
}
}

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,31 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) {
userConfig = config.NewEmptyConfig()
}

// Show notice on first use
if !update.HasUpdateConfig(userConfig) {
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")),
})

// Write a default channel so HasUpdateConfig returns true next time.
if err := update.SaveChannel(userConfig, update.LoadUpdateConfig(userConfig).Channel); err != nil {
log.Printf("warning: failed to persist default update channel: %v", err)
} else if err := a.configManager.Save(userConfig); err != nil {
log.Printf("warning: failed to save config after setting default channel: %v", err)
}
}

// 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 +142,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 All @@ -169,6 +164,22 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) {
fields.UpdateFromVersion.String(internal.VersionInfo().Version.String()),
)

// If only config flags were set (no channel change, no update needed), just confirm
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)
}
}
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeSuccess))
return &actions.ActionResult{
Message: &actions.ResultMessage{
Header: "Update preferences saved.",
},
}, nil
}

mgr := update.NewManager(a.commandRunner, nil)

// Block update in CI/CD environments
Expand Down Expand Up @@ -201,21 +212,6 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) {
}
}

// If only config flags were set (no channel change, no update needed), just confirm
if a.onlyConfigFlagsSet() {
if configChanged {
if err := a.configManager.Save(userConfig); err != nil {
return nil, fmt.Errorf("failed to save config: %w", err)
}
}
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeSuccess))
return &actions.ActionResult{
Message: &actions.ResultMessage{
Header: "Update preferences saved.",
},
}, nil
}

// Check for updates (always fresh for manual invocation)
a.console.ShowSpinner(ctx, "Checking for updates...", input.Step)
versionInfo, err := mgr.CheckForUpdate(ctx, cfg, true)
Expand Down Expand Up @@ -273,6 +269,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
28 changes: 28 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,30 @@ func TestPersistNonChannelFlags(t *testing.T) {
assert.Equal(t, 12, updateCfg.CheckIntervalHours)
})
}

func TestUpdateErrorCodes(t *testing.T) {
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,
}

seen := make(map[string]bool, len(codes))
for _, code := range codes {
assert.NotEmpty(t, code)
assert.True(t, strings.HasPrefix(code, "update."),
"code %q should have prefix %q", code, "update.")
assert.False(t, seen[code], "duplicate code %q", code)
seen[code] = true
}
}
Loading
Loading