Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| caa3cf4d4b | |||
| 8e53ce4e6b | |||
| 95c1a5757e | |||
| ee100354da | |||
| 70f1c47a28 | |||
| 4ee1ae89a5 | |||
| 185ca7a832 | |||
| 4ecff49b72 | |||
| df3ec6b815 | |||
| 4a9d83b102 | |||
| 57537d1a95 | |||
| 9948c08e7d | |||
| 3bf2f8596f | |||
| 510f2c96f9 | |||
| df13253d48 |
@@ -14,7 +14,7 @@ jobs:
|
||||
node-version: "24"
|
||||
cache: "pnpm"
|
||||
|
||||
- run: pnpm install --frozen-lockfile --ignore-scripts
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm typecheck
|
||||
- run: pnpm test
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
node-version: "24"
|
||||
cache: "pnpm"
|
||||
|
||||
- run: pnpm install --frozen-lockfile --ignore-scripts
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm runtest ${{ matrix.test }} ${{ matrix.agent }}
|
||||
|
||||
agnostic:
|
||||
@@ -87,5 +87,5 @@ jobs:
|
||||
node-version: "24"
|
||||
cache: "pnpm"
|
||||
|
||||
- run: pnpm install --frozen-lockfile --ignore-scripts
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm runtest ${{ matrix.test }}
|
||||
|
||||
+3
-1
@@ -1,6 +1,8 @@
|
||||
# sync action lockfile when action/package.json changes
|
||||
if git diff --cached --name-only | grep -q "^action/package.json$"; then
|
||||
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
|
||||
fi
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 pullfrog
|
||||
Copyright (c) 2026 Pullfrog, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
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");
|
||||
log.info(`» MCP config written to ${configPath}`);
|
||||
log.debug(`» MCP config written to ${configPath}`);
|
||||
return configPath;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export const claude = agent({
|
||||
// build disallowedTools based on tool permissions
|
||||
const disallowedTools = buildDisallowedTools(ctx);
|
||||
if (disallowedTools.length > 0) {
|
||||
log.info(`» disallowed tools: ${disallowedTools.join(", ")}`);
|
||||
log.debug(`» disallowed built-ins: ${JSON.stringify(disallowedTools)}`);
|
||||
}
|
||||
|
||||
// write MCP config file
|
||||
@@ -173,8 +173,7 @@ export const claude = agent({
|
||||
onStderr: (chunk) => {
|
||||
const trimmed = chunk.trim();
|
||||
if (trimmed) {
|
||||
log.debug(`[claude stderr] ${trimmed}`);
|
||||
log.warning(trimmed);
|
||||
log.info(`[claude stderr] ${trimmed}`);
|
||||
finalOutput += trimmed + "\n";
|
||||
}
|
||||
},
|
||||
@@ -239,10 +238,13 @@ const messageHandlers: SDKMessageHandlers = {
|
||||
user: (data, bashToolIds, thinkingTimer) => {
|
||||
if (data.message?.content) {
|
||||
for (const content of data.message.content) {
|
||||
if (typeof content === "string") {
|
||||
continue;
|
||||
}
|
||||
if (content.type === "tool_result") {
|
||||
thinkingTimer.markToolResult();
|
||||
|
||||
const toolUseId = (content as any).tool_use_id;
|
||||
const toolUseId = content.tool_use_id;
|
||||
const isBashTool = toolUseId && bashToolIds.has(toolUseId);
|
||||
|
||||
const outputContent =
|
||||
@@ -250,7 +252,13 @@ const messageHandlers: SDKMessageHandlers = {
|
||||
? content.content
|
||||
: Array.isArray(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")
|
||||
: String(content.content);
|
||||
|
||||
@@ -258,7 +266,7 @@ const messageHandlers: SDKMessageHandlers = {
|
||||
// Log bash output in a collapsed group
|
||||
log.startGroup(`bash output`);
|
||||
if (content.is_error) {
|
||||
log.warning(outputContent);
|
||||
log.info(outputContent);
|
||||
} else {
|
||||
log.info(outputContent);
|
||||
}
|
||||
@@ -266,7 +274,7 @@ const messageHandlers: SDKMessageHandlers = {
|
||||
// Clean up the tracked ID
|
||||
bashToolIds.delete(toolUseId);
|
||||
} else if (content.is_error) {
|
||||
log.warning(`Tool error: ${outputContent}`);
|
||||
log.info(`Tool error: ${outputContent}`);
|
||||
} else {
|
||||
// log successful non-bash tool result at debug level
|
||||
log.debug(`tool output: ${outputContent}`);
|
||||
@@ -301,11 +309,11 @@ const messageHandlers: SDKMessageHandlers = {
|
||||
],
|
||||
]);
|
||||
} 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") {
|
||||
log.error(`Execution error: ${JSON.stringify(data)}`);
|
||||
log.info(`Execution error: ${JSON.stringify(data)}`);
|
||||
} else {
|
||||
log.error(`Failed: ${JSON.stringify(data)}`);
|
||||
log.info(`Failed: ${JSON.stringify(data)}`);
|
||||
}
|
||||
},
|
||||
system: () => {},
|
||||
|
||||
+7
-8
@@ -43,7 +43,7 @@ async function isModelAvailable(ctx: { apiKey: string; model: string }): Promise
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!response.ok) {
|
||||
log.warning(
|
||||
log.info(
|
||||
`failed to list models (HTTP ${response.status}), falling back to ${FALLBACK_MODEL}`
|
||||
);
|
||||
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 }> };
|
||||
return body.data.some((m) => m.id === ctx.model);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
@@ -262,8 +262,7 @@ export const codex = agent({
|
||||
onStderr: (chunk) => {
|
||||
const trimmed = chunk.trim();
|
||||
if (trimmed) {
|
||||
log.debug(`[codex stderr] ${trimmed}`);
|
||||
log.warning(trimmed);
|
||||
log.info(`[codex stderr] ${trimmed}`);
|
||||
finalOutput += trimmed + "\n";
|
||||
}
|
||||
},
|
||||
@@ -319,7 +318,7 @@ const messageHandlers: {
|
||||
]);
|
||||
},
|
||||
"turn.failed": (event) => {
|
||||
log.error(`Turn failed: ${event.error.message}`);
|
||||
log.info(`Turn failed: ${event.error.message}`);
|
||||
},
|
||||
"item.started": (event, commandExecutionIds, thinkingTimer) => {
|
||||
const item = event.item;
|
||||
@@ -362,7 +361,7 @@ const messageHandlers: {
|
||||
thinkingTimer.markToolResult();
|
||||
log.startGroup(`bash output`);
|
||||
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 {
|
||||
log.info(item.aggregated_output || "");
|
||||
}
|
||||
@@ -372,7 +371,7 @@ const messageHandlers: {
|
||||
} else if (item.type === "mcp_tool_call") {
|
||||
thinkingTimer.markToolResult();
|
||||
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) {
|
||||
// log successful MCP tool call output so it appears in captured output
|
||||
const output = (item as any).output;
|
||||
@@ -388,6 +387,6 @@ const messageHandlers: {
|
||||
}
|
||||
},
|
||||
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 isError = result?.isError;
|
||||
if (isError) {
|
||||
log.warning("Tool call failed");
|
||||
log.info("Tool call failed");
|
||||
} else {
|
||||
// log successful tool result so it appears in output
|
||||
// handle both formats: { text: string } or { text: { text: string } }
|
||||
@@ -320,12 +320,12 @@ export const cursor = agent({
|
||||
const text = data.toString();
|
||||
stderr += text;
|
||||
process.stderr.write(text);
|
||||
log.warning(text);
|
||||
log.info(text);
|
||||
});
|
||||
|
||||
child.on("close", async (code, 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);
|
||||
@@ -440,5 +440,6 @@ function configureCursorTools(ctx: AgentRunContext): void {
|
||||
|
||||
writeFileSync(cliConfigPath, JSON.stringify(config, null, 2), "utf-8");
|
||||
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)}`);
|
||||
}
|
||||
|
||||
+5
-6
@@ -149,7 +149,7 @@ const messageHandlers = {
|
||||
if (event.status === "error") {
|
||||
const errorMsg =
|
||||
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) {
|
||||
// log successful tool result so it appears in output
|
||||
const outputStr =
|
||||
@@ -270,8 +270,7 @@ export const gemini = agent({
|
||||
onStderr: (chunk) => {
|
||||
const trimmed = chunk.trim();
|
||||
if (trimmed) {
|
||||
log.debug(`[gemini stderr] ${trimmed}`);
|
||||
log.warning(trimmed);
|
||||
log.info(`[gemini stderr] ${trimmed}`);
|
||||
finalOutput += trimmed + "\n";
|
||||
}
|
||||
},
|
||||
@@ -286,7 +285,7 @@ export const gemini = agent({
|
||||
|
||||
// retry on transient API errors (500, 503, INTERNAL, etc.)
|
||||
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...`
|
||||
);
|
||||
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
|
||||
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...`
|
||||
);
|
||||
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");
|
||||
log.info(`» Gemini settings written to ${settingsPath}`);
|
||||
if (exclude.length > 0) {
|
||||
log.info(`» excluded tools: ${exclude.join(", ")}`);
|
||||
log.debug(`» disallowed built-ins: ${JSON.stringify(exclude)}`);
|
||||
}
|
||||
|
||||
return model;
|
||||
|
||||
+11
-13
@@ -154,7 +154,7 @@ export const opencode = agent({
|
||||
activeToolCalls > 0
|
||||
? ` (waiting for ${activeToolCalls} tool call${activeToolCalls > 1 ? "s" : ""})`
|
||||
: " (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)`
|
||||
);
|
||||
}
|
||||
@@ -186,7 +186,7 @@ export const opencode = agent({
|
||||
const providerError = detectProviderError(trimmed);
|
||||
if (providerError) {
|
||||
lastProviderError = providerError;
|
||||
log.error(`» provider error detected (${providerError}): ${trimmed.substring(0, 500)}`);
|
||||
log.info(`» provider error detected (${providerError}): ${trimmed.substring(0, 500)}`);
|
||||
} else {
|
||||
// OpenCode's --print-logs output goes to stderr. demote internal
|
||||
// INFO/DEBUG bus traffic to debug so it doesn't drown out tool
|
||||
@@ -207,9 +207,9 @@ export const opencode = agent({
|
||||
const diagnosis = lastProviderError
|
||||
? `provider error: ${lastProviderError}`
|
||||
: "unknown cause (no stdout events received)";
|
||||
log.error(`» OpenCode produced 0 events (${diagnosis})`);
|
||||
log.info(`» OpenCode produced 0 events (${diagnosis})`);
|
||||
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"
|
||||
: `${eventCount} events were processed before the hang`;
|
||||
|
||||
log.error(
|
||||
log.info(
|
||||
`» OpenCode ${isActivityTimeout ? "hung" : "failed"} after ${(duration / 1000).toFixed(1)}s: ${errorMessage}`
|
||||
);
|
||||
log.error(`» diagnosis: ${diagnosis}`);
|
||||
log.info(`» diagnosis: ${diagnosis}`);
|
||||
if (stderrContext) {
|
||||
log.error(
|
||||
log.info(
|
||||
`» 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.debug(
|
||||
`» OpenCode permissions: edit=${permission.edit}, bash=${permission.bash}, webfetch=${permission.webfetch}`
|
||||
);
|
||||
log.debug(`» disallowed built-ins: ${JSON.stringify(permission)}`);
|
||||
log.debug(`OpenCode config contents:\n${configJson}`);
|
||||
}
|
||||
|
||||
@@ -602,7 +600,7 @@ const messageHandlers = {
|
||||
log.debug(` output: ${typeof output === "string" ? output : JSON.stringify(output)}`);
|
||||
}
|
||||
if (toolDuration > 5000) {
|
||||
log.warning(
|
||||
log.info(
|
||||
`» ⚠️ 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") {
|
||||
const errorMsg = typeof output === "string" ? output : JSON.stringify(output);
|
||||
log.error(`» ❌ tool call failed: ${errorMsg}`);
|
||||
log.info(`» ❌ tool call failed: ${errorMsg}`);
|
||||
} else if (output) {
|
||||
// log successful tool result so it appears in captured output
|
||||
const outputStr = typeof output === "string" ? output : JSON.stringify(output);
|
||||
@@ -626,7 +624,7 @@ const messageHandlers = {
|
||||
);
|
||||
|
||||
if (event.status === "error") {
|
||||
log.error(`» OpenCode CLI failed: ${JSON.stringify(event)}`);
|
||||
log.info(`» OpenCode CLI failed: ${JSON.stringify(event)}`);
|
||||
} else {
|
||||
// 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;
|
||||
|
||||
@@ -37,18 +37,6 @@ export const agent = <const input extends AgentInput>(input: input): defineAgent
|
||||
log.info(`» bash: ${ctx.payload.bash}`);
|
||||
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);
|
||||
},
|
||||
...agentsManifest[input.name],
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import { main } from "./main.ts";
|
||||
import { runCleanup } from "./utils/exitHandler.ts";
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
@@ -22,8 +21,6 @@ async function run(): Promise<void> {
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||
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 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
|
||||
// matches GitHub's permission levels: admin > write > maintain > triage > read > none
|
||||
export type AuthorPermission = "admin" | "maintain" | "write" | "triage" | "read" | "none";
|
||||
@@ -250,6 +272,8 @@ export interface WriteablePayload {
|
||||
agent?: AgentName | undefined;
|
||||
/** the user's actual request (body if @pullfrog tagged) */
|
||||
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) */
|
||||
eventInstructions?: string | undefined;
|
||||
/** 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["Proxy-Authorization"] = "Basic " + new Buffer(connectOptions.proxyAuth).toString("base64");
|
||||
}
|
||||
debug2("making CONNECT request");
|
||||
debug3("making CONNECT request");
|
||||
var connectReq = self2.request(connectOptions);
|
||||
connectReq.useChunkedEncodingByDefault = false;
|
||||
connectReq.once("response", onResponse);
|
||||
@@ -419,7 +419,7 @@ var require_tunnel = __commonJS({
|
||||
connectReq.removeAllListeners();
|
||||
socket.removeAllListeners();
|
||||
if (res.statusCode !== 200) {
|
||||
debug2(
|
||||
debug3(
|
||||
"tunneling socket could not be established, statusCode=%d",
|
||||
res.statusCode
|
||||
);
|
||||
@@ -431,7 +431,7 @@ var require_tunnel = __commonJS({
|
||||
return;
|
||||
}
|
||||
if (head.length > 0) {
|
||||
debug2("got illegal response body from proxy");
|
||||
debug3("got illegal response body from proxy");
|
||||
socket.destroy();
|
||||
var error2 = new Error("got illegal response body from proxy");
|
||||
error2.code = "ECONNRESET";
|
||||
@@ -439,13 +439,13 @@ var require_tunnel = __commonJS({
|
||||
self2.removeSocket(placeholder);
|
||||
return;
|
||||
}
|
||||
debug2("tunneling connection has established");
|
||||
debug3("tunneling connection has established");
|
||||
self2.sockets[self2.sockets.indexOf(placeholder)] = socket;
|
||||
return cb(socket);
|
||||
}
|
||||
function onError(cause) {
|
||||
connectReq.removeAllListeners();
|
||||
debug2(
|
||||
debug3(
|
||||
"tunneling socket could not be established, cause=%s\n",
|
||||
cause.message,
|
||||
cause.stack
|
||||
@@ -507,9 +507,9 @@ var require_tunnel = __commonJS({
|
||||
}
|
||||
return target;
|
||||
}
|
||||
var debug2;
|
||||
var debug3;
|
||||
if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) {
|
||||
debug2 = function() {
|
||||
debug3 = function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
if (typeof args[0] === "string") {
|
||||
args[0] = "TUNNEL: " + args[0];
|
||||
@@ -519,10 +519,10 @@ var require_tunnel = __commonJS({
|
||||
console.error.apply(console, args);
|
||||
};
|
||||
} 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";
|
||||
}
|
||||
exports.isDebug = isDebug2;
|
||||
function debug2(message) {
|
||||
function debug3(message) {
|
||||
(0, command_1.issueCommand)("debug", {}, message);
|
||||
}
|
||||
exports.debug = debug2;
|
||||
exports.debug = debug3;
|
||||
function error2(message, properties = {}) {
|
||||
(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");
|
||||
|
||||
// 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) {
|
||||
return args.map((arg) => {
|
||||
if (typeof arg === "string") return arg;
|
||||
@@ -25626,19 +25631,16 @@ 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: (...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) => {
|
||||
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) => {
|
||||
core.error(`${ts()}${formatArgs(args)}`);
|
||||
},
|
||||
@@ -25646,10 +25648,14 @@ var log = {
|
||||
success: (...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) => {
|
||||
if (isDebugEnabled()) {
|
||||
core.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] ${formatArgs(args)}`);
|
||||
if (isRunnerDebugEnabled()) {
|
||||
core.debug(formatArgs(args));
|
||||
return;
|
||||
}
|
||||
if (isLocalDebugEnabled()) {
|
||||
core.info(`${ts()}[DEBUG] ${formatArgs(args)}`);
|
||||
}
|
||||
},
|
||||
/** Print a formatted box with text */
|
||||
@@ -25740,9 +25746,7 @@ async function retry(fn, options = {}) {
|
||||
throw error2;
|
||||
}
|
||||
const delay = delayMs * attempt;
|
||||
log.warning(
|
||||
`\xBB ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`
|
||||
);
|
||||
log.info(`\xBB ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
@@ -25925,7 +25929,7 @@ async function revokeGitHubInstallationToken(token) {
|
||||
});
|
||||
log.debug("\xBB installation token revoked");
|
||||
} catch (error2) {
|
||||
log.warning(
|
||||
log.info(
|
||||
`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 { log, writeSummary } from "./utils/cli.ts";
|
||||
import { reportErrorToComment } from "./utils/errorReport.ts";
|
||||
import { setupExitHandler } from "./utils/exitHandler.ts";
|
||||
import { resolveGit } from "./utils/gitAuth.ts";
|
||||
import { createOctokit } from "./utils/github.ts";
|
||||
import { resolveInstructions } from "./utils/instructions.ts";
|
||||
@@ -52,8 +51,6 @@ export async function main(): Promise<MainResult> {
|
||||
typeof resolvedPromptInput !== "string" ? resolvedPromptInput.progressCommentId : undefined,
|
||||
});
|
||||
|
||||
setupExitHandler(toolState);
|
||||
|
||||
// resolve and fingerprint git binary before any agent code runs
|
||||
resolveGit();
|
||||
|
||||
@@ -167,6 +164,17 @@ export async function main(): Promise<MainResult> {
|
||||
repo: runContext.repo,
|
||||
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
|
||||
activityTimeout = createProcessOutputActivityTimeout({
|
||||
@@ -236,8 +244,6 @@ export async function main(): Promise<MainResult> {
|
||||
error: errorMessage,
|
||||
};
|
||||
} 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";
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -243,8 +243,8 @@ Use this tool to:
|
||||
|
||||
const finalExitCode = exitCode ?? (timedOut ? 124 : -1);
|
||||
if (finalExitCode !== 0) {
|
||||
log.error(`bash command failed with exit code ${finalExitCode}: ${params.command}`);
|
||||
if (output) log.error(`output: ${output.trim()}`);
|
||||
log.info(`bash command failed with exit code ${finalExitCode}: ${params.command}`);
|
||||
if (output) log.info(`output: ${output.trim()}`);
|
||||
}
|
||||
|
||||
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`);
|
||||
} 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) {
|
||||
// normalize agent crashes into the same return shape as clean failures
|
||||
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 {
|
||||
success: false,
|
||||
mode: selectedMode.name,
|
||||
|
||||
@@ -44,6 +44,22 @@ export function CreatePullRequestTool(ctx: ToolContext) {
|
||||
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 {
|
||||
success: true,
|
||||
pullRequestId: result.data.id,
|
||||
|
||||
@@ -290,7 +290,8 @@ function hasThumbsUpFrom(comment: ReviewThreadComment, username: string): boolea
|
||||
if (!comment.reactionGroups) return false;
|
||||
const thumbsUp = comment.reactionGroups.find((g) => g.content === "THUMBS_UP");
|
||||
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 {
|
||||
@@ -631,7 +632,7 @@ export function ResolveReviewThreadTool(ctx: ToolContext) {
|
||||
const message = isResolved
|
||||
? `thread ${params.thread_id} was already resolved`
|
||||
: `failed to resolve thread ${params.thread_id}: ${errorMessage}`;
|
||||
log.warning(message);
|
||||
log.info(message);
|
||||
|
||||
return {
|
||||
thread_id: params.thread_id,
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ export const execute = <T, R extends Record<string, any> | string>(
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const prefix = toolName ? `[${toolName}]` : "tool";
|
||||
log.error(`${prefix} error: ${errorMessage}`);
|
||||
log.info(`${prefix} error: ${errorMessage}`);
|
||||
log.debug(`${prefix} params: ${formatJsonValue(params)}`);
|
||||
return handleToolError(error);
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ export function computeModes(): Mode[] {
|
||||
name: "Review",
|
||||
description:
|
||||
"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.
|
||||
|
||||
@@ -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?
|
||||
- **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?
|
||||
- **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.
|
||||
|
||||
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:
|
||||
- **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.
|
||||
5. **WRITE SUMMARY** - Draft a 1-3 sentence summary for the review body. Include urgency level and any concerns about code outside the diff.
|
||||
|
||||
6. **WRITE SUMMARY** - Draft a 1-3 sentence summary for the review body. Include urgency level and any concerns about code outside the diff.
|
||||
|
||||
7. **SUBMIT** — Use ${ghPullfrogMcpName}/create_pull_request_review:
|
||||
- \`body\`: The summary from step 6
|
||||
- \`comments\`: The filtered inline comments from step 5
|
||||
6. **SUBMIT** — Use ${ghPullfrogMcpName}/create_pull_request_review:
|
||||
- \`body\`: The summary from step 5
|
||||
- \`comments\`: The inline comments from step 4
|
||||
|
||||
${permalinkTip}
|
||||
`,
|
||||
|
||||
+6
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pullfrog/pullfrog",
|
||||
"version": "0.0.164",
|
||||
"version": "0.0.165",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"index.js",
|
||||
@@ -20,7 +20,8 @@
|
||||
"runtest": "node test/run.ts",
|
||||
"scratch": "node scratch.ts",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -79,7 +80,9 @@
|
||||
"types": "./dist/index.d.cts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"./internal": "./dist/internal.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import arg from "arg";
|
||||
import { config } from "dotenv";
|
||||
import type { AgentResult } from "./agents/shared.ts";
|
||||
@@ -12,6 +12,7 @@ import { log } from "./utils/cli.ts";
|
||||
import { runInDocker } from "./utils/docker.ts";
|
||||
import { ensureGitHubToken } from "./utils/github.ts";
|
||||
import { isInsideDocker } from "./utils/globals.ts";
|
||||
import { runPostCleanup } from "./utils/postCleanup.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);
|
||||
|
||||
@@ -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({
|
||||
"--help": Boolean,
|
||||
"--raw": String,
|
||||
|
||||
Generated
+38
@@ -4,6 +4,8 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
packageExtensionsChecksum: sha256-Ae6BTffLg0DiuEWVZSk6skwAhBSw9mfAk50E5Iq3i80=
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -120,6 +122,15 @@ packages:
|
||||
peerDependencies:
|
||||
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':
|
||||
resolution: {integrity: sha512-zY/wDDhcvmt6/upQwZM766PAnvIzdEMcgydUGd9pqY9FMGNo9I9uE4RYAfms9AeUUtbZJu2h2Ua0tvFsO5XF4Q==}
|
||||
|
||||
@@ -129,6 +140,10 @@ packages:
|
||||
'@ark/util@0.56.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
|
||||
|
||||
@@ -1182,6 +1197,10 @@ packages:
|
||||
jose@6.1.3:
|
||||
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:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
@@ -1454,6 +1473,9 @@ packages:
|
||||
resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
ts-algebra@2.0.0:
|
||||
resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
|
||||
|
||||
tunnel@0.0.6:
|
||||
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
|
||||
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)':
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.77.0(zod@4.3.5)
|
||||
zod: 4.3.5
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.33.5
|
||||
@@ -1677,6 +1700,12 @@ snapshots:
|
||||
'@img/sharp-linuxmusl-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/schema@0.56.0':
|
||||
@@ -1685,6 +1714,8 @@ snapshots:
|
||||
|
||||
'@ark/util@0.56.0': {}
|
||||
|
||||
'@babel/runtime@7.28.6': {}
|
||||
|
||||
'@borewit/text-codec@0.1.1': {}
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.12':
|
||||
@@ -2589,6 +2620,11 @@ snapshots:
|
||||
|
||||
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-typed@8.0.2: {}
|
||||
@@ -2871,6 +2907,8 @@ snapshots:
|
||||
'@tokenizer/token': 0.3.0
|
||||
ieee754: 1.2.1
|
||||
|
||||
ts-algebra@2.0.0: {}
|
||||
|
||||
tunnel@0.0.6: {}
|
||||
|
||||
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["Proxy-Authorization"] = "Basic " + new Buffer(connectOptions.proxyAuth).toString("base64");
|
||||
}
|
||||
debug("making CONNECT request");
|
||||
debug2("making CONNECT request");
|
||||
var connectReq = self2.request(connectOptions);
|
||||
connectReq.useChunkedEncodingByDefault = false;
|
||||
connectReq.once("response", onResponse);
|
||||
@@ -419,7 +419,7 @@ var require_tunnel = __commonJS({
|
||||
connectReq.removeAllListeners();
|
||||
socket.removeAllListeners();
|
||||
if (res.statusCode !== 200) {
|
||||
debug(
|
||||
debug2(
|
||||
"tunneling socket could not be established, statusCode=%d",
|
||||
res.statusCode
|
||||
);
|
||||
@@ -431,7 +431,7 @@ var require_tunnel = __commonJS({
|
||||
return;
|
||||
}
|
||||
if (head.length > 0) {
|
||||
debug("got illegal response body from proxy");
|
||||
debug2("got illegal response body from proxy");
|
||||
socket.destroy();
|
||||
var error2 = new Error("got illegal response body from proxy");
|
||||
error2.code = "ECONNRESET";
|
||||
@@ -439,13 +439,13 @@ var require_tunnel = __commonJS({
|
||||
self2.removeSocket(placeholder);
|
||||
return;
|
||||
}
|
||||
debug("tunneling connection has established");
|
||||
debug2("tunneling connection has established");
|
||||
self2.sockets[self2.sockets.indexOf(placeholder)] = socket;
|
||||
return cb(socket);
|
||||
}
|
||||
function onError(cause) {
|
||||
connectReq.removeAllListeners();
|
||||
debug(
|
||||
debug2(
|
||||
"tunneling socket could not be established, cause=%s\n",
|
||||
cause.message,
|
||||
cause.stack
|
||||
@@ -507,9 +507,9 @@ var require_tunnel = __commonJS({
|
||||
}
|
||||
return target;
|
||||
}
|
||||
var debug;
|
||||
var debug2;
|
||||
if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) {
|
||||
debug = function() {
|
||||
debug2 = function() {
|
||||
var args2 = Array.prototype.slice.call(arguments);
|
||||
if (typeof args2[0] === "string") {
|
||||
args2[0] = "TUNNEL: " + args2[0];
|
||||
@@ -519,10 +519,10 @@ var require_tunnel = __commonJS({
|
||||
console.error.apply(console, args2);
|
||||
};
|
||||
} 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";
|
||||
}
|
||||
exports.isDebug = isDebug2;
|
||||
function debug(message) {
|
||||
function debug2(message) {
|
||||
(0, command_1.issueCommand)("debug", {}, message);
|
||||
}
|
||||
exports.debug = debug;
|
||||
exports.debug = debug2;
|
||||
function error2(message, properties = {}) {
|
||||
(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 });
|
||||
}
|
||||
async doExecute(chained, clearGlobalState, run2, free) {
|
||||
async doExecute(chained, clearGlobalState, run, free) {
|
||||
var error2, eventInfo, passed;
|
||||
if (this.retryCount === 0) {
|
||||
this._assertStatus("RUNNING");
|
||||
@@ -25858,10 +25858,10 @@ var require_light = __commonJS({
|
||||
}
|
||||
} catch (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;
|
||||
if (this._states.jobStatus(this.options.id === "RUNNING")) {
|
||||
this._states.next(this.options.id);
|
||||
@@ -25869,9 +25869,9 @@ var require_light = __commonJS({
|
||||
this._assertStatus("EXECUTING");
|
||||
eventInfo = { args: this.args, options: this.options, retryCount: this.retryCount };
|
||||
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;
|
||||
if (clearGlobalState()) {
|
||||
retry2 = await this.Events.trigger("failed", error2, eventInfo);
|
||||
@@ -25879,7 +25879,7 @@ var require_light = __commonJS({
|
||||
retryAfter = ~~retry2;
|
||||
this.Events.trigger("retry", `Retrying ${this.options.id} after ${retryAfter} ms`, eventInfo);
|
||||
this.retryCount++;
|
||||
return run2(retryAfter);
|
||||
return run(retryAfter);
|
||||
} else {
|
||||
this.doDone(eventInfo);
|
||||
await free(this.options, eventInfo);
|
||||
@@ -26517,17 +26517,17 @@ var require_light = __commonJS({
|
||||
}
|
||||
}
|
||||
_run(index, job, wait) {
|
||||
var clearGlobalState, free, run2;
|
||||
var clearGlobalState, free, run;
|
||||
job.doRun();
|
||||
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);
|
||||
return this._scheduled[index] = {
|
||||
timeout: setTimeout(() => {
|
||||
return job.doExecute(this._limiter, clearGlobalState, run2, free);
|
||||
return job.doExecute(this._limiter, clearGlobalState, run, free);
|
||||
}, wait),
|
||||
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,
|
||||
job
|
||||
};
|
||||
@@ -26949,9 +26949,9 @@ var require_constants6 = __commonJS({
|
||||
var require_debug = __commonJS({
|
||||
"node_modules/.pnpm/semver@7.7.3/node_modules/semver/internal/debug.js"(exports, module) {
|
||||
"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_LENGTH
|
||||
} = require_constants6();
|
||||
var debug = require_debug();
|
||||
var debug2 = require_debug();
|
||||
exports = module.exports = {};
|
||||
var re = exports.re = [];
|
||||
var safeRe = exports.safeRe = [];
|
||||
@@ -26987,7 +26987,7 @@ var require_re = __commonJS({
|
||||
var createToken = (name, value2, isGlobal) => {
|
||||
const safe = makeSafeRegex(value2);
|
||||
const index = R++;
|
||||
debug(name, index, value2);
|
||||
debug2(name, index, value2);
|
||||
t[name] = index;
|
||||
src[index] = value2;
|
||||
safeSrc[index] = safe;
|
||||
@@ -27091,7 +27091,7 @@ var require_identifiers = __commonJS({
|
||||
var require_semver = __commonJS({
|
||||
"node_modules/.pnpm/semver@7.7.3/node_modules/semver/classes/semver.js"(exports, module) {
|
||||
"use strict";
|
||||
var debug = require_debug();
|
||||
var debug2 = require_debug();
|
||||
var { MAX_LENGTH, MAX_SAFE_INTEGER } = require_constants6();
|
||||
var { safeRe: re, t } = require_re();
|
||||
var parseOptions = require_parse_options();
|
||||
@@ -27113,7 +27113,7 @@ var require_semver = __commonJS({
|
||||
`version is longer than ${MAX_LENGTH} characters`
|
||||
);
|
||||
}
|
||||
debug("SemVer", version, options);
|
||||
debug2("SemVer", version, options);
|
||||
this.options = options;
|
||||
this.loose = !!options.loose;
|
||||
this.includePrerelease = !!options.includePrerelease;
|
||||
@@ -27161,7 +27161,7 @@ var require_semver = __commonJS({
|
||||
return this.version;
|
||||
}
|
||||
compare(other) {
|
||||
debug("SemVer.compare", this.version, this.options, other);
|
||||
debug2("SemVer.compare", this.version, this.options, other);
|
||||
if (!(other instanceof _SemVer)) {
|
||||
if (typeof other === "string" && other === this.version) {
|
||||
return 0;
|
||||
@@ -27212,7 +27212,7 @@ var require_semver = __commonJS({
|
||||
do {
|
||||
const a = this.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) {
|
||||
return 0;
|
||||
} else if (b === void 0) {
|
||||
@@ -27234,7 +27234,7 @@ var require_semver = __commonJS({
|
||||
do {
|
||||
const a = this.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) {
|
||||
return 0;
|
||||
} else if (b === void 0) {
|
||||
@@ -27862,21 +27862,21 @@ var require_range = __commonJS({
|
||||
const loose = this.options.loose;
|
||||
const hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE];
|
||||
range2 = range2.replace(hr, hyphenReplace(this.options.includePrerelease));
|
||||
debug("hyphen replace", range2);
|
||||
debug2("hyphen replace", range2);
|
||||
range2 = range2.replace(re[t.COMPARATORTRIM], comparatorTrimReplace);
|
||||
debug("comparator trim", range2);
|
||||
debug2("comparator trim", range2);
|
||||
range2 = range2.replace(re[t.TILDETRIM], tildeTrimReplace);
|
||||
debug("tilde trim", range2);
|
||||
debug2("tilde trim", range2);
|
||||
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));
|
||||
if (loose) {
|
||||
rangeList = rangeList.filter((comp) => {
|
||||
debug("loose invalid filter", comp, this.options);
|
||||
debug2("loose invalid filter", comp, this.options);
|
||||
return !!comp.match(re[t.COMPARATORLOOSE]);
|
||||
});
|
||||
}
|
||||
debug("range list", rangeList);
|
||||
debug2("range list", rangeList);
|
||||
const rangeMap = /* @__PURE__ */ new Map();
|
||||
const comparators = rangeList.map((comp) => new Comparator(comp, this.options));
|
||||
for (const comp of comparators) {
|
||||
@@ -27931,7 +27931,7 @@ var require_range = __commonJS({
|
||||
var cache = new LRU();
|
||||
var parseOptions = require_parse_options();
|
||||
var Comparator = require_comparator();
|
||||
var debug = require_debug();
|
||||
var debug2 = require_debug();
|
||||
var SemVer = require_semver();
|
||||
var {
|
||||
safeRe: re,
|
||||
@@ -27957,15 +27957,15 @@ var require_range = __commonJS({
|
||||
};
|
||||
var parseComparator = (comp, options) => {
|
||||
comp = comp.replace(re[t.BUILD], "");
|
||||
debug("comp", comp, options);
|
||||
debug2("comp", comp, options);
|
||||
comp = replaceCarets(comp, options);
|
||||
debug("caret", comp);
|
||||
debug2("caret", comp);
|
||||
comp = replaceTildes(comp, options);
|
||||
debug("tildes", comp);
|
||||
debug2("tildes", comp);
|
||||
comp = replaceXRanges(comp, options);
|
||||
debug("xrange", comp);
|
||||
debug2("xrange", comp);
|
||||
comp = replaceStars(comp, options);
|
||||
debug("stars", comp);
|
||||
debug2("stars", comp);
|
||||
return comp;
|
||||
};
|
||||
var isX = (id) => !id || id.toLowerCase() === "x" || id === "*";
|
||||
@@ -27975,7 +27975,7 @@ var require_range = __commonJS({
|
||||
var replaceTilde = (comp, options) => {
|
||||
const r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE];
|
||||
return comp.replace(r, (_, M, m, p, pr) => {
|
||||
debug("tilde", comp, _, M, m, p, pr);
|
||||
debug2("tilde", comp, _, M, m, p, pr);
|
||||
let ret;
|
||||
if (isX(M)) {
|
||||
ret = "";
|
||||
@@ -27984,12 +27984,12 @@ var require_range = __commonJS({
|
||||
} else if (isX(p)) {
|
||||
ret = `>=${M}.${m}.0 <${M}.${+m + 1}.0-0`;
|
||||
} else if (pr) {
|
||||
debug("replaceTilde pr", pr);
|
||||
debug2("replaceTilde pr", pr);
|
||||
ret = `>=${M}.${m}.${p}-${pr} <${M}.${+m + 1}.0-0`;
|
||||
} else {
|
||||
ret = `>=${M}.${m}.${p} <${M}.${+m + 1}.0-0`;
|
||||
}
|
||||
debug("tilde return", ret);
|
||||
debug2("tilde return", ret);
|
||||
return ret;
|
||||
});
|
||||
};
|
||||
@@ -27997,11 +27997,11 @@ var require_range = __commonJS({
|
||||
return comp.trim().split(/\s+/).map((c) => replaceCaret(c, options)).join(" ");
|
||||
};
|
||||
var replaceCaret = (comp, options) => {
|
||||
debug("caret", comp, options);
|
||||
debug2("caret", comp, options);
|
||||
const r = options.loose ? re[t.CARETLOOSE] : re[t.CARET];
|
||||
const z = options.includePrerelease ? "-0" : "";
|
||||
return comp.replace(r, (_, M, m, p, pr) => {
|
||||
debug("caret", comp, _, M, m, p, pr);
|
||||
debug2("caret", comp, _, M, m, p, pr);
|
||||
let ret;
|
||||
if (isX(M)) {
|
||||
ret = "";
|
||||
@@ -28014,7 +28014,7 @@ var require_range = __commonJS({
|
||||
ret = `>=${M}.${m}.0${z} <${+M + 1}.0.0-0`;
|
||||
}
|
||||
} else if (pr) {
|
||||
debug("replaceCaret pr", pr);
|
||||
debug2("replaceCaret pr", pr);
|
||||
if (M === "0") {
|
||||
if (m === "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`;
|
||||
}
|
||||
} else {
|
||||
debug("no pr");
|
||||
debug2("no pr");
|
||||
if (M === "0") {
|
||||
if (m === "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`;
|
||||
}
|
||||
}
|
||||
debug("caret return", ret);
|
||||
debug2("caret return", ret);
|
||||
return ret;
|
||||
});
|
||||
};
|
||||
var replaceXRanges = (comp, options) => {
|
||||
debug("replaceXRanges", comp, options);
|
||||
debug2("replaceXRanges", comp, options);
|
||||
return comp.split(/\s+/).map((c) => replaceXRange(c, options)).join(" ");
|
||||
};
|
||||
var replaceXRange = (comp, options) => {
|
||||
comp = comp.trim();
|
||||
const r = options.loose ? re[t.XRANGELOOSE] : re[t.XRANGE];
|
||||
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 = xM || isX(m);
|
||||
const xp = xm || isX(p);
|
||||
@@ -28095,16 +28095,16 @@ var require_range = __commonJS({
|
||||
} else if (xp) {
|
||||
ret = `>=${M}.${m}.0${pr} <${M}.${+m + 1}.0-0`;
|
||||
}
|
||||
debug("xRange return", ret);
|
||||
debug2("xRange return", ret);
|
||||
return ret;
|
||||
});
|
||||
};
|
||||
var replaceStars = (comp, options) => {
|
||||
debug("replaceStars", comp, options);
|
||||
debug2("replaceStars", comp, options);
|
||||
return comp.trim().replace(re[t.STAR], "");
|
||||
};
|
||||
var replaceGTE0 = (comp, options) => {
|
||||
debug("replaceGTE0", comp, options);
|
||||
debug2("replaceGTE0", comp, options);
|
||||
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) => {
|
||||
@@ -28142,7 +28142,7 @@ var require_range = __commonJS({
|
||||
}
|
||||
if (version.prerelease.length && !options.includePrerelease) {
|
||||
for (let i = 0; i < set.length; i++) {
|
||||
debug(set[i].semver);
|
||||
debug2(set[i].semver);
|
||||
if (set[i].semver === Comparator.ANY) {
|
||||
continue;
|
||||
}
|
||||
@@ -28179,7 +28179,7 @@ var require_comparator = __commonJS({
|
||||
}
|
||||
}
|
||||
comp = comp.trim().split(/\s+/).join(" ");
|
||||
debug("comparator", comp, options);
|
||||
debug2("comparator", comp, options);
|
||||
this.options = options;
|
||||
this.loose = !!options.loose;
|
||||
this.parse(comp);
|
||||
@@ -28188,7 +28188,7 @@ var require_comparator = __commonJS({
|
||||
} else {
|
||||
this.value = this.operator + this.semver.version;
|
||||
}
|
||||
debug("comp", this);
|
||||
debug2("comp", this);
|
||||
}
|
||||
parse(comp) {
|
||||
const r = this.options.loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR];
|
||||
@@ -28210,7 +28210,7 @@ var require_comparator = __commonJS({
|
||||
return this.value;
|
||||
}
|
||||
test(version) {
|
||||
debug("Comparator.test", version, this.options.loose);
|
||||
debug2("Comparator.test", version, this.options.loose);
|
||||
if (this.semver === ANY || version === ANY) {
|
||||
return true;
|
||||
}
|
||||
@@ -28267,7 +28267,7 @@ var require_comparator = __commonJS({
|
||||
var parseOptions = require_parse_options();
|
||||
var { safeRe: re, t } = require_re();
|
||||
var cmp = require_cmp();
|
||||
var debug = require_debug();
|
||||
var debug2 = require_debug();
|
||||
var SemVer = require_semver();
|
||||
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
|
||||
var liftArray = (data) => Array.isArray(data) ? data : [data];
|
||||
var spliterate = (arr, predicate) => {
|
||||
@@ -37299,178 +37477,6 @@ var schema = ark.schema;
|
||||
var define2 = ark.define;
|
||||
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
|
||||
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>`;
|
||||
@@ -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
|
||||
var core4 = __toESM(require_core(), 1);
|
||||
var core3 = __toESM(require_core(), 1);
|
||||
|
||||
// external.ts
|
||||
var agentsManifest = {
|
||||
@@ -41258,7 +41235,7 @@ var Effort = type.enumerated("mini", "auto", "max");
|
||||
// package.json
|
||||
var package_default = {
|
||||
name: "@pullfrog/pullfrog",
|
||||
version: "0.0.164",
|
||||
version: "0.0.165",
|
||||
type: "module",
|
||||
files: [
|
||||
"index.js",
|
||||
@@ -41278,7 +41255,8 @@ var package_default = {
|
||||
runtest: "node test/run.ts",
|
||||
scratch: "node scratch.ts",
|
||||
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"
|
||||
},
|
||||
dependencies: {
|
||||
@@ -41337,7 +41315,9 @@ var package_default = {
|
||||
types: "./dist/index.d.cts",
|
||||
import: "./dist/index.js",
|
||||
require: "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"./internal": "./dist/internal.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
packageManager: "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
|
||||
};
|
||||
@@ -41369,6 +41349,7 @@ var JsonPayload = type({
|
||||
version: "string",
|
||||
"agent?": AgentName.or("undefined"),
|
||||
prompt: "string",
|
||||
"triggeringUser?": "string | undefined",
|
||||
"eventInstructions?": "string",
|
||||
"repoInstructions?": "string",
|
||||
"event?": "object",
|
||||
@@ -41389,7 +41370,7 @@ var Inputs = type({
|
||||
"cwd?": type.string.or("undefined")
|
||||
});
|
||||
function resolvePromptInput() {
|
||||
const prompt = core4.getInput("prompt", { required: true });
|
||||
const prompt = core3.getInput("prompt", { required: true });
|
||||
let parsed2;
|
||||
try {
|
||||
parsed2 = JSON.parse(prompt);
|
||||
@@ -41404,8 +41385,35 @@ function resolvePromptInput() {
|
||||
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;
|
||||
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) {
|
||||
if (!promptInput?.progressCommentId) {
|
||||
log.info("[post] no progressCommentId in prompt input, skipping cleanup");
|
||||
@@ -41427,11 +41435,12 @@ async function validateStuckProgressComment(promptInput, octokit, owner, repo) {
|
||||
return null;
|
||||
} catch (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;
|
||||
}
|
||||
}
|
||||
async function getIsCancelled(params) {
|
||||
if (!params.runIdStr) return false;
|
||||
try {
|
||||
const { data: jobs } = await params.octokit.rest.actions.listJobsForWorkflowRun({
|
||||
owner: params.repoContext.owner,
|
||||
@@ -41453,7 +41462,7 @@ async function getIsCancelled(params) {
|
||||
}
|
||||
log.info("[post] no cancellation found, assuming failure");
|
||||
} catch (error2) {
|
||||
log.warning(
|
||||
log.info(
|
||||
`[post] failed to get job status: ${error2 instanceof Error ? error2.message : String(error2)}`
|
||||
);
|
||||
}
|
||||
@@ -41462,13 +41471,12 @@ async function getIsCancelled(params) {
|
||||
async function runPostCleanup() {
|
||||
log.info("\xBB [post] starting post cleanup");
|
||||
const runIdStr = process.env.GITHUB_RUN_ID;
|
||||
if (!runIdStr) return log.info("\xBB [post] no GITHUB_RUN_ID available, skipping cleanup");
|
||||
let promptInput = null;
|
||||
try {
|
||||
const resolved = resolvePromptInput();
|
||||
if (typeof resolved !== "string") promptInput = resolved;
|
||||
} catch (error2) {
|
||||
log.warning(
|
||||
log.info(
|
||||
`[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");
|
||||
} catch (error2) {
|
||||
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
||||
log.error(`[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}`);
|
||||
log.info(`[post] failed to update comment: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
// post.ts
|
||||
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:
|
||||
|
||||
undici/lib/fetch/body.js:
|
||||
|
||||
@@ -6,180 +6,14 @@
|
||||
* 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 { buildErrorCommentBody } from "./utils/exitHandler.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
|
||||
}
|
||||
}
|
||||
import { runPostCleanup } from "./utils/postCleanup.ts";
|
||||
|
||||
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 { describe, expect, it } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
import { agentsManifest } from "../external.ts";
|
||||
import { agentsManifest, type WorkflowPermissions } from "../external.ts";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const actionDir = join(__dirname, "..");
|
||||
@@ -13,7 +13,7 @@ const rootDir = join(actionDir, "..");
|
||||
type WorkflowJob = {
|
||||
"runs-on": string;
|
||||
"timeout-minutes"?: number;
|
||||
permissions?: Record<string, string>;
|
||||
permissions?: WorkflowPermissions;
|
||||
strategy?: { "fail-fast": boolean; matrix: Record<string, string[]> };
|
||||
env?: Record<string, string>;
|
||||
steps?: unknown[];
|
||||
|
||||
+1
-6
@@ -5,11 +5,7 @@ import { config } from "dotenv";
|
||||
import { runInDocker } from "../utils/docker.ts";
|
||||
import { ensureGitHubToken } from "../utils/github.ts";
|
||||
import { isInsideDocker } from "../utils/globals.ts";
|
||||
import {
|
||||
installSignalHandlers,
|
||||
killTrackedChildren,
|
||||
setSignalHandler,
|
||||
} from "../utils/subprocess.ts";
|
||||
import { killTrackedChildren, setSignalHandler } from "../utils/subprocess.ts";
|
||||
import {
|
||||
type AgentResult,
|
||||
agents,
|
||||
@@ -482,7 +478,6 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
setSignalHandler(handleCancel);
|
||||
installSignalHandlers();
|
||||
|
||||
// run tests with limited concurrency to avoid overwhelming agent APIs
|
||||
const maxConcurrency = 5;
|
||||
|
||||
+1
-3
@@ -5,7 +5,7 @@ import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { agentsManifest } from "../external.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));
|
||||
|
||||
@@ -165,8 +165,6 @@ const DEFAULT_TEST_TIMEOUT = "10m";
|
||||
// run agent and stream output with prefix labels
|
||||
// note: activity timeout is enforced in action main and subprocess utils
|
||||
export async function runAgentStreaming(options: RunStreamingOptions): Promise<AgentResult> {
|
||||
installSignalHandlers();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const chunks: Buffer[] = [];
|
||||
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 type { ToolState } from "../mcp/server.ts";
|
||||
import { buildPullfrogFooter } from "./buildPullfrogFooter.ts";
|
||||
import { log } from "./cli.ts";
|
||||
import { createOctokit, parseRepoContext } from "./github.ts";
|
||||
import { revokeGitHubInstallationToken } from "./token.ts";
|
||||
import os from "node:os";
|
||||
|
||||
type ExitSignalHandler = (signal: "SIGINT" | "SIGTERM") => void | Promise<void>;
|
||||
|
||||
const handlers = new Set<ExitSignalHandler>();
|
||||
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: {
|
||||
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}`;
|
||||
export function onExitSignal(handler: ExitSignalHandler): () => void {
|
||||
installSignalHandlers();
|
||||
handlers.add(handler);
|
||||
return () => {
|
||||
handlers.delete(handler);
|
||||
};
|
||||
}
|
||||
|
||||
let cleanupFn: ((isCancellation: boolean) => Promise<void>) | undefined;
|
||||
function installSignalHandlers(): void {
|
||||
if (installed) return;
|
||||
installed = true;
|
||||
|
||||
export function setupExitHandler(toolState: ToolState): void {
|
||||
let hasCleanedUp = false;
|
||||
|
||||
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));
|
||||
async function handleSignal(signal: "SIGINT" | "SIGTERM") {
|
||||
await Promise.allSettled([...handlers].map((h) => Promise.try(h, signal)));
|
||||
exitWithSignal(signal);
|
||||
}
|
||||
|
||||
process.on("SIGINT", handleSignal);
|
||||
process.on("SIGTERM", handleSignal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run cleanup explicitly. Called from entry.ts in finally block.
|
||||
*/
|
||||
export async function runCleanup(): Promise<void> {
|
||||
try {
|
||||
await cleanupFn?.(false);
|
||||
} catch {
|
||||
// ignore errors during cleanup
|
||||
}
|
||||
export function exitWithSignal(signal: "SIGINT" | "SIGTERM") {
|
||||
process.exit(128 + os.constants.signals[signal]);
|
||||
}
|
||||
|
||||
+1
-1
@@ -161,7 +161,7 @@ export function $git(
|
||||
|
||||
if (result.status !== 0) {
|
||||
const stderr = result.stderr?.trim() ?? "";
|
||||
log.error(`git ${subcommand} failed: ${stderr}`);
|
||||
log.info(`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 { isGitHubActions, isInsideDocker } from "./globals.ts";
|
||||
|
||||
const isDebugEnabled = () =>
|
||||
process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true" || core.isDebug();
|
||||
const isRunnerDebugEnabled = () => 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
|
||||
@@ -206,23 +215,18 @@ function separator(length: number = 50): void {
|
||||
/**
|
||||
* 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 = {
|
||||
/** Print info message */
|
||||
info: (...args: unknown[]): void => {
|
||||
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 => {
|
||||
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 => {
|
||||
core.error(`${ts()}${formatArgs(args)}`);
|
||||
},
|
||||
@@ -232,10 +236,14 @@ export const log = {
|
||||
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 => {
|
||||
if (isDebugEnabled()) {
|
||||
core.info(`[${new Date().toISOString()}] [DEBUG] ${formatArgs(args)}`);
|
||||
if (isRunnerDebugEnabled()) {
|
||||
core.debug(formatArgs(args));
|
||||
return;
|
||||
}
|
||||
if (isLocalDebugEnabled()) {
|
||||
core.info(`${ts()}[DEBUG] ${formatArgs(args)}`);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export const JsonPayload = type({
|
||||
version: "string",
|
||||
"agent?": AgentName.or("undefined"),
|
||||
prompt: "string",
|
||||
"triggeringUser?": "string | undefined",
|
||||
"eventInstructions?": "string",
|
||||
"repoInstructions?": "string",
|
||||
"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(
|
||||
resolvedPromptInput: ResolvedPromptInput,
|
||||
repoSettings: RepoSettings
|
||||
@@ -161,6 +167,10 @@ export function resolvePayload(
|
||||
version: jsonPayload?.version ?? packageJson.version,
|
||||
agent: resolvedAgent,
|
||||
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,
|
||||
repoInstructions: jsonPayload?.repoInstructions,
|
||||
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;
|
||||
log.warning(
|
||||
`» ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`
|
||||
);
|
||||
log.info(`» ${label} failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -121,9 +121,7 @@ export async function setupGit(params: SetupGitParams): Promise<void> {
|
||||
} catch (error) {
|
||||
// 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
|
||||
log.warning(
|
||||
`Failed to set git config: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
log.info(`Failed to set git config: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
// 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 { DEFAULT_ACTIVITY_CHECK_INTERVAL_MS, DEFAULT_ACTIVITY_TIMEOUT_MS } from "./activity.ts";
|
||||
import { log } from "./cli.ts";
|
||||
import { onExitSignal } from "./exitHandler.ts";
|
||||
|
||||
export type TrackChildOptions = {
|
||||
child: ChildProcess;
|
||||
@@ -18,6 +19,9 @@ let externalSignalHandler: SignalHandler | null = null;
|
||||
|
||||
// track a child process for cleanup on Ctrl+C
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -32,8 +36,7 @@ export function setSignalHandler(handler: SignalHandler | null): void {
|
||||
}
|
||||
|
||||
// kill all tracked children without exiting
|
||||
export function killTrackedChildren(): number {
|
||||
const count = activeChildren.size;
|
||||
export function killTrackedChildren() {
|
||||
for (const entry of activeChildren) {
|
||||
const child = entry[0];
|
||||
const killGroup = entry[1];
|
||||
@@ -47,35 +50,24 @@ export function killTrackedChildren(): number {
|
||||
}
|
||||
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)
|
||||
let handlersInstalled = false;
|
||||
export function installSignalHandlers(): void {
|
||||
function installSignalHandler(): void {
|
||||
if (handlersInstalled) return;
|
||||
handlersInstalled = true;
|
||||
process.on("SIGINT", () => handleSignal("SIGINT"));
|
||||
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
||||
onExitSignal((signal) => {
|
||||
if (externalSignalHandler) {
|
||||
externalSignalHandler(signal);
|
||||
return;
|
||||
}
|
||||
const count = activeChildren.size;
|
||||
if (count > 0) {
|
||||
log.info(`» received ${signal}, killing ${count} subprocess(es)...`);
|
||||
}
|
||||
killTrackedChildren();
|
||||
});
|
||||
}
|
||||
|
||||
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 activityTimeoutMs = options.activityTimeout ?? DEFAULT_ACTIVITY_TIMEOUT_MS;
|
||||
|
||||
installSignalHandlers();
|
||||
installSignalHandler();
|
||||
|
||||
const startTime = performance.now();
|
||||
let stdoutBuffer = "";
|
||||
@@ -157,7 +149,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
|
||||
if (idleMs > activityTimeoutMs) {
|
||||
isActivityTimedOut = true;
|
||||
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");
|
||||
clearInterval(activityCheckIntervalId);
|
||||
}
|
||||
|
||||
+25
-7
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import * as core from "@actions/core";
|
||||
import type { PushPermission } from "../external.ts";
|
||||
import { log } from "./cli.ts";
|
||||
import { onExitSignal } from "./exitHandler.ts";
|
||||
import { acquireNewToken } from "./github.ts";
|
||||
import { isGitHubActions } from "./globals.ts";
|
||||
|
||||
@@ -131,18 +132,35 @@ export async function resolveTokens(params: ResolveTokensParams): Promise<TokenR
|
||||
const revertGithubToken = setEnvironmentVariable("GITHUB_TOKEN", mcpToken);
|
||||
mcpTokenValue = mcpToken;
|
||||
|
||||
return {
|
||||
gitToken,
|
||||
mcpToken,
|
||||
async [Symbol.asyncDispose]() {
|
||||
let disposingRef: PromiseWithResolvers<void> | undefined;
|
||||
|
||||
const dispose = async () => {
|
||||
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;
|
||||
revertGithubToken();
|
||||
// revoke both tokens
|
||||
await Promise.all([
|
||||
revokeGitHubInstallationToken(gitToken),
|
||||
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");
|
||||
} catch (error) {
|
||||
log.warning(
|
||||
log.info(
|
||||
`Failed to revoke installation token: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
|
||||
+2
-4
@@ -1,9 +1,7 @@
|
||||
import { resolve } from "node:path";
|
||||
import { config } from "dotenv";
|
||||
import { ensureGitHubToken } from "./utils/github.ts";
|
||||
|
||||
config({ path: resolve(import.meta.dirname, "../.env") });
|
||||
|
||||
// alias GITHUB_TOKEN to GH_TOKEN for tests
|
||||
if (!process.env.GH_TOKEN && process.env.GITHUB_TOKEN) {
|
||||
process.env.GH_TOKEN = process.env.GITHUB_TOKEN;
|
||||
}
|
||||
await ensureGitHubToken();
|
||||
|
||||
Reference in New Issue
Block a user