fix(update): run auto-update via runtime argv and keep it independent of checkOnStart

This commit is contained in:
Peter Steinberger
2026-02-22 17:40:42 +01:00
parent 35b162af76
commit dbc1ed8933
2 changed files with 166 additions and 20 deletions

View File

@@ -48,6 +48,7 @@ describe("update-startup", () => {
let resolveOpenClawPackageRoot: (typeof import("./openclaw-root.js"))["resolveOpenClawPackageRoot"];
let checkUpdateStatus: (typeof import("./update-check.js"))["checkUpdateStatus"];
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 scheduleGatewayUpdateCheck: (typeof import("./update-startup.js"))["scheduleGatewayUpdateCheck"];
let getUpdateAvailable: (typeof import("./update-startup.js"))["getUpdateAvailable"];
@@ -75,6 +76,7 @@ describe("update-startup", () => {
if (!loaded) {
({ resolveOpenClawPackageRoot } = await import("./openclaw-root.js"));
({ checkUpdateStatus, resolveNpmChannelTag } = await import("./update-check.js"));
({ runCommandWithTimeout } = await import("../process/exec.js"));
({
runGatewayUpdateCheck,
scheduleGatewayUpdateCheck,
@@ -86,6 +88,7 @@ describe("update-startup", () => {
vi.mocked(resolveOpenClawPackageRoot).mockClear();
vi.mocked(checkUpdateStatus).mockClear();
vi.mocked(resolveNpmChannelTag).mockClear();
vi.mocked(runCommandWithTimeout).mockClear();
resetUpdateAvailableStateForTest();
});
@@ -305,6 +308,7 @@ describe("update-startup", () => {
expect(runAutoUpdate).toHaveBeenCalledWith({
channel: "stable",
timeoutMs: 45 * 60 * 1000,
root: "/opt/openclaw",
});
});
@@ -345,9 +349,106 @@ describe("update-startup", () => {
expect(runAutoUpdate).toHaveBeenCalledWith({
channel: "beta",
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 () => {
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw");
vi.mocked(checkUpdateStatus).mockResolvedValue({

View File

@@ -231,17 +231,50 @@ function resolveStableAutoApplyAtMs(params: {
async function runAutoUpdateCommand(params: {
channel: "stable" | "beta";
timeoutMs: number;
root?: string;
}): 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 {
const res = await runCommandWithTimeout(
["openclaw", "update", "--yes", "--channel", params.channel, "--json"],
{
timeoutMs: params.timeoutMs,
env: {
OPENCLAW_AUTO_UPDATE: "1",
},
const res = await runCommandWithTimeout(argv, {
timeoutMs: params.timeoutMs,
env: {
OPENCLAW_AUTO_UPDATE: "1",
},
);
});
return {
ok: res.code === 0,
code: res.code,
@@ -273,6 +306,7 @@ export async function runGatewayUpdateCheck(params: {
runAutoUpdate?: (params: {
channel: "stable" | "beta";
timeoutMs: number;
root?: string;
}) => Promise<AutoUpdateRunResult>;
}): Promise<void> {
if (shouldSkipCheck(Boolean(params.allowInTests))) {
@@ -281,7 +315,9 @@ export async function runGatewayUpdateCheck(params: {
if (params.isNixMode) {
return;
}
if (params.cfg.update?.checkOnStart === false) {
const auto = resolveAutoUpdatePolicy(params.cfg);
const shouldRunUpdateHints = params.cfg.update?.checkOnStart !== false;
if (!shouldRunUpdateHints && !auto.enabled) {
return;
}
@@ -289,11 +325,18 @@ export async function runGatewayUpdateCheck(params: {
const state = await readState(statePath);
const now = Date.now();
const lastCheckedAt = state.lastCheckedAt ? Date.parse(state.lastCheckedAt) : null;
const persistedAvailable = resolvePersistedUpdateAvailable(state);
setUpdateAvailableCache({
next: persistedAvailable,
onUpdateAvailableChange: params.onUpdateAvailableChange,
});
if (shouldRunUpdateHints) {
const persistedAvailable = resolvePersistedUpdateAvailable(state);
setUpdateAvailableCache({
next: persistedAvailable,
onUpdateAvailableChange: params.onUpdateAvailableChange,
});
} else {
setUpdateAvailableCache({
next: null,
onUpdateAvailableChange: params.onUpdateAvailableChange,
});
}
const checkIntervalMs = resolveCheckIntervalMs(params.cfg);
if (lastCheckedAt && Number.isFinite(lastCheckedAt)) {
if (now - lastCheckedAt < checkIntervalMs) {
@@ -345,15 +388,17 @@ export async function runGatewayUpdateCheck(params: {
latestVersion: resolved.version,
channel: tag,
};
setUpdateAvailableCache({
next: nextAvailable,
onUpdateAvailableChange: params.onUpdateAvailableChange,
});
if (shouldRunUpdateHints) {
setUpdateAvailableCache({
next: nextAvailable,
onUpdateAvailableChange: params.onUpdateAvailableChange,
});
}
nextState.lastAvailableVersion = resolved.version;
nextState.lastAvailableTag = tag;
const shouldNotify =
state.lastNotifiedVersion !== resolved.version || state.lastNotifiedTag !== tag;
if (shouldNotify) {
if (shouldRunUpdateHints && shouldNotify) {
params.log.info(
`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;
}
const auto = resolveAutoUpdatePolicy(params.cfg);
if (auto.enabled && (channel === "stable" || channel === "beta")) {
const runAuto = params.runAutoUpdate ?? runAutoUpdateCommand;
const attemptIntervalMs =
@@ -407,6 +451,7 @@ export async function runGatewayUpdateCheck(params: {
const outcome = await runAuto({
channel,
timeoutMs: AUTO_UPDATE_COMMAND_TIMEOUT_MS,
root: root ?? undefined,
});
if (outcome.ok) {
nextState.autoLastSuccessVersion = resolved.version;