Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 823fa3a39b | |||
| caa3cf4d4b | |||
| 8e53ce4e6b | |||
| 95c1a5757e | |||
| ee100354da | |||
| 70f1c47a28 | |||
| 4ee1ae89a5 | |||
| 185ca7a832 | |||
| 4ecff49b72 | |||
| df3ec6b815 | |||
| 4a9d83b102 | |||
| 57537d1a95 | |||
| 9948c08e7d | |||
| 3bf2f8596f | |||
| 510f2c96f9 | |||
| df13253d48 |
@@ -14,7 +14,7 @@ jobs:
|
|||||||
node-version: "24"
|
node-version: "24"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- run: pnpm install --frozen-lockfile --ignore-scripts
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm typecheck
|
- run: pnpm typecheck
|
||||||
- run: pnpm test
|
- run: pnpm test
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
node-version: "24"
|
node-version: "24"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- run: pnpm install --frozen-lockfile --ignore-scripts
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm runtest ${{ matrix.test }} ${{ matrix.agent }}
|
- run: pnpm runtest ${{ matrix.test }} ${{ matrix.agent }}
|
||||||
|
|
||||||
agnostic:
|
agnostic:
|
||||||
@@ -87,5 +87,5 @@ jobs:
|
|||||||
node-version: "24"
|
node-version: "24"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
|
|
||||||
- run: pnpm install --frozen-lockfile --ignore-scripts
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm runtest ${{ matrix.test }}
|
- run: pnpm runtest ${{ matrix.test }}
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,8 @@
|
|||||||
# sync action lockfile when action/package.json changes
|
# sync action lockfile when action/package.json changes
|
||||||
if git diff --cached --name-only | grep -q "^action/package.json$"; then
|
if git diff --cached --name-only | grep -q "^action/package.json$"; then
|
||||||
echo "🔒 syncing action/pnpm-lock.yaml..."
|
echo "🔒 syncing action/pnpm-lock.yaml..."
|
||||||
pnpm --ignore-workspace -C action install --no-frozen-lockfile
|
# note: pnpm -C action install will *not* treat "action" as a monorepo root if run from repo root;
|
||||||
|
# to install with action/ as the workspace root (and search upwards), cd into action first:
|
||||||
|
(cd action && pnpm install --no-frozen-lockfile)
|
||||||
git add action/pnpm-lock.yaml
|
git add action/pnpm-lock.yaml
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 pullfrog
|
Copyright (c) 2026 Pullfrog, Inc.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
+19
-11
@@ -63,7 +63,7 @@ function writeMcpConfig(ctx: AgentRunContext): string {
|
|||||||
};
|
};
|
||||||
|
|
||||||
writeFileSync(configPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
|
writeFileSync(configPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
|
||||||
log.info(`» MCP config written to ${configPath}`);
|
log.debug(`» MCP config written to ${configPath}`);
|
||||||
return configPath;
|
return configPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ export const claude = agent({
|
|||||||
// build disallowedTools based on tool permissions
|
// build disallowedTools based on tool permissions
|
||||||
const disallowedTools = buildDisallowedTools(ctx);
|
const disallowedTools = buildDisallowedTools(ctx);
|
||||||
if (disallowedTools.length > 0) {
|
if (disallowedTools.length > 0) {
|
||||||
log.info(`» disallowed tools: ${disallowedTools.join(", ")}`);
|
log.debug(`» disallowed built-ins: ${JSON.stringify(disallowedTools)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// write MCP config file
|
// write MCP config file
|
||||||
@@ -173,8 +173,7 @@ export const claude = agent({
|
|||||||
onStderr: (chunk) => {
|
onStderr: (chunk) => {
|
||||||
const trimmed = chunk.trim();
|
const trimmed = chunk.trim();
|
||||||
if (trimmed) {
|
if (trimmed) {
|
||||||
log.debug(`[claude stderr] ${trimmed}`);
|
log.info(`[claude stderr] ${trimmed}`);
|
||||||
log.warning(trimmed);
|
|
||||||
finalOutput += trimmed + "\n";
|
finalOutput += trimmed + "\n";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -239,10 +238,13 @@ const messageHandlers: SDKMessageHandlers = {
|
|||||||
user: (data, bashToolIds, thinkingTimer) => {
|
user: (data, bashToolIds, thinkingTimer) => {
|
||||||
if (data.message?.content) {
|
if (data.message?.content) {
|
||||||
for (const content of data.message.content) {
|
for (const content of data.message.content) {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (content.type === "tool_result") {
|
if (content.type === "tool_result") {
|
||||||
thinkingTimer.markToolResult();
|
thinkingTimer.markToolResult();
|
||||||
|
|
||||||
const toolUseId = (content as any).tool_use_id;
|
const toolUseId = content.tool_use_id;
|
||||||
const isBashTool = toolUseId && bashToolIds.has(toolUseId);
|
const isBashTool = toolUseId && bashToolIds.has(toolUseId);
|
||||||
|
|
||||||
const outputContent =
|
const outputContent =
|
||||||
@@ -250,7 +252,13 @@ const messageHandlers: SDKMessageHandlers = {
|
|||||||
? content.content
|
? content.content
|
||||||
: Array.isArray(content.content)
|
: Array.isArray(content.content)
|
||||||
? content.content
|
? content.content
|
||||||
.map((c: any) => (typeof c === "string" ? c : c.text || JSON.stringify(c)))
|
.map((entry: unknown) =>
|
||||||
|
typeof entry === "string"
|
||||||
|
? entry
|
||||||
|
: typeof entry === "object" && entry !== null && "text" in entry
|
||||||
|
? String(entry.text)
|
||||||
|
: JSON.stringify(entry)
|
||||||
|
)
|
||||||
.join("\n")
|
.join("\n")
|
||||||
: String(content.content);
|
: String(content.content);
|
||||||
|
|
||||||
@@ -258,7 +266,7 @@ const messageHandlers: SDKMessageHandlers = {
|
|||||||
// Log bash output in a collapsed group
|
// Log bash output in a collapsed group
|
||||||
log.startGroup(`bash output`);
|
log.startGroup(`bash output`);
|
||||||
if (content.is_error) {
|
if (content.is_error) {
|
||||||
log.warning(outputContent);
|
log.info(outputContent);
|
||||||
} else {
|
} else {
|
||||||
log.info(outputContent);
|
log.info(outputContent);
|
||||||
}
|
}
|
||||||
@@ -266,7 +274,7 @@ const messageHandlers: SDKMessageHandlers = {
|
|||||||
// Clean up the tracked ID
|
// Clean up the tracked ID
|
||||||
bashToolIds.delete(toolUseId);
|
bashToolIds.delete(toolUseId);
|
||||||
} else if (content.is_error) {
|
} else if (content.is_error) {
|
||||||
log.warning(`Tool error: ${outputContent}`);
|
log.info(`Tool error: ${outputContent}`);
|
||||||
} else {
|
} else {
|
||||||
// log successful non-bash tool result at debug level
|
// log successful non-bash tool result at debug level
|
||||||
log.debug(`tool output: ${outputContent}`);
|
log.debug(`tool output: ${outputContent}`);
|
||||||
@@ -301,11 +309,11 @@ const messageHandlers: SDKMessageHandlers = {
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
} else if (data.subtype === "error_max_turns") {
|
} else if (data.subtype === "error_max_turns") {
|
||||||
log.error(`Max turns reached: ${JSON.stringify(data)}`);
|
log.info(`Max turns reached: ${JSON.stringify(data)}`);
|
||||||
} else if (data.subtype === "error_during_execution") {
|
} else if (data.subtype === "error_during_execution") {
|
||||||
log.error(`Execution error: ${JSON.stringify(data)}`);
|
log.info(`Execution error: ${JSON.stringify(data)}`);
|
||||||
} else {
|
} else {
|
||||||
log.error(`Failed: ${JSON.stringify(data)}`);
|
log.info(`Failed: ${JSON.stringify(data)}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
system: () => {},
|
system: () => {},
|
||||||
|
|||||||
+7
-8
@@ -43,7 +43,7 @@ async function isModelAvailable(ctx: { apiKey: string; model: string }): Promise
|
|||||||
signal: AbortSignal.timeout(10_000),
|
signal: AbortSignal.timeout(10_000),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
log.warning(
|
log.info(
|
||||||
`failed to list models (HTTP ${response.status}), falling back to ${FALLBACK_MODEL}`
|
`failed to list models (HTTP ${response.status}), falling back to ${FALLBACK_MODEL}`
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
@@ -51,7 +51,7 @@ async function isModelAvailable(ctx: { apiKey: string; model: string }): Promise
|
|||||||
const body = (await response.json()) as { data: Array<{ id: string }> };
|
const body = (await response.json()) as { data: Array<{ id: string }> };
|
||||||
return body.data.some((m) => m.id === ctx.model);
|
return body.data.some((m) => m.id === ctx.model);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.warning(`failed to list models: ${err}, falling back to ${FALLBACK_MODEL}`);
|
log.info(`failed to list models: ${err}, falling back to ${FALLBACK_MODEL}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,8 +262,7 @@ export const codex = agent({
|
|||||||
onStderr: (chunk) => {
|
onStderr: (chunk) => {
|
||||||
const trimmed = chunk.trim();
|
const trimmed = chunk.trim();
|
||||||
if (trimmed) {
|
if (trimmed) {
|
||||||
log.debug(`[codex stderr] ${trimmed}`);
|
log.info(`[codex stderr] ${trimmed}`);
|
||||||
log.warning(trimmed);
|
|
||||||
finalOutput += trimmed + "\n";
|
finalOutput += trimmed + "\n";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -319,7 +318,7 @@ const messageHandlers: {
|
|||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
"turn.failed": (event) => {
|
"turn.failed": (event) => {
|
||||||
log.error(`Turn failed: ${event.error.message}`);
|
log.info(`Turn failed: ${event.error.message}`);
|
||||||
},
|
},
|
||||||
"item.started": (event, commandExecutionIds, thinkingTimer) => {
|
"item.started": (event, commandExecutionIds, thinkingTimer) => {
|
||||||
const item = event.item;
|
const item = event.item;
|
||||||
@@ -362,7 +361,7 @@ const messageHandlers: {
|
|||||||
thinkingTimer.markToolResult();
|
thinkingTimer.markToolResult();
|
||||||
log.startGroup(`bash output`);
|
log.startGroup(`bash output`);
|
||||||
if (item.status === "failed" || (item.exit_code !== undefined && item.exit_code !== 0)) {
|
if (item.status === "failed" || (item.exit_code !== undefined && item.exit_code !== 0)) {
|
||||||
log.warning(item.aggregated_output || "Command failed");
|
log.info(item.aggregated_output || "Command failed");
|
||||||
} else {
|
} else {
|
||||||
log.info(item.aggregated_output || "");
|
log.info(item.aggregated_output || "");
|
||||||
}
|
}
|
||||||
@@ -372,7 +371,7 @@ const messageHandlers: {
|
|||||||
} else if (item.type === "mcp_tool_call") {
|
} else if (item.type === "mcp_tool_call") {
|
||||||
thinkingTimer.markToolResult();
|
thinkingTimer.markToolResult();
|
||||||
if (item.status === "failed" && item.error) {
|
if (item.status === "failed" && item.error) {
|
||||||
log.warning(`MCP tool call failed: ${item.error.message}`);
|
log.info(`MCP tool call failed: ${item.error.message}`);
|
||||||
} else if ((item as any).output) {
|
} else if ((item as any).output) {
|
||||||
// log successful MCP tool call output so it appears in captured output
|
// log successful MCP tool call output so it appears in captured output
|
||||||
const output = (item as any).output;
|
const output = (item as any).output;
|
||||||
@@ -388,6 +387,6 @@ const messageHandlers: {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (event) => {
|
error: (event) => {
|
||||||
log.error(`Error: ${event.message}`);
|
log.info(`Error: ${event.message}`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+4
-3
@@ -210,7 +210,7 @@ export const cursor = agent({
|
|||||||
const result = event.tool_call?.mcpToolCall?.result?.success;
|
const result = event.tool_call?.mcpToolCall?.result?.success;
|
||||||
const isError = result?.isError;
|
const isError = result?.isError;
|
||||||
if (isError) {
|
if (isError) {
|
||||||
log.warning("Tool call failed");
|
log.info("Tool call failed");
|
||||||
} else {
|
} else {
|
||||||
// log successful tool result so it appears in output
|
// log successful tool result so it appears in output
|
||||||
// handle both formats: { text: string } or { text: { text: string } }
|
// handle both formats: { text: string } or { text: { text: string } }
|
||||||
@@ -320,12 +320,12 @@ export const cursor = agent({
|
|||||||
const text = data.toString();
|
const text = data.toString();
|
||||||
stderr += text;
|
stderr += text;
|
||||||
process.stderr.write(text);
|
process.stderr.write(text);
|
||||||
log.warning(text);
|
log.info(text);
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("close", async (code, signal) => {
|
child.on("close", async (code, signal) => {
|
||||||
if (signal) {
|
if (signal) {
|
||||||
log.warning(`Cursor CLI terminated by signal: ${signal}`);
|
log.info(`Cursor CLI terminated by signal: ${signal}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const duration = ((performance.now() - startTime) / 1000).toFixed(1);
|
const duration = ((performance.now() - startTime) / 1000).toFixed(1);
|
||||||
@@ -440,5 +440,6 @@ function configureCursorTools(ctx: AgentRunContext): void {
|
|||||||
|
|
||||||
writeFileSync(cliConfigPath, JSON.stringify(config, null, 2), "utf-8");
|
writeFileSync(cliConfigPath, JSON.stringify(config, null, 2), "utf-8");
|
||||||
log.info(`» CLI config written to ${cliConfigPath}`);
|
log.info(`» CLI config written to ${cliConfigPath}`);
|
||||||
|
log.debug(`» disallowed built-ins: ${JSON.stringify(deny)}`);
|
||||||
log.debug(`» CLI config contents: ${JSON.stringify(config, null, 2)}`);
|
log.debug(`» CLI config contents: ${JSON.stringify(config, null, 2)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-6
@@ -149,7 +149,7 @@ const messageHandlers = {
|
|||||||
if (event.status === "error") {
|
if (event.status === "error") {
|
||||||
const errorMsg =
|
const errorMsg =
|
||||||
typeof event.output === "string" ? event.output : JSON.stringify(event.output);
|
typeof event.output === "string" ? event.output : JSON.stringify(event.output);
|
||||||
log.warning(`Tool call failed: ${errorMsg}`);
|
log.info(`Tool call failed: ${errorMsg}`);
|
||||||
} else if (event.output) {
|
} else if (event.output) {
|
||||||
// log successful tool result so it appears in output
|
// log successful tool result so it appears in output
|
||||||
const outputStr =
|
const outputStr =
|
||||||
@@ -270,8 +270,7 @@ export const gemini = agent({
|
|||||||
onStderr: (chunk) => {
|
onStderr: (chunk) => {
|
||||||
const trimmed = chunk.trim();
|
const trimmed = chunk.trim();
|
||||||
if (trimmed) {
|
if (trimmed) {
|
||||||
log.debug(`[gemini stderr] ${trimmed}`);
|
log.info(`[gemini stderr] ${trimmed}`);
|
||||||
log.warning(trimmed);
|
|
||||||
finalOutput += trimmed + "\n";
|
finalOutput += trimmed + "\n";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -286,7 +285,7 @@ export const gemini = agent({
|
|||||||
|
|
||||||
// retry on transient API errors (500, 503, INTERNAL, etc.)
|
// retry on transient API errors (500, 503, INTERNAL, etc.)
|
||||||
if (attempt < MAX_ATTEMPTS && isTransientApiError(errorMessage)) {
|
if (attempt < MAX_ATTEMPTS && isTransientApiError(errorMessage)) {
|
||||||
log.warning(
|
log.info(
|
||||||
`» transient Gemini API error on attempt ${attempt}/${MAX_ATTEMPTS}, retrying in ${RETRY_DELAY_MS / 1000}s...`
|
`» transient Gemini API error on attempt ${attempt}/${MAX_ATTEMPTS}, retrying in ${RETRY_DELAY_MS / 1000}s...`
|
||||||
);
|
);
|
||||||
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
||||||
@@ -313,7 +312,7 @@ export const gemini = agent({
|
|||||||
|
|
||||||
// retry on transient API errors from spawn exceptions too
|
// retry on transient API errors from spawn exceptions too
|
||||||
if (attempt < MAX_ATTEMPTS && isTransientApiError(errorMessage)) {
|
if (attempt < MAX_ATTEMPTS && isTransientApiError(errorMessage)) {
|
||||||
log.warning(
|
log.info(
|
||||||
`» transient Gemini API error on attempt ${attempt}/${MAX_ATTEMPTS}, retrying in ${RETRY_DELAY_MS / 1000}s...`
|
`» transient Gemini API error on attempt ${attempt}/${MAX_ATTEMPTS}, retrying in ${RETRY_DELAY_MS / 1000}s...`
|
||||||
);
|
);
|
||||||
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
||||||
@@ -413,7 +412,7 @@ function configureGeminiSettings(ctx: AgentRunContext): string {
|
|||||||
writeFileSync(settingsPath, JSON.stringify(newSettings, null, 2), "utf-8");
|
writeFileSync(settingsPath, JSON.stringify(newSettings, null, 2), "utf-8");
|
||||||
log.info(`» Gemini settings written to ${settingsPath}`);
|
log.info(`» Gemini settings written to ${settingsPath}`);
|
||||||
if (exclude.length > 0) {
|
if (exclude.length > 0) {
|
||||||
log.info(`» excluded tools: ${exclude.join(", ")}`);
|
log.debug(`» disallowed built-ins: ${JSON.stringify(exclude)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
|
|||||||
+11
-13
@@ -154,7 +154,7 @@ export const opencode = agent({
|
|||||||
activeToolCalls > 0
|
activeToolCalls > 0
|
||||||
? ` (waiting for ${activeToolCalls} tool call${activeToolCalls > 1 ? "s" : ""})`
|
? ` (waiting for ${activeToolCalls} tool call${activeToolCalls > 1 ? "s" : ""})`
|
||||||
: " (OpenCode may be processing internally - LLM calls, planning, etc.)";
|
: " (OpenCode may be processing internally - LLM calls, planning, etc.)";
|
||||||
log.warning(
|
log.info(
|
||||||
`» no activity for ${(timeSinceLastActivity / 1000).toFixed(1)}s${toolCallInfo} (${eventCount} events processed so far)`
|
`» no activity for ${(timeSinceLastActivity / 1000).toFixed(1)}s${toolCallInfo} (${eventCount} events processed so far)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -186,7 +186,7 @@ export const opencode = agent({
|
|||||||
const providerError = detectProviderError(trimmed);
|
const providerError = detectProviderError(trimmed);
|
||||||
if (providerError) {
|
if (providerError) {
|
||||||
lastProviderError = providerError;
|
lastProviderError = providerError;
|
||||||
log.error(`» provider error detected (${providerError}): ${trimmed.substring(0, 500)}`);
|
log.info(`» provider error detected (${providerError}): ${trimmed.substring(0, 500)}`);
|
||||||
} else {
|
} else {
|
||||||
// OpenCode's --print-logs output goes to stderr. demote internal
|
// OpenCode's --print-logs output goes to stderr. demote internal
|
||||||
// INFO/DEBUG bus traffic to debug so it doesn't drown out tool
|
// INFO/DEBUG bus traffic to debug so it doesn't drown out tool
|
||||||
@@ -207,9 +207,9 @@ export const opencode = agent({
|
|||||||
const diagnosis = lastProviderError
|
const diagnosis = lastProviderError
|
||||||
? `provider error: ${lastProviderError}`
|
? `provider error: ${lastProviderError}`
|
||||||
: "unknown cause (no stdout events received)";
|
: "unknown cause (no stdout events received)";
|
||||||
log.error(`» OpenCode produced 0 events (${diagnosis})`);
|
log.info(`» OpenCode produced 0 events (${diagnosis})`);
|
||||||
if (stderrContext) {
|
if (stderrContext) {
|
||||||
log.error(`» last stderr output:\n${stderrContext}`);
|
log.info(`» last stderr output:\n${stderrContext}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,12 +263,12 @@ export const opencode = agent({
|
|||||||
? "OpenCode produced 0 stdout events - check if the model provider is reachable"
|
? "OpenCode produced 0 stdout events - check if the model provider is reachable"
|
||||||
: `${eventCount} events were processed before the hang`;
|
: `${eventCount} events were processed before the hang`;
|
||||||
|
|
||||||
log.error(
|
log.info(
|
||||||
`» OpenCode ${isActivityTimeout ? "hung" : "failed"} after ${(duration / 1000).toFixed(1)}s: ${errorMessage}`
|
`» OpenCode ${isActivityTimeout ? "hung" : "failed"} after ${(duration / 1000).toFixed(1)}s: ${errorMessage}`
|
||||||
);
|
);
|
||||||
log.error(`» diagnosis: ${diagnosis}`);
|
log.info(`» diagnosis: ${diagnosis}`);
|
||||||
if (stderrContext) {
|
if (stderrContext) {
|
||||||
log.error(
|
log.info(
|
||||||
`» recent stderr (last ${Math.min(recentStderr.length, 10)} lines):\n${stderrContext}`
|
`» recent stderr (last ${Math.min(recentStderr.length, 10)} lines):\n${stderrContext}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -324,9 +324,7 @@ function configureOpenCode(ctx: AgentRunContext): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.info(`» OpenCode config written to ${configPath}`);
|
log.info(`» OpenCode config written to ${configPath}`);
|
||||||
log.debug(
|
log.debug(`» disallowed built-ins: ${JSON.stringify(permission)}`);
|
||||||
`» OpenCode permissions: edit=${permission.edit}, bash=${permission.bash}, webfetch=${permission.webfetch}`
|
|
||||||
);
|
|
||||||
log.debug(`OpenCode config contents:\n${configJson}`);
|
log.debug(`OpenCode config contents:\n${configJson}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,7 +600,7 @@ const messageHandlers = {
|
|||||||
log.debug(` output: ${typeof output === "string" ? output : JSON.stringify(output)}`);
|
log.debug(` output: ${typeof output === "string" ? output : JSON.stringify(output)}`);
|
||||||
}
|
}
|
||||||
if (toolDuration > 5000) {
|
if (toolDuration > 5000) {
|
||||||
log.warning(
|
log.info(
|
||||||
`» ⚠️ tool call took ${(toolDuration / 1000).toFixed(1)}s - this may indicate network latency or slow processing`
|
`» ⚠️ tool call took ${(toolDuration / 1000).toFixed(1)}s - this may indicate network latency or slow processing`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -610,7 +608,7 @@ const messageHandlers = {
|
|||||||
}
|
}
|
||||||
if (status === "error") {
|
if (status === "error") {
|
||||||
const errorMsg = typeof output === "string" ? output : JSON.stringify(output);
|
const errorMsg = typeof output === "string" ? output : JSON.stringify(output);
|
||||||
log.error(`» ❌ tool call failed: ${errorMsg}`);
|
log.info(`» ❌ tool call failed: ${errorMsg}`);
|
||||||
} else if (output) {
|
} else if (output) {
|
||||||
// log successful tool result so it appears in captured output
|
// log successful tool result so it appears in captured output
|
||||||
const outputStr = typeof output === "string" ? output : JSON.stringify(output);
|
const outputStr = typeof output === "string" ? output : JSON.stringify(output);
|
||||||
@@ -626,7 +624,7 @@ const messageHandlers = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (event.status === "error") {
|
if (event.status === "error") {
|
||||||
log.error(`» OpenCode CLI failed: ${JSON.stringify(event)}`);
|
log.info(`» OpenCode CLI failed: ${JSON.stringify(event)}`);
|
||||||
} else {
|
} else {
|
||||||
// log tokens once at the end (use stats from result if available, otherwise use accumulated from step_finish)
|
// log tokens once at the end (use stats from result if available, otherwise use accumulated from step_finish)
|
||||||
const inputTokens = event.stats?.input_tokens || accumulatedTokens.input || 0;
|
const inputTokens = event.stats?.input_tokens || accumulatedTokens.input || 0;
|
||||||
|
|||||||
@@ -37,18 +37,6 @@ export const agent = <const input extends AgentInput>(input: input): defineAgent
|
|||||||
log.info(`» bash: ${ctx.payload.bash}`);
|
log.info(`» bash: ${ctx.payload.bash}`);
|
||||||
log.debug(`» payload: ${JSON.stringify(ctx.payload, null, 2)}`);
|
log.debug(`» payload: ${JSON.stringify(ctx.payload, null, 2)}`);
|
||||||
|
|
||||||
// build log box content: eventInstructions (if any) + user request (if any) + event data
|
|
||||||
const logParts = [
|
|
||||||
ctx.instructions.eventInstructions
|
|
||||||
? `EVENT-LEVEL INSTRUCTIONS:\n${ctx.instructions.eventInstructions}`
|
|
||||||
: null,
|
|
||||||
ctx.instructions.user ? `USER REQUEST:\n${ctx.instructions.user}` : null,
|
|
||||||
ctx.instructions.event,
|
|
||||||
].filter(Boolean);
|
|
||||||
log.box(logParts.join("\n\n---\n\n"), {
|
|
||||||
title: "Instructions",
|
|
||||||
});
|
|
||||||
|
|
||||||
return input.run(ctx);
|
return input.run(ctx);
|
||||||
},
|
},
|
||||||
...agentsManifest[input.name],
|
...agentsManifest[input.name],
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { main } from "./main.ts";
|
import { main } from "./main.ts";
|
||||||
import { runCleanup } from "./utils/exitHandler.ts";
|
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -22,8 +21,6 @@ async function run(): Promise<void> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||||
core.setFailed(`Action failed: ${errorMessage}`);
|
core.setFailed(`Action failed: ${errorMessage}`);
|
||||||
} finally {
|
|
||||||
await runCleanup();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
@@ -61,6 +61,28 @@ export type ToolPermission = "disabled" | "enabled";
|
|||||||
export type BashPermission = "disabled" | "restricted" | "enabled";
|
export type BashPermission = "disabled" | "restricted" | "enabled";
|
||||||
export type PushPermission = "disabled" | "restricted" | "enabled";
|
export type PushPermission = "disabled" | "restricted" | "enabled";
|
||||||
|
|
||||||
|
// workflow yml permissions for GITHUB_TOKEN
|
||||||
|
export type WorkflowPermissionValue = "read" | "write" | "none";
|
||||||
|
export type WorkflowIdTokenPermissionValue = "write" | "none";
|
||||||
|
|
||||||
|
export interface WorkflowPermissions {
|
||||||
|
actions?: WorkflowPermissionValue;
|
||||||
|
attestations?: WorkflowPermissionValue;
|
||||||
|
checks?: WorkflowPermissionValue;
|
||||||
|
contents?: WorkflowPermissionValue;
|
||||||
|
deployments?: WorkflowPermissionValue;
|
||||||
|
discussions?: WorkflowPermissionValue;
|
||||||
|
"id-token"?: WorkflowIdTokenPermissionValue;
|
||||||
|
issues?: WorkflowPermissionValue;
|
||||||
|
models?: WorkflowPermissionValue;
|
||||||
|
packages?: WorkflowPermissionValue;
|
||||||
|
pages?: WorkflowPermissionValue;
|
||||||
|
"pull-requests"?: WorkflowPermissionValue;
|
||||||
|
"repository-projects"?: WorkflowPermissionValue;
|
||||||
|
"security-events"?: WorkflowPermissionValue;
|
||||||
|
statuses?: WorkflowPermissionValue;
|
||||||
|
}
|
||||||
|
|
||||||
// permission level for the author who triggered the event
|
// permission level for the author who triggered the event
|
||||||
// matches GitHub's permission levels: admin > write > maintain > triage > read > none
|
// matches GitHub's permission levels: admin > write > maintain > triage > read > none
|
||||||
export type AuthorPermission = "admin" | "maintain" | "write" | "triage" | "read" | "none";
|
export type AuthorPermission = "admin" | "maintain" | "write" | "triage" | "read" | "none";
|
||||||
@@ -250,6 +272,8 @@ export interface WriteablePayload {
|
|||||||
agent?: AgentName | undefined;
|
agent?: AgentName | undefined;
|
||||||
/** the user's actual request (body if @pullfrog tagged) */
|
/** the user's actual request (body if @pullfrog tagged) */
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
/** github username of the human who triggered this workflow run */
|
||||||
|
triggeringUser?: string | undefined;
|
||||||
/** event-level instructions for this trigger type (flag-expanded server-side) */
|
/** event-level instructions for this trigger type (flag-expanded server-side) */
|
||||||
eventInstructions?: string | undefined;
|
eventInstructions?: string | undefined;
|
||||||
/** repo-level instructions (flag-expanded server-side) */
|
/** repo-level instructions (flag-expanded server-side) */
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# `pullfrog/get-installation-token`
|
||||||
|
|
||||||
|
Get a GitHub App installation token in a workflow job. This convenience action makes it easier to integrate Pullfrog into existing CI workflows.
|
||||||
|
|
||||||
|
This action:
|
||||||
|
|
||||||
|
- Provides a GitHub App installation token for later workflow steps.
|
||||||
|
- Works for the current repository out of the box.
|
||||||
|
- Can optionally include additional repositories.
|
||||||
|
- Masks the token in logs.
|
||||||
|
- Revokes the token automatically in the post step.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Workflow or job permissions must include `id-token: write`.
|
||||||
|
- The Pullfrog GitHub App must be installed on the target repositories.
|
||||||
|
- If you pass `repos`, each repository must be installed for the same app installation.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
| Name | Required | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `repos` | no | Comma-separated additional repo names to include, for example: `repo1,repo2`. The current repo is always included. |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `token` | GitHub App installation token |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic (current repo only)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
example:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get installation token
|
||||||
|
id: token
|
||||||
|
uses: ./action/get-installation-token
|
||||||
|
|
||||||
|
- name: Call GitHub API with token
|
||||||
|
run: gh api repos/${{ github.repository }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.token.outputs.token }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Include extra repositories
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
example:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Get token for current repo plus extra repos
|
||||||
|
id: token
|
||||||
|
uses: ./action/get-installation-token
|
||||||
|
with:
|
||||||
|
repos: pullfrog,app
|
||||||
|
|
||||||
|
- name: Checkout another repo with installation token
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: pullfrog/pullfrog
|
||||||
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
path: action-repo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `repos` expects repository names, not `owner/repo`.
|
||||||
|
- Token lifetime is managed by GitHub, but this action also revokes the token during post-run cleanup.
|
||||||
|
- Prefer step output usage (`${{ steps.<id>.outputs.token }}`) rather than writing tokens to files.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- `Error: id-token permission is required`:
|
||||||
|
Add `id-token: write` in workflow or job permissions.
|
||||||
|
- Token works for current repo but not an extra repo:
|
||||||
|
Ensure that repository is listed in `repos` and the app installation has access to it.
|
||||||
@@ -399,7 +399,7 @@ var require_tunnel = __commonJS({
|
|||||||
connectOptions.headers = connectOptions.headers || {};
|
connectOptions.headers = connectOptions.headers || {};
|
||||||
connectOptions.headers["Proxy-Authorization"] = "Basic " + new Buffer(connectOptions.proxyAuth).toString("base64");
|
connectOptions.headers["Proxy-Authorization"] = "Basic " + new Buffer(connectOptions.proxyAuth).toString("base64");
|
||||||
}
|
}
|
||||||
debug2("making CONNECT request");
|
debug3("making CONNECT request");
|
||||||
var connectReq = self2.request(connectOptions);
|
var connectReq = self2.request(connectOptions);
|
||||||
connectReq.useChunkedEncodingByDefault = false;
|
connectReq.useChunkedEncodingByDefault = false;
|
||||||
connectReq.once("response", onResponse);
|
connectReq.once("response", onResponse);
|
||||||
@@ -419,7 +419,7 @@ var require_tunnel = __commonJS({
|
|||||||
connectReq.removeAllListeners();
|
connectReq.removeAllListeners();
|
||||||
socket.removeAllListeners();
|
socket.removeAllListeners();
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
debug2(
|
debug3(
|
||||||
"tunneling socket could not be established, statusCode=%d",
|
"tunneling socket could not be established, statusCode=%d",
|
||||||
res.statusCode
|
res.statusCode
|
||||||
);
|
);
|
||||||
@@ -431,7 +431,7 @@ var require_tunnel = __commonJS({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (head.length > 0) {
|
if (head.length > 0) {
|
||||||
debug2("got illegal response body from proxy");
|
debug3("got illegal response body from proxy");
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
var error2 = new Error("got illegal response body from proxy");
|
var error2 = new Error("got illegal response body from proxy");
|
||||||
error2.code = "ECONNRESET";
|
error2.code = "ECONNRESET";
|
||||||
@@ -439,13 +439,13 @@ var require_tunnel = __commonJS({
|
|||||||
self2.removeSocket(placeholder);
|
self2.removeSocket(placeholder);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debug2("tunneling connection has established");
|
debug3("tunneling connection has established");
|
||||||
self2.sockets[self2.sockets.indexOf(placeholder)] = socket;
|
self2.sockets[self2.sockets.indexOf(placeholder)] = socket;
|
||||||
return cb(socket);
|
return cb(socket);
|
||||||
}
|
}
|
||||||
function onError(cause) {
|
function onError(cause) {
|
||||||
connectReq.removeAllListeners();
|
connectReq.removeAllListeners();
|
||||||
debug2(
|
debug3(
|
||||||
"tunneling socket could not be established, cause=%s\n",
|
"tunneling socket could not be established, cause=%s\n",
|
||||||
cause.message,
|
cause.message,
|
||||||
cause.stack
|
cause.stack
|
||||||
@@ -507,9 +507,9 @@ var require_tunnel = __commonJS({
|
|||||||
}
|
}
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
var debug2;
|
var debug3;
|
||||||
if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) {
|
if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) {
|
||||||
debug2 = function() {
|
debug3 = function() {
|
||||||
var args = Array.prototype.slice.call(arguments);
|
var args = Array.prototype.slice.call(arguments);
|
||||||
if (typeof args[0] === "string") {
|
if (typeof args[0] === "string") {
|
||||||
args[0] = "TUNNEL: " + args[0];
|
args[0] = "TUNNEL: " + args[0];
|
||||||
@@ -519,10 +519,10 @@ var require_tunnel = __commonJS({
|
|||||||
console.error.apply(console, args);
|
console.error.apply(console, args);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
debug2 = function() {
|
debug3 = function() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
exports.debug = debug2;
|
exports.debug = debug3;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -19733,10 +19733,10 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
|
|||||||
return process.env["RUNNER_DEBUG"] === "1";
|
return process.env["RUNNER_DEBUG"] === "1";
|
||||||
}
|
}
|
||||||
exports.isDebug = isDebug2;
|
exports.isDebug = isDebug2;
|
||||||
function debug2(message) {
|
function debug3(message) {
|
||||||
(0, command_1.issueCommand)("debug", {}, message);
|
(0, command_1.issueCommand)("debug", {}, message);
|
||||||
}
|
}
|
||||||
exports.debug = debug2;
|
exports.debug = debug3;
|
||||||
function error2(message, properties = {}) {
|
function error2(message, properties = {}) {
|
||||||
(0, command_1.issueCommand)("error", (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message);
|
(0, command_1.issueCommand)("error", (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message);
|
||||||
}
|
}
|
||||||
@@ -25515,7 +25515,12 @@ var isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
|||||||
var isInsideDocker = existsSync("/.dockerenv");
|
var isInsideDocker = existsSync("/.dockerenv");
|
||||||
|
|
||||||
// utils/log.ts
|
// utils/log.ts
|
||||||
var isDebugEnabled = () => process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true" || core.isDebug();
|
var isRunnerDebugEnabled = () => core.isDebug();
|
||||||
|
var isLocalDebugEnabled = () => process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true";
|
||||||
|
var isDebugEnabled = () => isLocalDebugEnabled() || isRunnerDebugEnabled();
|
||||||
|
function ts() {
|
||||||
|
return isDebugEnabled() ? `[${(/* @__PURE__ */ new Date()).toISOString()}] ` : "";
|
||||||
|
}
|
||||||
function formatArgs(args) {
|
function formatArgs(args) {
|
||||||
return args.map((arg) => {
|
return args.map((arg) => {
|
||||||
if (typeof arg === "string") return arg;
|
if (typeof arg === "string") return arg;
|
||||||
@@ -25626,19 +25631,16 @@ function separator(length = 50) {
|
|||||||
const separatorText = "\u2500".repeat(length);
|
const separatorText = "\u2500".repeat(length);
|
||||||
core.info(separatorText);
|
core.info(separatorText);
|
||||||
}
|
}
|
||||||
function ts() {
|
|
||||||
return isDebugEnabled() ? `[${(/* @__PURE__ */ new Date()).toISOString()}] ` : "";
|
|
||||||
}
|
|
||||||
var log = {
|
var log = {
|
||||||
/** Print info message */
|
/** Print info message */
|
||||||
info: (...args) => {
|
info: (...args) => {
|
||||||
core.info(`${ts()}${formatArgs(args)}`);
|
core.info(`${ts()}${formatArgs(args)}`);
|
||||||
},
|
},
|
||||||
/** Print warning message */
|
/** Print a warning message. Use only for warnings that should be displayed in the job summary. */
|
||||||
warning: (...args) => {
|
warning: (...args) => {
|
||||||
core.warning(`${ts()}${formatArgs(args)}`);
|
core.warning(`${ts()}${formatArgs(args)}`);
|
||||||
},
|
},
|
||||||
/** Print error message */
|
/** Print an error message. Use only for errors that should be displayed in the job summary. */
|
||||||
error: (...args) => {
|
error: (...args) => {
|
||||||
core.error(`${ts()}${formatArgs(args)}`);
|
core.error(`${ts()}${formatArgs(args)}`);
|
||||||
},
|
},
|
||||||
@@ -25646,10 +25648,14 @@ var log = {
|
|||||||
success: (...args) => {
|
success: (...args) => {
|
||||||
core.info(`${ts()}\xBB ${formatArgs(args)}`);
|
core.info(`${ts()}\xBB ${formatArgs(args)}`);
|
||||||
},
|
},
|
||||||
/** Print debug message (only if LOG_LEVEL=debug) */
|
/** Print debug message (only when debug mode is enabled) */
|
||||||
debug: (...args) => {
|
debug: (...args) => {
|
||||||
if (isDebugEnabled()) {
|
if (isRunnerDebugEnabled()) {
|
||||||
core.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] ${formatArgs(args)}`);
|
core.debug(formatArgs(args));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isLocalDebugEnabled()) {
|
||||||
|
core.info(`${ts()}[DEBUG] ${formatArgs(args)}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/** Print a formatted box with text */
|
/** Print a formatted box with text */
|
||||||
@@ -25740,9 +25746,7 @@ async function retry(fn, options = {}) {
|
|||||||
throw error2;
|
throw error2;
|
||||||
}
|
}
|
||||||
const delay = delayMs * attempt;
|
const delay = delayMs * attempt;
|
||||||
log.warning(
|
log.info(`\xBB ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`);
|
||||||
`\xBB ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`
|
|
||||||
);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25925,7 +25929,7 @@ async function revokeGitHubInstallationToken(token) {
|
|||||||
});
|
});
|
||||||
log.debug("\xBB installation token revoked");
|
log.debug("\xBB installation token revoked");
|
||||||
} catch (error2) {
|
} catch (error2) {
|
||||||
log.warning(
|
log.info(
|
||||||
`Failed to revoke installation token: ${error2 instanceof Error ? error2.message : String(error2)}`
|
`Failed to revoke installation token: ${error2 instanceof Error ? error2.message : String(error2)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Internal entrypoint for the root app.
|
||||||
|
* Re-exports shared types, values, and utilities needed by the Next.js app.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AgentApiKeyName,
|
||||||
|
AgentManifest,
|
||||||
|
AuthorPermission,
|
||||||
|
BashPermission,
|
||||||
|
Payload,
|
||||||
|
PayloadEvent,
|
||||||
|
PushPermission,
|
||||||
|
ToolPermission,
|
||||||
|
WriteablePayload,
|
||||||
|
} from "../external.ts";
|
||||||
|
export {
|
||||||
|
AgentName,
|
||||||
|
agentsManifest,
|
||||||
|
Effort,
|
||||||
|
ghPullfrogMcpName,
|
||||||
|
} from "../external.ts";
|
||||||
|
export type {
|
||||||
|
AgentInfo,
|
||||||
|
BuildPullfrogFooterParams,
|
||||||
|
WorkflowRunFooterInfo,
|
||||||
|
} from "../utils/buildPullfrogFooter.ts";
|
||||||
|
export {
|
||||||
|
buildPullfrogFooter,
|
||||||
|
PULLFROG_DIVIDER,
|
||||||
|
stripExistingFooter,
|
||||||
|
} from "../utils/buildPullfrogFooter.ts";
|
||||||
|
|
||||||
|
export {
|
||||||
|
isValidTimeString,
|
||||||
|
parseTimeString,
|
||||||
|
TIMEOUT_DISABLED,
|
||||||
|
} from "../utils/time.ts";
|
||||||
@@ -12,7 +12,6 @@ import { validateAgentApiKey } from "./utils/apiKeys.ts";
|
|||||||
import { resolveBody } from "./utils/body.ts";
|
import { resolveBody } from "./utils/body.ts";
|
||||||
import { log, writeSummary } from "./utils/cli.ts";
|
import { log, writeSummary } from "./utils/cli.ts";
|
||||||
import { reportErrorToComment } from "./utils/errorReport.ts";
|
import { reportErrorToComment } from "./utils/errorReport.ts";
|
||||||
import { setupExitHandler } from "./utils/exitHandler.ts";
|
|
||||||
import { resolveGit } from "./utils/gitAuth.ts";
|
import { resolveGit } from "./utils/gitAuth.ts";
|
||||||
import { createOctokit } from "./utils/github.ts";
|
import { createOctokit } from "./utils/github.ts";
|
||||||
import { resolveInstructions } from "./utils/instructions.ts";
|
import { resolveInstructions } from "./utils/instructions.ts";
|
||||||
@@ -52,8 +51,6 @@ export async function main(): Promise<MainResult> {
|
|||||||
typeof resolvedPromptInput !== "string" ? resolvedPromptInput.progressCommentId : undefined,
|
typeof resolvedPromptInput !== "string" ? resolvedPromptInput.progressCommentId : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
setupExitHandler(toolState);
|
|
||||||
|
|
||||||
// resolve and fingerprint git binary before any agent code runs
|
// resolve and fingerprint git binary before any agent code runs
|
||||||
resolveGit();
|
resolveGit();
|
||||||
|
|
||||||
@@ -167,6 +164,17 @@ export async function main(): Promise<MainResult> {
|
|||||||
repo: runContext.repo,
|
repo: runContext.repo,
|
||||||
modes,
|
modes,
|
||||||
});
|
});
|
||||||
|
// log instructions as soon as they are fully resolved
|
||||||
|
const logParts = [
|
||||||
|
instructions.eventInstructions
|
||||||
|
? `EVENT-LEVEL INSTRUCTIONS:\n${instructions.eventInstructions}`
|
||||||
|
: null,
|
||||||
|
instructions.user ? `USER REQUEST:\n${instructions.user}` : null,
|
||||||
|
instructions.event,
|
||||||
|
].filter(Boolean);
|
||||||
|
log.box(logParts.join("\n\n---\n\n"), {
|
||||||
|
title: "Instructions",
|
||||||
|
});
|
||||||
|
|
||||||
// run agent, optionally with timeout enforcement
|
// run agent, optionally with timeout enforcement
|
||||||
activityTimeout = createProcessOutputActivityTimeout({
|
activityTimeout = createProcessOutputActivityTimeout({
|
||||||
@@ -236,8 +244,6 @@ export async function main(): Promise<MainResult> {
|
|||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
if (activityTimeout) {
|
activityTimeout?.stop();
|
||||||
activityTimeout.stop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-206
@@ -1,206 +0,0 @@
|
|||||||
# gh_pullfrog MCP Tools
|
|
||||||
|
|
||||||
this directory contains the mcp (model context protocol) server tools for interacting with github.
|
|
||||||
|
|
||||||
## available tools
|
|
||||||
|
|
||||||
### check suite tools
|
|
||||||
|
|
||||||
#### `get_check_suite_logs`
|
|
||||||
get workflow run logs for a failed check suite with intelligent log analysis.
|
|
||||||
|
|
||||||
**parameters:**
|
|
||||||
- `check_suite_id` (number): the id from check_suite.id in the webhook payload
|
|
||||||
|
|
||||||
**replaces:** `gh run list` and `gh run view --log`
|
|
||||||
|
|
||||||
**returns:**
|
|
||||||
structured failure information for each failed job:
|
|
||||||
- `_instructions`: explains how to use each field
|
|
||||||
- `failed_jobs[]`: array of failed job results, each containing:
|
|
||||||
- `job_id`, `job_name`, `job_url`: job identification
|
|
||||||
- `failed_steps`: which CI steps failed (e.g., "Step 6: Run tests")
|
|
||||||
- `log_index`: array of interesting lines (errors, warnings, failures) with line numbers
|
|
||||||
- `excerpt`: ~80 line curated window around the last error
|
|
||||||
- `full_log_path`: path to complete log file for deeper investigation
|
|
||||||
|
|
||||||
**log_index types:**
|
|
||||||
- `error`: lines matching `##[error]`, `Error:`, `ERR_`, `exit code N`
|
|
||||||
- `warning`: lines matching `##[warning]`, `WARN`
|
|
||||||
- `failure`: lines matching `N failed`, `FAIL`, `✕`
|
|
||||||
- `trace`: stack trace lines (deduplicated)
|
|
||||||
|
|
||||||
**workflow for using results:**
|
|
||||||
1. scan `log_index` to see where errors/warnings/failures are located in the log
|
|
||||||
2. read `excerpt` for immediate context around the main error
|
|
||||||
3. if excerpt doesn't show what you need, read specific line ranges from `full_log_path`
|
|
||||||
4. check `failed_steps` and read the workflow yml to understand what command failed
|
|
||||||
|
|
||||||
**example:**
|
|
||||||
```typescript
|
|
||||||
// when handling a check_suite_completed webhook
|
|
||||||
const result = await mcp.call("gh_pullfrog/get_check_suite_logs", {
|
|
||||||
check_suite_id: check_suite.id
|
|
||||||
});
|
|
||||||
|
|
||||||
// result.failed_jobs[0].log_index shows:
|
|
||||||
// [
|
|
||||||
// { line: 181, content: "WARN Failed to create bin...", type: "warning" },
|
|
||||||
// { line: 1079, content: "Error: expect(received).toBe(expected)", type: "error" },
|
|
||||||
// ...
|
|
||||||
// ]
|
|
||||||
// use these line numbers to read specific sections from full_log_path
|
|
||||||
```
|
|
||||||
|
|
||||||
### review tools
|
|
||||||
|
|
||||||
#### `get_review_comments`
|
|
||||||
get all line-by-line comments for a specific pull request review, including full thread context for replies.
|
|
||||||
|
|
||||||
**parameters:**
|
|
||||||
- `pull_number` (number): the pull request number
|
|
||||||
- `review_id` (number): the id from review.id in the webhook payload
|
|
||||||
- `approved_by` (string, optional): only return comments this user gave a 👍 to
|
|
||||||
|
|
||||||
**replaces:** `gh api repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments`
|
|
||||||
|
|
||||||
**returns:**
|
|
||||||
- `commentsPath`: path to XML file with full comment details
|
|
||||||
- `reviewer`: github username of the review author
|
|
||||||
- `count`: number of comments to address
|
|
||||||
|
|
||||||
**output format (XML):**
|
|
||||||
```xml
|
|
||||||
<review_comments count="2" reviewer="colinmcd94">
|
|
||||||
|
|
||||||
<summary>
|
|
||||||
<comment id="67890" file="src/utils/auth.ts" line="42">Actually, can you use a type guard...</comment>
|
|
||||||
<comment id="67891" file="src/api/handler.ts" line="15">This should handle the error case</comment>
|
|
||||||
</summary>
|
|
||||||
|
|
||||||
<comment id="67890" file="src/utils/auth.ts" line="42" author="colinmcd94">
|
|
||||||
<thread>
|
|
||||||
<message id="12345" author="colinmcd94">Please add null checking here</message>
|
|
||||||
<message id="23456" author="octocat">What about using optional chaining?</message>
|
|
||||||
</thread>
|
|
||||||
<diff>
|
|
||||||
@@ -40,7 +40,7 @@
|
|
||||||
const user = getUser(id);
|
|
||||||
- return user.name;
|
|
||||||
+ return user?.name;
|
|
||||||
</diff>
|
|
||||||
<body>Actually, can you use a type guard instead?</body>
|
|
||||||
</comment>
|
|
||||||
|
|
||||||
</review_comments>
|
|
||||||
```
|
|
||||||
|
|
||||||
- `<summary>` lists all comments to address with truncated preview
|
|
||||||
- `<thread>` shows parent comments (when replying to existing thread)
|
|
||||||
- `<diff>` contains the diff hunk around the commented line
|
|
||||||
- `<body>` is the actual comment text to address
|
|
||||||
|
|
||||||
**example:**
|
|
||||||
```typescript
|
|
||||||
// when handling a pull_request_review_submitted webhook
|
|
||||||
await mcp.call("gh_pullfrog/get_review_comments", {
|
|
||||||
pull_number: 47,
|
|
||||||
review_id: review.id
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `list_pull_request_reviews`
|
|
||||||
list all reviews for a pull request.
|
|
||||||
|
|
||||||
**parameters:**
|
|
||||||
- `pull_number` (number): the pull request number
|
|
||||||
|
|
||||||
**replaces:** `gh api repos/{owner}/{repo}/pulls/{pull_number}/reviews`
|
|
||||||
|
|
||||||
**returns:**
|
|
||||||
array of reviews with:
|
|
||||||
- review id, body, state (approved/changes_requested/commented)
|
|
||||||
- user, commit_id, submitted_at, html_url
|
|
||||||
|
|
||||||
**example:**
|
|
||||||
```typescript
|
|
||||||
await mcp.call("gh_pullfrog/list_pull_request_reviews", {
|
|
||||||
pull_number: 47
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `reply_to_review_comment`
|
|
||||||
reply to a PR review comment thread explaining how the feedback was addressed.
|
|
||||||
|
|
||||||
**parameters:**
|
|
||||||
- `pull_number` (number): the pull request number
|
|
||||||
- `comment_id` (number): the ID of the review comment to reply to
|
|
||||||
- `body` (string): the reply text explaining how the feedback was addressed
|
|
||||||
|
|
||||||
**replaces:** `gh api repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies`
|
|
||||||
|
|
||||||
**returns:**
|
|
||||||
the created reply comment including:
|
|
||||||
- comment id, body, html_url
|
|
||||||
- in_reply_to_id showing it's a reply to the specified comment
|
|
||||||
|
|
||||||
**example:**
|
|
||||||
```typescript
|
|
||||||
// after addressing a review comment
|
|
||||||
await mcp.call("gh_pullfrog/reply_to_review_comment", {
|
|
||||||
pull_number: 47,
|
|
||||||
comment_id: 2567334961,
|
|
||||||
body: "removed the function as requested"
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### output tools
|
|
||||||
|
|
||||||
#### `set_output`
|
|
||||||
set the action output for consumption by subsequent workflow steps. useful when pullfrog is used as a step in a user-defined CI workflow (e.g., generating release notes).
|
|
||||||
|
|
||||||
**parameters:**
|
|
||||||
- `value` (string): the output value to expose
|
|
||||||
|
|
||||||
**returns:**
|
|
||||||
- `success`: true on success
|
|
||||||
|
|
||||||
the value will be available as the `result` output of the action, accessible via `${{ steps.<step-id>.outputs.result }}`.
|
|
||||||
|
|
||||||
**example:**
|
|
||||||
```typescript
|
|
||||||
// when generating content for downstream consumption
|
|
||||||
await mcp.call("gh_pullfrog/set_output", {
|
|
||||||
value: "## Release Notes\n\n- Added new feature X\n- Fixed bug Y"
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**usage in workflow:**
|
|
||||||
```yaml
|
|
||||||
- uses: pullfrog/pullfrog@v1
|
|
||||||
id: notes
|
|
||||||
with:
|
|
||||||
prompt: "Generate release notes for v2.0.0"
|
|
||||||
|
|
||||||
- uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
body: ${{ steps.notes.outputs.result }}
|
|
||||||
```
|
|
||||||
|
|
||||||
### other tools
|
|
||||||
|
|
||||||
see individual files for documentation on other tools:
|
|
||||||
- `comment.ts` - create, edit, and update comments
|
|
||||||
- `issue.ts` - create issues
|
|
||||||
- `output.ts` - set action output for workflow consumption
|
|
||||||
- `pr.ts` - create pull requests
|
|
||||||
- `prInfo.ts` - get pull request information
|
|
||||||
- `review.ts` - create pull request reviews
|
|
||||||
- `delegate.ts` - delegate task to a subagent with a specific mode and effort level
|
|
||||||
|
|
||||||
## usage in agents
|
|
||||||
|
|
||||||
agents should prefer using the mcp tools provided by this server. the `gh` cli is available as a fallback if needed, but mcp tools handle authentication and provide better integration.
|
|
||||||
|
|
||||||
the agent instructions automatically include guidance on using these tools.
|
|
||||||
|
|
||||||
+3
-3
@@ -78,7 +78,7 @@ function detectSandboxMethod(): SandboxMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
detectedSandboxMethod = "none";
|
detectedSandboxMethod = "none";
|
||||||
log.warning("PID namespace isolation not available - falling back to env filtering only");
|
log.info("PID namespace isolation not available - falling back to env filtering only");
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,8 +243,8 @@ Use this tool to:
|
|||||||
|
|
||||||
const finalExitCode = exitCode ?? (timedOut ? 124 : -1);
|
const finalExitCode = exitCode ?? (timedOut ? 124 : -1);
|
||||||
if (finalExitCode !== 0) {
|
if (finalExitCode !== 0) {
|
||||||
log.error(`bash command failed with exit code ${finalExitCode}: ${params.command}`);
|
log.info(`bash command failed with exit code ${finalExitCode}: ${params.command}`);
|
||||||
if (output) log.error(`output: ${output.trim()}`);
|
if (output) log.info(`output: ${output.trim()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+1
-1
@@ -213,7 +213,7 @@ export function GetCheckSuiteLogsTool(ctx: ToolContext) {
|
|||||||
|
|
||||||
log.debug(`analyzed logs for job ${job.name}: ${analysis.index.length} indexed lines`);
|
log.debug(`analyzed logs for job ${job.name}: ${analysis.index.length} indexed lines`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(`failed to fetch logs for job ${job.id}: ${error}`);
|
log.info(`failed to fetch logs for job ${job.id}: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -112,7 +112,7 @@ export function DelegateTool(ctx: ToolContext) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// normalize agent crashes into the same return shape as clean failures
|
// normalize agent crashes into the same return shape as clean failures
|
||||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
log.error(`» delegation to ${selectedMode.name} crashed: ${errorMessage}`);
|
log.info(`» delegation to ${selectedMode.name} crashed: ${errorMessage}`);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
mode: selectedMode.name,
|
mode: selectedMode.name,
|
||||||
|
|||||||
@@ -44,6 +44,22 @@ export function CreatePullRequestTool(ctx: ToolContext) {
|
|||||||
base: base,
|
base: base,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// best-effort: request review from the user who triggered the workflow
|
||||||
|
const reviewer = ctx.payload.triggeringUser;
|
||||||
|
if (reviewer) {
|
||||||
|
try {
|
||||||
|
log.debug(`Requesting review from ${reviewer} on PR #${result.data.number}`);
|
||||||
|
await ctx.octokit.rest.pulls.requestReviewers({
|
||||||
|
owner: ctx.repo.owner,
|
||||||
|
repo: ctx.repo.name,
|
||||||
|
pull_number: result.data.number,
|
||||||
|
reviewers: [reviewer],
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
log.info(`failed to request review from ${reviewer} on PR #${result.data.number}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
pullRequestId: result.data.id,
|
pullRequestId: result.data.id,
|
||||||
|
|||||||
@@ -290,7 +290,8 @@ function hasThumbsUpFrom(comment: ReviewThreadComment, username: string): boolea
|
|||||||
if (!comment.reactionGroups) return false;
|
if (!comment.reactionGroups) return false;
|
||||||
const thumbsUp = comment.reactionGroups.find((g) => g.content === "THUMBS_UP");
|
const thumbsUp = comment.reactionGroups.find((g) => g.content === "THUMBS_UP");
|
||||||
if (!thumbsUp?.reactors?.nodes) return false;
|
if (!thumbsUp?.reactors?.nodes) return false;
|
||||||
return thumbsUp.reactors.nodes.some((r) => r?.login === username);
|
const usernameNeedle = username.toLowerCase();
|
||||||
|
return thumbsUp.reactors.nodes.some((r) => r?.login?.toLowerCase() === usernameNeedle);
|
||||||
}
|
}
|
||||||
|
|
||||||
function threadHasThumbsUpFrom(thread: ReviewThread, username: string): boolean {
|
function threadHasThumbsUpFrom(thread: ReviewThread, username: string): boolean {
|
||||||
@@ -631,7 +632,7 @@ export function ResolveReviewThreadTool(ctx: ToolContext) {
|
|||||||
const message = isResolved
|
const message = isResolved
|
||||||
? `thread ${params.thread_id} was already resolved`
|
? `thread ${params.thread_id} was already resolved`
|
||||||
: `failed to resolve thread ${params.thread_id}: ${errorMessage}`;
|
: `failed to resolve thread ${params.thread_id}: ${errorMessage}`;
|
||||||
log.warning(message);
|
log.info(message);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
thread_id: params.thread_id,
|
thread_id: params.thread_id,
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ export const execute = <T, R extends Record<string, any> | string>(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
const prefix = toolName ? `[${toolName}]` : "tool";
|
const prefix = toolName ? `[${toolName}]` : "tool";
|
||||||
log.error(`${prefix} error: ${errorMessage}`);
|
log.info(`${prefix} error: ${errorMessage}`);
|
||||||
log.debug(`${prefix} params: ${formatJsonValue(params)}`);
|
log.debug(`${prefix} params: ${formatJsonValue(params)}`);
|
||||||
return handleToolError(error);
|
return handleToolError(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export function computeModes(): Mode[] {
|
|||||||
name: "Review",
|
name: "Review",
|
||||||
description:
|
description:
|
||||||
"Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
"Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
|
||||||
prompt: `Follow these steps to review the PR. Your job is to find problems—assume they exist until you've proven otherwise. Do not submit a clean review without thorough investigation.
|
prompt: `Follow these steps to review the PR. Your job is to find problems—assume they exist until you've proven otherwise. Do not submit a clean review without thorough investigation. **If you have nothing interesting to say, do NOT submit a review at all—use \`report_progress\` instead.**
|
||||||
|
|
||||||
1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number. This should give you all PR metadata you need, including a \`diffPath\`: a path to a temp file containing the PR diff.
|
1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number. This should give you all PR metadata you need, including a \`diffPath\`: a path to a temp file containing the PR diff.
|
||||||
|
|
||||||
@@ -113,21 +113,17 @@ export function computeModes(): Mode[] {
|
|||||||
- **Explore failure modes**: What if this throws? What if that returns null? What if the network fails? What if this runs twice?
|
- **Explore failure modes**: What if this throws? What if that returns null? What if the network fails? What if this runs twice?
|
||||||
- **Verify assumptions**: If the code assumes X, verify X is actually true. Use grep, read related files, check documentation.
|
- **Verify assumptions**: If the code assumes X, verify X is actually true. Use grep, read related files, check documentation.
|
||||||
- **Consider lifecycle**: Initialization, cleanup, error recovery. Are resources acquired before use? Released after? What happens on cancellation?
|
- **Consider lifecycle**: Initialization, cleanup, error recovery. Are resources acquired before use? Released after? What happens on cancellation?
|
||||||
|
- **Spot performance issues**: Nested loops over large collections, blocking I/O, memory leaks, excessive object creation in hot paths, inefficient array operations (e.g., repeated \`.find()\` in a loop).
|
||||||
|
- **Check PR consistency**: Does the PR title/description match the actual code changes? Flag significant discrepancies.
|
||||||
- Do NOT stop at "this looks reasonable." Dig until you either find a problem or have concrete evidence there isn't one.
|
- Do NOT stop at "this looks reasonable." Dig until you either find a problem or have concrete evidence there isn't one.
|
||||||
|
|
||||||
4. **DRAFT LINE-BY-LINE COMMENTS** - For each issue found, draft an inline comment on the specific line. Use the NEW line number from the diff (second column: \`| OLD | NEW | TYPE | CODE\`). If no issues found, skip to step 6.
|
4. **DRAFT LINE-BY-LINE COMMENTS** - Every comment must be actionable: the author should need to change something in response. 2-3 sentences max. Use the NEW line number from the diff (second column: \`| OLD | NEW | TYPE | CODE\`). If no issues found, skip to step 5. Non-actionable comments (praise, style preferences, minor optimizations, documentation nits) must not be drafted. If no comments survive and you have no significant concerns, **do not submit a review**. Use \`${ghPullfrogMcpName}/report_progress\` to note the PR was reviewed and no issues were found.
|
||||||
|
|
||||||
5. **FILTER LINE-BY-LINE COMMENTS** - Each inline comment must be actionable. Remove anything that doesn't require action:
|
5. **WRITE SUMMARY** - Draft a 1-3 sentence summary for the review body. Include urgency level and any concerns about code outside the diff.
|
||||||
- **Not actionable → no comment**: Do NOT create inline comments for compliments (e.g., "this looks clean", "nice refactor") or general observations. These waste reviewer attention.
|
|
||||||
- **Actionable by agent → keep**: Bugs, logic errors, missing error handling, security issues, race conditions, resource leaks, incorrect assumptions.
|
|
||||||
- **Requires human decision → keep**: If something needs human judgment (architectural choice, product decision, tradeoff evaluation), create a comment clearly stating what decision is needed and why.
|
|
||||||
- Remove style-only comments (formatting, naming conventions) unless they cause real confusion.
|
|
||||||
|
|
||||||
6. **WRITE SUMMARY** - Draft a 1-3 sentence summary for the review body. Include urgency level and any concerns about code outside the diff.
|
6. **SUBMIT** — Use ${ghPullfrogMcpName}/create_pull_request_review:
|
||||||
|
- \`body\`: The summary from step 5
|
||||||
7. **SUBMIT** — Use ${ghPullfrogMcpName}/create_pull_request_review:
|
- \`comments\`: The inline comments from step 4
|
||||||
- \`body\`: The summary from step 6
|
|
||||||
- \`comments\`: The filtered inline comments from step 5
|
|
||||||
|
|
||||||
${permalinkTip}
|
${permalinkTip}
|
||||||
`,
|
`,
|
||||||
|
|||||||
+6
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/pullfrog",
|
"name": "@pullfrog/pullfrog",
|
||||||
"version": "0.0.164",
|
"version": "0.0.166",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
"runtest": "node test/run.ts",
|
"runtest": "node test/run.ts",
|
||||||
"scratch": "node scratch.ts",
|
"scratch": "node scratch.ts",
|
||||||
"upDeps": "pnpm up --latest",
|
"upDeps": "pnpm up --latest",
|
||||||
"lock": "pnpm --ignore-workspace install --no-frozen-lockfile",
|
"lock": "pnpm install --no-frozen-lockfile",
|
||||||
|
"postinstall": "node scripts/generate-proxies.ts",
|
||||||
"prepare": "cd .. && husky action/.husky"
|
"prepare": "cd .. && husky action/.husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -79,7 +80,9 @@
|
|||||||
"types": "./dist/index.d.cts",
|
"types": "./dist/index.d.cts",
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.js",
|
||||||
"require": "./dist/index.cjs"
|
"require": "./dist/index.cjs"
|
||||||
}
|
},
|
||||||
|
"./internal": "./dist/internal.js",
|
||||||
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
|
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { mkdtemp } from "node:fs/promises";
|
import { mkdtemp } from "node:fs/promises";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join, resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
import arg from "arg";
|
import arg from "arg";
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
import type { AgentResult } from "./agents/shared.ts";
|
import type { AgentResult } from "./agents/shared.ts";
|
||||||
@@ -12,6 +12,7 @@ import { log } from "./utils/cli.ts";
|
|||||||
import { runInDocker } from "./utils/docker.ts";
|
import { runInDocker } from "./utils/docker.ts";
|
||||||
import { ensureGitHubToken } from "./utils/github.ts";
|
import { ensureGitHubToken } from "./utils/github.ts";
|
||||||
import { isInsideDocker } from "./utils/globals.ts";
|
import { isInsideDocker } from "./utils/globals.ts";
|
||||||
|
import { runPostCleanup } from "./utils/postCleanup.ts";
|
||||||
import { setupTestRepo } from "./utils/setup.ts";
|
import { setupTestRepo } from "./utils/setup.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,7 +69,13 @@ export async function run(inputsOrPrompt: Inputs | string): Promise<AgentResult>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await main();
|
// wrap main() so post cleanup runs even on failure (mirrors action.yml post-if: "failure() || cancelled()")
|
||||||
|
let result: AgentResult;
|
||||||
|
try {
|
||||||
|
result = await main();
|
||||||
|
} finally {
|
||||||
|
await runPostCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
process.chdir(originalCwd);
|
process.chdir(originalCwd);
|
||||||
|
|
||||||
@@ -95,7 +102,11 @@ export async function run(inputsOrPrompt: Inputs | string): Promise<AgentResult>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
const isDirectExecution = process.argv[1]
|
||||||
|
? import.meta.url === pathToFileURL(resolve(process.argv[1])).href
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (isDirectExecution) {
|
||||||
const args = arg({
|
const args = arg({
|
||||||
"--help": Boolean,
|
"--help": Boolean,
|
||||||
"--raw": String,
|
"--raw": String,
|
||||||
|
|||||||
Generated
+38
@@ -4,6 +4,8 @@ settings:
|
|||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
packageExtensionsChecksum: sha256-Ae6BTffLg0DiuEWVZSk6skwAhBSw9mfAk50E5Iq3i80=
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@@ -120,6 +122,15 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^4.0.0
|
zod: ^4.0.0
|
||||||
|
|
||||||
|
'@anthropic-ai/sdk@0.77.0':
|
||||||
|
resolution: {integrity: sha512-TivlT6nfidz3sOyMF72T2x5AkmHrpT7JgL2e/0HNdh7b24v7JC8cR+rCY/42jA68xIsjmiGQ5IKMsH9feEKh3A==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.25.0 || ^4.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@ark/fs@0.56.0':
|
'@ark/fs@0.56.0':
|
||||||
resolution: {integrity: sha512-zY/wDDhcvmt6/upQwZM766PAnvIzdEMcgydUGd9pqY9FMGNo9I9uE4RYAfms9AeUUtbZJu2h2Ua0tvFsO5XF4Q==}
|
resolution: {integrity: sha512-zY/wDDhcvmt6/upQwZM766PAnvIzdEMcgydUGd9pqY9FMGNo9I9uE4RYAfms9AeUUtbZJu2h2Ua0tvFsO5XF4Q==}
|
||||||
|
|
||||||
@@ -129,6 +140,10 @@ packages:
|
|||||||
'@ark/util@0.56.0':
|
'@ark/util@0.56.0':
|
||||||
resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==}
|
resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==}
|
||||||
|
|
||||||
|
'@babel/runtime@7.28.6':
|
||||||
|
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@borewit/text-codec@0.1.1':
|
'@borewit/text-codec@0.1.1':
|
||||||
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
|
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
|
||||||
|
|
||||||
@@ -1182,6 +1197,10 @@ packages:
|
|||||||
jose@6.1.3:
|
jose@6.1.3:
|
||||||
resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
|
resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
|
||||||
|
|
||||||
|
json-schema-to-ts@3.1.1:
|
||||||
|
resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
json-schema-traverse@1.0.0:
|
json-schema-traverse@1.0.0:
|
||||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||||
|
|
||||||
@@ -1454,6 +1473,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
|
resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
|
ts-algebra@2.0.0:
|
||||||
|
resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
|
||||||
|
|
||||||
tunnel@0.0.6:
|
tunnel@0.0.6:
|
||||||
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
|
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
|
||||||
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
|
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
|
||||||
@@ -1666,6 +1688,7 @@ snapshots:
|
|||||||
|
|
||||||
'@anthropic-ai/claude-agent-sdk@0.2.39(zod@4.3.5)':
|
'@anthropic-ai/claude-agent-sdk@0.2.39(zod@4.3.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@anthropic-ai/sdk': 0.77.0(zod@4.3.5)
|
||||||
zod: 4.3.5
|
zod: 4.3.5
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@img/sharp-darwin-arm64': 0.33.5
|
'@img/sharp-darwin-arm64': 0.33.5
|
||||||
@@ -1677,6 +1700,12 @@ snapshots:
|
|||||||
'@img/sharp-linuxmusl-x64': 0.33.5
|
'@img/sharp-linuxmusl-x64': 0.33.5
|
||||||
'@img/sharp-win32-x64': 0.33.5
|
'@img/sharp-win32-x64': 0.33.5
|
||||||
|
|
||||||
|
'@anthropic-ai/sdk@0.77.0(zod@4.3.5)':
|
||||||
|
dependencies:
|
||||||
|
json-schema-to-ts: 3.1.1
|
||||||
|
optionalDependencies:
|
||||||
|
zod: 4.3.5
|
||||||
|
|
||||||
'@ark/fs@0.56.0': {}
|
'@ark/fs@0.56.0': {}
|
||||||
|
|
||||||
'@ark/schema@0.56.0':
|
'@ark/schema@0.56.0':
|
||||||
@@ -1685,6 +1714,8 @@ snapshots:
|
|||||||
|
|
||||||
'@ark/util@0.56.0': {}
|
'@ark/util@0.56.0': {}
|
||||||
|
|
||||||
|
'@babel/runtime@7.28.6': {}
|
||||||
|
|
||||||
'@borewit/text-codec@0.1.1': {}
|
'@borewit/text-codec@0.1.1': {}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.25.12':
|
'@esbuild/aix-ppc64@0.25.12':
|
||||||
@@ -2589,6 +2620,11 @@ snapshots:
|
|||||||
|
|
||||||
jose@6.1.3: {}
|
jose@6.1.3: {}
|
||||||
|
|
||||||
|
json-schema-to-ts@3.1.1:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.28.6
|
||||||
|
ts-algebra: 2.0.0
|
||||||
|
|
||||||
json-schema-traverse@1.0.0: {}
|
json-schema-traverse@1.0.0: {}
|
||||||
|
|
||||||
json-schema-typed@8.0.2: {}
|
json-schema-typed@8.0.2: {}
|
||||||
@@ -2871,6 +2907,8 @@ snapshots:
|
|||||||
'@tokenizer/token': 0.3.0
|
'@tokenizer/token': 0.3.0
|
||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
|
|
||||||
|
ts-algebra@2.0.0: {}
|
||||||
|
|
||||||
tunnel@0.0.6: {}
|
tunnel@0.0.6: {}
|
||||||
|
|
||||||
turndown@7.2.2:
|
turndown@7.2.2:
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
packages: [] # prevent looking upwards for the workspace root
|
||||||
|
|
||||||
|
packageExtensions:
|
||||||
|
"@anthropic-ai/claude-agent-sdk":
|
||||||
|
dependencies:
|
||||||
|
"@anthropic-ai/sdk": "*"
|
||||||
@@ -399,7 +399,7 @@ var require_tunnel = __commonJS({
|
|||||||
connectOptions.headers = connectOptions.headers || {};
|
connectOptions.headers = connectOptions.headers || {};
|
||||||
connectOptions.headers["Proxy-Authorization"] = "Basic " + new Buffer(connectOptions.proxyAuth).toString("base64");
|
connectOptions.headers["Proxy-Authorization"] = "Basic " + new Buffer(connectOptions.proxyAuth).toString("base64");
|
||||||
}
|
}
|
||||||
debug("making CONNECT request");
|
debug2("making CONNECT request");
|
||||||
var connectReq = self2.request(connectOptions);
|
var connectReq = self2.request(connectOptions);
|
||||||
connectReq.useChunkedEncodingByDefault = false;
|
connectReq.useChunkedEncodingByDefault = false;
|
||||||
connectReq.once("response", onResponse);
|
connectReq.once("response", onResponse);
|
||||||
@@ -419,7 +419,7 @@ var require_tunnel = __commonJS({
|
|||||||
connectReq.removeAllListeners();
|
connectReq.removeAllListeners();
|
||||||
socket.removeAllListeners();
|
socket.removeAllListeners();
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
debug(
|
debug2(
|
||||||
"tunneling socket could not be established, statusCode=%d",
|
"tunneling socket could not be established, statusCode=%d",
|
||||||
res.statusCode
|
res.statusCode
|
||||||
);
|
);
|
||||||
@@ -431,7 +431,7 @@ var require_tunnel = __commonJS({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (head.length > 0) {
|
if (head.length > 0) {
|
||||||
debug("got illegal response body from proxy");
|
debug2("got illegal response body from proxy");
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
var error2 = new Error("got illegal response body from proxy");
|
var error2 = new Error("got illegal response body from proxy");
|
||||||
error2.code = "ECONNRESET";
|
error2.code = "ECONNRESET";
|
||||||
@@ -439,13 +439,13 @@ var require_tunnel = __commonJS({
|
|||||||
self2.removeSocket(placeholder);
|
self2.removeSocket(placeholder);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debug("tunneling connection has established");
|
debug2("tunneling connection has established");
|
||||||
self2.sockets[self2.sockets.indexOf(placeholder)] = socket;
|
self2.sockets[self2.sockets.indexOf(placeholder)] = socket;
|
||||||
return cb(socket);
|
return cb(socket);
|
||||||
}
|
}
|
||||||
function onError(cause) {
|
function onError(cause) {
|
||||||
connectReq.removeAllListeners();
|
connectReq.removeAllListeners();
|
||||||
debug(
|
debug2(
|
||||||
"tunneling socket could not be established, cause=%s\n",
|
"tunneling socket could not be established, cause=%s\n",
|
||||||
cause.message,
|
cause.message,
|
||||||
cause.stack
|
cause.stack
|
||||||
@@ -507,9 +507,9 @@ var require_tunnel = __commonJS({
|
|||||||
}
|
}
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
var debug;
|
var debug2;
|
||||||
if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) {
|
if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) {
|
||||||
debug = function() {
|
debug2 = function() {
|
||||||
var args2 = Array.prototype.slice.call(arguments);
|
var args2 = Array.prototype.slice.call(arguments);
|
||||||
if (typeof args2[0] === "string") {
|
if (typeof args2[0] === "string") {
|
||||||
args2[0] = "TUNNEL: " + args2[0];
|
args2[0] = "TUNNEL: " + args2[0];
|
||||||
@@ -519,10 +519,10 @@ var require_tunnel = __commonJS({
|
|||||||
console.error.apply(console, args2);
|
console.error.apply(console, args2);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
debug = function() {
|
debug2 = function() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
exports.debug = debug;
|
exports.debug = debug2;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -19733,10 +19733,10 @@ Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
|
|||||||
return process.env["RUNNER_DEBUG"] === "1";
|
return process.env["RUNNER_DEBUG"] === "1";
|
||||||
}
|
}
|
||||||
exports.isDebug = isDebug2;
|
exports.isDebug = isDebug2;
|
||||||
function debug(message) {
|
function debug2(message) {
|
||||||
(0, command_1.issueCommand)("debug", {}, message);
|
(0, command_1.issueCommand)("debug", {}, message);
|
||||||
}
|
}
|
||||||
exports.debug = debug;
|
exports.debug = debug2;
|
||||||
function error2(message, properties = {}) {
|
function error2(message, properties = {}) {
|
||||||
(0, command_1.issueCommand)("error", (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message);
|
(0, command_1.issueCommand)("error", (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message);
|
||||||
}
|
}
|
||||||
@@ -25838,7 +25838,7 @@ var require_light = __commonJS({
|
|||||||
}
|
}
|
||||||
return this.Events.trigger("scheduled", { args: this.args, options: this.options });
|
return this.Events.trigger("scheduled", { args: this.args, options: this.options });
|
||||||
}
|
}
|
||||||
async doExecute(chained, clearGlobalState, run2, free) {
|
async doExecute(chained, clearGlobalState, run, free) {
|
||||||
var error2, eventInfo, passed;
|
var error2, eventInfo, passed;
|
||||||
if (this.retryCount === 0) {
|
if (this.retryCount === 0) {
|
||||||
this._assertStatus("RUNNING");
|
this._assertStatus("RUNNING");
|
||||||
@@ -25858,10 +25858,10 @@ var require_light = __commonJS({
|
|||||||
}
|
}
|
||||||
} catch (error1) {
|
} catch (error1) {
|
||||||
error2 = error1;
|
error2 = error1;
|
||||||
return this._onFailure(error2, eventInfo, clearGlobalState, run2, free);
|
return this._onFailure(error2, eventInfo, clearGlobalState, run, free);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
doExpire(clearGlobalState, run2, free) {
|
doExpire(clearGlobalState, run, free) {
|
||||||
var error2, eventInfo;
|
var error2, eventInfo;
|
||||||
if (this._states.jobStatus(this.options.id === "RUNNING")) {
|
if (this._states.jobStatus(this.options.id === "RUNNING")) {
|
||||||
this._states.next(this.options.id);
|
this._states.next(this.options.id);
|
||||||
@@ -25869,9 +25869,9 @@ var require_light = __commonJS({
|
|||||||
this._assertStatus("EXECUTING");
|
this._assertStatus("EXECUTING");
|
||||||
eventInfo = { args: this.args, options: this.options, retryCount: this.retryCount };
|
eventInfo = { args: this.args, options: this.options, retryCount: this.retryCount };
|
||||||
error2 = new BottleneckError$1(`This job timed out after ${this.options.expiration} ms.`);
|
error2 = new BottleneckError$1(`This job timed out after ${this.options.expiration} ms.`);
|
||||||
return this._onFailure(error2, eventInfo, clearGlobalState, run2, free);
|
return this._onFailure(error2, eventInfo, clearGlobalState, run, free);
|
||||||
}
|
}
|
||||||
async _onFailure(error2, eventInfo, clearGlobalState, run2, free) {
|
async _onFailure(error2, eventInfo, clearGlobalState, run, free) {
|
||||||
var retry2, retryAfter;
|
var retry2, retryAfter;
|
||||||
if (clearGlobalState()) {
|
if (clearGlobalState()) {
|
||||||
retry2 = await this.Events.trigger("failed", error2, eventInfo);
|
retry2 = await this.Events.trigger("failed", error2, eventInfo);
|
||||||
@@ -25879,7 +25879,7 @@ var require_light = __commonJS({
|
|||||||
retryAfter = ~~retry2;
|
retryAfter = ~~retry2;
|
||||||
this.Events.trigger("retry", `Retrying ${this.options.id} after ${retryAfter} ms`, eventInfo);
|
this.Events.trigger("retry", `Retrying ${this.options.id} after ${retryAfter} ms`, eventInfo);
|
||||||
this.retryCount++;
|
this.retryCount++;
|
||||||
return run2(retryAfter);
|
return run(retryAfter);
|
||||||
} else {
|
} else {
|
||||||
this.doDone(eventInfo);
|
this.doDone(eventInfo);
|
||||||
await free(this.options, eventInfo);
|
await free(this.options, eventInfo);
|
||||||
@@ -26517,17 +26517,17 @@ var require_light = __commonJS({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_run(index, job, wait) {
|
_run(index, job, wait) {
|
||||||
var clearGlobalState, free, run2;
|
var clearGlobalState, free, run;
|
||||||
job.doRun();
|
job.doRun();
|
||||||
clearGlobalState = this._clearGlobalState.bind(this, index);
|
clearGlobalState = this._clearGlobalState.bind(this, index);
|
||||||
run2 = this._run.bind(this, index, job);
|
run = this._run.bind(this, index, job);
|
||||||
free = this._free.bind(this, index, job);
|
free = this._free.bind(this, index, job);
|
||||||
return this._scheduled[index] = {
|
return this._scheduled[index] = {
|
||||||
timeout: setTimeout(() => {
|
timeout: setTimeout(() => {
|
||||||
return job.doExecute(this._limiter, clearGlobalState, run2, free);
|
return job.doExecute(this._limiter, clearGlobalState, run, free);
|
||||||
}, wait),
|
}, wait),
|
||||||
expiration: job.options.expiration != null ? setTimeout(function() {
|
expiration: job.options.expiration != null ? setTimeout(function() {
|
||||||
return job.doExpire(clearGlobalState, run2, free);
|
return job.doExpire(clearGlobalState, run, free);
|
||||||
}, wait + job.options.expiration) : void 0,
|
}, wait + job.options.expiration) : void 0,
|
||||||
job
|
job
|
||||||
};
|
};
|
||||||
@@ -26949,9 +26949,9 @@ var require_constants6 = __commonJS({
|
|||||||
var require_debug = __commonJS({
|
var require_debug = __commonJS({
|
||||||
"node_modules/.pnpm/semver@7.7.3/node_modules/semver/internal/debug.js"(exports, module) {
|
"node_modules/.pnpm/semver@7.7.3/node_modules/semver/internal/debug.js"(exports, module) {
|
||||||
"use strict";
|
"use strict";
|
||||||
var debug = typeof process === "object" && process.env && process.env.NODE_DEBUG && /\bsemver\b/i.test(process.env.NODE_DEBUG) ? (...args2) => console.error("SEMVER", ...args2) : () => {
|
var debug2 = typeof process === "object" && process.env && process.env.NODE_DEBUG && /\bsemver\b/i.test(process.env.NODE_DEBUG) ? (...args2) => console.error("SEMVER", ...args2) : () => {
|
||||||
};
|
};
|
||||||
module.exports = debug;
|
module.exports = debug2;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -26964,7 +26964,7 @@ var require_re = __commonJS({
|
|||||||
MAX_SAFE_BUILD_LENGTH,
|
MAX_SAFE_BUILD_LENGTH,
|
||||||
MAX_LENGTH
|
MAX_LENGTH
|
||||||
} = require_constants6();
|
} = require_constants6();
|
||||||
var debug = require_debug();
|
var debug2 = require_debug();
|
||||||
exports = module.exports = {};
|
exports = module.exports = {};
|
||||||
var re = exports.re = [];
|
var re = exports.re = [];
|
||||||
var safeRe = exports.safeRe = [];
|
var safeRe = exports.safeRe = [];
|
||||||
@@ -26987,7 +26987,7 @@ var require_re = __commonJS({
|
|||||||
var createToken = (name, value2, isGlobal) => {
|
var createToken = (name, value2, isGlobal) => {
|
||||||
const safe = makeSafeRegex(value2);
|
const safe = makeSafeRegex(value2);
|
||||||
const index = R++;
|
const index = R++;
|
||||||
debug(name, index, value2);
|
debug2(name, index, value2);
|
||||||
t[name] = index;
|
t[name] = index;
|
||||||
src[index] = value2;
|
src[index] = value2;
|
||||||
safeSrc[index] = safe;
|
safeSrc[index] = safe;
|
||||||
@@ -27091,7 +27091,7 @@ var require_identifiers = __commonJS({
|
|||||||
var require_semver = __commonJS({
|
var require_semver = __commonJS({
|
||||||
"node_modules/.pnpm/semver@7.7.3/node_modules/semver/classes/semver.js"(exports, module) {
|
"node_modules/.pnpm/semver@7.7.3/node_modules/semver/classes/semver.js"(exports, module) {
|
||||||
"use strict";
|
"use strict";
|
||||||
var debug = require_debug();
|
var debug2 = require_debug();
|
||||||
var { MAX_LENGTH, MAX_SAFE_INTEGER } = require_constants6();
|
var { MAX_LENGTH, MAX_SAFE_INTEGER } = require_constants6();
|
||||||
var { safeRe: re, t } = require_re();
|
var { safeRe: re, t } = require_re();
|
||||||
var parseOptions = require_parse_options();
|
var parseOptions = require_parse_options();
|
||||||
@@ -27113,7 +27113,7 @@ var require_semver = __commonJS({
|
|||||||
`version is longer than ${MAX_LENGTH} characters`
|
`version is longer than ${MAX_LENGTH} characters`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
debug("SemVer", version, options);
|
debug2("SemVer", version, options);
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.loose = !!options.loose;
|
this.loose = !!options.loose;
|
||||||
this.includePrerelease = !!options.includePrerelease;
|
this.includePrerelease = !!options.includePrerelease;
|
||||||
@@ -27161,7 +27161,7 @@ var require_semver = __commonJS({
|
|||||||
return this.version;
|
return this.version;
|
||||||
}
|
}
|
||||||
compare(other) {
|
compare(other) {
|
||||||
debug("SemVer.compare", this.version, this.options, other);
|
debug2("SemVer.compare", this.version, this.options, other);
|
||||||
if (!(other instanceof _SemVer)) {
|
if (!(other instanceof _SemVer)) {
|
||||||
if (typeof other === "string" && other === this.version) {
|
if (typeof other === "string" && other === this.version) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -27212,7 +27212,7 @@ var require_semver = __commonJS({
|
|||||||
do {
|
do {
|
||||||
const a = this.prerelease[i];
|
const a = this.prerelease[i];
|
||||||
const b = other.prerelease[i];
|
const b = other.prerelease[i];
|
||||||
debug("prerelease compare", i, a, b);
|
debug2("prerelease compare", i, a, b);
|
||||||
if (a === void 0 && b === void 0) {
|
if (a === void 0 && b === void 0) {
|
||||||
return 0;
|
return 0;
|
||||||
} else if (b === void 0) {
|
} else if (b === void 0) {
|
||||||
@@ -27234,7 +27234,7 @@ var require_semver = __commonJS({
|
|||||||
do {
|
do {
|
||||||
const a = this.build[i];
|
const a = this.build[i];
|
||||||
const b = other.build[i];
|
const b = other.build[i];
|
||||||
debug("build compare", i, a, b);
|
debug2("build compare", i, a, b);
|
||||||
if (a === void 0 && b === void 0) {
|
if (a === void 0 && b === void 0) {
|
||||||
return 0;
|
return 0;
|
||||||
} else if (b === void 0) {
|
} else if (b === void 0) {
|
||||||
@@ -27862,21 +27862,21 @@ var require_range = __commonJS({
|
|||||||
const loose = this.options.loose;
|
const loose = this.options.loose;
|
||||||
const hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE];
|
const hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE];
|
||||||
range2 = range2.replace(hr, hyphenReplace(this.options.includePrerelease));
|
range2 = range2.replace(hr, hyphenReplace(this.options.includePrerelease));
|
||||||
debug("hyphen replace", range2);
|
debug2("hyphen replace", range2);
|
||||||
range2 = range2.replace(re[t.COMPARATORTRIM], comparatorTrimReplace);
|
range2 = range2.replace(re[t.COMPARATORTRIM], comparatorTrimReplace);
|
||||||
debug("comparator trim", range2);
|
debug2("comparator trim", range2);
|
||||||
range2 = range2.replace(re[t.TILDETRIM], tildeTrimReplace);
|
range2 = range2.replace(re[t.TILDETRIM], tildeTrimReplace);
|
||||||
debug("tilde trim", range2);
|
debug2("tilde trim", range2);
|
||||||
range2 = range2.replace(re[t.CARETTRIM], caretTrimReplace);
|
range2 = range2.replace(re[t.CARETTRIM], caretTrimReplace);
|
||||||
debug("caret trim", range2);
|
debug2("caret trim", range2);
|
||||||
let rangeList = range2.split(" ").map((comp) => parseComparator(comp, this.options)).join(" ").split(/\s+/).map((comp) => replaceGTE0(comp, this.options));
|
let rangeList = range2.split(" ").map((comp) => parseComparator(comp, this.options)).join(" ").split(/\s+/).map((comp) => replaceGTE0(comp, this.options));
|
||||||
if (loose) {
|
if (loose) {
|
||||||
rangeList = rangeList.filter((comp) => {
|
rangeList = rangeList.filter((comp) => {
|
||||||
debug("loose invalid filter", comp, this.options);
|
debug2("loose invalid filter", comp, this.options);
|
||||||
return !!comp.match(re[t.COMPARATORLOOSE]);
|
return !!comp.match(re[t.COMPARATORLOOSE]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
debug("range list", rangeList);
|
debug2("range list", rangeList);
|
||||||
const rangeMap = /* @__PURE__ */ new Map();
|
const rangeMap = /* @__PURE__ */ new Map();
|
||||||
const comparators = rangeList.map((comp) => new Comparator(comp, this.options));
|
const comparators = rangeList.map((comp) => new Comparator(comp, this.options));
|
||||||
for (const comp of comparators) {
|
for (const comp of comparators) {
|
||||||
@@ -27931,7 +27931,7 @@ var require_range = __commonJS({
|
|||||||
var cache = new LRU();
|
var cache = new LRU();
|
||||||
var parseOptions = require_parse_options();
|
var parseOptions = require_parse_options();
|
||||||
var Comparator = require_comparator();
|
var Comparator = require_comparator();
|
||||||
var debug = require_debug();
|
var debug2 = require_debug();
|
||||||
var SemVer = require_semver();
|
var SemVer = require_semver();
|
||||||
var {
|
var {
|
||||||
safeRe: re,
|
safeRe: re,
|
||||||
@@ -27957,15 +27957,15 @@ var require_range = __commonJS({
|
|||||||
};
|
};
|
||||||
var parseComparator = (comp, options) => {
|
var parseComparator = (comp, options) => {
|
||||||
comp = comp.replace(re[t.BUILD], "");
|
comp = comp.replace(re[t.BUILD], "");
|
||||||
debug("comp", comp, options);
|
debug2("comp", comp, options);
|
||||||
comp = replaceCarets(comp, options);
|
comp = replaceCarets(comp, options);
|
||||||
debug("caret", comp);
|
debug2("caret", comp);
|
||||||
comp = replaceTildes(comp, options);
|
comp = replaceTildes(comp, options);
|
||||||
debug("tildes", comp);
|
debug2("tildes", comp);
|
||||||
comp = replaceXRanges(comp, options);
|
comp = replaceXRanges(comp, options);
|
||||||
debug("xrange", comp);
|
debug2("xrange", comp);
|
||||||
comp = replaceStars(comp, options);
|
comp = replaceStars(comp, options);
|
||||||
debug("stars", comp);
|
debug2("stars", comp);
|
||||||
return comp;
|
return comp;
|
||||||
};
|
};
|
||||||
var isX = (id) => !id || id.toLowerCase() === "x" || id === "*";
|
var isX = (id) => !id || id.toLowerCase() === "x" || id === "*";
|
||||||
@@ -27975,7 +27975,7 @@ var require_range = __commonJS({
|
|||||||
var replaceTilde = (comp, options) => {
|
var replaceTilde = (comp, options) => {
|
||||||
const r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE];
|
const r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE];
|
||||||
return comp.replace(r, (_, M, m, p, pr) => {
|
return comp.replace(r, (_, M, m, p, pr) => {
|
||||||
debug("tilde", comp, _, M, m, p, pr);
|
debug2("tilde", comp, _, M, m, p, pr);
|
||||||
let ret;
|
let ret;
|
||||||
if (isX(M)) {
|
if (isX(M)) {
|
||||||
ret = "";
|
ret = "";
|
||||||
@@ -27984,12 +27984,12 @@ var require_range = __commonJS({
|
|||||||
} else if (isX(p)) {
|
} else if (isX(p)) {
|
||||||
ret = `>=${M}.${m}.0 <${M}.${+m + 1}.0-0`;
|
ret = `>=${M}.${m}.0 <${M}.${+m + 1}.0-0`;
|
||||||
} else if (pr) {
|
} else if (pr) {
|
||||||
debug("replaceTilde pr", pr);
|
debug2("replaceTilde pr", pr);
|
||||||
ret = `>=${M}.${m}.${p}-${pr} <${M}.${+m + 1}.0-0`;
|
ret = `>=${M}.${m}.${p}-${pr} <${M}.${+m + 1}.0-0`;
|
||||||
} else {
|
} else {
|
||||||
ret = `>=${M}.${m}.${p} <${M}.${+m + 1}.0-0`;
|
ret = `>=${M}.${m}.${p} <${M}.${+m + 1}.0-0`;
|
||||||
}
|
}
|
||||||
debug("tilde return", ret);
|
debug2("tilde return", ret);
|
||||||
return ret;
|
return ret;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -27997,11 +27997,11 @@ var require_range = __commonJS({
|
|||||||
return comp.trim().split(/\s+/).map((c) => replaceCaret(c, options)).join(" ");
|
return comp.trim().split(/\s+/).map((c) => replaceCaret(c, options)).join(" ");
|
||||||
};
|
};
|
||||||
var replaceCaret = (comp, options) => {
|
var replaceCaret = (comp, options) => {
|
||||||
debug("caret", comp, options);
|
debug2("caret", comp, options);
|
||||||
const r = options.loose ? re[t.CARETLOOSE] : re[t.CARET];
|
const r = options.loose ? re[t.CARETLOOSE] : re[t.CARET];
|
||||||
const z = options.includePrerelease ? "-0" : "";
|
const z = options.includePrerelease ? "-0" : "";
|
||||||
return comp.replace(r, (_, M, m, p, pr) => {
|
return comp.replace(r, (_, M, m, p, pr) => {
|
||||||
debug("caret", comp, _, M, m, p, pr);
|
debug2("caret", comp, _, M, m, p, pr);
|
||||||
let ret;
|
let ret;
|
||||||
if (isX(M)) {
|
if (isX(M)) {
|
||||||
ret = "";
|
ret = "";
|
||||||
@@ -28014,7 +28014,7 @@ var require_range = __commonJS({
|
|||||||
ret = `>=${M}.${m}.0${z} <${+M + 1}.0.0-0`;
|
ret = `>=${M}.${m}.0${z} <${+M + 1}.0.0-0`;
|
||||||
}
|
}
|
||||||
} else if (pr) {
|
} else if (pr) {
|
||||||
debug("replaceCaret pr", pr);
|
debug2("replaceCaret pr", pr);
|
||||||
if (M === "0") {
|
if (M === "0") {
|
||||||
if (m === "0") {
|
if (m === "0") {
|
||||||
ret = `>=${M}.${m}.${p}-${pr} <${M}.${m}.${+p + 1}-0`;
|
ret = `>=${M}.${m}.${p}-${pr} <${M}.${m}.${+p + 1}-0`;
|
||||||
@@ -28025,7 +28025,7 @@ var require_range = __commonJS({
|
|||||||
ret = `>=${M}.${m}.${p}-${pr} <${+M + 1}.0.0-0`;
|
ret = `>=${M}.${m}.${p}-${pr} <${+M + 1}.0.0-0`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debug("no pr");
|
debug2("no pr");
|
||||||
if (M === "0") {
|
if (M === "0") {
|
||||||
if (m === "0") {
|
if (m === "0") {
|
||||||
ret = `>=${M}.${m}.${p}${z} <${M}.${m}.${+p + 1}-0`;
|
ret = `>=${M}.${m}.${p}${z} <${M}.${m}.${+p + 1}-0`;
|
||||||
@@ -28036,19 +28036,19 @@ var require_range = __commonJS({
|
|||||||
ret = `>=${M}.${m}.${p} <${+M + 1}.0.0-0`;
|
ret = `>=${M}.${m}.${p} <${+M + 1}.0.0-0`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug("caret return", ret);
|
debug2("caret return", ret);
|
||||||
return ret;
|
return ret;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
var replaceXRanges = (comp, options) => {
|
var replaceXRanges = (comp, options) => {
|
||||||
debug("replaceXRanges", comp, options);
|
debug2("replaceXRanges", comp, options);
|
||||||
return comp.split(/\s+/).map((c) => replaceXRange(c, options)).join(" ");
|
return comp.split(/\s+/).map((c) => replaceXRange(c, options)).join(" ");
|
||||||
};
|
};
|
||||||
var replaceXRange = (comp, options) => {
|
var replaceXRange = (comp, options) => {
|
||||||
comp = comp.trim();
|
comp = comp.trim();
|
||||||
const r = options.loose ? re[t.XRANGELOOSE] : re[t.XRANGE];
|
const r = options.loose ? re[t.XRANGELOOSE] : re[t.XRANGE];
|
||||||
return comp.replace(r, (ret, gtlt, M, m, p, pr) => {
|
return comp.replace(r, (ret, gtlt, M, m, p, pr) => {
|
||||||
debug("xRange", comp, ret, gtlt, M, m, p, pr);
|
debug2("xRange", comp, ret, gtlt, M, m, p, pr);
|
||||||
const xM = isX(M);
|
const xM = isX(M);
|
||||||
const xm = xM || isX(m);
|
const xm = xM || isX(m);
|
||||||
const xp = xm || isX(p);
|
const xp = xm || isX(p);
|
||||||
@@ -28095,16 +28095,16 @@ var require_range = __commonJS({
|
|||||||
} else if (xp) {
|
} else if (xp) {
|
||||||
ret = `>=${M}.${m}.0${pr} <${M}.${+m + 1}.0-0`;
|
ret = `>=${M}.${m}.0${pr} <${M}.${+m + 1}.0-0`;
|
||||||
}
|
}
|
||||||
debug("xRange return", ret);
|
debug2("xRange return", ret);
|
||||||
return ret;
|
return ret;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
var replaceStars = (comp, options) => {
|
var replaceStars = (comp, options) => {
|
||||||
debug("replaceStars", comp, options);
|
debug2("replaceStars", comp, options);
|
||||||
return comp.trim().replace(re[t.STAR], "");
|
return comp.trim().replace(re[t.STAR], "");
|
||||||
};
|
};
|
||||||
var replaceGTE0 = (comp, options) => {
|
var replaceGTE0 = (comp, options) => {
|
||||||
debug("replaceGTE0", comp, options);
|
debug2("replaceGTE0", comp, options);
|
||||||
return comp.trim().replace(re[options.includePrerelease ? t.GTE0PRE : t.GTE0], "");
|
return comp.trim().replace(re[options.includePrerelease ? t.GTE0PRE : t.GTE0], "");
|
||||||
};
|
};
|
||||||
var hyphenReplace = (incPr) => ($0, from, fM, fm, fp, fpr, fb, to, tM, tm, tp, tpr) => {
|
var hyphenReplace = (incPr) => ($0, from, fM, fm, fp, fpr, fb, to, tM, tm, tp, tpr) => {
|
||||||
@@ -28142,7 +28142,7 @@ var require_range = __commonJS({
|
|||||||
}
|
}
|
||||||
if (version.prerelease.length && !options.includePrerelease) {
|
if (version.prerelease.length && !options.includePrerelease) {
|
||||||
for (let i = 0; i < set.length; i++) {
|
for (let i = 0; i < set.length; i++) {
|
||||||
debug(set[i].semver);
|
debug2(set[i].semver);
|
||||||
if (set[i].semver === Comparator.ANY) {
|
if (set[i].semver === Comparator.ANY) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -28179,7 +28179,7 @@ var require_comparator = __commonJS({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
comp = comp.trim().split(/\s+/).join(" ");
|
comp = comp.trim().split(/\s+/).join(" ");
|
||||||
debug("comparator", comp, options);
|
debug2("comparator", comp, options);
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.loose = !!options.loose;
|
this.loose = !!options.loose;
|
||||||
this.parse(comp);
|
this.parse(comp);
|
||||||
@@ -28188,7 +28188,7 @@ var require_comparator = __commonJS({
|
|||||||
} else {
|
} else {
|
||||||
this.value = this.operator + this.semver.version;
|
this.value = this.operator + this.semver.version;
|
||||||
}
|
}
|
||||||
debug("comp", this);
|
debug2("comp", this);
|
||||||
}
|
}
|
||||||
parse(comp) {
|
parse(comp) {
|
||||||
const r = this.options.loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR];
|
const r = this.options.loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR];
|
||||||
@@ -28210,7 +28210,7 @@ var require_comparator = __commonJS({
|
|||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
test(version) {
|
test(version) {
|
||||||
debug("Comparator.test", version, this.options.loose);
|
debug2("Comparator.test", version, this.options.loose);
|
||||||
if (this.semver === ANY || version === ANY) {
|
if (this.semver === ANY || version === ANY) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -28267,7 +28267,7 @@ var require_comparator = __commonJS({
|
|||||||
var parseOptions = require_parse_options();
|
var parseOptions = require_parse_options();
|
||||||
var { safeRe: re, t } = require_re();
|
var { safeRe: re, t } = require_re();
|
||||||
var cmp = require_cmp();
|
var cmp = require_cmp();
|
||||||
var debug = require_debug();
|
var debug2 = require_debug();
|
||||||
var SemVer = require_semver();
|
var SemVer = require_semver();
|
||||||
var Range = require_range();
|
var Range = require_range();
|
||||||
}
|
}
|
||||||
@@ -28843,6 +28843,184 @@ var require_semver2 = __commonJS({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// utils/log.ts
|
||||||
|
var core = __toESM(require_core(), 1);
|
||||||
|
var import_table = __toESM(require_src(), 1);
|
||||||
|
|
||||||
|
// utils/globals.ts
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
var isCloudflareSandbox = !!process.env.CLOUDFLARE_APPLICATION_ID && !!process.env.SANDBOX_VERSION;
|
||||||
|
var isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
||||||
|
var isInsideDocker = existsSync("/.dockerenv");
|
||||||
|
|
||||||
|
// utils/log.ts
|
||||||
|
var isRunnerDebugEnabled = () => core.isDebug();
|
||||||
|
var isLocalDebugEnabled = () => process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true";
|
||||||
|
var isDebugEnabled = () => isLocalDebugEnabled() || isRunnerDebugEnabled();
|
||||||
|
function ts() {
|
||||||
|
return isDebugEnabled() ? `[${(/* @__PURE__ */ new Date()).toISOString()}] ` : "";
|
||||||
|
}
|
||||||
|
function formatArgs(args2) {
|
||||||
|
return args2.map((arg) => {
|
||||||
|
if (typeof arg === "string") return arg;
|
||||||
|
if (arg instanceof Error) return `${arg.message}
|
||||||
|
${arg.stack}`;
|
||||||
|
return JSON.stringify(arg);
|
||||||
|
}).join(" ");
|
||||||
|
}
|
||||||
|
function startGroup2(name) {
|
||||||
|
if (isGitHubActions) {
|
||||||
|
core.startGroup(name);
|
||||||
|
} else {
|
||||||
|
console.group(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function endGroup2() {
|
||||||
|
if (isGitHubActions) {
|
||||||
|
core.endGroup();
|
||||||
|
} else {
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function group(name, fn2) {
|
||||||
|
startGroup2(name);
|
||||||
|
fn2();
|
||||||
|
endGroup2();
|
||||||
|
}
|
||||||
|
function boxString(text, options) {
|
||||||
|
const { title, maxWidth = 80, indent: indent2 = "", padding = 1 } = options || {};
|
||||||
|
const lines = text.trim().split("\n");
|
||||||
|
const wrappedLines = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.length <= maxWidth - padding * 2) {
|
||||||
|
wrappedLines.push(line);
|
||||||
|
} else {
|
||||||
|
const words = line.split(" ");
|
||||||
|
let currentLine = "";
|
||||||
|
for (const word of words) {
|
||||||
|
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
||||||
|
if (testLine.length <= maxWidth - padding * 2) {
|
||||||
|
currentLine = testLine;
|
||||||
|
} else {
|
||||||
|
if (currentLine) {
|
||||||
|
wrappedLines.push(currentLine);
|
||||||
|
currentLine = "";
|
||||||
|
}
|
||||||
|
const maxLineLength2 = maxWidth - padding * 2;
|
||||||
|
let remainingWord = word;
|
||||||
|
while (remainingWord.length > maxLineLength2) {
|
||||||
|
wrappedLines.push(remainingWord.substring(0, maxLineLength2));
|
||||||
|
remainingWord = remainingWord.substring(maxLineLength2);
|
||||||
|
}
|
||||||
|
currentLine = remainingWord;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentLine) {
|
||||||
|
wrappedLines.push(currentLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const maxLineLength = Math.max(...wrappedLines.map((line) => line.length));
|
||||||
|
const contentBoxWidth = maxLineLength + padding * 2;
|
||||||
|
const titleLineLength = title ? ` ${title} `.length : 0;
|
||||||
|
const boxWidth = Math.max(contentBoxWidth, titleLineLength);
|
||||||
|
let result = "";
|
||||||
|
if (title) {
|
||||||
|
const titleLine = ` ${title} `;
|
||||||
|
const titlePadding = Math.max(0, boxWidth - titleLine.length);
|
||||||
|
result += `${indent2}\u250C${titleLine}${"\u2500".repeat(titlePadding)}\u2510
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (!title) {
|
||||||
|
result += `${indent2}\u250C${"\u2500".repeat(boxWidth)}\u2510
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
for (const line of wrappedLines) {
|
||||||
|
const paddedLine = line.padEnd(maxLineLength);
|
||||||
|
result += `${indent2}\u2502${" ".repeat(padding)}${paddedLine}${" ".repeat(padding)}\u2502
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
result += `${indent2}\u2514${"\u2500".repeat(boxWidth)}\u2518`;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function box(text, options) {
|
||||||
|
const boxContent = boxString(text, options);
|
||||||
|
core.info(boxContent);
|
||||||
|
}
|
||||||
|
function printTable(rows, options) {
|
||||||
|
const { title } = options || {};
|
||||||
|
const tableData = rows.map(
|
||||||
|
(row) => row.map((cell) => {
|
||||||
|
if (typeof cell === "string") {
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
return cell.data;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const formatted = (0, import_table.table)(tableData);
|
||||||
|
if (title) {
|
||||||
|
core.info(`
|
||||||
|
${title}`);
|
||||||
|
}
|
||||||
|
core.info(`
|
||||||
|
${formatted}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
function separator(length = 50) {
|
||||||
|
const separatorText = "\u2500".repeat(length);
|
||||||
|
core.info(separatorText);
|
||||||
|
}
|
||||||
|
var log = {
|
||||||
|
/** Print info message */
|
||||||
|
info: (...args2) => {
|
||||||
|
core.info(`${ts()}${formatArgs(args2)}`);
|
||||||
|
},
|
||||||
|
/** Print a warning message. Use only for warnings that should be displayed in the job summary. */
|
||||||
|
warning: (...args2) => {
|
||||||
|
core.warning(`${ts()}${formatArgs(args2)}`);
|
||||||
|
},
|
||||||
|
/** Print an error message. Use only for errors that should be displayed in the job summary. */
|
||||||
|
error: (...args2) => {
|
||||||
|
core.error(`${ts()}${formatArgs(args2)}`);
|
||||||
|
},
|
||||||
|
/** Print success message */
|
||||||
|
success: (...args2) => {
|
||||||
|
core.info(`${ts()}\xBB ${formatArgs(args2)}`);
|
||||||
|
},
|
||||||
|
/** Print debug message (only when debug mode is enabled) */
|
||||||
|
debug: (...args2) => {
|
||||||
|
if (isRunnerDebugEnabled()) {
|
||||||
|
core.debug(formatArgs(args2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isLocalDebugEnabled()) {
|
||||||
|
core.info(`${ts()}[DEBUG] ${formatArgs(args2)}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** Print a formatted box with text */
|
||||||
|
box,
|
||||||
|
/** Print a formatted table using the table package */
|
||||||
|
table: printTable,
|
||||||
|
/** Print a separator line */
|
||||||
|
separator,
|
||||||
|
/** Start a collapsed group (GitHub Actions) or regular group (local) */
|
||||||
|
startGroup: startGroup2,
|
||||||
|
/** End a collapsed group */
|
||||||
|
endGroup: endGroup2,
|
||||||
|
/** Run a callback within a collapsed group */
|
||||||
|
group,
|
||||||
|
/** Log tool call information to console with formatted output */
|
||||||
|
toolCall: ({ toolName, input }) => {
|
||||||
|
const inputFormatted = formatJsonValue(input);
|
||||||
|
const output = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
|
||||||
|
log.info(output.trimEnd());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function formatJsonValue(value2) {
|
||||||
|
const compact = JSON.stringify(value2);
|
||||||
|
return compact.length > 80 || compact.includes("\n") ? JSON.stringify(value2, null, 2) : compact;
|
||||||
|
}
|
||||||
|
|
||||||
// node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
|
// node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
|
||||||
var liftArray = (data) => Array.isArray(data) ? data : [data];
|
var liftArray = (data) => Array.isArray(data) ? data : [data];
|
||||||
var spliterate = (arr, predicate) => {
|
var spliterate = (arr, predicate) => {
|
||||||
@@ -37299,178 +37477,6 @@ var schema = ark.schema;
|
|||||||
var define2 = ark.define;
|
var define2 = ark.define;
|
||||||
var declare = ark.declare;
|
var declare = ark.declare;
|
||||||
|
|
||||||
// utils/log.ts
|
|
||||||
var core = __toESM(require_core(), 1);
|
|
||||||
var import_table = __toESM(require_src(), 1);
|
|
||||||
|
|
||||||
// utils/globals.ts
|
|
||||||
import { existsSync } from "node:fs";
|
|
||||||
var isCloudflareSandbox = !!process.env.CLOUDFLARE_APPLICATION_ID && !!process.env.SANDBOX_VERSION;
|
|
||||||
var isGitHubActions = !!process.env.GITHUB_ACTIONS;
|
|
||||||
var isInsideDocker = existsSync("/.dockerenv");
|
|
||||||
|
|
||||||
// utils/log.ts
|
|
||||||
var isDebugEnabled = () => process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true" || core.isDebug();
|
|
||||||
function formatArgs(args2) {
|
|
||||||
return args2.map((arg) => {
|
|
||||||
if (typeof arg === "string") return arg;
|
|
||||||
if (arg instanceof Error) return `${arg.message}
|
|
||||||
${arg.stack}`;
|
|
||||||
return JSON.stringify(arg);
|
|
||||||
}).join(" ");
|
|
||||||
}
|
|
||||||
function startGroup2(name) {
|
|
||||||
if (isGitHubActions) {
|
|
||||||
core.startGroup(name);
|
|
||||||
} else {
|
|
||||||
console.group(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function endGroup2() {
|
|
||||||
if (isGitHubActions) {
|
|
||||||
core.endGroup();
|
|
||||||
} else {
|
|
||||||
console.groupEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function group(name, fn2) {
|
|
||||||
startGroup2(name);
|
|
||||||
fn2();
|
|
||||||
endGroup2();
|
|
||||||
}
|
|
||||||
function boxString(text, options) {
|
|
||||||
const { title, maxWidth = 80, indent: indent2 = "", padding = 1 } = options || {};
|
|
||||||
const lines = text.trim().split("\n");
|
|
||||||
const wrappedLines = [];
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.length <= maxWidth - padding * 2) {
|
|
||||||
wrappedLines.push(line);
|
|
||||||
} else {
|
|
||||||
const words = line.split(" ");
|
|
||||||
let currentLine = "";
|
|
||||||
for (const word of words) {
|
|
||||||
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
||||||
if (testLine.length <= maxWidth - padding * 2) {
|
|
||||||
currentLine = testLine;
|
|
||||||
} else {
|
|
||||||
if (currentLine) {
|
|
||||||
wrappedLines.push(currentLine);
|
|
||||||
currentLine = "";
|
|
||||||
}
|
|
||||||
const maxLineLength2 = maxWidth - padding * 2;
|
|
||||||
let remainingWord = word;
|
|
||||||
while (remainingWord.length > maxLineLength2) {
|
|
||||||
wrappedLines.push(remainingWord.substring(0, maxLineLength2));
|
|
||||||
remainingWord = remainingWord.substring(maxLineLength2);
|
|
||||||
}
|
|
||||||
currentLine = remainingWord;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentLine) {
|
|
||||||
wrappedLines.push(currentLine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const maxLineLength = Math.max(...wrappedLines.map((line) => line.length));
|
|
||||||
const contentBoxWidth = maxLineLength + padding * 2;
|
|
||||||
const titleLineLength = title ? ` ${title} `.length : 0;
|
|
||||||
const boxWidth = Math.max(contentBoxWidth, titleLineLength);
|
|
||||||
let result = "";
|
|
||||||
if (title) {
|
|
||||||
const titleLine = ` ${title} `;
|
|
||||||
const titlePadding = Math.max(0, boxWidth - titleLine.length);
|
|
||||||
result += `${indent2}\u250C${titleLine}${"\u2500".repeat(titlePadding)}\u2510
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
if (!title) {
|
|
||||||
result += `${indent2}\u250C${"\u2500".repeat(boxWidth)}\u2510
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
for (const line of wrappedLines) {
|
|
||||||
const paddedLine = line.padEnd(maxLineLength);
|
|
||||||
result += `${indent2}\u2502${" ".repeat(padding)}${paddedLine}${" ".repeat(padding)}\u2502
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
result += `${indent2}\u2514${"\u2500".repeat(boxWidth)}\u2518`;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
function box(text, options) {
|
|
||||||
const boxContent = boxString(text, options);
|
|
||||||
core.info(boxContent);
|
|
||||||
}
|
|
||||||
function printTable(rows, options) {
|
|
||||||
const { title } = options || {};
|
|
||||||
const tableData = rows.map(
|
|
||||||
(row) => row.map((cell) => {
|
|
||||||
if (typeof cell === "string") {
|
|
||||||
return cell;
|
|
||||||
}
|
|
||||||
return cell.data;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const formatted = (0, import_table.table)(tableData);
|
|
||||||
if (title) {
|
|
||||||
core.info(`
|
|
||||||
${title}`);
|
|
||||||
}
|
|
||||||
core.info(`
|
|
||||||
${formatted}
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
function separator(length = 50) {
|
|
||||||
const separatorText = "\u2500".repeat(length);
|
|
||||||
core.info(separatorText);
|
|
||||||
}
|
|
||||||
function ts() {
|
|
||||||
return isDebugEnabled() ? `[${(/* @__PURE__ */ new Date()).toISOString()}] ` : "";
|
|
||||||
}
|
|
||||||
var log = {
|
|
||||||
/** Print info message */
|
|
||||||
info: (...args2) => {
|
|
||||||
core.info(`${ts()}${formatArgs(args2)}`);
|
|
||||||
},
|
|
||||||
/** Print warning message */
|
|
||||||
warning: (...args2) => {
|
|
||||||
core.warning(`${ts()}${formatArgs(args2)}`);
|
|
||||||
},
|
|
||||||
/** Print error message */
|
|
||||||
error: (...args2) => {
|
|
||||||
core.error(`${ts()}${formatArgs(args2)}`);
|
|
||||||
},
|
|
||||||
/** Print success message */
|
|
||||||
success: (...args2) => {
|
|
||||||
core.info(`${ts()}\xBB ${formatArgs(args2)}`);
|
|
||||||
},
|
|
||||||
/** Print debug message (only if LOG_LEVEL=debug) */
|
|
||||||
debug: (...args2) => {
|
|
||||||
if (isDebugEnabled()) {
|
|
||||||
core.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] ${formatArgs(args2)}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/** Print a formatted box with text */
|
|
||||||
box,
|
|
||||||
/** Print a formatted table using the table package */
|
|
||||||
table: printTable,
|
|
||||||
/** Print a separator line */
|
|
||||||
separator,
|
|
||||||
/** Start a collapsed group (GitHub Actions) or regular group (local) */
|
|
||||||
startGroup: startGroup2,
|
|
||||||
/** End a collapsed group */
|
|
||||||
endGroup: endGroup2,
|
|
||||||
/** Run a callback within a collapsed group */
|
|
||||||
group,
|
|
||||||
/** Log tool call information to console with formatted output */
|
|
||||||
toolCall: ({ toolName, input }) => {
|
|
||||||
const inputFormatted = formatJsonValue(input);
|
|
||||||
const output = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
|
|
||||||
log.info(output.trimEnd());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
function formatJsonValue(value2) {
|
|
||||||
const compact = JSON.stringify(value2);
|
|
||||||
return compact.length > 80 || compact.includes("\n") ? JSON.stringify(value2, null, 2) : compact;
|
|
||||||
}
|
|
||||||
|
|
||||||
// utils/buildPullfrogFooter.ts
|
// utils/buildPullfrogFooter.ts
|
||||||
var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
||||||
var FROG_LOGO = `<a href="https://pullfrog.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.com/logos/frog-white-full-18px.png"><img src="https://pullfrog.com/logos/frog-green-full-18px.png" width="9px" height="9px" style="vertical-align: middle; " alt="Pullfrog"></picture></a>`;
|
var FROG_LOGO = `<a href="https://pullfrog.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.com/logos/frog-white-full-18px.png"><img src="https://pullfrog.com/logos/frog-green-full-18px.png" width="9px" height="9px" style="vertical-align: middle; " alt="Pullfrog"></picture></a>`;
|
||||||
@@ -41192,37 +41198,8 @@ var ReplyToReviewComment = type({
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
// utils/token.ts
|
|
||||||
var core3 = __toESM(require_core(), 1);
|
|
||||||
function getJobToken() {
|
|
||||||
const inputToken = core3.getInput("token");
|
|
||||||
if (inputToken) {
|
|
||||||
return inputToken;
|
|
||||||
}
|
|
||||||
const fallbackToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
||||||
if (fallbackToken) {
|
|
||||||
return fallbackToken;
|
|
||||||
}
|
|
||||||
throw new Error("token input is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
// utils/exitHandler.ts
|
|
||||||
function buildErrorCommentBody(params) {
|
|
||||||
const workflowRunLink = params.runId ? `[workflow run logs](https://github.com/${params.owner}/${params.repo}/actions/runs/${params.runId})` : "workflow run logs";
|
|
||||||
const errorMessage = params.isCancellation ? `This run was cancelled \u{1F6D1}
|
|
||||||
|
|
||||||
The workflow was cancelled before completion. Please check the ${workflowRunLink} for details.` : `This run croaked \u{1F635}
|
|
||||||
|
|
||||||
The workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
|
|
||||||
const footer = buildPullfrogFooter({
|
|
||||||
triggeredBy: true,
|
|
||||||
workflowRun: params.runId ? { owner: params.owner, repo: params.repo, runId: params.runId } : void 0
|
|
||||||
});
|
|
||||||
return `${errorMessage}${footer}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// utils/payload.ts
|
// utils/payload.ts
|
||||||
var core4 = __toESM(require_core(), 1);
|
var core3 = __toESM(require_core(), 1);
|
||||||
|
|
||||||
// external.ts
|
// external.ts
|
||||||
var agentsManifest = {
|
var agentsManifest = {
|
||||||
@@ -41258,7 +41235,7 @@ var Effort = type.enumerated("mini", "auto", "max");
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/pullfrog",
|
name: "@pullfrog/pullfrog",
|
||||||
version: "0.0.164",
|
version: "0.0.166",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -41278,7 +41255,8 @@ var package_default = {
|
|||||||
runtest: "node test/run.ts",
|
runtest: "node test/run.ts",
|
||||||
scratch: "node scratch.ts",
|
scratch: "node scratch.ts",
|
||||||
upDeps: "pnpm up --latest",
|
upDeps: "pnpm up --latest",
|
||||||
lock: "pnpm --ignore-workspace install --no-frozen-lockfile",
|
lock: "pnpm install --no-frozen-lockfile",
|
||||||
|
postinstall: "node scripts/generate-proxies.ts",
|
||||||
prepare: "cd .. && husky action/.husky"
|
prepare: "cd .. && husky action/.husky"
|
||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
@@ -41337,7 +41315,9 @@ var package_default = {
|
|||||||
types: "./dist/index.d.cts",
|
types: "./dist/index.d.cts",
|
||||||
import: "./dist/index.js",
|
import: "./dist/index.js",
|
||||||
require: "./dist/index.cjs"
|
require: "./dist/index.cjs"
|
||||||
}
|
},
|
||||||
|
"./internal": "./dist/internal.js",
|
||||||
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
packageManager: "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
|
packageManager: "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
|
||||||
};
|
};
|
||||||
@@ -41369,6 +41349,7 @@ var JsonPayload = type({
|
|||||||
version: "string",
|
version: "string",
|
||||||
"agent?": AgentName.or("undefined"),
|
"agent?": AgentName.or("undefined"),
|
||||||
prompt: "string",
|
prompt: "string",
|
||||||
|
"triggeringUser?": "string | undefined",
|
||||||
"eventInstructions?": "string",
|
"eventInstructions?": "string",
|
||||||
"repoInstructions?": "string",
|
"repoInstructions?": "string",
|
||||||
"event?": "object",
|
"event?": "object",
|
||||||
@@ -41389,7 +41370,7 @@ var Inputs = type({
|
|||||||
"cwd?": type.string.or("undefined")
|
"cwd?": type.string.or("undefined")
|
||||||
});
|
});
|
||||||
function resolvePromptInput() {
|
function resolvePromptInput() {
|
||||||
const prompt = core4.getInput("prompt", { required: true });
|
const prompt = core3.getInput("prompt", { required: true });
|
||||||
let parsed2;
|
let parsed2;
|
||||||
try {
|
try {
|
||||||
parsed2 = JSON.parse(prompt);
|
parsed2 = JSON.parse(prompt);
|
||||||
@@ -41404,8 +41385,35 @@ function resolvePromptInput() {
|
|||||||
return jsonPayload;
|
return jsonPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
// post.ts
|
// utils/token.ts
|
||||||
|
var core4 = __toESM(require_core(), 1);
|
||||||
|
function getJobToken() {
|
||||||
|
const inputToken = core4.getInput("token");
|
||||||
|
if (inputToken) {
|
||||||
|
return inputToken;
|
||||||
|
}
|
||||||
|
const fallbackToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
||||||
|
if (fallbackToken) {
|
||||||
|
return fallbackToken;
|
||||||
|
}
|
||||||
|
throw new Error("token input is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// utils/postCleanup.ts
|
||||||
var SHOULD_CHECK_REASON = true;
|
var SHOULD_CHECK_REASON = true;
|
||||||
|
function buildErrorCommentBody(params) {
|
||||||
|
const workflowRunLink = params.runId ? `[workflow run logs](https://github.com/${params.owner}/${params.repo}/actions/runs/${params.runId})` : "workflow run logs";
|
||||||
|
const errorMessage = params.isCancellation ? `This run was cancelled \u{1F6D1}
|
||||||
|
|
||||||
|
The workflow was cancelled before completion. Please check the ${workflowRunLink} for details.` : `This run croaked \u{1F635}
|
||||||
|
|
||||||
|
The workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
|
||||||
|
const footer = buildPullfrogFooter({
|
||||||
|
triggeredBy: true,
|
||||||
|
workflowRun: params.runId ? { owner: params.owner, repo: params.repo, runId: params.runId } : void 0
|
||||||
|
});
|
||||||
|
return `${errorMessage}${footer}`;
|
||||||
|
}
|
||||||
async function validateStuckProgressComment(promptInput, octokit, owner, repo) {
|
async function validateStuckProgressComment(promptInput, octokit, owner, repo) {
|
||||||
if (!promptInput?.progressCommentId) {
|
if (!promptInput?.progressCommentId) {
|
||||||
log.info("[post] no progressCommentId in prompt input, skipping cleanup");
|
log.info("[post] no progressCommentId in prompt input, skipping cleanup");
|
||||||
@@ -41427,11 +41435,12 @@ async function validateStuckProgressComment(promptInput, octokit, owner, repo) {
|
|||||||
return null;
|
return null;
|
||||||
} catch (error2) {
|
} catch (error2) {
|
||||||
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
||||||
log.error(`[post] failed to get comment ${commentId}: ${errorMessage}`);
|
log.info(`[post] failed to get comment ${commentId}: ${errorMessage}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function getIsCancelled(params) {
|
async function getIsCancelled(params) {
|
||||||
|
if (!params.runIdStr) return false;
|
||||||
try {
|
try {
|
||||||
const { data: jobs } = await params.octokit.rest.actions.listJobsForWorkflowRun({
|
const { data: jobs } = await params.octokit.rest.actions.listJobsForWorkflowRun({
|
||||||
owner: params.repoContext.owner,
|
owner: params.repoContext.owner,
|
||||||
@@ -41453,7 +41462,7 @@ async function getIsCancelled(params) {
|
|||||||
}
|
}
|
||||||
log.info("[post] no cancellation found, assuming failure");
|
log.info("[post] no cancellation found, assuming failure");
|
||||||
} catch (error2) {
|
} catch (error2) {
|
||||||
log.warning(
|
log.info(
|
||||||
`[post] failed to get job status: ${error2 instanceof Error ? error2.message : String(error2)}`
|
`[post] failed to get job status: ${error2 instanceof Error ? error2.message : String(error2)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -41462,13 +41471,12 @@ async function getIsCancelled(params) {
|
|||||||
async function runPostCleanup() {
|
async function runPostCleanup() {
|
||||||
log.info("\xBB [post] starting post cleanup");
|
log.info("\xBB [post] starting post cleanup");
|
||||||
const runIdStr = process.env.GITHUB_RUN_ID;
|
const runIdStr = process.env.GITHUB_RUN_ID;
|
||||||
if (!runIdStr) return log.info("\xBB [post] no GITHUB_RUN_ID available, skipping cleanup");
|
|
||||||
let promptInput = null;
|
let promptInput = null;
|
||||||
try {
|
try {
|
||||||
const resolved = resolvePromptInput();
|
const resolved = resolvePromptInput();
|
||||||
if (typeof resolved !== "string") promptInput = resolved;
|
if (typeof resolved !== "string") promptInput = resolved;
|
||||||
} catch (error2) {
|
} catch (error2) {
|
||||||
log.warning(
|
log.info(
|
||||||
`[post] failed to resolve prompt input: ${error2 instanceof Error ? error2.message : String(error2)}`
|
`[post] failed to resolve prompt input: ${error2 instanceof Error ? error2.message : String(error2)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -41499,19 +41507,18 @@ async function runPostCleanup() {
|
|||||||
log.info("\xBB [post] successfully updated progress comment");
|
log.info("\xBB [post] successfully updated progress comment");
|
||||||
} catch (error2) {
|
} catch (error2) {
|
||||||
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
||||||
log.error(`[post] failed to update comment: ${errorMessage}`);
|
log.info(`[post] failed to update comment: ${errorMessage}`);
|
||||||
}
|
|
||||||
}
|
|
||||||
async function run() {
|
|
||||||
try {
|
|
||||||
await runPostCleanup();
|
|
||||||
} catch (error2) {
|
|
||||||
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
||||||
log.error(`[post] unexpected error: ${message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// post.ts
|
||||||
log.debug(`[post] script started at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
log.debug(`[post] script started at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
||||||
await run();
|
try {
|
||||||
|
await runPostCleanup();
|
||||||
|
} catch (error2) {
|
||||||
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
||||||
|
log.error(`[post] unexpected error: ${message}`);
|
||||||
|
}
|
||||||
/*! Bundled license information:
|
/*! Bundled license information:
|
||||||
|
|
||||||
undici/lib/fetch/body.js:
|
undici/lib/fetch/body.js:
|
||||||
|
|||||||
@@ -6,180 +6,14 @@
|
|||||||
* Searches for Pullfrog comment via GitHub API and updates if stuck on "Leaping into action".
|
* Searches for Pullfrog comment via GitHub API and updates if stuck on "Leaping into action".
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { LEAPING_INTO_ACTION_PREFIX } from "./mcp/comment.ts";
|
|
||||||
import { log } from "./utils/cli.ts";
|
import { log } from "./utils/cli.ts";
|
||||||
import { buildErrorCommentBody } from "./utils/exitHandler.ts";
|
import { runPostCleanup } from "./utils/postCleanup.ts";
|
||||||
import { createOctokit, parseRepoContext } from "./utils/github.ts";
|
|
||||||
import { type ResolvedPromptInput, resolvePromptInput } from "./utils/payload.ts";
|
|
||||||
import { getJobToken } from "./utils/token.ts";
|
|
||||||
|
|
||||||
type JsonPromptInput = Extract<ResolvedPromptInput, object>; // not string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controls whether the script should check the reason for the workflow termination.
|
|
||||||
* It can be either canceled or failed.
|
|
||||||
* YAML file cannot supply it (not in ENV), so an extra request is required to check it.
|
|
||||||
* */
|
|
||||||
const SHOULD_CHECK_REASON = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that the progress comment is stuck on "Leaping into action"
|
|
||||||
* Fetches the comment by ID and checks if it starts with LEAPING_INTO_ACTION_PREFIX
|
|
||||||
* Returns the comment ID if stuck, null otherwise
|
|
||||||
*/
|
|
||||||
async function validateStuckProgressComment(
|
|
||||||
promptInput: JsonPromptInput | null,
|
|
||||||
octokit: ReturnType<typeof createOctokit>,
|
|
||||||
owner: string,
|
|
||||||
repo: string
|
|
||||||
): Promise<number | null> {
|
|
||||||
if (!promptInput?.progressCommentId) {
|
|
||||||
log.info("[post] no progressCommentId in prompt input, skipping cleanup");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const commentId = parseInt(promptInput.progressCommentId, 10);
|
|
||||||
log.info(`[post] validating progressCommentId from prompt input: ${commentId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data: comment } = await octokit.rest.issues.getComment({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
comment_id: commentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// check if comment is stuck on "Leaping into action"
|
|
||||||
if (comment.body?.startsWith(LEAPING_INTO_ACTION_PREFIX)) {
|
|
||||||
log.info(`[post] comment ${commentId} is stuck on "Leaping into action"`);
|
|
||||||
return commentId;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(`[post] comment ${commentId} is not stuck (already updated or different content)`);
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
log.error(`[post] failed to get comment ${commentId}: ${errorMessage}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect if the workflow or its steps is cancelled.
|
|
||||||
* While the job is still in_progress, the individual steps may have their conclusions set.
|
|
||||||
*/
|
|
||||||
async function getIsCancelled(params: {
|
|
||||||
repoContext: ReturnType<typeof parseRepoContext>;
|
|
||||||
octokit: ReturnType<typeof createOctokit>;
|
|
||||||
runIdStr: string;
|
|
||||||
}): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const { data: jobs } = await params.octokit.rest.actions.listJobsForWorkflowRun({
|
|
||||||
owner: params.repoContext.owner,
|
|
||||||
repo: params.repoContext.name,
|
|
||||||
run_id: Number.parseInt(params.runIdStr, 10),
|
|
||||||
});
|
|
||||||
|
|
||||||
// find current job by matching GITHUB_JOB env var
|
|
||||||
// Note: GITHUB_JOB is the job ID (yaml key), but job.name is the display name
|
|
||||||
// For matrix jobs, the name includes matrix values like "build (ubuntu-latest, node-18)"
|
|
||||||
// So we match jobs that START with the job ID
|
|
||||||
const currentJobName = process.env.GITHUB_JOB;
|
|
||||||
const currentJob = currentJobName
|
|
||||||
? jobs.jobs.find((j) => j.name === currentJobName || j.name.startsWith(`${currentJobName} (`))
|
|
||||||
: jobs.jobs[0]; // fallback to first job
|
|
||||||
|
|
||||||
if (!currentJob) {
|
|
||||||
log.warning("[post] could not find current job");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(`[post] job status: ${currentJob.status}, conclusion: ${currentJob.conclusion}`);
|
|
||||||
if (currentJob.conclusion === "cancelled") return true; // whole job explicit cancellation
|
|
||||||
|
|
||||||
// but if it's still null, check steps for cancellation:
|
|
||||||
const cancelledStep = currentJob.steps?.find((step) => step.conclusion === "cancelled");
|
|
||||||
if (cancelledStep) {
|
|
||||||
log.info(`[post] found cancelled step: ${cancelledStep.name}`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
log.info("[post] no cancellation found, assuming failure");
|
|
||||||
} catch (error) {
|
|
||||||
log.warning(
|
|
||||||
`[post] failed to get job status: ${error instanceof Error ? error.message : String(error)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false; // assuming failure
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runPostCleanup(): Promise<void> {
|
|
||||||
log.info("» [post] starting post cleanup");
|
|
||||||
|
|
||||||
const runIdStr = process.env.GITHUB_RUN_ID;
|
|
||||||
|
|
||||||
if (!runIdStr) return log.info("» [post] no GITHUB_RUN_ID available, skipping cleanup");
|
|
||||||
|
|
||||||
// resolve prompt input once and use it for both issue number and comment ID extraction
|
|
||||||
// only use the object form (JSON payload), not plain string prompts
|
|
||||||
let promptInput: JsonPromptInput | null = null;
|
|
||||||
try {
|
|
||||||
const resolved = resolvePromptInput();
|
|
||||||
if (typeof resolved !== "string") promptInput = resolved;
|
|
||||||
} catch (error) {
|
|
||||||
log.warning(
|
|
||||||
`[post] failed to resolve prompt input: ${error instanceof Error ? error.message : String(error)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get job token for API calls
|
|
||||||
const token = getJobToken();
|
|
||||||
const repoContext = parseRepoContext();
|
|
||||||
const octokit = createOctokit(token);
|
|
||||||
|
|
||||||
// validate that progressCommentId from prompt input is stuck on "Leaping into action"
|
|
||||||
const commentId = await validateStuckProgressComment(
|
|
||||||
promptInput,
|
|
||||||
octokit,
|
|
||||||
repoContext.owner,
|
|
||||||
repoContext.name
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!commentId) return log.info("» [post] no stuck progress comment to update, skipping cleanup");
|
|
||||||
|
|
||||||
log.info(`» [post] validated stuck comment: ${commentId}, updating with error message`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const body = buildErrorCommentBody({
|
|
||||||
owner: repoContext.owner,
|
|
||||||
repo: repoContext.name,
|
|
||||||
runId: runIdStr,
|
|
||||||
isCancellation: SHOULD_CHECK_REASON
|
|
||||||
? await getIsCancelled({ octokit, repoContext, runIdStr })
|
|
||||||
: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await octokit.rest.issues.updateComment({
|
|
||||||
owner: repoContext.owner,
|
|
||||||
repo: repoContext.name,
|
|
||||||
comment_id: commentId,
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
|
|
||||||
log.info("» [post] successfully updated progress comment");
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
log.error(`[post] failed to update comment: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await runPostCleanup();
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
log.error(`[post] unexpected error: ${message}`);
|
|
||||||
// don't fail the post script - best effort cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug(`[post] script started at ${new Date().toISOString()}`);
|
log.debug(`[post] script started at ${new Date().toISOString()}`);
|
||||||
await run();
|
try {
|
||||||
|
await runPostCleanup();
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
log.error(`[post] unexpected error: ${message}`);
|
||||||
|
// don't fail the post script - best effort cleanup
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const proxies = [
|
||||||
|
{ dest: "dist/index.js", source: "../index.ts" },
|
||||||
|
{ dest: "dist/internal.js", source: "../internal/index.ts" },
|
||||||
|
];
|
||||||
|
|
||||||
|
mkdirSync("dist", { recursive: true });
|
||||||
|
|
||||||
|
for (const proxy of proxies) {
|
||||||
|
writeFileSync(proxy.dest, `export * from "${proxy.source}";\n`);
|
||||||
|
writeFileSync(proxy.dest.replace(/\.js$/, ".d.ts"), `export * from "${proxy.source}";\n`);
|
||||||
|
}
|
||||||
+2
-2
@@ -4,7 +4,7 @@ import { dirname, join } from "node:path";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { parse } from "yaml";
|
import { parse } from "yaml";
|
||||||
import { agentsManifest } from "../external.ts";
|
import { agentsManifest, type WorkflowPermissions } from "../external.ts";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const actionDir = join(__dirname, "..");
|
const actionDir = join(__dirname, "..");
|
||||||
@@ -13,7 +13,7 @@ const rootDir = join(actionDir, "..");
|
|||||||
type WorkflowJob = {
|
type WorkflowJob = {
|
||||||
"runs-on": string;
|
"runs-on": string;
|
||||||
"timeout-minutes"?: number;
|
"timeout-minutes"?: number;
|
||||||
permissions?: Record<string, string>;
|
permissions?: WorkflowPermissions;
|
||||||
strategy?: { "fail-fast": boolean; matrix: Record<string, string[]> };
|
strategy?: { "fail-fast": boolean; matrix: Record<string, string[]> };
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
steps?: unknown[];
|
steps?: unknown[];
|
||||||
|
|||||||
+1
-6
@@ -5,11 +5,7 @@ import { config } from "dotenv";
|
|||||||
import { runInDocker } from "../utils/docker.ts";
|
import { runInDocker } from "../utils/docker.ts";
|
||||||
import { ensureGitHubToken } from "../utils/github.ts";
|
import { ensureGitHubToken } from "../utils/github.ts";
|
||||||
import { isInsideDocker } from "../utils/globals.ts";
|
import { isInsideDocker } from "../utils/globals.ts";
|
||||||
import {
|
import { killTrackedChildren, setSignalHandler } from "../utils/subprocess.ts";
|
||||||
installSignalHandlers,
|
|
||||||
killTrackedChildren,
|
|
||||||
setSignalHandler,
|
|
||||||
} from "../utils/subprocess.ts";
|
|
||||||
import {
|
import {
|
||||||
type AgentResult,
|
type AgentResult,
|
||||||
agents,
|
agents,
|
||||||
@@ -482,7 +478,6 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSignalHandler(handleCancel);
|
setSignalHandler(handleCancel);
|
||||||
installSignalHandlers();
|
|
||||||
|
|
||||||
// run tests with limited concurrency to avoid overwhelming agent APIs
|
// run tests with limited concurrency to avoid overwhelming agent APIs
|
||||||
const maxConcurrency = 5;
|
const maxConcurrency = 5;
|
||||||
|
|||||||
+1
-3
@@ -5,7 +5,7 @@ import { dirname, join } from "node:path";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { agentsManifest } from "../external.ts";
|
import { agentsManifest } from "../external.ts";
|
||||||
import type { Inputs } from "../main.ts";
|
import type { Inputs } from "../main.ts";
|
||||||
import { installSignalHandlers, trackChild, untrackChild } from "../utils/subprocess.ts";
|
import { trackChild, untrackChild } from "../utils/subprocess.ts";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@@ -165,8 +165,6 @@ const DEFAULT_TEST_TIMEOUT = "10m";
|
|||||||
// run agent and stream output with prefix labels
|
// run agent and stream output with prefix labels
|
||||||
// note: activity timeout is enforced in action main and subprocess utils
|
// note: activity timeout is enforced in action main and subprocess utils
|
||||||
export async function runAgentStreaming(options: RunStreamingOptions): Promise<AgentResult> {
|
export async function runAgentStreaming(options: RunStreamingOptions): Promise<AgentResult> {
|
||||||
installSignalHandlers();
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
const prefix = getPrefix({ test: options.test, agent: options.agent });
|
const prefix = getPrefix({ test: options.test, agent: options.agent });
|
||||||
|
|||||||
+22
-106
@@ -1,119 +1,35 @@
|
|||||||
import { LEAPING_INTO_ACTION_PREFIX } from "../mcp/comment.ts";
|
import os from "node:os";
|
||||||
import type { ToolState } from "../mcp/server.ts";
|
|
||||||
import { buildPullfrogFooter } from "./buildPullfrogFooter.ts";
|
type ExitSignalHandler = (signal: "SIGINT" | "SIGTERM") => void | Promise<void>;
|
||||||
import { log } from "./cli.ts";
|
|
||||||
import { createOctokit, parseRepoContext } from "./github.ts";
|
const handlers = new Set<ExitSignalHandler>();
|
||||||
import { revokeGitHubInstallationToken } from "./token.ts";
|
let installed = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build error comment body with error message and footer
|
* Register a handler to run when the process receives SIGINT or SIGTERM.
|
||||||
|
* Returns a dispose function that removes the handler.
|
||||||
*/
|
*/
|
||||||
export function buildErrorCommentBody(params: {
|
export function onExitSignal(handler: ExitSignalHandler): () => void {
|
||||||
owner: string;
|
installSignalHandlers();
|
||||||
repo: string;
|
handlers.add(handler);
|
||||||
runId: string | undefined;
|
return () => {
|
||||||
isCancellation: boolean;
|
handlers.delete(handler);
|
||||||
}): string {
|
};
|
||||||
const workflowRunLink = params.runId
|
|
||||||
? `[workflow run logs](https://github.com/${params.owner}/${params.repo}/actions/runs/${params.runId})`
|
|
||||||
: "workflow run logs";
|
|
||||||
const errorMessage = params.isCancellation
|
|
||||||
? `This run was cancelled 🛑\n\nThe workflow was cancelled before completion. Please check the ${workflowRunLink} for details.`
|
|
||||||
: `This run croaked 😵\n\nThe workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
|
|
||||||
const footer = buildPullfrogFooter({
|
|
||||||
triggeredBy: true,
|
|
||||||
workflowRun: params.runId
|
|
||||||
? { owner: params.owner, repo: params.repo, runId: params.runId }
|
|
||||||
: undefined,
|
|
||||||
});
|
|
||||||
return `${errorMessage}${footer}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let cleanupFn: ((isCancellation: boolean) => Promise<void>) | undefined;
|
function installSignalHandlers(): void {
|
||||||
|
if (installed) return;
|
||||||
|
installed = true;
|
||||||
|
|
||||||
export function setupExitHandler(toolState: ToolState): void {
|
async function handleSignal(signal: "SIGINT" | "SIGTERM") {
|
||||||
let hasCleanedUp = false;
|
await Promise.allSettled([...handlers].map((h) => Promise.try(h, signal)));
|
||||||
|
exitWithSignal(signal);
|
||||||
async function cleanup(isCancellation: boolean): Promise<void> {
|
|
||||||
if (hasCleanedUp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
hasCleanedUp = true;
|
|
||||||
|
|
||||||
const token = process.env.GITHUB_TOKEN;
|
|
||||||
const commentId = toolState.progressCommentId;
|
|
||||||
const wasUpdated = toolState.wasUpdated === true;
|
|
||||||
|
|
||||||
// update progress comment if it was never updated (still shows "leaping into action")
|
|
||||||
if (token && commentId && !wasUpdated) {
|
|
||||||
try {
|
|
||||||
const repoContext = parseRepoContext();
|
|
||||||
const octokit = createOctokit(token);
|
|
||||||
|
|
||||||
const existingComment = await octokit.rest.issues.getComment({
|
|
||||||
owner: repoContext.owner,
|
|
||||||
repo: repoContext.name,
|
|
||||||
comment_id: commentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const commentBody = existingComment.data.body || "";
|
|
||||||
|
|
||||||
// only update if comment still shows the initial "leaping into action" message
|
|
||||||
if (commentBody.startsWith(LEAPING_INTO_ACTION_PREFIX)) {
|
|
||||||
const runId = process.env.GITHUB_RUN_ID;
|
|
||||||
|
|
||||||
const body = buildErrorCommentBody({
|
|
||||||
owner: repoContext.owner,
|
|
||||||
repo: repoContext.name,
|
|
||||||
runId,
|
|
||||||
isCancellation,
|
|
||||||
});
|
|
||||||
|
|
||||||
await octokit.rest.issues.updateComment({
|
|
||||||
owner: repoContext.owner,
|
|
||||||
repo: repoContext.name,
|
|
||||||
comment_id: commentId,
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
|
|
||||||
log.info("» updated progress comment with error message");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore errors during cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// revoke token
|
|
||||||
if (token) {
|
|
||||||
try {
|
|
||||||
await revokeGitHubInstallationToken(token);
|
|
||||||
log.debug("» installation token revoked");
|
|
||||||
} catch {
|
|
||||||
// ignore errors during cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// store cleanup function for runCleanup()
|
|
||||||
cleanupFn = cleanup;
|
|
||||||
|
|
||||||
// handle cancellation signals
|
|
||||||
function handleSignal(): void {
|
|
||||||
log.info("» workflow cancelled, cleaning up...");
|
|
||||||
cleanup(true).finally(() => process.exit(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on("SIGINT", handleSignal);
|
process.on("SIGINT", handleSignal);
|
||||||
process.on("SIGTERM", handleSignal);
|
process.on("SIGTERM", handleSignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function exitWithSignal(signal: "SIGINT" | "SIGTERM") {
|
||||||
* Run cleanup explicitly. Called from entry.ts in finally block.
|
process.exit(128 + os.constants.signals[signal]);
|
||||||
*/
|
|
||||||
export async function runCleanup(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await cleanupFn?.(false);
|
|
||||||
} catch {
|
|
||||||
// ignore errors during cleanup
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -161,7 +161,7 @@ export function $git(
|
|||||||
|
|
||||||
if (result.status !== 0) {
|
if (result.status !== 0) {
|
||||||
const stderr = result.stderr?.trim() ?? "";
|
const stderr = result.stderr?.trim() ?? "";
|
||||||
log.error(`git ${subcommand} failed: ${stderr}`);
|
log.info(`git ${subcommand} failed: ${stderr}`);
|
||||||
throw new Error(`git ${subcommand} failed: ${stderr}`);
|
throw new Error(`git ${subcommand} failed: ${stderr}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-12
@@ -6,8 +6,17 @@ import * as core from "@actions/core";
|
|||||||
import { table } from "table";
|
import { table } from "table";
|
||||||
import { isGitHubActions, isInsideDocker } from "./globals.ts";
|
import { isGitHubActions, isInsideDocker } from "./globals.ts";
|
||||||
|
|
||||||
const isDebugEnabled = () =>
|
const isRunnerDebugEnabled = () => core.isDebug();
|
||||||
process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true" || core.isDebug();
|
|
||||||
|
const isLocalDebugEnabled = () =>
|
||||||
|
process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true";
|
||||||
|
|
||||||
|
const isDebugEnabled = () => isLocalDebugEnabled() || isRunnerDebugEnabled();
|
||||||
|
|
||||||
|
/** timestamp prefix for debug mode — empty string when debug is off */
|
||||||
|
function ts(): string {
|
||||||
|
return isDebugEnabled() ? `[${new Date().toISOString()}] ` : "";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format arguments into a single string for logging
|
* Format arguments into a single string for logging
|
||||||
@@ -206,23 +215,18 @@ function separator(length: number = 50): void {
|
|||||||
/**
|
/**
|
||||||
* Main logging utility object - import this once and access all utilities
|
* Main logging utility object - import this once and access all utilities
|
||||||
*/
|
*/
|
||||||
/** timestamp prefix for debug mode — empty string when debug is off */
|
|
||||||
function ts(): string {
|
|
||||||
return isDebugEnabled() ? `[${new Date().toISOString()}] ` : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const log = {
|
export const log = {
|
||||||
/** Print info message */
|
/** Print info message */
|
||||||
info: (...args: unknown[]): void => {
|
info: (...args: unknown[]): void => {
|
||||||
core.info(`${ts()}${formatArgs(args)}`);
|
core.info(`${ts()}${formatArgs(args)}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Print warning message */
|
/** Print a warning message. Use only for warnings that should be displayed in the job summary. */
|
||||||
warning: (...args: unknown[]): void => {
|
warning: (...args: unknown[]): void => {
|
||||||
core.warning(`${ts()}${formatArgs(args)}`);
|
core.warning(`${ts()}${formatArgs(args)}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Print error message */
|
/** Print an error message. Use only for errors that should be displayed in the job summary. */
|
||||||
error: (...args: unknown[]): void => {
|
error: (...args: unknown[]): void => {
|
||||||
core.error(`${ts()}${formatArgs(args)}`);
|
core.error(`${ts()}${formatArgs(args)}`);
|
||||||
},
|
},
|
||||||
@@ -232,10 +236,14 @@ export const log = {
|
|||||||
core.info(`${ts()}» ${formatArgs(args)}`);
|
core.info(`${ts()}» ${formatArgs(args)}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Print debug message (only if LOG_LEVEL=debug) */
|
/** Print debug message (only when debug mode is enabled) */
|
||||||
debug: (...args: unknown[]): void => {
|
debug: (...args: unknown[]): void => {
|
||||||
if (isDebugEnabled()) {
|
if (isRunnerDebugEnabled()) {
|
||||||
core.info(`[${new Date().toISOString()}] [DEBUG] ${formatArgs(args)}`);
|
core.debug(formatArgs(args));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isLocalDebugEnabled()) {
|
||||||
|
core.info(`${ts()}[DEBUG] ${formatArgs(args)}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const JsonPayload = type({
|
|||||||
version: "string",
|
version: "string",
|
||||||
"agent?": AgentName.or("undefined"),
|
"agent?": AgentName.or("undefined"),
|
||||||
prompt: "string",
|
prompt: "string",
|
||||||
|
"triggeringUser?": "string | undefined",
|
||||||
"eventInstructions?": "string",
|
"eventInstructions?": "string",
|
||||||
"repoInstructions?": "string",
|
"repoInstructions?": "string",
|
||||||
"event?": "object",
|
"event?": "object",
|
||||||
@@ -108,6 +109,11 @@ function resolveNonPromptInputs() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPullfrog = (actor: string | null | undefined): boolean => {
|
||||||
|
actor = actor?.replace("[bot]", "");
|
||||||
|
return !!actor && (actor === "pullfrog" || actor === "pullfrogdev");
|
||||||
|
};
|
||||||
|
|
||||||
export function resolvePayload(
|
export function resolvePayload(
|
||||||
resolvedPromptInput: ResolvedPromptInput,
|
resolvedPromptInput: ResolvedPromptInput,
|
||||||
repoSettings: RepoSettings
|
repoSettings: RepoSettings
|
||||||
@@ -161,6 +167,10 @@ export function resolvePayload(
|
|||||||
version: jsonPayload?.version ?? packageJson.version,
|
version: jsonPayload?.version ?? packageJson.version,
|
||||||
agent: resolvedAgent,
|
agent: resolvedAgent,
|
||||||
prompt,
|
prompt,
|
||||||
|
triggeringUser:
|
||||||
|
jsonPayload?.triggeringUser ??
|
||||||
|
// it's not a common use case but GITHUB_ACTOR can be a user when the workflow is manually triggered by a user through GitHub Actions UI
|
||||||
|
(!isPullfrog(process.env.GITHUB_ACTOR) ? process.env.GITHUB_ACTOR : undefined),
|
||||||
eventInstructions: jsonPayload?.eventInstructions,
|
eventInstructions: jsonPayload?.eventInstructions,
|
||||||
repoInstructions: jsonPayload?.repoInstructions,
|
repoInstructions: jsonPayload?.repoInstructions,
|
||||||
event,
|
event,
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import { LEAPING_INTO_ACTION_PREFIX } from "../mcp/comment.ts";
|
||||||
|
import { buildPullfrogFooter } from "./buildPullfrogFooter.ts";
|
||||||
|
import { log } from "./cli.ts";
|
||||||
|
import { createOctokit, parseRepoContext } from "./github.ts";
|
||||||
|
import { type ResolvedPromptInput, resolvePromptInput } from "./payload.ts";
|
||||||
|
import { getJobToken } from "./token.ts";
|
||||||
|
|
||||||
|
type JsonPromptInput = Extract<ResolvedPromptInput, object>; // not string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls whether the script should check the reason for the workflow termination.
|
||||||
|
* It can be either canceled or failed.
|
||||||
|
* YAML file cannot supply it (not in ENV), so an extra request is required to check it.
|
||||||
|
* */
|
||||||
|
const SHOULD_CHECK_REASON = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build error comment body with error message and footer
|
||||||
|
*/
|
||||||
|
function buildErrorCommentBody(params: {
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
runId: string | undefined;
|
||||||
|
isCancellation: boolean;
|
||||||
|
}): string {
|
||||||
|
const workflowRunLink = params.runId
|
||||||
|
? `[workflow run logs](https://github.com/${params.owner}/${params.repo}/actions/runs/${params.runId})`
|
||||||
|
: "workflow run logs";
|
||||||
|
const errorMessage = params.isCancellation
|
||||||
|
? `This run was cancelled 🛑\n\nThe workflow was cancelled before completion. Please check the ${workflowRunLink} for details.`
|
||||||
|
: `This run croaked 😵\n\nThe workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
|
||||||
|
const footer = buildPullfrogFooter({
|
||||||
|
triggeredBy: true,
|
||||||
|
workflowRun: params.runId
|
||||||
|
? { owner: params.owner, repo: params.repo, runId: params.runId }
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
return `${errorMessage}${footer}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that the progress comment is stuck on "Leaping into action"
|
||||||
|
* Fetches the comment by ID and checks if it starts with LEAPING_INTO_ACTION_PREFIX
|
||||||
|
* Returns the comment ID if stuck, null otherwise
|
||||||
|
*/
|
||||||
|
async function validateStuckProgressComment(
|
||||||
|
promptInput: JsonPromptInput | null,
|
||||||
|
octokit: ReturnType<typeof createOctokit>,
|
||||||
|
owner: string,
|
||||||
|
repo: string
|
||||||
|
): Promise<number | null> {
|
||||||
|
if (!promptInput?.progressCommentId) {
|
||||||
|
log.info("[post] no progressCommentId in prompt input, skipping cleanup");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commentId = parseInt(promptInput.progressCommentId, 10);
|
||||||
|
log.info(`[post] validating progressCommentId from prompt input: ${commentId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: comment } = await octokit.rest.issues.getComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: commentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// check if comment is stuck on "Leaping into action"
|
||||||
|
if (comment.body?.startsWith(LEAPING_INTO_ACTION_PREFIX)) {
|
||||||
|
log.info(`[post] comment ${commentId} is stuck on "Leaping into action"`);
|
||||||
|
return commentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`[post] comment ${commentId} is not stuck (already updated or different content)`);
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
log.info(`[post] failed to get comment ${commentId}: ${errorMessage}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if the workflow or its steps is cancelled.
|
||||||
|
* While the job is still in_progress, the individual steps may have their conclusions set.
|
||||||
|
*/
|
||||||
|
async function getIsCancelled(params: {
|
||||||
|
repoContext: ReturnType<typeof parseRepoContext>;
|
||||||
|
octokit: ReturnType<typeof createOctokit>;
|
||||||
|
runIdStr: string | undefined;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
if (!params.runIdStr) return false; // can't check without a run ID — assume failure
|
||||||
|
try {
|
||||||
|
const { data: jobs } = await params.octokit.rest.actions.listJobsForWorkflowRun({
|
||||||
|
owner: params.repoContext.owner,
|
||||||
|
repo: params.repoContext.name,
|
||||||
|
run_id: Number.parseInt(params.runIdStr, 10),
|
||||||
|
});
|
||||||
|
|
||||||
|
// find current job by matching GITHUB_JOB env var
|
||||||
|
// Note: GITHUB_JOB is the job ID (yaml key), but job.name is the display name
|
||||||
|
// For matrix jobs, the name includes matrix values like "build (ubuntu-latest, node-18)"
|
||||||
|
// So we match jobs that START with the job ID
|
||||||
|
const currentJobName = process.env.GITHUB_JOB;
|
||||||
|
const currentJob = currentJobName
|
||||||
|
? jobs.jobs.find((j) => j.name === currentJobName || j.name.startsWith(`${currentJobName} (`))
|
||||||
|
: jobs.jobs[0]; // fallback to first job
|
||||||
|
|
||||||
|
if (!currentJob) {
|
||||||
|
log.warning("[post] could not find current job");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`[post] job status: ${currentJob.status}, conclusion: ${currentJob.conclusion}`);
|
||||||
|
if (currentJob.conclusion === "cancelled") return true; // whole job explicit cancellation
|
||||||
|
|
||||||
|
// but if it's still null, check steps for cancellation:
|
||||||
|
const cancelledStep = currentJob.steps?.find((step) => step.conclusion === "cancelled");
|
||||||
|
if (cancelledStep) {
|
||||||
|
log.info(`[post] found cancelled step: ${cancelledStep.name}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
log.info("[post] no cancellation found, assuming failure");
|
||||||
|
} catch (error) {
|
||||||
|
log.info(
|
||||||
|
`[post] failed to get job status: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false; // assuming failure
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runPostCleanup(): Promise<void> {
|
||||||
|
log.info("» [post] starting post cleanup");
|
||||||
|
|
||||||
|
const runIdStr = process.env.GITHUB_RUN_ID;
|
||||||
|
|
||||||
|
// resolve prompt input once and use it for both issue number and comment ID extraction
|
||||||
|
// only use the object form (JSON payload), not plain string prompts
|
||||||
|
let promptInput: JsonPromptInput | null = null;
|
||||||
|
try {
|
||||||
|
const resolved = resolvePromptInput();
|
||||||
|
if (typeof resolved !== "string") promptInput = resolved;
|
||||||
|
} catch (error) {
|
||||||
|
log.info(
|
||||||
|
`[post] failed to resolve prompt input: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get job token for API calls
|
||||||
|
const token = getJobToken();
|
||||||
|
const repoContext = parseRepoContext();
|
||||||
|
const octokit = createOctokit(token);
|
||||||
|
|
||||||
|
// validate that progressCommentId from prompt input is stuck on "Leaping into action"
|
||||||
|
const commentId = await validateStuckProgressComment(
|
||||||
|
promptInput,
|
||||||
|
octokit,
|
||||||
|
repoContext.owner,
|
||||||
|
repoContext.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!commentId) return log.info("» [post] no stuck progress comment to update, skipping cleanup");
|
||||||
|
|
||||||
|
log.info(`» [post] validated stuck comment: ${commentId}, updating with error message`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = buildErrorCommentBody({
|
||||||
|
owner: repoContext.owner,
|
||||||
|
repo: repoContext.name,
|
||||||
|
runId: runIdStr,
|
||||||
|
isCancellation: SHOULD_CHECK_REASON
|
||||||
|
? await getIsCancelled({ octokit, repoContext, runIdStr })
|
||||||
|
: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await octokit.rest.issues.updateComment({
|
||||||
|
owner: repoContext.owner,
|
||||||
|
repo: repoContext.name,
|
||||||
|
comment_id: commentId,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("» [post] successfully updated progress comment");
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
log.info(`[post] failed to update comment: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-3
@@ -37,9 +37,7 @@ export async function retry<T>(fn: () => Promise<T>, options: RetryOptions = {})
|
|||||||
}
|
}
|
||||||
|
|
||||||
const delay = delayMs * attempt;
|
const delay = delayMs * attempt;
|
||||||
log.warning(
|
log.info(`» ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`);
|
||||||
`» ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`
|
|
||||||
);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -121,9 +121,7 @@ export async function setupGit(params: SetupGitParams): Promise<void> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If git config fails, log warning but don't fail the action
|
// If git config fails, log warning but don't fail the action
|
||||||
// This can happen if we're not in a git repo or git isn't available
|
// This can happen if we're not in a git repo or git isn't available
|
||||||
log.warning(
|
log.info(`Failed to set git config: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
`Failed to set git config: ${error instanceof Error ? error.message : String(error)}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. setup authentication
|
// 2. setup authentication
|
||||||
|
|||||||
+19
-27
@@ -2,6 +2,7 @@ import { type ChildProcess, spawn as nodeSpawn } from "node:child_process";
|
|||||||
import { performance } from "node:perf_hooks";
|
import { performance } from "node:perf_hooks";
|
||||||
import { DEFAULT_ACTIVITY_CHECK_INTERVAL_MS, DEFAULT_ACTIVITY_TIMEOUT_MS } from "./activity.ts";
|
import { DEFAULT_ACTIVITY_CHECK_INTERVAL_MS, DEFAULT_ACTIVITY_TIMEOUT_MS } from "./activity.ts";
|
||||||
import { log } from "./cli.ts";
|
import { log } from "./cli.ts";
|
||||||
|
import { onExitSignal } from "./exitHandler.ts";
|
||||||
|
|
||||||
export type TrackChildOptions = {
|
export type TrackChildOptions = {
|
||||||
child: ChildProcess;
|
child: ChildProcess;
|
||||||
@@ -18,6 +19,9 @@ let externalSignalHandler: SignalHandler | null = null;
|
|||||||
|
|
||||||
// track a child process for cleanup on Ctrl+C
|
// track a child process for cleanup on Ctrl+C
|
||||||
export function trackChild(options: TrackChildOptions): void {
|
export function trackChild(options: TrackChildOptions): void {
|
||||||
|
// the signal handler cleans up all tracked children
|
||||||
|
// so we only have to install it once some child gets tracked
|
||||||
|
installSignalHandler();
|
||||||
activeChildren.set(options.child, options.killGroup ?? false);
|
activeChildren.set(options.child, options.killGroup ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,8 +36,7 @@ export function setSignalHandler(handler: SignalHandler | null): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// kill all tracked children without exiting
|
// kill all tracked children without exiting
|
||||||
export function killTrackedChildren(): number {
|
export function killTrackedChildren() {
|
||||||
const count = activeChildren.size;
|
|
||||||
for (const entry of activeChildren) {
|
for (const entry of activeChildren) {
|
||||||
const child = entry[0];
|
const child = entry[0];
|
||||||
const killGroup = entry[1];
|
const killGroup = entry[1];
|
||||||
@@ -47,35 +50,24 @@ export function killTrackedChildren(): number {
|
|||||||
}
|
}
|
||||||
child.kill("SIGKILL");
|
child.kill("SIGKILL");
|
||||||
}
|
}
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanup handler for SIGINT/SIGTERM - kills all tracked children
|
|
||||||
function cleanupAndExit(signal: string): void {
|
|
||||||
const count = killTrackedChildren();
|
|
||||||
if (count > 0) {
|
|
||||||
log.info(`» received ${signal}, killing ${count} subprocess(es)...`);
|
|
||||||
}
|
|
||||||
// force exit after a short delay if process is stuck
|
|
||||||
setTimeout(() => process.exit(1), 500).unref();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSignal(signal: NodeJS.Signals): void {
|
|
||||||
if (externalSignalHandler) {
|
|
||||||
externalSignalHandler(signal);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cleanupAndExit(signal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// install signal handlers once (call early in process lifecycle)
|
// install signal handlers once (call early in process lifecycle)
|
||||||
let handlersInstalled = false;
|
let handlersInstalled = false;
|
||||||
export function installSignalHandlers(): void {
|
function installSignalHandler(): void {
|
||||||
if (handlersInstalled) return;
|
if (handlersInstalled) return;
|
||||||
handlersInstalled = true;
|
handlersInstalled = true;
|
||||||
process.on("SIGINT", () => handleSignal("SIGINT"));
|
onExitSignal((signal) => {
|
||||||
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
if (externalSignalHandler) {
|
||||||
|
externalSignalHandler(signal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const count = activeChildren.size;
|
||||||
|
if (count > 0) {
|
||||||
|
log.info(`» received ${signal}, killing ${count} subprocess(es)...`);
|
||||||
|
}
|
||||||
|
killTrackedChildren();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpawnOptions {
|
export interface SpawnOptions {
|
||||||
@@ -106,7 +98,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
const { cmd, args, env, input, timeout, cwd, stdio, onStdout, onStderr } = options;
|
const { cmd, args, env, input, timeout, cwd, stdio, onStdout, onStderr } = options;
|
||||||
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
const activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
||||||
|
|
||||||
installSignalHandlers();
|
installSignalHandler();
|
||||||
|
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
let stdoutBuffer = "";
|
let stdoutBuffer = "";
|
||||||
@@ -157,7 +149,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
|||||||
if (idleMs > activityTimeoutMs) {
|
if (idleMs > activityTimeoutMs) {
|
||||||
isActivityTimedOut = true;
|
isActivityTimedOut = true;
|
||||||
const idleSec = Math.round(idleMs / 1000);
|
const idleSec = Math.round(idleMs / 1000);
|
||||||
log.error(`no output for ${idleSec}s from pid=${child.pid} (${cmd}), killing process`);
|
log.info(`no output for ${idleSec}s from pid=${child.pid} (${cmd}), killing process`);
|
||||||
child.kill("SIGKILL");
|
child.kill("SIGKILL");
|
||||||
clearInterval(activityCheckIntervalId);
|
clearInterval(activityCheckIntervalId);
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-7
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import type { PushPermission } from "../external.ts";
|
import type { PushPermission } from "../external.ts";
|
||||||
import { log } from "./cli.ts";
|
import { log } from "./cli.ts";
|
||||||
|
import { onExitSignal } from "./exitHandler.ts";
|
||||||
import { acquireNewToken } from "./github.ts";
|
import { acquireNewToken } from "./github.ts";
|
||||||
import { isGitHubActions } from "./globals.ts";
|
import { isGitHubActions } from "./globals.ts";
|
||||||
|
|
||||||
@@ -131,18 +132,35 @@ export async function resolveTokens(params: ResolveTokensParams): Promise<TokenR
|
|||||||
const revertGithubToken = setEnvironmentVariable("GITHUB_TOKEN", mcpToken);
|
const revertGithubToken = setEnvironmentVariable("GITHUB_TOKEN", mcpToken);
|
||||||
mcpTokenValue = mcpToken;
|
mcpTokenValue = mcpToken;
|
||||||
|
|
||||||
return {
|
let disposingRef: PromiseWithResolvers<void> | undefined;
|
||||||
gitToken,
|
|
||||||
mcpToken,
|
const dispose = async () => {
|
||||||
async [Symbol.asyncDispose]() {
|
if (disposingRef) {
|
||||||
|
// this can happen if the signal arrives when disposing tokens
|
||||||
|
// we make sure to wait for the current dispose to complete
|
||||||
|
return disposingRef.promise;
|
||||||
|
}
|
||||||
|
disposingRef = Promise.withResolvers();
|
||||||
|
try {
|
||||||
mcpTokenValue = undefined;
|
mcpTokenValue = undefined;
|
||||||
revertGithubToken();
|
revertGithubToken();
|
||||||
// revoke both tokens
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
revokeGitHubInstallationToken(gitToken),
|
revokeGitHubInstallationToken(gitToken),
|
||||||
revokeGitHubInstallationToken(mcpToken),
|
revokeGitHubInstallationToken(mcpToken),
|
||||||
]);
|
]);
|
||||||
},
|
} finally {
|
||||||
|
removeSignalHandler();
|
||||||
|
disposingRef.resolve();
|
||||||
|
disposingRef = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSignalHandler = onExitSignal(dispose);
|
||||||
|
|
||||||
|
return {
|
||||||
|
gitToken,
|
||||||
|
mcpToken,
|
||||||
|
[Symbol.asyncDispose]: dispose,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +187,7 @@ export async function revokeGitHubInstallationToken(token: string): Promise<void
|
|||||||
});
|
});
|
||||||
log.debug("» installation token revoked");
|
log.debug("» installation token revoked");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.warning(
|
log.info(
|
||||||
`Failed to revoke installation token: ${error instanceof Error ? error.message : String(error)}`
|
`Failed to revoke installation token: ${error instanceof Error ? error.message : String(error)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-4
@@ -1,9 +1,7 @@
|
|||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
|
import { ensureGitHubToken } from "./utils/github.ts";
|
||||||
|
|
||||||
config({ path: resolve(import.meta.dirname, "../.env") });
|
config({ path: resolve(import.meta.dirname, "../.env") });
|
||||||
|
|
||||||
// alias GITHUB_TOKEN to GH_TOKEN for tests
|
await ensureGitHubToken();
|
||||||
if (!process.env.GH_TOKEN && process.env.GITHUB_TOKEN) {
|
|
||||||
process.env.GH_TOKEN = process.env.GITHUB_TOKEN;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user