mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix(update): retry npm updates without optional deps
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user