fix: prevent log.writeSummary from overwriting reportProgress content (#87)

* fix: prevent log.writeSummary from overwriting reportProgress content

The run summary was showing logs instead of the final reportProgress content
because log.writeSummary() was called after reportProgress. Now
log.writeSummary() checks if the summary was already overwritten by
reportProgress and skips if so.

Fixes #86

* refactor: replace dynamic import with static import in cli.ts

Replace unnecessary dynamic import of wasSummaryOverwritten with
static import. No circular dependency exists since comment.ts doesn't
import from cli.ts.

* Fix run summary writing

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
This commit is contained in:
pullfrog[bot]
2026-01-15 00:55:42 +00:00
committed by pullfrog[bot]
parent 6e2a15c195
commit 71feba0a76
11 changed files with 12564 additions and 4609 deletions
+1 -1
View File
@@ -170,7 +170,7 @@ const messageHandlers: SDKMessageHandlers = {
const outputTokens = usage?.output_tokens || 0;
const totalInput = inputTokens + cacheRead + cacheWrite;
await log.summaryTable([
log.table([
[
{ data: "Cost", header: true },
{ data: "Input", header: true },
+1 -1
View File
@@ -194,7 +194,7 @@ const messageHandlers: {
// No logging needed
},
"turn.completed": async (event) => {
await log.summaryTable([
log.table([
[
{ data: "Input Tokens", header: true },
{ data: "Cached Input Tokens", header: true },
+1 -1
View File
@@ -154,7 +154,7 @@ const messageHandlers = {
String(stats.duration_ms || 0),
],
];
await log.summaryTable(rows);
log.table(rows);
} else if (event.status === "error") {
log.error(`Gemini CLI failed: ${JSON.stringify(event)}`);
}
+2 -2
View File
@@ -167,7 +167,7 @@ export const opencode = agent({
// 8. log tokens if they weren't logged yet (fallback if result event wasn't emitted)
if (!tokensLogged && (accumulatedTokens.input > 0 || accumulatedTokens.output > 0)) {
const totalTokens = accumulatedTokens.input + accumulatedTokens.output;
await log.summaryTable([
log.table([
[
{ data: "Input Tokens", header: true },
{ data: "Output Tokens", header: true },
@@ -571,7 +571,7 @@ const messageHandlers = {
);
if ((inputTokens > 0 || outputTokens > 0) && !tokensLogged) {
await log.summaryTable([
log.table([
[
{ data: "Input Tokens", header: true },
{ data: "Output Tokens", header: true },
+1018 -1128
View File
File diff suppressed because it is too large Load Diff
+1019 -1129
View File
File diff suppressed because it is too large Load Diff
+9455 -1053
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -211,7 +211,6 @@ export async function main(inputs: Inputs): Promise<MainResult> {
} catch {
// error reporting failed, but don't let it mask the original error
}
await log.writeSummary();
return {
success: false,
error: errorMessage,
@@ -553,7 +552,6 @@ async function handleAgentResult(result: AgentResult): Promise<MainResult> {
}
log.success("Task complete.");
await log.writeSummary();
return {
success: true,
+24 -31
View File
@@ -1,10 +1,10 @@
import * as core from "@actions/core";
import { type } from "arktype";
import type { Payload } from "../external.ts";
import { agentsManifest } from "../external.ts";
import type { ToolContext } from "../main.ts";
import { fetchWorkflowRunInfo } from "../utils/api.ts";
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
import { writeSummary } from "../utils/cli.ts";
import {
createOctokit,
getGitHubInstallationToken,
@@ -20,8 +20,6 @@ import { execute, tool } from "./shared.ts";
*/
export const LEAPING_INTO_ACTION_PREFIX = "Leaping into action";
const isGitHubActions = !!process.env.GITHUB_ACTIONS;
interface BuildCommentFooterParams {
payload: Payload;
octokit?: OctokitWithPlugins | undefined;
@@ -178,35 +176,30 @@ function getProgressCommentIdFromEnv(): number | null {
return null;
}
// module-level variable to track the progress comment ID
// initialized lazily on first use to allow env var to be set after module load
let progressCommentId: number | null = null;
let progressCommentIdInitialized = false;
// track whether the progress comment was updated during execution
let progressCommentWasUpdated = false;
// progress comment state - initialized lazily on first use to allow env var to be set after module load
const progressComment = {
id: null as number | null,
idInitialized: false,
wasUpdated: false,
};
function getProgressCommentId(): number | null {
if (!progressCommentIdInitialized) {
progressCommentId = getProgressCommentIdFromEnv();
progressCommentIdInitialized = true;
if (!progressComment.idInitialized) {
progressComment.id = getProgressCommentIdFromEnv();
progressComment.idInitialized = true;
}
return progressCommentId;
return progressComment.id;
}
function setProgressCommentId(id: number): void {
progressCommentId = id;
progressCommentIdInitialized = true;
progressComment.id = id;
progressComment.idInitialized = true;
}
export const ReportProgress = type({
body: type.string.describe("the progress update content to share"),
});
/** Updates job summary with the given text if running in GitHub Actions. */
const updateSummary = (text: string) => isGitHubActions && core.summary.addRaw(text).write({ overwrite: true });
/**
* Standalone function to report progress to GitHub comment.
* Can be called directly without going through the MCP tool interface.
@@ -251,9 +244,9 @@ export async function reportProgress(
body: bodyWithFooter,
});
progressCommentWasUpdated = true;
progressComment.wasUpdated = true;
await updateSummary(bodyWithFooter);
writeSummary(bodyWithFooter);
return {
commentId: result.data.id,
@@ -282,7 +275,7 @@ export async function reportProgress(
// store the comment ID for future updates
setProgressCommentId(result.data.id);
progressCommentWasUpdated = true;
progressComment.wasUpdated = true;
// if Plan mode, update the comment to add the "Implement plan" link
if (isPlanMode) {
@@ -302,7 +295,7 @@ export async function reportProgress(
body: bodyWithPlanLink,
});
await updateSummary(bodyWithPlanLink);
writeSummary(bodyWithPlanLink);
return {
commentId: updateResult.data.id,
@@ -312,7 +305,7 @@ export async function reportProgress(
};
}
await updateSummary(initialBody);
writeSummary(initialBody);
return {
commentId: result.data.id,
@@ -353,7 +346,7 @@ export function ReportProgressTool(ctx: ToolContext) {
* Check if the progress comment was updated during execution
*/
export function wasProgressCommentUpdated(): boolean {
return progressCommentWasUpdated;
return progressComment.wasUpdated;
}
/**
@@ -382,9 +375,9 @@ export async function deleteProgressComment(ctx: ToolContext): Promise<boolean>
}
// reset state but mark as "updated" so ensureProgressCommentUpdated doesn't try to handle it
progressCommentId = null;
progressCommentIdInitialized = true; // keep initialized so we don't re-fetch from env
progressCommentWasUpdated = true; // mark as handled so ensureProgressCommentUpdated skips
progressComment.id = null;
progressComment.idInitialized = true; // keep initialized so we don't re-fetch from env
progressComment.wasUpdated = true; // mark as handled so ensureProgressCommentUpdated skips
return true;
}
@@ -399,7 +392,7 @@ export async function deleteProgressComment(ctx: ToolContext): Promise<boolean>
*/
export async function ensureProgressCommentUpdated(payload?: Payload): Promise<void> {
// skip if comment was already updated during execution
if (progressCommentWasUpdated) {
if (progressComment.wasUpdated) {
return;
}
@@ -497,7 +490,7 @@ export function ReplyToReviewCommentTool(ctx: ToolContext) {
});
// mark progress as updated so ensureProgressCommentUpdated doesn't think the run failed
progressCommentWasUpdated = true;
progressComment.wasUpdated = true;
return {
success: true,
+1021 -1131
View File
File diff suppressed because it is too large Load Diff
+22 -130
View File
@@ -6,8 +6,10 @@ import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import * as core from "@actions/core";
import { table } from "table";
import { wasSummaryOverwritten } from "../mcp/comment.js";
const isGitHubActions = !!process.env.GITHUB_ACTIONS;
const isDebugEnabled = () =>
process.env.LOG_LEVEL === "debug" ||
process.env.ACTIONS_STEP_DEBUG === "true" ||
@@ -126,7 +128,6 @@ function boxString(
/**
* Print a formatted box with text
* Works well in both local and GitHub Actions environments
*/
function box(
text: string,
@@ -137,64 +138,25 @@ function box(
): void {
const boxContent = boxString(text, options);
core.info(boxContent);
if (isGitHubActions) {
// Add as markdown code block for summary (no headers)
core.summary.addRaw(`\`\`\`\n${text}\n\`\`\`\n`);
}
}
/**
* Add a table to GitHub Actions job summary (rich formatting)
* Also logs to console. Only use this once at the end of execution.
* Overwrite the job summary with the given text.
*/
async function summaryTable(
rows: Array<Array<{ data: string; header?: boolean } | string>>,
options?: {
title?: string;
}
): Promise<void> {
const { title } = options || {};
// Convert rows to format expected by Job Summaries API
const formattedRows = rows.map((row) =>
row.map((cell) => {
if (typeof cell === "string") {
return { data: cell };
}
return cell;
})
);
if (isGitHubActions) {
const summary = core.summary;
if (title) {
summary.addRaw(`**${title}**\n\n`);
}
summary.addTable(formattedRows);
// Note: Don't write immediately, let it accumulate with other summary content
}
// Also log to console for visibility
if (title) {
core.info(`\n${title}`);
}
const tableData = formattedRows.map((row) => row.map((cell) => cell.data));
const tableText = isGitHubActions
? tableData.map((row) => row.join(" | ")).join("\n")
: table(tableData);
core.info(`\n${tableText}\n`);
export function writeSummary(text: string): void {
if (!isGitHubActions) return;
core.summary.addRaw(text).write({ overwrite: true });
}
/**
* Print a formatted table using the table package
* Also logs to console and GitHub Actions summary
*/
async function printTable(
function printTable(
rows: Array<Array<{ data: string; header?: boolean } | string>>,
options?: {
title?: string;
}
): Promise<void> {
): void {
const { title } = options || {};
// Convert rows to string arrays for the table package
@@ -213,13 +175,6 @@ async function printTable(
core.info(`\n${title}`);
}
core.info(`\n${formatted}\n`);
if (isGitHubActions) {
if (title) {
core.summary.addRaw(`**${title}**\n\n`);
}
core.summary.addRaw(`\`\`\`\n${formatted}\n\`\`\`\n`);
}
}
/**
@@ -228,121 +183,58 @@ async function printTable(
function separator(length: number = 50): void {
const separatorText = "─".repeat(length);
core.info(separatorText);
if (isGitHubActions) {
core.summary.addRaw(`---\n`);
}
}
/**
* Main logging utility object - import this once and access all utilities
*/
export const log = {
/**
* Print info message
*/
/** Print info message */
info: (message: string): void => {
core.info(message);
if (isGitHubActions) {
core.summary.addRaw(`${message}\n`);
}
},
/**
* Print warning message
*/
/** Print warning message */
warning: (message: string): void => {
core.warning(message);
if (isGitHubActions) {
core.summary.addRaw(`⚠️ ${message}\n`);
}
},
/**
* Print error message
*/
/** Print error message */
error: (message: string): void => {
core.error(message);
if (isGitHubActions) {
core.summary.addRaw(`${message}\n`);
}
},
/**
* Print success message
*/
/** Print success message */
success: (message: string): void => {
const successMessage = `${message}`;
core.info(successMessage);
if (isGitHubActions) {
core.summary.addRaw(`${successMessage}\n`);
}
core.info(`${message}`);
},
/**
* Print debug message (only if LOG_LEVEL=debug)
*/
/** Print debug message (only if LOG_LEVEL=debug) */
debug: (message: string | unknown): void => {
if (isDebugEnabled()) {
if (isGitHubActions) {
// using this instead of core.debug
// because core.debug only logs when ACTIONS_STEP_DEBUG is set to true
// we are using LOG_LEVEL
core.info(`[DEBUG] ${message}`);
} else {
core.info(`[DEBUG] ${message}`);
}
core.info(`[DEBUG] ${message}`);
}
},
/**
* Print a formatted box with text
*/
/** Print a formatted box with text */
box,
/**
* Add a table to GitHub Actions job summary (rich formatting)
* Only use this once at the end of execution
*/
summaryTable,
/**
* Print a formatted table using the table package
*/
/** Print a formatted table using the table package */
table: printTable,
/**
* Print a separator line
*/
/** Print a separator line */
separator,
/**
* Write all accumulated summary content to the job summary
* Call this at the end of execution to finalize the summary
*/
writeSummary: async (): Promise<void> => {
if (isGitHubActions) {
await core.summary.write();
}
},
/**
* Start a collapsed group (GitHub Actions) or regular group (local)
*/
/** Start a collapsed group (GitHub Actions) or regular group (local) */
startGroup,
/**
* End a collapsed group
*/
/** End a collapsed group */
endGroup,
/**
* Run a callback within a collapsed group
*/
/** Run a callback within a collapsed group */
group,
/**
* Log tool call information to console with formatted output
*/
/** Log tool call information to console with formatted output */
toolCall: ({ toolName, input }: { toolName: string; input: unknown }): void => {
const inputFormatted = formatJsonValue(input);
const timestamp = isDebugEnabled() ? ` [${new Date().toISOString()}]` : "";