Skip to content

Commit 11eedcd

Browse files
authored
move update from alpha to beta (#7489)
* move update from alpha to preview * skip update error for error handling * add more telemetry * address feedback * update to beta * address feedback * fix failed test
1 parent 6ac724d commit 11eedcd

17 files changed

Lines changed: 315 additions & 136 deletions

cli/azd/cmd/middleware/error.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/azure/azure-dev/cli/azd/pkg/project"
3535
"github.com/azure/azure-dev/cli/azd/pkg/tools"
3636
"github.com/azure/azure-dev/cli/azd/pkg/tools/pack"
37+
"github.com/azure/azure-dev/cli/azd/pkg/update"
3738
uxlib "github.com/azure/azure-dev/cli/azd/pkg/ux"
3839
"go.opentelemetry.io/otel/codes"
3940
)
@@ -120,8 +121,16 @@ func shouldSkipErrorAnalysis(err error) bool {
120121
}
121122

122123
// Environment was already initialized
123-
_, ok := errors.AsType[*environment.EnvironmentInitError](err)
124-
return ok
124+
if _, ok := errors.AsType[*environment.EnvironmentInitError](err); ok {
125+
return true
126+
}
127+
128+
// Update errors have their own user-facing messages and suggestions
129+
if _, ok := errors.AsType[*update.UpdateError](err); ok {
130+
return true
131+
}
132+
133+
return false
125134
}
126135

127136
func NewErrorMiddleware(

cli/azd/cmd/middleware/error_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/azure/azure-dev/cli/azd/pkg/tools"
2727
"github.com/azure/azure-dev/cli/azd/pkg/tools/github"
2828
"github.com/azure/azure-dev/cli/azd/pkg/tools/pack"
29+
"github.com/azure/azure-dev/cli/azd/pkg/update"
2930
"github.com/azure/azure-dev/cli/azd/test/mocks"
3031
"github.com/blang/semver/v4"
3132
"github.com/stretchr/testify/require"
@@ -399,6 +400,19 @@ func Test_ShouldSkipErrorAnalysis(t *testing.T) {
399400
wrapped := fmt.Errorf("preflight declined: %w", internal.ErrAbortedByUser)
400401
require.True(t, shouldSkipErrorAnalysis(wrapped))
401402
})
403+
404+
t.Run("UpdateError is skipped", func(t *testing.T) {
405+
t.Parallel()
406+
err := &update.UpdateError{Code: update.CodeDownloadFailed, Err: errors.New("download failed")}
407+
require.True(t, shouldSkipErrorAnalysis(err))
408+
})
409+
410+
t.Run("Wrapped UpdateError is skipped", func(t *testing.T) {
411+
t.Parallel()
412+
inner := &update.UpdateError{Code: update.CodeReplaceFailed, Err: errors.New("replace failed")}
413+
wrapped := fmt.Errorf("update error: %w", inner)
414+
require.True(t, shouldSkipErrorAnalysis(wrapped))
415+
})
402416
}
403417

404418
func Test_TroubleshootCategory_Constants(t *testing.T) {

cli/azd/cmd/root.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,9 @@ func newRootCmd(
217217
ActionResolver: newUpdateAction,
218218
OutputFormats: []output.Format{output.NoneFormat},
219219
DefaultFormat: output.NoneFormat,
220+
GroupingOptions: actions.CommandGroupOptions{
221+
RootLevelHelp: actions.CmdGroupBeta,
222+
},
220223
})
221224

