Skip to content

feat: remove stale review requests (remove_stale_review_requests) - #182

Draft
C-Hipple wants to merge 2 commits into
mainfrom
feat/remove-stale-review-requests
Draft

feat: remove stale review requests (remove_stale_review_requests)#182
C-Hipple wants to merge 2 commits into
mainfrom
feat/remove-stale-review-requests

Conversation

@C-Hipple

@C-Hipple C-Hipple commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Summary / Background

Codeowners Plus only ever added review requests. requestReviews() computed required owners minus already-requested minus already-reviewed and called RequestReviewers; nothing ever ran in the other direction. So when a push changes which files a PR touches, the requests made for the old owner set stay on the PR while the review status comment updates to the new, smaller one — the PR ends up showing more teams as requested reviewers than are actually required.

This adds an opt-in remove_stale_review_requests setting that cleans those up, mirroring the existing request behavior.

How it decides what to remove

Running just before requestReviews(), it:

  1. Reads every reviewer currently requested on the PR — raw, not mapped through the user-reviewer map. That map is built only from current owners, so passing through it would silently drop exactly the stale entries we're looking for.
  2. Subtracts every owner of the current diff, required ∪ optional, taken pre-approval, so an owner who already approved keeps their review request instead of having it pulled out from under them.
  3. If anything remains, checks the issue timeline and keeps only reviewers whose most recent review_requested event was performed by the token user.
  4. DELETE /requested_reviewers for the survivors.

Step 3 is the important one. A blanket "remove everything not required" would also strip reviewers a human added by hand — including, on repos using enforcement.approval, the token owner that a PR author manually requested to satisfy CODEOWNERS. Real timeline from a PR that hit this bug:

review_requested  actor=ffunch    reviewer=mmllc-gh                            2026-08-04T21:16:34Z
review_requested  actor=mmllc-gh  reviewer=team:affiliate-program-experts      2026-08-04T21:17:02Z
review_requested  actor=mmllc-gh  reviewer=team:cb-backend-systems-reviewers   2026-08-04T21:17:02Z
review_requested  actor=mmllc-gh  reviewer=team:creator-demand-gen             2026-08-04T21:17:02Z
review_requested  actor=mmllc-gh  reviewer=team:payouts-team                   2026-08-04T21:17:02Z

Only affiliate-program-experts was still required after the author's update. With this change the three stale teams are removed and mmllc-gh — requested by a human — is left alone.

The timeline is only fetched when step 2 finds at least one candidate, so an ordinary run costs no extra API calls.

