@desplega.ai/agent-swarm 2.4.5 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/{acp-adapter-2wdkktv6.js → acp-adapter-11gp67nr.js} +8 -6
- package/dist/{actions-c40ebcm4.js → actions-8p2kze8j.js} +23 -21
- package/dist/{amp-adapter-wb67m95z.js → amp-adapter-z4qvhacj.js} +2 -2
- package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
- package/dist/{app-353vre8g.js → app-0qp0a2gk.js} +3 -3
- package/dist/{assistant-sj1njh23.js → assistant-jedd3ktm.js} +24 -22
- package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
- package/dist/{boot-reembed-qdkvxeem.js → boot-reembed-4xwq5tg0.js} +1 -1
- package/dist/{boot-reembed-b9c3az3c.js → boot-reembed-n4wq5dd2.js} +1 -1
- package/dist/{boot-scrub-sweep-m3wh8eks.js → boot-scrub-sweep-69h90xcq.js} +4 -4
- package/dist/{boot-seeds-j2273s54.js → boot-seeds-3vn0m9sg.js} +4 -4
- package/dist/{claude-adapter-m7q0a085.js → claude-adapter-221mvad5.js} +8 -6
- package/dist/{claude-managed-adapter-5ptb6tzs.js → claude-managed-adapter-gt6pa35d.js} +2 -2
- package/dist/{claude-managed-setup-smrqk5j5.js → claude-managed-setup-c3hp36ph.js} +4 -4
- package/dist/{claude-sdk-session-38xnz21e.js → claude-sdk-session-8thjsk5e.js} +9012 -8928
- package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
- package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
- package/dist/{cli-dk44dxec.js → cli-0y6f0ckq.js} +1 -1
- package/dist/{cli-kpw04va0.js → cli-0z3vjrpm.js} +1 -1
- package/dist/{cli-94gwyg7m.js → cli-179xcyky.js} +9 -9
- package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
- package/dist/{cli-4zfqgjc5.js → cli-1pr672hz.js} +2 -2
- package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
- package/dist/{cli-43x64hv3.js → cli-36j4jnk3.js} +4 -4
- package/dist/{cli-k8g16gz3.js → cli-3qv8naq8.js} +115 -119
- package/dist/cli-3zwye9sn.js +1193 -0
- package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
- package/dist/{cli-v8fjakwf.js → cli-4w24f7z3.js} +1 -1
- package/dist/{cli-n72rg53p.js → cli-5kzk3472.js} +812 -418
- package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
- package/dist/{cli-j99azg3g.js → cli-5rvg1ddk.js} +5 -5
- package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
- package/dist/{cli-848hxdwj.js → cli-6rg2jn2y.js} +2 -1
- package/dist/{cli-fv5hepyy.js → cli-7bwtc6yx.js} +3 -3
- package/dist/{cli-ms85309d.js → cli-7g40g8hq.js} +11 -11
- package/dist/{cli-xpa64rek.js → cli-92mqenkc.js} +2362 -159
- package/dist/{cli-018r4jq6.js → cli-9ce2xw98.js} +8 -8
- package/dist/{cli-1syn2xy1.js → cli-bgdtv62r.js} +32 -18
- package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
- package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
- package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
- package/dist/{cli-wt0jf9hm.js → cli-fcc5qe3f.js} +553 -193
- package/dist/{cli-ftc1g5tj.js → cli-fes51ya2.js} +3 -3
- package/dist/{cli-3a45zcgm.js → cli-ftmq4nvk.js} +2 -2
- package/dist/{cli-37j04k05.js → cli-fy1jyyym.js} +13 -13
- package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
- package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
- package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
- package/dist/{cli-g35m2xwm.js → cli-j1eghqfm.js} +2 -2
- package/dist/{cli-qfxc84gh.js → cli-jpxsrxcb.js} +1 -1
- package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
- package/dist/{cli-p14m2cnp.js → cli-mbfj11fq.js} +1 -1
- package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
- package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
- package/dist/{cli-f01h25yt.js → cli-n6kmqngw.js} +2 -2
- package/dist/{cli-v0yrfmdh.js → cli-nnvjb17s.js} +3 -3
- package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
- package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
- package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
- package/dist/cli-pkaqk57w.js +128 -0
- package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
- package/dist/{cli-9yqgm9nd.js → cli-r25qpaxt.js} +161 -63
- package/dist/{cli-eww120g8.js → cli-rh5smt8t.js} +18 -4
- package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
- package/dist/{cli-a92r1127.js → cli-s4sntwt4.js} +2 -2
- package/dist/{cli-be2161h1.js → cli-segs7hx9.js} +6 -6
- package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
- package/dist/{cli-z7d29rw9.js → cli-sy8f9705.js} +4 -4
- package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
- package/dist/{cli-st8aw1m6.js → cli-we0ra057.js} +4 -4
- package/dist/{cli-7c9qs530.js → cli-x21dqn18.js} +5 -5
- package/dist/{cli-1sp861s4.js → cli-x6vf4sad.js} +11 -11
- package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
- package/dist/{cli-tw4p90jb.js → cli-xawhpgc9.js} +5 -5
- package/dist/{cli-f0g3pgvf.js → cli-xd4eqtr2.js} +2 -2
- package/dist/{cli-1f3qzw6y.js → cli-xt768r5m.js} +2 -2
- package/dist/{cli-cxxe61f6.js → cli-y4x8ww6s.js} +2 -2
- package/dist/{cli-bnes4z6b.js → cli-y9jvxfhk.js} +3 -3
- package/dist/{cli-qwhy2qma.js → cli-z63a269p.js} +2 -2
- package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
- package/dist/cli.js +29 -27
- package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
- package/dist/{codex-adapter-tn6k5mkg.js → codex-adapter-tcg1q3jk.js} +6 -4
- package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
- package/dist/{codex-session-runner-7vjt7jbw.js → codex-session-runner-xfx4wd9x.js} +7 -5
- package/dist/{commands-t7rz9146.js → commands-ew83nj70.js} +2 -2
- package/dist/{controller-m2jasma5.js → controller-gnq1k1mf.js} +4 -4
- package/dist/{controllers-connect-9ftpex99.js → controllers-connect-tw1dan9r.js} +25 -23
- package/dist/{cursor-adapter-mjv79e4z.js → cursor-adapter-yq5xgfcg.js} +1 -1
- package/dist/{db-6sfrn9g3.js → db-7gbc53fd.js} +14 -2
- package/dist/{db-rn673r06.js → db-t86xrnes.js} +3 -3
- package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
- package/dist/{dispatcher-c155xz0p.js → dispatcher-64bdyhps.js} +22 -20
- package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
- package/dist/{dsh-adapter-3mmszc1x.js → dsh-adapter-tvhc4k2d.js} +1 -1
- package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
- package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
- package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
- package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
- package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
- package/dist/{external-channel-35k6tkv8.js → external-channel-e9nw24zv.js} +2 -2
- package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
- package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
- package/dist/{grok-adapter-tfrq7tg1.js → grok-adapter-v82srhrx.js} +9 -7
- package/dist/handlers-c9ajzhbn.js +41 -0
- package/dist/{heartbeat-dyg4npkp.js → heartbeat-2vqs1319.js} +25 -23
- package/dist/{hook-x26b31bg.js → hook-01nk13wx.js} +7 -5
- package/dist/{hook-drspbwpe.js → hook-jzpheegg.js} +9 -7
- package/dist/{http-pk972xyw.js → http-fc1k6ec8.js} +544 -76
- package/dist/index-07ef7nkm.js +42 -0
- package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
- package/dist/index-7yts8n9s.js +43 -0
- package/dist/index-q3t40ryv.js +46 -0
- package/dist/{index-arytgj5q.js → index-sepja052.js} +1 -1
- package/dist/{index-jwxkkex2.js → index-zdycv3ja.js} +13 -11
- package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
- package/dist/{keepalive-7yrng7px.js → keepalive-5nh2rhmr.js} +4 -4
- package/dist/lead-aayq64jn.js +79 -0
- package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
- package/dist/{maintenance-e6cemdqt.js → maintenance-hjhzdt3k.js} +1 -1
- package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
- package/dist/{node-de6akczr.js → node-jzjj7r9x.js} +1 -1
- package/dist/{oauth-refresh-sweep-yw3xdbcj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
- package/dist/{onboard-0a9y5yrc.js → onboard-1zq3m7db.js} +2 -2
- package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
- package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
- package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
- package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
- package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
- package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
- package/dist/{otel-impl-trjpkfb7.js → otel-impl-h9yyza47.js} +2 -2
- package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
- package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
- package/dist/{pricing-refresh-d2b5my0y.js → pricing-refresh-nevhat8h.js} +3 -3
- package/dist/{rbac-roles-nxh5ahwk.js → rbac-roles-4y6ngvzq.js} +1 -1
- package/dist/{rbac-roles-hey3yv9d.js → rbac-roles-exbg8xb6.js} +2 -2
- package/dist/{render-v2-rr3p2atg.js → render-v2-cfsh13z3.js} +3 -3
- package/dist/{resume-2efkf1s1.js → resume-5s2t0dbs.js} +10 -8
- package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
- package/dist/{session-access-s2mttxpw.js → session-access-dnbyp3cn.js} +6 -4
- package/dist/{setup-vypma5yt.js → setup-1wj1y77j.js} +1 -1
- package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
- package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
- package/dist/{work-objects-rvhckdkp.js → work-objects-7mg6216g.js} +25 -23
- package/dist/worker-nwb789mg.js +79 -0
- package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
- package/openapi.json +785 -5
- package/package.json +9 -9
- package/src/assets/key.ts +17 -0
- package/src/azure-devops/handlers.ts +1 -1
- package/src/be/db/approvals.ts +18 -0
- package/src/be/db/session-moves.ts +164 -0
- package/src/be/db/session-tree.ts +384 -0
- package/src/be/db/tasks/read.ts +40 -4
- package/src/be/db/workflow-runs.ts +96 -0
- package/src/be/db.ts +80 -236
- package/src/be/onboarding.ts +3 -0
- package/src/be/skill-sync.ts +43 -2
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/bitbucket/api.ts +81 -0
- package/src/bitbucket/auth.ts +64 -0
- package/src/bitbucket/handlers.ts +434 -0
- package/src/bitbucket/index.ts +28 -0
- package/src/bitbucket/templates.ts +134 -0
- package/src/bitbucket/types.ts +65 -0
- package/src/commands/runner.ts +55 -3
- package/src/e2b/env.ts +1 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +2 -2
- package/src/extensions/post-bridge.ts +1 -0
- package/src/github/handlers.ts +6 -6
- package/src/gitlab/handlers.ts +5 -5
- package/src/http/assets.ts +14 -2
- package/src/http/core.ts +12 -0
- package/src/http/index.ts +4 -0
- package/src/http/onboarding.ts +1 -0
- package/src/http/sessions.ts +366 -12
- package/src/http/skills.ts +48 -1
- package/src/http/tasks.ts +1 -1
- package/src/http/users.ts +1 -0
- package/src/http/webhooks.ts +141 -0
- package/src/integrations/chat-sdk/adapters.ts +19 -3
- package/src/integrations/chat-sdk/facts.ts +30 -12
- package/src/integrations/chat-sdk/outbound.ts +38 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +5 -53
- package/src/scripts-runtime/types/swarm-extension.d.ts +6 -4
- package/src/slack/handlers.test.ts +158 -0
- package/src/slack/handlers.ts +19 -28
- package/src/tasks/context-key.ts +61 -1
- package/src/tests/bitbucket-auth.test.ts +156 -0
- package/src/tests/bitbucket-handlers.test.ts +425 -0
- package/src/tests/bitbucket-webhook.test.ts +220 -0
- package/src/tests/chat-sdk-inbound.test.ts +39 -0
- package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
- package/src/tests/context-key.test.ts +50 -0
- package/src/tests/core-auth.test.ts +5 -0
- package/src/tests/db-task-read-contract.test.ts +7 -0
- package/src/tests/db-task-write-contract.test.ts +6 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
- package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
- package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
- package/src/tests/generate-mcp-docs.test.ts +101 -0
- package/src/tests/gitlab-handlers.test.ts +3 -3
- package/src/tests/gitlab-vcs-db.test.ts +4 -4
- package/src/tests/integration-disable-flags.test.ts +36 -0
- package/src/tests/mcp-tools.test.ts +1 -1
- package/src/tests/onboarding.test.ts +12 -0
- package/src/tests/pi-codemode.test.ts +8 -2
- package/src/tests/preload-env.test.ts +57 -0
- package/src/tests/preload.ts +21 -0
- package/src/tests/prompt-template-remaining.test.ts +38 -0
- package/src/tests/run-root-coverage.test.ts +39 -0
- package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
- package/src/tests/runner-skills-refresh.test.ts +58 -0
- package/src/tests/secret-scrubber.test.ts +15 -0
- package/src/tests/sessions-move.test.ts +576 -0
- package/src/tests/sessions-tree.test.ts +493 -0
- package/src/tests/skill-files-http.test.ts +50 -0
- package/src/tests/skills-bundle.test.ts +99 -0
- package/src/tests/swarm-config-export-to-env.test.ts +2 -1
- package/src/tests/vcs-bitbucket.test.ts +91 -0
- package/src/tests/vcs-provider-isolation.test.ts +123 -0
- package/src/tests/vcs-provider.test.ts +16 -1
- package/src/tests/vcs-tracking.test.ts +3 -3
- package/src/tests/workflow-foreach-bodies.test.ts +637 -0
- package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
- package/src/tests/workflow-foreach-retry.test.ts +537 -0
- package/src/tests/workflow-foreach.test.ts +44 -23
- package/src/tests/workflow-step-claims.test.ts +6 -6
- package/src/tests/workflow-tla-races.test.ts +2 -2
- package/src/tools/script-delete.ts +1 -1
- package/src/tools/workflows/create-workflow.ts +5 -3
- package/src/types.ts +3 -2
- package/src/utils/config-env-export.ts +1 -0
- package/src/utils/secret-scrubber.ts +4 -0
- package/src/utils/skills-refresh.ts +83 -43
- package/src/vcs/bitbucket.ts +58 -0
- package/src/vcs/index.ts +2 -0
- package/src/vcs/types.ts +2 -2
- package/src/workflows/approval-resolution.ts +4 -1
- package/src/workflows/checkpoint.ts +9 -1
- package/src/workflows/constants.ts +3 -0
- package/src/workflows/definition.ts +48 -17
- package/src/workflows/engine.ts +25 -15
- package/src/workflows/executors/foreach.ts +45 -99
- package/src/workflows/foreach-body.ts +146 -0
- package/src/workflows/foreach-driver.ts +552 -0
- package/src/workflows/foreach-join.ts +206 -7
- package/src/workflows/node-timeout.ts +21 -0
- package/src/workflows/recovery.ts +68 -3
- package/src/workflows/resume.ts +221 -43
- package/src/workflows/retry-poller.ts +39 -24
- package/src/workflows/step-input.ts +59 -0
- package/src/workflows/task-step-routing.ts +114 -56
- package/templates/skills/slack-interaction/content.md +5 -0
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-9hstj09h.js +0 -2657
- package/dist/cli-yw91rnmw.js +0 -13
- package/dist/handlers-7gw64dxw.js +0 -39
- package/dist/index-bgdyvcnv.js +0 -40
- package/dist/index-gabbcdn2.js +0 -43
- package/dist/index-tfb26zhy.js +0 -41
- package/dist/lead-7e02z839.js +0 -77
- package/dist/worker-5snytpbd.js +0 -77
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitbucket Cloud REST interactions — resolving the bot identity behind the
|
|
3
|
+
* token and posting pull request comments. Uses raw fetch, no SDK.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
7
|
+
import { getBitbucketToken } from "./auth";
|
|
8
|
+
|
|
9
|
+
export const BITBUCKET_API = "https://api.bitbucket.org/2.0";
|
|
10
|
+
|
|
11
|
+
function headers(): Record<string, string> {
|
|
12
|
+
const token = getBitbucketToken();
|
|
13
|
+
if (!token) throw new Error("[Bitbucket] No API token configured");
|
|
14
|
+
return {
|
|
15
|
+
Authorization: `Bearer ${token}`,
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
Accept: "application/json",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let cachedBotAccountId: string | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Atlassian `account_id` of the bot. A picker @mention is stored as
|
|
25
|
+
* `@{<account_id>}` in the comment's raw content, so this is what mention
|
|
26
|
+
* detection matches. Uses BITBUCKET_BOT_ACCOUNT_ID when set, otherwise the
|
|
27
|
+
* user that owns the token (`GET /2.0/user`), cached. Returns null when
|
|
28
|
+
* neither is available; callers then fall back to plain-text `@name` matching.
|
|
29
|
+
*/
|
|
30
|
+
export async function resolveBitbucketBotAccountId(): Promise<string | null> {
|
|
31
|
+
const fromEnv = process.env.BITBUCKET_BOT_ACCOUNT_ID?.trim();
|
|
32
|
+
if (fromEnv) return fromEnv;
|
|
33
|
+
if (cachedBotAccountId) return cachedBotAccountId;
|
|
34
|
+
if (!getBitbucketToken()) return null;
|
|
35
|
+
try {
|
|
36
|
+
// Redirects are refused so the token never follows a response to another host.
|
|
37
|
+
const resp = await fetch(`${BITBUCKET_API}/user`, { headers: headers(), redirect: "error" });
|
|
38
|
+
if (!resp.ok) {
|
|
39
|
+
console.error(`[Bitbucket] GET /user returned ${resp.status}`);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const data = (await resp.json()) as { account_id?: string };
|
|
43
|
+
cachedBotAccountId = data.account_id?.trim() || null;
|
|
44
|
+
return cachedBotAccountId;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(`[Bitbucket] Failed to resolve bot identity: ${scrubSecrets(String(err))}`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Reset the cached bot identity (tests, config reloads). */
|
|
52
|
+
export function resetBitbucketBotCache(): void {
|
|
53
|
+
cachedBotAccountId = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Post a comment on a pull request. `fullName` is `<workspace>/<repo_slug>`. */
|
|
57
|
+
export async function postBitbucketPullRequestComment(
|
|
58
|
+
fullName: string,
|
|
59
|
+
pullRequestId: number,
|
|
60
|
+
raw: string,
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
const [workspace, repoSlug] = fullName.split("/");
|
|
63
|
+
if (!workspace || !repoSlug) throw new Error(`[Bitbucket] Invalid repository: ${fullName}`);
|
|
64
|
+
const url =
|
|
65
|
+
`${BITBUCKET_API}/repositories/${encodeURIComponent(workspace)}/` +
|
|
66
|
+
`${encodeURIComponent(repoSlug)}/pullrequests/${pullRequestId}/comments`;
|
|
67
|
+
const resp = await fetch(url, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: headers(),
|
|
70
|
+
redirect: "error",
|
|
71
|
+
body: JSON.stringify({ content: { raw } }),
|
|
72
|
+
});
|
|
73
|
+
if (!resp.ok) {
|
|
74
|
+
const text = await resp.text().catch(() => "");
|
|
75
|
+
throw new Error(
|
|
76
|
+
scrubSecrets(
|
|
77
|
+
`[Bitbucket] Comment on ${fullName}#${pullRequestId} failed: ${resp.status} ${text}`,
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
2
|
+
import { verifyHmacSignature } from "../workflows/triggers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Bitbucket Cloud Authentication & Webhook Verification
|
|
6
|
+
*
|
|
7
|
+
* Webhooks are signed with the webhook's secret: `X-Hub-Signature` carries
|
|
8
|
+
* `sha256=<hex HMAC-SHA256 of the raw body>`, checked against
|
|
9
|
+
* BITBUCKET_WEBHOOK_SECRET. API calls send BITBUCKET_TOKEN (an Atlassian API
|
|
10
|
+
* token or a workspace access token) as a Bearer token.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
let initialized = false;
|
|
14
|
+
let webhookSecret: string | null = null;
|
|
15
|
+
let apiToken: string | null = null;
|
|
16
|
+
|
|
17
|
+
export function isBitbucketEnabled(): boolean {
|
|
18
|
+
return !!process.env.BITBUCKET_WEBHOOK_SECRET && !isEnvFlagEnabled("BITBUCKET_DISABLE", false);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function initBitbucket(): void {
|
|
22
|
+
if (initialized) return;
|
|
23
|
+
if (!isBitbucketEnabled()) {
|
|
24
|
+
console.log("[Bitbucket] Integration disabled (BITBUCKET_WEBHOOK_SECRET not set)");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
webhookSecret = process.env.BITBUCKET_WEBHOOK_SECRET!;
|
|
29
|
+
apiToken = process.env.BITBUCKET_TOKEN ?? null;
|
|
30
|
+
initialized = true;
|
|
31
|
+
|
|
32
|
+
console.log(`[Bitbucket] Integration initialized (API token: ${apiToken ? "set" : "not set"})`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Verify a Bitbucket webhook delivery from its raw body and `X-Hub-Signature`
|
|
37
|
+
* header. Only the `sha256=<hex>` form Bitbucket sends is accepted.
|
|
38
|
+
*/
|
|
39
|
+
export function verifyBitbucketWebhook(rawBody: string, signatureHeader: string | undefined) {
|
|
40
|
+
if (!webhookSecret) return false;
|
|
41
|
+
if (!signatureHeader?.startsWith("sha256=")) return false;
|
|
42
|
+
return verifyHmacSignature(webhookSecret, rawBody, signatureHeader);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Get the Bitbucket token for API calls. */
|
|
46
|
+
export function getBitbucketToken(): string | null {
|
|
47
|
+
return apiToken;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Bot name for plain-text `@name` mentions. Read on every call: a stored
|
|
52
|
+
* swarm_config value reaches process.env after this module loads, and again
|
|
53
|
+
* on each config reload.
|
|
54
|
+
*/
|
|
55
|
+
export function getBitbucketBotName(): string {
|
|
56
|
+
return process.env.BITBUCKET_BOT_NAME || "agent-swarm-bot";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Reset state for testing and config reloads. */
|
|
60
|
+
export function resetBitbucket(): void {
|
|
61
|
+
initialized = false;
|
|
62
|
+
webhookSecret = null;
|
|
63
|
+
apiToken = null;
|
|
64
|
+
}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitbucket Cloud webhook event handlers.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors src/azure-devops/handlers.ts and src/gitlab/handlers.ts:
|
|
5
|
+
* - Parses webhook payloads (pull request created / commented / merged / declined)
|
|
6
|
+
* - Creates agent tasks via createTaskWithSiblingAwareness()
|
|
7
|
+
* - Deduplicates deliveries (X-Request-UUID) and events via in-memory TTL maps
|
|
8
|
+
* - Detects bot mentions and reviewer requests
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
failTask,
|
|
13
|
+
findActiveTasksByVcs,
|
|
14
|
+
findTaskByVcs,
|
|
15
|
+
getAllAgents,
|
|
16
|
+
incrKv,
|
|
17
|
+
upsertKv,
|
|
18
|
+
} from "../be/db";
|
|
19
|
+
import { renderIdentity, resolveIdentity } from "../be/identity";
|
|
20
|
+
import { findUserByExternalId } from "../be/users";
|
|
21
|
+
import { resolveTemplate } from "../prompts/resolver";
|
|
22
|
+
import { bitbucketContextKey } from "../tasks/context-key";
|
|
23
|
+
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
24
|
+
import { resolveBitbucketBotAccountId } from "./api";
|
|
25
|
+
import { getBitbucketBotName } from "./auth";
|
|
26
|
+
// Side-effect import: registers all Bitbucket event templates in the in-memory registry
|
|
27
|
+
import "./templates";
|
|
28
|
+
import type {
|
|
29
|
+
BitbucketAccount,
|
|
30
|
+
BitbucketPullRequest,
|
|
31
|
+
PullRequestCommentEvent,
|
|
32
|
+
PullRequestEvent,
|
|
33
|
+
} from "./types";
|
|
34
|
+
|
|
35
|
+
type HandlerResult = { created: boolean; taskId?: string };
|
|
36
|
+
|
|
37
|
+
// ── Dedup caches (same pattern as GitHub/GitLab) ──
|
|
38
|
+
const DEDUP_TTL_MS = 60_000;
|
|
39
|
+
const processedEvents = new Map<string, number>();
|
|
40
|
+
const processedDeliveries = new Map<string, number>();
|
|
41
|
+
|
|
42
|
+
function sweep(cache: Map<string, number>, now: number): void {
|
|
43
|
+
for (const [k, ts] of cache) {
|
|
44
|
+
if (now - ts > DEDUP_TTL_MS) cache.delete(k);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Content-key dedup: true when the same event was handled in the last 60s. */
|
|
49
|
+
export function isDuplicate(key: string): boolean {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
sweep(processedEvents, now);
|
|
52
|
+
if (processedEvents.has(key)) return true;
|
|
53
|
+
processedEvents.set(key, now);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Hold the content-key reservation for `key` while `fn` runs. A throw releases
|
|
59
|
+
* it, so Bitbucket's retry of a delivery that failed with a 5xx is not taken
|
|
60
|
+
* for a duplicate. Returns null when the key is already held.
|
|
61
|
+
*/
|
|
62
|
+
async function withDedupReservation(
|
|
63
|
+
key: string,
|
|
64
|
+
fn: () => Promise<HandlerResult>,
|
|
65
|
+
): Promise<HandlerResult | null> {
|
|
66
|
+
if (isDuplicate(key)) return null;
|
|
67
|
+
try {
|
|
68
|
+
return await fn();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
processedEvents.delete(key);
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Delivery dedup on `X-Request-UUID`. Bitbucket retries a delivery (same
|
|
77
|
+
* UUID) when it gets a 5xx or times out, so only a delivery that was acked
|
|
78
|
+
* is recorded — see `markDeliveryProcessed`.
|
|
79
|
+
*/
|
|
80
|
+
export function isDuplicateDelivery(requestUuid: string | undefined): boolean {
|
|
81
|
+
if (!requestUuid) return false;
|
|
82
|
+
sweep(processedDeliveries, Date.now());
|
|
83
|
+
return processedDeliveries.has(requestUuid);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function markDeliveryProcessed(requestUuid: string | undefined): void {
|
|
87
|
+
if (requestUuid) processedDeliveries.set(requestUuid, Date.now());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Reset dedup state (tests). */
|
|
91
|
+
export function resetBitbucketDedup(): void {
|
|
92
|
+
processedEvents.clear();
|
|
93
|
+
processedDeliveries.clear();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Helpers ──
|
|
97
|
+
|
|
98
|
+
export function escapeRegExp(value: string): string {
|
|
99
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Mention patterns: the mention picker stores `@{<account_id>}` in the
|
|
104
|
+
* comment's raw content; a typed mention stays plain `@name`.
|
|
105
|
+
*/
|
|
106
|
+
function mentionPatterns(botAccountId: string | null): RegExp[] {
|
|
107
|
+
// `(?![\w-])` rather than `\b`, so `@agent-swarm-bot-two` is not the bot.
|
|
108
|
+
const patterns = [new RegExp(`@${escapeRegExp(getBitbucketBotName())}(?![\\w-])`, "gi")];
|
|
109
|
+
if (botAccountId) patterns.push(new RegExp(`@\\{${escapeRegExp(botAccountId)}\\}`, "gi"));
|
|
110
|
+
return patterns;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function detectMention(
|
|
114
|
+
text: string | null | undefined,
|
|
115
|
+
botAccountId: string | null,
|
|
116
|
+
): boolean {
|
|
117
|
+
if (!text) return false;
|
|
118
|
+
return mentionPatterns(botAccountId).some((re) => {
|
|
119
|
+
re.lastIndex = 0;
|
|
120
|
+
return re.test(text);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function extractMentionContext(text: string, botAccountId: string | null): string {
|
|
125
|
+
return mentionPatterns(botAccountId)
|
|
126
|
+
.reduce((acc, re) => acc.replace(re, ""), text)
|
|
127
|
+
.trim();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isBot(account: BitbucketAccount | undefined, botAccountId: string | null): boolean {
|
|
131
|
+
return !!botAccountId && account?.account_id === botAccountId;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function pullRequestWebUrl(repo: string, pr: BitbucketPullRequest): string {
|
|
135
|
+
return pr.links?.html?.href ?? `https://bitbucket.org/${repo}/pull-requests/${pr.id}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function contextKeyOf(event: PullRequestEvent): string {
|
|
139
|
+
return bitbucketContextKey({
|
|
140
|
+
repoUuid: event.repository.uuid,
|
|
141
|
+
pullRequestId: event.pullrequest.id,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Payload shape ──
|
|
146
|
+
|
|
147
|
+
/** The pull request event, or null when the payload lacks the fields we read. */
|
|
148
|
+
export function pullRequestEventOf(body: unknown): PullRequestEvent | null {
|
|
149
|
+
const event = body as Partial<PullRequestEvent> | null | undefined;
|
|
150
|
+
const pr = event?.pullrequest;
|
|
151
|
+
const repo = event?.repository;
|
|
152
|
+
if (
|
|
153
|
+
typeof pr?.id !== "number" ||
|
|
154
|
+
typeof pr.title !== "string" ||
|
|
155
|
+
typeof repo?.uuid !== "string" ||
|
|
156
|
+
typeof repo.full_name !== "string" ||
|
|
157
|
+
!event?.actor
|
|
158
|
+
) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return event as PullRequestEvent;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The comment event, or null when the pull request or comment is missing. */
|
|
165
|
+
export function pullRequestCommentEventOf(body: unknown): PullRequestCommentEvent | null {
|
|
166
|
+
const event = pullRequestEventOf(body) as PullRequestCommentEvent | null;
|
|
167
|
+
const comment = event?.comment;
|
|
168
|
+
if (!event || typeof comment?.id !== "number" || typeof comment.content?.raw !== "string") {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return event;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function warnMalformed(eventKey: string) {
|
|
175
|
+
console.warn(`[Bitbucket] Ignoring ${eventKey} event: required payload fields are missing`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function findLeadAgent() {
|
|
179
|
+
const agents = await getAllAgents();
|
|
180
|
+
return (
|
|
181
|
+
agents.find((a) => a.role === "lead" && a.status === "idle") ??
|
|
182
|
+
agents.find((a) => a.role === "lead") ??
|
|
183
|
+
null
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Identity resolution ──
|
|
188
|
+
|
|
189
|
+
const UNMAPPED_NAMESPACE = "integration:unmapped:bitbucket";
|
|
190
|
+
const UNMAPPED_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
191
|
+
|
|
192
|
+
function externalIdOf(account: BitbucketAccount | undefined): string | null {
|
|
193
|
+
return account?.account_id?.trim() || account?.uuid?.trim() || null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resolve a Bitbucket account to a `users.id` via its linked `bitbucket`
|
|
198
|
+
* identity (Atlassian `account_id`). Webhook payloads carry no email, so an
|
|
199
|
+
* unknown sender is recorded as an unmapped tracker entry for operator triage.
|
|
200
|
+
*/
|
|
201
|
+
export async function resolveBitbucketSender(
|
|
202
|
+
account: BitbucketAccount | undefined,
|
|
203
|
+
sampleEventType: string,
|
|
204
|
+
sampleContext: string,
|
|
205
|
+
): Promise<string | undefined> {
|
|
206
|
+
const externalId = externalIdOf(account);
|
|
207
|
+
if (!externalId) return undefined;
|
|
208
|
+
const existing = await findUserByExternalId("bitbucket", externalId);
|
|
209
|
+
if (existing) return existing.id;
|
|
210
|
+
|
|
211
|
+
await upsertKv({
|
|
212
|
+
namespace: UNMAPPED_NAMESPACE,
|
|
213
|
+
key: `${externalId}:meta`,
|
|
214
|
+
value: {
|
|
215
|
+
lastSeenAt: new Date().toISOString(),
|
|
216
|
+
displayName: account?.display_name,
|
|
217
|
+
sampleEventType,
|
|
218
|
+
sampleContext: sampleContext.slice(0, 100),
|
|
219
|
+
},
|
|
220
|
+
valueType: "json",
|
|
221
|
+
expiresAt: Date.now() + UNMAPPED_TTL_MS,
|
|
222
|
+
});
|
|
223
|
+
await incrKv(UNMAPPED_NAMESPACE, `${externalId}:count`, 1);
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Render a Bitbucket account for agent-visible text: the resolved canonical
|
|
229
|
+
* name or the explicit UNKNOWN sentinel.
|
|
230
|
+
*/
|
|
231
|
+
export async function renderBitbucketIdentity(
|
|
232
|
+
account: BitbucketAccount | undefined,
|
|
233
|
+
): Promise<string> {
|
|
234
|
+
return renderIdentity(await resolveIdentity("bitbucket", externalIdOf(account) ?? ""));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function createBitbucketTask(
|
|
238
|
+
text: string,
|
|
239
|
+
event: PullRequestEvent,
|
|
240
|
+
fields: {
|
|
241
|
+
taskType: "bitbucket-pr" | "bitbucket-comment";
|
|
242
|
+
vcsEventType: string;
|
|
243
|
+
author: BitbucketAccount | undefined;
|
|
244
|
+
requestedByUserId: string | undefined;
|
|
245
|
+
vcsCommentId?: number;
|
|
246
|
+
parentTaskId?: string;
|
|
247
|
+
},
|
|
248
|
+
): Promise<HandlerResult> {
|
|
249
|
+
const { pullrequest: pr, repository } = event;
|
|
250
|
+
const lead = await findLeadAgent();
|
|
251
|
+
const task = await createTaskWithSiblingAwareness(
|
|
252
|
+
text,
|
|
253
|
+
{
|
|
254
|
+
agentId: lead?.id ?? null,
|
|
255
|
+
routingReason: lead ? "skill" : undefined,
|
|
256
|
+
routingSource: lead ? "engine_default" : undefined,
|
|
257
|
+
source: "bitbucket",
|
|
258
|
+
vcsProvider: "bitbucket",
|
|
259
|
+
taskType: fields.taskType,
|
|
260
|
+
vcsRepo: repository.full_name,
|
|
261
|
+
vcsEventType: fields.vcsEventType,
|
|
262
|
+
vcsNumber: pr.id,
|
|
263
|
+
vcsCommentId: fields.vcsCommentId,
|
|
264
|
+
vcsAuthor: externalIdOf(fields.author) ?? undefined,
|
|
265
|
+
requestedByUserId: fields.requestedByUserId,
|
|
266
|
+
vcsUrl: pullRequestWebUrl(repository.full_name, pr),
|
|
267
|
+
parentTaskId: fields.parentTaskId,
|
|
268
|
+
contextKey: contextKeyOf(event),
|
|
269
|
+
},
|
|
270
|
+
{ origin: "webhook" },
|
|
271
|
+
);
|
|
272
|
+
return { created: true, taskId: task.id };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── Event Handlers ──
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* `pullrequest:created`: a bot mention in the description, or the bot listed
|
|
279
|
+
* as a reviewer, creates one task.
|
|
280
|
+
*/
|
|
281
|
+
export async function handlePullRequestCreated(body: unknown): Promise<HandlerResult> {
|
|
282
|
+
const event = pullRequestEventOf(body);
|
|
283
|
+
if (!event) {
|
|
284
|
+
warnMalformed("pullrequest:created");
|
|
285
|
+
return { created: false };
|
|
286
|
+
}
|
|
287
|
+
const { pullrequest: pr, repository } = event;
|
|
288
|
+
const repo = repository.full_name;
|
|
289
|
+
const author = pr.author ?? event.actor;
|
|
290
|
+
console.log(`[Bitbucket] PR #${pr.id} created by ${author.display_name} in ${repo}`);
|
|
291
|
+
|
|
292
|
+
const botAccountId = await resolveBitbucketBotAccountId();
|
|
293
|
+
if (isBot(author, botAccountId)) {
|
|
294
|
+
console.log(`[Bitbucket] PR opened by the bot itself, skipping`);
|
|
295
|
+
return { created: false };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const mentioned = detectMention(pr.description, botAccountId);
|
|
299
|
+
const reviewRequested =
|
|
300
|
+
!!botAccountId && (pr.reviewers ?? []).some((r) => r.account_id === botAccountId);
|
|
301
|
+
if (!mentioned && !reviewRequested) {
|
|
302
|
+
console.log(`[Bitbucket] PR opened without bot mention or review request, skipping`);
|
|
303
|
+
return { created: false };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const dedupKey = `bitbucket-pr-${repository.uuid}-${pr.id}-created`;
|
|
307
|
+
const outcome = await withDedupReservation(dedupKey, async () => {
|
|
308
|
+
const requestedByUserId = await resolveBitbucketSender(
|
|
309
|
+
author,
|
|
310
|
+
"pull_request",
|
|
311
|
+
`PR #${pr.id}: ${pr.title}`,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const context = pr.description ? extractMentionContext(pr.description, botAccountId) : "";
|
|
315
|
+
// A mention is the explicit ask; the reviewer template is for a bare review request.
|
|
316
|
+
const templateId = mentioned
|
|
317
|
+
? "bitbucket.pull_request.opened"
|
|
318
|
+
: "bitbucket.pull_request.review_requested";
|
|
319
|
+
const result = resolveTemplate(templateId, {
|
|
320
|
+
pr_id: pr.id,
|
|
321
|
+
pr_title: pr.title,
|
|
322
|
+
repo,
|
|
323
|
+
username: await renderBitbucketIdentity(author),
|
|
324
|
+
source_branch: pr.source?.branch?.name ?? "",
|
|
325
|
+
target_branch: pr.destination?.branch?.name ?? "",
|
|
326
|
+
pr_url: pullRequestWebUrl(repo, pr),
|
|
327
|
+
context_section: context ? `Context: ${context}\n\n` : "",
|
|
328
|
+
});
|
|
329
|
+
if (result.skipped) {
|
|
330
|
+
return { created: false };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return createBitbucketTask(result.text, event, {
|
|
334
|
+
taskType: "bitbucket-pr",
|
|
335
|
+
vcsEventType: "pull_request",
|
|
336
|
+
author,
|
|
337
|
+
requestedByUserId,
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
if (!outcome) console.log(`[Bitbucket] Skipping duplicate PR event`);
|
|
341
|
+
return outcome ?? { created: false };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* `pullrequest:comment_created`: a bot mention in a new PR comment (general or
|
|
346
|
+
* inline) creates a task, linked to any active task for the same PR.
|
|
347
|
+
*/
|
|
348
|
+
export async function handlePullRequestCommentCreated(body: unknown): Promise<HandlerResult> {
|
|
349
|
+
const event = pullRequestCommentEventOf(body);
|
|
350
|
+
if (!event) {
|
|
351
|
+
warnMalformed("pullrequest:comment_created");
|
|
352
|
+
return { created: false };
|
|
353
|
+
}
|
|
354
|
+
const { pullrequest: pr, repository, comment } = event;
|
|
355
|
+
const repo = repository.full_name;
|
|
356
|
+
const author = comment.user ?? event.actor;
|
|
357
|
+
|
|
358
|
+
const botAccountId = await resolveBitbucketBotAccountId();
|
|
359
|
+
if (isBot(author, botAccountId)) {
|
|
360
|
+
return { created: false };
|
|
361
|
+
}
|
|
362
|
+
if (!detectMention(comment.content.raw, botAccountId)) {
|
|
363
|
+
return { created: false };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
console.log(`[Bitbucket] Comment by ${author.display_name} on PR #${pr.id} in ${repo}`);
|
|
367
|
+
|
|
368
|
+
const dedupKey = `bitbucket-comment-${repository.uuid}-${pr.id}-${comment.id}`;
|
|
369
|
+
const outcome = await withDedupReservation(dedupKey, async () => {
|
|
370
|
+
const requestedByUserId = await resolveBitbucketSender(
|
|
371
|
+
author,
|
|
372
|
+
"pull_request_comment",
|
|
373
|
+
comment.content.raw,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const existingTask = await findTaskByVcs(repo, pr.id, "bitbucket");
|
|
377
|
+
const existingTaskNote = existingTask
|
|
378
|
+
? `\n\n_Note: There's an active task (${existingTask.id}) for this PR #${pr.id}._`
|
|
379
|
+
: "";
|
|
380
|
+
|
|
381
|
+
const line = comment.inline?.to ?? comment.inline?.from;
|
|
382
|
+
const locationSection = comment.inline
|
|
383
|
+
? `File: ${comment.inline.path}${line != null ? `:${line}` : ""}\n`
|
|
384
|
+
: "";
|
|
385
|
+
|
|
386
|
+
const result = resolveTemplate("bitbucket.comment.mentioned", {
|
|
387
|
+
pr_id: pr.id,
|
|
388
|
+
pr_title: pr.title,
|
|
389
|
+
username: await renderBitbucketIdentity(author),
|
|
390
|
+
repo,
|
|
391
|
+
pr_url: comment.links?.html?.href ?? pullRequestWebUrl(repo, pr),
|
|
392
|
+
location_section: locationSection,
|
|
393
|
+
context: extractMentionContext(comment.content.raw, botAccountId),
|
|
394
|
+
existing_task_note: existingTaskNote,
|
|
395
|
+
});
|
|
396
|
+
if (result.skipped) {
|
|
397
|
+
return { created: false };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return createBitbucketTask(result.text, event, {
|
|
401
|
+
taskType: "bitbucket-comment",
|
|
402
|
+
vcsEventType: "pull_request_comment",
|
|
403
|
+
author,
|
|
404
|
+
requestedByUserId,
|
|
405
|
+
vcsCommentId: comment.id,
|
|
406
|
+
parentTaskId: existingTask?.id,
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
return outcome ?? { created: false };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* `pullrequest:fulfilled` / `pullrequest:rejected`: cancel every active task
|
|
414
|
+
* for the PR.
|
|
415
|
+
*/
|
|
416
|
+
export async function handlePullRequestClosed(
|
|
417
|
+
body: unknown,
|
|
418
|
+
outcome: "merged" | "declined",
|
|
419
|
+
): Promise<HandlerResult> {
|
|
420
|
+
const event = pullRequestEventOf(body);
|
|
421
|
+
if (!event) {
|
|
422
|
+
warnMalformed(outcome === "merged" ? "pullrequest:fulfilled" : "pullrequest:rejected");
|
|
423
|
+
return { created: false };
|
|
424
|
+
}
|
|
425
|
+
const { pullrequest: pr, repository } = event;
|
|
426
|
+
// Every active task, not only the newest: a comment task is a child of the
|
|
427
|
+
// PR task, and failTask does not cascade up to the parent.
|
|
428
|
+
const reason = outcome === "merged" ? "PR was merged" : "PR was declined";
|
|
429
|
+
for (const task of await findActiveTasksByVcs(repository.full_name, pr.id, "bitbucket")) {
|
|
430
|
+
console.log(`[Bitbucket] Cancelling task ${task.id} — ${reason}`);
|
|
431
|
+
await failTask(task.id, reason);
|
|
432
|
+
}
|
|
433
|
+
return { created: false };
|
|
434
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Bitbucket Cloud Integration
|
|
2
|
+
export {
|
|
3
|
+
postBitbucketPullRequestComment,
|
|
4
|
+
resetBitbucketBotCache,
|
|
5
|
+
resolveBitbucketBotAccountId,
|
|
6
|
+
} from "./api";
|
|
7
|
+
export {
|
|
8
|
+
getBitbucketBotName,
|
|
9
|
+
getBitbucketToken,
|
|
10
|
+
initBitbucket,
|
|
11
|
+
isBitbucketEnabled,
|
|
12
|
+
resetBitbucket,
|
|
13
|
+
verifyBitbucketWebhook,
|
|
14
|
+
} from "./auth";
|
|
15
|
+
export {
|
|
16
|
+
handlePullRequestClosed,
|
|
17
|
+
handlePullRequestCommentCreated,
|
|
18
|
+
handlePullRequestCreated,
|
|
19
|
+
isDuplicateDelivery,
|
|
20
|
+
markDeliveryProcessed,
|
|
21
|
+
pullRequestCommentEventOf,
|
|
22
|
+
pullRequestEventOf,
|
|
23
|
+
} from "./handlers";
|
|
24
|
+
export type {
|
|
25
|
+
BitbucketWebhookEvent,
|
|
26
|
+
PullRequestCommentEvent,
|
|
27
|
+
PullRequestEvent,
|
|
28
|
+
} from "./types";
|