From df773fb88985d9a5c32c691d905931f5b8b70b67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:10:03 +0000 Subject: [PATCH 01/18] Initial plan From e15758cf05cee1c6c949ce9c9b3523d07bafa496 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:21:51 +0000 Subject: [PATCH 02/18] Clarify extension list latest version column Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> --- cli/azd/AGENTS.md | 6 ++++++ cli/azd/cmd/extension.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cli/azd/AGENTS.md b/cli/azd/AGENTS.md index d4abfa37846..fd94f4e2903 100644 --- a/cli/azd/AGENTS.md +++ b/cli/azd/AGENTS.md @@ -60,6 +60,12 @@ go test ./... ``` +Notes: +- Tests in `cli/azd/cmd` that exercise the azd CLI (like `TestFigSpec`/`TestUsage`) expect a built `azd` binary at + `cli/azd/azd`. Run `go build` in `cli/azd` before running them locally. +- When manually checking `azd extension list` output, add a local source first: + `go run . extension source add -n local -t file -l ./extensions/registry.json`. + Test file patterns: - Unit tests: `*_test.go` alongside source files - Functional tests: `test/functional/` diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index 4aaf1f1c653..34007758ab0 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -260,7 +260,7 @@ func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, e ValueTemplate: "{{.Name}}", }, { - Heading: "Version", + Heading: "Latest Version", ValueTemplate: `{{.Version}}`, }, { From 1e78f4bbeaa8801168bd0b59337e0e946e627acc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:26:09 +0000 Subject: [PATCH 03/18] Update plan after tests and screenshot Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> --- cli/azd/extensions/azure.ai.finetune/go.mod | 2 +- cli/azd/extensions/microsoft.azd.concurx/go.mod | 4 ++-- cli/azd/extensions/microsoft.azd.concurx/go.sum | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/azd/extensions/azure.ai.finetune/go.mod b/cli/azd/extensions/azure.ai.finetune/go.mod index 90b0dd3d22c..c056931f2b3 100644 --- a/cli/azd/extensions/azure.ai.finetune/go.mod +++ b/cli/azd/extensions/azure.ai.finetune/go.mod @@ -12,6 +12,7 @@ require ( github.com/openai/openai-go/v3 v3.2.0 github.com/sethvargo/go-retry v0.3.0 github.com/spf13/cobra v1.10.1 + github.com/stretchr/testify v1.11.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -69,7 +70,6 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/stretchr/testify v1.11.1 // indirect github.com/theckman/yacspin v0.13.12 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.2.0 // indirect diff --git a/cli/azd/extensions/microsoft.azd.concurx/go.mod b/cli/azd/extensions/microsoft.azd.concurx/go.mod index 1164ab05cfb..f6ca13b2857 100644 --- a/cli/azd/extensions/microsoft.azd.concurx/go.mod +++ b/cli/azd/extensions/microsoft.azd.concurx/go.mod @@ -76,9 +76,9 @@ require ( go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/net v0.47.0 // indirect + golang.org/x/net v0.48.0 // indirect golang.org/x/sys v0.39.0 // indirect - golang.org/x/term v0.37.0 // indirect + golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect google.golang.org/grpc v1.76.0 // indirect diff --git a/cli/azd/extensions/microsoft.azd.concurx/go.sum b/cli/azd/extensions/microsoft.azd.concurx/go.sum index f9299daa7c5..a3768ddd5b4 100644 --- a/cli/azd/extensions/microsoft.azd.concurx/go.sum +++ b/cli/azd/extensions/microsoft.azd.concurx/go.sum @@ -205,8 +205,8 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -214,8 +214,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= From 81120a321bb8602a1c516eda51a1264c052013aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:26:51 +0000 Subject: [PATCH 04/18] Revert unintended dependency file changes Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> --- cli/azd/extensions/azure.ai.finetune/go.mod | 2 +- cli/azd/extensions/microsoft.azd.concurx/go.mod | 4 ++-- cli/azd/extensions/microsoft.azd.concurx/go.sum | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/azd/extensions/azure.ai.finetune/go.mod b/cli/azd/extensions/azure.ai.finetune/go.mod index c056931f2b3..90b0dd3d22c 100644 --- a/cli/azd/extensions/azure.ai.finetune/go.mod +++ b/cli/azd/extensions/azure.ai.finetune/go.mod @@ -12,7 +12,6 @@ require ( github.com/openai/openai-go/v3 v3.2.0 github.com/sethvargo/go-retry v0.3.0 github.com/spf13/cobra v1.10.1 - github.com/stretchr/testify v1.11.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -70,6 +69,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/theckman/yacspin v0.13.12 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.2.0 // indirect diff --git a/cli/azd/extensions/microsoft.azd.concurx/go.mod b/cli/azd/extensions/microsoft.azd.concurx/go.mod index f6ca13b2857..1164ab05cfb 100644 --- a/cli/azd/extensions/microsoft.azd.concurx/go.mod +++ b/cli/azd/extensions/microsoft.azd.concurx/go.mod @@ -76,9 +76,9 @@ require ( go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/sys v0.39.0 // indirect - golang.org/x/term v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.32.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect google.golang.org/grpc v1.76.0 // indirect diff --git a/cli/azd/extensions/microsoft.azd.concurx/go.sum b/cli/azd/extensions/microsoft.azd.concurx/go.sum index a3768ddd5b4..f9299daa7c5 100644 --- a/cli/azd/extensions/microsoft.azd.concurx/go.sum +++ b/cli/azd/extensions/microsoft.azd.concurx/go.sum @@ -205,8 +205,8 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -214,8 +214,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= From 4158a99e8e08d20b6d1927439a7ad6c013ac095a Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Sat, 24 Jan 2026 00:20:00 +0000 Subject: [PATCH 05/18] Add update availability JSON property and show asterisk besides outdated extensions --- cli/azd/cmd/extension.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index 34007758ab0..d20b0c921a7 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -179,6 +179,7 @@ type extensionListItem struct { Namespace string `json:"namespace"` Version string `json:"version"` InstalledVersion string `json:"installedVersion"` + UpdateAvailable bool `json:"updateAvailable"` Source string `json:"source"` } @@ -214,17 +215,29 @@ func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, e continue } + latestVersion := extension.Versions[len(extension.Versions)-1].Version + var installedVersion string + var updateAvailable bool + if installed { installedVersion = installedExtension.Version + + // Compare versions to determine if an update is available + installedSemver, installedErr := semver.NewVersion(installedExtension.Version) + latestSemver, latestErr := semver.NewVersion(latestVersion) + if installedErr == nil && latestErr == nil { + updateAvailable = latestSemver.GreaterThan(installedSemver) + } } extensionRows = append(extensionRows, extensionListItem{ Id: extension.Id, Name: extension.DisplayName, Namespace: extension.Namespace, - Version: extension.Versions[len(extension.Versions)-1].Version, + Version: latestVersion, InstalledVersion: installedVersion, + UpdateAvailable: updateAvailable, Source: extension.Source, }) } @@ -265,7 +278,7 @@ func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, e }, { Heading: "Installed Version", - ValueTemplate: `{{.InstalledVersion}}`, + ValueTemplate: `{{.InstalledVersion}}{{if .UpdateAvailable}}*{{end}}`, }, { Heading: "Source", From 586babcd19370b945eca5fe3866a3b46b10c2716 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Sat, 24 Jan 2026 00:20:12 +0000 Subject: [PATCH 06/18] Update AGENTS.md --- cli/azd/AGENTS.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/cli/azd/AGENTS.md b/cli/azd/AGENTS.md index fd94f4e2903..178f4b68b23 100644 --- a/cli/azd/AGENTS.md +++ b/cli/azd/AGENTS.md @@ -45,6 +45,8 @@ go build ### Test +**Note**: In CI environments like inside a GitHub coding agent session, run `go build` first as the automatic build is skipped and the azd binary must exist for tests that spawn the CLI process. This applies to snapshot tests and functional tests in `test/functional/`. + ```bash # Specific test go test ./pkg/project/... -run TestProjectConfig @@ -60,12 +62,6 @@ go test ./... ``` -Notes: -- Tests in `cli/azd/cmd` that exercise the azd CLI (like `TestFigSpec`/`TestUsage`) expect a built `azd` binary at - `cli/azd/azd`. Run `go build` in `cli/azd` before running them locally. -- When manually checking `azd extension list` output, add a local source first: - `go run . extension source add -n local -t file -l ./extensions/registry.json`. - Test file patterns: - Unit tests: `*_test.go` alongside source files - Functional tests: `test/functional/` @@ -177,7 +173,7 @@ go build Feature-specific docs are in `docs/` — refer to them as needed. Some key docs include: -- `docs/new-azd-command.md` - Adding new commands -- `docs/extension-framework.md` - Extension development using gRPC extension framework -- `docs/guiding-principles.md` - Design principles +- `docs/style-guidelines/new-azd-command.md` - Adding new commands +- `docs/extensions/extension-framework.md` - Extension development using gRPC extension framework +- `docs/style-guidelines/guiding-principles.md` - Design principles - `docs/tracing-in-azd.md` - Tracing/telemetry guidelines From eb6dac4b14d893e2764f95b43304bf2a4af4bf6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:39:01 +0000 Subject: [PATCH 07/18] Plan: address review comments for extension list Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> --- cli/azd/cmd/extension.go | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index d20b0c921a7..2e2356e0eeb 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -208,6 +208,10 @@ func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, e extensionRows := []extensionListItem{} for _, extension := range registryExtensions { + if len(extension.Versions) == 0 { + continue + } + installedExtension, has := installedExtensions[extension.Id] installed := has && installedExtension.Source == extension.Source @@ -262,8 +266,8 @@ func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, e var formatErr error - if a.formatter.Kind() == output.TableFormat { - columns := []output.Column{ + if a.formatter.Kind() == output.TableFormat { + columns := []output.Column{ { Heading: "Id", ValueTemplate: "{{.Id}}", @@ -286,12 +290,16 @@ func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, e }, } - formatErr = a.formatter.Format(extensionRows, a.writer, output.TableFormatterOptions{ - Columns: columns, - }) - } else { - formatErr = a.formatter.Format(extensionRows, a.writer, nil) - } + formatErr = a.formatter.Format(extensionRows, a.writer, output.TableFormatterOptions{ + Columns: columns, + }) + + if formatErr == nil { + fmt.Fprintln(a.writer, "* Update available") + } + } else { + formatErr = a.formatter.Format(extensionRows, a.writer, nil) + } return nil, formatErr } From 90f3652c3229c4ecae3fb837042ad3537c29ce65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:44:00 +0000 Subject: [PATCH 08/18] Show update legend only when needed Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> --- cli/azd/cmd/extension.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index 2e2356e0eeb..4f0251174d8 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "slices" "strings" "text/tabwriter" @@ -290,13 +291,15 @@ func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, e }, } - formatErr = a.formatter.Format(extensionRows, a.writer, output.TableFormatterOptions{ - Columns: columns, - }) + formatErr = a.formatter.Format(extensionRows, a.writer, output.TableFormatterOptions{ + Columns: columns, + }) - if formatErr == nil { - fmt.Fprintln(a.writer, "* Update available") - } + if formatErr == nil && slices.ContainsFunc(extensionRows, func(row extensionListItem) bool { + return row.UpdateAvailable + }) { + fmt.Fprintln(a.writer, "* Update available") + } } else { formatErr = a.formatter.Format(extensionRows, a.writer, nil) } From f49e75bec4edc5f9c00ec4ca49bcd1f84c29decc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:45:45 +0000 Subject: [PATCH 09/18] Fix extension list formatting indentation Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> --- cli/azd/cmd/extension.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index 4f0251174d8..4a2b9d9c6fd 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -267,8 +267,8 @@ func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, e var formatErr error - if a.formatter.Kind() == output.TableFormat { - columns := []output.Column{ + if a.formatter.Kind() == output.TableFormat { + columns := []output.Column{ { Heading: "Id", ValueTemplate: "{{.Id}}", @@ -300,9 +300,9 @@ func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, e }) { fmt.Fprintln(a.writer, "* Update available") } - } else { - formatErr = a.formatter.Format(extensionRows, a.writer, nil) - } + } else { + formatErr = a.formatter.Format(extensionRows, a.writer, nil) + } return nil, formatErr } From 271fa4c7029e0b21d2298de359fca260b3457018 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:50:28 +0000 Subject: [PATCH 10/18] Add upgrade hints to extension list output Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> --- cli/azd/cmd/extension.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index 4a2b9d9c6fd..5b42cfcab9e 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -299,6 +299,10 @@ func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, e return row.UpdateAvailable }) { fmt.Fprintln(a.writer, "* Update available") + fmt.Fprintf(a.writer, "To upgrade: %s\n", + output.WithHighLightFormat("azd extension upgrade ")) + fmt.Fprintf(a.writer, "To upgrade all: %s\n", + output.WithHighLightFormat("azd extension upgrade --all")) } } else { formatErr = a.formatter.Format(extensionRows, a.writer, nil) From 277c719fd4be6294bdcdbc9133ea6bd49e24b4a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:54:51 +0000 Subject: [PATCH 11/18] Clarify version columns and update hints in `azd ext list` Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> --- cli/azd/extensions/azure.ai.finetune/go.mod | 2 +- cli/azd/extensions/microsoft.azd.concurx/go.mod | 4 ++-- cli/azd/extensions/microsoft.azd.concurx/go.sum | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/azd/extensions/azure.ai.finetune/go.mod b/cli/azd/extensions/azure.ai.finetune/go.mod index 90b0dd3d22c..c056931f2b3 100644 --- a/cli/azd/extensions/azure.ai.finetune/go.mod +++ b/cli/azd/extensions/azure.ai.finetune/go.mod @@ -12,6 +12,7 @@ require ( github.com/openai/openai-go/v3 v3.2.0 github.com/sethvargo/go-retry v0.3.0 github.com/spf13/cobra v1.10.1 + github.com/stretchr/testify v1.11.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -69,7 +70,6 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/stretchr/testify v1.11.1 // indirect github.com/theckman/yacspin v0.13.12 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.2.0 // indirect diff --git a/cli/azd/extensions/microsoft.azd.concurx/go.mod b/cli/azd/extensions/microsoft.azd.concurx/go.mod index 1164ab05cfb..f6ca13b2857 100644 --- a/cli/azd/extensions/microsoft.azd.concurx/go.mod +++ b/cli/azd/extensions/microsoft.azd.concurx/go.mod @@ -76,9 +76,9 @@ require ( go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/net v0.47.0 // indirect + golang.org/x/net v0.48.0 // indirect golang.org/x/sys v0.39.0 // indirect - golang.org/x/term v0.37.0 // indirect + golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect google.golang.org/grpc v1.76.0 // indirect diff --git a/cli/azd/extensions/microsoft.azd.concurx/go.sum b/cli/azd/extensions/microsoft.azd.concurx/go.sum index f9299daa7c5..a3768ddd5b4 100644 --- a/cli/azd/extensions/microsoft.azd.concurx/go.sum +++ b/cli/azd/extensions/microsoft.azd.concurx/go.sum @@ -205,8 +205,8 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -214,8 +214,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= From ef5c938076e149f282a844e892e77501a057c994 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Sat, 24 Jan 2026 19:36:14 +0000 Subject: [PATCH 12/18] Use console.Message --- cli/azd/cmd/extension.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index 5b42cfcab9e..5c8931df085 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -298,11 +298,9 @@ func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, e if formatErr == nil && slices.ContainsFunc(extensionRows, func(row extensionListItem) bool { return row.UpdateAvailable }) { - fmt.Fprintln(a.writer, "* Update available") - fmt.Fprintf(a.writer, "To upgrade: %s\n", - output.WithHighLightFormat("azd extension upgrade ")) - fmt.Fprintf(a.writer, "To upgrade all: %s\n", - output.WithHighLightFormat("azd extension upgrade --all")) + a.console.Message(ctx, "\n(*) Update available") + a.console.Message(ctx, fmt.Sprintf("To upgrade: %s", output.WithHighLightFormat("azd extension upgrade "))) + a.console.Message(ctx, fmt.Sprintf("To upgrade all: %s", output.WithHighLightFormat("azd extension upgrade --all"))) } } else { formatErr = a.formatter.Format(extensionRows, a.writer, nil) From ce6d4097fdd080d9e59e1f11bfa8210cfdc26801 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Sat, 24 Jan 2026 19:37:41 +0000 Subject: [PATCH 13/18] Fix cooldown logic and shallow copy --- cli/azd/cmd/extensions.go | 43 ++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/cli/azd/cmd/extensions.go b/cli/azd/cmd/extensions.go index 0f21792b612..121e29465e8 100644 --- a/cli/azd/cmd/extensions.go +++ b/cli/azd/cmd/extensions.go @@ -9,6 +9,7 @@ import ( "log" "os" "strings" + "time" "github.com/azure/azure-dev/cli/azd/cmd/actions" "github.com/azure/azure-dev/cli/azd/internal/grpcserver" @@ -160,14 +161,12 @@ func (a *extensionAction) Run(ctx context.Context) (*actions.ActionResult, error showUpdateWarning := !isJsonOutputFromArgs(os.Args) if showUpdateWarning { updateResultChan := make(chan *updateCheckOutcome, 1) - // Create a shallow copy of extension data for the goroutine to avoid race condition. - // The goroutine mutates LastUpdateWarning, so we copy only the needed fields. + // Create a minimal copy with only the fields needed for update checking. // Cannot copy the full Extension due to sync.Once (contains sync.noCopy). + // The goroutine will re-fetch the full extension from config when saving. extForCheck := &extensions.Extension{ Id: extension.Id, - Namespace: extension.Namespace, DisplayName: extension.DisplayName, - Description: extension.Description, Version: extension.Version, Source: extension.Source, LastUpdateWarning: extension.LastUpdateWarning, @@ -183,9 +182,12 @@ func (a *extensionAction) Run(ctx context.Context) (*actions.ActionResult, error if result != nil && result.shouldShow && result.warning != nil { a.console.MessageUxItem(ctx, result.warning) a.console.Message(ctx, "") + + // Record cooldown only after warning is actually displayed + a.recordUpdateWarningShown(result.extensionId) } default: - // Check didn't complete in time, skip warning + // Check didn't complete in time, skip warning (and don't record cooldown) } }() } @@ -256,8 +258,9 @@ func (a *extensionAction) Run(ctx context.Context) (*actions.ActionResult, error // updateCheckOutcome holds the result of an async update check type updateCheckOutcome struct { - shouldShow bool - warning *ux.WarningMessage + shouldShow bool + warning *ux.WarningMessage + extensionId string // Used to record cooldown only when warning is actually displayed } // checkForUpdateAsync performs the update check in a goroutine and sends the result to the channel. @@ -306,17 +309,29 @@ func (a *extensionAction) checkForUpdateAsync( if result.HasUpdate { outcome.shouldShow = true outcome.warning = extensions.FormatUpdateWarning(result) + outcome.extensionId = extension.Id + // Note: Cooldown is recorded by caller only when warning is actually displayed + } - // Record that we showed the warning (updates extension's LastUpdateWarning field) - updateChecker.RecordWarningShown(extension) + resultChan <- outcome +} - // Save the updated extension to config - if err := a.extensionManager.UpdateInstalled(extension); err != nil { - log.Printf("failed to save warning timestamp: %v", err) - } +// recordUpdateWarningShown saves the cooldown timestamp after a warning is displayed +func (a *extensionAction) recordUpdateWarningShown(extensionId string) { + // Re-fetch the full extension from config to avoid overwriting fields + fullExtension, err := a.extensionManager.GetInstalled(extensions.FilterOptions{Id: extensionId}) + if err != nil { + log.Printf("failed to get extension for saving warning timestamp: %v", err) + return } - resultChan <- outcome + // Record the warning timestamp + fullExtension.LastUpdateWarning = time.Now().UTC().Format(time.RFC3339) + + // Save the updated extension to config + if err := a.extensionManager.UpdateInstalled(fullExtension); err != nil { + log.Printf("failed to save warning timestamp: %v", err) + } } // refreshCacheForSource attempts to refresh the cache for a specific source From c2c461850c78219be8cf153163d698aff040bfde Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Sat, 24 Jan 2026 19:50:47 +0000 Subject: [PATCH 14/18] Fix lll --- cli/azd/cmd/extension.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cli/azd/cmd/extension.go b/cli/azd/cmd/extension.go index 5c8931df085..05569bc7120 100644 --- a/cli/azd/cmd/extension.go +++ b/cli/azd/cmd/extension.go @@ -299,8 +299,10 @@ func (a *extensionListAction) Run(ctx context.Context) (*actions.ActionResult, e return row.UpdateAvailable }) { a.console.Message(ctx, "\n(*) Update available") - a.console.Message(ctx, fmt.Sprintf("To upgrade: %s", output.WithHighLightFormat("azd extension upgrade "))) - a.console.Message(ctx, fmt.Sprintf("To upgrade all: %s", output.WithHighLightFormat("azd extension upgrade --all"))) + a.console.Message(ctx, fmt.Sprintf( + " To upgrade: %s", output.WithHighLightFormat("azd extension upgrade "))) + a.console.Message(ctx, fmt.Sprintf( + " To upgrade all: %s", output.WithHighLightFormat("azd extension upgrade --all"))) } } else { formatErr = a.formatter.Format(extensionRows, a.writer, nil) From 4c6765d05829c05e4f082d9d71c426bc95294875 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Sat, 24 Jan 2026 19:59:37 +0000 Subject: [PATCH 15/18] Revert unintended go.mod/go.sum changes in extensions --- cli/azd/extensions/azure.ai.finetune/go.mod | 2 +- cli/azd/extensions/microsoft.azd.concurx/go.mod | 4 ++-- cli/azd/extensions/microsoft.azd.concurx/go.sum | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/azd/extensions/azure.ai.finetune/go.mod b/cli/azd/extensions/azure.ai.finetune/go.mod index c056931f2b3..90b0dd3d22c 100644 --- a/cli/azd/extensions/azure.ai.finetune/go.mod +++ b/cli/azd/extensions/azure.ai.finetune/go.mod @@ -12,7 +12,6 @@ require ( github.com/openai/openai-go/v3 v3.2.0 github.com/sethvargo/go-retry v0.3.0 github.com/spf13/cobra v1.10.1 - github.com/stretchr/testify v1.11.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -70,6 +69,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/theckman/yacspin v0.13.12 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.2.0 // indirect diff --git a/cli/azd/extensions/microsoft.azd.concurx/go.mod b/cli/azd/extensions/microsoft.azd.concurx/go.mod index f6ca13b2857..1164ab05cfb 100644 --- a/cli/azd/extensions/microsoft.azd.concurx/go.mod +++ b/cli/azd/extensions/microsoft.azd.concurx/go.mod @@ -76,9 +76,9 @@ require ( go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/sys v0.39.0 // indirect - golang.org/x/term v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.32.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect google.golang.org/grpc v1.76.0 // indirect diff --git a/cli/azd/extensions/microsoft.azd.concurx/go.sum b/cli/azd/extensions/microsoft.azd.concurx/go.sum index a3768ddd5b4..f9299daa7c5 100644 --- a/cli/azd/extensions/microsoft.azd.concurx/go.sum +++ b/cli/azd/extensions/microsoft.azd.concurx/go.sum @@ -205,8 +205,8 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -214,8 +214,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= From 26a1bd978bb359ac921db1dd008dce7743ffc8ae Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Sat, 24 Jan 2026 20:11:59 +0000 Subject: [PATCH 16/18] Filter by ID + source --- cli/azd/cmd/extensions.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/cli/azd/cmd/extensions.go b/cli/azd/cmd/extensions.go index 121e29465e8..a210b018e8e 100644 --- a/cli/azd/cmd/extensions.go +++ b/cli/azd/cmd/extensions.go @@ -184,7 +184,7 @@ func (a *extensionAction) Run(ctx context.Context) (*actions.ActionResult, error a.console.Message(ctx, "") // Record cooldown only after warning is actually displayed - a.recordUpdateWarningShown(result.extensionId) + a.recordUpdateWarningShown(result.extensionId, result.extensionSource) } default: // Check didn't complete in time, skip warning (and don't record cooldown) @@ -258,9 +258,10 @@ func (a *extensionAction) Run(ctx context.Context) (*actions.ActionResult, error // updateCheckOutcome holds the result of an async update check type updateCheckOutcome struct { - shouldShow bool - warning *ux.WarningMessage - extensionId string // Used to record cooldown only when warning is actually displayed + shouldShow bool + warning *ux.WarningMessage + extensionId string // Used to record cooldown only when warning is actually displayed + extensionSource string // Source of the extension for precise lookup } // checkForUpdateAsync performs the update check in a goroutine and sends the result to the channel. @@ -310,6 +311,7 @@ func (a *extensionAction) checkForUpdateAsync( outcome.shouldShow = true outcome.warning = extensions.FormatUpdateWarning(result) outcome.extensionId = extension.Id + outcome.extensionSource = extension.Source // Note: Cooldown is recorded by caller only when warning is actually displayed } @@ -317,9 +319,12 @@ func (a *extensionAction) checkForUpdateAsync( } // recordUpdateWarningShown saves the cooldown timestamp after a warning is displayed -func (a *extensionAction) recordUpdateWarningShown(extensionId string) { +func (a *extensionAction) recordUpdateWarningShown(extensionId, extensionSource string) { // Re-fetch the full extension from config to avoid overwriting fields - fullExtension, err := a.extensionManager.GetInstalled(extensions.FilterOptions{Id: extensionId}) + fullExtension, err := a.extensionManager.GetInstalled(extensions.FilterOptions{ + Id: extensionId, + Source: extensionSource, + }) if err != nil { log.Printf("failed to get extension for saving warning timestamp: %v", err) return From 8604ca8c59bee515dc1e645a7cbbb599dde31e3e Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Sat, 24 Jan 2026 20:15:14 +0000 Subject: [PATCH 17/18] Add test --- cli/azd/pkg/extensions/manager_test.go | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/cli/azd/pkg/extensions/manager_test.go b/cli/azd/pkg/extensions/manager_test.go index 519760e6098..be0b882e454 100644 --- a/cli/azd/pkg/extensions/manager_test.go +++ b/cli/azd/pkg/extensions/manager_test.go @@ -1127,3 +1127,60 @@ func Test_FetchAndCacheMetadata(t *testing.T) { // The actual Install() function logs this as a warning but doesn't fail }) } + +func Test_GetInstalled_WithSourceFilter(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("AZD_CONFIG_DIR", tempDir) + + mockContext := mocks.NewMockContext(context.Background()) + fileConfigManager := config.NewFileConfigManager(config.NewManager()) + userConfigManager := config.NewUserConfigManager(fileConfigManager) + + sourceManager := NewSourceManager(mockContext.Container, userConfigManager, mockContext.HttpClient) + lazyRunner := lazy.NewLazy(func() (*Runner, error) { + return NewRunner(mockContext.CommandRunner), nil + }) + + manager, err := NewManager(userConfigManager, sourceManager, lazyRunner, mockContext.HttpClient) + require.NoError(t, err) + + // Install same extension ID from two different sources + extensions := map[string]*Extension{ + "test.ext": { + Id: "test.ext", + Namespace: "test", + DisplayName: "Test Extension", + Version: "1.0.0", + Source: "azd", + Path: "extensions/test.ext/test-ext", + }, + } + + err = manager.userConfig.Set(installedConfigKey, extensions) + require.NoError(t, err) + + t.Run("filter by ID only", func(t *testing.T) { + ext, err := manager.GetInstalled(FilterOptions{Id: "test.ext"}) + require.NoError(t, err) + require.NotNil(t, ext) + require.Equal(t, "test.ext", ext.Id) + }) + + t.Run("filter by ID and Source", func(t *testing.T) { + ext, err := manager.GetInstalled(FilterOptions{Id: "test.ext", Source: "azd"}) + require.NoError(t, err) + require.NotNil(t, ext) + require.Equal(t, "test.ext", ext.Id) + require.Equal(t, "azd", ext.Source) + }) + + t.Run("filter by ID and wrong Source returns not found", func(t *testing.T) { + _, err := manager.GetInstalled(FilterOptions{Id: "test.ext", Source: "local"}) + require.ErrorIs(t, err, ErrInstalledExtensionNotFound) + }) + + t.Run("filter by non-existent ID returns not found", func(t *testing.T) { + _, err := manager.GetInstalled(FilterOptions{Id: "non.existent"}) + require.ErrorIs(t, err, ErrInstalledExtensionNotFound) + }) +} From 0ed528291eb64b3cac43efffcb8a7d21dcbd2d03 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Mon, 26 Jan 2026 18:28:49 +0000 Subject: [PATCH 18/18] Use helper function --- cli/azd/cmd/extensions.go | 3 +-- cli/azd/pkg/extensions/update_checker.go | 4 ++-- cli/azd/pkg/extensions/update_checker_test.go | 2 +- cli/azd/pkg/extensions/update_integration_test.go | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cli/azd/cmd/extensions.go b/cli/azd/cmd/extensions.go index a210b018e8e..e2956b0177f 100644 --- a/cli/azd/cmd/extensions.go +++ b/cli/azd/cmd/extensions.go @@ -9,7 +9,6 @@ import ( "log" "os" "strings" - "time" "github.com/azure/azure-dev/cli/azd/cmd/actions" "github.com/azure/azure-dev/cli/azd/internal/grpcserver" @@ -331,7 +330,7 @@ func (a *extensionAction) recordUpdateWarningShown(extensionId, extensionSource } // Record the warning timestamp - fullExtension.LastUpdateWarning = time.Now().UTC().Format(time.RFC3339) + extensions.RecordUpdateWarningShown(fullExtension) // Save the updated extension to config if err := a.extensionManager.UpdateInstalled(fullExtension); err != nil { diff --git a/cli/azd/pkg/extensions/update_checker.go b/cli/azd/pkg/extensions/update_checker.go index e4d26898c3d..a1da3316c68 100644 --- a/cli/azd/pkg/extensions/update_checker.go +++ b/cli/azd/pkg/extensions/update_checker.go @@ -104,9 +104,9 @@ func (c *UpdateChecker) ShouldShowWarning(extension *Extension) bool { return time.Now().UTC().After(lastTime.Add(warningCoolDownPeriod)) } -// RecordWarningShown updates the extension's LastUpdateWarning timestamp +// RecordUpdateWarningShown updates the extension's LastUpdateWarning timestamp. // Mutates the provided extension in place (caller should save it via Manager.UpdateInstalled) -func (c *UpdateChecker) RecordWarningShown(extension *Extension) { +func RecordUpdateWarningShown(extension *Extension) { extension.LastUpdateWarning = time.Now().UTC().Format(time.RFC3339) } diff --git a/cli/azd/pkg/extensions/update_checker_test.go b/cli/azd/pkg/extensions/update_checker_test.go index 7334bfd43d1..f8335feda8b 100644 --- a/cli/azd/pkg/extensions/update_checker_test.go +++ b/cli/azd/pkg/extensions/update_checker_test.go @@ -108,7 +108,7 @@ func Test_UpdateChecker_WarningCooldown(t *testing.T) { require.True(t, updateChecker.ShouldShowWarning(extension)) // Record warning shown (updates extension's LastUpdateWarning) - updateChecker.RecordWarningShown(extension) + RecordUpdateWarningShown(extension) require.NotEmpty(t, extension.LastUpdateWarning) // Should not show warning again (within cooldown) diff --git a/cli/azd/pkg/extensions/update_integration_test.go b/cli/azd/pkg/extensions/update_integration_test.go index 5c9cd75a34a..29824f37721 100644 --- a/cli/azd/pkg/extensions/update_integration_test.go +++ b/cli/azd/pkg/extensions/update_integration_test.go @@ -131,7 +131,7 @@ func Test_Integration_UpdateCheck_FullFlow(t *testing.T) { require.True(t, updateChecker.ShouldShowWarning(extension)) // Record warning (updates extension's LastUpdateWarning field) - updateChecker.RecordWarningShown(extension) + RecordUpdateWarningShown(extension) require.NotEmpty(t, extension.LastUpdateWarning) // Immediately after - should not show warning (cooldown)