Skip to content

ci: auto-resolve Claude review threads once the finding is fixed - #3064

Draft
teeohhem wants to merge 1 commit into
mainfrom
spokane
Draft

ci: auto-resolve Claude review threads once the finding is fixed#3064
teeohhem wants to merge 1 commit into
mainfrom
spokane

Conversation

@teeohhem

@teeohhem teeohhem commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Inline review comments from claude-code-review.yml never close. The fingerprint dedup stops a finding being re-posted on the next push, but the thread it opened stays unresolved forever. Fixed findings pile up as stale bot conversations and block any branch protection requiring resolved threads.

Fix

New Resolve fixed review threads step calls GraphQL resolveReviewThread on a thread when all four hold:

  • it's ours — first comment is the bot's and carries an hdxr: fingerprint
  • this run's findings no longer mention that fingerprint
  • GitHub marks the thread isOutdated — the flagged line actually moved
  • no human has replied

The isOutdated pairing is the load-bearing part. The reviewer measures ~40% recall, so a finding vanishing from a run means "fixed" or "missed" with no way to tell them apart. Resolving on absence alone would silently launder a real critical into "resolved". Requiring the line to have moved too means we only close what the author plausibly touched.

The step is fail-soft, unlike everything else in this job — resolution is cosmetic, so a GraphQL error warns and moves on rather than failing a review that already posted. The next push retries for free. It runs after the trusted-tree verification, so the helper it require()s is already covered.

Resolve count is reported in the sticky summary, including on the all-clear branch — the run that resolves the most threads is usually the one that finds nothing left.

Not fixed here

  • deep-review.yml — posts one sticky issue comment. Issue comments have no thread and no resolve state, so there's nothing to resolve. It already self-heals via edit-mode: replace.
  • Human threads — out of scope; we only touch threads we opened.
  • A human who unresolves without commenting gets re-resolved next push. Detecting that needs the timeline API; the human-reply guard covers the case that actually happens.

Permissions

None added. pull-requests: write already covers the mutation, including on fork PRs — pull_request_target's token is base-repo-scoped and the threads live on the base repo.

Cost

trusted-hash.sh covers both changed files, so this bumps prompt_hash and re-reviews every open PR once at ~$3 each on merge. Designed behaviour, but worth timing.

Tests

11 new cases in review-comments.test.mjs (44 total, green), run by the workflow itself before it spends on a review. Each of the three guards was mutation-tested — removing isOutdated, the human-reply check, or the current-findings check each fails exactly one test.

make ci-lint green: 0 errors, ratchet ok, openapi in sync.

No changeset — CI-only, not user-facing.

🤖 Generated with Claude Code

Inline review comments from claude-code-review.yml never close. The
fingerprint dedup stops a finding being re-posted on the next push, but the
thread it opened stays unresolved forever -- so fixed findings accumulate as
stale bot conversations and block any branch protection that requires them
resolved.

Resolve a thread via GraphQL resolveReviewThread when all of:

  - it is ours (first comment is the bot's and carries an hdxr: fingerprint)
  - this run's findings no longer mention that fingerprint
  - GitHub marks the thread isOutdated -- the flagged line actually moved
  - no human has replied

The isOutdated pairing is the load-bearing part. The reviewer measures ~40%
recall, so absence from a run means "fixed" or "missed" with no way to tell
them apart; resolving on absence alone would silently launder a real critical
into "resolved". Requiring the line to have moved as well means we only close
what the author plausibly touched.

The step is deliberately fail-soft, unlike everything else in this job:
resolution is cosmetic, so a GraphQL error warns and moves on rather than
failing a review that already posted. The next push retries for free.

Needs no new permissions -- pull-requests: write already covers the mutation,
including on fork PRs, where pull_request_target's token is base-repo-scoped
and the threads live on the base repo.

Note this bumps prompt_hash (trusted-hash.sh covers both changed files), so
merging re-reviews every open PR once at ~$3 each.
@changeset-bot

changeset-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: c40c1fa

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated
hyperdx-oss Ignored Ignored Sep 2, 2026 7:40pm UTC
hyperdx-storybook Ignored Ignored Sep 2, 2026 7:40pm UTC

Request Review

@github-actions github-actions Bot added the review/tier-1 Trivial — auto-merge candidate once CI passes label Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🟢 Tier 1 — Trivial

Docs, images, lock files, a dependency bump, or an automated release. No functional code changes detected.

Why this tier:

  • All files are docs / images / lock files

Review process: Auto-merge once CI passes. No human review required.
SLA: Resolves automatically.

Stats
  • Production files changed: 0
  • Production lines changed: 0 (+ 180 in test files, excluded from tier calculation)
  • Branch: spokane
  • Author: teeohhem

To override this classification, remove the review/tier-1 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds fail-soft automatic resolution of stale Claude review threads after a finding disappears and its anchor becomes outdated.

  • Selects unresolved, outdated, bot-owned threads with no human replies.
  • Resolves eligible threads through GitHub GraphQL and reports the count in the sticky summary.
  • Adds regression coverage for ownership, current findings, outdated anchors, human replies, permissions, malformed input, and summary rendering.

Confidence Score: 5/5

The PR appears safe to merge with no actionable defects identified.

The resolver checks bot ownership, outdated state, resolvability, human participation, and current fingerprints before mutating a thread, while failures remain retryable and do not invalidate the completed review.

Important Files Changed

Filename Overview
.github/scripts/code-review/review-comments.cjs Adds conservative thread-selection logic and resolution-count rendering without an accepted correctness issue.
.github/workflows/claude-code-review.yml Adds paginated thread discovery, fail-soft GraphQL resolution, and summary output wiring.
.github/scripts/code-review/tests/review-comments.test.mjs Covers each resolution guard, malformed inputs, and both clean and finding-bearing summary branches.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Completed healthy review] --> B[Fetch all review threads]
  B --> C{Bot-owned and unresolved?}
  C -- No --> K[Keep open]
  C -- Yes --> D{Outdated and resolvable?}
  D -- No --> K
  D -- Yes --> E{Human replied?}
  E -- Yes --> K
  E -- No --> F{Fingerprint still reported?}
  F -- Yes --> K
  F -- No --> G[Resolve review thread]
  G --> H[Count successful resolutions]
  H --> I[Render count in sticky summary]
