bitbucket-gemini-action 1.0.11 → 1.0.13

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 CHANGED
@@ -52,6 +52,8 @@ pipelines:
52
52
  - `@gemini what does this function do?`
53
53
  - `@gemini check for security issues`
54
54
 
55
+ > **Note**: `@gemini` 멘션 기능을 사용하려면 [웹훅 설정](#webhook-setup-for-gemini-mentions)이 필요합니다.
56
+
55
57
  ## Configuration
56
58
 
57
59
  ### Environment Variables
@@ -235,6 +237,119 @@ Triggered by providing a `PROMPT` variable. Executes predefined tasks automatica
235
237
  - npx bitbucket-gemini-action
236
238
  ```
237
239
 
240
+ ### 자동 리뷰 + @gemini 멘션 동시 사용
241
+
242
+ 두 기능을 동시에 사용하려면:
243
+
244
+ 1. **자동 리뷰**: `PROMPT` 환경변수로 PR 생성/업데이트 시 자동 실행
245
+ 2. **@gemini 멘션**: 웹훅으로 코멘트 이벤트 수신 시 실행
246
+
247
+ ```yaml
248
+ image: node:20
249
+
250
+ pipelines:
251
+ pull-requests:
252
+ '**':
253
+ - step:
254
+ name: AI Code Review
255
+ script:
256
+ - export PROMPT="Review this PR"
257
+ - export ALLOW_BOTS=true
258
+ - export REVIEW_PRESETS="middle,nextjs,typescript"
259
+ - npx bitbucket-gemini-action@latest
260
+
261
+ custom:
262
+ gemini-comment:
263
+ - step:
264
+ name: Gemini Comment Handler
265
+ script:
266
+ - export ALLOW_BOTS=true
267
+ - npx bitbucket-gemini-action@latest
268
+ ```
269
+
270
+ - `pull-requests` 파이프라인: PR 생성/업데이트 시 자동 리뷰
271
+ - `custom/gemini-comment` 파이프라인: 웹훅에서 코멘트 이벤트 수신 시 `@gemini` 멘션 처리
272
+
273
+ ## Webhook Setup for @gemini Mentions
274
+
275
+ `@gemini` 멘션 기능을 사용하려면 Bitbucket 웹훅과 중간 서버가 필요합니다.
276
+
277
+ ### 왜 중간 서버가 필요한가?
278
+
279
+ Bitbucket Pipelines는 **외부 웹훅 URL로 직접 트리거할 수 없습니다**. 따라서:
280
+
281
+ 1. 웹훅 → 중간 서버 → Bitbucket API로 파이프라인 트리거
282
+
283
+ ### 옵션 1: 외부 서버 사용 (권장)
284
+
285
+ AWS Lambda, Cloudflare Workers, 또는 별도 서버에서 웹훅을 수신하고 Bitbucket API를 호출:
286
+
287
+ ```javascript
288
+ // Cloudflare Worker 예시
289
+ export default {
290
+ async fetch(request, env) {
291
+ const payload = await request.json();
292
+
293
+ // @gemini 멘션 확인
294
+ if (!payload.comment?.content?.raw?.includes('@gemini')) {
295
+ return new Response('No trigger', { status: 200 });
296
+ }
297
+
298
+ // Bitbucket Pipeline API 호출
299
+ const response = await fetch(
300
+ `https://api.bitbucket.org/2.0/repositories/${payload.repository.full_name}/pipelines/`,
301
+ {
302
+ method: 'POST',
303
+ headers: {
304
+ 'Authorization': `Bearer ${env.BITBUCKET_ACCESS_TOKEN}`,
305
+ 'Content-Type': 'application/json',
306
+ },
307
+ body: JSON.stringify({
308
+ target: {
309
+ ref_type: 'branch',
310
+ type: 'pipeline_ref_target',
311
+ ref_name: payload.pullrequest.source.branch.name,
312
+ selector: {
313
+ type: 'custom',
314
+ pattern: 'gemini-comment',
315
+ },
316
+ },
317
+ variables: [
318
+ { key: 'WEBHOOK_PAYLOAD', value: JSON.stringify(payload) },
319
+ { key: 'TRIGGER_EVENT', value: 'pullrequest:comment_created' },
320
+ ],
321
+ }),
322
+ }
323
+ );
324
+
325
+ return new Response('Pipeline triggered', { status: 200 });
326
+ },
327
+ };
328
+ ```
329
+
330
+ ### 옵션 2: Bitbucket Connect 앱 개발
331
+
332
+ 더 깊은 통합이 필요하면 Bitbucket Connect 앱을 개발하세요.
333
+
334
+ ### 웹훅 설정 방법
335
+
336
+ 1. **Repository Settings → Webhooks → Add webhook**
337
+ 2. **Title**: `bitbucket-gemini`
338
+ 3. **URL**: 중간 서버 URL (예: `https://your-worker.workers.dev/webhook`)
339
+ 4. **Triggers 선택**:
340
+ - ✅ Pull request: Comment created
341
+ - ✅ Pull request: Comment updated (선택사항)
342
+ 5. **Save**
343
+
344
+ ### 환경 변수
345
+
346
+ 웹훅 핸들러에서 파이프라인 트리거 시 다음 변수를 전달해야 합니다:
347
+
348
+ | Variable | Description |
349
+ |----------|-------------|
350
+ | `WEBHOOK_PAYLOAD` | 웹훅 페이로드 JSON 문자열 |
351
+ | `TRIGGER_EVENT` | `pullrequest:comment_created` 또는 `pullrequest:comment_updated` |
352
+
238
353
  ## Pipeline Examples
239
354
 
240
355
  ### Basic PR Review
package/dist/cli.js CHANGED
@@ -169,16 +169,21 @@ class BitbucketClient {
169
169
  async getPullRequestCommits(workspace, repoSlug, prId) {
170
170
  return this.paginatedRequest(`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/commits`);
171
171
  }
172
- async createPullRequestComment(workspace, repoSlug, prId, content, inline) {
172
+ async createPullRequestComment(workspace, repoSlug, prId, content, options) {
173
173
  const body = {
174
174
  content: {
175
175
  raw: content
176
176
  }
177
177
  };
178
- if (inline) {
178
+ if (options?.inline) {
179
179
  body.inline = {
180
- to: inline.line,
181
- path: inline.path
180
+ from: options.inline.line,
181
+ path: options.inline.path
182
+ };
183
+ }
184
+ if (options?.parentId) {
185
+ body.parent = {
186
+ id: options.parentId
182
187
  };
183
188
  }
184
189
  return this.request(`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/comments`, {
@@ -2849,19 +2854,21 @@ function detectMode(context, options) {
2849
2854
  reason: `Explicitly specified mode: ${options.mode}`
2850
2855
  };
2851
2856
  }
2857
+ if (context.eventType === "pullrequest:comment_created" || context.eventType === "pullrequest:comment_updated") {
2858
+ const tagTrigger = tagMode.shouldTrigger(context, {
2859
+ triggerPhrase: options.triggerPhrase
2860
+ });
2861
+ if (tagTrigger.shouldTrigger) {
2862
+ return {
2863
+ mode: tagMode,
2864
+ reason: "Trigger phrase detected in comment"
2865
+ };
2866
+ }
2867
+ }
2852
2868
  if (options.prompt) {
2853
2869
  return {
2854
2870
  mode: agentMode,
2855
- reason: "Explicit prompt provided"
2856
- };
2857
- }
2858
- const tagTrigger = tagMode.shouldTrigger(context, {
2859
- triggerPhrase: options.triggerPhrase
2860
- });
2861
- if (tagTrigger.shouldTrigger) {
2862
- return {
2863
- mode: tagMode,
2864
- reason: "Trigger phrase detected in comment"
2871
+ reason: "Explicit prompt provided for auto-review"
2865
2872
  };
2866
2873
  }
2867
2874
  if (context.eventType === "manual" || context.eventType === "schedule") {
@@ -2878,23 +2885,33 @@ function detectMode(context, options) {
2878
2885
 
2879
2886
  // src/bitbucket/validation/trigger.ts
2880
2887
  function validateTrigger(context, options) {
2881
- if (options.prompt) {
2888
+ if (context.eventType === "pullrequest:comment_created" || context.eventType === "pullrequest:comment_updated") {
2889
+ const commentResult = validateCommentTrigger(context.comment, options);
2890
+ if (commentResult.shouldTrigger) {
2891
+ return commentResult;
2892
+ }
2882
2893
  return {
2883
- shouldTrigger: true,
2884
- reason: "Explicit prompt provided",
2885
- triggerType: "automation",
2886
- userMessage: options.prompt
2894
+ shouldTrigger: false,
2895
+ reason: `Comment event but no trigger phrase found`
2887
2896
  };
2888
2897
  }
2889
2898
  if (context.eventType === "manual" || context.eventType === "schedule") {
2890
2899
  return {
2891
2900
  shouldTrigger: true,
2892
2901
  reason: `Triggered by ${context.eventType} event`,
2893
- triggerType: "manual"
2902
+ triggerType: "manual",
2903
+ userMessage: options.prompt
2894
2904
  };
2895
2905
  }
2896
- if (context.eventType === "pullrequest:comment_created" || context.eventType === "pullrequest:comment_updated") {
2897
- return validateCommentTrigger(context.comment, options);
2906
+ if (options.prompt) {
2907
+ if (context.eventType === "pullrequest:created" || context.eventType === "pullrequest:updated" || context.isPR) {
2908
+ return {
2909
+ shouldTrigger: true,
2910
+ reason: "Explicit prompt provided for PR event",
2911
+ triggerType: "automation",
2912
+ userMessage: options.prompt
2913
+ };
2914
+ }
2898
2915
  }
2899
2916
  return {
2900
2917
  shouldTrigger: false,
@@ -7214,6 +7231,7 @@ async function prepare() {
7214
7231
  containsTrigger: true,
7215
7232
  mode: mode.name,
7216
7233
  trackingCommentId: modeResult.trackingCommentId,
7234
+ triggerCommentId: context.comment?.id,
7217
7235
  prompt: triggerResult.userMessage || config.PROMPT || "",
7218
7236
  systemPrompt: modeResult.modeContext.systemPrompt,
7219
7237
  tools: modeResult.modeContext.tools,
@@ -8283,12 +8301,11 @@ async function updateTrackingComment(client, workspace, repoSlug, prId, commentI
8283
8301
  }
8284
8302
  async function createInlineComment(client, workspace, repoSlug, prId, filePath, line, content) {
8285
8303
  return client.createPullRequestComment(workspace, repoSlug, prId, content, {
8286
- path: filePath,
8287
- line
8304
+ inline: { path: filePath, line }
8288
8305
  });
8289
8306
  }
8290
- async function createPRComment(client, workspace, repoSlug, prId, content) {
8291
- return client.createPullRequestComment(workspace, repoSlug, prId, content);
8307
+ async function createPRComment(client, workspace, repoSlug, prId, content, parentId) {
8308
+ return client.createPullRequestComment(workspace, repoSlug, prId, content, parentId ? { parentId } : undefined);
8292
8309
  }
8293
8310
  function formatTrackingComment(content) {
8294
8311
  const statusEmoji = getStatusEmoji(content.status);
@@ -8482,16 +8499,15 @@ async function execute(options) {
8482
8499
  console.log(`✅ Gemini response received (${response.finishReason})`);
8483
8500
  let inlineCommentsCreated = 0;
8484
8501
  if (response.toolCalls && context.entityNumber) {
8485
- inlineCommentsCreated = await processToolCalls(bitbucketClient, context.workspace, context.repoSlug, context.entityNumber, response.toolCalls, opts.trackingCommentId);
8502
+ inlineCommentsCreated = await processToolCalls(bitbucketClient, context.workspace, context.repoSlug, context.entityNumber, response.toolCalls, opts.trackingCommentId, opts.triggerCommentId);
8486
8503
  }
8487
8504
  if (!response.toolCalls?.length && response.text && context.entityNumber) {
8488
- await createPRComment(bitbucketClient, context.workspace, context.repoSlug, context.entityNumber, response.text);
8505
+ await createPRComment(bitbucketClient, context.workspace, context.repoSlug, context.entityNumber, response.text, opts.triggerCommentId);
8489
8506
  }
8490
8507
  if (opts.trackingCommentId && context.entityNumber) {
8491
8508
  await updateTrackingComment(bitbucketClient, context.workspace, context.repoSlug, context.entityNumber, opts.trackingCommentId, {
8492
8509
  status: "completed",
8493
8510
  message: "Review completed successfully.",
8494
- summary: response.text.substring(0, 500),
8495
8511
  inlineCommentsCount: inlineCommentsCreated
8496
8512
  });
8497
8513
  }
@@ -8523,7 +8539,7 @@ async function execute(options) {
8523
8539
  };
8524
8540
  }
8525
8541
  }
8526
- async function processToolCalls(client, workspace, repoSlug, prId, toolCalls, trackingCommentId) {
8542
+ async function processToolCalls(client, workspace, repoSlug, prId, toolCalls, trackingCommentId, triggerCommentId) {
8527
8543
  let inlineCommentsCreated = 0;
8528
8544
  for (const toolCall of toolCalls) {
8529
8545
  console.log(`\uD83D\uDD27 Processing tool call: ${toolCall.name}`);
@@ -8537,7 +8553,7 @@ async function processToolCalls(client, workspace, repoSlug, prId, toolCalls, tr
8537
8553
  }
8538
8554
  case "create_pr_comment": {
8539
8555
  const args = toolCall.args;
8540
- await createPRComment(client, workspace, repoSlug, prId, args.content);
8556
+ await createPRComment(client, workspace, repoSlug, prId, args.content, triggerCommentId);
8541
8557
  break;
8542
8558
  }
8543
8559
  case "update_tracking_comment": {
@@ -8571,7 +8587,8 @@ function loadPrepareOutput() {
8571
8587
  mode: data.mode || "tag",
8572
8588
  prompt: data.prompt || "",
8573
8589
  systemPrompt: data.systemPrompt || "",
8574
- trackingCommentId: data.trackingCommentId
8590
+ trackingCommentId: data.trackingCommentId,
8591
+ triggerCommentId: data.triggerCommentId
8575
8592
  };
8576
8593
  }
8577
8594
 
@@ -1113,16 +1113,21 @@ class BitbucketClient {
1113
1113
  async getPullRequestCommits(workspace, repoSlug, prId) {
1114
1114
  return this.paginatedRequest(`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/commits`);
1115
1115
  }
1116
- async createPullRequestComment(workspace, repoSlug, prId, content, inline) {
1116
+ async createPullRequestComment(workspace, repoSlug, prId, content, options) {
1117
1117
  const body = {
1118
1118
  content: {
1119
1119
  raw: content
1120
1120
  }
1121
1121
  };
1122
- if (inline) {
1122
+ if (options?.inline) {
1123
1123
  body.inline = {
1124
- to: inline.line,
1125
- path: inline.path
1124
+ from: options.inline.line,
1125
+ path: options.inline.path
1126
+ };
1127
+ }
1128
+ if (options?.parentId) {
1129
+ body.parent = {
1130
+ id: options.parentId
1126
1131
  };
1127
1132
  }
1128
1133
  return this.request(`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/comments`, {
@@ -3741,12 +3746,11 @@ async function updateTrackingComment(client, workspace, repoSlug, prId, commentI
3741
3746
  }
3742
3747
  async function createInlineComment(client, workspace, repoSlug, prId, filePath, line, content) {
3743
3748
  return client.createPullRequestComment(workspace, repoSlug, prId, content, {
3744
- path: filePath,
3745
- line
3749
+ inline: { path: filePath, line }
3746
3750
  });
3747
3751
  }
3748
- async function createPRComment(client, workspace, repoSlug, prId, content) {
3749
- return client.createPullRequestComment(workspace, repoSlug, prId, content);
3752
+ async function createPRComment(client, workspace, repoSlug, prId, content, parentId) {
3753
+ return client.createPullRequestComment(workspace, repoSlug, prId, content, parentId ? { parentId } : undefined);
3750
3754
  }
3751
3755
  function formatTrackingComment(content) {
3752
3756
  const statusEmoji = getStatusEmoji(content.status);
@@ -7988,16 +7992,15 @@ async function execute(options) {
7988
7992
  console.log(`✅ Gemini response received (${response.finishReason})`);
7989
7993
  let inlineCommentsCreated = 0;
7990
7994
  if (response.toolCalls && context.entityNumber) {
7991
- inlineCommentsCreated = await processToolCalls(bitbucketClient, context.workspace, context.repoSlug, context.entityNumber, response.toolCalls, opts.trackingCommentId);
7995
+ inlineCommentsCreated = await processToolCalls(bitbucketClient, context.workspace, context.repoSlug, context.entityNumber, response.toolCalls, opts.trackingCommentId, opts.triggerCommentId);
7992
7996
  }
7993
7997
  if (!response.toolCalls?.length && response.text && context.entityNumber) {
7994
- await createPRComment(bitbucketClient, context.workspace, context.repoSlug, context.entityNumber, response.text);
7998
+ await createPRComment(bitbucketClient, context.workspace, context.repoSlug, context.entityNumber, response.text, opts.triggerCommentId);
7995
7999
  }
7996
8000
  if (opts.trackingCommentId && context.entityNumber) {
7997
8001
  await updateTrackingComment(bitbucketClient, context.workspace, context.repoSlug, context.entityNumber, opts.trackingCommentId, {
7998
8002
  status: "completed",
7999
8003
  message: "Review completed successfully.",
8000
- summary: response.text.substring(0, 500),
8001
8004
  inlineCommentsCount: inlineCommentsCreated
8002
8005
  });
8003
8006
  }
@@ -8029,7 +8032,7 @@ async function execute(options) {
8029
8032
  };
8030
8033
  }
8031
8034
  }
8032
- async function processToolCalls(client, workspace, repoSlug, prId, toolCalls, trackingCommentId) {
8035
+ async function processToolCalls(client, workspace, repoSlug, prId, toolCalls, trackingCommentId, triggerCommentId) {
8033
8036
  let inlineCommentsCreated = 0;
8034
8037
  for (const toolCall of toolCalls) {
8035
8038
  console.log(`\uD83D\uDD27 Processing tool call: ${toolCall.name}`);
@@ -8043,7 +8046,7 @@ async function processToolCalls(client, workspace, repoSlug, prId, toolCalls, tr
8043
8046
  }
8044
8047
  case "create_pr_comment": {
8045
8048
  const args = toolCall.args;
8046
- await createPRComment(client, workspace, repoSlug, prId, args.content);
8049
+ await createPRComment(client, workspace, repoSlug, prId, args.content, triggerCommentId);
8047
8050
  break;
8048
8051
  }
8049
8052
  case "update_tracking_comment": {
@@ -8077,7 +8080,8 @@ function loadPrepareOutput() {
8077
8080
  mode: data.mode || "tag",
8078
8081
  prompt: data.prompt || "",
8079
8082
  systemPrompt: data.systemPrompt || "",
8080
- trackingCommentId: data.trackingCommentId
8083
+ trackingCommentId: data.trackingCommentId,
8084
+ triggerCommentId: data.triggerCommentId
8081
8085
  };
8082
8086
  }
8083
8087
  export {
@@ -168,16 +168,21 @@ class BitbucketClient {
168
168
  async getPullRequestCommits(workspace, repoSlug, prId) {
169
169
  return this.paginatedRequest(`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/commits`);
170
170
  }
171
- async createPullRequestComment(workspace, repoSlug, prId, content, inline) {
171
+ async createPullRequestComment(workspace, repoSlug, prId, content, options) {
172
172
  const body = {
173
173
  content: {
174
174
  raw: content
175
175
  }
176
176
  };
177
- if (inline) {
177
+ if (options?.inline) {
178
178
  body.inline = {
179
- to: inline.line,
180
- path: inline.path
179
+ from: options.inline.line,
180
+ path: options.inline.path
181
+ };
182
+ }
183
+ if (options?.parentId) {
184
+ body.parent = {
185
+ id: options.parentId
181
186
  };
182
187
  }
183
188
  return this.request(`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/comments`, {
@@ -2848,19 +2853,21 @@ function detectMode(context, options) {
2848
2853
  reason: `Explicitly specified mode: ${options.mode}`
2849
2854
  };
2850
2855
  }
2856
+ if (context.eventType === "pullrequest:comment_created" || context.eventType === "pullrequest:comment_updated") {
2857
+ const tagTrigger = tagMode.shouldTrigger(context, {
2858
+ triggerPhrase: options.triggerPhrase
2859
+ });
2860
+ if (tagTrigger.shouldTrigger) {
2861
+ return {
2862
+ mode: tagMode,
2863
+ reason: "Trigger phrase detected in comment"
2864
+ };
2865
+ }
2866
+ }
2851
2867
  if (options.prompt) {
2852
2868
  return {
2853
2869
  mode: agentMode,
2854
- reason: "Explicit prompt provided"
2855
- };
2856
- }
2857
- const tagTrigger = tagMode.shouldTrigger(context, {
2858
- triggerPhrase: options.triggerPhrase
2859
- });
2860
- if (tagTrigger.shouldTrigger) {
2861
- return {
2862
- mode: tagMode,
2863
- reason: "Trigger phrase detected in comment"
2870
+ reason: "Explicit prompt provided for auto-review"
2864
2871
  };
2865
2872
  }
2866
2873
  if (context.eventType === "manual" || context.eventType === "schedule") {
@@ -2877,23 +2884,33 @@ function detectMode(context, options) {
2877
2884
 
2878
2885
  // src/bitbucket/validation/trigger.ts
2879
2886
  function validateTrigger(context, options) {
2880
- if (options.prompt) {
2887
+ if (context.eventType === "pullrequest:comment_created" || context.eventType === "pullrequest:comment_updated") {
2888
+ const commentResult = validateCommentTrigger(context.comment, options);
2889
+ if (commentResult.shouldTrigger) {
2890
+ return commentResult;
2891
+ }
2881
2892
  return {
2882
- shouldTrigger: true,
2883
- reason: "Explicit prompt provided",
2884
- triggerType: "automation",
2885
- userMessage: options.prompt
2893
+ shouldTrigger: false,
2894
+ reason: `Comment event but no trigger phrase found`
2886
2895
  };
2887
2896
  }
2888
2897
  if (context.eventType === "manual" || context.eventType === "schedule") {
2889
2898
  return {
2890
2899
  shouldTrigger: true,
2891
2900
  reason: `Triggered by ${context.eventType} event`,
2892
- triggerType: "manual"
2901
+ triggerType: "manual",
2902
+ userMessage: options.prompt
2893
2903
  };
2894
2904
  }
2895
- if (context.eventType === "pullrequest:comment_created" || context.eventType === "pullrequest:comment_updated") {
2896
- return validateCommentTrigger(context.comment, options);
2905
+ if (options.prompt) {
2906
+ if (context.eventType === "pullrequest:created" || context.eventType === "pullrequest:updated" || context.isPR) {
2907
+ return {
2908
+ shouldTrigger: true,
2909
+ reason: "Explicit prompt provided for PR event",
2910
+ triggerType: "automation",
2911
+ userMessage: options.prompt
2912
+ };
2913
+ }
2897
2914
  }
2898
2915
  return {
2899
2916
  shouldTrigger: false,
@@ -7213,6 +7230,7 @@ async function prepare() {
7213
7230
  containsTrigger: true,
7214
7231
  mode: mode.name,
7215
7232
  trackingCommentId: modeResult.trackingCommentId,
7233
+ triggerCommentId: context.comment?.id,
7216
7234
  prompt: triggerResult.userMessage || config.PROMPT || "",
7217
7235
  systemPrompt: modeResult.modeContext.systemPrompt,
7218
7236
  tools: modeResult.modeContext.tools,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitbucket-gemini-action",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Bitbucket Pipeline action for AI-powered code review using Google Gemini",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",