fix: make live updater checkout portable (oc-237) (#122873)

This commit is contained in:
Josh Lehman
2026-08-12 17:17:18 -07:00
committed by GitHub
parent a3207b574c
commit 542ac9ac68
3 changed files with 14 additions and 9 deletions
@@ -5,7 +5,7 @@ description: "Maintain the canonical live OpenClaw main checkout, managed Gatewa
# OpenClaw Live Updater
Keep `/Users/steipete/openclaw` a read-only-to-the-agent deployment mirror: clean, standalone, full, on `main`, and fast-forwarded only. Make every repair in the controlling Codex project worktree.
Keep one operator-selected canonical live checkout as a read-only-to-the-agent deployment mirror: clean, standalone, full, on `main`, and fast-forwarded only. Make every repair in the controlling Codex project worktree.
## Boundaries
@@ -20,10 +20,11 @@ Keep `/Users/steipete/openclaw` a read-only-to-the-agent deployment mirror: clea
1. Run the deterministic updater and retain its JSON:
```bash
cd "<canonical-live-checkout>"
node --import tsx .agents/skills/openclaw-live-updater/scripts/update-main.mjs
```
Stop on any failed invariant. Do not repair the mirror destructively. The helper holds one checkout-scoped lock across update, build, Gateway proof, and Mac work. A concurrent heartbeat returns `reason: "overlap"`; it must not start another build. A dead owner lock may be recovered, but unreadable or unsafe lock state fails closed.
The helper owns the current working directory by default; it does not search parent directories or infer a clone from the user's home directory. From a controlling worktree, pass the canonical mirror explicitly with `--checkout "<canonical-live-checkout>"`. Stop on any failed invariant. Do not repair the mirror destructively. The helper holds one checkout-scoped lock across update, build, Gateway proof, and Mac work. A concurrent heartbeat returns `reason: "overlap"`; it must not start another build. A dead owner lock may be recovered, but unreadable or unsafe lock state fails closed.
2. The helper verifies one unrewritten expected origin, an owned non-symlinked standalone/full clone, single worktree, clean `main`, fetches `origin/main`, rechecks for concurrent changes, and merges `--ff-only`. It then uses the source runner's canonical local-build metadata contract and parser: both `dist/.buildstamp` and `dist/.runtime-postbuildstamp` heads, required runtime-postbuild outputs, `dist/entry.js`, Control UI index plus referenced local assets, and `dist/build-info.json` must all match exact `afterSha`.
@@ -39,7 +40,7 @@ Keep `/Users/steipete/openclaw` a read-only-to-the-agent deployment mirror: clea
Re-run the canonical freshness check immediately before every `pnpm openclaw` restart or probe so the source runner cannot hide stale output with an implicit auto-build. Every pass, including a no-update/current-build pass, must run deep RPC status and verbose health. If that first probe fails while the build is already exact-current, perform one managed Gateway restart and repeat both probes once. Do not rebuild a current exact-SHA artifact merely to self-heal the managed process; fail and diagnose if the one restart does not recover it.
3. If changed paths can affect macOS, the helper runs `scripts/restart-mac.sh --sign --wait --target-only` with `SKIP_TSC=1` and `SKIP_UI_BUILD=1` only after the exact-SHA JS/UI build completes. Reusing those artifacts keeps the live app bundle out of any later JavaScript build cleanup. Target-only mode may stop the canonical `/Applications/OpenClaw.app` process and this checkout's exact `dist` process before launching the rebuilt `dist` app. It defers when another worktree, temporary bundle, test, or agent-owned OpenClaw process is active; it never kills that process. The script's immediate `OK` is not proof. The helper waits and requires the exact executable `/Users/steipete/openclaw/dist/OpenClaw.app/Contents/MacOS/OpenClaw`, then repeats Gateway RPC and health proof.
3. If changed paths can affect macOS, the helper runs `scripts/restart-mac.sh --sign --wait --target-only` with `SKIP_TSC=1` and `SKIP_UI_BUILD=1` only after the exact-SHA JS/UI build completes. Reusing those artifacts keeps the live app bundle out of any later JavaScript build cleanup. Target-only mode may stop the canonical `/Applications/OpenClaw.app` process and this checkout's exact `dist` process before launching the rebuilt `dist` app. It defers when another worktree, temporary bundle, test, or agent-owned OpenClaw process is active; it never kills that process. The script's immediate `OK` is not proof. The helper waits and requires the exact executable `<canonical-live-checkout>/dist/OpenClaw.app/Contents/MacOS/OpenClaw`, derived from the verified checkout, then repeats Gateway RPC and health proof.
Never kill another worktree, temporary bundle, test, or agent-owned OpenClaw process. If a foreign app prevents the exact target from staying alive, record the pending Mac attempt, report it, and retry on the next heartbeat. Escalate only after the conflict persists across repeated heartbeats; never claim Mac proof from another bundle or the short launch check. If `actions.macUiVerification` is true, exercise the changed behavior with the existing macOS/UI automation workflow after delayed exact-bundle proof.
@@ -35,7 +35,6 @@ import {
resolveRuntimePostBuildRequirement,
} from "../../../../scripts/run-node.mts";
const DEFAULT_CHECKOUT = "/Users/steipete/openclaw";
const DEFAULT_EXPECTED_ORIGIN = "openclaw/openclaw";
const FULL_SHA_RE = /^[0-9a-f]{40}$/u;
const GATEWAY_READINESS_ATTEMPTS = 7;
@@ -3800,7 +3799,7 @@ export async function maintainMain(options, dependencies = {}) {
}
function parseArgs(argv) {
const options = { checkout: DEFAULT_CHECKOUT, remote: "origin" };
const options = { checkout: process.cwd(), remote: "origin" };
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--checkout") {
+9 -4
View File
@@ -1537,7 +1537,7 @@ console.log(JSON.stringify({ ok: true, channels: {} }));
});
test("accepts only the delayed exact target bundle process", () => {
const executable = "/Users/steipete/openclaw/dist/OpenClaw.app/Contents/MacOS/OpenClaw";
const executable = "/fixture/live-checkout/dist/OpenClaw.app/Contents/MacOS/OpenClaw";
const foreign = "41 /tmp/agent/OpenClaw.app/Contents/MacOS/OpenClaw";
expect(findExactMacTarget(foreign, executable)).toBeNull();
expect(findExactMacTarget(`${foreign}\n42 ${executable} --attach-only`, executable)).toEqual({
@@ -3436,7 +3436,7 @@ console.log(JSON.stringify({ ok: true, channels: {} }));
});
});
test("keeps successful CLI stdout as one machine-readable JSON object", () => {
test("defaults to the current standalone checkout without a machine-specific path", () => {
const { root, mirror, origin } = makeFixture();
mkdirSync(path.join(mirror, "node_modules"));
writeBuild(mirror);
@@ -3453,14 +3453,19 @@ console.log(JSON.stringify({ ok: true, channels: {} }));
chmodSync(pnpm, 0o755);
chmodSync(gitShim, 0o755);
const result = spawnSync(process.execPath, [script, "--checkout", mirror], {
const result = spawnSync(process.execPath, [script], {
cwd: mirror,
encoding: "utf8",
env: { ...process.env, PATH: `${binDir}:${process.env.PATH}` },
});
expect(result.status, result.stderr).toBe(0);
expect(result.stdout.trim().split("\n")).toHaveLength(1);
expect(JSON.parse(result.stdout)).toMatchObject({ ok: true, updated: false });
expect(JSON.parse(result.stdout)).toMatchObject({
ok: true,
checkout: realpathSync(mirror),
updated: false,
});
expect(result.stderr).toContain("child-output");
});