222225
root.Add("vs-server", &actions.ActionDescriptorOptions{

cli/azd/cmd/testdata/TestFigSpec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2828,6 +2828,30 @@ const completionSpec: Fig.Spec = {
28282828
},
28292829
],
28302830
},
2831+
{
2832+
name: ['update'],
2833+
description: 'Updates azd to the latest version.',
2834+
options: [
2835+
{
2836+
name: ['--channel'],
2837+
description: 'Update channel: stable or daily.',
2838+
args: [
2839+
{
2840+
name: 'channel',
2841+
},
2842+
],
2843+
},
2844+
{
2845+
name: ['--check-interval-hours'],
2846+
description: 'Override the update check interval in hours.',
2847+
args: [
2848+
{
2849+
name: 'check-interval-hours',
2850+
},
2851+
],
2852+
},
2853+
],
2854+
},
28312855
{
28322856
name: ['version'],
28332857
description: 'Print the version number of Azure Developer CLI.',
@@ -3623,6 +3647,10 @@ const completionSpec: Fig.Spec = {
36233647
name: ['up'],
36243648
description: 'Provision and deploy your project to Azure with a single command.',
36253649
},
3650+
{
3651+
name: ['update'],
3652+
description: 'Updates azd to the latest version.',
3653+
},
36263654
{
36273655
name: ['version'],
36283656
description: 'Print the version number of Azure Developer CLI.',
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
Updates azd to the latest version.
3+
4+
Usage
5+
azd update [flags]
6+
7+
Flags
8+
--channel string : Update channel: stable or daily.
9+
--check-interval-hours int : Override the update check interval in hours.
10+
11+
Global Flags
12+
-C, --cwd string : Sets the current working directory.
13+
--debug : Enables debugging and diagnostics logging.
14+
--docs : Opens the documentation for azd update in your web browser.
15+
-e, --environment string : The name of the environment to use.
16+
-h, --help : Gets help for update.
17+
--no-prompt : Accepts the default value instead of prompting, or it fails if there is no default.
18+
19+
Find a bug? Want to let us know how we're doing? Fill out this brief survey: https://aka.ms/azure-dev/hats.
20+
21+

cli/azd/cmd/testdata/TestUsage-azd.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Commands
3333
pipeline : Manage and configure your deployment pipelines.
3434
restore : Restores the project's dependencies.
3535
template : Find and view template details.
36+
update : Updates azd to the latest version.
3637

3738
Enabled alpha commands
3839
copilot : Manage GitHub Copilot agent settings. (Preview)

cli/azd/cmd/update.go

Lines changed: 53 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import (
88
"errors"
99
"fmt"
1010
"io"
11+
"log"
1112

1213
"github.com/azure/azure-dev/cli/azd/cmd/actions"
1314
"github.com/azure/azure-dev/cli/azd/internal"
1415
"github.com/azure/azure-dev/cli/azd/internal/tracing"
1516
"github.com/azure/azure-dev/cli/azd/internal/tracing/fields"
1617
"github.com/azure/azure-dev/cli/azd/internal/tracing/resource"
17-
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
1818
"github.com/azure/azure-dev/cli/azd/pkg/config"
1919
"github.com/azure/azure-dev/cli/azd/pkg/exec"
2020
"github.com/azure/azure-dev/cli/azd/pkg/input"
@@ -57,20 +57,18 @@ func (f *updateFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandO
5757

5858
func newUpdateCmd() *cobra.Command {
5959
return &cobra.Command{
60-
Use: "update",
61-
Short: "Updates azd to the latest version.",
62-
Hidden: true,
60+
Use: "update",
61+
Short: "Updates azd to the latest version.",
6362
}
6463
}
6564

6665
type updateAction struct {
67-
flags *updateFlags
68-
console input.Console
69-
formatter output.Formatter
70-
writer io.Writer
71-
configManager config.UserConfigManager
72-
commandRunner exec.CommandRunner
73-
alphaFeatureManager *alpha.FeatureManager
66+
flags *updateFlags
67+
console input.Console
68+
formatter output.Formatter
69+
writer io.Writer
70+
configManager config.UserConfigManager
71+
commandRunner exec.CommandRunner
7472
}
7573

7674
func newUpdateAction(
@@ -80,16 +78,14 @@ func newUpdateAction(
8078
writer io.Writer,
8179
configManager config.UserConfigManager,
8280
commandRunner exec.CommandRunner,
83-
alphaFeatureManager *alpha.FeatureManager,
8481
) actions.Action {
8582
return &updateAction{
86-
flags: flags,
87-
console: console,
88-
formatter: formatter,
89-
writer: writer,
90-
configManager: configManager,
91-
commandRunner: commandRunner,
92-
alphaFeatureManager: alphaFeatureManager,
83+
flags: flags,
84+
console: console,
85+
formatter: formatter,
86+
writer: writer,
87+
configManager: configManager,
88+
commandRunner: commandRunner,
9389
}
9490
}
9591

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

105-
// Auto-enable the alpha feature if not already enabled.
106-
// The user's intent is clear by running `azd update` directly.
107-
if !a.alphaFeatureManager.IsEnabled(update.FeatureUpdate) {
108-
userCfg, err := a.configManager.Load()
109-
if err != nil {
110-
userCfg = config.NewEmptyConfig()
111-
}
112-
113-
if err := userCfg.Set(fmt.Sprintf("alpha.%s", update.FeatureUpdate), "on"); err != nil {
114-
return nil, fmt.Errorf("failed to enable update feature: %w", err)
115-
}
116-
117-
if err := a.configManager.Save(userCfg); err != nil {
118-
return nil, fmt.Errorf("failed to save config: %w", err)
119-
}
120-
121-
a.console.MessageUxItem(ctx, &ux.MessageTitle{
122-
Title: "azd update is in alpha. Channel-aware version checks are now enabled.\n",
123-
})
124-
}
125-
126101
// Track install method for telemetry
127102
installedBy := installer.InstalledBy()
128103
tracing.SetUsageAttributes(
@@ -134,13 +109,31 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) {
134109
userConfig = config.NewEmptyConfig()
135110
}
136111

112+
// Show notice on first use
113+
if !update.HasUpdateConfig(userConfig) {
114+
a.console.MessageUxItem(ctx, &ux.MessageTitle{
115+
Title: fmt.Sprintf(
116+
"azd update is currently in Beta. "+
117+
"To learn more about feature stages, visit %s.",
118+
output.WithLinkFormat("https://aka.ms/azd-feature-stages")),
119+
})
120+
121+
// Write a default channel so HasUpdateConfig returns true next time.
122+
if err := update.SaveChannel(userConfig, update.LoadUpdateConfig(userConfig).Channel); err != nil {
123+
log.Printf("warning: failed to persist default update channel: %v", err)
124+
} else if err := a.configManager.Save(userConfig); err != nil {
125+
log.Printf("warning: failed to save config after setting default channel: %v", err)
126+
}
127+
}
128+
137129
// Determine current channel BEFORE persisting any flags
138130
currentCfg := update.LoadUpdateConfig(userConfig)
139131
switchingChannels := a.flags.channel != "" && update.Channel(a.flags.channel) != currentCfg.Channel
140132

141-
// Persist non-channel config flags immediately (auto-update, check-interval)
133+
// Persist non-channel config flags immediately (check-interval)
142134
configChanged, err := a.persistNonChannelFlags(userConfig)
143135
if err != nil {
136+
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeConfigFailed))
144137
return nil, err
145138
}
146139

@@ -149,13 +142,15 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) {
149142
if switchingChannels {
150143
newChannel, err := update.ParseChannel(a.flags.channel)
151144
if err != nil {
145+
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeInvalidInput))
152146
return nil, err
153147
}
154148
_ = update.SaveChannel(userConfig, newChannel)
155149
configChanged = true
156150
} else if a.flags.channel != "" {
157151
// Same channel explicitly set — just persist it
158152
if err := update.SaveChannel(userConfig, update.Channel(a.flags.channel)); err != nil {
153+
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeConfigFailed))
159154
return nil, err
160155
}
161156
configChanged = true
@@ -169,6 +164,22 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) {
169164
fields.UpdateFromVersion.String(internal.VersionInfo().Version.String()),
170165
)
171166

