Skip to content

fix(review): log the 422 body, abandon superseded posts, and preserve refused reviews as comments - #705

Merged
devops-thiago merged 4 commits into
mainfrom
fix/704-review-post-resilience
Aug 13, 2026
Merged

fix(review): log the 422 body, abandon superseded posts, and preserve refused reviews as comments#705
devops-thiago merged 4 commits into
mainfrom
fix/704-review-post-resilience

Conversation

@devops-thiago

Copy link
Copy Markdown
Owner

What type of PR is this?

  • 🐛 Bug fix

Description

A review on #701 was lost to a GitHub 422 that the logs could not explain. This PR ships the three fixes from the postmortem:

  1. The 422 body is logged. A rejected review post now reads the HTTP response off the failure's cause chain through the existing GitHubApiError seam and logs its diagnostics line — status, throttle headers, and the response body, with credential-shaped text redacted and the length capped. The next 422 will name its cause.
  2. A superseded run abandons its post. Just before the first write, ReviewOrchestrator re-reads the PR head (a fresh read through the token-healing read seam), and when it moved from the sha the run reviewed, the post is abandoned: counted as a structured HEAD_MOVED skip via ReviewSkipEmitter, the stale check run concluded as skipped, and nothing user-facing posted — the dispatcher's coalesced run for the new head re-reviews and posts in its place. This also stops inline comments resolving against a diff that moved underneath them. Fail-open: a failed head read never abandons a finished review.
  3. A refused summary-only review falls back to an issue comment. createReviewWithFallback keeps today's retry-without-comments path; when a no-comments review (or that retry) is refused, the same body is posted as an issue comment — through the capped, paced conversation-write path — with a note that GitHub refused the review post, instead of discarding the generation behind "review could not be completed". ReviewPostException now fires only when the comment fallback fails too.

Per the issue, this does not attempt to fix the unknown 422 root cause (it cannot be named until fix 1 ships) and does not change review-event semantics beyond what the fallback needs.

Related Issues

Closes #704

How Has This Been Tested?

  • Unit tests
  • Integration tests
  • Manual testing

New tests cover: the rejection-diagnostics path with a response-carrying cause chain; the no-comments 422 falling back to an issue comment carrying the body (and the zero-issues message when the body is blank/null); the comment-carrying 422 keeping the retry-without-comments path and falling through to the comment fallback when the retry is refused too; ReviewPostException (with the comment failure suppressed) when the fallback also fails; the full-path head-moved abandon (no review, no comment, HEAD_MOVED counted, check run skipped); headMoved comparisons including fail-open; abandon surviving check-run and persistence failures; and currentHeadSha returning the fresh head and empty on every unreadable shape. ./mvnw verify passes (3222 tests) and a local merge-base JaCoCo simulation of codecov/patch reports 100% on changed lines.

Checklist

  • My code follows the project's coding standards
  • I have performed a self-review of my own code
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have updated the documentation accordingly
  • My changes generate no new warnings or errors

Screenshots / Logs

N/A

Additional Notes

CHANGELOG updated under [Unreleased] → Fixed.

… refused reviews as comments

A review on #701 was lost to a GitHub 422 the logs could not explain:
the response body was discarded, the run posted 9.5 minutes after the
dispatcher knew it was superseded, and the no-comments rejection path
threw the whole generation away.

- A rejected review post now logs GitHub's own response body through
  the GitHubApiError seam (credential-shaped text redacted, length
  capped), so the next 422 names its cause.
