mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
fix(update): harden post-core finalize (service-env strip, no-op skip)
- Strip gateway service identity (OPENCLAW_SERVICE_MARKER/KIND/PID) from the finalizer child so it is not mistaken for the managed service, matching the CLI post-core spawn. - Skip finalize for no-op git updates (unchanged SHA and version), mirroring the CLI resume gate, to avoid an unnecessary doctor/convergence run.
This commit is contained in:
committed by
Vincent Koc
parent
90d385cb93
commit
1b251a6af1
@@ -41,6 +41,20 @@ describe("runPostCoreFinalizeAfterGatewayUpdate", () => {
|
||||
expect(spawnFinalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips a no-op git update with no core change", async () => {
|
||||
const spawnFinalize = vi.fn<PostCoreFinalizeSpawner>();
|
||||
const outcome = await runPostCoreFinalizeAfterGatewayUpdate({
|
||||
result: gitOkResult({
|
||||
before: { sha: "same", version: "2026.6.1" },
|
||||
after: { sha: "same", version: "2026.6.1" },
|
||||
}),
|
||||
resolveEntrypoint: resolveEntrypointOk,
|
||||
spawnFinalize,
|
||||
});
|
||||
expect(outcome).toEqual({ status: "skipped", reason: "not-git-update" });
|
||||
expect(spawnFinalize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips when no built entrypoint is found", async () => {
|
||||
const spawnFinalize = vi.fn<PostCoreFinalizeSpawner>();
|
||||
const outcome = await runPostCoreFinalizeAfterGatewayUpdate({
|
||||
@@ -63,7 +77,7 @@ describe("runPostCoreFinalizeAfterGatewayUpdate", () => {
|
||||
});
|
||||
expect(outcome).toEqual({ status: "ok", entrypoint: ENTRYPOINT });
|
||||
expect(spawnFinalize).toHaveBeenCalledTimes(1);
|
||||
const call = spawnFinalize.mock.calls[0]![0];
|
||||
const call = spawnFinalize.mock.calls[0][0];
|
||||
// Reconcile runs through the designed finalizer; never restarts (RPC owns restart).
|
||||
expect(call.argv).toEqual([
|
||||
expect.any(String),
|
||||
@@ -82,6 +96,26 @@ describe("runPostCoreFinalizeAfterGatewayUpdate", () => {
|
||||
expect(call.env.OPENCLAW_COMPATIBILITY_HOST_VERSION).toBe("2026.6.1");
|
||||
});
|
||||
|
||||
it("strips the gateway service identity from the finalizer child env", async () => {
|
||||
const spawnFinalize = vi.fn<PostCoreFinalizeSpawner>(async () => ({ code: 0 }));
|
||||
await runPostCoreFinalizeAfterGatewayUpdate({
|
||||
result: gitOkResult(),
|
||||
resolveEntrypoint: resolveEntrypointOk,
|
||||
spawnFinalize,
|
||||
env: {
|
||||
PATH: "/usr/bin",
|
||||
OPENCLAW_SERVICE_MARKER: "openclaw",
|
||||
OPENCLAW_SERVICE_KIND: "gateway",
|
||||
OPENCLAW_GATEWAY_SERVICE_PID: "4242",
|
||||
},
|
||||
});
|
||||
const { env } = spawnFinalize.mock.calls[0][0];
|
||||
expect(env.PATH).toBe("/usr/bin");
|
||||
expect(env.OPENCLAW_SERVICE_MARKER).toBeUndefined();
|
||||
expect(env.OPENCLAW_SERVICE_KIND).toBeUndefined();
|
||||
expect(env.OPENCLAW_GATEWAY_SERVICE_PID).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits channel/timeout flags when not provided", async () => {
|
||||
const spawnFinalize = vi.fn<PostCoreFinalizeSpawner>(async () => ({ code: 0 }));
|
||||
await runPostCoreFinalizeAfterGatewayUpdate({
|
||||
@@ -89,7 +123,7 @@ describe("runPostCoreFinalizeAfterGatewayUpdate", () => {
|
||||
resolveEntrypoint: resolveEntrypointOk,
|
||||
spawnFinalize,
|
||||
});
|
||||
const argv = spawnFinalize.mock.calls[0]![0].argv;
|
||||
const argv = spawnFinalize.mock.calls[0][0].argv;
|
||||
expect(argv).not.toContain("--channel");
|
||||
expect(argv).not.toContain("--timeout");
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
// and `runPostCorePluginConvergence`). Finalization never restarts, so the RPC
|
||||
// handler keeps ownership of the gateway restart.
|
||||
import path from "node:path";
|
||||
import { GATEWAY_SERVICE_RUNTIME_PID_ENV } from "../daemon/constants.js";
|
||||
import { resolveGatewayInstallEntrypoint } from "../daemon/gateway-entrypoint.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { resolveStableNodePath } from "./stable-node-path.js";
|
||||
@@ -25,6 +26,22 @@ import type { UpdateRunResult } from "./update-runner.js";
|
||||
|
||||
const DEFAULT_FINALIZE_TIMEOUT_MS = 30 * 60_000;
|
||||
|
||||
// Strip the running gateway's service identity from the finalizer child so it is
|
||||
// not mistaken for the managed service process (matches the CLI post-core spawn).
|
||||
function buildFinalizeEnv(
|
||||
baseEnv: NodeJS.ProcessEnv,
|
||||
compatHostVersion?: string,
|
||||
): NodeJS.ProcessEnv {
|
||||
const env: NodeJS.ProcessEnv = { ...baseEnv };
|
||||
delete env.OPENCLAW_SERVICE_MARKER;
|
||||
delete env.OPENCLAW_SERVICE_KIND;
|
||||
delete env[GATEWAY_SERVICE_RUNTIME_PID_ENV];
|
||||
if (compatHostVersion) {
|
||||
env.OPENCLAW_COMPATIBILITY_HOST_VERSION = compatHostVersion;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
export type PostCoreFinalizeOutcome =
|
||||
| { status: "skipped"; reason: "not-git-update" | "entrypoint-missing" }
|
||||
| { status: "ok"; entrypoint: string }
|
||||
@@ -50,6 +67,25 @@ const defaultFinalizeSpawner: PostCoreFinalizeSpawner = async ({ argv, cwd, time
|
||||
return { code: res.code, ...(res.stderr ? { stderr: res.stderr } : {}) };
|
||||
};
|
||||
|
||||
function normalizeOptionalString(value: string | null | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
// A no-op git update (same SHA and version) has nothing new to converge against,
|
||||
// so skip finalize to avoid an unnecessary doctor/convergence run. Mirrors the
|
||||
// CLI's `shouldResumePostCoreUpdateInFreshProcess` git resume gate.
|
||||
function gitCoreChanged(result: UpdateRunResult): boolean {
|
||||
const beforeSha = normalizeOptionalString(result.before?.sha);
|
||||
const afterSha = normalizeOptionalString(result.after?.sha);
|
||||
if (beforeSha && afterSha && beforeSha !== afterSha) {
|
||||
return true;
|
||||
}
|
||||
const beforeVersion = normalizeOptionalString(result.before?.version);
|
||||
const afterVersion = normalizeOptionalString(result.after?.version);
|
||||
return Boolean(beforeVersion && afterVersion && beforeVersion !== afterVersion);
|
||||
}
|
||||
|
||||
// Only git/source updates routed through `runGatewayUpdate` defer-and-drop
|
||||
// plugin convergence. Package-manager/global installs already converge because
|
||||
// the RPC routes them through `startManagedServiceUpdateHandoff`, which
|
||||
@@ -61,7 +97,8 @@ function isGitUpdateNeedingFinalize(
|
||||
result.status === "ok" &&
|
||||
result.mode === "git" &&
|
||||
typeof result.root === "string" &&
|
||||
result.root.length > 0
|
||||
result.root.length > 0 &&
|
||||
gitCoreChanged(result)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,10 +160,7 @@ export async function runPostCoreFinalizeAfterGatewayUpdate(params: {
|
||||
// Pin the finalizer's host-compat resolution to the just-installed core
|
||||
// version so plugins reconcile against the new core, not the running process.
|
||||
const compatHostVersion = result.after?.version ?? undefined;
|
||||
const baseEnv = params.env ?? process.env;
|
||||
const env: NodeJS.ProcessEnv = compatHostVersion
|
||||
? { ...baseEnv, OPENCLAW_COMPATIBILITY_HOST_VERSION: compatHostVersion }
|
||||
: { ...baseEnv };
|
||||
const env = buildFinalizeEnv(params.env ?? process.env, compatHostVersion);
|
||||
|
||||
try {
|
||||
const spawnResult = await spawnFinalize({
|
||||
|
||||
Reference in New Issue
Block a user