167+
// If only config flags were set (no channel change, no update needed), just confirm
168+
if a.onlyConfigFlagsSet() {
169+
if configChanged {
170+
if err := a.configManager.Save(userConfig); err != nil {
171+
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeConfigFailed))
172+
return nil, fmt.Errorf("failed to save config: %w", err)
173+
}
174+
}
175+
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeSuccess))
176+
return &actions.ActionResult{
177+
Message: &actions.ResultMessage{
178+
Header: "Update preferences saved.",
179+
},
180+
}, nil
181+
}
182+
172183
mgr := update.NewManager(a.commandRunner, nil)
173184

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

204-
// If only config flags were set (no channel change, no update needed), just confirm
205-
if a.onlyConfigFlagsSet() {
206-
if configChanged {
207-
if err := a.configManager.Save(userConfig); err != nil {
208-
return nil, fmt.Errorf("failed to save config: %w", err)
209-
}
210-
}
211-
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeSuccess))
212-
return &actions.ActionResult{
213-
Message: &actions.ResultMessage{
214-
Header: "Update preferences saved.",
215-
},
216-
}, nil
217-
}
218-
219215
// Check for updates (always fresh for manual invocation)
220216
a.console.ShowSpinner(ctx, "Checking for updates...", input.Step)
221217
versionInfo, err := mgr.CheckForUpdate(ctx, cfg, true)
@@ -273,6 +269,7 @@ func (a *updateAction) Run(ctx context.Context) (*actions.ActionResult, error) {
273269
// Now persist all config changes (including channel) after confirmation
274270
if configChanged {
275271
if err := a.configManager.Save(userConfig); err != nil {
272+
tracing.SetUsageAttributes(fields.UpdateResult.String(update.CodeConfigFailed))
276273
return nil, fmt.Errorf("failed to save config: %w", err)
277274
}
278275
}

cli/azd/cmd/update_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package cmd
55

66
import (
7+
"strings"
78
"testing"
89

910
"github.com/azure/azure-dev/cli/azd/pkg/config"
@@ -101,3 +102,30 @@ func TestPersistNonChannelFlags(t *testing.T) {
101102
assert.Equal(t, 12, updateCfg.CheckIntervalHours)
102103
})
103104
}
105+
106+
func TestUpdateErrorCodes(t *testing.T) {
107+
t.Parallel()
108+
109+
// Verify telemetry result codes used in updateAction.Run() are non-empty
110+
// and follow the expected "update." prefix convention.
111+
codes := []string{
112+
update.CodeSuccess,
113+
update.CodeAlreadyUpToDate,
114+
update.CodeVersionCheckFailed,
115+
update.CodeSkippedCI,
116+
update.CodePackageManagerFailed,
117+
update.CodeChannelSwitchDecline,
118+
update.CodeReplaceFailed,
119+
update.CodeConfigFailed,
120+
update.CodeInvalidInput,
121+
}
122+
123+
seen := make(map[string]bool, len(codes))
124+
for _, code := range codes {
125+
assert.NotEmpty(t, code)
126+
assert.True(t, strings.HasPrefix(code, "update."),
127+
"code %q should have prefix %q", code, "update.")
128+
assert.False(t, seen[code], "duplicate code %q", code)
129+
seen[code] = true
130+
}
131+
}

0 commit comments

Comments
 (0)