Loading

Reviews (1): Last reviewed commit: "ci: auto-resolve Claude review threads o..." | Re-trigger Greptile

}

const ids = helpers.threadsToResolve({
threads, findings, botLogin: process.env.BOT_LOGIN,

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.

🟠 major — GraphQL author.login for the Actions bot has no [bot] suffix, so threadsToResolve matches nothing and no thread is ever resolved

Every comment this workflow posts is authored by the GitHub Actions App, and GraphQL returns App authors as a Bot whose login omits the [bot] suffix that REST's user.login carries — so authored(nodes[0]) !== botLogin (review-comments.cjs:129) rejects github-actions vs github-actions[bot] on every thread and the step logs auto-resolved 0/0 forever. The tests can't catch it because they feed the same literal to both sides. Normalize before comparing, as this repo already does at .github/scripts/pr-triage-classify.js:155 and .github/workflows/release-reminder.yml:36: pass both process.env.BOT_LOGIN and its .replace(/\[bot\]$/, '') form and accept a match against either, plus a test whose author is the un-suffixed github-actions.

}`;

let resolved = 0;
for (const id of ids) {

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.

🟠 major — A finding the reviewer re-reports after its thread was auto-resolved is silently hidden — dedup suppresses the re-post and the thread stays resolved

Push 2: the reviewer misses finding A (~40% recall) and its outdated thread is resolved. Push 3: A is reported again with the same fingerprint, but seenFingerprints (review-comments.cjs:99) is built from listReviewComments, which returns comments inside resolved threads, so buildInlineComments routes A to skipped — nothing is posted and the summary claims "unchanged from an earlier push (already inline above)" for a thread that is collapsed and resolved. Branch protection requiring resolved threads then passes with a re-confirmed finding outstanding. In the same step, also collect threads whose fingerprint IS in the current findings and whose isResolved is true and call unresolveReviewThread on them; the query at line 634 already fetches isResolved and the bodies, so this is a second list out of the same helper ({ resolve, unresolve }) plus one mutation.

const nodes = (t.comments && t.comments.nodes) || [];
const authored = c => (c.author && c.author.login) || '';
// The first comment carries the fingerprint; the rest tell us whether a human joined.
if (!nodes.length || authored(nodes[0]) !== botLogin) continue;

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.

🔵 minor — The nodes[0] bot-author check is subsumed by the human-reply check on the next line and no test separates them

nodes.some(c => authored(c) !== botLogin) on line 130 already covers nodes[0], so line 129's author comparison can never be the check that rejects a thread — only its !nodes.length half matters (it guards the nodes[0].body deref on line 131). The ignores threads that are not ours test at review-comments.test.mjs:87 passes with it deleted, because its human-authored thread carries no hdxr: fingerprint and falls out at FINGERPRINT_RE anyway. Reduce line 129 to if (!nodes.length) continue;, or keep it and add a test whose first comment is human-authored and carries helpers.commentBody(FIXED) — the only case that distinguishes the two guards.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

PR Review

3 finding(s): 🔴 0 critical · 🟠 2 major · 🔵 1 minor

3 posted as inline comment(s) on the changed lines.


Severity is the reviewer's own estimate and is used for ordering, not filtering.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 334 passed • 1 skipped • 1110s

Status Count
✅ Passed 334
❌ Failed 0
⚠️ Flaky 1
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found. The core feature (threadsToResolve + the fail-soft resolve step) is well-structured, correctly gated behind the trusted-tree verification, and thoroughly unit-tested for the pure helper. The items below are recommendations — the worst realistic outcome of any is a cosmetic no-op, a reversible mis-resolve, or documentation drift, not production breakage.

🟡 P2 -- recommended

  • .github/workflows/claude-code-review.yml:667 -- BOT_LOGIN is hardcoded to the REST-style github-actions[bot], but this is the only place the value is compared against a GraphQL author.login, which commonly omits the [bot] suffix; a mismatch makes authored(nodes[0]) !== botLogin always true so the feature silently resolves nothing while every unit test (which bakes in the REST value) stays green.
    • Fix: Confirm the GraphQL author.login returned for token-posted review comments and match both forms (strip a trailing [bot] before comparing), then add a fixture using the GraphQL-shaped login.
    • adversarial, correctness
  • .github/workflows/claude-code-review.yml:641 -- the thread query fetches only comments(first: 50) with no pagination, while the human-reply guard nodes.some(c => authored(c) !== botLogin) only inspects returned nodes, so a human reply past the 50th comment is invisible and an actively-debated thread can be auto-resolved.
    • Fix: Paginate thread comments, or skip resolution when comments.totalCount exceeds the fetched node count so a possibly-unseen human reply is treated conservatively.
    • adversarial, correctness, testing, maintainability
  • .github/scripts/code-review/review-comments.cjs:125 -- isOutdated reflects that the anchored hunk left the diff, not that the code was fixed, so an unrelated edit near a live finding plus a missed reviewer pass (documented ~40% recall) lets threadsToResolve close the thread on a still-unfixed issue, satisfying any "all threads resolved" branch protection on a real defect.
    • Fix: Require stronger evidence before resolving — e.g. only resolve when the outdating commit touched the anchored line range, require multiple consecutive absent passes, or post a non-resolving "appears fixed" reply instead of hard-resolving.
    • security, adversarial
  • agent_docs/code_style.md:18 -- this PR deletes the @source ratchet tracking and its agent_docs/AGENTS.md documentation, but packages/cli/CONTRIBUTING.md still mandates the convention and ~37 files under packages/cli/src/ still carry live @source tags, leaving one authority commanding a convention another no longer mentions and CI no longer tracks.
    • Fix: Either revert the @source tracking/doc removals or finish the removal end-to-end by also updating packages/cli/CONTRIBUTING.md and the live tags.
    • maintainability
🔵 P3 nitpicks (4)
  • .github/scripts/code-review/review-comments.cjs:132 -- fingerprint() is keyed on the finding title, so a reworded title changes the fingerprint and can resolve the old thread while re-posting a duplicate inline comment for the same live issue.
    • Fix: Use a title-independent thread identity for resolution, or gate resolution on the anchored range actually leaving the file rather than fingerprint absence.
  • .github/scripts/code-review/review-comments.cjs:234 -- resolvedNote is computed before the !healthy early return that never emits it, and the "Reported in both branches below" comment overlooks that the unhealthy path drops it.
    • Fix: Move the resolvedNote construction below the !healthy guard or correct the comment.
  • .github/workflows/claude-code-review.yml:652 -- the GraphQL calls have no per-request timeout, so a hung endpoint consumes the 30-minute job budget and the summary and state-marker steps that run after resolve never execute, forcing a full re-review on the next push.
    • Fix: Add a step-level timeout-minutes or wrap github.graphql in an AbortController/Promise.race so a hang becomes a caught error.
  • agent_docs/code_style.md:18 -- the ratchet advisory-pattern removal and the large agent_docs/AGENTS.md DRY/grep-first deletions are unrelated to the auto-resolve feature and bury a significant guidance change inside a CI-plumbing PR.
    • Fix: Split the documentation and ratchet changes into a separate PR so each can be reviewed and reverted independently.

Reviewers (6): correctness, reliability, security, testing, maintainability, adversarial.

Testing gaps:

  • The GraphQL pagination loop, the comments(first: 50) window, and the per-thread mutation error handling live inline in the workflow YAML and are untested; only the pure threadsToResolve helper is covered — extract the page-merge into review-comments.cjs to make it unit-testable.
  • No fixture uses a GraphQL-shaped bot login, so the identity check would pass tests even if it never matches in production.
  • No test covers isOutdated: true with the finding still real (missed pass), the exact condition the design's safeguard must handle.

@teeohhem
teeohhem marked this pull request as draft September 2, 2026 19:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-1 Trivial — auto-merge candidate once CI passes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant