mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 14:48:28 +00:00
fix(update): run auto-update via runtime argv and keep it independent of checkOnStart
This commit is contained in:
@@ -48,6 +48,7 @@ describe("update-startup", () => {
|
|||||||
let resolveOpenClawPackageRoot: (typeof import("./openclaw-root.js"))["resolveOpenClawPackageRoot"];
|
let resolveOpenClawPackageRoot: (typeof import("./openclaw-root.js"))["resolveOpenClawPackageRoot"];
|
||||||
let checkUpdateStatus: (typeof import("./update-check.js"))["checkUpdateStatus"];
|
let checkUpdateStatus: (typeof import("./update-check.js"))["checkUpdateStatus"];
|
||||||
let resolveNpmChannelTag: (typeof import("./update-check.js"))["resolveNpmChannelTag"];
|
let resolveNpmChannelTag: (typeof import("./update-check.js"))["resolveNpmChannelTag"];
|
||||||
|
let runCommandWithTimeout: (typeof import("../process/exec.js"))["runCommandWithTimeout"];
|
||||||
let runGatewayUpdateCheck: (typeof import("./update-startup.js"))["runGatewayUpdateCheck"];
|
let runGatewayUpdateCheck: (typeof import("./update-startup.js"))["runGatewayUpdateCheck"];
|
||||||
let scheduleGatewayUpdateCheck: (typeof import("./update-startup.js"))["scheduleGatewayUpdateCheck"];
|
let scheduleGatewayUpdateCheck: (typeof import("./update-startup.js"))["scheduleGatewayUpdateCheck"];
|
||||||
let getUpdateAvailable: (typeof import("./update-startup.js"))["getUpdateAvailable"];
|
let getUpdateAvailable: (typeof import("./update-startup.js"))["getUpdateAvailable"];
|
||||||
@@ -75,6 +76,7 @@ describe("update-startup", () => {
|
|||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
({ resolveOpenClawPackageRoot } = await import("./openclaw-root.js"));
|
({ resolveOpenClawPackageRoot } = await import("./openclaw-root.js"));
|
||||||
({ checkUpdateStatus, resolveNpmChannelTag } = await import("./update-check.js"));
|
({ checkUpdateStatus, resolveNpmChannelTag } = await import("./update-check.js"));
|
||||||
|
({ runCommandWithTimeout } = await import("../process/exec.js"));
|
||||||
({
|
({
|
||||||
runGatewayUpdateCheck,
|
runGatewayUpdateCheck,
|
||||||
scheduleGatewayUpdateCheck,
|
scheduleGatewayUpdateCheck,
|
||||||
@@ -86,6 +88,7 @@ describe("update-startup", () => {
|
|||||||
vi.mocked(resolveOpenClawPackageRoot).mockClear();
|
vi.mocked(resolveOpenClawPackageRoot).mockClear();
|
||||||
vi.mocked(checkUpdateStatus).mockClear();
|
vi.mocked(checkUpdateStatus).mockClear();
|
||||||
vi.mocked(resolveNpmChannelTag).mockClear();
|
vi.mocked(resolveNpmChannelTag).mockClear();
|
||||||
|
vi.mocked(runCommandWithTimeout).mockClear();
|
||||||
resetUpdateAvailableStateForTest();
|
resetUpdateAvailableStateForTest();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -305,6 +308,7 @@ describe("update-startup", () => {
|
|||||||
expect(runAutoUpdate).toHaveBeenCalledWith({
|
expect(runAutoUpdate).toHaveBeenCalledWith({
|
||||||
channel: "stable",
|
channel: "stable",
|
||||||
timeoutMs: 45 * 60 * 1000,
|
timeoutMs: 45 * 60 * 1000,
|
||||||
|
root: "/opt/openclaw",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -345,9 +349,106 @@ describe("update-startup", () => {
|
|||||||
expect(runAutoUpdate).toHaveBeenCalledWith({
|
expect(runAutoUpdate).toHaveBeenCalledWith({
|
||||||
channel: "beta",
|
channel: "beta",
|
||||||
timeoutMs: 45 * 60 * 1000,
|
timeoutMs: 45 * 60 * 1000,
|
||||||
|
root: "/opt/openclaw",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("runs auto-update when checkOnStart is false but auto-update is enabled", async () => {
|
||||||
|
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw");
|
||||||
|
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||||
|
root: "/opt/openclaw",
|
||||||
|
installKind: "package",
|
||||||
|
packageManager: "npm",
|
||||||
|
} satisfies UpdateCheckResult);
|
||||||
|
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||||
|
tag: "beta",
|
||||||
|
version: "2.0.0-beta.1",
|
||||||
|
});
|
||||||
|
const runAutoUpdate = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
code: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runGatewayUpdateCheck({
|
||||||
|
cfg: {
|
||||||
|
update: {
|
||||||
|
checkOnStart: false,
|
||||||
|
channel: "beta",
|
||||||
|
auto: {
|
||||||
|
enabled: true,
|
||||||
|
betaCheckIntervalHours: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
log: { info: vi.fn() },
|
||||||
|
isNixMode: false,
|
||||||
|
allowInTests: true,
|
||||||
|
runAutoUpdate,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(runAutoUpdate).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses current runtime + entrypoint for default auto-update command execution", async () => {
|
||||||
|
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw");
|
||||||
|
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||||
|
root: "/opt/openclaw",
|
||||||
|
installKind: "package",
|
||||||
|
packageManager: "npm",
|
||||||
|
} satisfies UpdateCheckResult);
|
||||||
|
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||||
|
tag: "beta",
|
||||||
|
version: "2.0.0-beta.1",
|
||||||
|
});
|
||||||
|
vi.mocked(runCommandWithTimeout).mockResolvedValue({
|
||||||
|
stdout: "{}",
|
||||||
|
stderr: "",
|
||||||
|
code: 0,
|
||||||
|
signal: null,
|
||||||
|
killed: false,
|
||||||
|
termination: "exit",
|
||||||
|
});
|
||||||
|
|
||||||
|
const originalArgv = process.argv.slice();
|
||||||
|
process.argv = [process.execPath, "/opt/openclaw/dist/entry.js"];
|
||||||
|
try {
|
||||||
|
await runGatewayUpdateCheck({
|
||||||
|
cfg: {
|
||||||
|
update: {
|
||||||
|
channel: "beta",
|
||||||
|
auto: {
|
||||||
|
enabled: true,
|
||||||
|
betaCheckIntervalHours: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
log: { info: vi.fn() },
|
||||||
|
isNixMode: false,
|
||||||
|
allowInTests: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
process.argv = originalArgv;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(runCommandWithTimeout).toHaveBeenCalledWith(
|
||||||
|
[
|
||||||
|
process.execPath,
|
||||||
|
"/opt/openclaw/dist/entry.js",
|
||||||
|
"update",
|
||||||
|
"--yes",
|
||||||
|
"--channel",
|
||||||
|
"beta",
|
||||||
|
"--json",
|
||||||
|
],
|
||||||
|
expect.objectContaining({
|
||||||
|
timeoutMs: 45 * 60 * 1000,
|
||||||
|
env: expect.objectContaining({
|
||||||
|
OPENCLAW_AUTO_UPDATE: "1",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("scheduleGatewayUpdateCheck returns a cleanup function", async () => {
|
it("scheduleGatewayUpdateCheck returns a cleanup function", async () => {
|
||||||
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw");
|
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw");
|
||||||
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||||
|
|||||||
@@ -231,17 +231,50 @@ function resolveStableAutoApplyAtMs(params: {
|
|||||||
async function runAutoUpdateCommand(params: {
|
async function runAutoUpdateCommand(params: {
|
||||||
channel: "stable" | "beta";
|
channel: "stable" | "beta";
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
|
root?: string;
|
||||||
}): Promise<AutoUpdateRunResult> {
|
}): Promise<AutoUpdateRunResult> {
|
||||||
|
const baseArgs = ["update", "--yes", "--channel", params.channel, "--json"];
|
||||||
|
const execPath = process.execPath?.trim();
|
||||||
|
const argv1 = process.argv[1]?.trim();
|
||||||
|
const lowerExecBase = execPath ? path.basename(execPath).toLowerCase() : "";
|
||||||
|
const runtimeIsNodeOrBun =
|
||||||
|
lowerExecBase === "node" ||
|
||||||
|
lowerExecBase === "node.exe" ||
|
||||||
|
lowerExecBase === "bun" ||
|
||||||
|
lowerExecBase === "bun.exe";
|
||||||
|
const argv: string[] = [];
|
||||||
|
if (execPath && argv1) {
|
||||||
|
argv.push(execPath, argv1, ...baseArgs);
|
||||||
|
} else if (execPath && !runtimeIsNodeOrBun) {
|
||||||
|
argv.push(execPath, ...baseArgs);
|
||||||
|
} else if (execPath && params.root) {
|
||||||
|
const candidates = [
|
||||||
|
path.join(params.root, "dist", "entry.js"),
|
||||||
|
path.join(params.root, "dist", "entry.mjs"),
|
||||||
|
path.join(params.root, "dist", "index.js"),
|
||||||
|
path.join(params.root, "dist", "index.mjs"),
|
||||||
|
];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
try {
|
||||||
|
await fs.access(candidate);
|
||||||
|
argv.push(execPath, candidate, ...baseArgs);
|
||||||
|
break;
|
||||||
|
} catch {
|
||||||
|
// try next candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (argv.length === 0) {
|
||||||
|
argv.push("openclaw", ...baseArgs);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await runCommandWithTimeout(
|
const res = await runCommandWithTimeout(argv, {
|
||||||
["openclaw", "update", "--yes", "--channel", params.channel, "--json"],
|
timeoutMs: params.timeoutMs,
|
||||||
{
|
env: {
|
||||||
timeoutMs: params.timeoutMs,
|
OPENCLAW_AUTO_UPDATE: "1",
|
||||||
env: {
|
|
||||||
OPENCLAW_AUTO_UPDATE: "1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
return {
|
return {
|
||||||
ok: res.code === 0,
|
ok: res.code === 0,
|
||||||
code: res.code,
|
code: res.code,
|
||||||
@@ -273,6 +306,7 @@ export async function runGatewayUpdateCheck(params: {
|
|||||||
runAutoUpdate?: (params: {
|
runAutoUpdate?: (params: {
|
||||||
channel: "stable" | "beta";
|
channel: "stable" | "beta";
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
|
root?: string;
|
||||||
}) => Promise<AutoUpdateRunResult>;
|
}) => Promise<AutoUpdateRunResult>;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if (shouldSkipCheck(Boolean(params.allowInTests))) {
|
if (shouldSkipCheck(Boolean(params.allowInTests))) {
|
||||||
@@ -281,7 +315,9 @@ export async function runGatewayUpdateCheck(params: {
|
|||||||
if (params.isNixMode) {
|
if (params.isNixMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (params.cfg.update?.checkOnStart === false) {
|
const auto = resolveAutoUpdatePolicy(params.cfg);
|
||||||
|
const shouldRunUpdateHints = params.cfg.update?.checkOnStart !== false;
|
||||||
|
if (!shouldRunUpdateHints && !auto.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,11 +325,18 @@ export async function runGatewayUpdateCheck(params: {
|
|||||||
const state = await readState(statePath);
|
const state = await readState(statePath);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const lastCheckedAt = state.lastCheckedAt ? Date.parse(state.lastCheckedAt) : null;
|
const lastCheckedAt = state.lastCheckedAt ? Date.parse(state.lastCheckedAt) : null;
|
||||||
const persistedAvailable = resolvePersistedUpdateAvailable(state);
|
if (shouldRunUpdateHints) {
|
||||||
setUpdateAvailableCache({
|
const persistedAvailable = resolvePersistedUpdateAvailable(state);
|
||||||
next: persistedAvailable,
|
setUpdateAvailableCache({
|
||||||
onUpdateAvailableChange: params.onUpdateAvailableChange,
|
next: persistedAvailable,
|
||||||
});
|
onUpdateAvailableChange: params.onUpdateAvailableChange,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setUpdateAvailableCache({
|
||||||
|
next: null,
|
||||||
|
onUpdateAvailableChange: params.onUpdateAvailableChange,
|
||||||
|
});
|
||||||
|
}
|
||||||
const checkIntervalMs = resolveCheckIntervalMs(params.cfg);
|
const checkIntervalMs = resolveCheckIntervalMs(params.cfg);
|
||||||
if (lastCheckedAt && Number.isFinite(lastCheckedAt)) {
|
if (lastCheckedAt && Number.isFinite(lastCheckedAt)) {
|
||||||
if (now - lastCheckedAt < checkIntervalMs) {
|
if (now - lastCheckedAt < checkIntervalMs) {
|
||||||
@@ -345,15 +388,17 @@ export async function runGatewayUpdateCheck(params: {
|
|||||||
latestVersion: resolved.version,
|
latestVersion: resolved.version,
|
||||||
channel: tag,
|
channel: tag,
|
||||||
};
|
};
|
||||||
setUpdateAvailableCache({
|
if (shouldRunUpdateHints) {
|
||||||
next: nextAvailable,
|
setUpdateAvailableCache({
|
||||||
onUpdateAvailableChange: params.onUpdateAvailableChange,
|
next: nextAvailable,
|
||||||
});
|
onUpdateAvailableChange: params.onUpdateAvailableChange,
|
||||||
|
});
|
||||||
|
}
|
||||||
nextState.lastAvailableVersion = resolved.version;
|
nextState.lastAvailableVersion = resolved.version;
|
||||||
nextState.lastAvailableTag = tag;
|
nextState.lastAvailableTag = tag;
|
||||||
const shouldNotify =
|
const shouldNotify =
|
||||||
state.lastNotifiedVersion !== resolved.version || state.lastNotifiedTag !== tag;
|
state.lastNotifiedVersion !== resolved.version || state.lastNotifiedTag !== tag;
|
||||||
if (shouldNotify) {
|
if (shouldRunUpdateHints && shouldNotify) {
|
||||||
params.log.info(
|
params.log.info(
|
||||||
`update available (${tag}): v${resolved.version} (current v${VERSION}). Run: ${formatCliCommand("openclaw update")}`,
|
`update available (${tag}): v${resolved.version} (current v${VERSION}). Run: ${formatCliCommand("openclaw update")}`,
|
||||||
);
|
);
|
||||||
@@ -361,7 +406,6 @@ export async function runGatewayUpdateCheck(params: {
|
|||||||
nextState.lastNotifiedTag = tag;
|
nextState.lastNotifiedTag = tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto = resolveAutoUpdatePolicy(params.cfg);
|
|
||||||
if (auto.enabled && (channel === "stable" || channel === "beta")) {
|
if (auto.enabled && (channel === "stable" || channel === "beta")) {
|
||||||
const runAuto = params.runAutoUpdate ?? runAutoUpdateCommand;
|
const runAuto = params.runAutoUpdate ?? runAutoUpdateCommand;
|
||||||
const attemptIntervalMs =
|
const attemptIntervalMs =
|
||||||
@@ -407,6 +451,7 @@ export async function runGatewayUpdateCheck(params: {
|
|||||||
const outcome = await runAuto({
|
const outcome = await runAuto({
|
||||||
channel,
|
channel,
|
||||||
timeoutMs: AUTO_UPDATE_COMMAND_TIMEOUT_MS,
|
timeoutMs: AUTO_UPDATE_COMMAND_TIMEOUT_MS,
|
||||||
|
root: root ?? undefined,
|
||||||
});
|
});
|
||||||
if (outcome.ok) {
|
if (outcome.ok) {
|
||||||
nextState.autoLastSuccessVersion = resolved.version;
|
nextState.autoLastSuccessVersion = resolved.version;
|
||||||
|
|||||||
Reference in New Issue
Block a user