Skip to content

Handle degenerate REVIEW_REQUIRED PR state in dismiss_pull_request_review#43657

Merged
pelikhan merged 5 commits into
mainfrom
copilot/handle-misconfigured-pr-states
Jul 6, 2026
Merged

Handle degenerate REVIEW_REQUIRED PR state in dismiss_pull_request_review#43657
pelikhan merged 5 commits into
mainfrom
copilot/handle-misconfigured-pr-states

Conversation

Copilot AI commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Some PRs surface as Review required in mobile iOS while having no dismissible requested-review entry in the web UX. This change hardens dismiss_pull_request_review so it can detect and handle that degenerate state instead of failing with an opaque “review not found” path.

  • Auto review resolution for dismissals

    • Added review_id: "auto" support to resolve the latest actor-authored dismissible review (APPROVED or CHANGES_REQUESTED) before calling dismissReview.
    • Preserves existing actor-bound safety checks.
  • Degenerate-state detection

    • When review_id: "auto" finds no actor-authored dismissible review, the handler now checks PR state and returns a dedicated error for the blocked/no-requested-reviewers-no-requested-teams case.
    • This explicitly distinguishes misconfigured review state from normal “no matching review” conditions.
  • Review lookup robustness

    • Review enumeration now paginates (bounded) instead of assuming first-page completeness, so auto-resolution still works on PRs with long review history.
  • Contract/schema sync

    • Updated safe-output tool/schema/type descriptions to document that review_id accepts either a numeric ID or "auto".
{
  "type": "dismiss_pull_request_review",
  "pull_request_number": 43495,
  "review_id": "auto",
  "justification": "Dismissing stale actor-authored review after follow-up changes."
}

Copilot AI and others added 2 commits July 6, 2026 01:26
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Handle degenerate REVIEW_REQUIRED states in dismiss review safe output Handle degenerate REVIEW_REQUIRED PR state in dismiss_pull_request_review Jul 6, 2026
Copilot AI requested a review from pelikhan July 6, 2026 01:42
@pelikhan pelikhan marked this pull request as ready for review July 6, 2026 01:45
Copilot AI review requested due to automatic review settings July 6, 2026 01:45
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #43657 does not have the 'implementation' label and has only 1 new line of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@pelikhan

pelikhan commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

@copilot this PR also has a degenerate review state #43649

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the dismiss_pull_request_review safe-output handler to handle PRs that appear “Review required” in some clients but have no dismissible requested-review entry, by adding an "auto" review-id resolution path and better error reporting.

Changes:

  • Added review_id: "auto" support to resolve the latest dismissible (APPROVED / CHANGES_REQUESTED) review authored by the current workflow actor.
  • Added degenerate-state detection for the “blocked + no requested reviewers/teams” case, returning a dedicated error.
  • Improved robustness by paginating review enumeration (bounded) and updating tool/schema/type descriptions to document "auto".
Show a summary per file
File Description
schemas/agent-output.json Documents review_id accepting "auto" for dismiss review output items.
pkg/workflow/js/safe_outputs_tools.json Updates safe-output tool schema description to mention "auto" resolution.
actions/setup/js/types/safe-outputs.d.ts Updates TypeScript doc comment to mention "auto" behavior.
actions/setup/js/safe_outputs_tools.json Updates setup-side safe-output tool schema description to mention "auto" resolution.
actions/setup/js/dismiss_pull_request_review.cjs Implements "auto" resolution, degenerate-state detection, and bounded pagination.
actions/setup/js/dismiss_pull_request_review.test.cjs Adds test coverage for "auto" resolution, degenerate-state detection, pagination, and actor matching.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 5/6 changed files
  • Comments generated: 4
  • Review effort level: Low

