Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions cli/azd/docs/extensions/extension-framework-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <command> [options]
version: 1.0.0
capabilities:
- framework-service-provider
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions cli/azd/extensions/microsoft.azd.extensions/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <command> [options]
version: 0.11.1
version: 0.11.2
capabilities:
- custom-commands
- metadata
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,63 @@ 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
},
})

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,
)
Comment thread
JeffreyCA marked this conversation as resolved.
}

func copyBinaryFiles(extensionId, sourcePath, destPath string) error {
if _, err := os.Stat(destPath); os.IsNotExist(err) {
if err := os.MkdirAll(destPath, os.ModePerm); err != nil {
Expand Down
Original file line number Diff line number Diff line change
@@ -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"))
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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/")
}
2 changes: 1 addition & 1 deletion cli/azd/extensions/microsoft.azd.extensions/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.11.1
0.11.2
Loading