improve runtest, optimize CI batching (#210)
This commit is contained in:
committed by
pullfrog[bot]
parent
2b3bd97b86
commit
18ba8e5fd0
@@ -98759,9 +98759,6 @@ configure({
|
||||
}
|
||||
});
|
||||
|
||||
// mcp/server.ts
|
||||
import { createServer } from "node:net";
|
||||
|
||||
// node_modules/.pnpm/@modelcontextprotocol+sdk@1.25.2_hono@4.11.3_zod@4.3.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
|
||||
init_v3();
|
||||
|
||||
@@ -128227,6 +128224,9 @@ var FastMCP = class extends FastMCPEventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
// mcp/server.ts
|
||||
import { createServer } from "node:net";
|
||||
|
||||
// node_modules/.pnpm/@ark+schema@0.56.0/node_modules/@ark/schema/out/shared/errors.js
|
||||
var ArkError = class _ArkError extends CastableBase {
|
||||
[arkKind] = "error";
|
||||
@@ -141505,8 +141505,118 @@ function isMetadataYarnClassic(metadataPath) {
|
||||
|
||||
// utils/subprocess.ts
|
||||
import { spawn as nodeSpawn } from "node:child_process";
|
||||
|
||||
// utils/activity.ts
|
||||
var DEFAULT_ACTIVITY_TIMEOUT_MS = 3e4;
|
||||
var DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5e3;
|
||||
function wrapWrite(original, onActivity) {
|
||||
const wrapped = (chunk, encodingOrCb, cb) => {
|
||||
onActivity();
|
||||
if (typeof encodingOrCb === "function") {
|
||||
return original(chunk, encodingOrCb);
|
||||
}
|
||||
return original(chunk, encodingOrCb, cb);
|
||||
};
|
||||
return wrapped;
|
||||
}
|
||||
function startProcessOutputMonitor(ctx) {
|
||||
let lastActivity = Date.now();
|
||||
let timedOut = false;
|
||||
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
||||
function markActivity() {
|
||||
lastActivity = Date.now();
|
||||
}
|
||||
process.stdout.write = wrapWrite(originalStdoutWrite, markActivity);
|
||||
process.stderr.write = wrapWrite(originalStderrWrite, markActivity);
|
||||
const intervalId = setInterval(() => {
|
||||
const idleMs = Date.now() - lastActivity;
|
||||
if (timedOut || idleMs <= ctx.timeoutMs) return;
|
||||
timedOut = true;
|
||||
ctx.onTimeout(idleMs);
|
||||
}, ctx.checkIntervalMs);
|
||||
function stop() {
|
||||
clearInterval(intervalId);
|
||||
process.stdout.write = originalStdoutWrite;
|
||||
process.stderr.write = originalStderrWrite;
|
||||
}
|
||||
return { stop };
|
||||
}
|
||||
function createProcessOutputActivityTimeout(ctx) {
|
||||
let rejectFn = null;
|
||||
const promise2 = new Promise((_, reject) => {
|
||||
rejectFn = reject;
|
||||
});
|
||||
let monitor = null;
|
||||
monitor = startProcessOutputMonitor({
|
||||
timeoutMs: ctx.timeoutMs,
|
||||
checkIntervalMs: ctx.checkIntervalMs,
|
||||
onTimeout: (idleMs) => {
|
||||
if (!rejectFn) return;
|
||||
const idleSec = Math.round(idleMs / 1e3);
|
||||
if (monitor) {
|
||||
monitor.stop();
|
||||
}
|
||||
rejectFn(new Error(`activity timeout: no output for ${idleSec}s`));
|
||||
}
|
||||
});
|
||||
return {
|
||||
promise: promise2,
|
||||
stop: monitor.stop
|
||||
};
|
||||
}
|
||||
|
||||
// utils/subprocess.ts
|
||||
var activeChildren = /* @__PURE__ */ new Map();
|
||||
var externalSignalHandler = null;
|
||||
function trackChild(options) {
|
||||
activeChildren.set(options.child, options.killGroup ?? false);
|
||||
}
|
||||
function untrackChild(child) {
|
||||
activeChildren.delete(child);
|
||||
}
|
||||
function killTrackedChildren() {
|
||||
const count = activeChildren.size;
|
||||
for (const entry of activeChildren) {
|
||||
const child = entry[0];
|
||||
const killGroup = entry[1];
|
||||
if (killGroup && child.pid) {
|
||||
try {
|
||||
process.kill(-child.pid, "SIGKILL");
|
||||
continue;
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
child.kill("SIGKILL");
|
||||
}
|
||||
return count;
|
||||
}
|
||||
function cleanupAndExit(signal) {
|
||||
const count = killTrackedChildren();
|
||||
if (count > 0) {
|
||||
log.info(`\xBB received ${signal}, killing ${count} subprocess(es)...`);
|
||||
}
|
||||
setTimeout(() => process.exit(1), 500).unref();
|
||||
process.exit(1);
|
||||
}
|
||||
function handleSignal(signal) {
|
||||
if (externalSignalHandler) {
|
||||
externalSignalHandler(signal);
|
||||
return;
|
||||
}
|
||||
cleanupAndExit(signal);
|
||||
}
|
||||
var handlersInstalled = false;
|
||||
function installSignalHandlers() {
|
||||
if (handlersInstalled) return;
|
||||
handlersInstalled = true;
|
||||
process.on("SIGINT", () => handleSignal("SIGINT"));
|
||||
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
||||
}
|
||||
async function spawn2(options) {
|
||||
const { cmd, args: args3, env: env3, input, timeout, cwd: cwd2, stdio, onStdout, onStderr } = options;
|
||||
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
||||
installSignalHandlers();
|
||||
const startTime = Date.now();
|
||||
let stdoutBuffer = "";
|
||||
let stderrBuffer = "";
|
||||
@@ -141519,8 +141629,12 @@ async function spawn2(options) {
|
||||
stdio: stdio || ["pipe", "pipe", "pipe"],
|
||||
cwd: cwd2 || process.cwd()
|
||||
});
|
||||
trackChild({ child });
|
||||
let timeoutId;
|
||||
let activityCheckIntervalId;
|
||||
let isTimedOut = false;
|
||||
let isActivityTimedOut = false;
|
||||
let lastActivityTime = Date.now();
|
||||
if (timeout) {
|
||||
timeoutId = setTimeout(() => {
|
||||
isTimedOut = true;
|
||||
@@ -141532,8 +141646,24 @@ async function spawn2(options) {
|
||||
}, 5e3);
|
||||
}, timeout);
|
||||
}
|
||||
if (activityTimeoutMs > 0) {
|
||||
activityCheckIntervalId = setInterval(() => {
|
||||
const idleMs = Date.now() - lastActivityTime;
|
||||
if (idleMs > activityTimeoutMs) {
|
||||
isActivityTimedOut = true;
|
||||
const idleSec = Math.round(idleMs / 1e3);
|
||||
log.error(`no output for ${idleSec}s, killing process`);
|
||||
child.kill("SIGKILL");
|
||||
clearInterval(activityCheckIntervalId);
|
||||
}
|
||||
}, DEFAULT_ACTIVITY_CHECK_INTERVAL_MS);
|
||||
}
|
||||
function updateActivity() {
|
||||
lastActivityTime = Date.now();
|
||||
}
|
||||
if (child.stdout) {
|
||||
child.stdout.on("data", (data) => {
|
||||
updateActivity();
|
||||
const chunk = data.toString();
|
||||
stdoutBuffer += chunk;
|
||||
onStdout?.(chunk);
|
||||
@@ -141541,6 +141671,7 @@ async function spawn2(options) {
|
||||
}
|
||||
if (child.stderr) {
|
||||
child.stderr.on("data", (data) => {
|
||||
updateActivity();
|
||||
const chunk = data.toString();
|
||||
stderrBuffer += chunk;
|
||||
onStderr?.(chunk);
|
||||
@@ -141548,11 +141679,16 @@ async function spawn2(options) {
|
||||
}
|
||||
child.on("close", (exitCode) => {
|
||||
const durationMs = Date.now() - startTime;
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
untrackChild(child);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
||||
if (isTimedOut) {
|
||||
reject(new Error(`Process timed out after ${timeout}ms`));
|
||||
reject(new Error(`process timed out after ${timeout}ms`));
|
||||
return;
|
||||
}
|
||||
if (isActivityTimedOut) {
|
||||
const idleSec = Math.round((Date.now() - lastActivityTime) / 1e3);
|
||||
reject(new Error(`activity timeout: no output for ${idleSec}s`));
|
||||
return;
|
||||
}
|
||||
resolve2({
|
||||
@@ -141564,10 +141700,10 @@ async function spawn2(options) {
|
||||
});
|
||||
child.on("error", (error50) => {
|
||||
const durationMs = Date.now() - startTime;
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
console.error(`[spawn] Process spawn error: ${error50.message}`);
|
||||
untrackChild(child);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
if (activityCheckIntervalId) clearInterval(activityCheckIntervalId);
|
||||
console.error(`[spawn] process spawn error: ${error50.message}`);
|
||||
resolve2({
|
||||
stdout: stdoutBuffer,
|
||||
stderr: stderrBuffer,
|
||||
@@ -143164,53 +143300,39 @@ function initToolState(ctx) {
|
||||
backgroundProcesses: /* @__PURE__ */ new Map()
|
||||
};
|
||||
}
|
||||
async function findAvailablePort(startPort) {
|
||||
const checkPort = (port2) => {
|
||||
return new Promise((resolve2) => {
|
||||
const server = createServer();
|
||||
server.once("error", () => {
|
||||
server.close();
|
||||
resolve2(false);
|
||||
});
|
||||
server.listen(port2, () => {
|
||||
server.close(() => {
|
||||
resolve2(true);
|
||||
});
|
||||
});
|
||||
var mcpPortStart = 3764;
|
||||
var mcpPortAttempts = 100;
|
||||
var mcpHost = "127.0.0.1";
|
||||
var mcpEndpoint = "/mcp";
|
||||
function readEnvPort() {
|
||||
const rawPort = process.env.PULLFROG_MCP_PORT;
|
||||
if (!rawPort) return null;
|
||||
const parsed2 = Number.parseInt(rawPort, 10);
|
||||
if (!Number.isInteger(parsed2) || parsed2 <= 0 || parsed2 > 65535) {
|
||||
throw new Error(`invalid PULLFROG_MCP_PORT: ${rawPort}`);
|
||||
}
|
||||
return parsed2;
|
||||
}
|
||||
function isPortAvailable(port) {
|
||||
return new Promise((resolve2) => {
|
||||
const server = createServer();
|
||||
server.unref();
|
||||
server.once("error", () => resolve2(false));
|
||||
server.once("listening", () => {
|
||||
server.close(() => resolve2(true));
|
||||
});
|
||||
};
|
||||
let port = startPort;
|
||||
while (port < startPort + 100) {
|
||||
if (await checkPort(port)) {
|
||||
return port;
|
||||
}
|
||||
port++;
|
||||
}
|
||||
throw new Error(`Could not find available port starting from ${startPort}`);
|
||||
}
|
||||
async function killBackgroundProcesses(toolState) {
|
||||
const backgroundProcesses = toolState.backgroundProcesses;
|
||||
if (backgroundProcesses.size === 0) return;
|
||||
for (const proc of backgroundProcesses.values()) {
|
||||
try {
|
||||
process.kill(-proc.pid, "SIGTERM");
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
await new Promise((resolve2) => setTimeout(resolve2, 200));
|
||||
for (const proc of backgroundProcesses.values()) {
|
||||
try {
|
||||
process.kill(-proc.pid, "SIGKILL");
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
backgroundProcesses.clear();
|
||||
}
|
||||
async function startMcpHttpServer(ctx) {
|
||||
const server = new FastMCP({
|
||||
name: ghPullfrogMcpName,
|
||||
version: "0.0.1"
|
||||
server.listen(port, mcpHost);
|
||||
});
|
||||
}
|
||||
function getErrorMessage(error50) {
|
||||
if (error50 instanceof Error) return error50.message;
|
||||
return String(error50);
|
||||
}
|
||||
function isAddressInUse(error50) {
|
||||
const message = getErrorMessage(error50).toLowerCase();
|
||||
return message.includes("eaddrinuse") || message.includes("address already in use");
|
||||
}
|
||||
function buildTools(ctx) {
|
||||
const tools = [
|
||||
SelectModeTool(ctx),
|
||||
StartDependencyInstallationTool(ctx),
|
||||
@@ -143242,24 +143364,96 @@ async function startMcpHttpServer(ctx) {
|
||||
tools.push(KillBackgroundTool(ctx));
|
||||
}
|
||||
tools.push(ReportProgressTool(ctx));
|
||||
addTools(ctx, server, tools);
|
||||
const port = await findAvailablePort(3764);
|
||||
const host = "127.0.0.1";
|
||||
const endpoint2 = "/mcp";
|
||||
await server.start({
|
||||
transportType: "httpStream",
|
||||
httpStream: {
|
||||
port,
|
||||
host,
|
||||
endpoint: endpoint2
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
async function tryStartMcpServer(ctx, port) {
|
||||
const server = new FastMCP({
|
||||
name: ghPullfrogMcpName,
|
||||
version: "0.0.1"
|
||||
});
|
||||
const url4 = `http://${host}:${port}${endpoint2}`;
|
||||
const tools = buildTools(ctx);
|
||||
addTools(ctx, server, tools);
|
||||
try {
|
||||
await server.start({
|
||||
transportType: "httpStream",
|
||||
httpStream: {
|
||||
port,
|
||||
host: mcpHost,
|
||||
endpoint: mcpEndpoint
|
||||
}
|
||||
});
|
||||
const url4 = `http://${mcpHost}:${port}${mcpEndpoint}`;
|
||||
return { server, url: url4, port };
|
||||
} catch (error50) {
|
||||
if (!isAddressInUse(error50)) {
|
||||
throw error50;
|
||||
}
|
||||
try {
|
||||
await server.stop();
|
||||
} catch {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function selectMcpPort(ctx) {
|
||||
let lastError = null;
|
||||
const requestedPort = readEnvPort();
|
||||
if (requestedPort !== null) {
|
||||
if (await isPortAvailable(requestedPort)) {
|
||||
const requestedResult = await tryStartMcpServer(ctx, requestedPort);
|
||||
if (requestedResult) {
|
||||
return requestedResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
const randomOffset = Math.floor(Math.random() * 50);
|
||||
for (let offset = 0; offset < mcpPortAttempts; offset++) {
|
||||
const port = mcpPortStart + randomOffset + offset;
|
||||
try {
|
||||
if (!await isPortAvailable(port)) {
|
||||
continue;
|
||||
}
|
||||
const result = await tryStartMcpServer(ctx, port);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
} catch (error50) {
|
||||
lastError = error50;
|
||||
if (!isAddressInUse(error50)) {
|
||||
throw error50;
|
||||
}
|
||||
}
|
||||
}
|
||||
const message = getErrorMessage(lastError);
|
||||
throw new Error(
|
||||
`could not find available mcp port starting at ${mcpPortStart} (last error: ${message})`
|
||||
);
|
||||
}
|
||||
async function killBackgroundProcesses(toolState) {
|
||||
const backgroundProcesses = toolState.backgroundProcesses;
|
||||
if (backgroundProcesses.size === 0) return;
|
||||
for (const proc of backgroundProcesses.values()) {
|
||||
try {
|
||||
process.kill(-proc.pid, "SIGTERM");
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
await new Promise((resolve2) => setTimeout(resolve2, 200));
|
||||
for (const proc of backgroundProcesses.values()) {
|
||||
try {
|
||||
process.kill(-proc.pid, "SIGKILL");
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
backgroundProcesses.clear();
|
||||
}
|
||||
async function startMcpHttpServer(ctx) {
|
||||
const startResult = await selectMcpPort(ctx);
|
||||
return {
|
||||
url: url4,
|
||||
url: startResult.url,
|
||||
[Symbol.asyncDispose]: async () => {
|
||||
await killBackgroundProcesses(ctx.toolState);
|
||||
await server.stop();
|
||||
await startResult.server.stop();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -160990,6 +161184,10 @@ var messageHandlers2 = {
|
||||
} else if (item.type === "mcp_tool_call") {
|
||||
if (item.status === "failed" && item.error) {
|
||||
log.warning(`MCP tool call failed: ${item.error.message}`);
|
||||
} else if (item.output) {
|
||||
const output = item.output;
|
||||
const outputStr = typeof output === "string" ? output : JSON.stringify(output);
|
||||
log.debug(`tool output: ${outputStr}`);
|
||||
}
|
||||
} else if (item.type === "reasoning") {
|
||||
const reasoningText = item.text.trim();
|
||||
@@ -161093,9 +161291,11 @@ var cursor = agent({
|
||||
if (isError) {
|
||||
log.warning("Tool call failed");
|
||||
} else {
|
||||
const text = result?.content?.[0]?.text?.text;
|
||||
const contentItem = result?.content?.[0];
|
||||
const textValue = contentItem?.text;
|
||||
const text = typeof textValue === "string" ? textValue : textValue?.text;
|
||||
if (text) {
|
||||
console.log(text);
|
||||
log.debug(`tool output: ${text}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161289,6 +161489,9 @@ var messageHandlers3 = {
|
||||
if (event.status === "error") {
|
||||
const errorMsg = typeof event.output === "string" ? event.output : JSON.stringify(event.output);
|
||||
log.warning(`Tool call failed: ${errorMsg}`);
|
||||
} else if (event.output) {
|
||||
const outputStr = typeof event.output === "string" ? event.output : JSON.stringify(event.output);
|
||||
log.debug(`tool output: ${outputStr}`);
|
||||
}
|
||||
},
|
||||
result: async (event) => {
|
||||
@@ -161735,6 +161938,9 @@ var messageHandlers4 = {
|
||||
if (status === "error") {
|
||||
const errorMsg = typeof output === "string" ? output : JSON.stringify(output);
|
||||
log.error(`\xBB \u274C tool call failed: ${errorMsg}`);
|
||||
} else if (output) {
|
||||
const outputStr = typeof output === "string" ? output : JSON.stringify(output);
|
||||
log.debug(`tool output: ${outputStr}`);
|
||||
}
|
||||
},
|
||||
result: async (event) => {
|
||||
@@ -162049,12 +162255,12 @@ The workflow encountered an error before any progress could be reported. Please
|
||||
}
|
||||
}
|
||||
cleanupFn = cleanup;
|
||||
function handleSignal() {
|
||||
function handleSignal2() {
|
||||
log.info("\xBB workflow cancelled, cleaning up...");
|
||||
cleanup(true).finally(() => process.exit(1));
|
||||
}
|
||||
process.on("SIGINT", handleSignal);
|
||||
process.on("SIGTERM", handleSignal);
|
||||
process.on("SIGINT", handleSignal2);
|
||||
process.on("SIGTERM", handleSignal2);
|
||||
}
|
||||
async function runCleanup() {
|
||||
try {
|
||||
@@ -162693,6 +162899,7 @@ async function main() {
|
||||
try {
|
||||
normalizeEnv();
|
||||
const timer = new Timer();
|
||||
let activityTimeout = null;
|
||||
const tokenRef = __using(_stack2, await resolveInstallationToken(), true);
|
||||
const octokit = createOctokit(tokenRef.token);
|
||||
const runContext = await resolveRunContextData({ octokit, token: tokenRef.token });
|
||||
@@ -162757,6 +162964,12 @@ async function main() {
|
||||
repo: runContext.repo,
|
||||
modes: modes2
|
||||
});
|
||||
activityTimeout = createProcessOutputActivityTimeout({
|
||||
timeoutMs: DEFAULT_ACTIVITY_TIMEOUT_MS,
|
||||
checkIntervalMs: DEFAULT_ACTIVITY_CHECK_INTERVAL_MS
|
||||
});
|
||||
activityTimeout.promise.catch(() => {
|
||||
});
|
||||
const agentPromise = agent2.run({
|
||||
payload,
|
||||
mcpServerUrl: mcpHttpServer.url,
|
||||
@@ -162765,11 +162978,11 @@ async function main() {
|
||||
});
|
||||
let result;
|
||||
if (payload.timeout === TIMEOUT_DISABLED) {
|
||||
result = await agentPromise;
|
||||
result = await Promise.race([agentPromise, activityTimeout.promise]);
|
||||
} else {
|
||||
const parsed2 = payload.timeout ? parseTimeString(payload.timeout) : null;
|
||||
if (payload.timeout && parsed2 === null) {
|
||||
log.warning(`Invalid timeout format "${payload.timeout}", using default 1h`);
|
||||
log.warning(`invalid timeout format "${payload.timeout}", using default 1h`);
|
||||
}
|
||||
const timeoutMs = parsed2 ?? 36e5;
|
||||
const actualTimeout = parsed2 !== null ? payload.timeout : "1h";
|
||||
@@ -162782,7 +162995,7 @@ async function main() {
|
||||
timeoutPromise.catch(() => {
|
||||
});
|
||||
try {
|
||||
result = await Promise.race([agentPromise, timeoutPromise]);
|
||||
result = await Promise.race([agentPromise, timeoutPromise, activityTimeout.promise]);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
@@ -162790,6 +163003,9 @@ async function main() {
|
||||
if (toolState.lastProgressBody) {
|
||||
await writeSummary(toolState.lastProgressBody);
|
||||
}
|
||||
if (toolState.output) {
|
||||
log.info(`::pullfrog-output::${Buffer.from(toolState.output).toString("base64")}`);
|
||||
}
|
||||
return {
|
||||
...handleAgentResult(result),
|
||||
result: toolState.output
|
||||
@@ -162801,7 +163017,8 @@ async function main() {
|
||||
_promise2 && await _promise2;
|
||||
}
|
||||
} catch (error50) {
|
||||
const errorMessage = error50 instanceof Error ? error50.message : "Unknown error occurred";
|
||||
const errorMessage = error50 instanceof Error ? error50.message : "unknown error occurred";
|
||||
killTrackedChildren();
|
||||
log.error(errorMessage);
|
||||
try {
|
||||
await reportErrorToComment({ toolState, error: errorMessage });
|
||||
@@ -162811,6 +163028,10 @@ async function main() {
|
||||
success: false,
|
||||
error: errorMessage
|
||||
};
|
||||
} finally {
|
||||
if (activityTimeout) {
|
||||
activityTimeout.stop();
|
||||
}
|
||||
}
|
||||
} catch (_2) {
|
||||
var _error2 = _2, _hasError2 = true;
|
||||
|
||||
Reference in New Issue
Block a user