Comment thread schemas/agent-output.json
Comment on lines 414 to 417
"review_id": {
"type": ["number", "string"],
"description": "The numeric review ID to dismiss."
"description": "The numeric review ID to dismiss, or 'auto' to resolve the latest dismissible review authored by the current workflow actor."
},
Comment on lines +75 to +93
async function listAllPullRequestReviews(githubClient, owner, repo, pullRequestNumber) {
const all = [];
const maxPages = 10;
let page = 1;
while (page <= maxPages) {
const { data } = await githubClient.rest.pulls.listReviews({
owner,
repo,
pull_number: pullRequestNumber,
per_page: 100,
page,
});
if (!Array.isArray(data) || data.length === 0) break;
all.push(...data);
if (data.length < 100) break;
page++;
}
return all;
}
Comment on lines 525 to 529
"review_id": {
"type": ["number", "string"],
"description": "Numeric pull request review ID to dismiss (for example, 123456789).",
"description": "Numeric pull request review ID to dismiss (for example, 123456789), or 'auto' to resolve the latest dismissible review authored by the current workflow actor.",
"x-synonyms": ["reviewId"]
},
Comment on lines 525 to 529
"review_id": {
"type": ["number", "string"],
"description": "Numeric pull request review ID to dismiss (for example, 123456789).",
"description": "Numeric pull request review ID to dismiss (for example, 123456789), or 'auto' to resolve the latest dismissible review authored by the current workflow actor.",
"x-synonyms": ["reviewId"]
},
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 93/100 — Excellent

Analyzed 9 test(s): 9 design, 0 implementation, 0 violation(s).

📊 Metrics (9 tests)
Metric Value
Analyzed 9 (Go: 0, JS: 9)
✅ Design 9 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 7 (78%)
Duplicate clusters 0
Inflation No (1.83:1)
🚨 Violations 0
Test File Classification Issues
dismisses a review when author matches current actor dismiss_pull_request_review.test.cjs:83 design_testhigh_value None
rejects when provided author differs from current actor :105 design_testhigh_value None
rejects when fetched review author differs from current actor :118 design_testhigh_value None
resolves review_id=auto to latest dismissible review by current actor :136 design_testhigh_value None
detects degenerate blocked state with no requested reviewers :175 design_testhigh_value None
returns actor-specific error when review_id=auto finds no reviews :196 design_testhigh_value None
paginates reviews when resolving review_id=auto :219 design_testhigh_value None
uses GITHUB_ACTOR for review_id=auto actor matching :258 design_testhigh_value None
caps review pagination to avoid excessive API calls :290 design_testhigh_value None

Score formula: (9/9 × 40) + (7/9 × 30) + (0 dup clusters → 20) + (no inflation → 10) = 93

Verdict

Passed. 0% implementation tests (threshold: 30%). All 9 tests enforce behavioral contracts — including the new degenerate REVIEW_REQUIRED state detection, pagination cap, actor-specific error messaging, and GITHUB_ACTOR env var contract. Mocks are appropriately scoped to external GitHub API I/O only. No violations detected.

References:

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 31.1 AIC · ⌖ 11.6 AIC · ⊞ 6.8K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 93/100. 0% implementation tests (threshold: 30%).

Copilot AI commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@copilot this PR also has a degenerate review state #43649

Investigated PR #43649. It's blocked by two CHANGES_REQUESTED reviews from github-actions[bot] (Test Quality Sentinel and PR Code Quality Reviewer) on commit 7eeb90c.

The current review_id: "auto" implementation resolves the latest actor-authored dismissible review — it won't surface the github-actions[bot] reviews unless the workflow actor is that bot identity.

PR #43649 also has requested_reviewers: ["pelikhan"], so the degenerate-state guard we added (blocked + no requested reviewers/teams → dedicated error) won't fire for it.

The two blocking reviews flag real issues still present in the code:

  1. astutil_test.go is missing the mandatory //go:build !integration build tag.
  2. httpnoctx.go has a vestigial ctxType parameter after the refactor.

Would you like me to fix those issues on PR #43649's branch so the bot reviews can re-run and clear, or is there a different aspect of the degenerate state you'd like me to address?

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — commenting with actionable issues; not blocking merge.

📋 Key Themes & Highlights

Key Themes

  • mergeable_state reliability: The degenerate-state detection heuristic (mergeable_state === "blocked") uses an undocumented, internal API field. A PR blocked for unrelated reasons (missing status checks, branch protection, dirty branch) will trigger the misleading "degenerate review-required state" error.
  • Staged mode contract gap: When review_id: "auto" is used in staged mode, the response returns review_id: "auto" instead of the resolved numeric ID. Live runs return a number; staged runs return a string — asymmetric contract.
  • Test isolation: The GITHUB_ACTOR env mutation in one test isn't cleaned up with a finally block, making the suite subtly order-dependent.
  • Weak pagination test assertion: The pagination-cap test verifies call count but doesn't assert the error message, so it can't distinguish the "no match after full pagination" path from the "degenerate state" path.

