From 13fca5af873e8f8af0a14551609cd5e84f22e1ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 20:44:52 +0000 Subject: [PATCH 1/5] Initial plan From 370b0ee02882434c13e627fad0dd66ca8a8b7af8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 20:52:58 +0000 Subject: [PATCH 2/5] Fix gitignore embedding and add local registry warning to azd x build Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> --- .../extension-framework-services.md | 19 ++++-- .../microsoft.azd.extensions/CHANGELOG.md | 5 ++ .../microsoft.azd.extensions/extension.yaml | 2 +- .../internal/cmd/build.go | 52 +++++++++++++++ .../internal/cmd/build_test.go | 65 +++++++++++++++++++ .../internal/resources/resources.go | 4 +- .../internal/resources/resources_test.go | 32 +++++++++ .../microsoft.azd.extensions/version.txt | 2 +- 8 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build_test.go create mode 100644 cli/azd/extensions/microsoft.azd.extensions/internal/resources/resources_test.go diff --git a/cli/azd/docs/extensions/extension-framework-services.md b/cli/azd/docs/extensions/extension-framework-services.md index 538966075b2..de012b016a2 100644 --- a/cli/azd/docs/extensions/extension-framework-services.md +++ b/cli/azd/docs/extensions/extension-framework-services.md @@ -30,12 +30,20 @@ id: my.custom.extension namespace: my.extension displayName: My Custom Language Extension description: Adds support for Rust programming language +usage: azd my extension [options] version: 1.0.0 capabilities: - framework-service-provider - lifecycle-events # Optional: for additional lifecycle hooks ``` +> **Note:** The `usage` property is required. If it is omitted, `azd extension` +> validation reports a "Missing usage information" warning, which is treated as an +> error when building or publishing the extension. The value is a free-form usage +> hint shown to users; it does not have to reference an azd command. For a +> framework-only extension whose sole job is to register a language, a short +> description such as `usage: Adds Rust language support to azd` is acceptable. + ### 2. Implement the FrameworkServiceProvider Interface Create a Go struct that implements the `azdext.FrameworkServiceProvider` interface: @@ -266,12 +274,13 @@ func newListenCommand() *cobra.Command { } defer azdClient.Close() - // Create your framework service provider - rustFrameworkProvider := NewRustFrameworkServiceProvider(azdClient) - - // Register it with the extension host + // Register your framework service provider with the extension host. + // WithFrameworkService takes a factory function that returns a new + // provider instance, so the provider is constructed lazily when needed. host := azdext.NewExtensionHost(azdClient). - WithFrameworkService("rust", rustFrameworkProvider) + WithFrameworkService("rust", func() azdext.FrameworkServiceProvider { + return NewRustFrameworkServiceProvider(azdClient) + }) // Start listening for events return host.Run(ctx) diff --git a/cli/azd/extensions/microsoft.azd.extensions/CHANGELOG.md b/cli/azd/extensions/microsoft.azd.extensions/CHANGELOG.md index d03dba2ff78..77b92b76f0d 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/CHANGELOG.md +++ b/cli/azd/extensions/microsoft.azd.extensions/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## 0.11.2 (2026-06-05) + +- [[#8552]](https://github.com/Azure/azure-dev/pull/8552) Embed language template dotfiles so generated extensions include a `.gitignore` (the Go template excludes `bin/`). +- [[#8552]](https://github.com/Azure/azure-dev/pull/8552) Warn during `azd x build` when the local extension source registry is missing or does not contain the extension, since the binaries are installed but the extension would not appear in `azd extension list`. + ## 0.11.1 (2026-06-03) - [[#8498]](https://github.com/Azure/azure-dev/pull/8498) Disable HTML escaping when writing `registry.json` during `azd x publish` and local registry creation. diff --git a/cli/azd/extensions/microsoft.azd.extensions/extension.yaml b/cli/azd/extensions/microsoft.azd.extensions/extension.yaml index 9ba6268900d..cd1cc4977b3 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/extension.yaml +++ b/cli/azd/extensions/microsoft.azd.extensions/extension.yaml @@ -5,7 +5,7 @@ language: go displayName: azd extensions Developer Kit description: This extension provides a set of tools for azd extension developers to test and debug their extensions. usage: azd x [options] -version: 0.11.1 +version: 0.11.2 capabilities: - custom-commands - metadata diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build.go b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build.go index a242847e7d9..cdbf2376df4 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build.go @@ -239,6 +239,16 @@ func runBuildAction(ctx context.Context, flags *buildFlags) error { ) } + // The binaries are installed, but `azd extension list` only surfaces + // extensions that are present in the local source registry. When the + // registry is missing (for example after a fresh clone or a rebuilt + // dev container) or does not yet contain this extension, surface a + // warning that points the user at `azd x pack` / `azd x publish`. + if warning := localRegistryWarning(azdConfigDir, schema.Id); warning != "" { + buildWarnings = append(buildWarnings, warning) + return ux.Warning, fmt.Errorf("extension is not registered in the local source registry; see details below") + } + return ux.Success, nil }, }) @@ -246,6 +256,48 @@ func runBuildAction(ctx context.Context, flags *buildFlags) error { return taskList.Run() } +// localRegistryWarning returns a human-friendly warning when the local extension +// source registry is missing or does not yet contain the given extension id. In +// those cases `azd x build` installs the binaries but the extension will not show +// up in `azd extension list`, so the message points the user at the commands that +// register it. An empty string is returned when the extension is registered. +func localRegistryWarning(azdConfigDir, extensionId string) string { + registryPath := filepath.Join(azdConfigDir, "registry.json") + + if _, err := os.Stat(registryPath); errors.Is(err, os.ErrNotExist) { + return fmt.Sprintf( + "The local extension source registry (%s) was not found. The extension binaries were "+ + "installed but the extension is not registered, so it will not appear in 'azd extension list'. "+ + "Run 'azd x pack' followed by 'azd x publish' to create the local registry and register the extension.", + registryPath, + ) + } + + registry, err := models.LoadRegistry(registryPath) + if err != nil { + // Surface load/parse failures so the user knows the registry is unusable. + return fmt.Sprintf( + "Failed to read the local extension source registry (%s): %v. The extension binaries were "+ + "installed but it may not appear in 'azd extension list'. Run 'azd x pack' followed by "+ + "'azd x publish' to register the extension.", + registryPath, err, + ) + } + + for _, extension := range registry.Extensions { + if extension.Id == extensionId { + return "" + } + } + + return fmt.Sprintf( + "Extension '%s' is not registered in the local extension source registry (%s). The extension binaries "+ + "were installed but it will not appear in 'azd extension list'. Run 'azd x pack' followed by "+ + "'azd x publish' to register the extension.", + extensionId, registryPath, + ) +} + func copyBinaryFiles(extensionId, sourcePath, destPath string) error { if _, err := os.Stat(destPath); os.IsNotExist(err) { if err := os.MkdirAll(destPath, os.ModePerm); err != nil { diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build_test.go b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build_test.go new file mode 100644 index 00000000000..2ede2132c52 --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build_test.go @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLocalRegistryWarning(t *testing.T) { + const extensionId = "my.custom.extension" + + t.Run("missing registry", func(t *testing.T) { + dir := t.TempDir() + + warning := localRegistryWarning(dir, extensionId) + + require.Contains(t, warning, "was not found") + require.Contains(t, warning, "azd x publish") + }) + + t.Run("extension not registered", func(t *testing.T) { + dir := t.TempDir() + registryPath := filepath.Join(dir, "registry.json") + require.NoError(t, os.WriteFile( + registryPath, + []byte(`{"extensions":[{"id":"some.other.extension"}]}`), + 0600, + )) + + warning := localRegistryWarning(dir, extensionId) + + require.Contains(t, warning, "is not registered") + require.Contains(t, warning, extensionId) + }) + + t.Run("extension registered", func(t *testing.T) { + dir := t.TempDir() + registryPath := filepath.Join(dir, "registry.json") + require.NoError(t, os.WriteFile( + registryPath, + []byte(`{"extensions":[{"id":"`+extensionId+`"}]}`), + 0600, + )) + + warning := localRegistryWarning(dir, extensionId) + + require.Empty(t, warning) + }) + + t.Run("invalid registry", func(t *testing.T) { + dir := t.TempDir() + registryPath := filepath.Join(dir, "registry.json") + require.NoError(t, os.WriteFile(registryPath, []byte("not-json"), 0600)) + + warning := localRegistryWarning(dir, extensionId) + + require.True(t, strings.HasPrefix(warning, "Failed to read")) + }) +} diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/resources.go b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/resources.go index 5648949ceff..a10c0cce713 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/resources.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/resources.go @@ -7,5 +7,7 @@ import ( "embed" ) -//go:embed languages +// The `all:` prefix ensures dotfiles such as `.gitignore` are embedded; without it +// `go:embed` skips files and directories whose names begin with `.` or `_`. +//go:embed all:languages var Languages embed.FS diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/resources_test.go b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/resources_test.go new file mode 100644 index 00000000000..d177710a10d --- /dev/null +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/resources_test.go @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package resources + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestGitignoreEmbedded verifies that the dotfiles (.gitignore) shipped with each +// language template are embedded. Without the `all:` prefix on the go:embed +// directive these files are silently skipped, which previously meant generated +// extensions had no .gitignore (so build artifacts under bin/ could be committed). +func TestGitignoreEmbedded(t *testing.T) { + for _, language := range []string{"go", "dotnet", "javascript", "python"} { + t.Run(language, func(t *testing.T) { + contents, err := Languages.ReadFile("languages/" + language + "/.gitignore") + require.NoError(t, err) + require.NotEmpty(t, contents) + }) + } +} + +// TestGoGitignoreExcludesBin ensures the generated Go extension ignores the build +// output directory so binaries are not accidentally committed. +func TestGoGitignoreExcludesBin(t *testing.T) { + contents, err := Languages.ReadFile("languages/go/.gitignore") + require.NoError(t, err) + require.Contains(t, string(contents), "bin/") +} diff --git a/cli/azd/extensions/microsoft.azd.extensions/version.txt b/cli/azd/extensions/microsoft.azd.extensions/version.txt index af88ba82486..a8839f70de0 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/version.txt +++ b/cli/azd/extensions/microsoft.azd.extensions/version.txt @@ -1 +1 @@ -0.11.1 +0.11.2 \ No newline at end of file From 96db3b1d5bfb07603245fa5e672b6ad441380e25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 20:54:45 +0000 Subject: [PATCH 3/5] Apply gofmt to embed directive and verify scaffolding Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> --- .../microsoft.azd.extensions/internal/resources/resources.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/resources.go b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/resources.go index a10c0cce713..3f54984f1e8 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/resources/resources.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/resources/resources.go @@ -9,5 +9,6 @@ import ( // The `all:` prefix ensures dotfiles such as `.gitignore` are embedded; without it // `go:embed` skips files and directories whose names begin with `.` or `_`. +// //go:embed all:languages var Languages embed.FS From 28ca73d4c67d3cc0babc105732caf8b9c5d97213 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 20:56:05 +0000 Subject: [PATCH 4/5] Correct usage doc note to reflect non-fatal warning behavior Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> --- cli/azd/docs/extensions/extension-framework-services.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/azd/docs/extensions/extension-framework-services.md b/cli/azd/docs/extensions/extension-framework-services.md index de012b016a2..f02e93bc958 100644 --- a/cli/azd/docs/extensions/extension-framework-services.md +++ b/cli/azd/docs/extensions/extension-framework-services.md @@ -37,10 +37,10 @@ capabilities: - lifecycle-events # Optional: for additional lifecycle hooks ``` -> **Note:** The `usage` property is required. If it is omitted, `azd extension` -> validation reports a "Missing usage information" warning, which is treated as an -> error when building or publishing the extension. The value is a free-form usage -> hint shown to users; it does not have to reference an azd command. For a +> **Note:** Set the `usage` property on every extension. If it is omitted, `azd x build` +> reports a non-fatal "Missing 'usage' field" warning (older azd versions treated this +> warning as a fatal error). The value is a free-form usage hint shown to users in +> `azd --help`; it does not have to reference an azd command. For a > framework-only extension whose sole job is to register a language, a short > description such as `usage: Adds Rust language support to azd` is acceptable. From 5b6b7d225d40cf1dfd953de8d15c7c8c2f83a335 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Fri, 5 Jun 2026 22:05:55 +0000 Subject: [PATCH 5/5] Address feedback --- .../extension-framework-services.md | 7 ---- .../internal/cmd/build.go | 36 +++++++++---------- .../internal/cmd/build_test.go | 4 +-- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/cli/azd/docs/extensions/extension-framework-services.md b/cli/azd/docs/extensions/extension-framework-services.md index f02e93bc958..ba2f9c97ae1 100644 --- a/cli/azd/docs/extensions/extension-framework-services.md +++ b/cli/azd/docs/extensions/extension-framework-services.md @@ -37,13 +37,6 @@ capabilities: - lifecycle-events # Optional: for additional lifecycle hooks ``` -> **Note:** Set the `usage` property on every extension. If it is omitted, `azd x build` -> reports a non-fatal "Missing 'usage' field" warning (older azd versions treated this -> warning as a fatal error). The value is a free-form usage hint shown to users in -> `azd --help`; it does not have to reference an azd command. For a -> framework-only extension whose sole job is to register a language, a short -> description such as `usage: Adds Rust language support to azd` is acceptable. - ### 2. Implement the FrameworkServiceProvider Interface Create a Go struct that implements the `azdext.FrameworkServiceProvider` interface: diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build.go b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build.go index cdbf2376df4..6d7cc98e486 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build.go @@ -243,10 +243,10 @@ func runBuildAction(ctx context.Context, flags *buildFlags) error { // extensions that are present in the local source registry. When the // registry is missing (for example after a fresh clone or a rebuilt // dev container) or does not yet contain this extension, surface a - // warning that points the user at `azd x pack` / `azd x publish`. + // warning that points the user at `azd x publish`. if warning := localRegistryWarning(azdConfigDir, schema.Id); warning != "" { buildWarnings = append(buildWarnings, warning) - return ux.Warning, fmt.Errorf("extension is not registered in the local source registry; see details below") + return ux.Warning, fmt.Errorf("not registered in the local registry; see details below") } return ux.Success, nil @@ -256,20 +256,22 @@ func runBuildAction(ctx context.Context, flags *buildFlags) error { return taskList.Run() } -// localRegistryWarning returns a human-friendly warning when the local extension -// source registry is missing or does not yet contain the given extension id. In -// those cases `azd x build` installs the binaries but the extension will not show -// up in `azd extension list`, so the message points the user at the commands that -// register it. An empty string is returned when the extension is registered. +// localRegistryWarning returns a short warning when the local extension source +// registry is missing or does not yet contain the given extension id. In those +// cases `azd x build` installs the binaries but the extension will not show up in +// `azd extension list`, so the message points the user at `azd x pack` followed by +// `azd x publish`, which register it. An empty string is returned when the +// extension is registered. func localRegistryWarning(azdConfigDir, extensionId string) string { registryPath := filepath.Join(azdConfigDir, "registry.json") + registryDisplay := output.WithGrayFormat(registryPath) + listCmd := output.WithHighLightFormat("azd ext list") + registerCmds := output.WithHighLightFormat("azd x pack") + " then " + output.WithHighLightFormat("azd x publish") if _, err := os.Stat(registryPath); errors.Is(err, os.ErrNotExist) { return fmt.Sprintf( - "The local extension source registry (%s) was not found. The extension binaries were "+ - "installed but the extension is not registered, so it will not appear in 'azd extension list'. "+ - "Run 'azd x pack' followed by 'azd x publish' to create the local registry and register the extension.", - registryPath, + "Local registry not found (%s) — extension won't appear in %s. Run %s to register it.", + registryDisplay, listCmd, registerCmds, ) } @@ -277,10 +279,8 @@ func localRegistryWarning(azdConfigDir, extensionId string) string { if err != nil { // Surface load/parse failures so the user knows the registry is unusable. return fmt.Sprintf( - "Failed to read the local extension source registry (%s): %v. The extension binaries were "+ - "installed but it may not appear in 'azd extension list'. Run 'azd x pack' followed by "+ - "'azd x publish' to register the extension.", - registryPath, err, + "Failed to read the local registry (%s): %v. Run %s to register the extension.", + registryDisplay, err, registerCmds, ) } @@ -291,10 +291,8 @@ func localRegistryWarning(azdConfigDir, extensionId string) string { } return fmt.Sprintf( - "Extension '%s' is not registered in the local extension source registry (%s). The extension binaries "+ - "were installed but it will not appear in 'azd extension list'. Run 'azd x pack' followed by "+ - "'azd x publish' to register the extension.", - extensionId, registryPath, + "%s isn't registered in the local registry (%s), so it won't appear in %s. Run %s to register it.", + output.WithHighLightFormat(extensionId), registryDisplay, listCmd, registerCmds, ) } diff --git a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build_test.go b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build_test.go index 2ede2132c52..2b93b534aa2 100644 --- a/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build_test.go +++ b/cli/azd/extensions/microsoft.azd.extensions/internal/cmd/build_test.go @@ -20,7 +20,7 @@ func TestLocalRegistryWarning(t *testing.T) { warning := localRegistryWarning(dir, extensionId) - require.Contains(t, warning, "was not found") + require.Contains(t, warning, "not found") require.Contains(t, warning, "azd x publish") }) @@ -35,7 +35,7 @@ func TestLocalRegistryWarning(t *testing.T) { warning := localRegistryWarning(dir, extensionId) - require.Contains(t, warning, "is not registered") + require.Contains(t, warning, "isn't registered") require.Contains(t, warning, extensionId) })