- ReviewOrchestrator re-reads the PR head just before the first write
  (a fresh read through the #693-healed seam) and abandons the post
  when the head moved: counted as a structured HEAD_MOVED skip, the
  stale check run concluded as skipped, nothing user-facing — the
  coalesced run for the new head posts in its place. Fail-open: a
  failed head read never abandons a finished review.
- createReviewWithFallback keeps the retry-without-comments path, and
  when a summary-only review (or the retry) is refused, posts the same
  body as an issue comment through the capped, paced write path with a
  note that GitHub refused the review post, instead of surfacing
  "review could not be completed". ReviewPostException now fires only
  when the comment fallback fails too.

Closes #704
@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@thrillhousebot

Copy link
Copy Markdown
Contributor

⚠️ Findings not fully verified. The 2 finding(s) were NOT verified by the second-pass audit — no verdicts were returned, so they post as the reviewer raised them.

🤖 ThrillhouseBot PR Summary

What this PR does

Ships the three #704 postmortem fixes: a rejected review post now logs GitHub's own response body through the existing GitHubApiError seam (redacted and capped); ReviewOrchestrator re-reads the PR head just before posting and abandons a superseded run (HEAD_MOVED skip, check run concluded skipped, session failed, nothing user-facing posted) when the head moved during the model call; and a refused summary-only review (or a refused retry) falls back to posting the same body as an issue comment through the paced conversation-write path, with ReviewPostException firing only when that comment fallback also fails.

Description vs. Implementation

No mismatch found between the PR description and the change.

Control-Flow Diagram

🔀 Show diagram
flowchart TD
  A["review() finishes the model call"] --> B["currentHeadSha: fresh GET of PR head"]
  B --> C{"head moved from reviewed sha?"}
  C -- "yes" --> D["abandonSupersededRun: HEAD_MOVED skip, check run skipped, session failed"]
  D --> E["return false: nothing posted"]
  C -- "no" --> F["publishSummaryBestEffort"]
  F --> G["postReview"]
  G --> H{"createReview throws?"}
  H -- "yes" --> I["logReviewRejection: response body via GitHubApiError"]
  I --> J{"comments empty or retry failed?"}
  J -- "yes" --> K["postReviewBodyAsComment: REVIEW_REFUSED_NOTE plus body"]
  J -- "no" --> L["retry without comments"]
  L --> H
  K --> M["ReviewPostException only when comment fallback fails too"]
  H -- "no" --> N["review posted"]
Loading

Changes Overview

  • Files changed: 7
  • Lines added: +473
  • Lines removed: -10

Changed Files

File Change Summary
CHANGELOG.md Modified Documents the three #704 fixes under Unreleased -> Fixed.
src/main/java/dev/thiagogonzaga/thrillhousebot/review/ReviewContextLoader.java Modified Adds currentHeadSha — a fail-open fresh head read through getPullRequest, used as the pre-post guard.
src/main/java/dev/thiagogonzaga/thrillhousebot/review/ReviewOrchestrator.java Modified Re-reads the PR head before posting; abandons superseded runs (HEAD_MOVED skip, skipped check run, failed session) when the head moved during review.
src/main/java/dev/thiagogonzaga/thrillhousebot/review/ReviewPublisher.java Modified createReviewWithFallback logs rejection diagnostics and preserves a refused review body as an issue comment; ReviewPostException only when the comment fallback fails too.
src/main/java/dev/thiagogonzaga/thrillhousebot/review/ReviewSkipReason.java Modified Adds the HEAD_MOVED skip reason for superseded-run abandonment.
src/test/java/dev/thiagogonzaga/thrillhousebot/review/ReviewContextLoaderTest.java Modified Tests currentHeadSha: returns the fresh head, and is empty on throw, null PR, null head, or blank sha.
src/test/java/dev/thiagogonzaga/thrillhousebot/review/ReviewOrchestratorTest.java Modified Adds tests for head-moved abandon, HEAD_MOVED skip counting, issue-comment fallback for refused reviews, and 422 diagnostics; adjusts failure-path stubs.

Risk Assessment

Risk Count
🔴 Critical 0
🟠 High 0
🟡 Medium 2
🔵 Low 0

Key Findings

  • MEDIUM: Inserting currentHeadSha detaches fetchPrTotals' existing Javadoc (src/main/java/dev/thiagogonzaga/thrillhousebot/review/ReviewContextLoader.java:491)

Things to double-check

1 lower-confidence finding
  • MEDIUM: 422-body logging relies on production exception being a WebApplicationException and its test never asserts the log (src/main/java/dev/thiagogonzaga/thrillhousebot/review/ReviewPublisher.java:850) (low confidence — verify before acting)

⚠️ Required CI Checks Status

Some required checks are still pending or have failed:

Check Type Status Detail
format check-run ⏳ Pending -
frontend check-run ⏳ Pending -
test check-run ⏳ Pending -
trivy check-run ⏳ Pending -
dependency-review check-run ⏳ Pending -

Automated review by ThrillhouseBot. Reply with /review to re-run.

@thrillhousebot thrillhousebot 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.

ThrillhouseBot noted 1 lower-confidence item(s) under Things to double-check in the PR summary (not posted as inline threads):

  • MEDIUM: 422-body logging relies on production exception being a WebApplicationException and its test never asserts the log (src/main/java/dev/thiagogonzaga/thrillhousebot/review/ReviewPublisher.java:850)
    Fix 1 only reads the HTTP response if webApplicationFailure finds a jakarta.ws.rs.WebApplicationException in the failure's cause chain (if (t instanceof WebApplicationException web)). The #701 log quoted in the issue shows the real production failure is a ClientWebApplicationException thrown by the MicroProfile REST client, while the only test covering this path fabricates the trigger as new RuntimeException(new jakarta.ws.rs.WebApplicationException(response)) — a shape chosen by the test author, not the production one. If the REST client exception is not assignable to jakarta.ws.rs.WebApplicationException (or does not carry one in its cause chain), logReviewRejection falls into the orElse("no HTTP response to read ...") branch and the PR's headline fix silently does nothing in production. Additionally, shouldLogGitHubResponseBodyWhenReviewRejected never asserts that any log or diagnostics output was produced — it only verifies the fallback comment was created, so it would stay green even if the diagnostics computation were broken. Verify that the exception Quarkus/RESTEasy surfaces for a 422 (the ClientWebApplicationException seen in #701) is caught by instanceof WebApplicationException and that GitHubApiError.of(...) / diagnostics() reads the response body; consider asserting the diagnostics log line in the test.

@thrillhousebot thrillhousebot Bot added bug Something isn't working java Pull requests that update java code testing Test coverage and test quality labels Aug 13, 2026
…e and fix Sonar findings

- Replace the fabricated WebApplicationException in the 422-logging test
  with the REST client's own ClientWebApplicationException (which extends
  jakarta.ws.rs.WebApplicationException and carries the response), and
  assert the logged line carries status=422 and GitHub's message.
- Guard the Optional access Sonar flagged (S3655) by filtering the fresh
  head through headMoved before isPresent/get.
- Reattach fetchPrTotals' javadoc, moving currentHeadSha below it (S8491).
@devops-thiago

Copy link
Copy Markdown
Owner Author

@thrillhousebot resolved src/main/java/dev/thiagogonzaga/thrillhousebot/review/ReviewPublisher.java:850 — 422-body logging relies on production exception being a WebApplicationException and its test never asserts the log

Verified against resteasy-reactive-common 3.38.0: org.jboss.resteasy.reactive.ClientWebApplicationException extends jakarta.ws.rs.WebApplicationException and carries the Response, so the instanceof WebApplicationException walk catches the production shape. The test now constructs ClientWebApplicationException with a 422 response end-to-end and asserts the logged line contains status=422 and GitHub's message text.

🤖 Addressed by Claude Code

@thrillhousebot

Copy link
Copy Markdown
Contributor

A line number followed by a spaced separator and another number reads as a line range, and a range names no single finding. The one exception is the summary's own row: when an em dash (—) after the line number is followed by the finding's full title exactly as the summary prints it, the next review will still close that finding. Anything else — a real range, a shortened title, or a dotted spelling — clears nothing; name each finding by its path:line and full title as printed.

@devops-thiago

Copy link
Copy Markdown
Owner Author

@thrillhousebot resolved src/main/java/dev/thiagogonzaga/thrillhousebot/review/ReviewPublisher.java:850 — 422-body logging relies on production exception being a WebApplicationException and its test never asserts the log

@thrillhousebot

Copy link
Copy Markdown
Contributor

A line number followed by a spaced separator and another number reads as a line range, and a range names no single finding. The one exception is the summary's own row: when an em dash (—) after the line number is followed by the finding's full title exactly as the summary prints it, the next review will still close that finding. Anything else — a real range, a shortened title, or a dotted spelling — clears nothing; name each finding by its path:line and full title as printed.

@thrillhousebot

Copy link
Copy Markdown
Contributor

🤖 ThrillhouseBot — changes since the last review

  • New findings this round: 1
  • Previous findings resolved: 2
  • Previous findings still open: 0

⚠️ The 1 finding(s) were not verified (no verdicts were returned).

An ambiguous failure — a client-side timeout, a connection reset, a
5xx — carries no proof GitHub refused the review post; the POST may
have been applied, so posting the body again as a comment would
duplicate the review while asserting a refusal the code cannot
support. The fallback now fires only when the failure carries a
response with a 4xx status (the same distinction logReviewRejection
already draws); everything else propagates as ReviewPostException and
keeps today's fail-and-mark-failed behavior.
@thrillhousebot

Copy link
Copy Markdown
Contributor

🤖 ThrillhouseBot — changes since the last review

  • New findings this round: 1
  • Previous findings resolved: 1
  • Previous findings still open: 0

…ore the comment fallback

rejection = retryFailure overwrote the first attempt's failure, so an
ambiguous first attempt (which may have landed its review) followed by
a refused retry classified the run by its last failure alone and fell
into the comment fallback — risking a duplicate of the landed review.
The run now tracks whether any attempt was ambiguous and throws
ReviewPostException when one was; the fallback fires only when every
attempt drew a response-carrying 4xx. Both mixed orders are tested.
@sonarqubecloud

Copy link
Copy Markdown

@thrillhousebot

Copy link
Copy Markdown
Contributor

🤖 ThrillhouseBot — changes since the last review

  • New findings this round: 0
  • Previous findings resolved: 1
  • Previous findings still open: 0

@thrillhousebot thrillhousebot 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.

ThrillhouseBot found no issues in this PR, but some checks are still pending or failed:

  • Check test is pending
  • Check format is pending
  • Check trivy is pending
  • Check frontend is pending
  • Check dependency-review is pending

@devops-thiago
devops-thiago merged commit 280dc92 into main Aug 13, 2026
17 checks passed
@devops-thiago
devops-thiago deleted the fix/704-review-post-resilience branch August 13, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working java Pull requests that update java code testing Test coverage and test quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A review lost to a GitHub 422: the response body is not logged, and a superseded run still posts

1 participant