Skip to content

fix(comment): dedupe duplicate managed comments in the gh fallback path#489

Merged
Zach Dunn (zachdunn) merged 4 commits into
mainfrom
claude/gh-fallback-dedupe-486
Jul 23, 2026
Merged

fix(comment): dedupe duplicate managed comments in the gh fallback path#489
Zach Dunn (zachdunn) merged 4 commits into
mainfrom
claude/gh-fallback-dedupe-486

Conversation

@zachdunn

@zachdunn Zach Dunn (zachdunn) commented Jul 23, 2026

Copy link
Copy Markdown
Member

Closes #486.

The gap

The duplicate-managed-comment dedupe from #470/#480 lived entirely in the bot path. The local-gh fallback resolved its comment with .find(...) — first hit only — and had zero DELETE calls, so it patched the oldest marker comment and left any duplicate stale forever.

Worse than #480 in one respect: the gh path has no id cache, so there is no 404-drop or TTL to force a re-hunt. A duplicate there never heals on its own.

The fix

Mirror the bot path in findManagedComment / upsertAttachmentsComment (packages/uploads/src/github-gh.ts): collect every comment carrying the workspace's exact namespaced marker, patch the oldest, then best-effort gh api -X DELETE the extras with each delete wrapped so a failure never fails the caller's command.

Preserved unchanged: legacy unnamespaced markers stay adopt-only (they can belong to another workspace, so they are never deleted), the createIfMissing patch-only-when-empty gate, and the existing return shape. A create can't have extras by construction — no marker hit is what led to the create.

Reviewer note: this deletes under your credentials

Worth a deliberate look rather than a skim. This is the first place the CLI deletes a GitHub resource under the invoking human's own gh credentials — every other run("gh", …) in packages/uploads is a read or a create/PATCH, and every other DELETE in the repo runs under the App's bot token.

Assessment, for the record:

  • Data loss: nil. The deleted content is always a rendered attachments comment, fully reconstructible from workspace data.
  • Permissions degrade gracefully. Deleting a comment authored by a teammate needs triage/maintain; a 403 is already swallowed best-effort.
  • What actually changes is attribution — the delete lands in a real person's audit trail rather than uploads-sh[bot]'s.

Deliberately implemented as the straightforward mirror: no prompt, flag, or config toggle to soften this. If the attribution posture is judged wrong, the right fix is a design change, not a knob.

Tests

Four new cases in packages/uploads/test/github-gh.test.ts:

  • duplicates collapsed — oldest patched, extras deleted in order
  • a failed delete still reports updated overall
  • a legacy unnamespaced comment is adopted but never deleted
  • cross-tenant: another workspace's namespaced marker is never deleted

That last one guards a property that rests entirely on the trailing " -->" in the marker — the only reason ws=acme doesn't match ws=acme-2. It is mutation-checked: removing that " -->" from the matcher fails this test and only this test.

Golden fixtures deliberately untouched — only comment-hunting/deletion changed, not rendering.

Full suite: 2905 passed. pnpm typecheck and pnpm lint clean.

Considered: making dedupe App-only

Whether to skip this entirely and declare dedupe a GitHub-App-only feature was raised and rejected. Recorded here and in the source doc comment so it isn't re-litigated:

  • The duplication is pre-existing. This file already reimplements the hunt, the legacy-marker adoption and the create-vs-patch gate against a different transport. Making dedupe App-only wouldn't remove that — it would institutionalize a permanent asymmetry between two implementations meant to behave identically. gh fallback never dedupes duplicate managed comments (and can't self-heal) #486 existed because they drifted; this converges them.
  • The gh path's failure mode is the worst of the three. No id cache means no 404-drop and no TTL, so a duplicate there never heals — strictly worse than the time-bounded bug Duplicate-comment dedupe never runs while the comment-id cache is warm #480 fixed on the bot path.
  • It's 21 lines of implementation (the rest of the diff is comments and tests).

Consequence: treat any behavior change to upsertBotComment as owing a matching change here.

Summary by CodeRabbit

  • Bug Fixes
    • Improved managed attachment comment handling when duplicate comments are created.
    • The oldest matching comment is now updated, while newer duplicates are cleaned up automatically.
    • Cleanup failures no longer interrupt successful attachment updates.
    • Prevented deletion of comments belonging to other workspaces or legacy unnamespaced markers.

