Skip to content

feat(api): adopt uploads.sh links pasted into PR bodies/comments (#701) - #707

Merged
Zach Dunn (zachdunn) merged 1 commit into
mainfrom
claude/701-bot-link-adoption-v2
Aug 18, 2026
Merged

feat(api): adopt uploads.sh links pasted into PR bodies/comments (#701)#707
Zach Dunn (zachdunn) merged 1 commit into
mainfrom
claude/701-bot-link-adoption-v2

Conversation

@zachdunn

Copy link
Copy Markdown
Member

Closes #701.

Builds on #706 (github-attach module), which merged to main while this branch was in progress — this PR targets main directly (rebased after #706 landed), no stacking needed.

What

Webhook "link adoption": when a PR body or issue_comment (created/edited) references an uploads.sh file URL that was pasted in directly — not uploaded via --pr/attach --branch — the webhook now adopts it into that PR/issue's attachment context, so it gets pairing, dedupe, the activity feed, and screenshots-page grouping automatically, same as anything attached through the paved path.

Reuses #706's github-attach module verbatim rather than reimplementing it:

  • resolveAttachSourceKey for URL→key resolution (storage host, embed host, /f/ page — all three, and a URL that doesn't resolve to an object in the scanning repo's own bound workspace just doesn't resolve, so cross-workspace URLs are silently dropped, never claimed).
  • attachExistingObject for the copy + additive gh.repo/gh.kind/gh.number/gh.ref metadata merge (PR feat: per-file metadata — PR/issue context, custom tags, and search #157 preserve contract — the source's own metadata rides along unchanged).

New module: apps/api/src/github-link-adopt.ts.

Copy vs. metadata-only

Went with copy (via attachExistingObject), not metadata-only-in-place tagging. The managed-comment renderer (gatherCommentBody) lists objects by R2 key prefix under gh/<owner>/<name>/..., not by a gh.* metadata query — a metadata-only tag on the original f/ key would be invisible to the comment/screenshots-page renderer without also changing it, which felt like more surface area than this issue needs. Copying (never moving — move defaults to false) means:

  • The originally-pasted URL keeps resolving untouched — nothing here can "rotate" a link someone already has in a thread.
  • Private repos never get their adopted (already-public) f/ source migrated into gh/private/ — only the fresh copy lands there; the source stays exactly where it was.
  • The comment/screenshots-page integration is "for free" because the copy lands exactly where every other attachment already does.

Noise guard

Per the issue: a lone adopted image already visible inline in the PR/comment shouldn't trigger a managed comment that just repeats it. adoptLinkedFiles in github-link-adopt.ts gathers the PR's pre-adoption attachment count first, then only calls postManagedComment when:

  • 2+ links resolve in this pass, or
  • 1 resolves and the target already has other attachments (staged/attached/previously-adopted), or
  • a managed comment already exists for this target (heal it rather than leave it stale).

Config knob

.uploads.yml's adoptLinkedFiles (boolean, default on for bound repos — binding is already an explicit opt-in relationship, same posture as ingestGithubAttachments). Plumbed the same way as ingestBotAttachments, end to end:

  • packages/comment-config/src/index.ts (schema, parse, resolve, AUTO_COMMENT_OPTIONS)
  • test/fixtures/comment-config-golden.json (new parse + resolve cases, adoptLinkedFiles added to every existing resolve-case expectation)
  • packages/uploads/src/comment-config.generated.ts (regenerated via node packages/uploads/scripts/inline-shared.mjs — do not hand-edit)
  • apps/api/src/workspace.ts (githubAdoptLinkedFiles workspace default) + apps/api/src/repo-comment-config.ts mapping
  • apps/web/src/pages/docs/comment-config.astro docs

Idempotency / constraints

  • Re-adopting the same source into the same target overwrites the same destination key in place (attachExistingObject's own contract) — safe across webhook redeliveries and comment-edit rescans.
  • Applies to pull_request opened/edited and issue_comment created/edited. issue_comment deleted is intentionally excluded from adoption (unlike ingest) — there's no ledger to detach from; a deleted comment just has nothing left to scan.
  • Extraction gating in extractWebhookEvent is a cheap payload-only check (hasLinkCandidate — any http(s):// substring); real workspace-scoped resolution happens downstream in adoptLinkedFilesForWebhook, same "compact WebhookEvent, consumer re-fetches current text" shape ingest already uses.

Tests

  • apps/api/src/github-link-adopt.test.ts — URL extraction, workspace-scoped resolution (including cross-workspace drop), additive-metadata copy, all three noise-guard branches, idempotency, adoptLinkedFilesForWebhook's no-op contract (unlinked repo, knob off).
  • apps/api/src/github-webhook-queue.test.tsextractWebhookEvent adopt-gating cases (mirroring the existing ingest cases) and queue dispatch/retry for adopt events.
  • packages/comment-config/src/index.test.ts / packages/uploads/test/comment-config.test.ts — exercise the new golden-fixture cases automatically (shared fixture, no new test code needed there).

pnpm test (305 files / 4530 tests), pnpm typecheck, and pnpm check all pass locally.

Deviations from the brief

  • Local-gh fallback parity (issue mentions "Applies to both the bot path and local-gh fallback parity where feasible") is not implemented here — the CLI's local-gh comment path doesn't currently scan body/comment text for links at all, and doing so felt like a separate CLI-side change with its own test surface. Flagging as an explicit gap rather than silently skipping it.
  • The comment-sync gate only inspects the current pass's pre-adoption attachment count, not a persisted "was this file previously adopted" ledger — adoption has no ledger (unlike ingest's asset-id ledger), so a file adopted once and then referenced again later is treated as a fresh resolve each time, which is fine given the copy's own idempotency but means the noise-guard's "≥2 adopted" count doesn't distinguish new vs. previously-adopted links within one pass.

Scans pull_request opened/edited and issue_comment created/edited text for uploads.sh file URLs (storage host, embed host, /f/ page — resolved via #702's resolveAttachSourceKey) and copies every one that resolves to an object in the repo's own bound workspace into that PR/issue's attachment prefix, reusing #702's attachExistingObject verbatim for the copy + additive gh.* metadata merge (PR #157 preserve contract).

Chose copy over metadata-only-in-place adoption: the managed-comment renderer (gatherCommentBody) lists objects by R2 key prefix, not by gh.* metadata query, so a copy under the PR's attachment prefix is what makes adoption show up in the comment and screenshots-page grouping without also changing the renderer. Copy (never move) leaves the originally-pasted URL resolving untouched, satisfying the no-private-migration constraint.

New adoptLinkedFiles knob in .uploads.yml (default on for bound repos), plumbed through packages/comment-config, the golden fixture, the generated CLI copy, workspace defaults, and the comment-config docs page, mirroring ingestBotAttachments end to end.

Noise guard: a lone adopted link with nothing else to consolidate does not trigger a managed comment on its own — sync only fires on 2+ adopted files this pass, one adopted file mixed with existing attachments, or an already-existing managed comment.
@changeset-bot

changeset-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6d31dae

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@buildinternet/uploads Minor

Not sure what this means? Click here to learn what changesets are.

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

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

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 Plus

Run ID: 971ed118-7f04-4efb-a502-634ea7c753fb

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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.

Bot: adopt uploads.sh file URLs referenced in PR bodies and comments into the managed attachments comment

1 participant