From ef2e5db2638bc308e4793576f8b63a8aa42983e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:18:53 +0000 Subject: [PATCH 1/8] Initial plan From cd82110abe8a9b59ad33065608fae09312edbd1b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:25:23 +0000 Subject: [PATCH 2/8] Honor extension source during upgrade - Modified extensionUpgradeAction to use installed extension's source when --source flag not provided - Added checkForNewerVersionInOtherSources to warn users if newer version exists in different source - Updated error message to include source name when extension not found - Added test placeholder for source honoring behavior Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com> --- cli/azd/cmd/extension.go | 128 ++++++++++++++++++++++++++++++++- cli/azd/cmd/extensions_test.go | 25 +++++++ 2 files changed, 151 insertions(+), 2 deletions(-) diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index 72ace3e83ed..5cd7dc4b97b 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "log" "slices" "strings" "text/tabwriter" @@ -913,9 +914,16 @@ func (a *extensionUpgradeAction) Run(ctx context.Context) (*actions.ActionResult return nil, fmt.Errorf("failed to get installed extension: %w", err) } + // Honor the source from which the extension was originally installed + // Only use the --source flag if explicitly provided by the user + sourceToUse := installed.Source + if a.flags.source != "" { + sourceToUse = a.flags.source + } + filterOptions := &extensions.FilterOptions{ Id: extensionId, - Source: a.flags.source, + Source: sourceToUse, Version: a.flags.version, } @@ -927,9 +935,11 @@ func (a *extensionUpgradeAction) Run(ctx context.Context) (*actions.ActionResult if len(matches) == 0 { a.console.StopSpinner(ctx, stepMessage, input.StepFailed) - return nil, fmt.Errorf("extension %s not found", extensionId) + return nil, fmt.Errorf("extension %s not found in source %s", extensionId, sourceToUse) } + // Since we're filtering by the exact source, there should only be one match + // But we'll use selectDistinctExtension for consistency selectedExtension, err := selectDistinctExtension(ctx, a.console, extensionId, matches, a.flags.global) if err != nil { return nil, err @@ -938,6 +948,20 @@ func (a *extensionUpgradeAction) Run(ctx context.Context) (*actions.ActionResult a.console.ShowSpinner(ctx, stepMessage, input.Step) latestVersion := selectedExtension.Versions[len(selectedExtension.Versions)-1] + // Check if there's a newer version available in other sources + // Only do this if the user didn't explicitly specify a source + if a.flags.source == "" { + if err := a.checkForNewerVersionInOtherSources( + ctx, + extensionId, + installed.Source, + latestVersion.Version, + ); err != nil { + // Log the error but don't fail the upgrade + log.Printf("Warning: failed to check for newer versions in other sources: %v", err) + } + } + // Parse semantic versions for proper comparison installedSemver, err := semver.NewVersion(installed.Version) if err != nil { @@ -982,6 +1006,106 @@ func (a *extensionUpgradeAction) Run(ctx context.Context) (*actions.ActionResult }, nil } +// checkForNewerVersionInOtherSources checks if there's a newer version of the extension +// available in sources other than the current one, and displays a warning if found. +func (a *extensionUpgradeAction) checkForNewerVersionInOtherSources( + ctx context.Context, + extensionId string, + currentSource string, + currentLatestVersion string, +) error { + // Find all versions of this extension across all sources (excluding current source) + allMatches, err := a.extensionManager.FindExtensions(ctx, &extensions.FilterOptions{ + Id: extensionId, + }) + if err != nil { + return err + } + + // Parse the current latest version + currentSemver, err := semver.NewVersion(currentLatestVersion) + if err != nil { + return fmt.Errorf("failed to parse current latest version '%s': %w", currentLatestVersion, err) + } + + // Check each source for newer versions + var newerVersionSource string + var newerVersion string + + for _, match := range allMatches { + // Skip the current source + if strings.EqualFold(match.Source, currentSource) { + continue + } + + // Get the latest version from this source + if len(match.Versions) == 0 { + continue + } + + latestInSource := match.Versions[len(match.Versions)-1] + latestSemver, err := semver.NewVersion(latestInSource.Version) + if err != nil { + // Skip versions that can't be parsed + continue + } + + // Check if this version is newer + if latestSemver.GreaterThan(currentSemver) { + // Update if this is the newest we've found so far + if newerVersion == "" { + newerVersion = latestInSource.Version + newerVersionSource = match.Source + } else { + newerSemver, err := semver.NewVersion(newerVersion) + if err == nil && latestSemver.GreaterThan(newerSemver) { + newerVersion = latestInSource.Version + newerVersionSource = match.Source + } + } + } + } + + // Display warning if a newer version was found + if newerVersion != "" { + a.console.MessageUxItem(ctx, &ux.WarningMessage{ + Description: fmt.Sprintf( + "A newer version (%s) of %s is available in source '%s', but the extension was installed from source '%s'.", + output.WithHighLightFormat(newerVersion), + output.WithHighLightFormat(extensionId), + output.WithHighLightFormat(newerVersionSource), + output.WithHighLightFormat(currentSource), + ), + HidePrefix: false, + }) + a.console.Message(ctx, "") + a.console.Message(ctx, + fmt.Sprintf( + "To upgrade to the newer version from '%s', first uninstall the extension and then "+ + "install it from the desired source:", + newerVersionSource, + ), + ) + a.console.Message(ctx, + fmt.Sprintf( + " %s", + output.WithHighLightFormat(fmt.Sprintf("azd extension uninstall %s", extensionId)), + ), + ) + a.console.Message(ctx, + fmt.Sprintf( + " %s", + output.WithHighLightFormat( + fmt.Sprintf("azd extension install %s --source %s", extensionId, newerVersionSource), + ), + ), + ) + a.console.Message(ctx, "") + } + + return nil +} + type extensionSourceListAction struct { formatter output.Formatter writer io.Writer diff --git a/cli/azd/cmd/extensions_test.go b/cli/azd/cmd/extensions_test.go index 4e0787c0626..310b244aba9 100644 --- a/cli/azd/cmd/extensions_test.go +++ b/cli/azd/cmd/extensions_test.go @@ -176,3 +176,28 @@ func TestBindExtension_DeeplyNestedNamespace(t *testing.T) { require.Equal(t, "Extension for fine tuning AI models.", finetuneCmd.Options.Command.Short) require.Equal(t, "Extension for evaluating AI models.", evalCmd.Options.Command.Short) } + +func TestExtensionUpgrade_HonorsInstalledSource(t *testing.T) { + // This test verifies that extension upgrade uses the installed extension's source + // when no --source flag is provided + + // Note: This is a focused test for the logic change. Integration tests should be added + // to verify the end-to-end behavior with real extension sources. + + t.Run("uses installed source when no flag provided", func(t *testing.T) { + // The key assertion is in the code change itself: + // When a.flags.source is empty, filterOptions.Source should be set to installed.Source + + // This is verified through code inspection and manual testing + // A full integration test would require: + // 1. Mock extension manager + // 2. Mock console + // 3. Simulate installed extension with a source + // 4. Call Run() without --source flag + // 5. Verify FindExtensions is called with the installed source + + // For now, we rely on manual testing and existing integration tests + t.Skip("Requires integration test setup with mocked dependencies") + }) +} + From f0be14913553c1be9cc08b70455b5ee523d67911 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:33:52 +0000 Subject: [PATCH 3/8] Address code review feedback - Replace log.Printf with console.MessageUxItem for consistent logging - Remove unused log import - Remove placeholder test that provided no value Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com> --- cli/azd/cmd/extension.go | 9 +++++++-- cli/azd/cmd/extensions_test.go | 24 ------------------------ 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index 5cd7dc4b97b..02a8b493ee2 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "io" - "log" "slices" "strings" "text/tabwriter" @@ -958,7 +957,13 @@ func (a *extensionUpgradeAction) Run(ctx context.Context) (*actions.ActionResult latestVersion.Version, ); err != nil { // Log the error but don't fail the upgrade - log.Printf("Warning: failed to check for newer versions in other sources: %v", err) + // Using console warning instead of log.Printf for consistency + a.console.MessageUxItem(ctx, &ux.WarningMessage{ + Description: fmt.Sprintf( + "Failed to check for newer versions in other sources: %v", + err, + ), + }) } } diff --git a/cli/azd/cmd/extensions_test.go b/cli/azd/cmd/extensions_test.go index 310b244aba9..2f82ba3a77b 100644 --- a/cli/azd/cmd/extensions_test.go +++ b/cli/azd/cmd/extensions_test.go @@ -177,27 +177,3 @@ func TestBindExtension_DeeplyNestedNamespace(t *testing.T) { require.Equal(t, "Extension for evaluating AI models.", evalCmd.Options.Command.Short) } -func TestExtensionUpgrade_HonorsInstalledSource(t *testing.T) { - // This test verifies that extension upgrade uses the installed extension's source - // when no --source flag is provided - - // Note: This is a focused test for the logic change. Integration tests should be added - // to verify the end-to-end behavior with real extension sources. - - t.Run("uses installed source when no flag provided", func(t *testing.T) { - // The key assertion is in the code change itself: - // When a.flags.source is empty, filterOptions.Source should be set to installed.Source - - // This is verified through code inspection and manual testing - // A full integration test would require: - // 1. Mock extension manager - // 2. Mock console - // 3. Simulate installed extension with a source - // 4. Call Run() without --source flag - // 5. Verify FindExtensions is called with the installed source - - // For now, we rely on manual testing and existing integration tests - t.Skip("Requires integration test setup with mocked dependencies") - }) -} - From 6aeb9527a33a05630be76c0be85394096b91b16d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:35:16 +0000 Subject: [PATCH 4/8] Simplify code based on code review feedback - Remove unnecessary selectDistinctExtension call since filtering by exact source guarantees single match - Simplify version comparison logic by tracking newest semver directly instead of nested conditionals Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com> --- cli/azd/cmd/extension.go | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index 02a8b493ee2..81e93321c8c 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -937,12 +937,8 @@ func (a *extensionUpgradeAction) Run(ctx context.Context) (*actions.ActionResult return nil, fmt.Errorf("extension %s not found in source %s", extensionId, sourceToUse) } - // Since we're filtering by the exact source, there should only be one match - // But we'll use selectDistinctExtension for consistency - selectedExtension, err := selectDistinctExtension(ctx, a.console, extensionId, matches, a.flags.global) - if err != nil { - return nil, err - } + // When filtering by exact source, there should only be one match per extension ID + selectedExtension := matches[0] a.console.ShowSpinner(ctx, stepMessage, input.Step) latestVersion := selectedExtension.Versions[len(selectedExtension.Versions)-1] @@ -1035,7 +1031,7 @@ func (a *extensionUpgradeAction) checkForNewerVersionInOtherSources( // Check each source for newer versions var newerVersionSource string - var newerVersion string + var newestSemver *semver.Version for _, match := range allMatches { // Skip the current source @@ -1055,28 +1051,21 @@ func (a *extensionUpgradeAction) checkForNewerVersionInOtherSources( continue } - // Check if this version is newer + // Check if this version is newer than current AND newer than what we've found so far if latestSemver.GreaterThan(currentSemver) { - // Update if this is the newest we've found so far - if newerVersion == "" { - newerVersion = latestInSource.Version + if newestSemver == nil || latestSemver.GreaterThan(newestSemver) { + newestSemver = latestSemver newerVersionSource = match.Source - } else { - newerSemver, err := semver.NewVersion(newerVersion) - if err == nil && latestSemver.GreaterThan(newerSemver) { - newerVersion = latestInSource.Version - newerVersionSource = match.Source - } } } } // Display warning if a newer version was found - if newerVersion != "" { + if newestSemver != nil { a.console.MessageUxItem(ctx, &ux.WarningMessage{ Description: fmt.Sprintf( "A newer version (%s) of %s is available in source '%s', but the extension was installed from source '%s'.", - output.WithHighLightFormat(newerVersion), + output.WithHighLightFormat(newestSemver.String()), output.WithHighLightFormat(extensionId), output.WithHighLightFormat(newerVersionSource), output.WithHighLightFormat(currentSource), From a7d58c71a5baed72ac25c2bf812aab0bdd420b42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:36:46 +0000 Subject: [PATCH 5/8] Final code review improvements - Add defensive check for multiple matches when filtering by source and ID - Improve warning message clarity to explain that upgrade will proceed with original source version - Change "upgrade to" to "switch to" in instructions for clarity Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com> --- cli/azd/cmd/extension.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index 81e93321c8c..a3132a5bce3 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -937,7 +937,18 @@ func (a *extensionUpgradeAction) Run(ctx context.Context) (*actions.ActionResult return nil, fmt.Errorf("extension %s not found in source %s", extensionId, sourceToUse) } - // When filtering by exact source, there should only be one match per extension ID + // When filtering by exact source and extension ID, there should only be one match + // However, if there are multiple matches (unexpected), we'll use the first one + if len(matches) > 1 { + a.console.MessageUxItem(ctx, &ux.WarningMessage{ + Description: fmt.Sprintf( + "Found %d matches for %s in source %s, using the first match", + len(matches), + extensionId, + sourceToUse, + ), + }) + } selectedExtension := matches[0] a.console.ShowSpinner(ctx, stepMessage, input.Step) @@ -1064,18 +1075,21 @@ func (a *extensionUpgradeAction) checkForNewerVersionInOtherSources( if newestSemver != nil { a.console.MessageUxItem(ctx, &ux.WarningMessage{ Description: fmt.Sprintf( - "A newer version (%s) of %s is available in source '%s', but the extension was installed from source '%s'.", + "A newer version (%s) of %s is available in source '%s', "+ + "but the extension was installed from source '%s'. "+ + "The extension will be upgraded to version %s from the original source.", output.WithHighLightFormat(newestSemver.String()), output.WithHighLightFormat(extensionId), output.WithHighLightFormat(newerVersionSource), output.WithHighLightFormat(currentSource), + output.WithHighLightFormat(currentLatestVersion), ), HidePrefix: false, }) a.console.Message(ctx, "") a.console.Message(ctx, fmt.Sprintf( - "To upgrade to the newer version from '%s', first uninstall the extension and then "+ + "To switch to the newer version from '%s', first uninstall the extension and then "+ "install it from the desired source:", newerVersionSource, ), From b3aa4c9e39bb42d7a50e6ead805e62f49fea7d78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:37:46 +0000 Subject: [PATCH 6/8] Remove unnecessary HidePrefix field assignment The HidePrefix field defaults to false, so explicitly setting it is unnecessary Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com> --- cli/azd/cmd/extension.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index a3132a5bce3..59405aefaea 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -1084,7 +1084,6 @@ func (a *extensionUpgradeAction) checkForNewerVersionInOtherSources( output.WithHighLightFormat(currentSource), output.WithHighLightFormat(currentLatestVersion), ), - HidePrefix: false, }) a.console.Message(ctx, "") a.console.Message(ctx, From c7f0de44f6f25eb1110b953067e19a953b60aba0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:38:52 +0000 Subject: [PATCH 7/8] Improve code readability in warning message formatting - Extract warning description to variable for better readability - Extract instruction message to variable - Extract command strings to variables before formatting - Make code more maintainable and easier to read Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com> --- cli/azd/cmd/extension.go | 55 +++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index 59405aefaea..dfa7ac0f4b0 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -1073,40 +1073,31 @@ func (a *extensionUpgradeAction) checkForNewerVersionInOtherSources( // Display warning if a newer version was found if newestSemver != nil { - a.console.MessageUxItem(ctx, &ux.WarningMessage{ - Description: fmt.Sprintf( - "A newer version (%s) of %s is available in source '%s', "+ - "but the extension was installed from source '%s'. "+ - "The extension will be upgraded to version %s from the original source.", - output.WithHighLightFormat(newestSemver.String()), - output.WithHighLightFormat(extensionId), - output.WithHighLightFormat(newerVersionSource), - output.WithHighLightFormat(currentSource), - output.WithHighLightFormat(currentLatestVersion), - ), - }) - a.console.Message(ctx, "") - a.console.Message(ctx, - fmt.Sprintf( - "To switch to the newer version from '%s', first uninstall the extension and then "+ - "install it from the desired source:", - newerVersionSource, - ), - ) - a.console.Message(ctx, - fmt.Sprintf( - " %s", - output.WithHighLightFormat(fmt.Sprintf("azd extension uninstall %s", extensionId)), - ), + warningDesc := fmt.Sprintf( + "A newer version (%s) of %s is available in source '%s', but the extension was installed "+ + "from source '%s'. The extension will be upgraded to version %s from the original source.", + output.WithHighLightFormat(newestSemver.String()), + output.WithHighLightFormat(extensionId), + output.WithHighLightFormat(newerVersionSource), + output.WithHighLightFormat(currentSource), + output.WithHighLightFormat(currentLatestVersion), ) - a.console.Message(ctx, - fmt.Sprintf( - " %s", - output.WithHighLightFormat( - fmt.Sprintf("azd extension install %s --source %s", extensionId, newerVersionSource), - ), - ), + a.console.MessageUxItem(ctx, &ux.WarningMessage{Description: warningDesc}) + + a.console.Message(ctx, "") + instructionMsg := fmt.Sprintf( + "To switch to the newer version from '%s', first uninstall the extension and then "+ + "install it from the desired source:", + newerVersionSource, ) + a.console.Message(ctx, instructionMsg) + + uninstallCmd := fmt.Sprintf("azd extension uninstall %s", extensionId) + a.console.Message(ctx, fmt.Sprintf(" %s", output.WithHighLightFormat(uninstallCmd))) + + installCmd := fmt.Sprintf("azd extension install %s --source %s", extensionId, newerVersionSource) + a.console.Message(ctx, fmt.Sprintf(" %s", output.WithHighLightFormat(installCmd))) + a.console.Message(ctx, "") } From 9cd64a9e461975e69ddf9264cdfe4aedb7957376 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:42:52 +0000 Subject: [PATCH 8/8] Honor installed extension source during upgrade Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com> --- cli/azd/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/go.mod b/cli/azd/go.mod index e9f0e57b2ea..c401f81475b 100644 --- a/cli/azd/go.mod +++ b/cli/azd/go.mod @@ -51,6 +51,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/invopop/jsonschema v0.13.0 + github.com/jmespath-community/go-jmespath v1.1.1 github.com/joho/godotenv v1.5.1 github.com/mark3labs/mcp-go v0.41.1 github.com/mattn/go-colorable v0.1.14 @@ -115,7 +116,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jmespath-community/go-jmespath v1.1.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kylelemons/godebug v1.1.0 // indirect