fix(safe-output): prevent silent 422 on PR review submission#32910
Conversation
Sub-pattern A: Guard against empty review POST (body="" + 0 comments)
- In submitReview(), after building the comments array and applying
footer logic, return { success: false } when body is empty AND no
inline comments remain. This blocks the 422 "Unprocessable Entity"
that GitHub returns for such submissions.
Sub-pattern B: Validate comment paths against PR diff before POST
- Before POSTing, fetch the PR's changed-files list via
pulls.listFiles (paginated, up to 10 pages).
- Filter out any buffered comments whose path is not in the diff,
warning for each one skipped.
- Fail-open: if listFiles returns an empty list (unusual) or the API
call throws, path validation is skipped and the review proceeds.
Fix 3: Roll back per-message success counts on finalization failure
- The submit_pull_request_review and create_pull_request_review_comment
handlers return success:true during message processing (they only
buffer). When the finalization POST fails, those buffered-message
results are marked success:false so the Processing Summary shows the
correct Failed count instead of the misleading "Failed: 0".
- Also upgrades the failure log from core.warning to core.error so it
is surfaced correctly.
Tests:
- 10 new tests in pr_review_buffer.test.cjs covering:
- Sub-pattern A: empty body + no comments → failure (not POST)
- Sub-pattern A: body present or comments present → POST proceeds
- Sub-pattern B: invalid paths filtered, valid paths pass
- Sub-pattern B: all paths invalid → empty-review guard triggers
- Sub-pattern B: empty listFiles response → no filtering
- Sub-pattern B: listFiles API failure → fail-open with warning
- Sub-pattern B: paginated listFiles (100+ files)
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
@copilot validate submitted review in safe output MCP server |
Add MCP server-phase validation for submit_pull_request_review and create_pull_request_review_comment tool calls in safe_outputs_handlers.cjs. The agent now receives an immediate MCP error (code -32602) when: 1. event is REQUEST_CHANGES but body is empty (GitHub API requirement) 2. The review would be contentless — no body AND no inline comments were buffered in this session (early guard for Sub-pattern A, mirrors the pr_review_buffer.cjs guard at finalization time) The session-scoped inline comment counter is reset after each successful submit_pull_request_review call so that subsequent reviews in the same MCP session also benefit from the guard. Both handlers are registered in safe_outputs_tools_loader.cjs and covered by 12 new tests in safe_outputs_handlers.test.cjs. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Added MCP server-phase validation for The agent now receives an immediate MCP error (
A session-scoped |
There was a problem hiding this comment.
Pull request overview
This PR aims to prevent silent PR review submission failures by validating empty reviews and filtering inline comments whose paths are not in the PR diff.
Changes:
- Adds PR review MCP-phase handlers and empty-review validation.
- Filters review comments by changed PR files before submission.
- Marks buffered review messages as failed when final review submission fails.
Show a summary per file
| File | Description |
|---|---|
actions/setup/js/safe_outputs_tools_loader.cjs |
Attaches PR review tools to custom MCP handlers. |
actions/setup/js/safe_outputs_handlers.cjs |
Adds MCP handlers for review comments and review submission validation. |
actions/setup/js/safe_outputs_handlers.test.cjs |
Adds tests for the new MCP review handlers. |
actions/setup/js/safe_output_handler_manager.cjs |
Rolls back buffered review result statuses when finalization fails. |
actions/setup/js/pr_review_buffer.cjs |
Adds path validation and empty-review guard before creating reviews. |
actions/setup/js/pr_review_buffer.test.cjs |
Adds tests for empty-review and path-filtering behavior. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 6/6 changed files
- Comments generated: 6
| page: listPage, | ||
| }); | ||
| if (!Array.isArray(files) || files.length === 0) break; | ||
| for (const f of files) changedPaths.add(f.filename); |
| create_pull_request_review_comment: handlers.createPullRequestReviewCommentHandler, | ||
| submit_pull_request_review: handlers.submitPullRequestReviewHandler, |
| const event = (args && args.event ? String(args.event).toUpperCase() : "COMMENT"); | ||
|
|
| * Handler for create_pull_request_review_comment tool (MCP server phase). | ||
| * Increments the session-scoped inline comment counter so that the subsequent | ||
| * submitPullRequestReviewHandler can detect an otherwise-empty review. | ||
| * Per Safe Outputs Specification MCE1: enforces constraints during tool invocation | ||
| * to provide immediate feedback to the LLM before recording to NDJSON. | ||
| */ | ||
| const createPullRequestReviewCommentHandler = args => { | ||
| inlineReviewCommentCount++; | ||
| return defaultHandler("create_pull_request_review_comment")(args); |
| // Cap at 10 pages (1,000 files). PRs with more than 1,000 changed files are | ||
| // extremely rare and path validation is best-effort; we proceed without filtering | ||
| // if any individual listFiles call throws (see catch block below). | ||
| const MAX_LIST_FILES_PAGES = 10; | ||
| while (listPage <= MAX_LIST_FILES_PAGES) { |
| (r.type === "submit_pull_request_review" || r.type === "create_pull_request_review_comment") && | ||
| r.success === true | ||
| ) { | ||
| r.success = false; | ||
| r.error = `Review finalization failed: ${reviewFailureError}`; |
|
@copilot Review all comments |
- pr_review_buffer.cjs: include previous_filename for renamed files in path validation set - pr_review_buffer.cjs: fail-open when pagination cap (10 pages) is reached with a full last page - safe_outputs_handlers.cjs: validate event against APPROVE/REQUEST_CHANGES/COMMENT before appending - safe_outputs_handlers.cjs: increment inlineReviewCommentCount only after successful defaultHandler call - safe_output_handler_manager.cjs: extract rollback logic into exported rollbackReviewResults() helper - safe_outputs_tools_loader.test.cjs: add tests for create_pull_request_review_comment and submit_pull_request_review handler mappings - safe_output_handler_manager.test.cjs: add 6 tests for rollbackReviewResults covering all branches - safe_outputs_handlers.test.cjs: add tests for event validation and counter-not-incremented-on-throw - pr_review_buffer.test.cjs: add tests for previous_filename support and pagination-cap fail-open Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
All six review comments addressed in commits
|
Over 24h, 11 PR review submissions returned
422 Unprocessable Entitybut the workflow reportedSuccessful: 2 / Failed: 0— the error was swallowed and no review was posted.Root causes and fixes
Sub-pattern A — empty review (10 cases): Agent called
submit_pull_request_reviewwith no body and zero inline comments. GitHub 422s on a contentless review.Pre-check added in
submitReview()after footer generation: ifbodyis empty andcomments.length === 0, return{ success: false }instead of POSTing.Sub-pattern B — paths not in diff (1 case): Inline comments buffered at paths outside the PR diff cause
422: "Path could not be resolved".Before POSTing,
submitReview()now fetchespulls.listFiles(paginated, ≤1,000 files) and silently drops comments whosepathis absent from the diff. Fail-open: if the API call throws or returns an empty list, path validation is skipped.Fix 3 — misleading success count: Both
submit_pull_request_reviewandcreate_pull_request_review_commenthandlers returnsuccess: trueimmediately (they only buffer). When finalization fails, those results are retroactively flipped tosuccess: falseinprocessingResult.results, so the Processing Summary reflects the real outcome. The failure is also promoted fromcore.warningtocore.error.Files changed
pr_review_buffer.cjs— Sub-pattern A guard + Sub-pattern B path validation insubmitReview()safe_output_handler_manager.cjs— rollback loop +core.errorpromotion in finalization blockpr_review_buffer.test.cjs— 10 new tests (empty-review guard, path filtering, fail-open, pagination)