fix(update): retry npm updates without optional deps

This commit is contained in:
Peter Steinberger
2026-04-26 09:49:44 +01:00
parent 832bdbc777
commit 42487d0dac
3 changed files with 91 additions and 1 deletions

View File

@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
- Gateway/Tailscale: let Tailscale-authenticated Control UI operator sessions with browser device identity skip the device-pairing round trip while still rejecting device-less and node-role connections. Refs #71986. Thanks @jokedul.
- Doctor: honor `OPENCLAW_SERVICE_REPAIR_POLICY=external` by reporting gateway service health while skipping service install/start/restart/bootstrap, supervisor rewrites, and legacy service cleanup for externally managed environments. Thanks @shakkernerd.
- CLI/update: run package post-update doctor with `--fix` so package updates repair config migrations before restart. Thanks @shakkernerd.
- CLI/update: retry failed npm global updates with `--omit=optional` and ignore the superseded first failure when the fallback succeeds. Thanks @shakkernerd.
- Agents/Discord: keep raw `Agent failed before reply` runner failures out of Discord group/channel chats and show detailed runner errors in direct chats only when `/verbose` is enabled. Thanks @codex.
- Package: include patched dependency files in the published npm package so downstream installs can resolve `patchedDependencies`. (#69224) Thanks @gucasbrg and @vincentkoc.
- Plugins/channels: treat malformed bundled channel plugin loaders that return `undefined` as unavailable instead of crashing config and help paths. Fixes #69044. Thanks @frankhli843 and @vincentkoc.

View File

@@ -1283,6 +1283,76 @@ describe("update-cli", () => {
).not.toContain("already-current");
});
it("retries package updates without optional deps when npm global update fails", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-optional-"));
const nodeModules = path.join(tempDir, "node_modules");
const pkgRoot = path.join(nodeModules, "openclaw");
mockPackageInstallStatus(pkgRoot);
await fs.mkdir(pkgRoot, { recursive: true });
await fs.writeFile(
path.join(pkgRoot, "package.json"),
JSON.stringify({ name: "openclaw", version: "1.0.0" }),
"utf-8",
);
vi.mocked(runCommandWithTimeout).mockImplementation(async (argv) => {
if (Array.isArray(argv) && argv[0] === "npm" && argv[1] === "root" && argv[2] === "-g") {
return {
stdout: `${nodeModules}\n`,
stderr: "",
code: 0,
signal: null,
killed: false,
termination: "exit",
};
}
if (
Array.isArray(argv) &&
argv[0] === "npm" &&
argv.includes("-g") &&
!argv.includes("--omit=optional")
) {
return {
stdout: "",
stderr: "node-gyp failed",
code: 1,
signal: null,
killed: false,
termination: "exit",
};
}
return {
stdout: "",
stderr: "",
code: 0,
signal: null,
killed: false,
termination: "exit",
};
});
await updateCommand({ yes: true, restart: false });
expect(runCommandWithTimeout).toHaveBeenCalledWith(
["npm", "i", "-g", "openclaw@latest", "--no-fund", "--no-audit", "--loglevel=error"],
expect.any(Object),
);
expect(runCommandWithTimeout).toHaveBeenCalledWith(
[
"npm",
"i",
"-g",
"openclaw@latest",
"--omit=optional",
"--no-fund",
"--no-audit",
"--loglevel=error",
],
expect.any(Object),
);
expect(defaultRuntime.exit).not.toHaveBeenCalledWith(1);
});
it("uses the owning npm binary for package updates when PATH npm points elsewhere", async () => {
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
const brewPrefix = createCaseDir("brew-prefix");

View File

@@ -37,6 +37,7 @@ import {
canResolveRegistryVersionForPackageTarget,
createGlobalInstallEnv,
cleanupGlobalRenameDirs,
globalInstallFallbackArgs,
globalInstallArgs,
resolveExpectedInstalledVersionFromSpec,
resolveGlobalInstallTarget,
@@ -407,6 +408,21 @@ async function runPackageInstallUpdate(params: {
});
const steps = [updateStep];
let finalInstallStep = updateStep;
if (updateStep.exitCode !== 0) {
const fallbackArgv = globalInstallFallbackArgs(installTarget, installSpec);
if (fallbackArgv) {
const fallbackStep = await runUpdateStep({
name: "global update (omit optional)",
argv: fallbackArgv,
env: installEnv,
timeoutMs: params.timeoutMs,
progress: params.progress,
});
steps.push(fallbackStep);
finalInstallStep = fallbackStep;
}
}
let afterVersion = beforeVersion;
const verifiedPackageRoot =
@@ -451,7 +467,10 @@ async function runPackageInstallUpdate(params: {
}
}
const failedStep = steps.find((step) => step.exitCode !== 0);
const failedStep =
finalInstallStep.exitCode !== 0
? finalInstallStep
: (steps.find((step) => step !== updateStep && step.exitCode !== 0) ?? null);
return {
status: failedStep ? "error" : "ok",
mode: manager,