mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
fix(update): resume git post-update in updated process
This commit is contained in:
@@ -602,6 +602,36 @@ describe("update-cli", () => {
|
||||
expect(runDaemonRestart).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("respawns into the updated git root before requested channel persistence", async () => {
|
||||
const { entrypoints } = setupUpdatedRootRefresh({
|
||||
gatewayUpdateImpl: async (root) =>
|
||||
makeOkUpdateResult({
|
||||
mode: "git",
|
||||
root,
|
||||
before: { sha: "old-sha", version: "2026.4.26" },
|
||||
after: { sha: "new-sha", version: "2026.4.27" },
|
||||
}),
|
||||
});
|
||||
|
||||
await updateCommand({ channel: "dev", yes: true, restart: false });
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/node/),
|
||||
[entrypoints[0], "update", "--no-restart", "--yes"],
|
||||
expect.objectContaining({
|
||||
stdio: "inherit",
|
||||
env: expect.objectContaining({
|
||||
OPENCLAW_UPDATE_POST_CORE: "1",
|
||||
OPENCLAW_UPDATE_POST_CORE_CHANNEL: "dev",
|
||||
OPENCLAW_UPDATE_POST_CORE_REQUESTED_CHANNEL: "dev",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(replaceConfigFile).not.toHaveBeenCalled();
|
||||
expect(syncPluginsForUpdateChannel).not.toHaveBeenCalled();
|
||||
expect(updateNpmInstalledPlugins).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps downgrade post-update work in the current process", async () => {
|
||||
const downgradedRoot = createCaseDir("openclaw-downgraded-root");
|
||||
setupUpdatedRootRefresh({
|
||||
@@ -686,6 +716,47 @@ describe("update-cli", () => {
|
||||
expect(spawn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("post-core resume mode persists the requested update channel with the updated process", async () => {
|
||||
vi.mocked(readConfigFileSnapshot).mockResolvedValue({
|
||||
...baseSnapshot,
|
||||
parsed: { update: { channel: "stable" } },
|
||||
resolved: { update: { channel: "stable" } } as OpenClawConfig,
|
||||
sourceConfig: { update: { channel: "stable" } } as OpenClawConfig,
|
||||
runtimeConfig: { update: { channel: "stable" } } as OpenClawConfig,
|
||||
config: { update: { channel: "stable" } } as OpenClawConfig,
|
||||
hash: "stable-hash",
|
||||
});
|
||||
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_UPDATE_POST_CORE: "1",
|
||||
OPENCLAW_UPDATE_POST_CORE_CHANNEL: "dev",
|
||||
OPENCLAW_UPDATE_POST_CORE_REQUESTED_CHANNEL: "dev",
|
||||
},
|
||||
async () => {
|
||||
await updateCommand({ restart: false });
|
||||
},
|
||||
);
|
||||
|
||||
expect(runGatewayUpdate).not.toHaveBeenCalled();
|
||||
expect(replaceConfigFile).toHaveBeenCalledWith({
|
||||
nextConfig: {
|
||||
update: {
|
||||
channel: "dev",
|
||||
},
|
||||
},
|
||||
baseHash: "stable-hash",
|
||||
});
|
||||
expect(syncPluginsForUpdateChannel).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "dev",
|
||||
config: expect.objectContaining({
|
||||
update: expect.objectContaining({ channel: "dev" }),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes the update timeout budget into post-core plugin updates", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
@@ -1538,7 +1609,18 @@ describe("update-cli", () => {
|
||||
[expect.stringMatching(/node/), entryPath, "doctor", "--non-interactive", "--fix"],
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(updateNpmInstalledPlugins).toHaveBeenCalled();
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/node/),
|
||||
[entryPath, "update", "--no-restart", "--yes"],
|
||||
expect.objectContaining({
|
||||
stdio: "inherit",
|
||||
env: expect.objectContaining({
|
||||
OPENCLAW_UPDATE_POST_CORE: "1",
|
||||
OPENCLAW_UPDATE_POST_CORE_CHANNEL: "stable",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(updateNpmInstalledPlugins).not.toHaveBeenCalled();
|
||||
expect(
|
||||
vi
|
||||
.mocked(defaultRuntime.log)
|
||||
|
||||
@@ -54,7 +54,6 @@ import { defaultRuntime } from "../../runtime.js";
|
||||
import { normalizeOptionalString } from "../../shared/string-coerce.js";
|
||||
import { stylePromptMessage } from "../../terminal/prompt-style.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import { pathExists } from "../../utils.js";
|
||||
import { replaceCliName, resolveCliName } from "../cli-name.js";
|
||||
import { formatCliCommand } from "../command-format.js";
|
||||
import { installCompletion } from "../completion-runtime.js";
|
||||
@@ -93,6 +92,7 @@ const SERVICE_REFRESH_TIMEOUT_MS = 60_000;
|
||||
const DEFAULT_UPDATE_STEP_TIMEOUT_MS = 30 * 60_000;
|
||||
const POST_CORE_UPDATE_ENV = "OPENCLAW_UPDATE_POST_CORE";
|
||||
const POST_CORE_UPDATE_CHANNEL_ENV = "OPENCLAW_UPDATE_POST_CORE_CHANNEL";
|
||||
const POST_CORE_UPDATE_REQUESTED_CHANNEL_ENV = "OPENCLAW_UPDATE_POST_CORE_REQUESTED_CHANNEL";
|
||||
const POST_CORE_UPDATE_RESULT_PATH_ENV = "OPENCLAW_UPDATE_POST_CORE_RESULT_PATH";
|
||||
const SERVICE_REFRESH_PATH_ENV_KEYS = [
|
||||
"OPENCLAW_HOME",
|
||||
@@ -1093,6 +1093,40 @@ async function runPostCorePluginUpdate(params: {
|
||||
});
|
||||
}
|
||||
|
||||
async function persistRequestedUpdateChannel(params: {
|
||||
configSnapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>;
|
||||
requestedChannel: "stable" | "beta" | "dev" | null;
|
||||
}): Promise<Awaited<ReturnType<typeof readConfigFileSnapshot>>> {
|
||||
if (!params.requestedChannel || !params.configSnapshot.valid) {
|
||||
return params.configSnapshot;
|
||||
}
|
||||
const storedChannel = normalizeUpdateChannel(params.configSnapshot.config.update?.channel);
|
||||
if (params.requestedChannel === storedChannel) {
|
||||
return params.configSnapshot;
|
||||
}
|
||||
|
||||
const next = {
|
||||
...params.configSnapshot.sourceConfig,
|
||||
update: {
|
||||
...params.configSnapshot.sourceConfig.update,
|
||||
channel: params.requestedChannel,
|
||||
},
|
||||
};
|
||||
await replaceConfigFile({
|
||||
nextConfig: next,
|
||||
baseHash: params.configSnapshot.hash,
|
||||
});
|
||||
return {
|
||||
...params.configSnapshot,
|
||||
hash: undefined,
|
||||
parsed: next,
|
||||
sourceConfig: asResolvedSourceConfig(next),
|
||||
resolved: asResolvedSourceConfig(next),
|
||||
runtimeConfig: asRuntimeConfig(next),
|
||||
config: asRuntimeConfig(next),
|
||||
};
|
||||
}
|
||||
|
||||
async function writePostCorePluginUpdateResultFile(
|
||||
filePath: string | undefined,
|
||||
result: PostCorePluginUpdateResult,
|
||||
@@ -1125,10 +1159,11 @@ async function readPostCorePluginUpdateResultFile(
|
||||
async function continuePostCoreUpdateInFreshProcess(params: {
|
||||
root: string;
|
||||
channel: "stable" | "beta" | "dev";
|
||||
requestedChannel: "stable" | "beta" | "dev" | null;
|
||||
opts: UpdateCommandOptions;
|
||||
}): Promise<{ resumed: boolean; pluginUpdate?: PostCorePluginUpdateResult }> {
|
||||
const entryPath = path.join(params.root, "dist", "entry.js");
|
||||
if (!(await pathExists(entryPath))) {
|
||||
const entryPath = await resolveGatewayInstallEntrypoint(params.root);
|
||||
if (!entryPath) {
|
||||
return { resumed: false };
|
||||
}
|
||||
|
||||
@@ -1158,6 +1193,9 @@ async function continuePostCoreUpdateInFreshProcess(params: {
|
||||
...disableUpdatedPackageCompileCacheEnv(process.env),
|
||||
[POST_CORE_UPDATE_ENV]: "1",
|
||||
[POST_CORE_UPDATE_CHANNEL_ENV]: params.channel,
|
||||
...(params.requestedChannel
|
||||
? { [POST_CORE_UPDATE_REQUESTED_CHANNEL_ENV]: params.requestedChannel }
|
||||
: {}),
|
||||
...(resultPath ? { [POST_CORE_UPDATE_RESULT_PATH_ENV]: resultPath } : {}),
|
||||
},
|
||||
});
|
||||
@@ -1195,7 +1233,23 @@ function shouldResumePostCoreUpdateInFreshProcess(params: {
|
||||
result: UpdateRunResult;
|
||||
downgradeRisk: boolean;
|
||||
}): boolean {
|
||||
return isPackageManagerUpdateMode(params.result.mode) && !params.downgradeRisk;
|
||||
if (params.downgradeRisk) {
|
||||
return false;
|
||||
}
|
||||
if (isPackageManagerUpdateMode(params.result.mode)) {
|
||||
return true;
|
||||
}
|
||||
if (params.result.mode !== "git") {
|
||||
return false;
|
||||
}
|
||||
const beforeSha = normalizeOptionalString(params.result.before?.sha);
|
||||
const afterSha = normalizeOptionalString(params.result.after?.sha);
|
||||
if (beforeSha && afterSha && beforeSha !== afterSha) {
|
||||
return true;
|
||||
}
|
||||
const beforeVersion = normalizeOptionalString(params.result.before?.version);
|
||||
const afterVersion = normalizeOptionalString(params.result.after?.version);
|
||||
return Boolean(beforeVersion && afterVersion && beforeVersion !== afterVersion);
|
||||
}
|
||||
|
||||
export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
@@ -1203,6 +1257,8 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
const invocationCwd = tryResolveInvocationCwd();
|
||||
const postCoreUpdateResume = process.env[POST_CORE_UPDATE_ENV] === "1";
|
||||
const postCoreUpdateChannel = process.env[POST_CORE_UPDATE_CHANNEL_ENV]?.trim();
|
||||
const postCoreRequestedChannelInput =
|
||||
process.env[POST_CORE_UPDATE_REQUESTED_CHANNEL_ENV]?.trim() ?? "";
|
||||
|
||||
const timeoutMs = parseTimeoutMsOrExit(opts.timeout);
|
||||
const shouldRestart = opts.restart !== false;
|
||||
@@ -1223,10 +1279,24 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const postCoreRequestedChannel = postCoreRequestedChannelInput
|
||||
? normalizeUpdateChannel(postCoreRequestedChannelInput)
|
||||
: null;
|
||||
if (postCoreRequestedChannelInput && !postCoreRequestedChannel) {
|
||||
defaultRuntime.error("Invalid post-core requested update channel context.");
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const postCoreConfigSnapshot = await persistRequestedUpdateChannel({
|
||||
configSnapshot: await readConfigFileSnapshot(),
|
||||
requestedChannel: postCoreRequestedChannel,
|
||||
});
|
||||
|
||||
const pluginUpdate = await runPostCorePluginUpdate({
|
||||
root,
|
||||
channel: postCoreUpdateChannel,
|
||||
configSnapshot: await readConfigFileSnapshot(),
|
||||
configSnapshot: postCoreConfigSnapshot,
|
||||
opts,
|
||||
timeoutMs: updateStepTimeoutMs,
|
||||
});
|
||||
@@ -1567,46 +1637,45 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldResumePostCoreInFreshProcess = shouldResumePostCoreUpdateInFreshProcess({
|
||||
result,
|
||||
downgradeRisk,
|
||||
});
|
||||
|
||||
let postUpdateConfigSnapshot = configSnapshot;
|
||||
if (requestedChannel && configSnapshot.valid && requestedChannel !== storedChannel) {
|
||||
const next = {
|
||||
...configSnapshot.sourceConfig,
|
||||
update: {
|
||||
...configSnapshot.sourceConfig.update,
|
||||
channel: requestedChannel,
|
||||
},
|
||||
};
|
||||
await replaceConfigFile({
|
||||
nextConfig: next,
|
||||
baseHash: configSnapshot.hash,
|
||||
if (!shouldResumePostCoreInFreshProcess) {
|
||||
postUpdateConfigSnapshot = await persistRequestedUpdateChannel({
|
||||
configSnapshot,
|
||||
requestedChannel,
|
||||
});
|
||||
postUpdateConfigSnapshot = {
|
||||
...configSnapshot,
|
||||
hash: undefined,
|
||||
parsed: next,
|
||||
sourceConfig: asResolvedSourceConfig(next),
|
||||
resolved: asResolvedSourceConfig(next),
|
||||
runtimeConfig: asRuntimeConfig(next),
|
||||
config: asRuntimeConfig(next),
|
||||
};
|
||||
if (!opts.json) {
|
||||
defaultRuntime.log(theme.muted(`Update channel set to ${requestedChannel}.`));
|
||||
}
|
||||
}
|
||||
if (
|
||||
requestedChannel &&
|
||||
configSnapshot.valid &&
|
||||
requestedChannel !== storedChannel &&
|
||||
!shouldResumePostCoreInFreshProcess &&
|
||||
!opts.json
|
||||
) {
|
||||
defaultRuntime.log(theme.muted(`Update channel set to ${requestedChannel}.`));
|
||||
} else if (
|
||||
requestedChannel &&
|
||||
configSnapshot.valid &&
|
||||
requestedChannel !== storedChannel &&
|
||||
shouldResumePostCoreInFreshProcess &&
|
||||
!opts.json
|
||||
) {
|
||||
defaultRuntime.log(theme.muted(`Update channel will be set to ${requestedChannel}.`));
|
||||
}
|
||||
|
||||
const postUpdateRoot = result.root ?? root;
|
||||
|
||||
let postCorePluginUpdate: PostCorePluginUpdateResult | undefined;
|
||||
let pluginsUpdatedInFreshProcess = false;
|
||||
if (
|
||||
shouldResumePostCoreUpdateInFreshProcess({
|
||||
result,
|
||||
downgradeRisk,
|
||||
})
|
||||
) {
|
||||
if (shouldResumePostCoreInFreshProcess) {
|
||||
const freshProcessResult = await continuePostCoreUpdateInFreshProcess({
|
||||
root: postUpdateRoot,
|
||||
channel,
|
||||
requestedChannel,
|
||||
opts,
|
||||
});
|
||||
pluginsUpdatedInFreshProcess = freshProcessResult.resumed;
|
||||
@@ -1614,6 +1683,12 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
}
|
||||
|
||||
if (!pluginsUpdatedInFreshProcess) {
|
||||
if (shouldResumePostCoreInFreshProcess) {
|
||||
postUpdateConfigSnapshot = await persistRequestedUpdateChannel({
|
||||
configSnapshot,
|
||||
requestedChannel,
|
||||
});
|
||||
}
|
||||
postCorePluginUpdate = await runPostCorePluginUpdate({
|
||||
root: postUpdateRoot,
|
||||
channel,
|
||||
|
||||
Reference in New Issue
Block a user