fix(update): isolate plugin sync failures

Disable and skip plugins that fail package-update plugin sync so broken plugin packages do not fail an otherwise successful OpenClaw update.
This commit is contained in:
Vincent Koc
2026-05-04 14:06:44 -07:00
committed by GitHub
parent fdaa5a0c3d
commit 7c0f5463a5
5 changed files with 212 additions and 74 deletions

View File

@@ -1032,6 +1032,61 @@ describe("updateNpmInstalledPlugins", () => {
]);
});
it("disables enabled tracked plugin update failures when requested", async () => {
const warn = vi.fn();
installPluginFromNpmSpecMock.mockResolvedValue({
ok: false,
error: "registry timeout",
});
const config = {
plugins: {
entries: {
demo: {
enabled: true,
config: { preserved: true },
},
},
installs: {
demo: {
source: "npm" as const,
spec: "@acme/demo",
installPath: "/tmp/demo",
},
},
},
} satisfies OpenClawConfig;
const result = await updateNpmInstalledPlugins({
config,
skipDisabledPlugins: true,
disableOnFailure: true,
logger: { warn },
});
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
expect.objectContaining({
spec: "@acme/demo",
expectedPluginId: "demo",
}),
);
const message =
'Disabled "demo" after plugin update failure; OpenClaw will continue without it. Failed to update demo: registry timeout';
expect(warn).toHaveBeenCalledWith(message);
expect(result.changed).toBe(true);
expect(result.config.plugins?.entries?.demo).toEqual({
enabled: false,
config: { preserved: true },
});
expect(result.config.plugins?.installs?.demo).toEqual(config.plugins.installs.demo);
expect(result.outcomes).toEqual([
{
pluginId: "demo",
status: "skipped",
message,
},
]);
});
it("aborts exact pinned npm plugin updates on integrity drift by default", async () => {
const warn = vi.fn();
installPluginFromNpmSpecMock.mockImplementation(

View File

@@ -747,12 +747,30 @@ function createPluginUpdateIntegrityDriftHandler(params: {
};
}
function disablePluginConfigEntry(config: OpenClawConfig, pluginId: string): OpenClawConfig {
const existingEntry = config.plugins?.entries?.[pluginId];
return {
...config,
plugins: {
...config.plugins,
entries: {
...config.plugins?.entries,
[pluginId]: {
...existingEntry,
enabled: false,
},
},
},
};
}
export async function updateNpmInstalledPlugins(params: {
config: OpenClawConfig;
logger?: PluginUpdateLogger;
pluginIds?: string[];
skipIds?: Set<string>;
skipDisabledPlugins?: boolean;
disableOnFailure?: boolean;
timeoutMs?: number;
dryRun?: boolean;
updateChannel?: UpdateChannel;
@@ -771,6 +789,28 @@ export async function updateNpmInstalledPlugins(params: {
let next = params.config;
let changed = false;
const recordFailure = (pluginId: string, message: string) => {
if (params.disableOnFailure && !params.dryRun) {
const disabledMessage =
`Disabled "${pluginId}" after plugin update failure; OpenClaw will continue without it. ` +
message;
logger.warn?.(disabledMessage);
next = disablePluginConfigEntry(next, pluginId);
changed = true;
outcomes.push({
pluginId,
status: "skipped",
message: disabledMessage,
});
return;
}
outcomes.push({
pluginId,
status: "error",
message,
});
};
for (const pluginId of targets) {
if (params.skipIds?.has(pluginId)) {
outcomes.push({
@@ -928,11 +968,7 @@ export async function updateNpmInstalledPlugins(params: {
record.installPath?.trim() || resolvePluginInstallDir(pluginId),
);
} catch (err) {
outcomes.push({
pluginId,
status: "error",
message: `Invalid install path for "${pluginId}": ${String(err)}`,
});
recordFailure(pluginId, `Invalid install path for "${pluginId}": ${String(err)}`);
continue;
}
const currentVersion = await readInstalledPackageVersion(installPath);
@@ -1037,11 +1073,7 @@ export async function updateNpmInstalledPlugins(params: {
logger,
});
} catch (err) {
outcomes.push({
pluginId,
status: "error",
message: `Failed to check ${pluginId}: ${String(err)}`,
});
recordFailure(pluginId, `Failed to check ${pluginId}: ${String(err)}`);
continue;
}
let usedNpmFallback = false;
@@ -1096,43 +1128,41 @@ export async function updateNpmInstalledPlugins(params: {
});
}
if (!probe.ok) {
outcomes.push({
recordFailure(
pluginId,
status: "error",
message:
record.source === "npm"
? formatNpmInstallFailure({
record.source === "npm"
? formatNpmInstallFailure({
pluginId,
spec: npmUpdateFailureSpec({
effectiveSpec,
fallbackSpec: npmSpecs?.fallbackSpec,
usedFallback: usedNpmFallback,
}),
phase: "check",
result: probe,
})
: record.source === "clawhub"
? formatClawHubInstallFailure({
pluginId,
spec: npmUpdateFailureSpec({
effectiveSpec,
fallbackSpec: npmSpecs?.fallbackSpec,
usedFallback: usedNpmFallback,
}),
spec: effectiveSpec ?? `clawhub:${record.clawhubPackage!}`,
phase: "check",
result: probe,
error: probe.error,
})
: record.source === "clawhub"
? formatClawHubInstallFailure({
: record.source === "git"
? formatGitInstallFailure({
pluginId,
spec: effectiveSpec ?? `clawhub:${record.clawhubPackage!}`,
spec: effectiveSpec!,
phase: "check",
error: probe.error,
})
: record.source === "git"
? formatGitInstallFailure({
pluginId,
spec: effectiveSpec!,
phase: "check",
error: probe.error,
})
: formatMarketplaceInstallFailure({
pluginId,
marketplaceSource: record.marketplaceSource!,
marketplacePlugin: record.marketplacePlugin!,
phase: "check",
error: probe.error,
}),
});
: formatMarketplaceInstallFailure({
pluginId,
marketplaceSource: record.marketplaceSource!,
marketplacePlugin: record.marketplacePlugin!,
phase: "check",
error: probe.error,
}),
);
continue;
}
@@ -1224,11 +1254,7 @@ export async function updateNpmInstalledPlugins(params: {
logger,
});
} catch (err) {
outcomes.push({
pluginId,
status: "error",
message: `Failed to update ${pluginId}: ${String(err)}`,
});
recordFailure(pluginId, `Failed to update ${pluginId}: ${String(err)}`);
continue;
}
let usedNpmFallback = false;
@@ -1281,43 +1307,41 @@ export async function updateNpmInstalledPlugins(params: {
});
}
if (!result.ok) {
outcomes.push({
recordFailure(
pluginId,
status: "error",
message:
record.source === "npm"
? formatNpmInstallFailure({
record.source === "npm"
? formatNpmInstallFailure({
pluginId,
spec: npmUpdateFailureSpec({
effectiveSpec,
fallbackSpec: npmSpecs?.fallbackSpec,
usedFallback: usedNpmFallback,
}),
phase: "update",
result: result,
})
: record.source === "clawhub"
? formatClawHubInstallFailure({
pluginId,
spec: npmUpdateFailureSpec({
effectiveSpec,
fallbackSpec: npmSpecs?.fallbackSpec,
usedFallback: usedNpmFallback,
}),
spec: effectiveSpec ?? `clawhub:${record.clawhubPackage!}`,
phase: "update",
result: result,
error: result.error,
})
: record.source === "clawhub"
? formatClawHubInstallFailure({
: record.source === "git"
? formatGitInstallFailure({
pluginId,
spec: effectiveSpec ?? `clawhub:${record.clawhubPackage!}`,
spec: effectiveSpec!,
phase: "update",
error: result.error,
})
: record.source === "git"
? formatGitInstallFailure({
pluginId,
spec: effectiveSpec!,
phase: "update",
error: result.error,
})
: formatMarketplaceInstallFailure({
pluginId,
marketplaceSource: record.marketplaceSource!,
marketplacePlugin: record.marketplacePlugin!,
phase: "update",
error: result.error,
}),
});
: formatMarketplaceInstallFailure({
pluginId,
marketplaceSource: record.marketplaceSource!,
marketplacePlugin: record.marketplacePlugin!,
phase: "update",
error: result.error,
}),
);
continue;
}