From 3a85082a0cdd9dec228e94d535e14f405f4b9a11 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Thu, 15 Oct 2026 11:30:59 -0600 Subject: [PATCH 1/2] feat(provider): deprecate built-in MicroSandbox and update CLI alias --- cmd/provider/add.go | 8 +- cmd/provider/add_test.go | 172 ++++++++++++++++++ cmd/provider/list.go | 6 + cmd/provider/list_test.go | 113 ++++++++++++ desktop/src/main/__tests__/watcher.test.ts | 28 ++- desktop/src/main/watcher.ts | 2 + .../components/provider/ProviderCard.svelte | 6 + .../components/provider/ProviderCard.test.ts | 21 +++ .../components/provider/ProviderSheet.svelte | 9 + .../components/provider/ProviderSheet.test.ts | 26 +++ .../provider/ProviderWizard.test.ts | 2 +- desktop/src/renderer/src/lib/types/index.ts | 1 + e2e/tests/up/MICROSANDBOX.md | 5 + e2e/tests/up/microsandbox_session.go | 2 +- e2e/tests/up/provider_microsandbox.go | 23 ++- pkg/provider/microsandbox_source_test.go | 73 ++++++++ pkg/provider/resolve.go | 7 + pkg/workspace/provider.go | 9 +- pkg/workspace/provider_deprecation_test.go | 105 +++++++++++ .../external-runtime-rollout.mdx | 15 +- .../managing-providers/manage-providers.mdx | 24 ++- 21 files changed, 641 insertions(+), 16 deletions(-) create mode 100644 cmd/provider/add_test.go create mode 100644 cmd/provider/list_test.go create mode 100644 pkg/provider/microsandbox_source_test.go create mode 100644 pkg/workspace/provider_deprecation_test.go diff --git a/cmd/provider/add.go b/cmd/provider/add.go index 2bf7f026ba..5b41bff40d 100644 --- a/cmd/provider/add.go +++ b/cmd/provider/add.go @@ -20,6 +20,8 @@ import ( "github.com/spf13/cobra" ) +const currentMicrosandboxSource = "github.com/devsy-org/devsy-provider-microsandbox@v0.1.8" + // AddCmd holds the cmd flags. type AddCmd struct { *flags.GlobalFlags @@ -177,7 +179,11 @@ func (cmd *AddCmd) resolveAddSource( return resolvedAddSource{}, fmt.Errorf("specify either a URL or path, " + "e.g. devsy provider add https://path/to/my/provider.yaml") } - raw, source, err := provider.ResolveProvider(ctx, args[0]) + sourceInput := args[0] + if sourceInput == provider.MicrosandboxDriver { + sourceInput = currentMicrosandboxSource + } + raw, source, err := provider.ResolveProvider(ctx, sourceInput) if err != nil { return resolvedAddSource{}, err } diff --git a/cmd/provider/add_test.go b/cmd/provider/add_test.go new file mode 100644 index 0000000000..d26671c659 --- /dev/null +++ b/cmd/provider/add_test.go @@ -0,0 +1,172 @@ +package provider + +import ( + "bytes" + "context" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/config" + devsyhttp "github.com/devsy-org/devsy/pkg/http" + providerpkg "github.com/devsy-org/devsy/pkg/provider" + "github.com/stretchr/testify/suite" +) + +const externalMicrosandboxManifest = `name: microsandbox +version: v0.1.8 +agent: + path: devsy + driver: external + external: + binary: DEVSY_RUNTIME_DRIVER + binaries: + DEVSY_RUNTIME_DRIVER: + - os: linux + arch: amd64 + path: https://example.invalid/devsy-runtime + checksum: "0000000000000000000000000000000000000000000000000000000000000000" +exec: + command: echo hi + create: echo hi + delete: echo hi +` + +type addDeprecationSuite struct { + suite.Suite +} + +func TestAddDeprecationSuite(t *testing.T) { + suite.Run(t, new(addDeprecationSuite)) +} + +func (s *addDeprecationSuite) SetupTest() { + client := devsyhttp.GetHTTPClient() + previousTransport := client.Transport + client.Transport = providerAddRoundTripper(func(req *http.Request) (*http.Response, error) { + const manifestURL = "https://github.com/devsy-org/devsy-provider-microsandbox/" + + "releases/download/v0.1.8/provider.yaml" + if req.URL.String() != manifestURL { + s.T().Fatalf("unexpected source request: %s", req.URL) + } + return providerAddResponse(req, externalMicrosandboxManifest), nil + }) + s.T().Cleanup(func() { client.Transport = previousTransport }) +} + +func (s *addDeprecationSuite) TestMicrosandboxAliasUsesPinnedExternalProvider() { + cmd := &AddCmd{GlobalFlags: &flags.GlobalFlags{}} + for _, tc := range []struct { + name string + want string + }{ + {name: "custom-msb", want: "custom-msb"}, + {want: providerpkg.MicrosandboxDriver}, + } { + resolved, err := cmd.resolveAddSource( + context.Background(), + []string{providerpkg.MicrosandboxDriver}, + tc.name, + ) + s.Require().NoError(err) + s.Require().Equal(tc.want, resolved.name) + s.Require().Equal(currentMicrosandboxSource, resolved.source.Raw) + s.Require().Equal("devsy-org/devsy-provider-microsandbox", resolved.source.Github) + parsed, err := providerpkg.ParseProvider(bytes.NewReader(resolved.raw)) + s.Require().NoError(err) + s.Require().Equal(providerpkg.ExternalDriver, parsed.Agent.Driver) + } +} + +func (s *addDeprecationSuite) TestCloneAndOtherSourcesRemainUnchanged() { + cmd := &AddCmd{GlobalFlags: &flags.GlobalFlags{}, FromExisting: "docker"} + resolved, err := cmd.resolveAddSource( + context.Background(), + []string{providerpkg.MicrosandboxDriver}, + "named-copy", + ) + s.Require().NoError(err) + s.Require().Equal("named-copy", resolved.name) + s.Require().Nil(resolved.source) + + cmd.FromExisting = "" + manifest := filepath.Join(s.T().TempDir(), "provider.yaml") + err = os.WriteFile(manifest, []byte("name: parsed\nagent:\n path: devsy\n"), 0o600) + s.Require().NoError(err) + resolved, err = cmd.resolveAddSource(context.Background(), []string{manifest}, "custom") + s.Require().NoError(err) + s.Require().Equal("custom", resolved.name) + s.Require().Equal(manifest, resolved.source.File) +} + +func (s *addDeprecationSuite) TestExplicitDeprecatedSelectorUsesBuiltin() { + cmd := &AddCmd{GlobalFlags: &flags.GlobalFlags{}} + resolved, err := cmd.resolveAddSource( + context.Background(), + []string{providerpkg.DeprecatedMicrosandboxSource}, + "legacy-copy", + ) + s.Require().NoError(err) + s.Require().Equal(providerpkg.MicrosandboxDriver, resolved.source.Raw) + s.Require().True(resolved.source.Internal) + s.Require().Equal("legacy-copy", resolved.name) +} + +func (s *addDeprecationSuite) TestAliasDownloadFailureDoesNotInstallOrChangeDefault() { + s.T().Setenv("DEVSY_HOME", s.T().TempDir()) + s.T().Setenv("DEVSY_CONFIG", "") + config.ResetPathManager() + s.T().Cleanup(config.ResetPathManager) + client := devsyhttp.GetHTTPClient() + client.Transport = providerAddRoundTripper(func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusNotFound, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader("not found")), + Request: req, + }, nil + }) + cmd := &AddCmd{GlobalFlags: &flags.GlobalFlags{}} + devsyConfig := &config.Config{ + DefaultContext: config.DefaultContext, + Contexts: map[string]*config.ContextConfig{ + config.DefaultContext: { + DefaultProvider: "docker", + Providers: map[string]*config.ProviderConfig{}, + }, + }, + } + s.Require().NoError(config.SaveConfig(devsyConfig)) + configPath, err := config.GetConfigPath() + s.Require().NoError(err) + before, err := os.ReadFile(filepath.Clean(configPath)) + s.Require().NoError(err) + err = cmd.Run(context.Background(), devsyConfig, []string{providerpkg.MicrosandboxDriver}) + s.Require().Error(err) + s.Require().NotContains(devsyConfig.Current().Providers, providerpkg.MicrosandboxDriver) + s.Require().Equal("docker", devsyConfig.Current().DefaultProvider) + after, err := os.ReadFile(filepath.Clean(configPath)) + s.Require().NoError(err) + s.Require().Equal(before, after) + _, err = providerpkg.LoadProviderConfig(config.DefaultContext, providerpkg.MicrosandboxDriver) + s.Require().Error(err) +} + +func providerAddResponse(req *http.Request, body string) *http.Response { + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(body)), + Request: req, + } +} + +type providerAddRoundTripper func(*http.Request) (*http.Response, error) + +func (roundTrip providerAddRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return roundTrip(req) +} diff --git a/cmd/provider/list.go b/cmd/provider/list.go index cdbaf79776..86453e8c18 100644 --- a/cmd/provider/list.go +++ b/cmd/provider/list.go @@ -111,11 +111,16 @@ func (cmd *ListCmd) renderInstalledPlain( ) error { tableEntries := [][]string{} for _, entry := range providers { + lifecycle := "" + if entry.Deprecated { + lifecycle = "deprecated" + } tableEntries = append(tableEntries, []string{ entry.Config.Name, entry.Config.Version, strconv.FormatBool(devsyConfig.Current().DefaultProvider == entry.Config.Name), string(entry.Status), + lifecycle, entry.Config.Description, }) } @@ -128,6 +133,7 @@ func (cmd *ListCmd) renderInstalledPlain( "Version", "Default", "Status", + "Lifecycle", "Description", }, tableEntries) diff --git a/cmd/provider/list_test.go b/cmd/provider/list_test.go new file mode 100644 index 0000000000..8f83fe380c --- /dev/null +++ b/cmd/provider/list_test.go @@ -0,0 +1,113 @@ +package provider + +import ( + "encoding/json" + "errors" + "io" + "os" + "testing" + + "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/provider" + "github.com/devsy-org/devsy/pkg/workspace" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type listDeprecationSuite struct { + suite.Suite +} + +const ( + testMicrosandboxName = provider.MicrosandboxDriver + testLegacyName = "legacy" +) + +func TestListDeprecationSuite(t *testing.T) { + suite.Run(t, new(listDeprecationSuite)) +} + +func (s *listDeprecationSuite) TestPlainListShowsLifecycleSeparately() { + cmd := &ListCmd{} + cfg := &config.Config{ + DefaultContext: config.DefaultContext, + Contexts: map[string]*config.ContextConfig{ + config.DefaultContext: {DefaultProvider: testMicrosandboxName}, + }, + } + entries := map[string]*workspace.ProviderWithOptions{ + testMicrosandboxName: { + Config: &provider.ProviderConfig{ + Name: testMicrosandboxName, + Version: "v1", + Description: testLegacyName, + }, + Deprecated: true, + }, + } + output := captureProviderListOutput(s.T(), func() error { + return cmd.renderInstalledPlain(cfg, entries) + }) + s.Require().NoError(output.err) + s.Require().Contains(output.text, "Lifecycle") + s.Require().Contains(output.text, "deprecated") + s.Require().Contains(output.text, "true") +} + +func (s *listDeprecationSuite) TestJSONListIncludesDeprecationMetadata() { + cmd := &ListCmd{} + cfg := &config.Config{ + DefaultContext: config.DefaultContext, + Contexts: map[string]*config.ContextConfig{ + config.DefaultContext: {DefaultProvider: testLegacyName}, + }, + } + entries := map[string]*workspace.ProviderWithOptions{ + testLegacyName: {Config: &provider.ProviderConfig{Name: testLegacyName}, Deprecated: true}, + "external": {Config: &provider.ProviderConfig{Name: testMicrosandboxName}}, + } + output := captureProviderListOutput(s.T(), func() error { + return cmd.renderInstalledJSON(cfg, map[string]*config.ProviderConfig{}, entries) + }) + s.Require().NoError(output.err) + var result map[string]map[string]json.RawMessage + s.Require().NoError(json.Unmarshal([]byte(output.text), &result)) + var legacyDeprecated, legacyDefault bool + s.Require().NoError(json.Unmarshal(result[testLegacyName]["deprecated"], &legacyDeprecated)) + s.Require().NoError(json.Unmarshal(result[testLegacyName]["default"], &legacyDefault)) + s.Require().True(legacyDeprecated) + s.Require().True(legacyDefault) + s.Require().NotContains(result["external"], "deprecated") + var externalName string + var externalConfig map[string]json.RawMessage + s.Require().NoError(json.Unmarshal(result["external"]["config"], &externalConfig)) + s.Require().NoError(json.Unmarshal(externalConfig["name"], &externalName)) + s.Require().Equal("microsandbox", externalName) +} + +type providerListOutput struct { + text string + err error +} + +func captureProviderListOutput(t *testing.T, render func() error) providerListOutput { + t.Helper() + read, write, err := os.Pipe() + require.NoError(t, err) + previous := os.Stdout + os.Stdout = write + defer func() { + os.Stdout = previous + _ = write.Close() + _ = read.Close() + }() + renderErr := render() + os.Stdout = previous + closeErr := write.Close() + data, readErr := io.ReadAll(read) + readCloseErr := read.Close() + return providerListOutput{ + text: string(data), + err: errors.Join(renderErr, closeErr, readErr, readCloseErr), + } +} diff --git a/desktop/src/main/__tests__/watcher.test.ts b/desktop/src/main/__tests__/watcher.test.ts index f10e703fe3..9c6dc381a1 100644 --- a/desktop/src/main/__tests__/watcher.test.ts +++ b/desktop/src/main/__tests__/watcher.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest" -import { Watcher } from "../watcher.js" +import { parseProviderEntries, Watcher } from "../watcher.js" import { WorkspaceJobs } from "../workspace-jobs.js" function makeWatcher( @@ -39,6 +39,32 @@ function makeWatcher( return { watcher, cli, state, workspaceJobs } } +describe("parseProviderEntries", () => { + it("maps the authoritative deprecation flag and defaults it for older CLI output", () => { + const [deprecated, legacy, external] = parseProviderEntries({ + deprecated: { + config: { name: "custom-builtin-name" }, + deprecated: true, + }, + legacy: { config: { name: "legacy-provider" } }, + external: { + config: { + name: "microsandbox", + source: { github: "example/provider" }, + }, + deprecated: false, + }, + }) + + expect(deprecated).toMatchObject({ + name: "custom-builtin-name", + deprecated: true, + }) + expect(legacy).toMatchObject({ name: "legacy-provider", deprecated: false }) + expect(external).toMatchObject({ name: "microsandbox", deprecated: false }) + }) +}) + describe("Watcher.refreshProviders", () => { it("does not run concurrently with another in-flight provider query", async () => { let inFlight = 0 diff --git a/desktop/src/main/watcher.ts b/desktop/src/main/watcher.ts index abb91927b4..b5f2f7af9a 100644 --- a/desktop/src/main/watcher.ts +++ b/desktop/src/main/watcher.ts @@ -38,6 +38,7 @@ export interface ProviderEntry { state?: { initialized?: boolean; singleMachine?: boolean } status?: string default?: boolean + deprecated?: boolean } export function parseProviderEntries(raw: Record) { @@ -50,6 +51,7 @@ export function parseProviderEntries(raw: Record) { options: entry.config.options ?? {}, optionGroups: entry.config.optionGroups ?? [], isDefault: entry.default ?? false, + deprecated: entry.deprecated ?? false, status: entry.status ?? "not_initialized", state: { initialized: entry.state?.initialized ?? false, diff --git a/desktop/src/renderer/src/lib/components/provider/ProviderCard.svelte b/desktop/src/renderer/src/lib/components/provider/ProviderCard.svelte index aee34a34d3..1960646d8c 100644 --- a/desktop/src/renderer/src/lib/components/provider/ProviderCard.svelte +++ b/desktop/src/renderer/src/lib/components/provider/ProviderCard.svelte @@ -65,6 +65,12 @@ function sourceDisplay(p: Provider): string { + {#if provider.deprecated} +
+ Deprecated built-in +
+ {/if} + {#if provider.description}

{provider.description}

{/if} diff --git a/desktop/src/renderer/src/lib/components/provider/ProviderCard.test.ts b/desktop/src/renderer/src/lib/components/provider/ProviderCard.test.ts index fccafe0156..077540e2e5 100644 --- a/desktop/src/renderer/src/lib/components/provider/ProviderCard.test.ts +++ b/desktop/src/renderer/src/lib/components/provider/ProviderCard.test.ts @@ -51,6 +51,27 @@ describe("ProviderCard", () => { unmount() }) + it("shows the deprecation badge only when the provider flag is set", () => { + const deprecated = render(ProviderCard, { + props: { + provider: makeProvider("custom-builtin-name", { deprecated: true }), + }, + }) + expect(deprecated.getByText("Deprecated built-in")).toBeDefined() + deprecated.unmount() + + const external = render(ProviderCard, { + props: { + provider: makeProvider("microsandbox", { + source: { github: "example/provider" }, + deprecated: false, + }), + }, + }) + expect(external.queryByText("Deprecated built-in")).toBeNull() + external.unmount() + }) + it("shows the initializing badge while an uninitialized provider is in flight", () => { providerJobs.set({ ssh: { activity: "initializing", phase: "running_init" }, diff --git a/desktop/src/renderer/src/lib/components/provider/ProviderSheet.svelte b/desktop/src/renderer/src/lib/components/provider/ProviderSheet.svelte index 64426982f3..11e1322203 100644 --- a/desktop/src/renderer/src/lib/components/provider/ProviderSheet.svelte +++ b/desktop/src/renderer/src/lib/components/provider/ProviderSheet.svelte @@ -529,6 +529,15 @@ async function handleSaveOptions() { {#if provider.description} {provider.description} {/if} + {#if provider.deprecated} + + Deprecated built-in provider + + This built-in provider is scheduled for removal. Existing provider records remain available + during the compatibility period. + + + {/if}
diff --git a/desktop/src/renderer/src/lib/components/provider/ProviderSheet.test.ts b/desktop/src/renderer/src/lib/components/provider/ProviderSheet.test.ts index 85294a4d60..e08f4c6bac 100644 --- a/desktop/src/renderer/src/lib/components/provider/ProviderSheet.test.ts +++ b/desktop/src/renderer/src/lib/components/provider/ProviderSheet.test.ts @@ -184,6 +184,32 @@ describe("ProviderSheet", () => { unmount() }) + it("shows deprecation guidance from the provider flag, not its name", async () => { + const deprecated = render(ProviderSheet, { + props: { + provider: makeProvider("custom-builtin-name", { deprecated: true }), + open: true, + }, + }) + await flushAsync() + expect(deprecated.getByText("Deprecated built-in provider")).toBeDefined() + expect(deprecated.getByText(/scheduled for removal/i)).toBeDefined() + deprecated.unmount() + + const external = render(ProviderSheet, { + props: { + provider: makeProvider("microsandbox", { + source: { github: "example/provider" }, + deprecated: false, + }), + open: true, + }, + }) + await flushAsync() + expect(external.queryByText("Deprecated built-in provider")).toBeNull() + external.unmount() + }) + it("does not refetch when provider prop ref changes but name stays the same", async () => { const { rerender, unmount } = render(ProviderSheet, { props: { provider: makeProvider("ssh"), open: true }, diff --git a/desktop/src/renderer/src/lib/components/provider/ProviderWizard.test.ts b/desktop/src/renderer/src/lib/components/provider/ProviderWizard.test.ts index c150a1f5d6..ddc3d91764 100644 --- a/desktop/src/renderer/src/lib/components/provider/ProviderWizard.test.ts +++ b/desktop/src/renderer/src/lib/components/provider/ProviderWizard.test.ts @@ -111,7 +111,7 @@ describe("ProviderWizard", () => { page.unmount() }) - it("keeps the literal Microsandbox source available for the embedded provider", async () => { + it("forwards the bare Microsandbox alias to the CLI", async () => { const page = renderWizard() await fireEvent.input( screen.getByPlaceholderText("github.com/org/provider"), diff --git a/desktop/src/renderer/src/lib/types/index.ts b/desktop/src/renderer/src/lib/types/index.ts index e34ba9da73..af2120ef13 100644 --- a/desktop/src/renderer/src/lib/types/index.ts +++ b/desktop/src/renderer/src/lib/types/index.ts @@ -81,6 +81,7 @@ export interface Provider { options?: Record optionGroups?: ProviderOptionGroup[] isDefault?: boolean + deprecated?: boolean status?: ProviderStatus state?: { initialized?: boolean diff --git a/e2e/tests/up/MICROSANDBOX.md b/e2e/tests/up/MICROSANDBOX.md index 4adea6c2d3..a15ad20059 100644 --- a/e2e/tests/up/MICROSANDBOX.md +++ b/e2e/tests/up/MICROSANDBOX.md @@ -11,6 +11,11 @@ configuration, the CLI framework, temporary workspaces and provider registration The shared provider selector supplies the pinned source, runtime identity and provider label while callers retain their descriptive provider names. +Built-in cases explicitly select `builtin:microsandbox`, which persists the +original internal source identity. The fresh-install CLI alias `microsandbox` +selects the pinned external provider; it must not silently replace the built-in +half of these parity or existing-workspace migration tests. + Workspace cleanup registration is explicit. Ginkgo cleanup runs in reverse order; register volume cleanup before workspace cleanup so the VM is torn down first. Specialized fixtures own their volumes, registries, endpoints, processes diff --git a/e2e/tests/up/microsandbox_session.go b/e2e/tests/up/microsandbox_session.go index 0bf10a17e3..9cf4f652ef 100644 --- a/e2e/tests/up/microsandbox_session.go +++ b/e2e/tests/up/microsandbox_session.go @@ -16,7 +16,7 @@ import ( ) const ( - microsandboxResourceBuiltin = "microsandbox" + microsandboxResourceBuiltin = provider.DeprecatedMicrosandboxSource osLinux = "linux" microsandboxExternalProvider = "github.com/devsy-org/devsy-provider-microsandbox@v0.1.8" microsandboxRootUser = "root" diff --git a/e2e/tests/up/provider_microsandbox.go b/e2e/tests/up/provider_microsandbox.go index bb691d26bc..8823963506 100644 --- a/e2e/tests/up/provider_microsandbox.go +++ b/e2e/tests/up/provider_microsandbox.go @@ -14,8 +14,10 @@ import ( "time" "github.com/devsy-org/devsy/e2e/framework" + "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/devcontainer" "github.com/devsy-org/devsy/pkg/docker" + "github.com/devsy-org/devsy/pkg/provider" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" ) @@ -34,7 +36,7 @@ var _ = ginkgo.Describe( }) ginkgo.DescribeTable("runs the shared lifecycle and ownership contract", - func(ctx context.Context, source, name string) { + func(ctx context.Context, source, name, expectedDriver string) { ginkgo.GinkgoT().Setenv("DEVSY_HOME", ginkgo.GinkgoT().TempDir()) ginkgo.GinkgoT().Setenv("DEVSY_CONFIG", "") f := framework.NewDefaultFramework(initialDir + "/bin") @@ -50,7 +52,18 @@ var _ = ginkgo.Describe( }) ginkgo.DeferCleanup(f.CleanupWorkspace, tempDir) - performance, err := newMicrosandboxPerformanceCapture(source, name, f) + installed, err := provider.LoadProviderConfig(config.DefaultContext, name) + framework.ExpectNoError(err) + gomega.Expect(installed.Agent.Driver).To(gomega.Equal(expectedDriver)) + isBuiltin := expectedDriver == provider.MicrosandboxDriver + gomega.Expect(installed.Source.Internal).To(gomega.Equal(isBuiltin)) + expectedSource := microsandboxExternalProvider + if isBuiltin { + expectedSource = provider.MicrosandboxDriver + } + gomega.Expect(installed.Source.Raw).To(gomega.Equal(expectedSource)) + + performance, err := newMicrosandboxPerformanceCapture(installed.Source.Raw, name, f) framework.ExpectNoError(err) // full up: boots the microVM, streams in the agent, opens the tunnel @@ -164,12 +177,14 @@ var _ = ginkgo.Describe( framework.ExpectNoError(err) gomega.Expect(string(output)).NotTo(gomega.ContainSubstring(sandbox)) }, - ginkgo.Entry("built-in", "microsandbox", "microsandbox-builtin-parity", + ginkgo.Entry("built-in", microsandboxResourceBuiltin, "microsandbox-builtin-parity", + provider.MicrosandboxDriver, ginkgo.Label("microsandbox-builtin"), ginkgo.SpecTimeout(framework.TimeoutLong())), ginkgo.Entry( "external v0.1.8", - microsandboxExternalProvider, + provider.MicrosandboxDriver, "microsandbox-external-parity", + provider.ExternalDriver, ginkgo.Label("microsandbox-external"), ginkgo.SpecTimeout(framework.TimeoutLong()), ), ) diff --git a/pkg/provider/microsandbox_source_test.go b/pkg/provider/microsandbox_source_test.go new file mode 100644 index 0000000000..1cd1f99ca5 --- /dev/null +++ b/pkg/provider/microsandbox_source_test.go @@ -0,0 +1,73 @@ +package provider + +import ( + "bytes" + "context" + "os" + "path/filepath" + "testing" + + "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/types" + "github.com/stretchr/testify/suite" +) + +type microsandboxSourceSuite struct { + suite.Suite +} + +func TestMicrosandboxSource(t *testing.T) { + suite.Run(t, new(microsandboxSourceSuite)) +} + +func (s *microsandboxSourceSuite) TestLegacySelectorsKeepCanonicalIdentity() { + for _, input := range []string{MicrosandboxDriver, DeprecatedMicrosandboxSource} { + s.Run(input, func() { + raw, source, err := ResolveProvider(context.Background(), input) + s.Require().NoError(err) + s.Equal(ProviderSource{Internal: true, Raw: MicrosandboxDriver}, *source) + parsed, err := ParseProvider(bytes.NewReader(raw)) + s.Require().NoError(err) + s.Equal(MicrosandboxDriver, parsed.Agent.Driver) + s.Equal(MicrosandboxDriver, GetProviderSource(*source, "custom-name")) + }) + } +} + +func (s *microsandboxSourceSuite) TestLoadingLegacyRecordDoesNotMigrateOrRewriteIt() { + setupTestHome(s.T()) + stored := &ProviderConfig{ + Name: "my-sandbox", Version: staleVersion, + Source: ProviderSource{Internal: true, Raw: MicrosandboxDriver}, + Agent: ProviderAgentConfig{ + Driver: MicrosandboxDriver, + Microsandbox: ProviderMicrosandboxDriverConfig{ + Memory: "1024", WorkspaceHostPermissions: "private", + }, + }, + Options: map[string]*types.Option{ + "MICROSANDBOX_MEMORY": {Default: "1024"}, + }, + Exec: ProviderCommands{Command: []string{staleHelperShCommand}}, + } + s.Require().NoError(SaveProviderConfig(config.DefaultContext, stored)) + dir, err := GetProviderDir(config.DefaultContext, stored.Name) + s.Require().NoError(err) + path := filepath.Join(dir, ProviderConfigFile) + before, err := os.ReadFile(filepath.Clean(path)) + s.Require().NoError(err) + + loaded, err := LoadProviderConfig(config.DefaultContext, stored.Name) + s.Require().NoError(err) + s.Equal(stored.Name, loaded.Name) + s.Equal(stored.Version, loaded.Version) + s.Equal(stored.Source, loaded.Source) + s.Equal(stored.Agent.Driver, loaded.Agent.Driver) + s.Equal(stored.Agent.Microsandbox, loaded.Agent.Microsandbox) + s.Equal(stored.Options, loaded.Options) + s.Require().Len(loaded.Exec.Command, 1) + s.NotContains(loaded.Exec.Command[0], "helper sh") + after, err := os.ReadFile(filepath.Clean(path)) + s.Require().NoError(err) + s.Equal(before, after) +} diff --git a/pkg/provider/resolve.go b/pkg/provider/resolve.go index 132361a00c..3dc33fca03 100644 --- a/pkg/provider/resolve.go +++ b/pkg/provider/resolve.go @@ -17,6 +17,9 @@ import ( "github.com/devsy-org/devsy/providers" ) +// DeprecatedMicrosandboxSource explicitly selects the legacy built-in provider. +const DeprecatedMicrosandboxSource = "builtin:microsandbox" + // ResolveProvider resolves a provider source string into raw provider YAML // bytes and a ProviderSource describing the origin (internal, URL, file, or // GitHub release). @@ -24,6 +27,10 @@ func ResolveProvider( ctx context.Context, providerSource string, ) ([]byte, *ProviderSource, error) { + if providerSource == DeprecatedMicrosandboxSource { + // Keep stored legacy records compatible with embedded exec refresh and reload. + providerSource = MicrosandboxDriver + } retSource := &ProviderSource{Raw: strings.TrimSpace(providerSource)} if out, ok := resolveInternalProvider(providerSource, retSource); ok { diff --git a/pkg/workspace/provider.go b/pkg/workspace/provider.go index 2d6bc3e253..5dba89d3aa 100644 --- a/pkg/workspace/provider.go +++ b/pkg/workspace/provider.go @@ -19,9 +19,10 @@ import ( var ErrNoWorkspaceFound = errors.New("no workspace found") type ProviderWithOptions struct { - Config *provider.ProviderConfig `json:"config,omitempty"` - State *config.ProviderConfig `json:"state,omitempty"` - Status provider.InitState `json:"status,omitempty"` + Config *provider.ProviderConfig `json:"config,omitempty"` + State *config.ProviderConfig `json:"state,omitempty"` + Status provider.InitState `json:"status,omitempty"` + Deprecated bool `json:"deprecated,omitempty"` } type ProviderParams struct { @@ -66,6 +67,8 @@ func LoadAllProviders( } for name, entry := range retProviders { + entry.Deprecated = entry.Config != nil && + entry.Config.Agent.Driver == provider.MicrosandboxDriver status, err := provider.ResolveInitState(devsyConfig.DefaultContext, name, entry.State) if err != nil { log.Warnf("error resolving init state for provider %s: %v", name, err) diff --git a/pkg/workspace/provider_deprecation_test.go b/pkg/workspace/provider_deprecation_test.go new file mode 100644 index 0000000000..9f33ada086 --- /dev/null +++ b/pkg/workspace/provider_deprecation_test.go @@ -0,0 +1,105 @@ +package workspace + +import ( + "encoding/json" + "testing" + + "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/provider" + "github.com/stretchr/testify/suite" +) + +type providerDeprecationSuite struct { + suite.Suite +} + +const ( + testAgentPath = config.BinaryName + testFileSource = "/tmp/custom-microsandbox.yaml" +) + +func TestProviderDeprecationSuite(t *testing.T) { + suite.Run(t, new(providerDeprecationSuite)) +} + +func (s *providerDeprecationSuite) SetupTest() { + setupTestPathManager(s.T()) +} + +func (s *providerDeprecationSuite) TestLoadAllProvidersMarksDeprecatedByDriver() { + require := s.Require() + const legacyName = "legacy-runtime" + const externalName = "microsandbox" + const customFileName = "custom-file-runtime" + devsyConfig := &config.Config{ + DefaultContext: testDefaultContext, + Contexts: map[string]*config.ContextConfig{ + testDefaultContext: { + Providers: map[string]*config.ProviderConfig{ + legacyName: {}, + externalName: {}, + customFileName: {}, + }, + }, + }, + } + require.NoError(config.SaveConfig(devsyConfig)) + for name, source := range map[string]provider.ProviderSource{ + legacyName: {Internal: true, Raw: provider.MicrosandboxDriver}, + customFileName: {File: testFileSource, Raw: testFileSource}, + } { + s.saveProviderConfig(&provider.ProviderConfig{ + Name: name, Version: "v1.0.0", + Agent: provider.ProviderAgentConfig{ + Path: testAgentPath, Driver: provider.MicrosandboxDriver, + }, + Exec: provider.ProviderCommands{Command: []string{config.BinaryName}}, + Source: source, + }) + } + s.saveProviderConfig(&provider.ProviderConfig{ + Name: externalName, + Version: "v0.1.8", + Agent: provider.ProviderAgentConfig{ + Path: testAgentPath, + Driver: provider.ExternalDriver, + External: provider.ProviderExternalDriverConfig{Binary: "RUNTIME"}, + Binaries: map[string][]*provider.ProviderBinary{ + "RUNTIME": {{ + OS: "linux", Arch: "amd64", Path: "https://example.invalid/runtime", + Checksum: "0000000000000000000000000000000000000000000000000000000000000000", + }}, + }, + }, + Exec: provider.ProviderCommands{Command: []string{config.BinaryName}}, + Source: provider.ProviderSource{ + Github: "devsy-org/devsy-provider-microsandbox", + Raw: "devsy-provider-microsandbox@v0.1.8", + }, + }) + + providers, err := LoadAllProviders(devsyConfig) + require.NoError(err) + s.assertDeprecationMetadata(providers[legacyName], true) + s.assertDeprecationMetadata(providers[customFileName], true) + s.assertDeprecationMetadata(providers[externalName], false) +} + +func (s *providerDeprecationSuite) saveProviderConfig(providerConfig *provider.ProviderConfig) { + s.Require().NoError(provider.SaveProviderConfig(testDefaultContext, providerConfig)) +} + +func (s *providerDeprecationSuite) assertDeprecationMetadata( + entry *ProviderWithOptions, + deprecated bool, +) { + s.Require().NotNil(entry) + s.Equal(deprecated, entry.Deprecated) + data, err := json.Marshal(entry) + s.Require().NoError(err) + if deprecated { + s.Contains(string(data), `"deprecated":true`) + } else { + s.NotContains(string(data), `"deprecated"`) + } +} diff --git a/sites/docs-devsy-sh/content/docs/developing-providers/external-runtime-rollout.mdx b/sites/docs-devsy-sh/content/docs/developing-providers/external-runtime-rollout.mdx index 3da6891cec..c9d09c8265 100644 --- a/sites/docs-devsy-sh/content/docs/developing-providers/external-runtime-rollout.mdx +++ b/sites/docs-devsy-sh/content/docs/developing-providers/external-runtime-rollout.mdx @@ -4,7 +4,7 @@ sidebar_label: Runtime Rollout Decision description: Fresh Desktop provider selection, existing-provider migration, and external MicroSandbox runtime evidence. --- -**Status: fresh Desktop preset selects the published provider.** The Microsandbox preset installs the pinned external source for a new provider while retaining the name `microsandbox`. Existing internal providers continue to use the embedded MicroSandbox driver until an explicit source migration. The runtime evidence remains limited to the covered Linux/KVM scenarios. +**Status: fresh Desktop and CLI installs select the published provider; the built-in driver is deprecated.** The Microsandbox preset and `devsy provider add microsandbox` install the pinned external source for a new provider while retaining the name `microsandbox`. Existing internal providers continue to use the embedded MicroSandbox driver until an explicit source migration. The runtime evidence remains limited to the covered Linux/KVM scenarios. ## Current host and runtime contract @@ -24,9 +24,16 @@ The separate Windows provider-update job exercises Go archive and provider-updat ## Fresh Desktop installation -The Desktop Microsandbox preset installs the pinned source [`github.com/devsy-org/devsy-provider-microsandbox@v0.1.8`](https://github.com/devsy-org/devsy-provider-microsandbox/releases/tag/v0.1.8), while retaining the provider name `microsandbox`. Reloading this provider without a replacement source retains the pinned tag. For an already pinned GitHub provider, a Desktop update to a displayed version explicitly selects that version; refreshing without a known target retains the configured pin. The existing Desktop setup path uses `provider add ... --use=false`; this preserves its current default-provider behavior. The explicit embedded source alias `microsandbox` remains available, as does the embedded manifest for parity tests and compatibility with existing internal provider records. +The Desktop Microsandbox preset and fresh CLI alias install the pinned source [`github.com/devsy-org/devsy-provider-microsandbox@v0.1.8`](https://github.com/devsy-org/devsy-provider-microsandbox/releases/tag/v0.1.8), while retaining the provider name `microsandbox`. Reloading this provider without a replacement source retains the pinned tag. For an already pinned GitHub provider, a Desktop update to a displayed version explicitly selects that version; refreshing without a known target retains the configured pin. The existing Desktop setup path uses `provider add ... --use=false`; this preserves its current default-provider behavior. CLI `--name` and `--use` retain their normal behavior. The explicit compatibility source `builtin:microsandbox` remains available, as does the embedded manifest for parity tests and compatibility with existing internal provider records. -This choice applies only to a new provider. The wizard refuses an existing provider name before installation; it does not replace or migrate that record. Other presets and custom names retain their behavior, and entering `microsandbox` as a custom source explicitly chooses the embedded provider. Existing records change source only through the supported `provider set-source` command. Loading an internal provider refreshes its embedded Exec blocks, not its stored driver or binaries. If initialization fails after a source change is committed, the new source remains selected and the provider records `InitError`; the command does not promise rollback. The running and stopped migration cases provide evidence for the tested successful transition, not for rollback after arbitrary failures. +This choice applies only to a new provider. The wizard refuses an existing provider name before installation; it does not replace or migrate that record. Other presets and custom names retain their behavior. Entering `builtin:microsandbox` as a custom source deliberately chooses the deprecated built-in provider. Source-omitted reloads of stored internal records retain their legacy meaning; the fresh-install alias does not reinterpret those records. Migrating a stored internal provider to the external runtime requires passing the full external source explicitly to `provider set-source`. Loading an internal provider refreshes its embedded Exec blocks, not its stored driver or binaries. If initialization fails after a source change is committed, the new source remains selected and the provider records `InitError`; the command does not promise rollback. The running and stopped migration cases provide evidence for the tested successful transition, not for rollback after arbitrary failures. + +Deprecation is reported separately from initialization and default-provider state. +It follows the selected built-in driver, including custom-named installations and +custom provider manifests using that driver. An external provider named +`microsandbox` is not deprecated. No compatibility flag, automatic migration or +runtime removal is introduced in this deprecation stage; those require their own +announced transition and validation. ## Evidence and compatibility limits @@ -34,4 +41,4 @@ The performance artifact from run 38628263920 records one Linux/amd64 hosted run Other provider-supported host profiles remain untested here. Upstream requires Apple Silicon on macOS, KVM on Linux, and WHP on Windows; this campaign does not claim runtime parity on those other hosts. The registry tests do not cover every proxy, Docker daemon/context/socket, credential-store, or temp-directory configuration. Neither the built-in driver nor the external provider guarantees removal of every effect after a failed or interrupted create; use a fresh `Find` and explicit cleanup where recovery is needed. The exact native VM artifacts surviving a particular interruption remain unproven. -The source-migration review and merge gates are complete. The fresh Desktop selection preserves these platform, performance, environment, and recovery limits. The embedded source alias, manifest, built-in implementation and explicit built-in parity fixture remain available for existing records and deliberate embedded installs; this change removes none of them. +The source-migration review and merge gates are complete. Fresh installation and deprecation preserve these platform, performance, environment, and recovery limits. The explicit compatibility selector, embedded manifest, built-in implementation and built-in parity fixture remain available for existing records and deliberate compatibility installs. diff --git a/sites/docs-devsy-sh/content/docs/managing-providers/manage-providers.mdx b/sites/docs-devsy-sh/content/docs/managing-providers/manage-providers.mdx index ae8ff7a51e..130d332ea5 100644 --- a/sites/docs-devsy-sh/content/docs/managing-providers/manage-providers.mdx +++ b/sites/docs-devsy-sh/content/docs/managing-providers/manage-providers.mdx @@ -9,7 +9,7 @@ Providers you can add: - [Podman (podman)](https://github.com/devsy-org/devsy/tree/main/providers/podman) - [Kubernetes (kubernetes)](https://github.com/devsy-org/devsy/tree/main/providers/kubernetes) - [Apple (apple)](https://github.com/devsy-org/devsy/tree/main/providers/apple) -- [Microsandbox (microsandbox)](https://github.com/devsy-org/devsy/tree/main/providers/microsandbox) +- [MicroSandbox (microsandbox)](https://github.com/devsy-org/devsy-provider-microsandbox) - [Lima (lima)](https://github.com/devsy-org/devsy-provider-lima) - [OrbStack (orbstack)](https://github.com/devsy-org/devsy-provider-orbstack) - [SSH (ssh)](https://github.com/devsy-org/devsy-provider-ssh) @@ -43,6 +43,28 @@ Click a provider in the list to edit its settings, switch version, update, re-in devsy provider add docker ``` +`devsy provider add microsandbox` installs the pinned external MicroSandbox +provider from `github.com/devsy-org/devsy-provider-microsandbox@v0.1.8`. The Desktop +MicroSandbox preset selects the same published source. + +Existing built-in MicroSandbox installations are listed as **Deprecated built-in** +in Desktop and marked deprecated by `devsy provider list`. Their names, defaults, +options and existing workspaces are unchanged. The label also applies to custom +provider definitions that select the built-in MicroSandbox driver; it does not +apply to an external provider merely named `microsandbox`. + +For deliberate compatibility use, `builtin:microsandbox` explicitly selects the +deprecated implementation: + +```sh +devsy provider add builtin:microsandbox --name legacy-microsandbox --use=false +``` + +Adding the external provider does not migrate an installed provider with the same +name. Existing records retain their source until an explicit `provider set-source` +operation. See the [runtime rollout decision](/docs/developing-providers/external-runtime-rollout) +for the tested migration behavior and its compatibility limits. + Use `--name` to add the same provider more than once with different options: ```sh From ef8253fac81cf14ce24a1f67163259cf822bb0c7 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Thu, 15 Oct 2026 12:41:30 -0600 Subject: [PATCH 2/2] fix(provider): resolve templated drivers for deprecation --- pkg/workspace/provider.go | 8 +- pkg/workspace/provider_deprecation_test.go | 109 ++++++++++++++++++ .../external-runtime-rollout.mdx | 3 +- 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/pkg/workspace/provider.go b/pkg/workspace/provider.go index 5dba89d3aa..ae75092990 100644 --- a/pkg/workspace/provider.go +++ b/pkg/workspace/provider.go @@ -11,6 +11,7 @@ import ( client2 "github.com/devsy-org/devsy/pkg/client" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/log" + "github.com/devsy-org/devsy/pkg/options/resolver" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/types" @@ -67,8 +68,11 @@ func LoadAllProviders( } for name, entry := range retProviders { - entry.Deprecated = entry.Config != nil && - entry.Config.Agent.Driver == provider.MicrosandboxDriver + if entry.Config != nil { + options := provider.ToOptions(nil, nil, devsyConfig.ProviderOptions(entry.Config.Name)) + driver := resolver.ResolveDefaultValue(entry.Config.Agent.Driver, options) + entry.Deprecated = driver == provider.MicrosandboxDriver + } status, err := provider.ResolveInitState(devsyConfig.DefaultContext, name, entry.State) if err != nil { log.Warnf("error resolving init state for provider %s: %v", name, err) diff --git a/pkg/workspace/provider_deprecation_test.go b/pkg/workspace/provider_deprecation_test.go index 9f33ada086..021146092f 100644 --- a/pkg/workspace/provider_deprecation_test.go +++ b/pkg/workspace/provider_deprecation_test.go @@ -2,10 +2,14 @@ package workspace import ( "encoding/json" + "os" + "path/filepath" "testing" + "al.essio.dev/pkg/shellescape" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/provider" + "github.com/devsy-org/devsy/pkg/types" "github.com/stretchr/testify/suite" ) @@ -13,6 +17,14 @@ type providerDeprecationSuite struct { suite.Suite } +type templatedDriverCase struct { + name string + savedDriver string + hasSavedValue bool + defaultDriver string + deprecated bool +} + const ( testAgentPath = config.BinaryName testFileSource = "/tmp/custom-microsandbox.yaml" @@ -85,6 +97,103 @@ func (s *providerDeprecationSuite) TestLoadAllProvidersMarksDeprecatedByDriver() s.assertDeprecationMetadata(providers[externalName], false) } +func (s *providerDeprecationSuite) TestTemplatedDriverUsesSavedOptionWithoutMutationOrCommand() { + require := s.Require() + commandMarker := filepath.Join(s.T().TempDir(), "option-command-ran") + testCases := []templatedDriverCase{ + { + name: "templated-microsandbox", savedDriver: provider.MicrosandboxDriver, + hasSavedValue: true, defaultDriver: provider.DockerDriver, deprecated: true, + }, + { + name: "templated-docker", savedDriver: provider.DockerDriver, + hasSavedValue: true, defaultDriver: provider.MicrosandboxDriver, + }, + {name: "templated-unresolved"}, + } + devsyConfig := s.saveTemplatedDriverFixtures(testCases, commandMarker) + originalState, err := json.Marshal(devsyConfig.Current().Providers) + require.NoError(err) + + providers, err := LoadAllProviders(devsyConfig) + require.NoError(err) + s.assertTemplatedDriverResults(providers, testCases) + require.Equal("templated-microsandbox", devsyConfig.Current().DefaultProvider) + afterState, err := json.Marshal(devsyConfig.Current().Providers) + require.NoError(err) + require.JSONEq(string(originalState), string(afterState)) + _, err = os.Stat(commandMarker) + require.ErrorIs(err, os.ErrNotExist) +} + +func (s *providerDeprecationSuite) saveTemplatedDriverFixtures( + testCases []templatedDriverCase, + commandMarker string, +) *config.Config { + configuredProviders := make(map[string]*config.ProviderConfig, len(testCases)) + for _, tc := range testCases { + state := &config.ProviderConfig{Initialized: true, InitAttempted: true} + if tc.hasSavedValue { + state.Options = map[string]config.OptionValue{ + "DRIVER": {Value: tc.savedDriver, UserProvided: true}, + } + } + configuredProviders[tc.name] = state + } + devsyConfig := &config.Config{ + DefaultContext: testDefaultContext, + Contexts: map[string]*config.ContextConfig{ + testDefaultContext: { + DefaultProvider: "templated-microsandbox", + Providers: configuredProviders, + }, + }, + } + s.Require().NoError(config.SaveConfig(devsyConfig)) + for _, tc := range testCases { + definitions := map[string]*types.Option{ + "UNUSED": { + Command: "echo executed > " + shellescape.Quote(filepath.ToSlash(commandMarker)), + }, + } + if tc.defaultDriver != "" { + definitions["DRIVER"] = &types.Option{Default: tc.defaultDriver} + } + s.saveProviderConfig(&provider.ProviderConfig{ + Name: tc.name, + Version: "v1.0.0", + Options: definitions, + Agent: provider.ProviderAgentConfig{Path: testAgentPath, Driver: "${DRIVER}"}, + Exec: provider.ProviderCommands{Command: []string{config.BinaryName}}, + Source: provider.ProviderSource{ + File: testFileSource, + Raw: testFileSource, + }, + }) + } + return devsyConfig +} + +func (s *providerDeprecationSuite) assertTemplatedDriverResults( + providers map[string]*ProviderWithOptions, + testCases []templatedDriverCase, +) { + require := s.Require() + for _, tc := range testCases { + entry := providers[tc.name] + require.NotNil(entry) + require.Equal(tc.deprecated, entry.Deprecated, tc.name) + require.Equal("${DRIVER}", entry.Config.Agent.Driver) + if tc.hasSavedValue { + require.Equal(tc.savedDriver, entry.State.Options["DRIVER"].Value) + require.True(entry.State.Options["DRIVER"].UserProvided) + require.Nil(entry.State.Options["DRIVER"].Filled) + } else { + require.Empty(entry.State.Options) + } + } +} + func (s *providerDeprecationSuite) saveProviderConfig(providerConfig *provider.ProviderConfig) { s.Require().NoError(provider.SaveProviderConfig(testDefaultContext, providerConfig)) } diff --git a/sites/docs-devsy-sh/content/docs/developing-providers/external-runtime-rollout.mdx b/sites/docs-devsy-sh/content/docs/developing-providers/external-runtime-rollout.mdx index c9d09c8265..777ac9d063 100644 --- a/sites/docs-devsy-sh/content/docs/developing-providers/external-runtime-rollout.mdx +++ b/sites/docs-devsy-sh/content/docs/developing-providers/external-runtime-rollout.mdx @@ -30,7 +30,8 @@ This choice applies only to a new provider. The wizard refuses an existing provi Deprecation is reported separately from initialization and default-provider state. It follows the selected built-in driver, including custom-named installations and -custom provider manifests using that driver. An external provider named +custom provider manifests. Driver templates use saved provider options; listing +does not run option commands to determine the value. An external provider named `microsandbox` is not deprecated. No compatibility flag, automatic migration or runtime removal is introduced in this deprecation stage; those require their own announced transition and validation.