diff --git a/CHANGELOG.md b/CHANGELOG.md index 184a735c2..b6f7c405c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,6 @@ ### Changed - Host status, import and export, and credential tools now start on for new installs -- Service Manager is now in the standard image and starts on for new installs - Sync now starts on in a new desktop app, so it can link to a server - The first run plugin picker leaves plugins you did not choose off instead of removing them diff --git a/docker/bundled-plugins.json b/docker/bundled-plugins.json index e966cd5a1..007f1ea5c 100644 --- a/docker/bundled-plugins.json +++ b/docker/bundled-plugins.json @@ -67,7 +67,11 @@ "source": "tmxplug", "url": "https://github.com/Termix-SSH/Plugin-Credential-Tools/releases/download/v1.0.0/credential-tools-1.0.0.tmxplug", "sha256": "413eff710b5c832a2575a890bbab1c90d28ea55f10cd7a7b94b851d51dd77dce", - "tier": "standard" + "tier": "standard", + "onboarding": { + "recommended": true, + "consent": false + } }, { "id": "databases", @@ -128,7 +132,11 @@ "source": "tmxplug", "url": "https://github.com/Termix-SSH/Plugin-Host-Status/releases/download/v1.0.0/host-status-1.0.0.tmxplug", "sha256": "1ba62577dc7373a2a2787bab7efdb89b544a30a8003b1de91ce51f372fc4f170", - "tier": "standard" + "tier": "standard", + "onboarding": { + "recommended": true, + "consent": false + } }, { "id": "hosts-config", @@ -142,7 +150,11 @@ "source": "tmxplug", "url": "https://github.com/Termix-SSH/Plugin-Import-Export/releases/download/v1.0.1/import-export-1.0.1.tmxplug", "sha256": "d8f2f865e1e6b226f4fe38501bf69b752f47a643bed1d9d127181083e27da2f0", - "tier": "standard" + "tier": "standard", + "onboarding": { + "recommended": true, + "consent": false + } }, { "id": "kubernetes", @@ -288,9 +300,7 @@ "onboarding": { "recommended": false, "consent": false, - "enabledByEnv": [ - "OIDC_CLIENT_ID" - ] + "enabledByEnv": ["OIDC_CLIENT_ID"] } }, { @@ -305,7 +315,11 @@ "source": "tmxplug", "url": "https://github.com/Termix-SSH/Plugin-Sync/releases/download/v1.1.0/sync-1.1.0.tmxplug", "sha256": "9023d547f0e0bcef582c6ba7b7a8ab2ef68d8d0b973dde17d9e8c84d92b6429e", - "tier": "standard" + "tier": "standard", + "onboarding": { + "recommended": "desktop", + "consent": false + } }, { "id": "tailscale", diff --git a/scripts/bundled-plugins.test.ts b/scripts/bundled-plugins.test.ts index e84c12982..541d95b5e 100644 --- a/scripts/bundled-plugins.test.ts +++ b/scripts/bundled-plugins.test.ts @@ -537,6 +537,38 @@ describe("default look", () => { ).toMatchObject({ id: "look", defaultLook: true }); }); + it("leaves out new plugins with no usable release when pinning all", () => { + const stable = { + version: "1.0.0", + url: "https://x/a.tmxplug", + sha256: SHA, + }; + const index = { + plugins: [ + { id: "ready", versions: [stable] }, + { + id: "beta-only", + versions: [], + prereleases: [{ ...stable, version: "1.0.0-beta.1" }], + }, + { + id: "too-new", + versions: [], + gated: [{ ...stable, engine: { termix: ">=99.0.0" } }], + }, + ], + }; + expect( + pinsFromIndex(index, null, [], "26.11.1").map( + (pin: { id: string }) => pin.id, + ), + ).toEqual(["ready"]); + expect(() => + pinsFromIndex(index, null, [{ id: "beta-only" }], "26.11.1"), + ).toThrow(/beta-only/); + expect(() => pinsFromIndex(index, ["beta-only"])).toThrow(/beta-only/); + }); + it("copies a local build's theme files", () => { const repo = tempDir(); fs.mkdirSync(path.join(repo, "dist"), { recursive: true }); diff --git a/scripts/lib/bundled-plugins.cjs b/scripts/lib/bundled-plugins.cjs index deeb173ce..00262c736 100644 --- a/scripts/lib/bundled-plugins.cjs +++ b/scripts/lib/bundled-plugins.cjs @@ -278,14 +278,15 @@ function pluginsForProfile(plugins, profile) { * pin list, so fields the registry does not know (onboarding, tier) carry over; * a plugin new to the list starts in the full tier. coreVersion, when given, * skips releases whose engine.termix range does not include it, so the image - * never ships a plugin its own core cannot run. + * never ships a plugin its own core cannot run. With ids null, a plugin new to + * the list that has no usable release (only betas, say) is left out. */ function pinsFromIndex(index, ids, current = [], coreVersion = null) { const plugins = Array.isArray(index?.plugins) ? index.plugins : []; const byId = new Map(plugins.map((plugin) => [plugin.id, plugin])); const currentById = new Map(current.map((entry) => [entry.id, entry])); const wanted = ids ?? plugins.map((plugin) => plugin.id); - return [...wanted].sort().map((id) => { + const pins = [...wanted].sort().map((id) => { const listed = byId.get(id); // gated holds releases that need a newer Termix than old builds check for. const latest = [...(listed?.versions ?? []), ...(listed?.gated ?? [])] @@ -300,7 +301,10 @@ function pinsFromIndex(index, ids, current = [], coreVersion = null) { }), ) .sort((a, b) => semver.rcompare(a.version, b.version))[0]; - if (!latest) throw new Error(`${id} is not in the registry index`); + if (!latest) { + if (ids === null && !currentById.has(id)) return null; + throw new Error(`${id} is not in the registry index`); + } const onboarding = currentById.get(id)?.onboarding; const defaultLook = currentById.get(id)?.defaultLook === true; const tier = currentById.get(id)?.tier ?? "full"; @@ -314,6 +318,7 @@ function pinsFromIndex(index, ids, current = [], coreVersion = null) { ...(defaultLook ? { defaultLook } : {}), }; }); + return pins.filter(Boolean); } const LOCAL_FILES = [ diff --git a/scripts/pin-bundled-plugins.cjs b/scripts/pin-bundled-plugins.cjs index 5db6dfdb9..e01dd8507 100644 --- a/scripts/pin-bundled-plugins.cjs +++ b/scripts/pin-bundled-plugins.cjs @@ -4,7 +4,7 @@ * re-released. * * node scripts/pin-bundled-plugins.cjs refresh the plugins already listed - * node scripts/pin-bundled-plugins.cjs --all also add plugins new to the index (tier full) + * node scripts/pin-bundled-plugins.cjs --all also add plugins new to the index that have a stable release (tier full) * --core-version X skip releases whose engine.termix excludes X * * The release workflow runs it with --all and the version being released. diff --git a/src/ui/onboarding/plugin-choices.ts b/src/ui/onboarding/plugin-choices.ts index 8d576e44c..844d16973 100644 --- a/src/ui/onboarding/plugin-choices.ts +++ b/src/ui/onboarding/plugin-choices.ts @@ -10,8 +10,9 @@ export function currentChoice(plugin: OnboardingPluginInfo): PluginChoice { /** * What the picker starts on. On a fresh install recommended plugins are on - * and the rest are set to be removed; consent ones start on. Later runs - * start from how things are now. + * and the rest stay installed but off, so a full image keeps every plugin + * unless the admin picks Remove; consent ones start on. Later runs start from + * how things are now. */ export function defaultChoices( list: OnboardingPluginList, @@ -26,7 +27,7 @@ export function defaultChoices( return [plugin.id, currentChoice(plugin)]; } if (plugin.consent) return [plugin.id, "enabled"]; - return [plugin.id, plugin.recommended ? "enabled" : "remove"]; + return [plugin.id, plugin.recommended ? "enabled" : "disabled"]; }), ); } @@ -37,7 +38,7 @@ export function recommendedChoices( return Object.fromEntries( list.plugins.map((plugin) => [ plugin.id, - plugin.recommended || plugin.consent ? "enabled" : "remove", + plugin.recommended || plugin.consent ? "enabled" : "disabled", ]), ); } diff --git a/src/ui/tests/onboarding/steps/PluginsStep.test.tsx b/src/ui/tests/onboarding/steps/PluginsStep.test.tsx index 18a782e33..335a2a85e 100644 --- a/src/ui/tests/onboarding/steps/PluginsStep.test.tsx +++ b/src/ui/tests/onboarding/steps/PluginsStep.test.tsx @@ -49,11 +49,11 @@ const LIST: OnboardingPluginList = { }; describe("defaultChoices", () => { - it("keeps recommended plugins and removes the rest on a fresh install", () => { + it("keeps recommended plugins on and the rest off on a fresh install", () => { expect(defaultChoices(LIST)).toEqual({ term: "enabled", - base: "remove", - addon: "remove", + base: "disabled", + addon: "disabled", stats: "enabled", }); }); @@ -139,8 +139,8 @@ describe("PluginsStep", () => { resolved: {}, adjustments: [], enabled: [], - disabled: [], - removed: ["base", "addon"], + disabled: ["base", "addon"], + removed: [], failed: [], }); api.getOnboardingPlugins.mockResolvedValueOnce(LIST).mockResolvedValue({ @@ -155,8 +155,8 @@ describe("PluginsStep", () => { expect(ok).toBe(true); expect(api.applyOnboardingPlugins).toHaveBeenCalledWith({ term: "enabled", - base: "remove", - addon: "remove", + base: "disabled", + addon: "disabled", stats: "enabled", }); expect(loader.syncPlugins).toHaveBeenCalled();