Skip to content

fix(webhook): bound the manual-review authorization GitHub API call on the ACK path (#92) - #164

Merged
devops-thiago merged 4 commits into
release/v0.2.0from
claude/busy-babbage-942447
Jun 19, 2026
Merged

fix(webhook): bound the manual-review authorization GitHub API call on the ACK path (#92)#164
devops-thiago merged 4 commits into
release/v0.2.0from
claude/busy-babbage-942447

Conversation

@devops-thiago

@devops-thiago devops-thiago commented Jun 19, 2026

Copy link
Copy Markdown
Owner

What type of PR is this?

  • 🐛 Bug fix
  • ✨ Feature
  • 📝 Documentation
  • 🔧 Refactor
  • 🚀 Performance
  • ✅ Test
  • 🔒 Security
  • 📦 Dependency update
  • 🏗️ CI/CD

Description

ManualReviewAuthorizer.hasWriteAccess makes a blocking GitHub call — collaboratorPermission, plus a possible installation-token mint on a cold cache — on the synchronous webhook request thread before the 200 ACK is returned (WebhookController.handleIssueCommentisAuthorized). GitHub expects the webhook ACK within ~10s; under a degraded GitHub a single authorized /review could occupy a webhook worker thread past that SLA, risk a redelivery, and tie up the request pool.

This PR adds an explicit, short, configurable timeout to that authorization work so it fails fast on the ACK path.

Why an in-code timeout rather than a REST-client read-timeout

Both blocking calls route through the github-api REST client — the same client that fetches large PR diffs (getPullRequestFiles / compareCommits) on the review path. Setting quarkus.rest-client."github-api".read-timeout to a few seconds would also clamp those large-diff fetches, risking spurious review failures. So the bound is applied in code, scoped to exactly the ACK-path authorization and covering both the token mint and the permission call.

What changed

  • ManualReviewAuthorizer runs the write-access check on a dedicated virtual-thread executor (matching the existing ReviewExecutorProducer idiom) and applies Future.get(timeout). It still fails closed on missing permission / any API error — same behavior as before — and now also on timeout or interrupt. The executor is torn down via @PreDestroy.
  • ThrillhouseConfig.ReviewConfig gains manual-trigger-auth-timeout (Duration, default 5s).
  • application.properties surfaces it as the MANUAL_TRIGGER_AUTH_TIMEOUT env override.

The negative author_association pre-filter (zero-API rejection of non-write associations) and the allowlist short-circuit are untouched, so the common abuse path still makes no API call. Automatic pull_request reviews are unaffected.

Related Issues

Fixes #92
Refs #85, #70

How Has This Been Tested?

  • Unit tests

  • Integration tests

  • Manual testing

  • New ManualReviewAuthorizerTest.shouldFailClosedWhenPermissionCheckExceedsTimeout: with a 100ms timeout and a permission call that would otherwise answer "admin" after 2s, the check abandons it and denies.

  • ManualReviewAuthorizerTest + WebhookControllerTest: 45 tests, 0 failures (run with -Djacoco.skip=true -Dquarkus.jacoco.enabled=false on JDK 25).

  • ./mvnw spotless:check passes.

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

Additional Notes

The timeout is operator-tunable (MANUAL_TRIGGER_AUTH_TIMEOUT, default 5s) and bounds the entire authorization round-trip on the webhook acknowledgement thread, including a cold-cache installation-token mint.

…n the ACK path (#92)

ManualReviewAuthorizer.hasWriteAccess makes a blocking GitHub call
(collaboratorPermission, plus a possible installation-token mint on a
cold cache) on the synchronous webhook request thread before the 200
ACK. Under a degraded GitHub this could occupy a webhook worker past
GitHub's ~10s delivery SLA.

Both calls route through the shared github-api REST client that also
fetches large PR diffs on the review path, so a config-level read-timeout
would clamp those fetches too. Instead, bound the work in code, scoped to
exactly the ACK-path authorization: run the write-access check on a
virtual-thread executor and apply a configurable timeout
(manual-trigger-auth-timeout, default 5s), failing closed on
timeout/interrupt/error as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Dependency Review

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

Scanned Files

None

@thrillhousebot

Copy link
Copy Markdown

🤖 ThrillhouseBot PR Summary

What this PR does

Adds a configurable timeout for the GitHub write-access permission check during manual-review authorization, executing the check asynchronously on virtual threads so that the webhook acknowledgement thread is not blocked past GitHub's SLA. If the check exceeds the timeout, authorization is denied (fail‑closed).

Changes Overview

  • Files changed: 4
  • Lines added: +87
  • Lines removed: -8

Risk Assessment

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

No new issues found in this PR, but the review cannot be approved until the required checks are passing.

⚠️ Required CI Checks Status

Some required checks are still pending or have failed:

Check Type Status Detail
docker-pr check-run ⏳ Pending -
test check-run ⏳ Pending -
Analyze (java-kotlin) check-run ⏳ Pending -

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

@thrillhousebot thrillhousebot Bot left a comment

Copy link
Copy Markdown

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 docker-pr is pending
  • Check test is pending
  • Check Analyze (java-kotlin) is pending

@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.92%. Comparing base (48586d2) to head (6fff066).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #164      +/-   ##
============================================
- Coverage     98.99%   98.92%   -0.08%     
- Complexity     1437     1440       +3     
============================================
  Files            51       51              
  Lines          3791     3810      +19     
  Branches        541      542       +1     
============================================
+ Hits           3753     3769      +16     
- Misses            8       10       +2     
- Partials         30       31       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…erage gate

SonarCloud new-code coverage was 78.8% (< 80% gate): the @PreDestroy
shutdown, the InterruptedException handler, and the getCause() null
branch were uncovered.

- Simplify the ExecutionException log to pass the cause as the SLF4J
  throwable arg, dropping the null-cause ternary (a task ExecutionException
  always carries a non-null cause) — removes the uncovered condition.
- Add tests for the interrupted-while-waiting path and for shutdown().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…check

Moving the write-access check onto a Callable meant any Throwable it threw
(including Errors such as OutOfMemoryError) was wrapped in ExecutionException
and swallowed as a denied review — the original synchronous code caught only
RuntimeException and let Errors propagate. Rethrow the cause when it is an
Error to preserve that fail-fast behavior; add a test asserting propagation.

Addresses review feedback on #164.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@thrillhousebot thrillhousebot Bot left a comment

Copy link
Copy Markdown

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 docker-pr is pending
  • Check test is pending
  • Check Analyze (java-kotlin) is pending

…n check

- Use unnamed catch variables (`_`) for the unused TimeoutException and
  InterruptedException bindings (java:S7467), matching the codebase idiom.
- Replace Thread.sleep in the timeout/interrupt tests with a CountDownLatch
  that keeps the mocked call in flight deterministically (java:S2925); also
  drops ~4s of wall-clock wait from the suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@thrillhousebot thrillhousebot Bot left a comment

Copy link
Copy Markdown

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 docker-pr is pending
  • Check Analyze (java-kotlin) is pending
  • Check test is pending

@devops-thiago
devops-thiago changed the base branch from main to release/v0.2.0 June 19, 2026 14:03
@sonarqubecloud

Copy link
Copy Markdown

@devops-thiago
devops-thiago merged commit d93a44f into release/v0.2.0 Jun 19, 2026
16 checks passed
@devops-thiago
devops-thiago deleted the claude/busy-babbage-942447 branch June 19, 2026 14:04
devops-thiago added a commit that referenced this pull request Jun 19, 2026
…dit (#165)

Follow-ups from the `release/v0.2.0` review of the eight merged feature
PRs (#157#164). The quality gate passed on all of them, but the audit
surfaced 7 open Sonar code smells and 7 behavior findings. This PR
addresses every one, with tests, and targets `release/v0.2.0`.

## Sonar code smells (now resolved)
| Rule | Where | Fix |
|------|-------|-----|
| S3776 | `WebhookController.handleIssueComment` | Extracted
`handleCommentCommand` + `maybeDispatchConversationalMention` to drop
cognitive complexity from 17 |
| S135 | `MaintainerReplyService.renderThread`,
`CommentCommandService.botRootCommentIds` | Collapsed the double
`break`/`continue` (do-while paging) |
| S2629 | `CommentCommandService` `/summary` logs | Removed the eager
`repository(ctx)` string concat from the log args |
| S7467 | `CommentCommandService.notifyPaused` | Log the
previously-unused `catch` exception |
| S5976 | `WebhookControllerTest` | Parameterized the three "ignore
review comment" tests into one `@ParameterizedTest` |

## Review findings (fixed)
1. **Reply context pagination** — `MaintainerReplyService` listed PR
review comments with the non-paged client (GitHub's 30/page default), so
on a busy thread the root comment and prior replies past page one were
missed. It now walks pages (100/page, bounded).
2. **Commands in quoted text** — `TriggerDetector` matched `/pause`,
`/resolve`, etc. inside fenced code blocks and blockquotes. It now
strips fenced code, blockquotes, and inline code before detection, so
quoting or documenting a command no longer executes it.
3. **`/resolve` thread cap** — `ReviewThreadService` queried
`reviewThreads(first: 100)` with no cursor, so `/resolve` silently
skipped bot threads beyond the first 100. It now paginates via
`pageInfo`/`after` (bounded).
4. **`/pause` race** — the check-then-insert could throw on a concurrent
`/pause` (unique-constraint loser), dropping the confirmation. `/pause`
is now idempotent: a row that already exists counts as success and still
posts the confirmation; only a genuine failure propagates.
5. **Hard-coded bot logins** — the infinite-loop guard hard-coded two
logins, so a different App slug would let the bot answer its own
replies. Bot logins are now configurable
(`thrillhousebot.github.bot-logins`, env `GITHUB_BOT_LOGINS`), falling
back to the built-in logins if empty.
6. **Null head/base NPE** — a `pull_request` event missing `head`/`base`
would NPE and silently drop the delivery; `WebhookController` now guards
and ignores it.
7. **Non-numeric `GITHUB_APP_ID`** — passed the fail-fast validator and
only failed on the first webhook; `StartupConfigValidator` now rejects
it at boot.

## Notes
- The bot's own inline findings on the original PRs were already
validated by the maintainer (5 fixed, 5 declined). The `/pause` race
here is the one decline ("premise doesn't hold") that was valid — the
async `/pause` path runs on the unbounded virtual-thread executor, so
concurrent commands can race. Fixed with bounded impact.
- The four "declined false positives" in #159 were independently
re-verified as correct (GitHub review threads are flat; arrow-case
yields the boolean; non-PR mentions are guarded) and are **not** changed
here.

## Testing
- Full suite: **1160 tests, 0 failures**, `spotless:check` clean (run
with `-Djacoco.skip=true -Dquarkus.jacoco.enabled=false` per the JDK 25
JaCoCo issue).
- New/updated tests cover: app-id numeric validation, fenced/quoted
command stripping, configurable + empty-fallback bot logins, GraphQL
thread pagination, reply-comment pagination, the `/pause` race (both
branches), and the null head/base guard.
devops-thiago added a commit that referenced this pull request Jun 19, 2026
…ance (#168)

Adds `.github/thrillhousebot.md` — the per-repo review-instructions file
the bot's own `instructions-file` config reads first, which this
repository was not dogfooding.

The seed set comes directly from the v0.2.0 review retrospective (PRs
#157#165), targeting the bot's observed failure modes on its own code:

- **Platform facts** that caused false positives — *GitHub review
threads are flat* (the bot raised a now-declined "nested replies"
finding twice on #159); *list endpoints default to 30/page*.
- **Review heuristics** for recurring misses — paginate GitHub
collection fetches (missed on #159/#160, see #166); command parsers must
ignore fenced/quoted text (missed on #160); validators must check format
not just presence (missed on #157); new config keys must be documented
(missed on #164).
- **House conventions** — identify the bot's own comments via
`TriggerDetector.isBotComment(...)` not a hardcoded login (#165); guard
nullable payload fields like `head`/`base` consistently (#158).

This is the cheap, immediate mitigation that pays off on the next
review, ahead of the deeper prompt/verifier work tracked in #112 / #117
/ #123 / #166 and the automated learnings in #38.

Closes #167.
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.

fix(webhook): bound the manual-review authorization GitHub API call on the ACK path

1 participant