Behavior notes

  • Off by default. Nothing changes for existing users until they set remove_stale_review_requests = true.
  • Needs a resolvable token user. Attribution requires GET /user, which 403s for GITHUB_TOKEN. In that case removal is skipped with a warning rather than guessing. Fine for the teams use case, which already requires a PAT, but it means the feature is a no-op on GITHUB_TOKEN-only setups — called out in the README.
  • Skipped in quiet mode, alongside comments and review requests.
  • Removal failures never fail the run. A RemoveReviewers error warns and continues — a cosmetic cleanup shouldn't block a PR's check on a transient API error. (This started out fatal to mirror RequestReviewers; PRism pushed back and it was right — requesting reviews is functionally required for the check, removal isn't.)

Code Changes

  • internal/github/gh.go — three new Client methods:
    • GetRequestedReviewers — raw requested reviewers as owner slugs (@login, @owner/team-slug)
    • GetSelfRequestedReviewers — the subset whose latest request event was performed by the token user, via a paginated timeline walk
    • RemoveReviewersDELETE /requested_reviewers
  • internal/app/app.goremoveStaleReviewRequests, called from processApprovalsAndReviewers with the pre-approval required ∪ optional owner set.
  • internal/config/config.goremove_stale_review_requests (default false).
  • README.md — new "Review Request Removal" section, TOC entry, config docs, quiet-mode note.

Follow-up commit (a1a25aa) from PRism review feedback:

  • GetTokenUser now caches the login, so it costs at most one GET /user per run rather than one per caller.
  • NewClient switched to named struct fields — adding a field to a ten-field positional literal was asking for a silently misassigned value.
  • Fixed the indvidualReviewers typo, including the two pre-existing occurrences in RequestReviewers and splitReviewers.

Tests

  • Policy table for removeStaleReviewRequests: not-enabled, quiet, no candidates, self-requested vs. other-requested, optional owners kept, case-insensitive matching, lookup failure, and both error paths.
  • Pure-function table for the timeline attribution: later-request-wins, review_request_removed, reviewers no longer on the PR, reviewers with no request event, case-insensitive actor.
  • Mock-server tests for the new API calls, plus NoPRError coverage.
  • A wiring test asserting the owner set is required ∪ optional pre-approval.

Mutation-checked three ways — dropping optional owners from the keep set, using the post-approval required set, and removing the opt-in gate each make a test fail.

Coverage badge regenerated (82.6% → 83.4%).

🤖 Generated with Claude Code

Codeowners Plus only ever added review requests, so when a push changed
which files a PR touches, the requests made for the old owner set stayed
on the PR while the review status comment updated to the new, smaller
one.

Add an opt-in `remove_stale_review_requests` setting which drops the
review requests the action made for owners the current diff no longer
involves. The owner set is taken pre-approval and includes optional
reviewers, so an owner who already approved keeps their review request
instead of having it pulled out from under them.

Only requests whose most recent `review_requested` timeline event was
performed by the token user are removed, so reviewers somebody added by
hand survive a push. Telling the two apart needs a resolvable token
user, so removal is skipped with a warning when it cannot be read (e.g.
GITHUB_TOKEN), and in quiet mode. The timeline is only fetched when
there is at least one candidate, so an ordinary run costs no extra API
calls.

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

Copy link
Copy Markdown

Codeowners approval required for this PR:

@C-Hipple
C-Hipple marked this pull request as draft August 12, 2026 14:18
@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds an opt-in cleanup pass for stale review requests, preserving current required and optional owners and attempting to remove only requests attributed to the token user.

  • Adds raw requested-reviewer retrieval, timeline-based request attribution, and reviewer removal APIs.
  • Runs cleanup before requesting currently required reviewers, with quiet-mode and configuration gates.
  • Documents the feature and adds application, configuration, attribution, and API tests.

Confidence Score: 4/5

The PR should not merge until manual review requests made by the token-owning account can no longer be mistaken for Codeowners Plus requests and deleted.

Timeline actor identity records which account performed a request, not whether that account acted interactively or through this application, so the new deletion path can remove a request the feature promises to preserve.

Files Needing Attention: internal/github/gh.go, internal/app/app.go

Important Files Changed

Filename Overview
internal/app/app.go Adds the opt-in stale-request cleanup orchestration and protects required and optional owners before requesting new reviewers.
internal/github/gh.go Adds raw reviewer retrieval, timeline attribution, and deletion; actor-only attribution cannot distinguish automation from manual actions by the token owner.
internal/config/config.go Adds the disabled-by-default remove_stale_review_requests configuration field.
README.md Documents setup, token requirements, preservation semantics, and quiet-mode behavior.
internal/app/app_test.go Covers cleanup policy and wiring but omits manual requests made interactively by the token-owning account.
internal/github/gh_test.go Covers API payloads and timeline attribution but models token-owner events exclusively as action-owned requests.

Sequence Diagram

sequenceDiagram
    participant App as Codeowners Plus
    participant PR as GitHub Pull Request
    participant Timeline as Issue Timeline
    App->>PR: Read currently requested reviewers
    App->>App: Subtract required and optional owners
    alt stale candidates exist
        App->>Timeline: Read review request events
        App->>App: Compare latest actor with token user
        App->>PR: Delete attributed stale requests
    end
    App->>PR: Request missing required reviewers
Loading

Reviews (1): Last reviewed commit: "feat: remove stale review requests (remo..." | Re-trigger Greptile

Comment thread internal/github/gh.go

return f.Filtered(requestedReviewers(pr, owner), func(reviewer codeowners.Slug) bool {
requester, found := requesters[reviewer.Normalized()]
return found && tokenUserSlug.EqualsString(requester)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Token identity misattributes requests

When a human-owned PAT is used and its owner manually requests a reviewer, actor-only attribution classifies that request as one made by Codeowners Plus, causing the new cleanup path to delete the manually maintained request after the reviewer stops owning changed files.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a new feature to automatically remove stale review requests made by Codeowners Plus when a PR's changed files no longer require those owners. This is controlled by a new configuration option remove_stale_review_requests. The changes include implementing the cleanup logic in the application and GitHub client, along with comprehensive unit tests. The feedback suggests a minor improvement in internal/github/gh.go to explicitly return nil instead of err at the end of RemoveReviewers for better readability.

Comment thread internal/github/gh.go
defer func() {
_ = res.Body.Close()
}()
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Returning err at the end of the function when it is already guaranteed to be nil is non-idiomatic and can be confusing to readers. It is much clearer to explicitly return nil to signal successful execution.

Suggested change
return err
return nil

- Downgrade a RemoveReviewers failure to a warning. Removal is an opt-in
  cosmetic cleanup, so a transient GitHub API error should not fail the
  check. This also makes the function consistent: both of its network
  calls now warn and continue, leaving only the nil-PR guard fatal.
- Cache the token user on GHClient so GetTokenUser costs at most one
  GET /user per run. It can now be called twice, from
  processTokenOwnerApproval and GetSelfRequestedReviewers.
- Fix the `indvidualReviewers` typo in RemoveReviewers along with the two
  pre-existing occurrences in RequestReviewers and splitReviewers.

NewClient now uses named struct fields - adding tokenUser to a ten-field
positional literal was asking for a silently misassigned value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant