feat(groom): durable dedup/rejection ledger backed by GitHub issue state (BE-3874)#43
Conversation
…ate (BE-3874)
The stateless groom CI run starts fresh every time, so without a durable memory
it would re-file already-filed OR already human-rejected findings on every
scheduled run. Add a dedup ledger keyed on (repo, finding_signature) ->
{filed | rejected | superseded} that uses GitHub issue state itself as the
store — the GitHub-native option that needs no net-new secret and is fully
auditable.
- .github/groom/ledger.py: lists groom-labeled issues (state=all), recovers each
embedded signature marker, and classifies it. Human rejection (close-as-not-
planned or the groom-rejected label) maps to REJECTED and durably suppresses
that signature. Only genuinely-new (unknown) signatures are filed.
- Pure logic (marker round-trip, classification, ledger build, partition) is
split from the one thin gh I/O shell so it is fully unit-tested with no network.
- README documents the load-bearing filing contract (embed the marker + apply
the groom label) and CI test workflow mirrors the sibling test-*.yml.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 54 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughChangesGroom ledger
Sequence Diagram(s)sequenceDiagram
participant GroomCLI
participant GitHubIssues
participant Ledger
GroomCLI->>GitHubIssues: Fetch groom-labeled issues
GitHubIssues-->>GroomCLI: Return paginated issue JSON
GroomCLI->>Ledger: Build statuses and partition findings
Ledger-->>GroomCLI: Return filing and suppression decisions
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/groom/ledger.py:
- Around line 92-99: Update signature_marker and its paired extraction logic in
.github/groom/ledger.py:92-99 to encode normalized opaque signatures with a
delimiter-safe format such as URL-safe base64 before placing them in the HTML
comment, then decode the payload during extraction while preserving round-trip
behavior. Add a test in .github/groom/tests/test_ledger.py:38-58 covering
signatures containing "-->" and preferably newlines to verify extraction returns
the original signature.
- Around line 218-228: The candidate classification loop in ledger.py around
status() must deduplicate signatures within one batch: track signatures already
accepted into to_file, keep the first as a filing candidate, and route later
duplicates to a documented non-filing result without marking them filed. Update
.github/groom/tests/test_ledger.py lines 162-175 with two same-signature
candidates and assert exactly one is returned in to_file.
In @.github/workflows/test-groom-scripts.yml:
- Around line 28-36: Update the Checkout and Set up Python steps to pin
actions/checkout and actions/setup-python to their full commit SHA references,
retaining the required trailing version comments (for example, # v6) after each
SHA instead of using mutable tags.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 86d339ee-6e98-4e26-bf7c-2e10b9f136b0
📒 Files selected for processing (5)
.github/groom/README.md.github/groom/ledger.py.github/groom/tests/test_ledger.py.github/workflows/test-groom-scripts.ymlAGENTS.md
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 10 finding(s).
| Severity | Count |
|---|---|
| 🟠 High | 2 |
| 🟡 Medium | 4 |
| 🟢 Low | 4 |
Panel: 6/8 reviewers contributed findings.
Reviewers that did not contribute: kimi-k2.5:adversarial (empty), kimi-k2.5:edge-case (empty)
Address CodeRabbit + cursor-review panel findings on the dedup/rejection ledger: - Encode the opaque signature as URL-safe base64 in the HTML-comment marker so a signature containing `-->`, newlines, or markdown can no longer close the comment early (which truncated the recovered key and re-filed the finding every run) or inject into the public issue body. Bounded, possessive regex removes the O(n^2) ReDoS the old lazy `(.+?)` DOTALL pattern carried. - Recover the LAST marker, not the first, so a marker-shaped comment planted in an attacker-controlled snippet cannot shadow the authoritative marker the filing step appends last. - Deduplicate repeated signatures within a single candidate batch: the first new signature files, later duplicates are suppressed as `pending` (not falsely labeled `filed`), preventing duplicate issues in one run. - Make `should_file`/`--check` reject blank signatures (route to invalid), matching `partition` instead of reporting `unknown` and filing an un-dedupable issue. - Bound the `gh api` call with a timeout and validate `--repo` against `owner/name` before interpolating it into the URL. - Document that the (not-yet-written) caller workflow must serialize runs with a `concurrency:` group to close the read-then-file TOCTOU. - Pin actions/checkout and actions/setup-python to full commit SHAs per the repo's pin-by-SHA standard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…jection-ledger # Conflicts: # .github/groom/README.md
ELI-5
The "groom" janitor bot files GitHub issues for code cleanups it spots. In CI it
has no memory — every scheduled run it wakes up with a blank brain. So it
would file the same cleanup again next week, and — worse — re-file things a
human already looked at and said "no thanks" to. That's how you get everyone to
mute and disable the bot.
This PR gives it a memory that survives across runs, without any new database
or secret: it just uses the GitHub issues themselves as the memory. Before
filing, it lists the cleanup issues it already opened and skips any it's seen —
including ones a human closed as "won't fix" (those stay suppressed forever).
What
.github/groom/ledger.py— a durable dedup/rejection ledger keyed on(repo, finding_signature) -> {filed | rejected | superseded}, built by readinglive GitHub issue state.
groomissuefiledfiledrejectedgroom-rejectedlabel (open or closed)rejectedgroom-supersededlabelsupersededgroomissue carries itunknownFiling embeds the verifier's opaque signature as an invisible HTML-comment
marker (
<!-- groom-signature: … -->) in the issue body; the next run recoversit. Pure logic (marker round-trip, classification, ledger build, candidate
partition) is separated from the one thin
gh apiI/O shell, so it's fullyunit-tested with no network (
.github/groom/tests/, 29 cases, wired into CI viatest-groom-scripts.yml).CLI the groom workflow calls right before filing:
python3 .github/groom/ledger.py --repo owner/name --candidates findings.json --out decision.json # -> {to_file, suppressed, invalid, ledger_size}; open issues only for to_fileWhy this store (design decision)
The ticket asked to prefer the GitHub-native option that needs no net-new
secret. Issue state wins on all three acceptance points at once: it needs no
separate store, the run's built-in
GITHUB_TOKENalready reads issues (no newsecret), and it's inherently auditable (the record is the issues). It also
captures human rejection for free — a maintainer closing an issue as "not
planned" is the rejection signal, no extra tracking needed. I considered a
committed
.ledger.jsonbut rejected it: it duplicates state that already livesin the issues and adds bot-commit noise/merge churn on every run.
Acceptance criteria
existing
groomissue for a signature makes itknown;should_fileis trueonly for
unknown. (AcceptanceScenarioTest,LedgerDecisionTest.)not_plannedor thegroom-rejectedlabel →rejected; read live every run, so it never expires.gh apiwith the run'sGITHUB_TOKEN.Self-review notes / judgment calls
workflow). The memory only works if the step that opens an issue applies
the
groomlabel and appendssignature_marker(sig)to the body. This isthe correct seam — a ledger can't remember what the filer didn't record — and
it's documented in the module docstring + README. The filing step is Phase 1/2
of BE-3870; this PR is the standalone hard piece it will call.
debt). Deliberate anti-annoyance default matching the acceptance wording
("filed in run N not re-filed in N+1"). If re-detecting regressions is wanted
later, treat
completedasunknown— it's a one-constant change.supersededis a defined hook with no producer in this PR — thegroom-supersededlabel is classified if present, part of the ticket's{filed|rejected|superseded}model; nothing here sets it yet.gh apierrors,fetch_groom_issuesraisesrather than returning an empty ledger — an empty ledger would re-file
everything (the exact spam this prevents).
supported" path; the
invalidbucket is defensive routing of malformed(signature-less) input, surfaced in output — not a user-facing denial. Trigger
does not fire.
changed (Chesterton's Fence clean). AGENTS.md integrity checker still passes.