diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index 72ace3e83ed..dfa7ac0f4b0 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -913,9 +913,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,17 +934,46 @@ 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) } - selectedExtension, err := selectDistinctExtension(ctx, a.console, extensionId, matches, a.flags.global) - if err != nil { - return nil, err + // 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) 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 + // 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, + ), + }) + } + } + // Parse semantic versions for proper comparison installedSemver, err := semver.NewVersion(installed.Version) if err != nil { @@ -982,6 +1018,92 @@ 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 newestSemver *semver.Version + + 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 than current AND newer than what we've found so far + if latestSemver.GreaterThan(currentSemver) { + if newestSemver == nil || latestSemver.GreaterThan(newestSemver) { + newestSemver = latestSemver + newerVersionSource = match.Source + } + } + } + + // Display warning if a newer version was found + if newestSemver != nil { + 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.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, "") + } + + 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..2f82ba3a77b 100644 --- a/cli/azd/cmd/extensions_test.go +++ b/cli/azd/cmd/extensions_test.go @@ -176,3 +176,4 @@ 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) } + 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