Mirrors the bot path's #470/#484 self-healing dedupe: findManagedComment now
collects every comment carrying the exact namespaced marker instead of
stopping at the first hit, and upsertAttachmentsComment patches the oldest
then best-effort `gh api -X DELETE`s the rest, swallowing failures. This
path has no id cache, so unlike the bot path a duplicate here never healed
on its own (issue #486). Legacy unnamespaced marker comments remain
adopt-only and are never deleted.
The dedupe filters on the exact namespaced marker, so a neighbouring
workspace's comment is safe only because of the trailing " -->" in the
marker text. That is load-bearing for cross-tenant safety and the bot
path tests it, so mirror the coverage here: `acme-2` is deliberately a
superstring of `acme`, and only our own duplicate may be deleted.

Mutation-checked: loosening the matcher to drop the trailing " -->"
fails this test and only this test.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (2)
  • coderabbit:review
  • review
🚫 Excluded labels (none allowed) (1)
  • wip

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 815611e4-57d0-42f9-9f73-f5817430b422

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The local gh fallback now finds all exact namespaced marker comments, updates the oldest, and best-effort deletes newer duplicates. Legacy markers remain adopt-only. Tests cover deletion failures, workspace isolation, and legacy-marker preservation.

Changes

Managed comment deduplication

Layer / File(s) Summary
Comment lookup and duplicate cleanup
packages/uploads/src/github-gh.ts, .changeset/dedupe-gh-fallback-comment.md
The fallback selects the oldest exact marker, tracks duplicate matches, patches the selected comment, and best-effort deletes extras.
Deduplication behavior tests
packages/uploads/test/github-gh.test.ts
Tests cover duplicate cleanup, swallowed delete failures, cross-workspace isolation, and legacy-marker safety.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • buildinternet/uploads issue 486: Directly covers the gh fallback deduplication implemented here.
  • buildinternet/uploads issue 480: Also concerns duplicate managed-marker comments and identifies a cache-warm path outside this deduplication logic.

Suggested labels: coderabbit:review

Poem

I’m a rabbit with comments in neat little rows,
The oldest gets patched while each duplicate goes.
If deletion fails, I simply shrug twice—
Legacy markers remain safe and nice.
Hop, hop, the fallback is tidy tonight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: deduplicating managed comments in the gh fallback path.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/gh-fallback-dedupe-486

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 23, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
uploads-auth 44849d7 Commit Preview URL

Branch Preview URL
Jul 23 2026, 09:51 PM

Considered making dedupe an App-only feature and decided against it: the
gh fallback is a supported path, not a stopgap. This file already
reimplements the hunt, the legacy adoption and the create-vs-patch gate
against a different transport, and #486 existed because those drifted --
so the parity is deliberate and future changes to upsertBotComment owe a
matching change here.

Comment-only; also notes the credentials posture at the point of use.
@zachdunn

Copy link
Copy Markdown
Member Author

CodeRabbit (@coderabbitai) review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Actionable comments posted: 1

🤖 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 `@packages/uploads/src/github-gh.ts`:
- Around line 248-250: Update the matching logic in the uploads flow at
packages/uploads/src/github-gh.ts lines 248-250 so ATTACHMENTS_MARKER returns
only the oldest matching comment without an extras collection, while preserving
extras behavior for other markers. Add a default-marker test at
packages/uploads/test/github-gh.test.ts lines 332-353 with multiple legacy
matches and assert that no DELETE request is made.
🪄 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: CHILL

Plan: Pro

Run ID: e1eb96f8-7a07-4fd7-ab35-91f1d916e212

📥 Commits

Reviewing files that changed from the base of the PR and between a6b1ae2 and 9fb48ca.

📒 Files selected for processing (3)
  • .changeset/dedupe-gh-fallback-comment.md
  • packages/uploads/src/github-gh.ts
  • packages/uploads/test/github-gh.test.ts

Comment thread packages/uploads/src/github-gh.ts
In legacy mode (no workspace to namespace with) the "exact" marker IS the
shared legacy one, so hits.slice(1) handed other workspaces' comments to
the delete loop -- contradicting the adopt-only contract the same doc
comment states. The ambiguity that makes a legacy comment undeletable
applies just as much when it is the marker being hunted on.

Fixed in the gh path AND the bot path: the bot path has the identical
shape, and its marker is only slug-safe by construction rather than by
guarantee. Fixing one and not the other would break the parity contract
this PR just documented, on its first exercise.

Both guards mutation-checked: dropping the legacy branch fails the new
test on each side and nothing else.

Reported by CodeRabbit on #489.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 23, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
uploads-api 44849d7 Commit Preview URL

Branch Preview URL
Jul 23 2026, 09:50 PM

@zachdunn
Zach Dunn (zachdunn) merged commit 52da4d8 into main Jul 23, 2026
5 checks passed
@zachdunn
Zach Dunn (zachdunn) deleted the claude/gh-fallback-dedupe-486 branch July 23, 2026 21:57
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.

gh fallback never dedupes duplicate managed comments (and can't self-heal)

1 participant