fix bodyless review bug by using pending + submit flow (#469)
* fix bodyless review bug by using pending + submit flow createReview with event:"COMMENT" publishes immediately, so the subsequent updateReview (to add footer with Fix links) fails when the agent omits the review-level body — GitHub rejects editing a bodyless review. this left ghost reviews and caused retries with a garbage body like `" "`. switch to a two-phase flow: createReview without event (PENDING), then submitReview with the full body + footer. single atomic publish, no updateReview needed. Made-with: Cursor * support bodyless reviews — skip footer when no body provided Made-with: Cursor * early return for bodyless reviews Made-with: Cursor * extract submitAndCleanup and buildReviewFooter helpers Made-with: Cursor * fix: default approved to false for buildReviewFooter Made-with: Cursor * run action typecheck alongside root tsc Made-with: Cursor * refactor: extract submitReview helper, keep cleanup inline Made-with: Cursor * skip pending+submit for bodyless reviews — single createReview instead Made-with: Cursor * restore pre-existing comments Made-with: Cursor
This commit is contained in:
committed by
pullfrog[bot]
parent
9c99bcbbac
commit
089a05b13e
@@ -147194,7 +147194,6 @@ function CreatePullRequestReviewTool(ctx) {
|
|||||||
pull_number,
|
pull_number,
|
||||||
event
|
event
|
||||||
};
|
};
|
||||||
if (body) params.body = body;
|
|
||||||
if (commit_id) {
|
if (commit_id) {
|
||||||
params.commit_id = commit_id;
|
params.commit_id = commit_id;
|
||||||
} else {
|
} else {
|
||||||
@@ -147224,44 +147223,17 @@ function CreatePullRequestReviewTool(ctx) {
|
|||||||
return reviewComment;
|
return reviewComment;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const result = await ctx.octokit.rest.pulls.createReview(params);
|
const result = body ? await createAndSubmitWithFooter(ctx, params, {
|
||||||
log.debug(`createReview response: ${JSON.stringify(result.data)}`);
|
body,
|
||||||
|
approved: approved ?? false,
|
||||||
|
hasComments: comments.length > 0
|
||||||
|
}) : await ctx.octokit.rest.pulls.createReview(params);
|
||||||
if (!result.data.id) {
|
if (!result.data.id) {
|
||||||
throw new Error(`createReview returned invalid data: ${JSON.stringify(result.data)}`);
|
throw new Error(`createReview returned invalid data: ${JSON.stringify(result.data)}`);
|
||||||
}
|
}
|
||||||
const reviewId = result.data.id;
|
const reviewId = result.data.id;
|
||||||
const reviewNodeId = result.data.node_id;
|
const reviewNodeId = result.data.node_id;
|
||||||
await reportReviewNodeId(ctx, reviewNodeId);
|
await reportReviewNodeId(ctx, reviewNodeId);
|
||||||
const customParts = [];
|
|
||||||
if (!approved) {
|
|
||||||
if (comments.length > 0) {
|
|
||||||
const apiUrl = getApiUrl();
|
|
||||||
const fixAllUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${pull_number}?action=fix&review_id=${reviewId}`;
|
|
||||||
const fixApprovedUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${pull_number}?action=fix-approved&review_id=${reviewId}`;
|
|
||||||
customParts.push(`[Fix all \u2794](${fixAllUrl})`, `[Fix \u{1F44D}s \u2794](${fixApprovedUrl})`);
|
|
||||||
} else if (body) {
|
|
||||||
const apiUrl = getApiUrl();
|
|
||||||
const fixUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${pull_number}?action=fix&review_id=${reviewId}`;
|
|
||||||
customParts.push(`[Fix it \u2794](${fixUrl})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const footer = buildPullfrogFooter({
|
|
||||||
workflowRun: ctx.runId ? {
|
|
||||||
owner: ctx.repo.owner,
|
|
||||||
repo: ctx.repo.name,
|
|
||||||
runId: ctx.runId,
|
|
||||||
jobId: ctx.jobId
|
|
||||||
} : void 0,
|
|
||||||
customParts
|
|
||||||
});
|
|
||||||
const updatedBody = (body || "") + footer;
|
|
||||||
await ctx.octokit.rest.pulls.updateReview({
|
|
||||||
owner: ctx.repo.owner,
|
|
||||||
repo: ctx.repo.name,
|
|
||||||
pull_number,
|
|
||||||
review_id: reviewId,
|
|
||||||
body: updatedBody
|
|
||||||
});
|
|
||||||
await deleteProgressComment(ctx);
|
await deleteProgressComment(ctx);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -147274,6 +147246,37 @@ function CreatePullRequestReviewTool(ctx) {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
async function createAndSubmitWithFooter(ctx, params, opts) {
|
||||||
|
const { event: _, ...pendingParams } = params;
|
||||||
|
const pending = await ctx.octokit.rest.pulls.createReview(pendingParams);
|
||||||
|
if (!pending.data.id) {
|
||||||
|
throw new Error(`createReview returned invalid data: ${JSON.stringify(pending.data)}`);
|
||||||
|
}
|
||||||
|
const customParts = [];
|
||||||
|
if (!opts.approved) {
|
||||||
|
const apiUrl = getApiUrl();
|
||||||
|
if (opts.hasComments) {
|
||||||
|
const fixAllUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${params.pull_number}?action=fix&review_id=${pending.data.id}`;
|
||||||
|
const fixApprovedUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${params.pull_number}?action=fix-approved&review_id=${pending.data.id}`;
|
||||||
|
customParts.push(`[Fix all \u2794](${fixAllUrl})`, `[Fix \u{1F44D}s \u2794](${fixApprovedUrl})`);
|
||||||
|
} else {
|
||||||
|
const fixUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${params.pull_number}?action=fix&review_id=${pending.data.id}`;
|
||||||
|
customParts.push(`[Fix it \u2794](${fixUrl})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const footer = buildPullfrogFooter({
|
||||||
|
workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
|
||||||
|
customParts
|
||||||
|
});
|
||||||
|
return ctx.octokit.rest.pulls.submitReview({
|
||||||
|
owner: params.owner,
|
||||||
|
repo: params.repo,
|
||||||
|
pull_number: params.pull_number,
|
||||||
|
review_id: pending.data.id,
|
||||||
|
event: params.event,
|
||||||
|
body: opts.body + footer
|
||||||
|
});
|
||||||
|
}
|
||||||
async function reportReviewNodeId(ctx, reviewNodeId) {
|
async function reportReviewNodeId(ctx, reviewNodeId) {
|
||||||
for (let remaining = 2; remaining >= 0; remaining--) {
|
for (let remaining = 2; remaining >= 0; remaining--) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+54
-43
@@ -88,7 +88,6 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
|
|||||||
pull_number,
|
pull_number,
|
||||||
event,
|
event,
|
||||||
};
|
};
|
||||||
if (body) params.body = body;
|
|
||||||
if (commit_id) {
|
if (commit_id) {
|
||||||
params.commit_id = commit_id;
|
params.commit_id = commit_id;
|
||||||
} else {
|
} else {
|
||||||
@@ -124,8 +123,16 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await ctx.octokit.rest.pulls.createReview(params);
|
// no body → single-step createReview (no footer needed)
|
||||||
log.debug(`createReview response: ${JSON.stringify(result.data)}`);
|
// has body → pending + submit so we can build footer with Fix links using review ID
|
||||||
|
const result = body
|
||||||
|
? await createAndSubmitWithFooter(ctx, params, {
|
||||||
|
body,
|
||||||
|
approved: approved ?? false,
|
||||||
|
hasComments: comments.length > 0,
|
||||||
|
})
|
||||||
|
: await ctx.octokit.rest.pulls.createReview(params);
|
||||||
|
|
||||||
if (!result.data.id) {
|
if (!result.data.id) {
|
||||||
throw new Error(`createReview returned invalid data: ${JSON.stringify(result.data)}`);
|
throw new Error(`createReview returned invalid data: ${JSON.stringify(result.data)}`);
|
||||||
}
|
}
|
||||||
@@ -137,46 +144,6 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
|
|||||||
// awaited (not fire-and-forget) to guarantee the signal lands before
|
// awaited (not fire-and-forget) to guarantee the signal lands before
|
||||||
// any subsequent push's webhook checks for in-flight runs.
|
// any subsequent push's webhook checks for in-flight runs.
|
||||||
await reportReviewNodeId(ctx, reviewNodeId);
|
await reportReviewNodeId(ctx, reviewNodeId);
|
||||||
|
|
||||||
// build quick links footer and update the review body
|
|
||||||
// only include "Fix all" and "Fix 👍s" links if there are actual review comments
|
|
||||||
const customParts: string[] = [];
|
|
||||||
if (!approved) {
|
|
||||||
if (comments.length > 0) {
|
|
||||||
const apiUrl = getApiUrl();
|
|
||||||
const fixAllUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${pull_number}?action=fix&review_id=${reviewId}`;
|
|
||||||
const fixApprovedUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${pull_number}?action=fix-approved&review_id=${reviewId}`;
|
|
||||||
customParts.push(`[Fix all ➔](${fixAllUrl})`, `[Fix 👍s ➔](${fixApprovedUrl})`);
|
|
||||||
} else if (body) {
|
|
||||||
const apiUrl = getApiUrl();
|
|
||||||
const fixUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${pull_number}?action=fix&review_id=${reviewId}`;
|
|
||||||
customParts.push(`[Fix it ➔](${fixUrl})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const footer = buildPullfrogFooter({
|
|
||||||
workflowRun: ctx.runId
|
|
||||||
? {
|
|
||||||
owner: ctx.repo.owner,
|
|
||||||
repo: ctx.repo.name,
|
|
||||||
runId: ctx.runId,
|
|
||||||
jobId: ctx.jobId,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
customParts,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedBody = (body || "") + footer;
|
|
||||||
|
|
||||||
// update the review with the footer
|
|
||||||
await ctx.octokit.rest.pulls.updateReview({
|
|
||||||
owner: ctx.repo.owner,
|
|
||||||
repo: ctx.repo.name,
|
|
||||||
pull_number,
|
|
||||||
review_id: reviewId,
|
|
||||||
body: updatedBody,
|
|
||||||
});
|
|
||||||
|
|
||||||
await deleteProgressComment(ctx);
|
await deleteProgressComment(ctx);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -191,6 +158,50 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FooterOpts = { body: string; approved: boolean; hasComments: boolean };
|
||||||
|
|
||||||
|
async function createAndSubmitWithFooter(
|
||||||
|
ctx: ToolContext,
|
||||||
|
params: RestEndpointMethodTypes["pulls"]["createReview"]["parameters"],
|
||||||
|
opts: FooterOpts
|
||||||
|
) {
|
||||||
|
// create as PENDING (strip event) so we get the review ID before publishing
|
||||||
|
const { event: _, ...pendingParams } = params;
|
||||||
|
const pending = await ctx.octokit.rest.pulls.createReview(pendingParams);
|
||||||
|
if (!pending.data.id) {
|
||||||
|
throw new Error(`createReview returned invalid data: ${JSON.stringify(pending.data)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const customParts: string[] = [];
|
||||||
|
if (!opts.approved) {
|
||||||
|
const apiUrl = getApiUrl();
|
||||||
|
if (opts.hasComments) {
|
||||||
|
const fixAllUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${params.pull_number}?action=fix&review_id=${pending.data.id}`;
|
||||||
|
const fixApprovedUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${params.pull_number}?action=fix-approved&review_id=${pending.data.id}`;
|
||||||
|
customParts.push(`[Fix all ➔](${fixAllUrl})`, `[Fix 👍s ➔](${fixApprovedUrl})`);
|
||||||
|
} else {
|
||||||
|
const fixUrl = `${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${params.pull_number}?action=fix&review_id=${pending.data.id}`;
|
||||||
|
customParts.push(`[Fix it ➔](${fixUrl})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const footer = buildPullfrogFooter({
|
||||||
|
workflowRun: ctx.runId
|
||||||
|
? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId }
|
||||||
|
: undefined,
|
||||||
|
customParts,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ctx.octokit.rest.pulls.submitReview({
|
||||||
|
owner: params.owner,
|
||||||
|
repo: params.repo,
|
||||||
|
pull_number: params.pull_number,
|
||||||
|
review_id: pending.data.id,
|
||||||
|
event: params.event!,
|
||||||
|
body: opts.body + footer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function reportReviewNodeId(ctx: ToolContext, reviewNodeId: string): Promise<void> {
|
async function reportReviewNodeId(ctx: ToolContext, reviewNodeId: string): Promise<void> {
|
||||||
for (let remaining = 2; remaining >= 0; remaining--) {
|
for (let remaining = 2; remaining >= 0; remaining--) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user