diff --git a/cli/azd/docs/extensions/extension-framework-services.md b/cli/azd/docs/extensions/extension-framework-services.md index 538966075b2..ba2f9c97ae1 100644 --- a/cli/azd/docs/extensions/extension-framework-services.md +++ b/cli/azd/docs/extensions/extension-framework-services.md @@ -30,6 +30,7 @@ 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 @@ -266,12 +267,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..6d7cc98e486 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 publish`. + if warning := localRegistryWarning(azdConfigDir, schema.Id); warning != "" { + buildWarnings = append(buildWarnings, warning) + return ux.Warning, fmt.Errorf("not registered in the local registry; see details below") + } + return ux.Success, nil }, }) @@ -246,6 +256,46 @@ func runBuildAction(ctx context.Context, flags *buildFlags) error { return taskList.Run() } +// 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( + "Local registry not found (%s) — extension won't appear in %s. Run %s to register it.", + registryDisplay, listCmd, registerCmds, + ) + } + + 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 registry (%s): %v. Run %s to register the extension.", + registryDisplay, err, registerCmds, + ) + } + + for _, extension := range registry.Extensions { + if extension.Id == extensionId { + return "" + } + } + + return fmt.Sprintf( + "%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, + ) +} + 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..2b93b534aa2 --- /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, "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, "isn't 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..3f54984f1e8 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,8 @@ 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