Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb7d9e0d34 | |||
| dcbac16663 |
@@ -1,4 +1,5 @@
|
|||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { log } from "../utils/cli.ts";
|
import { log } from "../utils/cli.ts";
|
||||||
import { spawn } from "../utils/subprocess.ts";
|
import { spawn } from "../utils/subprocess.ts";
|
||||||
@@ -10,6 +11,14 @@ import {
|
|||||||
setupProcessAgentEnv,
|
setupProcessAgentEnv,
|
||||||
} from "./shared.ts";
|
} from "./shared.ts";
|
||||||
|
|
||||||
|
// import { createOpencode } from "@opencode-ai/sdk"
|
||||||
|
|
||||||
|
// const { client } = await createOpencode({
|
||||||
|
// config: {
|
||||||
|
// ''
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
// opencode cli event types inferred from json output format
|
// opencode cli event types inferred from json output format
|
||||||
interface OpenCodeInitEvent {
|
interface OpenCodeInitEvent {
|
||||||
type: "init";
|
type: "init";
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
Use the debug_shell_command MCP tool to run `git status` and tell me what agent is running
|
Tell me a joke
|
||||||
@@ -52,12 +52,13 @@ export interface MainResult {
|
|||||||
|
|
||||||
export async function main(inputs: Inputs): Promise<MainResult> {
|
export async function main(inputs: Inputs): Promise<MainResult> {
|
||||||
let mcpServerClose: (() => Promise<void>) | undefined;
|
let mcpServerClose: (() => Promise<void>) | undefined;
|
||||||
|
let payload: Payload | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const timer = new Timer();
|
const timer = new Timer();
|
||||||
|
|
||||||
// parse payload early to extract agent
|
// parse payload early to extract agent
|
||||||
const payload = parsePayload(inputs);
|
payload = parsePayload(inputs);
|
||||||
|
|
||||||
const partialCtx = await initializeContext(inputs, payload);
|
const partialCtx = await initializeContext(inputs, payload);
|
||||||
const ctx = partialCtx as MainContext;
|
const ctx = partialCtx as MainContext;
|
||||||
@@ -86,21 +87,29 @@ export async function main(inputs: Inputs): Promise<MainResult> {
|
|||||||
|
|
||||||
const result = await runAgent(ctx);
|
const result = await runAgent(ctx);
|
||||||
const mainResult = await handleAgentResult(result);
|
const mainResult = await handleAgentResult(result);
|
||||||
// ensure progress comment is updated if it was never updated during execution
|
|
||||||
await ensureProgressCommentUpdated();
|
|
||||||
return mainResult;
|
return mainResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||||
log.error(errorMessage);
|
log.error(errorMessage);
|
||||||
await reportErrorToComment({ error: errorMessage });
|
try {
|
||||||
|
await reportErrorToComment({ error: errorMessage });
|
||||||
|
} catch {
|
||||||
|
// error reporting failed, but don't let it mask the original error
|
||||||
|
}
|
||||||
await log.writeSummary();
|
await log.writeSummary();
|
||||||
// ensure progress comment is updated if it was never updated during execution
|
|
||||||
await ensureProgressCommentUpdated();
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
|
// ensure progress comment is updated if it was never updated during execution
|
||||||
|
// do this before revoking the token so we can still make API calls
|
||||||
|
try {
|
||||||
|
await ensureProgressCommentUpdated(payload);
|
||||||
|
} catch {
|
||||||
|
// error updating comment, but don't let it mask the original error
|
||||||
|
}
|
||||||
|
|
||||||
if (mcpServerClose) {
|
if (mcpServerClose) {
|
||||||
await mcpServerClose();
|
await mcpServerClose();
|
||||||
}
|
}
|
||||||
|
|||||||
+81
-32
@@ -1,7 +1,10 @@
|
|||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
|
import { LEAPING_INTO_ACTION_PREFIX } from "../../utils/github/leapingComment.ts";
|
||||||
import type { Payload } from "../external.ts";
|
import type { Payload } from "../external.ts";
|
||||||
import { agentsManifest } from "../external.ts";
|
import { agentsManifest } from "../external.ts";
|
||||||
import { parseRepoContext } from "../utils/github.ts";
|
import { fetchWorkflowRunInfo } from "../utils/api.ts";
|
||||||
|
import { getGitHubInstallationToken, parseRepoContext } from "../utils/github.ts";
|
||||||
import { contextualize, getMcpContext, tool } from "./shared.ts";
|
import { contextualize, getMcpContext, tool } from "./shared.ts";
|
||||||
|
|
||||||
const PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
const PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
|
||||||
@@ -178,8 +181,7 @@ export async function reportProgress({ body }: { body: string }): Promise<
|
|||||||
// no existing comment - create one
|
// no existing comment - create one
|
||||||
const issueNumber = ctx.payload.event.issue_number;
|
const issueNumber = ctx.payload.event.issue_number;
|
||||||
if (issueNumber === undefined) {
|
if (issueNumber === undefined) {
|
||||||
// fail silently - cannot create comment without issue_number
|
throw new Error("cannot create progress comment: no issue_number found in the payload event");
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await ctx.octokit.rest.issues.createComment({
|
const result = await ctx.octokit.rest.issues.createComment({
|
||||||
@@ -209,13 +211,6 @@ export const ReportProgressTool = tool({
|
|||||||
execute: contextualize(async ({ body }) => {
|
execute: contextualize(async ({ body }) => {
|
||||||
const result = await reportProgress({ body });
|
const result = await reportProgress({ body });
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "cannot create progress comment: no issue_number found in the payload event",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
...result,
|
...result,
|
||||||
@@ -234,39 +229,93 @@ export function wasProgressCommentUpdated(): boolean {
|
|||||||
* Ensure the progress comment is updated with a generic error message if it was never updated.
|
* 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
|
* This should be called after agent execution completes to handle cases where the agent
|
||||||
* exited without ever calling reportProgress.
|
* exited without ever calling reportProgress.
|
||||||
|
*
|
||||||
|
* Works even if MCP context is not initialized (e.g., if error occurs before MCP server starts).
|
||||||
|
* Will fetch comment ID from database if not available in environment variable.
|
||||||
*/
|
*/
|
||||||
export async function ensureProgressCommentUpdated(): Promise<void> {
|
export async function ensureProgressCommentUpdated(payload?: Payload): Promise<void> {
|
||||||
// only update if we have a progress comment ID from env var and it was never updated
|
// skip if comment was already updated during execution
|
||||||
const existingCommentId = getProgressCommentId();
|
if (progressCommentWasUpdated) {
|
||||||
if (!existingCommentId || progressCommentWasUpdated) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if MCP context is initialized (MCP server started)
|
// try to get comment ID from env var first, then from database if needed
|
||||||
|
let existingCommentId = getProgressCommentId();
|
||||||
|
|
||||||
|
// if not in env var, try fetching from database using run ID
|
||||||
|
if (!existingCommentId) {
|
||||||
|
const runId = process.env.GITHUB_RUN_ID;
|
||||||
|
if (runId) {
|
||||||
|
try {
|
||||||
|
const workflowRunInfo = await fetchWorkflowRunInfo(runId);
|
||||||
|
if (workflowRunInfo.progressCommentId) {
|
||||||
|
existingCommentId = parseInt(workflowRunInfo.progressCommentId, 10);
|
||||||
|
// cache it in env var for future use
|
||||||
|
if (!Number.isNaN(existingCommentId)) {
|
||||||
|
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// database fetch failed, continue without comment ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if still no comment ID, nothing to update
|
||||||
|
if (!existingCommentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if comment still says "leaping into action" - if it's been updated with an error, don't overwrite it
|
||||||
|
const repoContext = parseRepoContext();
|
||||||
|
const token = getGitHubInstallationToken();
|
||||||
|
const octokit = new Octokit({ auth: token });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingComment = await octokit.rest.issues.getComment({
|
||||||
|
owner: repoContext.owner,
|
||||||
|
repo: repoContext.name,
|
||||||
|
comment_id: existingCommentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const commentBody = existingComment.data.body || "";
|
||||||
|
// if comment doesn't start with the leaping prefix, it's already been updated with an error or progress
|
||||||
|
if (!commentBody.startsWith(LEAPING_INTO_ACTION_PREFIX)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// can't fetch comment, skip update
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to get payload from MCP context if available, otherwise use provided payload
|
||||||
|
let resolvedPayload: Payload | undefined;
|
||||||
try {
|
try {
|
||||||
const ctx = getMcpContext();
|
const ctx = getMcpContext();
|
||||||
const repoContext = parseRepoContext();
|
resolvedPayload = ctx.payload;
|
||||||
const runId = process.env.GITHUB_RUN_ID;
|
} catch {
|
||||||
const workflowRunLink = runId
|
// MCP context not initialized, use provided payload
|
||||||
? `[workflow](https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId})`
|
resolvedPayload = payload;
|
||||||
: "workflow";
|
}
|
||||||
|
|
||||||
const errorMessage = `❌ this run croaked
|
const runId = process.env.GITHUB_RUN_ID;
|
||||||
|
const workflowRunLink = runId
|
||||||
|
? `[workflow](https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId})`
|
||||||
|
: "workflow";
|
||||||
|
|
||||||
|
const errorMessage = `❌ this run croaked
|
||||||
|
|
||||||
The workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
|
The workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
|
||||||
|
|
||||||
const bodyWithFooter = addFooter(errorMessage, ctx.payload);
|
// add footer if we have payload, otherwise use plain message
|
||||||
|
const body = resolvedPayload ? addFooter(errorMessage, resolvedPayload) : errorMessage;
|
||||||
|
|
||||||
await ctx.octokit.rest.issues.updateComment({
|
await octokit.rest.issues.updateComment({
|
||||||
owner: ctx.owner,
|
owner: repoContext.owner,
|
||||||
repo: ctx.name,
|
repo: repoContext.name,
|
||||||
comment_id: existingCommentId,
|
comment_id: existingCommentId,
|
||||||
body: bodyWithFooter,
|
body,
|
||||||
});
|
});
|
||||||
} 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({
|
export const ReplyToReviewComment = type({
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/action",
|
"name": "@pullfrog/action",
|
||||||
"version": "0.0.130",
|
"version": "0.0.131",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"@octokit/rest": "^22.0.0",
|
"@octokit/rest": "^22.0.0",
|
||||||
"@octokit/webhooks-types": "^7.6.1",
|
"@octokit/webhooks-types": "^7.6.1",
|
||||||
"@openai/codex-sdk": "0.58.0",
|
"@openai/codex-sdk": "0.58.0",
|
||||||
|
"@opencode-ai/sdk": "^1.0.143",
|
||||||
"@standard-schema/spec": "1.0.0",
|
"@standard-schema/spec": "1.0.0",
|
||||||
"arktype": "2.1.28",
|
"arktype": "2.1.28",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
|||||||
Generated
+8
@@ -32,6 +32,9 @@ importers:
|
|||||||
'@openai/codex-sdk':
|
'@openai/codex-sdk':
|
||||||
specifier: 0.58.0
|
specifier: 0.58.0
|
||||||
version: 0.58.0
|
version: 0.58.0
|
||||||
|
'@opencode-ai/sdk':
|
||||||
|
specifier: ^1.0.143
|
||||||
|
version: 1.0.143
|
||||||
'@standard-schema/spec':
|
'@standard-schema/spec':
|
||||||
specifier: 1.0.0
|
specifier: 1.0.0
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
@@ -440,6 +443,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Z5vK294gUS9cn7bpU/lJtgzqJy1UqIGee7WMP+1Z4a6AxDcTxFCLZI4YkH9praJfrgoj5bFeu+3V9HIoBBTzcw==}
|
resolution: {integrity: sha512-Z5vK294gUS9cn7bpU/lJtgzqJy1UqIGee7WMP+1Z4a6AxDcTxFCLZI4YkH9praJfrgoj5bFeu+3V9HIoBBTzcw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@opencode-ai/sdk@1.0.143':
|
||||||
|
resolution: {integrity: sha512-dtmkBfJ7IIAHzL6KCzAlwc9GybfJONVeCsF6ePYySpkuhslDbRkZBJYb5vqGd1H5zdsgjc6JjuvmOf0rPWUL6A==}
|
||||||
|
|
||||||
'@sec-ant/readable-stream@0.4.1':
|
'@sec-ant/readable-stream@0.4.1':
|
||||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
||||||
|
|
||||||
@@ -1410,6 +1416,8 @@ snapshots:
|
|||||||
|
|
||||||
'@openai/codex-sdk@0.58.0': {}
|
'@openai/codex-sdk@0.58.0': {}
|
||||||
|
|
||||||
|
'@opencode-ai/sdk@1.0.143': {}
|
||||||
|
|
||||||
'@sec-ant/readable-stream@0.4.1': {}
|
'@sec-ant/readable-stream@0.4.1': {}
|
||||||
|
|
||||||
'@sindresorhus/merge-streams@4.0.0': {}
|
'@sindresorhus/merge-streams@4.0.0': {}
|
||||||
|
|||||||
+57
-26
@@ -1,24 +1,21 @@
|
|||||||
import { reportProgress } from "../mcp/comment.ts";
|
import { Octokit } from "@octokit/rest";
|
||||||
import { getMcpContext } from "../mcp/shared.ts";
|
import { fetchWorkflowRunInfo } from "./api.ts";
|
||||||
import { log } from "./cli.ts";
|
import { getGitHubInstallationToken, parseRepoContext } from "./github.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if MCP context is initialized (i.e., MCP server has started)
|
* Get progress comment ID from environment variable or database.
|
||||||
*/
|
*/
|
||||||
function isMcpContextInitialized(): boolean {
|
function getProgressCommentIdFromEnv(): number | null {
|
||||||
try {
|
const envCommentId = process.env.PULLFROG_PROGRESS_COMMENT_ID;
|
||||||
getMcpContext();
|
if (envCommentId) {
|
||||||
return true;
|
const parsed = parseInt(envCommentId, 10);
|
||||||
} catch {
|
if (!Number.isNaN(parsed)) {
|
||||||
return false;
|
return parsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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({
|
export async function reportErrorToComment({
|
||||||
error,
|
error,
|
||||||
title,
|
title,
|
||||||
@@ -26,18 +23,52 @@ export async function reportErrorToComment({
|
|||||||
error: string;
|
error: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
// only report if MCP context is initialized (MCP server has started)
|
const formattedError = title ? `${title}\n\n${error}` : `❌ ${error}`;
|
||||||
if (!isMcpContextInitialized()) {
|
|
||||||
log.debug("skipping error comment update: MCP context not initialized");
|
// try to get comment ID from env var first, then from database if needed
|
||||||
return;
|
let commentId = getProgressCommentIdFromEnv();
|
||||||
|
|
||||||
|
// if not in env var, try fetching from database using run ID
|
||||||
|
if (!commentId) {
|
||||||
|
const runId = process.env.GITHUB_RUN_ID;
|
||||||
|
if (runId) {
|
||||||
|
try {
|
||||||
|
const workflowRunInfo = await fetchWorkflowRunInfo(runId);
|
||||||
|
if (workflowRunInfo.progressCommentId) {
|
||||||
|
const parsed = parseInt(workflowRunInfo.progressCommentId, 10);
|
||||||
|
if (!Number.isNaN(parsed)) {
|
||||||
|
commentId = parsed;
|
||||||
|
// cache it in env var for future use
|
||||||
|
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// database fetch failed, continue without comment ID
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// if no comment ID available, try using reportProgress (requires MCP context)
|
||||||
const formattedError = title ? `${title}\n\n${error}` : `❌ ${error}`;
|
if (!commentId) {
|
||||||
await reportProgress({ body: formattedError });
|
try {
|
||||||
} catch (reportError) {
|
const { reportProgress } = await import("../mcp/comment.ts");
|
||||||
// log but don't throw - we don't want error reporting to fail the workflow
|
await reportProgress({ body: formattedError });
|
||||||
const errorMessage = reportError instanceof Error ? reportError.message : String(reportError);
|
return;
|
||||||
log.warning(`failed to report error to comment: ${errorMessage}`);
|
} catch {
|
||||||
|
// MCP context not available, can't create/update comment
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update comment directly using GitHub API
|
||||||
|
const repoContext = parseRepoContext();
|
||||||
|
const token = getGitHubInstallationToken();
|
||||||
|
const octokit = new Octokit({ auth: token });
|
||||||
|
|
||||||
|
await octokit.rest.issues.updateComment({
|
||||||
|
owner: repoContext.owner,
|
||||||
|
repo: repoContext.name,
|
||||||
|
comment_id: commentId,
|
||||||
|
body: formattedError,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user