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
27 changes: 24 additions & 3 deletions cli/azd/cmd/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@ type extensionInstallFlags struct {
version string
source string
force bool
channel string
global *internal.GlobalCommandOptions
}

Expand All @@ -742,6 +743,8 @@ func newExtensionInstallFlags(cmd *cobra.Command, global *internal.GlobalCommand
cmd.Flags().StringVarP(&flags.version, "version", "v", "", "The version of the extension to install")
cmd.Flags().
BoolVarP(&flags.force, "force", "f", false, "Force installation, including downgrades and reinstalls")
cmd.Flags().StringVar(&flags.channel, "channel", "",
"Pre-release channel to include when selecting the latest version (e.g. 'alpha')")

return flags
}
Expand Down Expand Up @@ -845,7 +848,7 @@ func (a *extensionInstallAction) Run(ctx context.Context) (*actions.ActionResult

// Check azd version compatibility
compatibleExtension, compatResult, err := resolveCompatibleExtension(
selectedExtension, extensionId, a.flags.version, azdVersion,
selectedExtension, extensionId, a.flags.version, azdVersion, a.flags.channel,
)
if err != nil {
a.console.StopSpinner(ctx, stepMessage, input.StepFailed)
Expand Down Expand Up @@ -1075,6 +1078,7 @@ type extensionUpgradeFlags struct {
source string
all bool
noDependencyUpgrades bool
channel string
global *internal.GlobalCommandOptions
}

Expand All @@ -1087,6 +1091,8 @@ func newExtensionUpgradeFlags(cmd *cobra.Command, global *internal.GlobalCommand
cmd.Flags().BoolVar(&flags.all, "all", false, "Upgrade all installed extensions")
cmd.Flags().BoolVar(&flags.noDependencyUpgrades, "no-dependency-upgrades", false,
"Do not upgrade dependencies when upgrading an extension that has dependencies")
cmd.Flags().StringVar(&flags.channel, "channel", "",
"Pre-release channel to include when selecting the latest version (e.g. 'alpha')")

return flags
}
Expand Down Expand Up @@ -1416,7 +1422,7 @@ func (a *extensionUpgradeAction) upgradeOneExtension(

// Check azd version compatibility
compatExt, compatResult, err := resolveCompatibleExtension(
selectedExt, extensionId, a.flags.version, azdVersion,
selectedExt, extensionId, a.flags.version, azdVersion, a.flags.channel,
)
if err != nil {
return fail(err)
Expand Down Expand Up @@ -2256,17 +2262,32 @@ func currentAzdSemver() *semver.Version {
// resolveCompatibleExtension filters extension versions for azd version compatibility.
// Returns the (possibly filtered) extension metadata and the compatibility result for displaying warnings.
// Returns an error if no compatible versions are found or the specific requested version is incompatible.
// When channel is empty, alpha pre-release versions are excluded before any other filtering.
// Pass channel="alpha" to include alpha nightly builds.
func resolveCompatibleExtension(
selectedExtension *extensions.ExtensionMetadata,
extensionId string,
requestedVersion string,
azdVersion *semver.Version,
channel string,
) (*extensions.ExtensionMetadata, *extensions.VersionCompatibilityResult, error) {
if azdVersion == nil {
return selectedExtension, nil, nil
}

if requestedVersion != "" && requestedVersion != "latest" {
// When no specific version is requested and no channel is set, exclude alpha builds.
if requestedVersion == "" || strings.EqualFold(requestedVersion, "latest") {
if !strings.EqualFold(channel, "alpha") {
filtered := extensions.ExcludePreReleaseChannel(selectedExtension.Versions, "alpha")
if len(filtered) != len(selectedExtension.Versions) {
filteredCopy := *selectedExtension
filteredCopy.Versions = filtered
selectedExtension = &filteredCopy
}
}
}

if requestedVersion != "" && !strings.EqualFold(requestedVersion, "latest") {
// Validate compatibility for the specific requested version
if err := validateVersionCompatibility(
selectedExtension.Versions, requestedVersion, extensionId, azdVersion,
Expand Down
58 changes: 52 additions & 6 deletions cli/azd/cmd/extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func TestResolveCompatibleExtension_NilAzdVersion(t *testing.T) {
},
}

result, compat, err := resolveCompatibleExtension(metadata, "test-ext", "", nil)
result, compat, err := resolveCompatibleExtension(metadata, "test-ext", "", nil, "")
require.NoError(t, err)
assert.Equal(t, metadata, result)
assert.Nil(t, compat)
Expand All @@ -272,7 +272,7 @@ func TestResolveCompatibleExtension_SpecificVersion(t *testing.T) {
},
}

result, compat, err := resolveCompatibleExtension(metadata, "test-ext", "0.1.0", azdVersion)
result, compat, err := resolveCompatibleExtension(metadata, "test-ext", "0.1.0", azdVersion, "")
require.NoError(t, err)
assert.Equal(t, metadata, result)
assert.Nil(t, compat)
Expand All @@ -292,7 +292,7 @@ func TestResolveCompatibleExtension_FilterVersions(t *testing.T) {
},
}

result, compat, err := resolveCompatibleExtension(metadata, "test-ext", "", azdVersion)
result, compat, err := resolveCompatibleExtension(metadata, "test-ext", "", azdVersion, "")
require.NoError(t, err)
require.NotNil(t, result)
require.NotNil(t, compat)
Expand All @@ -314,7 +314,7 @@ func TestResolveCompatibleExtension_NoCompatible(t *testing.T) {
},
}

_, compat, err := resolveCompatibleExtension(metadata, "test-ext", "", azdVersion)
_, compat, err := resolveCompatibleExtension(metadata, "test-ext", "", azdVersion, "")
require.Error(t, err)
assert.Contains(t, err.Error(), "no compatible version")
require.NotNil(t, compat)
Expand All @@ -334,7 +334,7 @@ func TestResolveCompatibleExtension_AllCompatible(t *testing.T) {
},
}

result, compat, err := resolveCompatibleExtension(metadata, "test-ext", "", azdVersion)
result, compat, err := resolveCompatibleExtension(metadata, "test-ext", "", azdVersion, "")
require.NoError(t, err)
assert.Equal(t, metadata, result) // same pointer, no filtering needed
require.NotNil(t, compat)
Expand All @@ -354,11 +354,57 @@ func TestResolveCompatibleExtension_LatestVersion(t *testing.T) {
}

// "latest" should go through the filter path, not the specific version path
result, _, err := resolveCompatibleExtension(metadata, "test-ext", "latest", azdVersion)
result, _, err := resolveCompatibleExtension(metadata, "test-ext", "latest", azdVersion, "")
require.NoError(t, err)
assert.Equal(t, metadata, result)
}

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

azdVersion, err := semver.NewVersion("1.24.0")
require.NoError(t, err)

metadata := &extensions.ExtensionMetadata{
Id: "test-ext",
Versions: []extensions.ExtensionVersion{
{Version: "0.9.0-preview.1"},
{Version: "0.9.0-alpha.202501010000"},
{Version: "0.10.0-alpha.202502010000"},
},
}

// Without --channel alpha: alpha versions are excluded; highest remaining is preview
result, _, err := resolveCompatibleExtension(metadata, "test-ext", "", azdVersion, "")
require.NoError(t, err)
require.Len(t, result.Versions, 1)
assert.Equal(t, "0.9.0-preview.1", result.Versions[0].Version)

// With --channel alpha: all versions are included
resultAlpha, _, err := resolveCompatibleExtension(metadata, "test-ext", "", azdVersion, "alpha")
require.NoError(t, err)
assert.Len(t, resultAlpha.Versions, 3)
}

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

azdVersion, err := semver.NewVersion("1.24.0")
require.NoError(t, err)

// When all available versions are alpha, ExcludePreReleaseChannel falls back to original list.
metadata := &extensions.ExtensionMetadata{
Id: "test-ext",
Versions: []extensions.ExtensionVersion{
{Version: "0.1.0-alpha.202501010000"},
},
}

result, _, err := resolveCompatibleExtension(metadata, "test-ext", "", azdVersion, "")
require.NoError(t, err)
assert.Len(t, result.Versions, 1)
}

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

Expand Down
18 changes: 18 additions & 0 deletions cli/azd/cmd/testdata/TestFigSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5647,6 +5647,15 @@ const completionSpec: Fig.Spec = {
name: ['install'],
description: 'Installs specified extensions.',
options: [
{
name: ['--channel'],
description: 'Pre-release channel to include when selecting the latest version (e.g. \'alpha\')',
args: [
{
name: 'channel',
},
],
},
{
name: ['--force', '-f'],
description: 'Force installation, including downgrades and reinstalls',
Expand Down Expand Up @@ -5810,6 +5819,15 @@ const completionSpec: Fig.Spec = {
name: ['--all'],
description: 'Upgrade all installed extensions',
},
{
name: ['--channel'],
description: 'Pre-release channel to include when selecting the latest version (e.g. \'alpha\')',
args: [
{
name: 'channel',
},
],
},
{
name: ['--no-dependency-upgrades'],
description: 'Do not upgrade dependencies when upgrading an extension that has dependencies',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Usage
azd extension install <extension-id> [flags]

Flags
--channel string : Pre-release channel to include when selecting the latest version (e.g. 'alpha')
-f, --force : Force installation, including downgrades and reinstalls
-s, --source string : The extension source to use for installs
-v, --version string : The version of the extension to install
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Usage

Flags
--all : Upgrade all installed extensions
--channel string : Pre-release channel to include when selecting the latest version (e.g. 'alpha')
--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
Expand Down
36 changes: 35 additions & 1 deletion cli/azd/docs/extensions/extension-resolution-and-versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ azd extension install my.extension --version latest
azd extension install my.extension
```

#### CLI `--channel` flag (pre-release opt-in)

When resolving `latest` (or when `--version` is omitted), **`alpha` pre-release builds are excluded by default** so that `stable`/`preview` users are never moved onto the nightly channel implicitly. Opt in with `--channel`:

```bash
# Include alpha nightly builds when resolving the latest version
azd extension install azure.ai.agents --source dev --channel alpha

# Keep alpha installs on the nightly channel during upgrades
azd extension upgrade --all --channel alpha
```

`--channel` is available on both `azd extension install` and `azd extension upgrade`. An explicit `--version <x>` always installs that exact version, including a named pre-release. If the only versions available for an extension are `alpha`, they are used as a fallback so the install does not dead-end.

#### `azure.yaml` `requiredVersions.extensions`

The `requiredVersions.extensions` section in `azure.yaml` supports the full semver constraint syntax provided by the [Masterminds semver](https://github.com/Masterminds/semver) library:
Expand Down Expand Up @@ -264,7 +278,7 @@ Use pre-release suffixes for testing before a stable release:
2.0.0-rc.1
```

When `latest` is specified (or the version is omitted), `azd` selects the **highest semantic version**, which can be a pre-release if it sorts higher than the latest stable version. For semver range constraints in `azure.yaml`, pre-release versions are generally excluded unless the constraint itself explicitly includes a pre-release identifier.
When `latest` is specified (or the version is omitted), `azd` selects the **highest semantic version**, which can be a pre-release if it sorts higher than the latest stable version. The one exception is the CLI `azd extension install`/`upgrade` path, which **excludes `alpha` pre-release builds (the nightly channel) by default** unless `--channel alpha` is passed; other pre-release identifiers (`beta`, `rc`, `preview`, …) remain eligible. For semver range constraints in `azure.yaml`, pre-release versions are generally excluded unless the constraint itself explicitly includes a pre-release identifier.

## Troubleshooting

Expand Down Expand Up @@ -346,6 +360,26 @@ When using experimental extensions, expect:

The dev registry is intended for early adopters, extension authors testing pre-release builds, and internal teams validating extensions before official publication.

### Nightly Builds

A scheduled pipeline (`eng/pipelines/release-ext-nightly.yml`) publishes **nightly pre-release builds** of the first-party `azure.ai.*` extensions to the dev registry once per day.

- **Versioning** — Each nightly build is stamped with a timestamped pre-release version derived from the extension's `version.txt`, in the form `<base>-alpha.<yyyyMMddHHmm>` (for example `0.1.39-preview.alpha.202606150300`). The timestamp is constant for all extensions in a given run, and newer builds always sort higher than older ones under semver.
- **Change detection** — Only extensions whose source (`cli/azd/extensions/<id>`) changed since their previous nightly are rebuilt and republished. If nothing changed, no pre-release is created and no registry pull request is opened.
- **Publishing** — Each changed extension is published as an unsigned GitHub pre-release tagged `azd-ext-<sanitized-id>_<version>`, and a single combined pull request updates `registry.dev.json` for all changed extensions.
- **Unsigned** — As with all dev registry binaries, nightly artifacts are **not signed**.

Because nightly builds use the `alpha` pre-release channel, they are **excluded from default `latest` resolution**. To pick up the latest nightly, opt in with `--channel alpha`:

```bash
azd extension install azure.ai.agents --source dev --channel alpha
```

Once installed from the `alpha` channel, use `azd extension upgrade --channel alpha` to keep moving onto newer nightly builds. The automatic update check is channel-matched: only `alpha` installs are notified about newer `alpha` builds, so `stable`/`preview` installs are never nudged onto the nightly channel.

> [!NOTE]
> Nightly pre-release versions accumulate in `registry.dev.json` over time. Pruning old nightly entries is handled separately and is not part of the nightly pipeline.

### Adding the Dev Registry

The dev registry is **not** configured by default. To opt in:
Expand Down
29 changes: 21 additions & 8 deletions cli/azd/pkg/extensions/registry_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,28 +167,41 @@ func (m *RegistryCacheManager) Set(
return nil
}

// GetExtensionLatestVersion finds an extension in the cache and returns its latest version
func (m *RegistryCacheManager) GetExtensionLatestVersion(
// GetExtensionVersions finds an extension in the cache and returns all of its known versions.
func (m *RegistryCacheManager) GetExtensionVersions(
ctx context.Context,
sourceName string,
extensionId string,
) (string, error) {
) ([]ExtensionVersion, error) {
cache, err := m.Get(ctx, sourceName)
if err != nil {
return "", err
return nil, err
}

for _, ext := range cache.Extensions {
if strings.EqualFold(ext.Id, extensionId) {
if len(ext.Versions) == 0 {
return "", fmt.Errorf("extension %s has no versions", extensionId)
return nil, fmt.Errorf("extension %s has no versions", extensionId)
}
latest := LatestVersion(ext.Versions)
return latest.Version, nil
return ext.Versions, nil
}
}

return "", fmt.Errorf("extension %s not found in cache", extensionId)
return nil, fmt.Errorf("extension %s not found in cache", extensionId)
}

// GetExtensionLatestVersion finds an extension in the cache and returns its latest version
func (m *RegistryCacheManager) GetExtensionLatestVersion(
ctx context.Context,
sourceName string,
extensionId string,
) (string, error) {
versions, err := m.GetExtensionVersions(ctx, sourceName, extensionId)
if err != nil {
return "", err
}

return LatestVersion(versions).Version, nil
}

// IsExpiredOrMissing checks if cache for a source needs refresh
Expand Down
13 changes: 11 additions & 2 deletions cli/azd/pkg/extensions/update_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,22 @@ func (c *UpdateChecker) CheckForUpdate(
}

// Get latest version from cache
latestVersion, err := c.cacheManager.GetExtensionLatestVersion(ctx, extension.Source, extension.Id)
versions, err := c.cacheManager.GetExtensionVersions(ctx, extension.Source, extension.Id)
if err != nil {
// Cache miss or extension not found - not an error, just no update info
log.Printf("could not get latest version for %s: %v", extension.Id, err)
log.Printf("could not get versions for %s: %v", extension.Id, err)
return result, nil
}

// Channel matching: only surface alpha pre-release builds when the installed
// version is itself an alpha build. Stable/preview installs ignore alpha so the
// auto-update prompt stays consistent with what 'azd extension upgrade' selects
// by default (without --channel alpha).
if !IsPreReleaseChannel(extension.Version, "alpha") {
versions = ExcludePreReleaseChannel(versions, "alpha")
}

latestVersion := LatestVersion(versions).Version
result.LatestVersion = latestVersion

// Compare versions using semver
Expand Down
Loading
Loading