Positive Highlights

  • ✅ Excellent coverage breadth — 6 new test cases cover auto-resolution, degenerate detection, pagination, actor matching, and edge cases
  • ✅ Bounded pagination (max 10 pages × 100/page) is a good defensive pattern
  • findLatestDismissibleReviewForActor is a clean, well-typed pure function with a clear sort strategy and ID tie-breaker
  • ✅ The Promise.all to fetch PR metadata and reviews concurrently is a nice performance touch
  • ✅ Schema/type docs kept in sync across all 4 locations

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 64.5 AIC · ⌖ 10.7 AIC · ⊞ 6.7K
Comment /matt to run again


const autoReview = findLatestDismissibleReviewForActor(reviews, expectedAuthor);
if (!autoReview || !Number.isInteger(autoReview.id) || autoReview.id <= 0) {
const isBlockedWithoutReviewers = hasNoRequestedReviewersOrTeams(pullRequest) && pullRequest?.mergeable_state === "blocked";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] The degenerate-state check relies on mergeable_state === "blocked" — an undocumented, internal GitHub API field not in the REST contract. A PR can be blocked for many other reasons (missing status checks, branch protection rules, merge conflicts), so this condition may misidentify normal blocked PRs as the degenerate "no-reviewer" case.

💡 Suggested improvement

Consider narrowing the heuristic or documenting its limitations explicitly:

// mergeable_state is undocumented — valid known values include:
// "clean", "blocked", "behind", "unstable", "draft", "dirty"
// We rely on it being "blocked" alongside empty reviewers/teams as
// a proxy for the REVIEW_REQUIRED degenerate state.

Alternatively, always return the actor-specific error (review_id=auto did not find a dismissible review authored by ...) regardless, so callers don't get a false-positive "degenerate state" diagnosis on a legitimately blocked PR.

@copilot please address this.

@@ -99,19 +172,47 @@
if (filterResult) return filterResult;

if (isStaged) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] In staged mode, review_id: "auto" is returned verbatim in the response without resolving the actual review ID. This means the caller gets no useful preview of which review would be dismissed — the staged output has a different shape than the live output ("auto" vs a numeric ID). This breaks the contract symmetry between staged and live runs.

💡 Suggested fix

For staged mode, still fetch and resolve the review (or at least validate that a dismissible review exists), so the preview includes the real numeric review_id. This makes staged previews trustworthy and catches misconfiguration early:

if (isStaged) {
  // Resolve auto even in staged mode so the preview shows the real review ID
  if (useAutoReviewId) {
    const reviews = await listAllPullRequestReviews(...);
    const autoReview = findLatestDismissibleReviewForActor(reviews, expectedAuthor);
    if (!autoReview) return { success: false, error: '...' };
    reviewId = autoReview.id;
  }
  logStagedPreviewInfo(`Would dismiss review #${reviewId} ...`);
  ...
}

@copilot please address this.

expect(mockDismissReview).toHaveBeenCalledWith(expect.objectContaining({ review_id: 777 }));
});

it("caps review pagination to avoid excessive API calls", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The pagination cap test verifies that mockListReviews is called exactly 10 times, but it doesn't assert the error message or confirm that success: false is due to exhausted pagination specifically versus finding no matching review. Both cases return success: false and could mask regression.

💡 Suggested addition

Add a more specific assertion to distinguish the "no match after full pagination" case:

expect(result.success).toBe(false);
expect(result.error).toContain("did not find a dismissible review"); // not "degenerate"
expect(mockListReviews).toHaveBeenCalledTimes(10);

This ensures the pagination-exhaustion path returns the right error, not the degenerate-state error.

@copilot please address this.

);
});

