@hgarcianareia/ai-pr-review-bitbucket 1.0.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/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@hgarcianareia/ai-pr-review-bitbucket",
3
+ "version": "1.0.0",
4
+ "description": "Bitbucket Cloud adapter for AI-powered PR reviews",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./adapter": "./src/bitbucket-adapter.js"
10
+ },
11
+ "bin": {
12
+ "ai-pr-review-bitbucket": "./src/cli.js"
13
+ },
14
+ "files": [
15
+ "src/**/*.js"
16
+ ],
17
+ "scripts": {
18
+ "review": "node src/cli.js",
19
+ "test": "echo 'No tests configured yet'"
20
+ },
21
+ "dependencies": {
22
+ "@hgarcianareia/ai-pr-review-core": "^1.0.0"
23
+ },
24
+ "engines": {
25
+ "node": ">=20.0.0"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/hgarcianareia/GitHubPRReviewer.git",
30
+ "directory": "packages/bitbucket"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "keywords": [
36
+ "ai",
37
+ "code-review",
38
+ "pull-request",
39
+ "bitbucket",
40
+ "bitbucket-cloud",
41
+ "bitbucket-pipelines",
42
+ "claude",
43
+ "anthropic"
44
+ ],
45
+ "author": "hgarcianareia",
46
+ "license": "MIT"
47
+ }
@@ -0,0 +1,660 @@
1
+ /**
2
+ * Bitbucket Cloud Platform Adapter
3
+ *
4
+ * Implements the PlatformAdapter interface for Bitbucket Cloud.
5
+ * Uses the Bitbucket REST API 2.0 for all operations.
6
+ */
7
+
8
+ import fs from 'fs/promises';
9
+ import path from 'path';
10
+ import { execSync } from 'child_process';
11
+ import {
12
+ PlatformAdapter,
13
+ AI_REVIEW_MARKER,
14
+ parsePRNumber,
15
+ validateRepoOwner,
16
+ validateRepoName,
17
+ validateGitSha,
18
+ sanitizeBranchName
19
+ } from '@hgarcianareia/ai-pr-review-core';
20
+
21
+ // Bitbucket API configuration
22
+ const BITBUCKET_API_BASE = 'https://api.bitbucket.org/2.0';
23
+
24
+ // Rate limiting configuration
25
+ const RATE_LIMIT = {
26
+ maxRetries: 3,
27
+ initialDelayMs: 1000,
28
+ maxDelayMs: 30000
29
+ };
30
+
31
+ // Git command timeout configuration (in milliseconds)
32
+ const GIT_TIMEOUT = {
33
+ local: 30000, // 30s for local operations
34
+ network: 120000 // 120s for network operations
35
+ };
36
+
37
+ /**
38
+ * Sleep for specified milliseconds
39
+ */
40
+ function sleep(ms) {
41
+ return new Promise(resolve => setTimeout(resolve, ms));
42
+ }
43
+
44
+ /**
45
+ * Exponential backoff retry wrapper
46
+ */
47
+ async function withRetry(fn, operation) {
48
+ let lastError;
49
+ let delay = RATE_LIMIT.initialDelayMs;
50
+
51
+ for (let attempt = 1; attempt <= RATE_LIMIT.maxRetries; attempt++) {
52
+ try {
53
+ return await fn();
54
+ } catch (error) {
55
+ lastError = error;
56
+
57
+ if (error.status === 429 || error.message?.includes('rate')) {
58
+ console.log(`[WARN] Rate limited on ${operation}, attempt ${attempt}/${RATE_LIMIT.maxRetries}`);
59
+ await sleep(delay);
60
+ delay = Math.min(delay * 2, RATE_LIMIT.maxDelayMs);
61
+ continue;
62
+ }
63
+
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ throw lastError;
69
+ }
70
+
71
+ /**
72
+ * Safely validate an environment variable with user-friendly error message
73
+ */
74
+ function safeValidateEnv(name, validator, defaultValue = undefined) {
75
+ try {
76
+ return validator(process.env[name]);
77
+ } catch (error) {
78
+ if (defaultValue !== undefined) {
79
+ return defaultValue;
80
+ }
81
+ console.error('='.repeat(60));
82
+ console.error(`[FATAL] ${name} Validation Failed`);
83
+ console.error('='.repeat(60));
84
+ console.error(` ${error.message}`);
85
+ console.error('');
86
+ console.error('Please check your Bitbucket Pipelines configuration.');
87
+ console.error('='.repeat(60));
88
+ process.exit(1);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Bitbucket Cloud Platform Adapter Implementation
94
+ */
95
+ export class BitbucketAdapter extends PlatformAdapter {
96
+ /**
97
+ * @param {Object} context - Platform context
98
+ * @param {string} token - Bitbucket access token
99
+ */
100
+ constructor(context, token) {
101
+ super(context);
102
+ this.token = token;
103
+ }
104
+
105
+ /**
106
+ * Factory method to create a BitbucketAdapter instance
107
+ * @returns {Promise<BitbucketAdapter>}
108
+ */
109
+ static async create() {
110
+ // Validate required token
111
+ const token = process.env.BITBUCKET_TOKEN;
112
+ if (!token) {
113
+ console.error('='.repeat(60));
114
+ console.error('[FATAL] BITBUCKET_TOKEN is required');
115
+ console.error('='.repeat(60));
116
+ console.error(' Please add BITBUCKET_TOKEN to your repository variables.');
117
+ console.error(' This should be an App Password with repository and PR permissions.');
118
+ console.error('='.repeat(60));
119
+ process.exit(1);
120
+ }
121
+
122
+ // Check if this is a manual dispatch
123
+ const isManualDispatch = process.env.AI_REVIEW_TRIGGER === 'manual';
124
+
125
+ // Build context from environment variables
126
+ const context = {
127
+ owner: safeValidateEnv('BITBUCKET_WORKSPACE', validateRepoOwner),
128
+ repo: safeValidateEnv('BITBUCKET_REPO_SLUG', validateRepoName),
129
+ prNumber: safeValidateEnv('BITBUCKET_PR_ID', parsePRNumber),
130
+ prTitle: '',
131
+ prBody: '',
132
+ prAuthor: '',
133
+ baseSha: null,
134
+ headSha: safeValidateEnv('BITBUCKET_COMMIT', (v) => validateGitSha(v, 'BITBUCKET_COMMIT')),
135
+ eventName: process.env.AI_REVIEW_TRIGGER || 'opened',
136
+ isManualTrigger: isManualDispatch
137
+ };
138
+
139
+ const adapter = new BitbucketAdapter(context, token);
140
+
141
+ // Load PR metadata from API
142
+ await adapter._loadPRMetadata();
143
+
144
+ return adapter;
145
+ }
146
+
147
+ /**
148
+ * Make an authenticated request to the Bitbucket API
149
+ * @private
150
+ */
151
+ async _fetchBitbucket(endpoint, options = {}) {
152
+ const url = endpoint.startsWith('http') ? endpoint : `${BITBUCKET_API_BASE}${endpoint}`;
153
+
154
+ const response = await fetch(url, {
155
+ ...options,
156
+ headers: {
157
+ 'Authorization': `Bearer ${this.token}`,
158
+ 'Content-Type': 'application/json',
159
+ 'Accept': 'application/json',
160
+ ...options.headers
161
+ }
162
+ });
163
+
164
+ if (!response.ok) {
165
+ const error = new Error(`Bitbucket API error: ${response.status} ${response.statusText}`);
166
+ error.status = response.status;
167
+ try {
168
+ error.body = await response.json();
169
+ } catch (e) {
170
+ // Ignore JSON parse errors
171
+ }
172
+ throw error;
173
+ }
174
+
175
+ // Handle empty responses
176
+ const contentType = response.headers.get('content-type');
177
+ if (contentType && contentType.includes('application/json')) {
178
+ return response.json();
179
+ }
180
+ return response.text();
181
+ }
182
+
183
+ /**
184
+ * Load PR metadata from Bitbucket API
185
+ * @private
186
+ */
187
+ async _loadPRMetadata() {
188
+ try {
189
+ const pr = await this._fetchBitbucket(
190
+ `/repositories/${this.context.owner}/${this.context.repo}/pullrequests/${this.context.prNumber}`
191
+ );
192
+
193
+ this.context.prTitle = pr.title || '';
194
+ this.context.prBody = pr.description || '';
195
+ this.context.prAuthor = pr.author?.display_name || pr.author?.nickname || '';
196
+ this.context.baseSha = pr.destination?.commit?.hash || null;
197
+
198
+ // Update headSha if available from API
199
+ if (pr.source?.commit?.hash) {
200
+ this.context.headSha = pr.source.commit.hash;
201
+ }
202
+
203
+ console.log('[INFO] Loaded PR metadata from Bitbucket API');
204
+ } catch (error) {
205
+ console.error('[ERROR] Failed to load PR metadata:', error.message);
206
+ throw new Error('Failed to load PR metadata from Bitbucket API');
207
+ }
208
+
209
+ // Validate baseSha after loading metadata
210
+ if (!this.context.baseSha) {
211
+ console.error('='.repeat(60));
212
+ console.error('[FATAL] BASE_SHA Validation Failed');
213
+ console.error('='.repeat(60));
214
+ console.error(' Could not determine base commit SHA from PR metadata');
215
+ console.error('='.repeat(60));
216
+ process.exit(1);
217
+ }
218
+ }
219
+
220
+ // =========================================================================
221
+ // Interface Implementation
222
+ // =========================================================================
223
+
224
+ getPlatformType() {
225
+ return 'bitbucket';
226
+ }
227
+
228
+ getCapabilities() {
229
+ return {
230
+ supportsReactions: false, // Limited reaction support in Bitbucket
231
+ supportsReviewStates: true, // Supports APPROVE and REQUEST_CHANGES
232
+ supportsAutoFixPR: true,
233
+ supportsCaching: true
234
+ };
235
+ }
236
+
237
+ // =========================================================================
238
+ // PR Data Access
239
+ // =========================================================================
240
+
241
+ async getDiff() {
242
+ // First try to read from file (prepared by pipeline script)
243
+ try {
244
+ const diffContent = await fs.readFile('pr_diff.txt', 'utf-8');
245
+ return diffContent;
246
+ } catch (error) {
247
+ // Fall back to API
248
+ console.log('[INFO] Fetching diff from Bitbucket API...');
249
+ }
250
+
251
+ try {
252
+ const diff = await this._fetchBitbucket(
253
+ `/repositories/${this.context.owner}/${this.context.repo}/pullrequests/${this.context.prNumber}/diff`,
254
+ { headers: { 'Accept': 'text/plain' } }
255
+ );
256
+ return diff;
257
+ } catch (error) {
258
+ console.error('[ERROR] Failed to get diff:', error.message);
259
+ throw error;
260
+ }
261
+ }
262
+
263
+ async getChangedFiles() {
264
+ // First try to read from file (prepared by pipeline script)
265
+ try {
266
+ const filesContent = await fs.readFile('changed_files.txt', 'utf-8');
267
+ return filesContent.split('\n').filter(f => f.trim());
268
+ } catch (error) {
269
+ // Fall back to API
270
+ console.log('[INFO] Fetching changed files from Bitbucket API...');
271
+ }
272
+
273
+ try {
274
+ const diffstat = await this._fetchBitbucket(
275
+ `/repositories/${this.context.owner}/${this.context.repo}/pullrequests/${this.context.prNumber}/diffstat`
276
+ );
277
+
278
+ const files = [];
279
+ for (const entry of diffstat.values || []) {
280
+ // Use new path if available, otherwise old path
281
+ const filePath = entry.new?.path || entry.old?.path;
282
+ if (filePath) {
283
+ files.push(filePath);
284
+ }
285
+ }
286
+
287
+ return files;
288
+ } catch (error) {
289
+ console.error('[ERROR] Failed to get changed files:', error.message);
290
+ throw error;
291
+ }
292
+ }
293
+
294
+ async getExistingComments() {
295
+ // First try to read from file (prepared by pipeline script)
296
+ try {
297
+ const commentsJson = await fs.readFile('pr_comments.json', 'utf-8');
298
+ const comments = JSON.parse(commentsJson);
299
+
300
+ // Filter for AI review comments
301
+ return comments.filter(c =>
302
+ c.content?.raw?.includes(AI_REVIEW_MARKER) ||
303
+ c.content?.raw?.includes('AI Code Review')
304
+ ).map(c => ({
305
+ id: c.id,
306
+ path: c.inline?.path,
307
+ line: c.inline?.to,
308
+ body: c.content?.raw
309
+ }));
310
+ } catch (error) {
311
+ // Fall back to API
312
+ console.log('[INFO] Fetching existing comments from Bitbucket API...');
313
+ }
314
+
315
+ try {
316
+ const response = await this._fetchBitbucket(
317
+ `/repositories/${this.context.owner}/${this.context.repo}/pullrequests/${this.context.prNumber}/comments`
318
+ );
319
+
320
+ const comments = response.values || [];
321
+
322
+ // Filter for AI review comments
323
+ return comments.filter(c =>
324
+ c.content?.raw?.includes(AI_REVIEW_MARKER) ||
325
+ c.content?.raw?.includes('AI Code Review')
326
+ ).map(c => ({
327
+ id: c.id,
328
+ path: c.inline?.path,
329
+ line: c.inline?.to,
330
+ body: c.content?.raw
331
+ }));
332
+ } catch (error) {
333
+ console.log('[WARN] Could not load existing comments:', error.message);
334
+ return [];
335
+ }
336
+ }
337
+
338
+ async getExistingReviews() {
339
+ // Bitbucket doesn't have a separate reviews concept like GitHub
340
+ // Return empty array - we'll use comments for threading
341
+ return [];
342
+ }
343
+
344
+ async getFileContent(filePath) {
345
+ try {
346
+ const fullPath = path.join(process.cwd(), filePath);
347
+ return await fs.readFile(fullPath, 'utf-8');
348
+ } catch (error) {
349
+ // Try fetching from API if local file not found
350
+ try {
351
+ const content = await this._fetchBitbucket(
352
+ `/repositories/${this.context.owner}/${this.context.repo}/src/${this.context.headSha}/${encodeURIComponent(filePath)}`,
353
+ { headers: { 'Accept': 'text/plain' } }
354
+ );
355
+ return content;
356
+ } catch (apiError) {
357
+ console.log(`[WARN] Could not read file ${filePath}:`, error.message);
358
+ throw error;
359
+ }
360
+ }
361
+ }
362
+
363
+ // =========================================================================
364
+ // PR Interaction
365
+ // =========================================================================
366
+
367
+ async postReview(body, comments = [], event = 'COMMENT') {
368
+ try {
369
+ // Add marker to the body for identification
370
+ const markedBody = `${AI_REVIEW_MARKER}\n${body}`;
371
+
372
+ // Post main summary comment
373
+ await withRetry(
374
+ () => this._fetchBitbucket(
375
+ `/repositories/${this.context.owner}/${this.context.repo}/pullrequests/${this.context.prNumber}/comments`,
376
+ {
377
+ method: 'POST',
378
+ body: JSON.stringify({
379
+ content: { raw: markedBody }
380
+ })
381
+ }
382
+ ),
383
+ 'postSummaryComment'
384
+ );
385
+
386
+ console.log('[INFO] Posted review summary comment');
387
+
388
+ // Post inline comments
389
+ for (const comment of comments) {
390
+ try {
391
+ await withRetry(
392
+ () => this._fetchBitbucket(
393
+ `/repositories/${this.context.owner}/${this.context.repo}/pullrequests/${this.context.prNumber}/comments`,
394
+ {
395
+ method: 'POST',
396
+ body: JSON.stringify({
397
+ content: { raw: `${AI_REVIEW_MARKER}\n${comment.body}` },
398
+ inline: {
399
+ to: comment.position, // Line number in new file
400
+ path: comment.path
401
+ }
402
+ })
403
+ }
404
+ ),
405
+ 'postInlineComment'
406
+ );
407
+ } catch (error) {
408
+ console.log(`[WARN] Failed to post inline comment on ${comment.path}:${comment.position}:`, error.message);
409
+ }
410
+ }
411
+
412
+ console.log(`[INFO] Posted ${comments.length} inline comments`);
413
+
414
+ // Handle review state
415
+ if (event === 'APPROVE') {
416
+ try {
417
+ await withRetry(
418
+ () => this._fetchBitbucket(
419
+ `/repositories/${this.context.owner}/${this.context.repo}/pullrequests/${this.context.prNumber}/approve`,
420
+ { method: 'POST' }
421
+ ),
422
+ 'approvePR'
423
+ );
424
+ console.log('[INFO] Approved PR');
425
+ } catch (error) {
426
+ console.log('[WARN] Could not approve PR:', error.message);
427
+ }
428
+ } else if (event === 'REQUEST_CHANGES') {
429
+ try {
430
+ await withRetry(
431
+ () => this._fetchBitbucket(
432
+ `/repositories/${this.context.owner}/${this.context.repo}/pullrequests/${this.context.prNumber}/request-changes`,
433
+ { method: 'POST' }
434
+ ),
435
+ 'requestChangesPR'
436
+ );
437
+ console.log('[INFO] Requested changes on PR');
438
+ } catch (error) {
439
+ console.log('[WARN] Could not request changes on PR:', error.message);
440
+ }
441
+ }
442
+
443
+ } catch (error) {
444
+ console.error('[ERROR] Failed to post review:', error.message);
445
+ throw error;
446
+ }
447
+ }
448
+
449
+ calculateCommentPosition(file, targetLine) {
450
+ // Bitbucket uses line numbers directly, not diff positions
451
+ // Validate the line exists in an added or context section
452
+ for (const hunk of file.hunks || []) {
453
+ for (const change of hunk.changes || []) {
454
+ if ((change.type === 'add' || change.type === 'context') &&
455
+ change.newLine === targetLine) {
456
+ return targetLine;
457
+ }
458
+ }
459
+ }
460
+
461
+ // If exact line not found, find closest added line within 5 lines
462
+ let closestLine = null;
463
+ let closestDistance = Infinity;
464
+
465
+ for (const hunk of file.hunks || []) {
466
+ for (const change of hunk.changes || []) {
467
+ if (change.type === 'add' && change.newLine) {
468
+ const distance = Math.abs(change.newLine - targetLine);
469
+ if (distance < closestDistance && distance <= 5) {
470
+ closestDistance = distance;
471
+ closestLine = change.newLine;
472
+ }
473
+ }
474
+ }
475
+ }
476
+
477
+ return closestLine;
478
+ }
479
+
480
+ // =========================================================================
481
+ // Feature: Feedback Loop (Reactions)
482
+ // =========================================================================
483
+
484
+ async getCommentReactions(commentId) {
485
+ // Bitbucket has limited reaction support - return null
486
+ return null;
487
+ }
488
+
489
+ async getReviewReactions(reviewId) {
490
+ // Bitbucket doesn't have review reactions - return null
491
+ return null;
492
+ }
493
+
494
+ // =========================================================================
495
+ // Feature: Auto-fix PRs
496
+ // =========================================================================
497
+
498
+ async createAutoFixPR(branchName, fixes, prTitle, prBody) {
499
+ if (fixes.length === 0) {
500
+ return null;
501
+ }
502
+
503
+ const sanitizedBranch = sanitizeBranchName(branchName);
504
+
505
+ try {
506
+ // Stash any current changes
507
+ execSync('git stash', {
508
+ encoding: 'utf-8',
509
+ stdio: 'pipe',
510
+ timeout: GIT_TIMEOUT.local
511
+ });
512
+
513
+ // Create and checkout new branch
514
+ execSync(`git checkout -b ${sanitizedBranch}`, {
515
+ encoding: 'utf-8',
516
+ stdio: 'pipe',
517
+ timeout: GIT_TIMEOUT.local
518
+ });
519
+
520
+ // Group fixes by file
521
+ const fixesByFile = new Map();
522
+ for (const fix of fixes) {
523
+ if (!fixesByFile.has(fix.file)) {
524
+ fixesByFile.set(fix.file, []);
525
+ }
526
+ fixesByFile.get(fix.file).push(fix);
527
+ }
528
+
529
+ // Apply fixes to each file
530
+ for (const [filePath, fileFixes] of fixesByFile) {
531
+ try {
532
+ const fullPath = path.join(process.cwd(), filePath);
533
+ const content = await fs.readFile(fullPath, 'utf-8');
534
+ const lines = content.split('\n');
535
+
536
+ // Sort fixes by line number descending to apply from bottom up
537
+ const sortedFixes = [...fileFixes].sort((a, b) => b.line - a.line);
538
+
539
+ for (const fix of sortedFixes) {
540
+ if (fix.line > 0 && fix.line <= lines.length) {
541
+ lines[fix.line - 1] = fix.suggested;
542
+ }
543
+ }
544
+
545
+ await fs.writeFile(fullPath, lines.join('\n'), 'utf-8');
546
+ } catch (error) {
547
+ console.log(`[WARN] Failed to apply fixes to ${filePath}:`, error.message);
548
+ }
549
+ }
550
+
551
+ // Commit changes
552
+ execSync('git add -A', {
553
+ encoding: 'utf-8',
554
+ stdio: 'pipe',
555
+ timeout: GIT_TIMEOUT.local
556
+ });
557
+ execSync(`git commit -m "${prTitle}"`, {
558
+ encoding: 'utf-8',
559
+ stdio: 'pipe',
560
+ timeout: GIT_TIMEOUT.local
561
+ });
562
+
563
+ // Push branch
564
+ execSync(`git push origin ${sanitizedBranch}`, {
565
+ encoding: 'utf-8',
566
+ stdio: 'pipe',
567
+ timeout: GIT_TIMEOUT.network
568
+ });
569
+
570
+ // Get the base branch (the PR's source branch)
571
+ const prInfo = await this.getPRInfo(this.context.prNumber);
572
+ const baseBranch = prInfo.source?.branch?.name;
573
+
574
+ if (!baseBranch) {
575
+ throw new Error('Could not determine source branch for auto-fix PR');
576
+ }
577
+
578
+ // Create PR via Bitbucket API
579
+ const newPR = await this._fetchBitbucket(
580
+ `/repositories/${this.context.owner}/${this.context.repo}/pullrequests`,
581
+ {
582
+ method: 'POST',
583
+ body: JSON.stringify({
584
+ title: prTitle,
585
+ description: prBody,
586
+ source: {
587
+ branch: { name: sanitizedBranch }
588
+ },
589
+ destination: {
590
+ branch: { name: baseBranch }
591
+ },
592
+ close_source_branch: true
593
+ })
594
+ }
595
+ );
596
+
597
+ // Checkout back to original branch
598
+ execSync('git checkout -', {
599
+ encoding: 'utf-8',
600
+ stdio: 'pipe',
601
+ timeout: GIT_TIMEOUT.local
602
+ });
603
+ execSync('git stash pop || true', {
604
+ encoding: 'utf-8',
605
+ stdio: 'pipe',
606
+ timeout: GIT_TIMEOUT.local
607
+ });
608
+
609
+ console.log(`[INFO] Created auto-fix PR #${newPR.id}`);
610
+
611
+ return {
612
+ prNumber: newPR.id,
613
+ prUrl: newPR.links?.html?.href || `https://bitbucket.org/${this.context.owner}/${this.context.repo}/pull-requests/${newPR.id}`,
614
+ branch: sanitizedBranch,
615
+ fixCount: fixes.length
616
+ };
617
+ } catch (error) {
618
+ console.error('[ERROR] Failed to create auto-fix PR:', error.message);
619
+
620
+ // Try to restore original state
621
+ try {
622
+ execSync('git checkout -', {
623
+ encoding: 'utf-8',
624
+ stdio: 'pipe',
625
+ timeout: GIT_TIMEOUT.local
626
+ });
627
+ execSync('git stash pop || true', {
628
+ encoding: 'utf-8',
629
+ stdio: 'pipe',
630
+ timeout: GIT_TIMEOUT.local
631
+ });
632
+ } catch (e) {
633
+ // Ignore cleanup errors
634
+ }
635
+
636
+ return null;
637
+ }
638
+ }
639
+
640
+ async getPRInfo(prNumber) {
641
+ const response = await this._fetchBitbucket(
642
+ `/repositories/${this.context.owner}/${this.context.repo}/pullrequests/${prNumber}`
643
+ );
644
+ return response;
645
+ }
646
+
647
+ // =========================================================================
648
+ // Metrics & Summary
649
+ // =========================================================================
650
+
651
+ async writeMetricsSummary(summary) {
652
+ // Bitbucket Pipelines doesn't have a native step summary feature like GitHub Actions
653
+ // Log to console instead
654
+ console.log('\n' + '='.repeat(60));
655
+ console.log('AI Review Metrics Summary');
656
+ console.log('='.repeat(60));
657
+ console.log(summary);
658
+ console.log('='.repeat(60) + '\n');
659
+ }
660
+ }
package/src/cli.js ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for Bitbucket AI PR Review
4
+ *
5
+ * This script is the main entry point when running the review
6
+ * from Bitbucket Pipelines or the command line.
7
+ */
8
+
9
+ import { ReviewEngine } from '@hgarcianareia/ai-pr-review-core';
10
+ import { BitbucketAdapter } from './bitbucket-adapter.js';
11
+
12
+ async function main() {
13
+ // Validate required API key
14
+ if (!process.env.ANTHROPIC_API_KEY) {
15
+ console.error('='.repeat(60));
16
+ console.error('[FATAL] ANTHROPIC_API_KEY is required');
17
+ console.error('='.repeat(60));
18
+ console.error(' Please add ANTHROPIC_API_KEY to your repository variables.');
19
+ console.error('='.repeat(60));
20
+ process.exit(1);
21
+ }
22
+
23
+ try {
24
+ // Create the Bitbucket adapter
25
+ const adapter = await BitbucketAdapter.create();
26
+
27
+ // Create and run the review engine
28
+ const engine = new ReviewEngine({
29
+ platformAdapter: adapter,
30
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY
31
+ });
32
+
33
+ const result = await engine.run();
34
+
35
+ if (result.skipped) {
36
+ console.log(`[INFO] Review skipped: ${result.reason}`);
37
+ process.exit(0);
38
+ }
39
+
40
+ console.log('[INFO] Review completed successfully');
41
+ process.exit(0);
42
+ } catch (error) {
43
+ console.error('[FATAL] Review failed:', error.message);
44
+ console.error(error.stack);
45
+ process.exit(1);
46
+ }
47
+ }
48
+
49
+ main();
package/src/index.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @hgarcianareia/ai-pr-review-bitbucket
3
+ *
4
+ * Bitbucket Cloud adapter for AI-powered PR reviews.
5
+ * Use this package with @hgarcianareia/ai-pr-review-core to review
6
+ * Pull Requests in Bitbucket Pipelines.
7
+ *
8
+ * Usage:
9
+ * import { BitbucketAdapter } from '@hgarcianareia/ai-pr-review-bitbucket';
10
+ * import { ReviewEngine } from '@hgarcianareia/ai-pr-review-core';
11
+ *
12
+ * const adapter = await BitbucketAdapter.create();
13
+ * const engine = new ReviewEngine({
14
+ * platformAdapter: adapter,
15
+ * anthropicApiKey: process.env.ANTHROPIC_API_KEY
16
+ * });
17
+ *
18
+ * await engine.run();
19
+ */
20
+
21
+ export { BitbucketAdapter } from './bitbucket-adapter.js';