fix: reject submit_pull_request_review max > 1 at compile time - #45647
Conversation
The runtime uses a single shared PR review buffer that cannot preserve independent context (repo, PR number, body, event) across multiple calls. Accepting max > 1 silently collapses all calls to first-target/last-body. Add checkMaxFieldAtMostOne() which wraps checkMaxField() (keeping standard range validation for 0/-2/etc.) and additionally rejects -1 (unlimited) and any value > 1 with an actionable error message that explains the single-buffer limitation and recommends target: "*" with max: 1 for multi-PR workflows. Closes #44856 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Address code review feedback: - Add explicit comment explaining why nil is returned on Atoi failure (consistent with checkMaxField: non-numeric non-expression values skipped) - Rephrase error message to better explain that target:"*" with max:1 creates separate invocations each with their own review buffer Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
@copilot add support for multi review buffers |
On it — I'll implement multi-buffer support in the runtime so that each distinct
Done. The implementation adds Key changes:
Two calls targeting distinct PRs now produce two independently submitted reviews. See commits |
- Add createPrReviewBufferRegistry() to pr_review_buffer.cjs: manages a Map<"repo#prNum" → buffer> in insertion order; getOrCreate() returns null for unresolvable targets; applies staged/supersedeOlderReviews/footerMode defaults to each new buffer - Update submit_pr_review.cjs: detect _prReviewBufferRegistry (registry mode) vs _prReviewBuffer (legacy mode); in registry mode resolve target first, then getOrCreate() per-PR buffer so each PR gets independent metadata/context; include pull_request_number + repo in registry-mode result for per-PR rollback - Update create_pr_review_comment.cjs: detect registry vs legacy; in registry mode call getOrCreate(repo, prNum) per message, set footer context per-buffer (idempotent), remove cross-PR rejection (each PR has its own buffer); legacy path unchanged - Update safe_output_handler_manager.cjs: create prReviewBufferRegistry instead of single buffer; pass as _prReviewBufferRegistry; finalize all registry entries in a loop with per-PR logging; add rollbackReviewResultsForPR / skipReviewResultsForPR; keep existing rollbackReviewResults/skipReviewResults for backward compat; export new functions - Remove orphaned TestCheckMaxFieldAtMostOne + TestSubmitPullRequestReviewMaxValidation from safe_outputs_max_validation_test.go (reverted functions) - Add tests: createPrReviewBufferRegistry unit tests (pr_review_buffer.test.cjs), multi-buffer integration tests (submit_pr_review.test.cjs) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
- submit_pr_review.cjs: add warning when both _prReviewBufferRegistry and _prReviewBuffer are provided; document that registry takes precedence - create_pr_review_comment.cjs: clarify setFooterContext is first-wins internally, so repeated calls for the same PR buffer are safe no-ops - safe_output_handler_manager.cjs: add core.warning() when rollbackReviewResultsForPR falls back to rolling back all review results (transparent behavior for the implicit fallback path) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
🤖 PR Triage
Adds compile-time rejection of submit_pull_request_review with max > 1 — prevents silent data loss. Large change (+575/-128), draft. High correctness impact. Prioritize review after undrafting.
|
|
/matt |
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /codebase-design and /tdd — requesting changes on three issues: a silent missing-footer regression in registry mode, a dangerous fallback in the new rollback helper, and an unclear max contract for multi-PR scenarios.
📋 Key Themes & Highlights
Issues
-
Footer context not propagated to registry buffers (correctness) —
safe_output_handler_managerexportssetDefaultFooterContextfrom the registry but never calls it. Body-only reviews viasubmit_pull_request_reviewin registry mode will silently omit the workflow footer. The fix is a one-liner in the manager. -
rollbackReviewResultsForPRbroad fallback is unsafe in multi-PR runs (correctness) — When no per-PR result match is found, the fallback rolls back all review results. In a multi-PR run this would corrupt success counts for unrelated PRs. The fallback should be scope-guarded by registry size. -
processedCountsemantics are ambiguous withtarget: "*"(contract gap) — The counter is global across all target PRs.max: 1with two distinct PRs silently drops the second. The contract needs to be documented and tested.
Positive Highlights
- ✅ Clean registry abstraction in
createPrReviewBufferRegistrywith first-wins semantics for defaults - ✅ Backward-compat legacy path retained without duplication
- ✅ Good test coverage for registry unit tests (insertion order, null guards, idempotency)
- ✅ Per-PR rollback/skip helpers are the right architectural direction
- ✅
setReviewContextOnBufferextraction is a nice deduplication
|
@copilot run pr-finisher |
…t_pr_review, and awf_reflect Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
@copilot add tests for multiple reviews |
All review feedback has been addressed and changes are committed locally, but the push is blocked by a 403 permission error for Changes made (commit
|
… tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in commit Added tests in two files:
Also fixed the bug these tests exposed:
|
|
@copilot please run the
|
…erEach import Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Done. Three remaining issues from the automated review are now fixed (commit
Pre-existing test failure in |
|
@copilot merge main and recompile Fix https://github.com/github/gh-aw/actions/runs/29408426569/job/87351079808#step:7:1 |
…ll-request-review Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…39, merge main Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in commit
|
🤖 PR Triage
Score breakdown: Impact 42 + Urgency 24 + Quality 12 Rationale: Fixes a silent data-loss bug in safe-outputs runtime —
|
|
Run: https://github.com/github/gh-aw/actions/runs/29420425558
|
All review feedback has been addressed in the commits on this branch. Here's the current state: Reviews
Checks (local): Mergeable: branch is up to date with main (merged in commit Hand-off: CI must be re-triggered by a maintainer (close/reopen or push). The PR title/description should be updated to reflect the multi-buffer implementation scope before merge. |
|
🎉 This pull request is included in a new release. Release: |
The runtime uses a single shared
createReviewBuffer()for the entire run. Withmax > 1, the handler accepts all calls but submits only one review — silently collapsing to the first call's target and the last call's body. Intermediate calls are lost with no error.Changes
pkg/workflow/safe_outputs_max_validation.gocheckMaxFieldAtMostOne(): wrapscheckMaxField()(keeps standard range rejection for0/-2/etc.), then additionally rejects-1(unlimited) and any value> 1validateSafeOutputsMax: switchessubmit_pull_request_reviewfromcheckMaxField→checkMaxFieldAtMostOneError message is actionable — names the single-buffer limitation and directs authors to
target: "*"withmax: 1:pkg/workflow/safe_outputs_max_validation_test.goTestCheckMaxFieldAtMostOne: unit tests for the new helperTestSubmitPullRequestReviewMaxValidation: end-to-end throughvalidateSafeOutputsMax— coversmax=2rejected,max=-1rejected,max=1accepted,nilaccepted, expression skippedThe existing
TestValidateSafeOutputsMaxFieldCoverage(usesmax=0) continues to pass becausecheckMaxFieldAtMostOnedelegates tocheckMaxFieldfirst, which produces the standard error for out-of-range values.