it("uses GITHUB_ACTOR for review_id=auto actor matching", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The GITHUB_ACTOR test modifies process.env.GITHUB_ACTOR and calls vi.resetModules() + require() to reload the module, but it doesn't restore process.env.GITHUB_ACTOR after the test. This leaks the "custom-bot" actor into subsequent tests if test order changes.

💡 Suggested fix

Add cleanup in the test or rely on the beforeEach that already sets process.env.GITHUB_ACTOR = "github-actions[bot]". Since beforeEach runs before each test and calls vi.resetModules(), the leak is currently harmless — but it's fragile. Consider using afterEach or a scoped approach:

it('uses GITHUB_ACTOR for review_id=auto actor matching', async () => {
  const origActor = process.env.GITHUB_ACTOR;
  try {
    process.env.GITHUB_ACTOR = 'custom-bot';
    // ... test body ...
  } finally {
    process.env.GITHUB_ACTOR = origActor;
  }
});

@copilot please address this.

@pelikhan

pelikhan commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

@copilot update safe outputs to dismiss all review request from the request user to deal with these cases.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Handle degenerate REVIEW_REQUIRED PR state in dismiss_pull_request_review

The approach is well-structured and the new test coverage is thorough. Found 4 non-blocking issues worth addressing:

  1. Unnecessary Promise.all on happy path (line 190, main file) — pullRequest is only needed in the failure branch, so the pulls.get call is wasted on every successful auto-resolution. Lazy fetch is the fix.

  2. GITHUB_ACTOR env var leaks between tests (line 259, test file) — no teardown restores the value after the custom-bot test, risking ordering-dependent test failures.

  3. mergeable_state === "blocked" is over-broad (line 201, main file) — many things cause blocked state beyond the degenerate review-required case. The error message should be hedged as a best-guess diagnosis.

  4. Staged mode bypasses auto-resolution (line 174, main file) — a staged run with review_id=auto always returns success: true without checking whether a dismissible review exists. Staged previews should surface the same failure paths as live runs.

All four inline comments include specific suggestions. None block correctness of the happy path.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 99.6 AIC · ⌖ 8.75 AIC · ⊞ 4.9K


try {
if (useAutoReviewId) {
const [{ data: pullRequest }, reviews] = await Promise.all([

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary eager fetch of PR details on happy path. pullRequest is only used in the failure branch (when no dismissible review is found), yet it is fetched unconditionally in a Promise.all on every review_id=auto call. On the common happy path this is a wasted API call.

Suggest fetching it lazily:

const reviews = await listAllPullRequestReviews(githubClient, owner, repo, pullRequestNumber);
const autoReview = findLatestDismissibleReviewForActor(reviews, expectedAuthor);
if (!autoReview || !Number.isInteger(autoReview.id) || autoReview.id <= 0) {
  const { data: pullRequest } = await githubClient.rest.pulls.get({ owner, repo, pull_number: pullRequestNumber });
  ...
}

@copilot please address this.

});

it("uses GITHUB_ACTOR for review_id=auto actor matching", async () => {
process.env.GITHUB_ACTOR = "custom-bot";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GITHUB_ACTOR env var leak across tests. This test sets process.env.GITHUB_ACTOR = "custom-bot" but there is no afterEach or afterAll that restores the original value. Because vi.resetModules() is called in beforeEach but the env var is not reset, any test that runs after this one (e.g., in the same file with a different order) would unexpectedly use custom-bot as the actor.

Add cleanup in an afterEach or restore at the end of this test:

const previousActor = process.env.GITHUB_ACTOR;
process.env.GITHUB_ACTOR = "custom-bot";
// ... test body ...
process.env.GITHUB_ACTOR = previousActor;

@copilot please address this.


const autoReview = findLatestDismissibleReviewForActor(reviews, expectedAuthor);
if (!autoReview || !Number.isInteger(autoReview.id) || autoReview.id <= 0) {
const isBlockedWithoutReviewers = hasNoRequestedReviewersOrTeams(pullRequest) && pullRequest?.mergeable_state === "blocked";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mergeable_state === "blocked" is too broad for degenerate-state detection. A PR can be blocked for many reasons: failing required status checks, a required review is pending from a different reviewer, branch protection mismatches, etc. None of those are the degenerate review-required state this code is trying to detect.

The combination of blocked + no requested_reviewers + no requested_teams + no dismissible actor review is a reasonable heuristic, but the code and error message present it as a definitive detection. Consider softening the message to reflect this is a best-guess diagnosis:

error: "no dismissible actor-authored review was found; PR may be in a degenerate REVIEW_REQUIRED state (blocked with no pending reviewer requests)"

This avoids misleading callers who hit blocked for unrelated reasons.

@copilot please address this.

@@ -99,19 +172,47 @@
if (filterResult) return filterResult;

if (isStaged) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Staged mode with review_id=auto reports success: true without verifying a dismissible review exists. In staged/dry-run mode the function returns early (line 178) with review_id: "auto" in the payload, bypassing the auto-resolution logic entirely. This means a staged run will always succeed even when the same payload would fail at runtime (e.g., no actor-authored review, or degenerate state).

Consider running the review lookup in staged mode too (skip only the actual dismissReview call), so that staged previews surface failures before they reach production.

@copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — three correctness issues need fixing before merge.

### Blocking findings (3)
  1. mergeable_state race condition (line 201): GitHub computes mergeable_state asynchronously; it returns null or "unknown" immediately after pushes/review events. The === "blocked" gate reliably misses the degenerate state in those windows, defeating the core purpose of this PR.

  2. hasNoRequestedReviewersOrTeams false-negative when fields are absent (line 63): Array.isArray(undefined) is false, so the function returns false when the API response omits either field — degenerate-state detection silently never triggers.

  3. Staged+auto path emits review_id: "auto" (string) (line 175): every non-staged path returns a numeric review_id; this inconsistency will cause schema/type mismatches for downstream consumers.

Non-blocking findings (1)

  1. Silent 1000-review cap (line 77): no warning is emitted when pagination hits maxPages, so the wrong (non-latest) review may be silently selected on PRs with long review histories.

🔎 Code quality review by PR Code Quality Reviewer · 155.9 AIC · ⌖ 8.55 AIC · ⊞ 5.4K
Comment /review to run again


const autoReview = findLatestDismissibleReviewForActor(reviews, expectedAuthor);
if (!autoReview || !Number.isInteger(autoReview.id) || autoReview.id <= 0) {
const isBlockedWithoutReviewers = hasNoRequestedReviewersOrTeams(pullRequest) && pullRequest?.mergeable_state === "blocked";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mergeable_state race condition silently defeats degenerate-state detection: GitHub computes mergeable_state asynchronously and commonly returns null or "unknown" immediately after pushes/review events, so === "blocked" will miss the degenerate state during those windows.

💡 Suggested fix

The degenerate condition is really no requested reviewers/teamsmergeable_state should not be the gating signal:

// Before (fragile — misses null/"unknown" during async recompute)
const isBlockedWithoutReviewers =
  hasNoRequestedReviewersOrTeams(pullRequest) &&
  pullRequest?.mergeable_state === "blocked";

// After (reliable — only checks the structural invariant)
const isBlockedWithoutReviewers = hasNoRequestedReviewersOrTeams(pullRequest);

If the intent is to distinguish a truly blocked PR from one that just has no pending reviewers for other reasons, document the exact conditions under which mergeable_state can be trusted and add a test for null/"unknown". Otherwise drop the mergeable_state guard entirely; the requested_reviewers/requested_teams check alone describes the misconfigured state the PR describes.

* @returns {boolean}
*/
function hasNoRequestedReviewersOrTeams(pullRequest) {
const hasNoRequestedReviewers = Array.isArray(pullRequest?.requested_reviewers) && pullRequest.requested_reviewers.length === 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasNoRequestedReviewersOrTeams silently returns false when fields are absent: if the pulls.get response omits requested_reviewers or requested_teams (e.g. a minimal/partial response), Array.isArray(undefined) is false, so both local variables are false and the function returns false — degenerate-state detection never fires.

💡 Suggested fix

Treat absent fields as empty (zero reviewers), which is the safe default for this invariant:

function hasNoRequestedReviewersOrTeams(pullRequest) {
  const reviewers = pullRequest?.requested_reviewers;
  const teams = pullRequest?.requested_teams;
  const hasNoReviewers = !Array.isArray(reviewers) || reviewers.length === 0;
  const hasNoTeams = !Array.isArray(teams) || teams.length === 0;
  return hasNoReviewers && hasNoTeams;
}

This way a missing field is treated as an empty list (no reviewers requested) rather than silently suppressing the detection path.


if (isStaged) {
logStagedPreviewInfo(`Would dismiss review #${reviewId} on PR #${pullRequestNumber} (${owner}/${repo}) as ${dismisser}`);
const previewReviewId = useAutoReviewId ? "auto" : reviewId;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Staged+auto path returns review_id: "auto" (string) instead of a resolved integer: every non-staged code path returns review_id as a number, but the staged+auto early-return emits the string "auto". Downstream consumers (schema validation, workflow steps reading the output) that expect a numeric ID will receive a type mismatch with no indication that resolution was skipped.

💡 Suggested fix

Document the intentional difference or normalise the output. At minimum, add a review_id_resolved: false flag to the staged response so consumers can distinguish it from a real ID:

return {
  success: true,
  staged: true,
  review_id: previewReviewId, // still "auto" — resolution skipped in staged mode
  review_id_resolved: !useAutoReviewId,
  pull_request_number: pullRequestNumber,
  repo: `${owner}/${repo}`,
  author: expectedAuthor,
};

Or if a string "auto" is intentionally allowed in the output schema, update the TypeScript type and JSON schema to reflect that.

*/
async function listAllPullRequestReviews(githubClient, owner, repo, pullRequestNumber) {
const all = [];
const maxPages = 10;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent truncation at 1000 reviews can select the wrong (non-latest) review to dismiss: maxPages=10 with per_page=100 caps total fetched reviews at 1000 with no warning. On PRs with >1000 reviews, findLatestDismissibleReviewForActor sorts over an incomplete set and may select a stale review, silently dismissing the wrong one.

💡 Suggested fix

At minimum, log a warning when the cap is reached so the caller is not blindsided:

if (page > maxPages) {
  // Fetched maximum reviewable pages; actor's latest review may be
  // in the unfetched tail. Log and surface this to the caller.
  console.warn(`[listAllPullRequestReviews] Reached max page limit (${maxPages}); review list may be incomplete.`);
}
return all;

A stronger fix would be to short-circuit once a page containing an actor-authored dismissible review is found (since GitHub returns reviews in ascending chronological order, scan from the last page backward). That avoids fetching the entire history and eliminates the silent-truncation risk.

Copilot AI and others added 2 commits July 6, 2026 02:17
…he latest

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@copilot update safe outputs to dismiss all review request from the request user to deal with these cases.

Done. review_id: "auto" now dismisses all dismissible (APPROVED or CHANGES_REQUESTED) reviews authored by the workflow actor, not just the latest one. The response includes review_ids (array) and dismissed_count for the auto path.

Key changes in commit feat: review_id=auto dismisses all actor-authored reviews, not just the latest:

  • findLatestDismissibleReviewForActorfindAllDismissibleReviewsForActor (returns full array)
  • Auto path loops over all matching reviews and calls dismissReview on each
  • listAllPullRequestReviews now returns { reviews, truncated } and surfaces an explicit error when the 1000-entry pagination cap is hit (instead of silently returning incomplete results)
  • Tests updated to verify multi-review dismissal and the truncation error path

@pelikhan

pelikhan commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

@copilot auto is the default value of review_id is not provided

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@copilot auto is the default value of review_id is not provided

Done. review_id is now optional and defaults to "auto" when omitted. Changes in commit feat: make review_id optional, defaulting to auto when omitted:

  • Handler treats missing/empty review_id as "auto" (dismisses all actor-authored dismissible reviews)
  • Removed review_id from required in both safe_outputs_tools.json files and schemas/agent-output.json
  • Updated tool descriptions to document the default behavior
  • Added a test verifying omitted review_id behaves identically to review_id: "auto"

@pelikhan pelikhan merged commit b4a8458 into main Jul 6, 2026
37 checks passed
@pelikhan pelikhan deleted the copilot/handle-misconfigured-pr-states branch July 6, 2026 02:55
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address the remaining unresolved review feedback, and rerun checks once the branch is current.

Remaining feedback to close out:

  • github-actions — make the degenerate-state detection robust when mergeable_state is null/unknown or reviewer fields are absent
  • github-actions — keep staged and live review_id=auto behavior/schema consistent, including omitted review_id defaulting cleanly
  • github-actions — tighten the auto-resolution diagnostics/tests around pagination caps and environment cleanup

Run: https://github.com/github/gh-aw/actions/runs/28764674525

Generated by 👨‍🍳 PR Sous Chef · 4.08 AIC · ⌖ 9.33 AIC · ⊞ 7.1K ·
Comment /souschef to run again

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants