mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-29 23:08:46 +00:00
fix(scripts): restore sparse crabbox changed gates
This commit is contained in:
@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Cron: accept leading-plus relative durations such as `+5m` for one-shot `--at` schedules. (#86341) Thanks @mushuiyu886.
|
||||
- Agents/media: preserve async-started media tool metadata so background generation starts no longer surface generic incomplete-turn warnings while replay stays unsafe. (#85933) Thanks @fuller-stack-dev.
|
||||
- Docker E2E: dedupe scheduler lane resources so npm/service package lanes are not over-counted and serialized unnecessarily.
|
||||
- Crabbox: bootstrap Git metadata for sparse remote changed gates so raw synced workspaces can run `pnpm check:changed` from the intended diff.
|
||||
- xAI/LM Studio: avoid buffering ordinary bracketed or `final` prose until stream completion while watching for plain-text tool-call fallbacks.
|
||||
- Discord: suppress a bot's previous reply body and referenced media from prompt context when a user replies to that bot message, while keeping reply metadata for routing. (#86238) Thanks @fuller-stack-dev.
|
||||
- Docker E2E: avoid rebuilding the Control UI twice while preparing the shared OpenClaw package tarball for package-backed scenario runs.
|
||||
|
||||
@@ -10,7 +10,10 @@ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repoLocal = resolveCrabboxBinary(process.env, process.platform);
|
||||
const pathLocal = resolvePathBinary("crabbox", process.env, process.platform);
|
||||
const binary =
|
||||
repoLocal ?? pathLocal ?? resolveGitCommonCrabboxBinary(process.env, process.platform) ?? "crabbox";
|
||||
repoLocal ??
|
||||
pathLocal ??
|
||||
resolveGitCommonCrabboxBinary(process.env, process.platform) ??
|
||||
"crabbox";
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args[0] === "--") {
|
||||
@@ -470,6 +473,21 @@ function absolutizeLocalRunPaths(commandArgs) {
|
||||
return normalizedArgs;
|
||||
}
|
||||
|
||||
function shellQuote(value) {
|
||||
const text = `${value}`;
|
||||
if (text === "") {
|
||||
return "''";
|
||||
}
|
||||
if (/^[A-Za-z0-9_./:=@%+-]+$/u.test(text)) {
|
||||
return text;
|
||||
}
|
||||
return `'${text.replaceAll("'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
function shellJoin(commandArgs) {
|
||||
return commandArgs.map(shellQuote).join(" ");
|
||||
}
|
||||
|
||||
function isLocalContainerProvider(providerName) {
|
||||
return ["local-container", "docker", "container", "local-docker"].includes(providerName);
|
||||
}
|
||||
@@ -511,22 +529,62 @@ function isChangedGateCommand(commandArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
function headInRemoteRefs() {
|
||||
const refs = gitOutput([
|
||||
"for-each-ref",
|
||||
"--contains",
|
||||
"HEAD",
|
||||
"--format=%(refname)",
|
||||
"refs/remotes",
|
||||
]);
|
||||
return refs.status === 0 && refs.stdout !== "";
|
||||
}
|
||||
|
||||
function mergeBaseForChangedGate() {
|
||||
const base = gitOutput(["merge-base", "origin/main", "HEAD"]);
|
||||
return base.status === 0 && base.stdout ? base.stdout : "origin/main";
|
||||
}
|
||||
|
||||
function remoteGitBootstrapForChangedGate(changedGateBase) {
|
||||
const quotedBase = shellQuote(changedGateBase);
|
||||
return [
|
||||
"if ! git rev-parse --git-dir >/dev/null 2>&1; then",
|
||||
"git init -q;",
|
||||
"git remote add origin https://github.com/openclaw/openclaw.git 2>/dev/null || git remote set-url origin https://github.com/openclaw/openclaw.git;",
|
||||
`git fetch -q --depth=1 origin ${quotedBase}:refs/remotes/origin/main;`,
|
||||
"git reset --mixed --quiet refs/remotes/origin/main;",
|
||||
"git add -A;",
|
||||
"if ! git diff --cached --quiet; then git -c user.name=OpenClaw -c user.email=ci@openclaw.local commit -q --no-gpg-sign -m remote-changed-gate-tree; fi;",
|
||||
"fi",
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function isWindowsRemoteTarget(commandArgs) {
|
||||
return (
|
||||
optionValue(commandArgs, "--target") === "windows" || hasOption(commandArgs, "--windows-mode")
|
||||
);
|
||||
}
|
||||
|
||||
function injectRemoteChangedGateGitBootstrap(commandArgs, changedGateBase) {
|
||||
if (!changedGateBase || commandArgs[0] !== "run" || isWindowsRemoteTarget(commandArgs)) {
|
||||
return commandArgs;
|
||||
}
|
||||
|
||||
const { start, optionEnd } = runCommandBounds(commandArgs);
|
||||
if (start < 0) {
|
||||
return commandArgs;
|
||||
}
|
||||
|
||||
const normalizedArgs = [...commandArgs];
|
||||
const remoteCommand = normalizedArgs.slice(start);
|
||||
const originalShellCommand =
|
||||
hasOption(normalizedArgs, "--shell") && remoteCommand.length === 1
|
||||
? remoteCommand[0]
|
||||
: shellJoin(remoteCommand);
|
||||
const shellCommand = `${remoteGitBootstrapForChangedGate(changedGateBase)} && ${originalShellCommand}`;
|
||||
|
||||
if (!hasOption(normalizedArgs, "--shell")) {
|
||||
normalizedArgs.splice(optionEnd, 0, "--shell");
|
||||
}
|
||||
|
||||
const updatedBounds = runCommandBounds(normalizedArgs);
|
||||
normalizedArgs.splice(
|
||||
updatedBounds.start,
|
||||
normalizedArgs.length - updatedBounds.start,
|
||||
shellCommand,
|
||||
);
|
||||
return normalizedArgs;
|
||||
}
|
||||
|
||||
function isSparseCheckout() {
|
||||
const config = gitOutput(["config", "--bool", "core.sparseCheckout"]);
|
||||
if (config.status === 0 && config.stdout === "true") {
|
||||
@@ -544,10 +602,7 @@ function shouldUseFullCheckoutForCleanSparseRemoteSync(commandArgs, providerName
|
||||
if (commandArgs[0] !== "run" || isLocalContainerProvider(providerName)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
hasOption(commandArgs, "--no-sync") ||
|
||||
hasOption(commandArgs, "--id")
|
||||
) {
|
||||
if (hasOption(commandArgs, "--no-sync") || hasOption(commandArgs, "--id")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -738,13 +793,14 @@ if (provider === "blacksmith-testbox") {
|
||||
let childCwd = repoRoot;
|
||||
let cleanupChildCwd = () => {};
|
||||
let cleanupDone = false;
|
||||
let remoteChangedGateBase = "";
|
||||
if (shouldUseFullCheckoutForCleanSparseRemoteSync(normalizedArgs, provider)) {
|
||||
const runWords = runCommandArgs(normalizedArgs);
|
||||
const changedGateBase =
|
||||
isChangedGateCommand(runWords) && !headInRemoteRefs() ? mergeBaseForChangedGate() : "";
|
||||
const changedGateBase = isChangedGateCommand(runWords) ? mergeBaseForChangedGate() : "";
|
||||
const checkout = prepareFullCheckoutForSync({ changedGateBase });
|
||||
childCwd = checkout.dir;
|
||||
cleanupChildCwd = () => checkout.cleanup();
|
||||
remoteChangedGateBase = checkout.changedGateBase;
|
||||
console.error(
|
||||
`[crabbox] sparse clean checkout detected; syncing from temporary full checkout ${checkout.dir}`,
|
||||
);
|
||||
@@ -797,7 +853,13 @@ if (
|
||||
);
|
||||
}
|
||||
|
||||
const childArgs = childCwd === repoRoot ? normalizedArgs : absolutizeLocalRunPaths(normalizedArgs);
|
||||
const childArgs =
|
||||
childCwd === repoRoot
|
||||
? normalizedArgs
|
||||
: injectRemoteChangedGateGitBootstrap(
|
||||
absolutizeLocalRunPaths(normalizedArgs),
|
||||
remoteChangedGateBase,
|
||||
);
|
||||
const childInvocation = spawnInvocation(binary, childArgs, childEnv, process.platform);
|
||||
const child = spawn(childInvocation.command, childInvocation.args, {
|
||||
cwd: childCwd,
|
||||
|
||||
@@ -40,7 +40,9 @@ function writeFakeCrabbox(binDir: string, helpText: string): string {
|
||||
return crabboxPath;
|
||||
}
|
||||
|
||||
function makeFakeGit(responses: Record<string, { status?: number; stdout?: string; stderr?: string }>): string {
|
||||
function makeFakeGit(
|
||||
responses: Record<string, { status?: number; stdout?: string; stderr?: string }>,
|
||||
): string {
|
||||
const binDir = mkdtempSync(path.join(tmpdir(), "openclaw-fake-git-"));
|
||||
tempDirs.push(binDir);
|
||||
const gitPath = path.join(binDir, "git");
|
||||
@@ -91,7 +93,10 @@ function runWrapper(
|
||||
});
|
||||
}
|
||||
|
||||
function parseFakeCrabboxOutput(result: ReturnType<typeof runWrapper>): { args: string[]; cwd: string } {
|
||||
function parseFakeCrabboxOutput(result: ReturnType<typeof runWrapper>): {
|
||||
args: string[];
|
||||
cwd: string;
|
||||
} {
|
||||
return JSON.parse(result.stdout.trim()) as { args: string[]; cwd: string };
|
||||
}
|
||||
|
||||
@@ -261,11 +266,9 @@ describe("scripts/crabbox-wrapper", () => {
|
||||
const staleBinDir = mkdtempSync(path.join(tmpdir(), "openclaw-stale-crabbox-"));
|
||||
tempDirs.push(staleBinDir);
|
||||
writeFileSync(path.join(staleBinDir, "crabbox"), "not executable\n", "utf8");
|
||||
const result = runWrapper(
|
||||
"provider: aws\n",
|
||||
["run", "--provider", "aws", "--", "echo ok"],
|
||||
{ extraPathEntries: [staleBinDir] },
|
||||
);
|
||||
const result = runWrapper("provider: aws\n", ["run", "--provider", "aws", "--", "echo ok"], {
|
||||
extraPathEntries: [staleBinDir],
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(parseFakeCrabboxOutput(result).args).toContain("aws");
|
||||
@@ -411,6 +414,96 @@ describe("scripts/crabbox-wrapper", () => {
|
||||
expect(parseFakeCrabboxOutput(result).cwd).toContain("openclaw-crabbox-sync-");
|
||||
});
|
||||
|
||||
it("bootstraps Git metadata for sparse changed gates on remote raw syncs", () => {
|
||||
const result = runWrapper(
|
||||
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
|
||||
["run", "--provider", "aws", "--", "corepack", "pnpm", "check:changed"],
|
||||
{
|
||||
gitResponses: {
|
||||
["config\u0000--bool\u0000core.sparseCheckout"]: { stdout: "true\n" },
|
||||
["status\u0000--porcelain=v1"]: { stdout: "" },
|
||||
["merge-base\u0000origin/main\u0000HEAD"]: { stdout: "abc123\n" },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const output = parseFakeCrabboxOutput(result);
|
||||
const remoteCommand = output.args.at(-1) ?? "";
|
||||
expect(result.status).toBe(0);
|
||||
expect(output.args).toContain("--shell");
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toContain(
|
||||
"git fetch -q --depth=1 origin abc123:refs/remotes/origin/main",
|
||||
);
|
||||
expect(remoteCommand).toContain("git reset --mixed --quiet refs/remotes/origin/main");
|
||||
expect(remoteCommand).toContain("git add -A");
|
||||
expect(remoteCommand).toContain("git diff --cached --quiet");
|
||||
expect(remoteCommand).toContain("commit -q --no-gpg-sign -m remote-changed-gate-tree");
|
||||
expect(remoteCommand).toMatch(/&& corepack pnpm check:changed$/u);
|
||||
});
|
||||
|
||||
it("preserves existing shell changed-gate commands after remote Git bootstrap", () => {
|
||||
const result = runWrapper(
|
||||
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
|
||||
["run", "--provider", "aws", "--shell", "--", "env CI=1 pnpm check:changed"],
|
||||
{
|
||||
gitResponses: {
|
||||
["config\u0000--bool\u0000core.sparseCheckout"]: { stdout: "true\n" },
|
||||
["status\u0000--porcelain=v1"]: { stdout: "" },
|
||||
["merge-base\u0000origin/main\u0000HEAD"]: { stdout: "abc123\n" },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const output = parseFakeCrabboxOutput(result);
|
||||
const remoteCommand = output.args.at(-1) ?? "";
|
||||
expect(result.status).toBe(0);
|
||||
expect(output.args.filter((arg) => arg === "--shell")).toHaveLength(1);
|
||||
expect(remoteCommand).toContain(
|
||||
"git fetch -q --depth=1 origin abc123:refs/remotes/origin/main",
|
||||
);
|
||||
expect(remoteCommand).toMatch(/&& env CI=1 pnpm check:changed$/u);
|
||||
});
|
||||
|
||||
it("does not inject the POSIX changed-gate bootstrap for Windows targets", () => {
|
||||
const result = runWrapper(
|
||||
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
|
||||
[
|
||||
"run",
|
||||
"--provider",
|
||||
"aws",
|
||||
"--target",
|
||||
"windows",
|
||||
"--",
|
||||
"corepack",
|
||||
"pnpm",
|
||||
"check:changed",
|
||||
],
|
||||
{
|
||||
gitResponses: {
|
||||
["config\u0000--bool\u0000core.sparseCheckout"]: { stdout: "true\n" },
|
||||
["status\u0000--porcelain=v1"]: { stdout: "" },
|
||||
["merge-base\u0000origin/main\u0000HEAD"]: { stdout: "abc123\n" },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const output = parseFakeCrabboxOutput(result);
|
||||
expect(result.status).toBe(0);
|
||||
expect(output.args).not.toContain("--shell");
|
||||
expect(output.args).toEqual([
|
||||
"run",
|
||||
"--provider",
|
||||
"aws",
|
||||
"--target",
|
||||
"windows",
|
||||
"--",
|
||||
"corepack",
|
||||
"pnpm",
|
||||
"check:changed",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps clean sparse local-container syncs on the original checkout", () => {
|
||||
const result = runWrapper(
|
||||
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
|
||||
@@ -508,7 +601,9 @@ describe("scripts/crabbox-wrapper", () => {
|
||||
const output = parseFakeCrabboxOutput(result);
|
||||
expect(result.status).toBe(0);
|
||||
expect(output.cwd).toContain("openclaw-crabbox-sync-");
|
||||
expect(output.args).toContain(`--capture-stdout=${path.join(repoRoot, ".artifacts/stdout.log")}`);
|
||||
expect(output.args).toContain(
|
||||
`--capture-stdout=${path.join(repoRoot, ".artifacts/stdout.log")}`,
|
||||
);
|
||||
expect(output.args).toContain(path.join(repoRoot, ".artifacts/stderr.log"));
|
||||
expect(output.args).toContain(`/tmp/proof=${path.join(repoRoot, ".artifacts/proof")}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user