From 7f8141525984c52db7a0b8653e399be0c0d1b7d5 Mon Sep 17 00:00:00 2001 From: Shawn Morreau Date: Thu, 4 Dec 2025 15:05:53 -0500 Subject: [PATCH] update working comment on error --- main.ts | 45 ++++++++----- mcp/comment.ts | 155 +++++++++++++++++++++++++++++++------------ utils/errorReport.ts | 45 +++++++++++++ 3 files changed, 184 insertions(+), 61 deletions(-) create mode 100644 utils/errorReport.ts diff --git a/main.ts b/main.ts index f9d77cf..e82c18c 100644 --- a/main.ts +++ b/main.ts @@ -8,12 +8,14 @@ import { agents } from "./agents/index.ts"; import type { AgentResult } from "./agents/shared.ts"; import type { AgentName, AgentName as AgentNameType, Payload } from "./external.ts"; import { agentsManifest } from "./external.ts"; +import { ensureProgressCommentUpdated } from "./mcp/comment.ts"; import { createMcpConfigs } from "./mcp/config.ts"; import { startMcpHttpServer } from "./mcp/server.ts"; import { modes } from "./modes.ts"; import packageJson from "./package.json" with { type: "json" }; import { fetchRepoSettings, fetchWorkflowRunInfo } from "./utils/api.ts"; import { log } from "./utils/cli.ts"; +import { reportErrorToComment } from "./utils/errorReport.ts"; import { parseRepoContext, type RepoContext, @@ -68,14 +70,20 @@ export async function main(inputs: Inputs): Promise { mcpServerClose = ctx.mcpServerClose; setupMcpServers(ctx); await installAgentCli(ctx); - validateApiKey(ctx); + await validateApiKey(ctx); const result = await runAgent(ctx); - return await handleAgentResult(result); + const mainResult = await handleAgentResult(result); + // ensure progress comment is updated if it was never updated during execution + await ensureProgressCommentUpdated(); + return mainResult; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; log.error(errorMessage); + await reportErrorToComment({ error: errorMessage }); await log.writeSummary(); + // ensure progress comment is updated if it was never updated during execution + await ensureProgressCommentUpdated(); return { success: false, error: errorMessage, @@ -111,18 +119,17 @@ function getAllPossibleKeyNames(): string[] { /** * Throw an error for missing API key with helpful message linking to repo settings */ -function throwMissingApiKeyError({ - agentName, - inputKeys, +async function throwMissingApiKeyError({ + agent, repoContext, }: { - agentName: string | null; - inputKeys: string[]; + agent: (typeof agents)[AgentNameType] | null; repoContext: RepoContext; -}): never { +}): Promise { const apiUrl = process.env.API_URL || "https://pullfrog.ai"; const settingsUrl = `${apiUrl}/console/${repoContext.owner}/${repoContext.name}`; + const inputKeys = agent?.apiKeyNames || getAllPossibleKeyNames(); const secretNames = inputKeys.map((key) => `\`${key.toUpperCase()}\``); const secretNameList = inputKeys.length === 1 ? secretNames[0] : `one of ${secretNames.join(" or ")}`; @@ -131,9 +138,9 @@ function throwMissingApiKeyError({ const githubSecretsUrl = `${githubRepoUrl}/settings/secrets/actions`; let message = `${ - agentName === null + agent === null ? "Pullfrog has no agent configured and no API keys are available in the environment." - : `Pullfrog is configured to use ${agentName}, but the associated API key was not provided.` + : `Pullfrog is configured to use ${agent.displayName}, but the associated API key was not provided.` } To fix this, add the required secret to your GitHub repository: @@ -144,11 +151,13 @@ To fix this, add the required secret to your GitHub repository: 4. Set the value to your API key 5. Click "Add secret"`; - if (agentName === null) { + if (agent === null) { message += `\n\nAlternatively, configure Pullfrog to use an agent at ${settingsUrl}`; } log.error(message); + // report to comment if MCP context is available (server has started) + await reportErrorToComment({ error: message }); throw new Error(message); } @@ -229,9 +238,8 @@ async function resolveAgent( log.debug(`Available agents: ${availableAgentNames || "none"}`); if (availableAgents.length === 0) { - throwMissingApiKeyError({ - agentName: configuredAgentName, - inputKeys: getAllPossibleKeyNames(), + await throwMissingApiKeyError({ + agent: null, repoContext, }); } @@ -306,14 +314,15 @@ async function installAgentCli(ctx: MainContext): Promise { } } -function validateApiKey(ctx: MainContext): void { +async function validateApiKey(ctx: MainContext): Promise { const matchingInputKey = ctx.agent.apiKeyNames.find((inputKey) => ctx.inputs[inputKey]); if (!matchingInputKey) { - throwMissingApiKeyError({ - agentName: ctx.agentName, - inputKeys: ctx.agent.apiKeyNames, + await throwMissingApiKeyError({ + agent: ctx.agent, repoContext: ctx.repoContext, }); + // unreachable - throwMissingApiKeyError always throws + return; } ctx.apiKey = ctx.inputs[matchingInputKey]!; } diff --git a/mcp/comment.ts b/mcp/comment.ts index 2cf8d04..4f16951 100644 --- a/mcp/comment.ts +++ b/mcp/comment.ts @@ -2,7 +2,7 @@ import { type } from "arktype"; import type { Payload } from "../external.ts"; import { agentsManifest } from "../external.ts"; import { parseRepoContext } from "../utils/github.ts"; -import { contextualize, tool } from "./shared.ts"; +import { contextualize, getMcpContext, tool } from "./shared.ts"; const PULLFROG_DIVIDER = ""; @@ -117,6 +117,9 @@ function getProgressCommentIdFromEnv(): number | null { let progressCommentId: number | null = null; let progressCommentIdInitialized = false; +// track whether the progress comment was updated during execution +let progressCommentWasUpdated = false; + function getProgressCommentId(): number | null { if (!progressCommentIdInitialized) { progressCommentId = getProgressCommentIdFromEnv(); @@ -134,66 +137,132 @@ export const ReportProgress = type({ body: type.string.describe("the progress update content to share"), }); +/** + * Standalone function to report progress to GitHub comment. + * Can be called directly without going through the MCP tool interface. + * Returns result data if successful, undefined if comment cannot be created. + */ +export async function reportProgress({ body }: { body: string }): Promise< + | { + commentId: number; + url: string; + body: string; + action: "created" | "updated"; + } + | undefined +> { + const ctx = getMcpContext(); + + const bodyWithFooter = addFooter(body, ctx.payload); + const existingCommentId = getProgressCommentId(); + + // if we already have a progress comment, update it + if (existingCommentId) { + const result = await ctx.octokit.rest.issues.updateComment({ + owner: ctx.owner, + repo: ctx.name, + comment_id: existingCommentId, + body: bodyWithFooter, + }); + + progressCommentWasUpdated = true; + + return { + commentId: result.data.id, + url: result.data.html_url, + body: result.data.body || "", + action: "updated", + }; + } + + // no existing comment - create one + const issueNumber = ctx.payload.event.issue_number; + if (issueNumber === undefined) { + // fail silently - cannot create comment without issue_number + return undefined; + } + + const result = await ctx.octokit.rest.issues.createComment({ + owner: ctx.owner, + repo: ctx.name, + issue_number: issueNumber, + body: bodyWithFooter, + }); + + // store the comment ID for future updates + setProgressCommentId(result.data.id); + progressCommentWasUpdated = true; + + return { + commentId: result.data.id, + url: result.data.html_url, + body: result.data.body || "", + action: "created", + }; +} + export const ReportProgressTool = tool({ name: "report_progress", description: "Share progress on the associated GitHub issue/PR. Call this to post updates as you work. The first call creates a comment, subsequent calls update it. Use this throughout your work to keep stakeholders informed.", parameters: ReportProgress, - execute: contextualize(async ({ body }, ctx) => { - const bodyWithFooter = addFooter(body, ctx.payload); - const existingCommentId = getProgressCommentId(); + execute: contextualize(async ({ body }) => { + const result = await reportProgress({ body }); - // if we already have a progress comment, update it - if (existingCommentId) { - const result = await ctx.octokit.rest.issues.updateComment({ - owner: ctx.owner, - repo: ctx.name, - comment_id: existingCommentId, - body: bodyWithFooter, - }); - - return { - success: true, - commentId: result.data.id, - url: result.data.html_url, - body: result.data.body, - action: "updated", - }; - } - - // no existing comment - create one - const issueNumber = ctx.payload.event.issue_number; - if (issueNumber === undefined) { - // fail silently + if (!result) { return { success: false, message: "cannot create progress comment: no issue_number found in the payload event", }; - // throw new Error( - // "cannot create progress comment: no issue_number found in the payload event" - // ); } - const result = await ctx.octokit.rest.issues.createComment({ - owner: ctx.owner, - repo: ctx.name, - issue_number: issueNumber, - body: bodyWithFooter, - }); - - // store the comment ID for future updates - setProgressCommentId(result.data.id); - return { success: true, - commentId: result.data.id, - url: result.data.html_url, - body: result.data.body, - action: "created", + ...result, }; }), }); +/** + * Check if the progress comment was updated during execution + */ +export function wasProgressCommentUpdated(): boolean { + return progressCommentWasUpdated; +} + +/** + * Ensure the progress comment is updated with a generic error message if it was never updated. + * This should be called after agent execution completes to handle cases where the agent + * exited without ever calling reportProgress. + */ +export async function ensureProgressCommentUpdated(): Promise { + // only update if we have a progress comment ID from env var and it was never updated + const existingCommentId = getProgressCommentId(); + if (!existingCommentId || progressCommentWasUpdated) { + return; + } + + // check if MCP context is initialized (MCP server started) + try { + const ctx = getMcpContext(); + const errorMessage = `🐸 this run croaked + +The workflow encountered an error before any progress could be reported. Please check the workflow run logs for details.`; + + const bodyWithFooter = addFooter(errorMessage, ctx.payload); + + await ctx.octokit.rest.issues.updateComment({ + owner: ctx.owner, + repo: ctx.name, + comment_id: existingCommentId, + body: bodyWithFooter, + }); + } catch { + // fail silently - MCP context not initialized or other error + // don't want to fail the workflow if we can't update the comment + } +} + export const ReplyToReviewComment = type({ pull_number: type.number.describe("the pull request number"), comment_id: type.number.describe("the ID of the review comment to reply to"), diff --git a/utils/errorReport.ts b/utils/errorReport.ts new file mode 100644 index 0000000..7470687 --- /dev/null +++ b/utils/errorReport.ts @@ -0,0 +1,45 @@ +import { log } from "./cli.ts"; +import { reportProgress } from "../mcp/comment.ts"; +import { getMcpContext } from "../mcp/shared.ts"; + +/** + * Check if MCP context is initialized (i.e., MCP server has started) + */ +function isMcpContextInitialized(): boolean { + try { + getMcpContext(); + return true; + } catch { + return false; + } +} + +/** + * Report an error to the GitHub working comment. + * Formats the error message for GitHub markdown and updates the progress comment. + * Handles failures gracefully - logs but doesn't throw. + */ +export async function reportErrorToComment({ + error, + title, +}: { + error: string; + title?: string; +}): Promise { + // only report if MCP context is initialized (MCP server has started) + if (!isMcpContextInitialized()) { + log.debug("skipping error comment update: MCP context not initialized"); + return; + } + + try { + const formattedError = title ? `${title}\n\n${error}` : `❌ ${error}`; + await reportProgress({ body: formattedError }); + } catch (reportError) { + // log but don't throw - we don't want error reporting to fail the workflow + const errorMessage = + reportError instanceof Error ? reportError.message : String(reportError); + log.warning(`failed to report error to comment: ${errorMessage}`); + } +} +