Skip to content

Commit 4fcdb19

Browse files
Adding in support for making any extension pipeline a "nightly" publishing pipeline #8740
Most of this comes from the discussion I had with Daniel Jurek. Yes _the_ Daniel Jurek. Basically: - We know it's a "nightly" build because it's scheduled. That's it. - We don't push GitHub releases for nightlies (it's a lot of clutter). They only get pushed to our blob storage account. - Nightlies have their own branch for the registry ('nightly') and their own registry (cli/azd/extensions/registry.nightly.json). To enable this for an extension, just add a nightly publish trigger in Azure DevOps (not as part of the YAML itself). I've tested this with a few builds, and even did an upgrade after the extension was pushed: Here's the latest build of the azd-demo that produced a nightly build: [dev.azure.com, publishing microsoft.azd.demo as a nightly build](https://dev.azure.com/azure-sdk/internal/_build/results?buildId=6460907&view=results) First part of the work for #8729
1 parent bc95c20 commit 4fcdb19

9 files changed

Lines changed: 524 additions & 84 deletions

cli/azd/docs/extensions/extension-framework.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ azd extension source add -n dev -t url -l "https://aka.ms/azd/extensions/registr
8484

8585
Extensions installed from the dev registry are automatically promoted to the main registry when a newer version becomes available there. See the [Dev/Experimental Extension Registry](./extension-resolution-and-versioning.md#devexperimental-extension-registry) section for full details on stability expectations, submission guidelines, promotion behavior, and troubleshooting.
8686

87+
A separate **nightly** registry distributes always-latest, automatically built snapshots of first-party extensions (signed on Windows/macOS, built from `main`). To opt in:
88+
89+
```bash
90+
# Add a new extension source name 'nightly' to your `azd` configuration.
91+
azd extension source add -n nightly -t url -l "https://raw.githubusercontent.com/Azure/azure-dev/nightly/cli/azd/extensions/registry.nightly.json"
92+
```
93+
94+
See the [Nightly Extension Registry](./extension-resolution-and-versioning.md#nightly-extension-registry) section for version semantics, promotion behavior, and caveats.
95+
8796
#### `azd extension source list`
8897

8998
Displays a list of installed extension sources.

cli/azd/docs/extensions/extension-resolution-and-versioning.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,54 @@ If the dev registry URL is unreachable (network issue, DNS failure), operations
618618
azd extension source remove dev
619619
```
620620

621+
## Nightly Extension Registry
622+
623+
The nightly registry contains **automatically built, always-latest** development snapshots of first-party extensions. Each scheduled pipeline run rebuilds an extension from `main`, signs the Windows and macOS binaries, uploads them to an always-latest storage folder, and updates a single entry in the nightly registry. Installing a nightly always gives you the most recent nightly build available at that time.
624+
625+
| Property | Main Registry | Nightly Registry |
626+
|----------|---------------|------------------|
627+
| URL | `https://aka.ms/azd/extensions/registry` | `https://raw.githubusercontent.com/Azure/azure-dev/nightly/cli/azd/extensions/registry.nightly.json` |
628+
| Source file | `cli/azd/extensions/registry.json` (on `main`) | `cli/azd/extensions/registry.nightly.json` (on the `nightly` branch) |
629+
| Source name | `azd` (built-in default) | `nightly` (opt-in) |
630+
| Version shape | `1.2.3` | `1.2.3-nightly.<buildId>` (or `1.2.3-preview.nightly.<buildId>`) |
631+
| Signed binaries | Yes | Windows/macOS signed; Linux unsigned |
632+
| History retained | Yes | No — only the latest nightly per extension |
633+
| Support | Covered by Azure support | **Not covered** |
634+
635+
> [!CAUTION]
636+
> Nightly extensions are built from `main` and come with **no stability guarantees**. Only the current nightly version is retained - older nightly versions are not installable.
637+
638+
### Adding the Nightly Registry
639+
640+
The nightly registry must be added, manually. To opt in:
641+
642+
```bash
643+
# Add the nightly registry as a source named "nightly"
644+
azd extension source add -n nightly -t url -l "https://raw.githubusercontent.com/Azure/azure-dev/nightly/cli/azd/extensions/registry.nightly.json"
645+
```
646+
647+
Then, to install a nightly-built extension:
648+
649+
```bash
650+
azd extension install <extension-id> --source nightly
651+
```
652+
653+
To remove the nightly registry later:
654+
655+
```bash
656+
azd extension source remove nightly
657+
```
658+
659+
### Upgrade and Nightly→Main Promotion
660+
661+
Nightly versions use semver prerelease labels, so the standard `azd extension upgrade` flow works:
662+
663+
- A newer nightly (higher build id, or a higher base version) supersedes an older one, so `azd extension upgrade` pulls the latest nightly.
664+
- When the extension ships a **stable** release whose base version matches your nightly (for example stable `1.2.3` versus `1.2.3-nightly.200`), the stable release outranks the nightly and you are **automatically promoted** to the `azd` registry on your next upgrade.
665+
666+
> [!NOTE]
667+
> If your nightly was built from a **prerelease** base (for example `1.2.3-preview.nightly.60`), it sorts **above** the matching stable prerelease `1.2.3-preview`. In that case you are not promoted until the stable registry advances to a higher base version. This is expected semver precedence behavior.
668+
621669
## Related Documentation
622670

623671
| Document | Description |
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package extensions
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// Test_UpdateChecker_NightlyVersions verifies that the nightly version scheme
13+
// (X.Y.Z-nightly.<buildId> and X.Y.Z-preview.nightly.<buildId>) produces the
14+
// expected "has update" results through the same semver comparison azd uses at
15+
// runtime. These pin the upgrade behavior we rely on for nightlies.
16+
func Test_UpdateChecker_NightlyVersions(t *testing.T) {
17+
tests := []struct {
18+
name string
19+
installed string
20+
available []string // available versions in the source (latest is the max semver)
21+
wantUpdate bool
22+
}{
23+
{
24+
name: "newer nightly supersedes older nightly",
25+
installed: "1.2.3-nightly.100",
26+
available: []string{"1.2.3-nightly.100", "1.2.3-nightly.200"},
27+
wantUpdate: true,
28+
},
29+
{
30+
name: "same nightly is not an update",
31+
installed: "1.2.3-nightly.222",
32+
available: []string{"1.2.3-nightly.222"},
33+
wantUpdate: false,
34+
},
35+
{
36+
name: "base version bump supersedes older nightly",
37+
installed: "1.2.3-nightly.999",
38+
available: []string{"1.2.3-nightly.999", "1.2.4-nightly.1"},
39+
wantUpdate: true,
40+
},
41+
{
42+
name: "stable release supersedes nightly of same base",
43+
installed: "1.2.3-nightly.200",
44+
available: []string{"1.2.3-nightly.200", "1.2.3"},
45+
wantUpdate: true,
46+
},
47+
{
48+
// Documents the prerelease-base caveat: a nightly built off a
49+
// prerelease base sorts ABOVE the matching stable prerelease, so a
50+
// user on the stable preview sees the nightly as an update.
51+
name: "nightly off preview base outranks stable preview",
52+
installed: "1.2.3-preview",
53+
available: []string{"1.2.3-preview", "1.2.3-preview.nightly.60"},
54+
wantUpdate: true,
55+
},
56+
{
57+
name: "newer preview nightly supersedes older preview nightly",
58+
installed: "1.2.3-preview.nightly.50",
59+
available: []string{"1.2.3-preview.nightly.50", "1.2.3-preview.nightly.60"},
60+
wantUpdate: true,
61+
},
62+
}
63+
64+
for _, tt := range tests {
65+
t.Run(tt.name, func(t *testing.T) {
66+
t.Setenv("AZD_CONFIG_DIR", t.TempDir())
67+
68+
cacheManager, err := NewRegistryCacheManager()
69+
require.NoError(t, err)
70+
71+
ctx := t.Context()
72+
sourceName := "nightly"
73+
74+
versions := make([]ExtensionVersion, 0, len(tt.available))
75+
for _, v := range tt.available {
76+
versions = append(versions, ExtensionVersion{Version: v})
77+
}
78+
79+
err = cacheManager.Set(ctx, sourceName, []*ExtensionMetadata{
80+
{
81+
Id: "test.extension",
82+
DisplayName: "Test Extension",
83+
Versions: versions,
84+
},
85+
})
86+
require.NoError(t, err)
87+
88+
updateChecker := NewUpdateChecker(cacheManager)
89+
90+
result, err := updateChecker.CheckForUpdate(ctx, &Extension{
91+
Id: "test.extension",
92+
DisplayName: "Test Extension",
93+
Version: tt.installed,
94+
Source: sourceName,
95+
})
96+
require.NoError(t, err)
97+
require.Equal(t, tt.wantUpdate, result.HasUpdate)
98+
})
99+
}
100+
}
101+
102+
// Test_ResolveUpgradeSource_NightlyPromotion verifies how a nightly-sourced
103+
// install promotes (or not) to the stable "azd" registry given the chosen
104+
// version strings. Promotion happens only when the stable registry's latest
105+
// version is strictly greater (semver) than the installed nightly's.
106+
func Test_ResolveUpgradeSource_NightlyPromotion(t *testing.T) {
107+
makeExt := func(source string, versions ...string) *ExtensionMetadata {
108+
ext := &ExtensionMetadata{Id: "test.extension", Source: source}
109+
for _, v := range versions {
110+
ext.Versions = append(ext.Versions, ExtensionVersion{Version: v})
111+
}
112+
return ext
113+
}
114+
115+
tests := []struct {
116+
name string
117+
nightlyLatest string
118+
mainLatest string // empty => extension not in the stable registry
119+
wantPromotion bool
120+
wantSource string
121+
}{
122+
{
123+
name: "stable release promotes nightly of same base",
124+
nightlyLatest: "1.2.3-nightly.200",
125+
mainLatest: "1.2.3",
126+
wantPromotion: true,
127+
wantSource: MainRegistryName,
128+
},
129+
{
130+
name: "nightly off preview base stays on nightly (outranks stable preview)",
131+
nightlyLatest: "1.2.3-preview.nightly.60",
132+
mainLatest: "1.2.3-preview",
133+
wantPromotion: false,
134+
wantSource: "nightly",
135+
},
136+
{
137+
name: "higher stable base promotes preview nightly",
138+
nightlyLatest: "1.2.3-preview.nightly.60",
139+
mainLatest: "1.2.4",
140+
wantPromotion: true,
141+
wantSource: MainRegistryName,
142+
},
143+
{
144+
name: "no stable entry keeps user on nightly",
145+
nightlyLatest: "1.2.3-nightly.200",
146+
mainLatest: "",
147+
wantPromotion: false,
148+
wantSource: "nightly",
149+
},
150+
}
151+
152+
for _, tt := range tests {
153+
t.Run(tt.name, func(t *testing.T) {
154+
installed := &Extension{Id: "test.extension", Source: "nightly"}
155+
156+
allMatches := []*ExtensionMetadata{makeExt("nightly", tt.nightlyLatest)}
157+
if tt.mainLatest != "" {
158+
allMatches = append(allMatches, makeExt(MainRegistryName, tt.mainLatest))
159+
}
160+
161+
result := ResolveUpgradeSource(installed, allMatches, "")
162+
require.NotNil(t, result)
163+
require.Equal(t, tt.wantPromotion, result.IsPromotion)
164+
require.Equal(t, tt.wantSource, result.NewSource)
165+
})
166+
}
167+
}

0 commit comments

Comments
 (0)