Skip to content

feat(bitbucket): add Bitbucket adapter (Cloud + Data Center) - #50

Merged
lao merged 8 commits into
mainfrom
worktree-bitbucket-adapter
Aug 6, 2026
Merged

feat(bitbucket): add Bitbucket adapter (Cloud + Data Center)#50
lao merged 8 commits into
mainfrom
worktree-bitbucket-adapter

Conversation

@lao

@lao lao commented Aug 3, 2026

Copy link
Copy Markdown
Owner

What

Adds Bitbucket as a botbooter platform: a consumer writes one bot and runs it on Bitbucket pull-request and issue comments the same way it already runs on GitHub issue_comment and GitLab Note Hook deliveries.

Cloud + Data Center in one package, selected at runtime by Config.BaseURL (empty → Cloud REST 2.0 via ktrysmt/go-bitbucket; a value → Data Center REST 1.0 via plain net/http, no second dependency). This is not the two-flavor WhatsApp import-path split — the flavor divergence (event keys, payload shapes, reply bodies, self-identity) lives behind an unexported flavor interface, so server.go routes on a flavor-computed category and never branches on flavor.

Design

  • Auth: an Atlassian API token (HTTP Basic email:token) or a repository/project/workspace access token (Bearer). App passwords are never offered — removed platform-wide 2026-07-28. Exactly one mode is required.
  • Webhook auth: X-Hub-Signature HMAC-SHA256 over the raw body (same header/algorithm on both flavors), verified constant-time (hmac.Equal). The body is read before auth (GitHub's posture, since the signature covers it).
  • Self / reply-loop guard: keys on actor.uuid on Cloud (account_id is GDPR-unreliable) and actor.slug on Data Center. Config.Self is resolved via GET /2.0/user in Cloud API-token mode and required in access-token mode and on Data Center (that endpoint 401s for access tokens; DC has no whoami).
  • Threading via a comment's parent.id on both flavors → implements core.ThreadedSender.
  • Issue comments are Cloud only (Data Center has no issue tracker); workspace/repo#N is rejected on DC. Comment edits are ignored (separate unhandled key). Reactions are permanently omitted — Bitbucket has none.
  • OnPullRequest / OnPush callbacks route those deliveries on the same endpoint; nil callbacks ack-and-drop. Every authentic delivery acks 200 except the bad-signature 401, and handlers must be idempotent (Bitbucket Cloud retries a timed-out delivery).
  • Channel IDs: workspace/repo!N (PR), workspace/repo#N (Cloud issue), PROJECTKEY/repo!N / ~USERNAME/repo!N (Data Center, incl. personal repos).

The lifecycle (per-connection semaphore snapshot, drain, detached dispatch context, Addr/Path normalization) is ported from internal/gitlab guard-for-guard — intentional duplication per CLAUDE.md, not hoisted into a shared helper.

Verification

  • make all clean: go vet (incl. _examples), golangci-lint v2 (0 issues), go test -race ./....
  • internal/bitbucket coverage 97.2% (≥ the GitLab sibling's 97.0%); public facade 100%.
  • isolation_deps_test proves ktrysmt/go-bitbucket is in the bitbucket build closure and absent from every other platform (and no other SDK leaks into bitbucket).
  • Two independent blind reviews (security + 5-axis): no critical/high/medium. The one real finding — Data Center personal-repo project keys (~USERNAME) were unaddressable — is fixed (~ admitted in validSegment, traversal guarantee preserved).
  • Confirmed against current Atlassian docs that Bitbucket Cloud sends X-Hub-Signature: sha256=… when a webhook secret is configured.

Docs

Full platform-enumeration sweep in BotType iota order (between GitLab and Signal): CLAUDE.md, botbooter package doc, README.md, _docs/platforms.md (new Bitbucket section covering both flavors, both auth modes, the Config.Self requirement and why, the idempotency warning, and the no-issue-tracker / no-reactions omissions). _examples/basic gains a bitbucket case.

Summary by CodeRabbit

  • New Features

    • Added Bitbucket Cloud and Data Center support.
    • Added webhook handling for comments, pull requests, and pushes.
    • Added authentication options, threaded pull-request replies, and optional event callbacks.
    • Added Cloud issue commenting and attachment reporting.
  • Documentation

    • Added setup guidance, configuration examples, and quickstart documentation.
    • Documented limitations, including unavailable reaction events, attachment uploads, and Data Center issue comments.

lao added 3 commits August 3, 2026 19:26
Add the BitbucketBotType iota const between GitLab and Signal, its
String() case ("bitbucket"), and the root re-export, so the
platform-agnostic layer can identify a Bitbucket bot.
Add a webhook adapter for Bitbucket, serving both Cloud (REST 2.0 via
ktrysmt/go-bitbucket) and Data Center (REST 1.0 via net/http) from one
package, selected by Config.BaseURL. Cloud and Data Center share only the
webhook framing (X-Event-Key routing, X-Hub-Signature HMAC-SHA256 over the
raw body); the divergence in event keys, payload shapes, reply bodies and
self-identity lives behind an unexported flavor interface so server.go
routes on a flavor-computed category and never branches on flavor.

- Replies as pull-request and issue comments; issue comments are Cloud only
  (Data Center has no issue tracker), so a workspace/repo#N channel id is
  rejected on Data Center. Threading is native on both flavors via a
  comment's parent.id, so the adapter implements core.ThreadedSender.
- Auth is an Atlassian API token (HTTP Basic) or a repository/project/
  workspace access token (Bearer); app passwords are never offered. Self
  identity for the reply-loop guard resolves via GET /2.0/user in Cloud
  API-token mode and is required (Config.Self) in access-token mode and on
  Data Center, where no whoami exists.
- OnPullRequest and OnPush callbacks route those deliveries on the same
  endpoint; nil callbacks ack and drop. Every authentic delivery acks 200
  except the bad-signature 401; handlers must be idempotent (Bitbucket
  Cloud retries a timed-out delivery).
- Reactions are permanently omitted: Bitbucket has no comment reactions.

Ports the internal/gitlab lifecycle (per-connection semaphore, drain,
detached dispatch context) guard for guard. Public bitbucket/ facade with
CloudClient/RawEvent/Addr accessors plus import guards; isolation_deps_test
proves ktrysmt is confined to the bitbucket build closure.
Sweep every platform enumeration in BotType iota order (between GitLab and
Signal): CLAUDE.md architecture, botbooter package doc, README (tagline,
features, run list, constructor table, threading, reactions, credentials,
roadmap) and _docs/platforms.md (intro, official-docs list, a full
Bitbucket section covering both flavors and both auth modes, the threaded-
replies table). Add a bitbucket case to _examples/basic.
@botbooter-test

botbooter-test Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

✅ Review posted — 1 finding(s). d9fb6ea1

  • Fetch changed files
  • Build repository context
  • Generate review
  • Post findings

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a3a723dd-8563-4925-8486-b16791212b95

📥 Commits

Reviewing files that changed from the base of the PR and between d9fb6ea and b42d73d.

📒 Files selected for processing (18)
  • README.md
  • _docs/platforms.md
  • cli/imports_test.go
  • discord/imports_test.go
  • github/imports_test.go
  • gitlab/imports_test.go
  • imports_guard_test.go
  • internal/bitbucket/bitbucket.go
  • internal/bitbucket/bitbucket_test.go
  • internal/bitbucket/coverage_test.go
  • internal/bitbucket/message_test.go
  • internal/bitbucket/send.go
  • internal/bitbucket/send_test.go
  • internal/bitbucket/server_test.go
  • signal/imports_test.go
  • slack/imports_test.go
  • teams/imports_test.go
  • telegram/imports_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • internal/bitbucket/send_test.go
  • internal/bitbucket/server_test.go
  • internal/bitbucket/coverage_test.go
  • README.md
  • internal/bitbucket/send.go
  • _docs/platforms.md
  • internal/bitbucket/bitbucket.go

📝 Walkthrough

Walkthrough

The PR adds Bitbucket Cloud and Data Center support, including configuration, webhook verification, event parsing, comment delivery, threading, callbacks, lifecycle management, public wrappers, tests, examples, and documentation.

Changes

Bitbucket adapter

Layer / File(s) Summary
Platform contract and public wiring
internal/bitbucket/..., bitbucket/..., internal/core/..., go.mod, isolation_deps_test.go
Adds Bitbucket configuration, authentication, platform registration, public wrappers, accessors, attachment behavior, dependency isolation, and validation.
Webhook message conversion
internal/bitbucket/message.go, internal/bitbucket/message_test.go
Parses Cloud and Data Center events into normalized messages and typed raw events.
Comment delivery and threading
internal/bitbucket/send.go, internal/bitbucket/send_test.go, internal/bitbucket/coverage_test.go
Adds Cloud and Data Center comment delivery, threaded replies, channel validation, authentication, identity resolution, and error handling.
Webhook intake and lifecycle
internal/bitbucket/server.go, internal/bitbucket/server_test.go, internal/bitbucket/coverage_test.go
Adds HMAC verification, bounded intake and dispatch, callbacks, self-event filtering, serving, shutdown, cancellation, and reconnect handling.
Documentation and example integration
README.md, _docs/platforms.md, CLAUDE.md, _examples/..., botbooter.go, */imports_test.go
Documents Bitbucket setup, behavior, limitations, environment variables, examples, package support, and SDK isolation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Bitbucket
  participant WebhookServer
  participant MessageParser
  participant Callback
  Bitbucket->>WebhookServer: Send signed webhook
  WebhookServer->>WebhookServer: Validate signature and limits
  WebhookServer->>MessageParser: Parse event
  MessageParser-->>WebhookServer: Return normalized event
  WebhookServer->>Callback: Dispatch configured callback
Loading

Possibly related PRs

  • lao/botbooter#25: Adds a comparable platform adapter with wrappers, webhook handling, documentation, examples, and tests.
  • lao/botbooter#46: Uses the same adapter registration, webhook, SDK isolation, and documentation patterns.
  • lao/botbooter#9: Provides the normalized-message, raw-payload, and adapter-accessor architecture extended here.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 49.49% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding a Bitbucket adapter for Cloud and Data Center.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-bitbucket-adapter

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.

@botbooter-test botbooter-test Bot left a comment

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.

🗂️ Old review — superseded

Verdict: comment · Grade: 8/10

To reach a higher grade: Add ktrysmt/go-bitbucket to the root imports_guard_test.go and the sibling */imports_test.go banned lists, matching how go-github and gitlab client-go were rolled into every guard.

📝 Summary of changes

Adds a Bitbucket adapter (Cloud + Data Center in one package, selected at runtime by Config.BaseURL) mirroring the GitHub/GitLab webhook siblings: HMAC-SHA256 X-Hub-Signature verification over the raw body, per-connection dispatch/read semaphores, drain-on-shutdown, detached dispatch context, core.ThreadedSender via parent.id, and a flavor interface that keeps Cloud/DC divergence out of the lifecycle code. Auth is API-token (Basic) or access-token (Bearer), with Config.Self required where whoami is unavailable. The change is well-structured, thoroughly tested (signature rejections, self-drop, cap/semaphore shedding, callbacks, reconnect, drain deadline, channel-id hardening including ~USERNAME DC personal repos), and the docs/enumeration sweep across CLAUDE.md, README.md, _docs/platforms.md, and botbooter.go is complete and in iota order.

I reviewed the signature/auth path, self-resolution lifecycle, channel-id parsing, threading precedence, and the concurrency of the semaphore/inflight/drain machinery; all are consistent with the established sibling adapters and I found no correctness, security, or concurrency defect. The documented ktrysmt-context and Cloud-whoami goroutine ceilings are acceptable and clearly annotated.

The one gap is a consistency issue in the direct-import test guards: unlike every prior platform SDK, ktrysmt/go-bitbucket was not added to the sibling */imports_test.go banned lists nor to the root imports_guard_test.go. Actual isolation is still proven transitively by isolation_deps_test.go, so this is low severity, but it leaves the fast direct-import guards non-exhaustive for the new SDK.

🤖 AI prompt to fix all 1 finding(s) (review before running)
Fix 1 issue(s) found during code review of lao/botbooter (PR #50).

--- Issue 1 ---
File: isolation_deps_test.go:77  (side RIGHT)
Severity: low
Issue: New ktrysmt SDK added to transitive guard but not to the direct-import guards

The transitive `isolation_deps_test.go` rows are correctly updated to require `ktrysmt/go-bitbucket` present in the bitbucket closure and absent everywhere else. However, the direct-import guards were not correspondingly updated: the root `imports_guard_test.go` banned list and every sibling `*/imports_test.go` (slack, discord, telegram, teams, github, gitlab, signal, cli, whatsapp) still omit `ktrysmt/go-bitbucket`.

This breaks the established convention — when GitHub and GitLab landed, `google/go-github`, `bradleyfalzon/ghinstallation`, and `gitlab-org/api/client-go` were added to every sibling guard (see `slack/imports_test.go`, which bans all three). The direct guards are the fast first line of defense (no toolchain needed), and `goListDeps` skips the transitive test entirely when `go` is not on PATH, so a stray `ktrysmt` import into, e.g., the SDK-free root `botbooter.go` could go uncaught in that environment.

Suggestion: add `"ktrysmt/go-bitbucket"` (or the `github.com/ktrysmt/go-bitbucket` substring) to the banned list in root `imports_guard_test.go` and in each non-bitbucket `*/imports_test.go`, mirroring the treatment of the other platform SDKs.

Apply minimal, correct fixes that resolve these issues. Match the surrounding code's existing style and conventions, and do not change unrelated behavior.

Reviewed by prbooter. Comment /recheck to re-run, /security-review for a security audit, /auto-fix to have me attempt the fixes, or /clean to remove my comments.

Comment thread isolation_deps_test.go
// confined to the bitbucket closure and never leak into another package
// (Data Center replies use plain net/http, so even a Bitbucket consumer's
// binary carries it only through the Cloud path).
ktrysmtSDK = "github.com/ktrysmt/go-bitbucket"

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.

[low] New ktrysmt SDK added to transitive guard but not to the direct-import guards

The transitive isolation_deps_test.go rows are correctly updated to require ktrysmt/go-bitbucket present in the bitbucket closure and absent everywhere else. However, the direct-import guards were not correspondingly updated: the root imports_guard_test.go banned list and every sibling */imports_test.go (slack, discord, telegram, teams, github, gitlab, signal, cli, whatsapp) still omit ktrysmt/go-bitbucket.

This breaks the established convention — when GitHub and GitLab landed, google/go-github, bradleyfalzon/ghinstallation, and gitlab-org/api/client-go were added to every sibling guard (see slack/imports_test.go, which bans all three). The direct guards are the fast first line of defense (no toolchain needed), and goListDeps skips the transitive test entirely when go is not on PATH, so a stray ktrysmt import into, e.g., the SDK-free root botbooter.go could go uncaught in that environment.

Suggestion: add "ktrysmt/go-bitbucket" (or the github.com/ktrysmt/go-bitbucket substring) to the banned list in root imports_guard_test.go and in each non-bitbucket */imports_test.go, mirroring the treatment of the other platform SDKs.

🤖 AI prompt to fix (review before running)
Fix an issue found during code review of lao/botbooter (PR #50).

File: isolation_deps_test.go:77  (side RIGHT)
Severity: low
Issue: New ktrysmt SDK added to transitive guard but not to the direct-import guards

The transitive `isolation_deps_test.go` rows are correctly updated to require `ktrysmt/go-bitbucket` present in the bitbucket closure and absent everywhere else. However, the direct-import guards were not correspondingly updated: the root `imports_guard_test.go` banned list and every sibling `*/imports_test.go` (slack, discord, telegram, teams, github, gitlab, signal, cli, whatsapp) still omit `ktrysmt/go-bitbucket`.

This breaks the established convention — when GitHub and GitLab landed, `google/go-github`, `bradleyfalzon/ghinstallation`, and `gitlab-org/api/client-go` were added to every sibling guard (see `slack/imports_test.go`, which bans all three). The direct guards are the fast first line of defense (no toolchain needed), and `goListDeps` skips the transitive test entirely when `go` is not on PATH, so a stray `ktrysmt` import into, e.g., the SDK-free root `botbooter.go` could go uncaught in that environment.

Suggestion: add `"ktrysmt/go-bitbucket"` (or the `github.com/ktrysmt/go-bitbucket` substring) to the banned list in root `imports_guard_test.go` and in each non-bitbucket `*/imports_test.go`, mirroring the treatment of the other platform SDKs.

Apply a minimal, correct fix that resolves this issue. Match the surrounding code's existing style and conventions, and do not change unrelated behavior.

@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: 4

🧹 Nitpick comments (8)
internal/bitbucket/bitbucket_test.go (2)

80-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover both large-request callback paths.

requestByteLimit raises the limit when either OnPullRequest or OnPush is set. This test covers only OnPush, so a regression in the pull-request branch would pass. Add an OnPullRequest case and assert largeRequestBytes. The corresponding implementation is in internal/bitbucket/bitbucket.go Lines 222-227.

🤖 Prompt for 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.

In `@internal/bitbucket/bitbucket_test.go` around lines 80 - 90, Extend
TestRequestByteLimitScales to cover the OnPullRequest callback path in addition
to OnPush. Create an adapter configuration with OnPullRequest set and assert its
maxRequestBytes equals largeRequestBytes, while preserving the existing
small-cap and OnPush assertions.

37-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover Data Center access-token construction.

Add a valid case for Data Center with AccessToken and Self. The current table does not exercise this distinct dataCenter && accessMode path, so a regression in Data Center Bearer authentication could pass unnoticed. The constructor selects these branches in internal/bitbucket/bitbucket.go Lines 275-339.

Suggested test case
 		{name: "DataCenter", cfg: Config{Secret: "s", Addr: ":0", Email: "e", APIToken: "t", BaseURL: "https://bb.example.com", Self: "botuser"}},
+		{name: "DataCenterAccessToken", cfg: Config{Secret: "s", Addr: ":0", AccessToken: "a", BaseURL: "https://bb.example.com", Self: "botuser"}},
🤖 Prompt for 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.

In `@internal/bitbucket/bitbucket_test.go` around lines 37 - 53, Add a valid Data
Center test case to the TestNewValid table using Config with Secret, Addr,
AccessToken, BaseURL, and Self populated, while preserving the existing Cloud
and Data Center cases so the New constructor’s Data Center access-token branch
is exercised.
bitbucket/imports_test.go (1)

13-16: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding the WhatsApp and Teams SDK markers to the banned list.

The banned list omits go.mau.fi/whatsmeow, modernc.org/sqlite, and golang-jwt. The module-level closure test already asserts these are absent from the bitbucket row, so this is a completeness nit for the direct-import guard only.

♻️ Proposed addition
 	asserts.CheckBannedImports(t, ".",
-		[]string{"discordgo", "slack-go/slack", "go-telegram/bot", "google/go-github", "bradleyfalzon/ghinstallation", "gitlab-org/api/client-go"}, "bitbucket")
+		[]string{"discordgo", "slack-go/slack", "go-telegram/bot", "google/go-github", "bradleyfalzon/ghinstallation", "gitlab-org/api/client-go", "whatsmeow", "modernc.org/sqlite", "golang-jwt"}, "bitbucket")
🤖 Prompt for 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.

In `@bitbucket/imports_test.go` around lines 13 - 16, Extend the banned import
markers in TestBitbucketImportsNoForeignSDK to include go.mau.fi/whatsmeow,
modernc.org/sqlite, and golang-jwt, matching the module-level closure test’s
exclusions while preserving the existing bitbucket marker.
internal/bitbucket/message_test.go (1)

33-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a Cloud timestamp case with fractional seconds and an explicit offset.

Cloud sends created_on values such as 2026-08-03T10:00:00.123456+00:00. The current fixture uses a whole-second Z form only. A case with fractional seconds and a numeric offset locks in that parseRFC3339 does not silently fall back to the zero time.

🤖 Prompt for 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.

In `@internal/bitbucket/message_test.go` around lines 33 - 64, Extend
TestCloudParseComment with a timestamp case using fractional seconds and an
explicit numeric offset, such as 2026-08-03T10:00:00.123456+00:00, and assert
parseComment preserves the expected instant and fractional precision. Reuse the
existing PullRequest fixture/assertions so the test verifies parseRFC3339
handles this format without falling back to zero time.
internal/bitbucket/send.go (1)

186-196: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider draining the response body on the success path.

The success path closes the body without reading it. An undrained body prevents connection reuse for the next comment call. A short io.Copy(io.Discard, io.LimitReader(resp.Body, N)) before returning keeps the keep-alive connection usable.

🤖 Prompt for 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.

In `@internal/bitbucket/send.go` around lines 186 - 196, The successful response
path in the pull-request comment request should drain a bounded amount of
resp.Body before closing it so the HTTP connection remains reusable. Update the
flow around the existing defer and status check, using io.Copy with io.Discard
and a limited reader before returning nil, while preserving the current non-2xx
snippet handling.
internal/bitbucket/send_test.go (1)

63-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the cap variable and consider guarding capture.

cap shadows the builtin cap inside recordingServer and in every caller. Rename it to rec or got for clarity.

capture fields are written on the server goroutine and read on the test goroutine. The single request/response per test orders these accesses today. If a future test issues concurrent requests, -race will report a data race. A mutex or an accessor makes the helper safe for reuse.

♻️ Proposed rename
-func recordingServer(t *testing.T, status int) (*httptest.Server, *capture) {
-	t.Helper()
-	cap := &capture{}
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		cap.method = r.Method
-		cap.path = r.URL.Path
-		raw, _ := io.ReadAll(r.Body)
-		_ = json.Unmarshal(raw, &cap.body)
+func recordingServer(t *testing.T, status int) (*httptest.Server, *capture) {
+	t.Helper()
+	got := &capture{}
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		got.method = r.Method
+		got.path = r.URL.Path
+		raw, _ := io.ReadAll(r.Body)
+		_ = json.Unmarshal(raw, &got.body)
 		w.WriteHeader(status)
 		_, _ = w.Write([]byte(`{"id":1}`))
 	}))
 	t.Cleanup(srv.Close)
-	return srv, cap
+	return srv, got
 }
🤖 Prompt for 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.

In `@internal/bitbucket/send_test.go` around lines 63 - 76, Update recordingServer
to rename the cap variable and its returned value to a non-shadowing name such
as rec, including all callers. Protect capture field writes and reads with a
mutex or synchronized accessor so the helper remains race-safe when requests are
concurrent.
internal/bitbucket/coverage_test.go (2)

33-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse discardLogger in the sibling test files.

server_test.go repeats slog.New(slog.NewTextHandler(io.Discard, nil)) in discardDeps, newCloudAdapter, and several tests. All files share the bitbucket package, so they can call discardLogger() instead. This removes the duplication.

🤖 Prompt for 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.

In `@internal/bitbucket/coverage_test.go` at line 33, Replace repeated
slog.New(slog.NewTextHandler(io.Discard, nil)) constructions in server_test.go,
including discardDeps, newCloudAdapter, and the affected tests, with calls to
the existing discardLogger helper. Keep the logging behavior unchanged and reuse
the shared helper across the bitbucket package.

103-114: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reduce the handler sleep to keep the test fast.

httptest.Server.Close waits for outstanding handlers. The handler sleeps 500ms, so this test always costs about 500ms even though resolveSelf fails at once. A shorter sleep, or a handler that waits on r.Context().Done(), keeps the assertion and removes the delay.

🤖 Prompt for 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.

In `@internal/bitbucket/coverage_test.go` around lines 103 - 114, Reduce the 500ms
delay in the httptest handler within TestResolveSelfCloudCancelled, preferably
by waiting for r.Context().Done() so the cancelled request exits promptly, while
preserving the existing response and cancellation assertion.
🤖 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 `@_docs/platforms.md`:
- Line 776: Update the direct subsections under the `## Bitbucket` heading,
including `Step 1`, `Step 2`, `Step 3`, and `No response?`, from `####` to `###`
so the heading hierarchy satisfies MD001; apply the same correction to the
additional occurrences identified in the comment.
- Line 5: Update _docs/platforms.md line 5 to distinguish Bitbucket Cloud and
Data Center credential requirements instead of grouping all Bitbucket
deployments under Cloud. Update README.md lines 21 and 101 to state that
pull-request comments are supported by both flavors, while issue comments are
supported only on Bitbucket Cloud.

In `@CLAUDE.md`:
- Line 36: In the GitLab section’s documentation, replace every occurrence of
“noteable” with the correctly spelled “notable,” including compound references
such as noteable type and noteable filter; leave the surrounding behavior and
terminology unchanged.

In `@internal/bitbucket/bitbucket_test.go`:
- Around line 16-35: Update newAdapter to reject AccessToken combined with
either Email or APIToken, not only when both Basic-auth fields are present;
retain the existing mutual-exclusion behavior and add TestNewValidation
regression cases for AccessToken with only Email and only APIToken, setting Self
so authentication validation is reached.

---

Nitpick comments:
In `@bitbucket/imports_test.go`:
- Around line 13-16: Extend the banned import markers in
TestBitbucketImportsNoForeignSDK to include go.mau.fi/whatsmeow,
modernc.org/sqlite, and golang-jwt, matching the module-level closure test’s
exclusions while preserving the existing bitbucket marker.

In `@internal/bitbucket/bitbucket_test.go`:
- Around line 80-90: Extend TestRequestByteLimitScales to cover the
OnPullRequest callback path in addition to OnPush. Create an adapter
configuration with OnPullRequest set and assert its maxRequestBytes equals
largeRequestBytes, while preserving the existing small-cap and OnPush
assertions.
- Around line 37-53: Add a valid Data Center test case to the TestNewValid table
using Config with Secret, Addr, AccessToken, BaseURL, and Self populated, while
preserving the existing Cloud and Data Center cases so the New constructor’s
Data Center access-token branch is exercised.

In `@internal/bitbucket/coverage_test.go`:
- Line 33: Replace repeated slog.New(slog.NewTextHandler(io.Discard, nil))
constructions in server_test.go, including discardDeps, newCloudAdapter, and the
affected tests, with calls to the existing discardLogger helper. Keep the
logging behavior unchanged and reuse the shared helper across the bitbucket
package.
- Around line 103-114: Reduce the 500ms delay in the httptest handler within
TestResolveSelfCloudCancelled, preferably by waiting for r.Context().Done() so
the cancelled request exits promptly, while preserving the existing response and
cancellation assertion.

In `@internal/bitbucket/message_test.go`:
- Around line 33-64: Extend TestCloudParseComment with a timestamp case using
fractional seconds and an explicit numeric offset, such as
2026-08-03T10:00:00.123456+00:00, and assert parseComment preserves the expected
instant and fractional precision. Reuse the existing PullRequest
fixture/assertions so the test verifies parseRFC3339 handles this format without
falling back to zero time.

In `@internal/bitbucket/send_test.go`:
- Around line 63-76: Update recordingServer to rename the cap variable and its
returned value to a non-shadowing name such as rec, including all callers.
Protect capture field writes and reads with a mutex or synchronized accessor so
the helper remains race-safe when requests are concurrent.

In `@internal/bitbucket/send.go`:
- Around line 186-196: The successful response path in the pull-request comment
request should drain a bounded amount of resp.Body before closing it so the HTTP
connection remains reusable. Update the flow around the existing defer and
status check, using io.Copy with io.Discard and a limited reader before
returning nil, while preserving the current non-2xx snippet handling.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3f7d8454-9525-4c59-a07f-a24a2950fa09

📥 Commits

Reviewing files that changed from the base of the PR and between e75c7d8 and d9fb6ea.

⛔ Files ignored due to path filters (2)
  • _examples/go.sum is excluded by !**/*.sum
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (22)
  • CLAUDE.md
  • README.md
  • _docs/platforms.md
  • _examples/basic/platforms.go
  • _examples/go.mod
  • bitbucket/bitbucket.go
  • bitbucket/imports_test.go
  • bitbucket/wrapper_test.go
  • botbooter.go
  • go.mod
  • internal/bitbucket/bitbucket.go
  • internal/bitbucket/bitbucket_test.go
  • internal/bitbucket/coverage_test.go
  • internal/bitbucket/message.go
  • internal/bitbucket/message_test.go
  • internal/bitbucket/send.go
  • internal/bitbucket/send_test.go
  • internal/bitbucket/server.go
  • internal/bitbucket/server_test.go
  • internal/core/core.go
  • internal/core/core_test.go
  • isolation_deps_test.go

Comment thread _docs/platforms.md Outdated
Comment thread _docs/platforms.md
Cloud, or its **user slug** on Data Center — it is how the adapter drops its own
comments to avoid reply loops.

#### Step 1, Create the webhook

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the heading hierarchy under ## Bitbucket.

## Bitbucket is followed directly by #### Step 1, #### Step 2, #### Step 3, and #### No response?. Use ### for these direct subsections, or add an intervening ### parent. The current structure triggers markdownlint MD001.

Also applies to: 792-792, 816-816, 868-868

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 776-776: Heading levels should only increment by one level at a time
Expected: h3; Actual: h4

(MD001, heading-increment)

🤖 Prompt for 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.

In `@_docs/platforms.md` at line 776, Update the direct subsections under the `##
Bitbucket` heading, including `Step 1`, `Step 2`, `Step 3`, and `No response?`,
from `####` to `###` so the heading hierarchy satisfies MD001; apply the same
correction to the additional occurrences identified in the comment.

Source: Linters/SAST tools

Comment thread CLAUDE.md
- **`internal/{cli,slack,discord,telegram,whatsapp/cloud,whatsapp/whatsmeow,teams,github,gitlab,signal}`** — one `core.Adapter` implementation each. Each exposes `New(...)` returning a `*core.Bot` built via `core.New(botType, adapter)`, plus package-level accessors (`slack.Client`, `discord.Session`, …) that recover the concrete adapter from a `*Bot` via `core.AdapterAs[T]` and hand back the raw client. WhatsApp Cloud API, Teams, GitHub and GitLab are webhook adapters: they run their own HTTP server (`Connect` binds a listener; `Disconnect` shuts it down + drains in-flight dispatch) and reply over REST. Teams authenticates every inbound request against the Bot Connector JWKS (its only third-party dep, `golang-jwt/jwt/v5`, used solely for signature/claim verification) and routes replies via an adapter-side `conversationID→serviceUrl` map populated on inbound Activities. GitHub verifies `issue_comment` webhooks and replies via the REST API using `google/go-github`, authenticating with either a PAT or a GitHub App (`bradleyfalzon/ghinstallation`), both under one `Config`. Two optional `Config` callbacks (the whatsmeow-`QRCallback` pattern — GitHub-only events, so no core ingress path) route further webhook deliveries on the same endpoint: `OnPullRequest` (actions opened/reopened/synchronize only, self/bot authors dropped via `isSelfOrBotUser`) and `OnPush` (unfiltered); nil callbacks keep those deliveries acked-and-dropped, and both run on drain-covered dispatch goroutines.
- **`internal/{cli,slack,discord,telegram,whatsapp/cloud,whatsapp/whatsmeow,teams,github,gitlab,bitbucket,signal}`** — one `core.Adapter` implementation each. Each exposes `New(...)` returning a `*core.Bot` built via `core.New(botType, adapter)`, plus package-level accessors (`slack.Client`, `discord.Session`, …) that recover the concrete adapter from a `*Bot` via `core.AdapterAs[T]` and hand back the raw client. WhatsApp Cloud API, Teams, GitHub, GitLab and Bitbucket are webhook adapters: they run their own HTTP server (`Connect` binds a listener; `Disconnect` shuts it down + drains in-flight dispatch) and reply over REST. Teams authenticates every inbound request against the Bot Connector JWKS (its only third-party dep, `golang-jwt/jwt/v5`, used solely for signature/claim verification) and routes replies via an adapter-side `conversationID→serviceUrl` map populated on inbound Activities. GitHub verifies `issue_comment` webhooks and replies via the REST API using `google/go-github`, authenticating with either a PAT or a GitHub App (`bradleyfalzon/ghinstallation`), both under one `Config`. Two optional `Config` callbacks (the whatsmeow-`QRCallback` pattern — GitHub-only events, so no core ingress path) route further webhook deliveries on the same endpoint: `OnPullRequest` (actions opened/reopened/synchronize only, self/bot authors dropped via `isSelfOrBotUser`) and `OnPush` (unfiltered); nil callbacks keep those deliveries acked-and-dropped, and both run on drain-covered dispatch goroutines.

GitLab mirrors GitHub's shape with GitLab-native seams: it authenticates each delivery by comparing the `X-Gitlab-Token` header (constant-time) against `Config.Secret` **before** reading the body — no body HMAC, unlike GitHub — and replies via `gitlab.com/gitlab-org/api/client-go` notes. Auth is a single access-token mode (personal/project/group token, sent as `Private-Token`), so there is no ambiguous-auth error and no ghinstallation; self-identity is resolved via `GET /user` at Connect (fatal on failure) and the bot's own notes are dropped by author id (GitLab note webhooks carry no reliable bot flag, so arbitrary *other* bots are not filtered — the Slack situation). `client-go`'s `ParseWebhook` splits a Note Hook by noteable type into typed `IssueCommentEvent`/`MergeCommentEvent` (commit/snippet notes and system/edited notes are acked-and-dropped); the `Confidential Note Hook` trigger is shaped identically and routes through the same handler, but GitLab picks that trigger when the note is internal **or** its noteable is confidential, so internal notes are dropped on the note's own `object_attributes.internal` flag — GitLab only ships it since **18.6**, client-go's typed events do not expose it, so `internalNote` side-parses the raw body — with the noteable as the second discriminator for a payload that arrives without the flag (on that trigger, a non-confidential issue or any MR, since MRs cannot be confidential, means the note is internal by elimination). A note on a confidential issue dispatches (the plain-note reply inherits the issue's audience); an internal note is acked-and-dropped, since `Send` cannot answer in kind — and it is the one filtered drop that logs (`Debug`). **Below 18.6 the flag is absent**, leaving only the noteable filter, which cannot see past a confidential issue: an internal note there is answered with a plain note, widening its audience. The note's pre-rename `confidential` spelling was never in a note payload at all (the only `confidential` key describes the noteable), so do not "restore" it as a fallback — it would drop ordinary comments on confidential issues. `ChannelID` is GitLab-native `group/project#iid` (issue) or `group/project!iid` (merge request). **Every authentic delivery acks 200** — including dropped, unreadable (over the body cap or truncated) and concurrency-shed ones; the unreadable/unparseable and concurrency-shed ones are logged as warnings, while filtered drops (system/internal/edited/self notes, commit and snippet notes, nil-callback and non-matching events) are silent: GitLab never re-delivers a failed webhook and counts *consecutive* failures (4xx, 5xx, timeouts and other HTTP errors alike; one success resets the count) toward auto-disabling the hook, temporarily first and then permanently — the current thresholds live in `_docs/platforms.md` only, so they are corrected in one place — so a non-200 loses the delivery *and* suppresses later ones. Only the bad-token 401 is non-200, deliberately. **No re-delivery is not exactly-once**, though: GitLab does not retry a failed delivery indefinitely, but the same payload can still arrive twice — a delivery that times out on our side (GitLab gives up after ~10s and records a failure even though the note already dispatched) or a manual *Resend* from the hook's *Recent events* tab. So handlers must be **idempotent**: never assume a delivered note is received exactly once. **This is a real divergence from the GitHub sibling** (which answers 400/503 there) — do not "sync" it back. The parity callbacks are `OnMergeRequest` (actions open/reopen/update, self author dropped) and `OnPush` (unfiltered). Reactions are a deliberate v1 omission (GitLab has a native Emoji webhook, so the correct future design is webhook ingress, not the REST poller GitHub needs — see the Reactions section).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the spelling in the GitLab section.

Replace each noteable occurrence on Line 36 with notable.

🧰 Tools
🪛 LanguageTool

[grammar] ~36-~36: Ensure spelling is correct
Context: ... the flag is absent**, leaving only the noteable filter, which cannot see past a confide...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for 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.

In `@CLAUDE.md` at line 36, In the GitLab section’s documentation, replace every
occurrence of “noteable” with the correctly spelled “notable,” including
compound references such as noteable type and noteable filter; leave the
surrounding behavior and terminology unchanged.

Source: Linters/SAST tools

Comment thread internal/bitbucket/bitbucket_test.go
lao added 5 commits August 4, 2026 09:36
…ield

newAdapter only flagged ambiguous auth when BOTH Email and APIToken were set
alongside AccessToken. Setting an access token plus just one Basic field
(Email or APIToken) silently fell through to access-token mode, ignoring the
stray field. Reject any access token paired with either Basic field, and add
regression cases plus a Data Center access-token valid case and the
OnPullRequest cap-scaling case.
…reusable

The Data Center reply path closed resp.Body on 2xx without reading it, which
prevents the keep-alive connection from being reused. Drain a bounded amount
before returning.
The transitive isolation_deps_test already proves ktrysmt is confined to the
bitbucket closure, but the fast direct-import guards omitted it. Add it to the
root guard and every non-bitbucket platform guard, matching how go-github and
client-go are banned everywhere.
…ra cases

- recordingServer's capture renamed off the 'cap' builtin and guarded by a
  mutex so field access is race-safe.
- Reuse the discardLogger helper across server_test instead of repeating
  slog.New(slog.NewTextHandler(io.Discard, nil)).
- Add a fractional-second + explicit-offset timestamp case (parseRFC3339 must
  preserve sub-second precision), and make the cancelled-probe test block on
  the request context instead of a fixed 500ms sleep.
… support

Pull-request comments work on both flavors; issue comments are Cloud only.
Distinguish Bitbucket's credential requirement (a token against Cloud or a
self-hosted Data Center instance) from the cloud-only webhook platforms.
@lao

lao commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

/loop

@botbooter-test

botbooter-test Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🔁 Review-fix loop started — target 9/10, up to 3 fix round(s).

@botbooter-test

botbooter-test Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

✅ No issues found. b42d73dc

  • Fetch changed files
  • Build repository context
  • Generate review
  • Post findings

@botbooter-test botbooter-test Bot left a comment

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.

Verdict: approve · Grade: 9/10

To reach a higher grade: Nothing blocking; the only residual risk is the external assumption that Bitbucket Cloud emits X-Hub-Signature when a secret is configured, which cannot be verified from the diff.

📝 Summary of changes

Overview

Adds a Bitbucket platform adapter (Cloud + Data Center in one package, selected at runtime by Config.BaseURL), following the established GitHub/GitLab webhook-adapter shape. The flavor divergence (event keys, payload shapes, reply bodies, self-identity) is hidden behind an unexported flavor interface so server.go never branches on flavor — a clean design that keeps the lifecycle code flavor-independent.

The change spans: the internal adapter (internal/bitbucket/{bitbucket,message,send,server}.go + thorough tests), the public wrapper (bitbucket/), a new BotType const + String() case, the isolation-deps and import-guard sweep across every sibling package, and a complete documentation sweep (CLAUDE.md, botbooter.go doc, README.md, _docs/platforms.md, _examples/basic).

Assessment

This is a high-quality, well-scoped addition. The lifecycle scaffolding (per-connection dispatch semaphore snapshotted under the lock, readSem/body-cap pre-auth buffering bound, detached dispatch context, drain-before-cancel, identity-compared ctx-watch teardown) is ported guard-for-guard from the GitLab sibling and correct. Auth-mode validation, Self-required logic for access-token/Data-Center, channel-id parsing with validSegment traversal hardening (including the ~USERNAME personal-repo case), constant-time signature check, self-drop reply-loop guard keyed on actor.uuid/actor.slug, and threading via parent.id are all implemented consistently and matched by tests. The isolation-deps additions correctly assert ktrysmt/go-bitbucket is confined to the bitbucket closure and absent everywhere else, and every sibling import guard bans it.

I could not find a concrete correctness, security, or concurrency defect. One external-fact dependency carries residual risk that I cannot verify from the diff: the whole Cloud path depends on Bitbucket Cloud emitting X-Hub-Signature: sha256=… when a webhook secret is set (the author states this was confirmed against current Atlassian docs). If that assumption were wrong the adapter would 401 every Cloud delivery, but there is no evidence in the code that it is incorrect, so I am not raising it as a finding.

Documentation is thorough and the platform enumeration sweep is complete and in the correct BotType iota order.

✅ No issues found.


Reviewed by prbooter. Comment /recheck to re-run, /security-review for a security audit, /auto-fix to have me attempt the fixes, or /clean to remove my comments.

@botbooter-test

botbooter-test Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🔁 Review-fix loop finished — target 9/10.

  • Reviews run: 1
  • Fix commits pushed: 0
  • Grade trajectory: 9
  • Stopped: target reached

@lao
lao merged commit ed36a4a into main Aug 6, 2026
2 checks passed
@lao
lao deleted the worktree-bitbucket-adapter branch August 6, 2026 07:40
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