mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 02:50:35 +00:00
fix(scripts): detect shell-wrapped changed gates
This commit is contained in:
@@ -134,6 +134,15 @@ const shellControlCommandPrefixes = new Set([
|
||||
"!",
|
||||
]);
|
||||
const shellCommandExecutionPrefixes = new Set(["exec"]);
|
||||
const shellInlineCommandInterpreters = new Set(["bash", "dash", "ksh", "sh", "zsh"]);
|
||||
const shellInlineCommandOptionsWithNextValue = new Set([
|
||||
"+O",
|
||||
"+o",
|
||||
"-O",
|
||||
"-o",
|
||||
"--init-file",
|
||||
"--rcfile",
|
||||
]);
|
||||
|
||||
function escapeBatchCommand(command) {
|
||||
return `${command}`.replace(cmdMetaCharactersRe, "^$1");
|
||||
@@ -553,18 +562,38 @@ function commandRuntimeEntrypoint(commandArgs) {
|
||||
|
||||
function commandWordsRuntimeEntrypoint(words) {
|
||||
const first = (words[0] ?? "").split("/").pop();
|
||||
return jsRuntimeEntrypoints.has(first) ? first : "";
|
||||
if (jsRuntimeEntrypoints.has(first)) {
|
||||
return first;
|
||||
}
|
||||
|
||||
const inlineCommand = shellInlineCommand(words);
|
||||
if (!inlineCommand) {
|
||||
return "";
|
||||
}
|
||||
for (const candidateWords of shellCommandWordCandidates(inlineCommand)) {
|
||||
const shellRuntime = commandWordsRuntimeEntrypoint(candidateWords);
|
||||
if (shellRuntime) {
|
||||
return shellRuntime;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function isChangedGateCommand(commandArgs) {
|
||||
if (commandArgs.length === 1) {
|
||||
return shellCommandWordCandidates(commandArgs[0]).some(isChangedGateWords);
|
||||
return shellCommandWordCandidates(commandArgs[0]).some(isChangedGateCommandWords);
|
||||
}
|
||||
const words = normalizedCommandWords(commandArgs);
|
||||
return isChangedGateCommandWords(words);
|
||||
}
|
||||
|
||||
function isChangedGateCommandWords(words) {
|
||||
if (isChangedGateWords(words)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
const inlineCommand = shellInlineCommand(words);
|
||||
return inlineCommand ? shellCommandWordCandidates(inlineCommand).some(isChangedGateCommandWords) : false;
|
||||
}
|
||||
|
||||
function isChangedGateWords(words) {
|
||||
@@ -579,6 +608,34 @@ function isChangedGateWords(words) {
|
||||
);
|
||||
}
|
||||
|
||||
function shellInlineCommand(words) {
|
||||
const command = shellWordBasename(words[0]);
|
||||
if (!shellInlineCommandInterpreters.has(command)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
for (let index = 1; index < words.length; index += 1) {
|
||||
const word = words[index];
|
||||
if (word === "--") {
|
||||
return "";
|
||||
}
|
||||
if (!word.startsWith("-") && !word.startsWith("+")) {
|
||||
return "";
|
||||
}
|
||||
if (word === "-c" || /^-[^-]*c/u.test(word)) {
|
||||
return words[index + 1] ?? "";
|
||||
}
|
||||
if (shellInlineCommandOptionConsumesNextValue(word)) {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function shellInlineCommandOptionConsumesNextValue(word) {
|
||||
return shellInlineCommandOptionsWithNextValue.has(word) || /^[+-][^-+]*[oO]$/u.test(word);
|
||||
}
|
||||
|
||||
function shellCommandWordCandidates(command) {
|
||||
return shellCommandSegments(stripHeredocBodies(command.replace(/\\\r?\n/gu, " ")));
|
||||
}
|
||||
|
||||
@@ -1117,6 +1117,28 @@ describe("scripts/crabbox-wrapper", () => {
|
||||
expectGroupedShellCommand(remoteCommand, shellScript);
|
||||
});
|
||||
|
||||
it("preserves macOS JS and Git bootstraps for shell-wrapped sparse changed gates", () => {
|
||||
const shellScript = "bash -lc 'pnpm check:changed'";
|
||||
const result = runWrapper(
|
||||
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
|
||||
["run", "--provider", "aws", "--target", "macos", "--shell", "--", shellScript],
|
||||
{
|
||||
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 = normalizeShellLineEndings(output.args.at(-1) ?? "");
|
||||
expect(result.status).toBe(0);
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toContain("openclaw_crabbox_bootstrap_macos_js");
|
||||
expectGroupedShellCommand(remoteCommand, shellScript);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for assignment-prefix command substitutions", () => {
|
||||
const shellScript = "TOOL_ROOT=$(pwd) pnpm check:changed";
|
||||
const result = runWrapper(
|
||||
@@ -1159,6 +1181,94 @@ describe("scripts/crabbox-wrapper", () => {
|
||||
expect(remoteCommand).toContain(`&& ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for bash -lc shell commands", () => {
|
||||
const shellScript =
|
||||
"env CI=1 NODE_OPTIONS=--max-old-space-size=4096 bash -lc 'set -euo pipefail; pnpm check:changed'";
|
||||
const result = runWrapper(
|
||||
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
|
||||
["run", "--provider", "aws", "--shell", "--", shellScript],
|
||||
{
|
||||
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 = normalizeShellLineEndings(output.args.at(-1) ?? "");
|
||||
expect(result.status).toBe(0);
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toContain(
|
||||
"git fetch -q --depth=1 origin abc123:refs/remotes/origin/main",
|
||||
);
|
||||
expect(remoteCommand).toContain(`&& ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for shell option values before -c", () => {
|
||||
const shellScript = "bash -o pipefail -c 'pnpm check:changed'";
|
||||
const result = runWrapper(
|
||||
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
|
||||
["run", "--provider", "aws", "--shell", "--", shellScript],
|
||||
{
|
||||
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 = normalizeShellLineEndings(output.args.at(-1) ?? "");
|
||||
expect(result.status).toBe(0);
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toContain(`&& ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for attached shell option values before -c", () => {
|
||||
const shellScript = "bash --rcfile=./ci.bashrc -c 'pnpm check:changed'";
|
||||
const result = runWrapper(
|
||||
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
|
||||
["run", "--provider", "aws", "--shell", "--", shellScript],
|
||||
{
|
||||
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 = normalizeShellLineEndings(output.args.at(-1) ?? "");
|
||||
expect(result.status).toBe(0);
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toContain(`&& ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for grouped shell options before -c", () => {
|
||||
const shellScript = "bash -eo pipefail -c 'pnpm check:changed'";
|
||||
const result = runWrapper(
|
||||
"provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",
|
||||
["run", "--provider", "aws", "--shell", "--", shellScript],
|
||||
{
|
||||
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 = normalizeShellLineEndings(output.args.at(-1) ?? "");
|
||||
expect(result.status).toBe(0);
|
||||
expect(remoteCommand).toContain("git init -q");
|
||||
expect(remoteCommand).toContain(`&& ${shellScript}`);
|
||||
});
|
||||
|
||||
it("preserves sparse changed-gate Git bootstrap for absolute time-prefixed shell commands", () => {
|
||||
const shellScript = "/usr/bin/time -l pnpm check:changed";
|
||||
const result = runWrapper(
|
||||
|
||||
Reference in New Issue
Block a user