Skip to content
Merged
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
344 changes: 298 additions & 46 deletions cli/azd/cmd/extension.go

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions cli/azd/cmd/extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,38 @@ func TestValidateVersionCompatibility(t *testing.T) {
})
}

func TestValidateExactVersionFlag(t *testing.T) {
t.Parallel()

tests := []struct {
name string
version string
wantErr bool
}{
{name: "empty", version: ""},
{name: "latest", version: "latest"},
{name: "semver", version: "1.2.3"},
{name: "prerelease", version: "1.2.3-preview.1"},
{name: "non_semver_version", version: "nightly"},
{name: "range", version: ">=1.2.3", wantErr: true},
{name: "tilde", version: "~1.2.0", wantErr: true},
{name: "wildcard", version: "1.x", wantErr: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

err := validateExactVersionFlag(tt.version)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}

func TestResolveCompatibleExtension_NilAzdVersion(t *testing.T) {
t.Parallel()

Expand Down
4 changes: 4 additions & 0 deletions cli/azd/cmd/testdata/TestFigSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3485,6 +3485,10 @@ const completionSpec: Fig.Spec = {
name: ['--all'],
description: 'Upgrade all installed extensions',
},
{
name: ['--no-dependency-upgrades'],
description: 'Do not upgrade dependencies when upgrading an extension that has dependencies',
},
{
name: ['--source', '-s'],
description: 'The extension source to use for upgrades',
Expand Down
7 changes: 4 additions & 3 deletions cli/azd/cmd/testdata/TestUsage-azd-extension-upgrade.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ Usage
azd extension upgrade [extension-id] [flags]

Flags
--all : Upgrade all installed extensions
-s, --source string : The extension source to use for upgrades
-v, --version string : The version of the extension to upgrade to
--all : Upgrade all installed extensions
--no-dependency-upgrades : Do not upgrade dependencies when upgrading an extension that has dependencies
-s, --source string : The extension source to use for upgrades
-v, --version string : The version of the extension to upgrade to

Global Flags
-C, --cwd string : Sets the current working directory.
Expand Down
37 changes: 34 additions & 3 deletions cli/azd/docs/extensions/extension-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Shows detailed information for a specific extension, including description, tags

Installs one or more extensions from any configured extension source.

- `-v, --version` Specifies the version constraint to apply when installing extensions. Supports any semver constraint notation.
- `-v, --version` Specifies the exact version to install.
- `-s, --source` Specifies the extension source used for installations.

#### `azd extension uninstall <extension-ids> [flags]`
Expand All @@ -136,8 +136,9 @@ Uninstalls one or more previously installed extensions.
Upgrades one or more extensions to the latest versions.

- `--all` Upgrades all previously installed extensions when specified.
- `-v, --version` Upgrades a specified extension using a semver version constraint, if provided.
- `-v, --version` Upgrades a specified extension to an exact version, if provided.
- `-s, --source` Specifies the extension source used for installations.
Comment thread
JeffreyCA marked this conversation as resolved.
- `--no-dependency-upgrades` Skips upgrading dependencies declared by extension packs.

## Developing Extensions

Expand Down Expand Up @@ -1001,13 +1002,16 @@ A [JSON schema](../../extensions/extension.schema.json) is available to support
**Required Properties:**
- `id`: Unique identifier for the extension
- `version`: Semantic version following MAJOR.MINOR.PATCH format
- `capabilities`: Array of extension capabilities (see below)
- `displayName`: Human-readable name of the extension
- `description`: Detailed description of the extension

Each manifest must include either `capabilities` or `dependencies`. Extension packs may omit `capabilities`
when they declare `dependencies` instead and have no executable.

**Optional Properties:**
- `namespace`: Command namespace for grouping extension commands
- `entryPoint`: Executable or script that serves as the entry point
- `capabilities`: Array of extension capabilities (see below)
- `usage`: Instructions on how to use the extension
- `examples`: Array of usage examples with name, description, and usage
- `tags`: Keywords for categorization and filtering
Expand Down Expand Up @@ -1109,6 +1113,33 @@ Dependencies support semantic versioning constraints:
- `~1.2.0`: Compatible with version 1.2.x
- `>=1.0.0 <2.0.0`: Range specification

azd installs or upgrades to the highest published version that satisfies the dependency constraint.
Pre-release versions follow standard semver constraint rules: a constraint must include a pre-release comparator to match pre-release versions.
For example, `>=0.1.0` does not match `0.1.31-preview`; use `>=0.1.0-0` or a pre-release range such as `~0.1.0-preview` when preview versions should be eligible.

#### Extension Packs

An extension pack is an extension manifest that groups related extensions so users can install them with one command. Packs declare `dependencies` but do not provide an executable, command namespace, or runtime capabilities of their own. Use a pack when you want to publish a curated set of extensions, such as a product family or scenario bundle, without asking users to install each extension separately.

```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/refs/heads/main/cli/azd/extensions/extension.schema.json

id: contoso.ai
displayName: Contoso AI Extension Pack
description: Installs the Contoso AI azd extensions.
version: 0.1.0

dependencies:
- id: contoso.ai.agents
version: "~0.1.0-preview"
- id: contoso.ai.models
version: "~0.1.0-preview"
```

Pack manifests must include at least one dependency. They may omit `capabilities`, `namespace`, `entryPoint`, `usage`, and `examples` when the pack has no commands of its own. Installing a pack installs its dependencies recursively from the same extension source as the pack. Dependency versions in the manifest support semver constraints, but command-line `--version` values for `azd extension install` and `azd extension upgrade` are exact versions.

Upgrading a pack upgrades the pack and, by default, reconciles installed dependencies to the highest published versions that satisfy the pack's declared dependency constraints. This dependency reconciliation still runs when the pack itself is already current, because an unchanged pack can point to a dependency range with newer matching versions. Users can disable automatic dependency upgrades with `azd extension upgrade <pack-id> --no-dependency-upgrades`.

#### Provider Registration

When your extension provides custom service targets or framework services, declare them in the `providers` section:
Expand Down
31 changes: 19 additions & 12 deletions cli/azd/extensions/extension.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@
}
},
"required": [
"id",
"version"
"id"
]
},
"Provider": {
Expand Down Expand Up @@ -113,7 +112,7 @@
"capabilities": {
"type": "array",
"title": "Capabilities",
"description": "List of capabilities provided by the extension. Supported values: custom-commands, lifecycle-events, mcp-server, service-target-provider, framework-service-provider, provisioning-provider, metadata. Select one or more from the allowed list. Each value must be unique.",
"description": "List of capabilities provided by the extension. Supported values: custom-commands, lifecycle-events, mcp-server, service-target-provider, framework-service-provider, provisioning-provider, metadata. Select one or more from the allowed list. Each value must be unique. Not required for extension packs, which declare dependencies instead and have no executable.",
"minItems": 1,
"uniqueItems": true,
"items": {
Expand Down Expand Up @@ -200,7 +199,8 @@
"description": "List of other extensions that this extension depends on. These will be resolved and installed automatically.",
"items": {
"$ref": "#/definitions/ExtensionDependency"
Comment thread
JeffreyCA marked this conversation as resolved.
}
},
"minItems": 1
},
"providers": {
"type": "array",
Expand Down Expand Up @@ -256,11 +256,18 @@
"required": ["serve"]
}
},
"required": [
"id",
"version",
"capabilities",
"displayName",
"description"
]
}
"required": [
"id",
"version",
"displayName",
"description"
],
"anyOf": [
{
"required": ["capabilities"]
},
{
"required": ["dependencies"]
Comment thread
JeffreyCA marked this conversation as resolved.
}
]
}
13 changes: 13 additions & 0 deletions cli/azd/internal/tracing/fields/fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,19 @@ var (
Classification: SystemMetadata,
Purpose: FeatureInsight,
}
// ExtensionDependencyOf is the parent extension for a dependency upgrade.
ExtensionDependencyOf = AttributeKey{
Key: attribute.Key("extension.dependency_of"),
Classification: SystemMetadata,
Purpose: FeatureInsight,
}
// ExtensionDependencyUpgradeCount is the recursive dependency upgrade count.
ExtensionDependencyUpgradeCount = AttributeKey{
Key: attribute.Key("extension.dependency_upgrade_count"),
Classification: SystemMetadata,
Purpose: FeatureInsight,
IsMeasurement: true,
}
)

// Update related fields
Expand Down
Loading
Loading