@atlassian-dc-mcp/bitbucket 0.29.0 → 0.31.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/CHANGELOG.md +25 -0
- package/README.md +56 -2
- package/build/__tests__/bitbucket-service.test.js +60 -2
- package/build/__tests__/bitbucket-service.test.js.map +1 -1
- package/build/__tests__/bitbucket-token-optimization.test.js +11 -0
- package/build/__tests__/bitbucket-token-optimization.test.js.map +1 -1
- package/build/__tests__/merge-gateway.test.d.ts +2 -0
- package/build/__tests__/merge-gateway.test.d.ts.map +1 -0
- package/build/__tests__/merge-gateway.test.js +98 -0
- package/build/__tests__/merge-gateway.test.js.map +1 -0
- package/build/__tests__/pr-comment-mapper.test.js +91 -11
- package/build/__tests__/pr-comment-mapper.test.js.map +1 -1
- package/build/__tests__/pr-merge.test.d.ts +2 -0
- package/build/__tests__/pr-merge.test.d.ts.map +1 -0
- package/build/__tests__/pr-merge.test.js +166 -0
- package/build/__tests__/pr-merge.test.js.map +1 -0
- package/build/bitbucket-response-mapper.d.ts +10 -0
- package/build/bitbucket-response-mapper.d.ts.map +1 -1
- package/build/bitbucket-response-mapper.js +16 -0
- package/build/bitbucket-response-mapper.js.map +1 -1
- package/build/bitbucket-service.d.ts +68 -9
- package/build/bitbucket-service.d.ts.map +1 -1
- package/build/bitbucket-service.js +91 -15
- package/build/bitbucket-service.js.map +1 -1
- package/build/index.js +34 -4
- package/build/index.js.map +1 -1
- package/build/merge-gateway.d.ts +34 -0
- package/build/merge-gateway.d.ts.map +1 -0
- package/build/merge-gateway.js +86 -0
- package/build/merge-gateway.js.map +1 -0
- package/build/pr-comment-mapper.d.ts +3 -0
- package/build/pr-comment-mapper.d.ts.map +1 -1
- package/build/pr-comment-mapper.js +8 -3
- package/build/pr-comment-mapper.js.map +1 -1
- package/build/pr-merge.d.ts +23 -0
- package/build/pr-merge.d.ts.map +1 -0
- package/build/pr-merge.js +59 -0
- package/build/pr-merge.js.map +1 -0
- package/package.json +3 -3
- package/src/__tests__/bitbucket-service.test.ts +154 -2
- package/src/__tests__/bitbucket-token-optimization.test.ts +11 -0
- package/src/__tests__/merge-gateway.test.ts +119 -0
- package/src/__tests__/pr-comment-mapper.test.ts +95 -11
- package/src/__tests__/pr-merge.test.ts +206 -0
- package/src/bitbucket-response-mapper.ts +27 -0
- package/src/bitbucket-service.ts +100 -14
- package/src/index.ts +55 -6
- package/src/merge-gateway.ts +132 -0
- package/src/pr-comment-mapper.ts +11 -3
- package/src/pr-merge.ts +94 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { PullRequestsService } from '../bitbucket-client/index.js';
|
|
2
|
+
import { fetchMergeability, mergePullRequest } from '../pr-merge.js';
|
|
3
|
+
import type { MergeGateway } from '../merge-gateway.js';
|
|
4
|
+
|
|
5
|
+
jest.mock('../bitbucket-client/index.js', () => ({
|
|
6
|
+
PullRequestsService: {
|
|
7
|
+
canMerge: jest.fn(),
|
|
8
|
+
merge: jest.fn(),
|
|
9
|
+
get3: jest.fn(),
|
|
10
|
+
},
|
|
11
|
+
OpenAPI: { BASE: '', TOKEN: '', VERSION: '' },
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const canMerge = PullRequestsService.canMerge as jest.Mock;
|
|
15
|
+
const merge = PullRequestsService.merge as jest.Mock;
|
|
16
|
+
const getPullRequest = PullRequestsService.get3 as jest.Mock;
|
|
17
|
+
|
|
18
|
+
const OPEN_GATEWAY: MergeGateway = { enabled: true, repos: ['PROJ/demo'], targetRefs: [] };
|
|
19
|
+
const CLEAN = { canMerge: true, conflicted: false, outcome: 'CLEAN', vetoes: [] };
|
|
20
|
+
const MERGED_PR = {
|
|
21
|
+
id: 42,
|
|
22
|
+
version: 3,
|
|
23
|
+
title: 'Add merge tool',
|
|
24
|
+
state: 'MERGED',
|
|
25
|
+
fromRef: { id: 'refs/heads/feature' },
|
|
26
|
+
toRef: { id: 'refs/heads/develop' },
|
|
27
|
+
reviewers: [],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const baseParams = {
|
|
31
|
+
projectKey: 'PROJ',
|
|
32
|
+
repositorySlug: 'demo',
|
|
33
|
+
pullRequestId: '42',
|
|
34
|
+
version: 2,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe('fetchMergeability', () => {
|
|
38
|
+
beforeEach(() => jest.clearAllMocks());
|
|
39
|
+
|
|
40
|
+
it('shapes vetoes into summary/detail pairs', async () => {
|
|
41
|
+
canMerge.mockResolvedValue({
|
|
42
|
+
canMerge: false,
|
|
43
|
+
conflicted: false,
|
|
44
|
+
outcome: 'VETOED',
|
|
45
|
+
vetoes: [{ summaryMessage: 'Not enough approvals', detailedMessage: '2 required, 1 given' }],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const result = await fetchMergeability('PROJ', 'demo', '42');
|
|
49
|
+
|
|
50
|
+
expect(canMerge).toHaveBeenCalledWith('PROJ', '42', 'demo');
|
|
51
|
+
expect(result.data).toEqual({
|
|
52
|
+
canMerge: false,
|
|
53
|
+
conflicted: false,
|
|
54
|
+
outcome: 'VETOED',
|
|
55
|
+
vetoes: [{ summary: 'Not enough approvals', detail: '2 required, 1 given' }],
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('reports API failures without shaping', async () => {
|
|
60
|
+
canMerge.mockRejectedValue(new Error('boom'));
|
|
61
|
+
|
|
62
|
+
const result = await fetchMergeability('PROJ', 'demo', '42');
|
|
63
|
+
|
|
64
|
+
expect(result.success).toBe(false);
|
|
65
|
+
expect(result.error).toBe('boom');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('mergePullRequest', () => {
|
|
70
|
+
beforeEach(() => jest.clearAllMocks());
|
|
71
|
+
|
|
72
|
+
it('merges with the version as an optimistic lock and returns a compact ack', async () => {
|
|
73
|
+
canMerge.mockResolvedValue(CLEAN);
|
|
74
|
+
merge.mockResolvedValue(MERGED_PR);
|
|
75
|
+
|
|
76
|
+
const result = await mergePullRequest({ ...baseParams, gateway: OPEN_GATEWAY });
|
|
77
|
+
|
|
78
|
+
expect(merge).toHaveBeenCalledWith('PROJ', '42', 'demo', '2', { version: 2 });
|
|
79
|
+
expect(result.success).toBe(true);
|
|
80
|
+
expect(result.data).toEqual({
|
|
81
|
+
id: 42,
|
|
82
|
+
version: 3,
|
|
83
|
+
title: 'Add merge tool',
|
|
84
|
+
state: 'MERGED',
|
|
85
|
+
fromRefId: 'refs/heads/feature',
|
|
86
|
+
toRefId: 'refs/heads/develop',
|
|
87
|
+
reviewerCount: 0,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('passes the strategy and message through, and can return the full payload', async () => {
|
|
92
|
+
canMerge.mockResolvedValue(CLEAN);
|
|
93
|
+
merge.mockResolvedValue(MERGED_PR);
|
|
94
|
+
|
|
95
|
+
const result = await mergePullRequest({
|
|
96
|
+
...baseParams,
|
|
97
|
+
gateway: OPEN_GATEWAY,
|
|
98
|
+
strategyId: 'squash',
|
|
99
|
+
message: 'Squashed',
|
|
100
|
+
output: 'full',
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(merge).toHaveBeenCalledWith('PROJ', '42', 'demo', '2', {
|
|
104
|
+
version: 2,
|
|
105
|
+
strategyId: 'squash',
|
|
106
|
+
message: 'Squashed',
|
|
107
|
+
});
|
|
108
|
+
expect(result.data).toBe(MERGED_PR);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('refuses without any request when merging is disabled', async () => {
|
|
112
|
+
const result = await mergePullRequest({
|
|
113
|
+
...baseParams,
|
|
114
|
+
gateway: { enabled: false, repos: [], targetRefs: [] },
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(result.success).toBe(false);
|
|
118
|
+
expect(result.error).toMatch(/disabled on this server/);
|
|
119
|
+
expect(canMerge).not.toHaveBeenCalled();
|
|
120
|
+
expect(merge).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('refuses a repository outside the allowed list without any request', async () => {
|
|
124
|
+
const result = await mergePullRequest({
|
|
125
|
+
...baseParams,
|
|
126
|
+
repositorySlug: 'other-repo',
|
|
127
|
+
gateway: OPEN_GATEWAY,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(result.success).toBe(false);
|
|
131
|
+
expect(result.error).toMatch(/not allowed in PROJ\/other-repo/);
|
|
132
|
+
expect(merge).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('refuses a target branch outside the allowed refs and never merges', async () => {
|
|
136
|
+
getPullRequest.mockResolvedValue({ toRef: { id: 'refs/heads/master' } });
|
|
137
|
+
|
|
138
|
+
const result = await mergePullRequest({
|
|
139
|
+
...baseParams,
|
|
140
|
+
gateway: { enabled: true, repos: ['PROJ/demo'], targetRefs: ['refs/heads/develop'] },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(result.success).toBe(false);
|
|
144
|
+
expect(result.error).toMatch(/refs\/heads\/master is not allowed/);
|
|
145
|
+
expect(canMerge).not.toHaveBeenCalled();
|
|
146
|
+
expect(merge).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('merges when the pull request targets an allowed branch', async () => {
|
|
150
|
+
getPullRequest.mockResolvedValue({ toRef: { id: 'refs/heads/release/1.2' } });
|
|
151
|
+
canMerge.mockResolvedValue(CLEAN);
|
|
152
|
+
merge.mockResolvedValue(MERGED_PR);
|
|
153
|
+
|
|
154
|
+
const result = await mergePullRequest({
|
|
155
|
+
...baseParams,
|
|
156
|
+
gateway: { enabled: true, repos: ['PROJ/demo'], targetRefs: ['refs/heads/release/*'] },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(result.success).toBe(true);
|
|
160
|
+
expect(merge).toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('does not fetch the pull request when target branches are unrestricted', async () => {
|
|
164
|
+
canMerge.mockResolvedValue(CLEAN);
|
|
165
|
+
merge.mockResolvedValue(MERGED_PR);
|
|
166
|
+
|
|
167
|
+
await mergePullRequest({ ...baseParams, gateway: OPEN_GATEWAY });
|
|
168
|
+
|
|
169
|
+
expect(getPullRequest).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('refuses a conflicted pull request before issuing the merge', async () => {
|
|
173
|
+
canMerge.mockResolvedValue({ canMerge: false, conflicted: true, outcome: 'CONFLICTED', vetoes: [] });
|
|
174
|
+
|
|
175
|
+
const result = await mergePullRequest({ ...baseParams, gateway: OPEN_GATEWAY });
|
|
176
|
+
|
|
177
|
+
expect(result.success).toBe(false);
|
|
178
|
+
expect(result.error).toMatch(/has conflicts/);
|
|
179
|
+
expect(merge).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('refuses a vetoed pull request and reports the veto reasons', async () => {
|
|
183
|
+
canMerge.mockResolvedValue({
|
|
184
|
+
canMerge: false,
|
|
185
|
+
conflicted: false,
|
|
186
|
+
vetoes: [{ summaryMessage: 'Unresolved tasks', detailedMessage: '1 open task' }],
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const result = await mergePullRequest({ ...baseParams, gateway: OPEN_GATEWAY });
|
|
190
|
+
|
|
191
|
+
expect(result.success).toBe(false);
|
|
192
|
+
expect(result.error).toMatch(/merge check vetoed the merge\. Unresolved tasks — 1 open task/);
|
|
193
|
+
expect(merge).not.toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('surfaces a server-side merge failure such as a stale version', async () => {
|
|
197
|
+
canMerge.mockResolvedValue(CLEAN);
|
|
198
|
+
merge.mockRejectedValue({ status: 409, statusText: 'Conflict', body: { errors: [{ message: 'stale' }] } });
|
|
199
|
+
|
|
200
|
+
const result = await mergePullRequest({ ...baseParams, gateway: OPEN_GATEWAY });
|
|
201
|
+
|
|
202
|
+
expect(result.success).toBe(false);
|
|
203
|
+
expect(result.error).toBe('Error merging pull request: 409 Conflict');
|
|
204
|
+
expect(result.details).toEqual({ errors: [{ message: 'stale' }] });
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -52,6 +52,8 @@ export function shapePullRequestCommentsResponse(
|
|
|
52
52
|
totalActivities: Array.isArray(filteredResponse.values) ? filteredResponse.values.length : 0,
|
|
53
53
|
commentCount: getCommentSummary(filteredResponse, options).length,
|
|
54
54
|
unresolvedCount: 0,
|
|
55
|
+
blockerCount: 0,
|
|
56
|
+
unresolvedBlockerCount: 0,
|
|
55
57
|
},
|
|
56
58
|
items: getCommentSummary(filteredResponse, options),
|
|
57
59
|
};
|
|
@@ -100,12 +102,37 @@ export function shapePullRequestAck(pullRequest: any): Record<string, any> {
|
|
|
100
102
|
};
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
export interface MergeVeto {
|
|
106
|
+
summary?: string;
|
|
107
|
+
detail?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function shapeMergeability(mergeability: any): {
|
|
111
|
+
canMerge: boolean;
|
|
112
|
+
conflicted: boolean;
|
|
113
|
+
outcome?: string;
|
|
114
|
+
vetoes: MergeVeto[];
|
|
115
|
+
} {
|
|
116
|
+
const vetoes = Array.isArray(mergeability?.vetoes) ? mergeability.vetoes : [];
|
|
117
|
+
return {
|
|
118
|
+
canMerge: mergeability?.canMerge === true,
|
|
119
|
+
conflicted: mergeability?.conflicted === true,
|
|
120
|
+
...(typeof mergeability?.outcome === 'string' ? { outcome: mergeability.outcome } : {}),
|
|
121
|
+
vetoes: vetoes.map((veto: any) => ({
|
|
122
|
+
...(typeof veto?.summaryMessage === 'string' ? { summary: veto.summaryMessage } : {}),
|
|
123
|
+
...(typeof veto?.detailedMessage === 'string' ? { detail: veto.detailedMessage } : {}),
|
|
124
|
+
})),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
103
128
|
export function shapePullRequestCommentAck(comment: any): Record<string, any> {
|
|
104
129
|
const link = getLink(comment?.links);
|
|
105
130
|
return {
|
|
106
131
|
...(comment?.id !== undefined ? { id: comment.id } : {}),
|
|
107
132
|
...(comment?.parent?.id !== undefined ? { parentId: comment.parent.id } : {}),
|
|
108
133
|
...(typeof comment?.state === 'string' ? { state: comment.state } : {}),
|
|
134
|
+
...(typeof comment?.threadResolved === 'boolean' ? { threadResolved: comment.threadResolved } : {}),
|
|
135
|
+
...(typeof comment?.severity === 'string' ? { severity: comment.severity } : {}),
|
|
109
136
|
pending: comment?.state === 'PENDING',
|
|
110
137
|
...(typeof comment?.anchor?.path === 'string'
|
|
111
138
|
? {
|
package/src/bitbucket-service.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { request as __request } from './bitbucket-client/core/request.js';
|
|
|
4
4
|
import { handleApiOperation, resolveOpenApiBase } from '@atlassian-dc-mcp/common';
|
|
5
5
|
import { simplifyInboxPullRequests } from './inbox-pr-mapper.js';
|
|
6
6
|
import { BITBUCKET_PRODUCT, getDefaultPageSize, getMissingConfig } from './config.js';
|
|
7
|
+
import { fetchMergeability, mergePullRequest, type MergePullRequestParams } from './pr-merge.js';
|
|
7
8
|
import {
|
|
8
9
|
BitbucketMutationOutputMode,
|
|
9
10
|
BitbucketOutputMode,
|
|
@@ -214,6 +215,25 @@ export class BitbucketService {
|
|
|
214
215
|
);
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Get the raw content of a file in a repository
|
|
220
|
+
* @param projectKey The project key
|
|
221
|
+
* @param repositorySlug The repository slug
|
|
222
|
+
* @param path The path to the file in the repository
|
|
223
|
+
* @param at Optional branch, tag or commit to read the file at. Defaults to the repository default branch
|
|
224
|
+
* @returns Promise with the raw file content
|
|
225
|
+
*/
|
|
226
|
+
async getFileContent(projectKey: string, repositorySlug: string, path: string, at?: string) {
|
|
227
|
+
projectKey = projectKey.toUpperCase();
|
|
228
|
+
repositorySlug = repositorySlug.toLowerCase();
|
|
229
|
+
// Leading slashes would produce a double slash in the URL and a 404 from Bitbucket.
|
|
230
|
+
const filePath = path.replace(/^\/+/, '');
|
|
231
|
+
return handleApiOperation(
|
|
232
|
+
() => RepositoryService.streamRaw(filePath, projectKey, repositorySlug, at),
|
|
233
|
+
'Error fetching file content'
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
217
237
|
/**
|
|
218
238
|
* Get pull requests for a repository
|
|
219
239
|
* @param projectKey The project key
|
|
@@ -460,18 +480,20 @@ export class BitbucketService {
|
|
|
460
480
|
}
|
|
461
481
|
|
|
462
482
|
/**
|
|
463
|
-
* Update an existing pull request comment. Use this to edit text, change severity,
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
483
|
+
* Update an existing pull request comment. Use this to edit text, change severity, change state, or
|
|
484
|
+
* resolve/reopen a comment thread. State and thread resolution are independent: `state` is the task
|
|
485
|
+
* state of a BLOCKER comment ('RESOLVED' ticks the task, 'OPEN' un-ticks it), while `threadResolved`
|
|
486
|
+
* toggles the thread-level "Resolve" button. A root BLOCKER comment can hold these independently, so
|
|
487
|
+
* they are sent as separate fields.
|
|
467
488
|
* @param projectKey The project key
|
|
468
489
|
* @param repositorySlug The repository slug
|
|
469
490
|
* @param pullRequestId The pull request ID
|
|
470
491
|
* @param commentId The comment ID to update
|
|
471
492
|
* @param version The current version of the comment (required for optimistic locking)
|
|
472
493
|
* @param text Optional new comment text
|
|
473
|
-
* @param state Optional new state. On a BLOCKER comment, 'RESOLVED' ticks the task and 'OPEN' un-ticks it.
|
|
494
|
+
* @param state Optional new task state. On a BLOCKER comment, 'RESOLVED' ticks the task and 'OPEN' un-ticks it.
|
|
474
495
|
* @param severity Optional new severity. 'BLOCKER' converts a comment into a task, 'NORMAL' converts a task back to a regular comment.
|
|
496
|
+
* @param threadResolved Optional thread resolution. `true` resolves the comment thread, `false` reopens it.
|
|
475
497
|
* @returns Promise with updated comment data
|
|
476
498
|
*/
|
|
477
499
|
async updatePullRequestComment(
|
|
@@ -483,6 +505,7 @@ export class BitbucketService {
|
|
|
483
505
|
text?: string,
|
|
484
506
|
state?: 'OPEN' | 'RESOLVED',
|
|
485
507
|
severity?: 'NORMAL' | 'BLOCKER',
|
|
508
|
+
threadResolved?: boolean,
|
|
486
509
|
output: BitbucketMutationOutputMode = 'ack'
|
|
487
510
|
) {
|
|
488
511
|
projectKey = projectKey.toUpperCase();
|
|
@@ -501,6 +524,10 @@ export class BitbucketService {
|
|
|
501
524
|
comment.severity = severity;
|
|
502
525
|
}
|
|
503
526
|
|
|
527
|
+
if (threadResolved !== undefined) {
|
|
528
|
+
comment.threadResolved = threadResolved;
|
|
529
|
+
}
|
|
530
|
+
|
|
504
531
|
const result = await handleApiOperation(
|
|
505
532
|
() => PullRequestsService.updateComment2(
|
|
506
533
|
projectKey,
|
|
@@ -522,6 +549,36 @@ export class BitbucketService {
|
|
|
522
549
|
return result;
|
|
523
550
|
}
|
|
524
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Check whether a pull request can be merged. Read-only: reports conflicts and any
|
|
554
|
+
* merge-check vetoes without changing anything.
|
|
555
|
+
* @param projectKey The project key
|
|
556
|
+
* @param repositorySlug The repository slug
|
|
557
|
+
* @param pullRequestId The pull request ID
|
|
558
|
+
* @returns Promise with the mergeability status
|
|
559
|
+
*/
|
|
560
|
+
async getPullRequestMergeability(projectKey: string, repositorySlug: string, pullRequestId: string) {
|
|
561
|
+
return fetchMergeability(projectKey.toUpperCase(), repositorySlug.toLowerCase(), pullRequestId);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Merge a pull request. Only permitted for repositories (and target branches) the operator
|
|
566
|
+
* allowed through the merge gateway; see merge-gateway.ts. A conflicted or vetoed pull
|
|
567
|
+
* request is refused before the merge request is sent.
|
|
568
|
+
* @param params.version The current pull request version, required for optimistic locking
|
|
569
|
+
* @param params.strategyId Optional merge strategy id (e.g. 'no-ff', 'squash', 'ff')
|
|
570
|
+
* @param params.message Optional merge commit message
|
|
571
|
+
* @param params.gateway The resolved operator merge policy
|
|
572
|
+
* @returns Promise with the merged pull request
|
|
573
|
+
*/
|
|
574
|
+
async mergePullRequest(params: MergePullRequestParams) {
|
|
575
|
+
return mergePullRequest({
|
|
576
|
+
...params,
|
|
577
|
+
projectKey: params.projectKey.toUpperCase(),
|
|
578
|
+
repositorySlug: params.repositorySlug.toLowerCase(),
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
525
582
|
/**
|
|
526
583
|
* Get a user by slug, or search for users by name/email filter
|
|
527
584
|
* @param userSlug Optional exact slug to look up a specific user
|
|
@@ -649,8 +706,8 @@ export class BitbucketService {
|
|
|
649
706
|
|
|
650
707
|
/**
|
|
651
708
|
* Create a pull request
|
|
652
|
-
* @param projectKey The project key
|
|
653
|
-
* @param repositorySlug The repository slug
|
|
709
|
+
* @param projectKey The project key (also used as the destination repository's project)
|
|
710
|
+
* @param repositorySlug The repository slug (also used as the destination repository's slug)
|
|
654
711
|
* @param title The pull request title
|
|
655
712
|
* @param description Optional pull request description
|
|
656
713
|
* @param fromRefId The source branch (e.g., 'refs/heads/feature-branch')
|
|
@@ -658,6 +715,10 @@ export class BitbucketService {
|
|
|
658
715
|
* @param reviewers Optional array of reviewer usernames
|
|
659
716
|
* @param draft Optional flag to create the pull request as a draft
|
|
660
717
|
* @param output Return a compact acknowledgement or the full API response. Defaults to 'ack'.
|
|
718
|
+
* @param fromProjectKey Optional source repository's project key, when it differs from
|
|
719
|
+
* projectKey (fork-based pull requests). Defaults to projectKey.
|
|
720
|
+
* @param fromRepositorySlug Optional source repository's slug, when it differs from
|
|
721
|
+
* repositorySlug (fork-based pull requests). Defaults to repositorySlug.
|
|
661
722
|
* @returns Promise with created pull request data
|
|
662
723
|
*/
|
|
663
724
|
async createPullRequest(
|
|
@@ -669,7 +730,9 @@ export class BitbucketService {
|
|
|
669
730
|
toRefId: string,
|
|
670
731
|
reviewers?: string[],
|
|
671
732
|
draft?: boolean,
|
|
672
|
-
output: BitbucketMutationOutputMode = 'ack'
|
|
733
|
+
output: BitbucketMutationOutputMode = 'ack',
|
|
734
|
+
fromProjectKey?: string,
|
|
735
|
+
fromRepositorySlug?: string
|
|
673
736
|
) {
|
|
674
737
|
projectKey = projectKey.toUpperCase();
|
|
675
738
|
repositorySlug = repositorySlug.toLowerCase();
|
|
@@ -679,9 +742,9 @@ export class BitbucketService {
|
|
|
679
742
|
fromRef: {
|
|
680
743
|
id: fromRefId,
|
|
681
744
|
repository: {
|
|
682
|
-
slug: repositorySlug,
|
|
745
|
+
slug: (fromRepositorySlug ?? repositorySlug).toLowerCase(),
|
|
683
746
|
project: {
|
|
684
|
-
key: projectKey
|
|
747
|
+
key: (fromProjectKey ?? projectKey).toUpperCase()
|
|
685
748
|
}
|
|
686
749
|
}
|
|
687
750
|
},
|
|
@@ -984,6 +1047,12 @@ export const bitbucketToolSchemas = {
|
|
|
984
1047
|
projectKey: z.string().describe("The project key"),
|
|
985
1048
|
repositorySlug: z.string().describe("The repository slug")
|
|
986
1049
|
},
|
|
1050
|
+
getFileContent: {
|
|
1051
|
+
projectKey: z.string().describe("The project key"),
|
|
1052
|
+
repositorySlug: z.string().describe("The repository slug"),
|
|
1053
|
+
path: z.string().describe("Path to the file in the repository (e.g. 'src/index.ts')"),
|
|
1054
|
+
at: z.string().optional().describe("A branch, tag or commit to read the file at (e.g. 'refs/heads/main', 'feature/x', or a commit id). Defaults to the repository's default branch")
|
|
1055
|
+
},
|
|
987
1056
|
getCommits: {
|
|
988
1057
|
projectKey: z.string().describe("The project key"),
|
|
989
1058
|
repositorySlug: z.string().describe("The repository slug"),
|
|
@@ -1035,8 +1104,9 @@ export const bitbucketToolSchemas = {
|
|
|
1035
1104
|
commentId: z.string().describe("The ID of the comment to update"),
|
|
1036
1105
|
version: z.number().describe("The current version of the comment, required for optimistic locking. Get it from bitbucket_getPR_CommentsAndAction or from the response of the original post/update."),
|
|
1037
1106
|
text: z.string().optional().describe("New comment text. Omit to leave unchanged."),
|
|
1038
|
-
state: z.enum(['OPEN', 'RESOLVED']).optional().describe("New state. On a BLOCKER (task) comment, 'RESOLVED' ticks the task and 'OPEN' un-ticks it. This is
|
|
1107
|
+
state: z.enum(['OPEN', 'RESOLVED']).optional().describe("New task state. On a BLOCKER (task) comment, 'RESOLVED' ticks the task and 'OPEN' un-ticks it. This is the task state only — to resolve or reopen the comment thread itself use threadResolved."),
|
|
1039
1108
|
severity: z.enum(['NORMAL', 'BLOCKER']).optional().describe("New severity. Use 'BLOCKER' to convert a comment into a task, 'NORMAL' to convert it back."),
|
|
1109
|
+
threadResolved: z.boolean().optional().describe("Thread resolution. Set true to resolve the comment thread (the 'Resolve' button) or false to reopen it. Independent of state: a BLOCKER comment's task state and its thread resolution can differ."),
|
|
1040
1110
|
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
|
|
1041
1111
|
},
|
|
1042
1112
|
getUser: {
|
|
@@ -1064,15 +1134,17 @@ export const bitbucketToolSchemas = {
|
|
|
1064
1134
|
whitespace: z.string().optional().describe("Optional whitespace flag which can be set to 'ignore-all'")
|
|
1065
1135
|
},
|
|
1066
1136
|
createPullRequest: {
|
|
1067
|
-
projectKey: z.string().describe("The project key"),
|
|
1068
|
-
repositorySlug: z.string().describe("The repository slug"),
|
|
1137
|
+
projectKey: z.string().describe("The destination repository's project key"),
|
|
1138
|
+
repositorySlug: z.string().describe("The destination repository's slug"),
|
|
1069
1139
|
title: z.string().describe("The pull request title"),
|
|
1070
1140
|
description: z.string().optional().describe("The pull request description"),
|
|
1071
1141
|
fromRefId: z.string().describe("The source branch reference ID (e.g., 'refs/heads/feature-branch')"),
|
|
1072
1142
|
toRefId: z.string().describe("The destination branch reference ID (e.g., 'refs/heads/main')"),
|
|
1073
1143
|
draft: z.boolean().optional().describe("If true, the pull request is created as a draft (work-in-progress) and cannot be merged until marked ready."),
|
|
1074
1144
|
reviewers: z.array(z.string()).optional().describe("Optional array of reviewer usernames (use the 'name' field from Bitbucket user objects, not 'slug')"),
|
|
1075
|
-
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
|
|
1145
|
+
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack."),
|
|
1146
|
+
fromProjectKey: z.string().optional().describe("The source repository's project key, when creating a fork-based pull request where the source repository differs from the destination repository (projectKey). Defaults to projectKey."),
|
|
1147
|
+
fromRepositorySlug: z.string().optional().describe("The source repository's slug, when creating a fork-based pull request where the source repository differs from the destination repository (repositorySlug). Defaults to repositorySlug.")
|
|
1076
1148
|
},
|
|
1077
1149
|
updatePullRequest: {
|
|
1078
1150
|
projectKey: z.string().describe("The project key"),
|
|
@@ -1085,6 +1157,20 @@ export const bitbucketToolSchemas = {
|
|
|
1085
1157
|
reviewers: z.array(z.string()).optional().describe("Optional array of reviewer usernames to set (use the 'name' field from Bitbucket user objects, not 'slug')"),
|
|
1086
1158
|
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
|
|
1087
1159
|
},
|
|
1160
|
+
canMergePullRequest: {
|
|
1161
|
+
projectKey: z.string().describe("The project key"),
|
|
1162
|
+
repositorySlug: z.string().describe("The repository slug"),
|
|
1163
|
+
pullRequestId: z.string().describe("The pull request ID")
|
|
1164
|
+
},
|
|
1165
|
+
mergePullRequest: {
|
|
1166
|
+
projectKey: z.string().describe("The project key"),
|
|
1167
|
+
repositorySlug: z.string().describe("The repository slug"),
|
|
1168
|
+
pullRequestId: z.string().describe("The pull request ID"),
|
|
1169
|
+
version: z.number().describe("The current version of the pull request (required for optimistic locking). Fetch it with bitbucket_getPullRequest immediately before merging — the server rejects a stale version, which is what stops a merge of code you have not seen."),
|
|
1170
|
+
strategyId: z.string().optional().describe("Merge strategy id, e.g. 'no-ff', 'ff', 'ff-only', 'rebase-no-ff', 'rebase-ff-only', 'squash', 'squash-ff-only'. Omit to use the strategy configured for the repository."),
|
|
1171
|
+
message: z.string().optional().describe("Commit message for the merge commit. Omit to let Bitbucket generate it."),
|
|
1172
|
+
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
|
|
1173
|
+
},
|
|
1088
1174
|
getRequiredReviewers: {
|
|
1089
1175
|
projectKey: z.string().describe("The project key"),
|
|
1090
1176
|
repositorySlug: z.string().describe("The repository slug"),
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { connectServer, createMcpServer, formatToolResponse, initializeRuntimeConfig } from '@atlassian-dc-mcp/common';
|
|
2
2
|
import { BitbucketService, bitbucketToolSchemas } from './bitbucket-service.js';
|
|
3
3
|
import { getBitbucketRuntimeConfig, getDefaultPageSize } from './config.js';
|
|
4
|
+
import { resolveMergeGateway } from './merge-gateway.js';
|
|
4
5
|
import { createRequire } from 'node:module';
|
|
5
6
|
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
@@ -67,6 +68,16 @@ server.tool(
|
|
|
67
68
|
}
|
|
68
69
|
);
|
|
69
70
|
|
|
71
|
+
server.tool(
|
|
72
|
+
"bitbucket_getFileContent",
|
|
73
|
+
"Get the raw content of a file in a Bitbucket repository, at a branch, tag or commit. Use this to read a source file without cloning the repository. 'at' defaults to the repository's default branch. Pointing 'path' at a directory returns that directory's git tree listing instead of file content.",
|
|
74
|
+
bitbucketToolSchemas.getFileContent,
|
|
75
|
+
async ({ projectKey, repositorySlug, path, at }) => {
|
|
76
|
+
const result = await bitbucketService.getFileContent(projectKey, repositorySlug, path, at);
|
|
77
|
+
return formatToolResponse(result);
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
70
81
|
server.tool(
|
|
71
82
|
"bitbucket_getCommits",
|
|
72
83
|
"Get commits for a Bitbucket repository",
|
|
@@ -139,10 +150,10 @@ server.tool(
|
|
|
139
150
|
|
|
140
151
|
server.tool(
|
|
141
152
|
"bitbucket_updatePullRequestComment",
|
|
142
|
-
"Update an existing pull request comment. Use to edit text, change severity, or
|
|
153
|
+
"Update an existing pull request comment. Use to edit text, change severity, change task state, or resolve/reopen the comment thread. On a BLOCKER (task) comment, state: 'RESOLVED' ticks the task; threadResolved: true toggles the thread-level 'Resolve' button. These are independent and can be set separately. Requires the current 'version' from optimistic locking; fetch it via bitbucket_getPR_CommentsAndAction or use the version returned when the comment was created.",
|
|
143
154
|
bitbucketToolSchemas.updatePullRequestComment,
|
|
144
|
-
async ({ projectKey, repositorySlug, pullRequestId, commentId, version, text, state, severity, output }) => {
|
|
145
|
-
const result = await bitbucketService.updatePullRequestComment(projectKey, repositorySlug, pullRequestId, commentId, version, text, state, severity, output);
|
|
155
|
+
async ({ projectKey, repositorySlug, pullRequestId, commentId, version, text, state, severity, threadResolved, output }) => {
|
|
156
|
+
const result = await bitbucketService.updatePullRequestComment(projectKey, repositorySlug, pullRequestId, commentId, version, text, state, severity, threadResolved, output);
|
|
146
157
|
return formatToolResponse(result);
|
|
147
158
|
}
|
|
148
159
|
);
|
|
@@ -170,10 +181,10 @@ server.tool(
|
|
|
170
181
|
|
|
171
182
|
server.tool(
|
|
172
183
|
"bitbucket_createPullRequest",
|
|
173
|
-
"Create a new pull request in a Bitbucket repository. IMPORTANT: Before creating a PR, use bitbucket_getRequiredReviewers to fetch required reviewers for the source and target branches to ensure the PR is not created without mandatory reviewers.",
|
|
184
|
+
"Create a new pull request in a Bitbucket repository. Supports fork-based pull requests by passing fromProjectKey/fromRepositorySlug when the source repository differs from the destination (projectKey/repositorySlug). IMPORTANT: Before creating a PR, use bitbucket_getRequiredReviewers to fetch required reviewers for the source and target branches to ensure the PR is not created without mandatory reviewers.",
|
|
174
185
|
bitbucketToolSchemas.createPullRequest,
|
|
175
|
-
async ({ projectKey, repositorySlug, title, description, fromRefId, toRefId, reviewers, draft, output }) => {
|
|
176
|
-
const result = await bitbucketService.createPullRequest(projectKey, repositorySlug, title, description, fromRefId, toRefId, reviewers, draft, output);
|
|
186
|
+
async ({ projectKey, repositorySlug, title, description, fromRefId, toRefId, reviewers, draft, output, fromProjectKey, fromRepositorySlug }) => {
|
|
187
|
+
const result = await bitbucketService.createPullRequest(projectKey, repositorySlug, title, description, fromRefId, toRefId, reviewers, draft, output, fromProjectKey, fromRepositorySlug);
|
|
177
188
|
return formatToolResponse(result);
|
|
178
189
|
}
|
|
179
190
|
);
|
|
@@ -188,6 +199,44 @@ server.tool(
|
|
|
188
199
|
}
|
|
189
200
|
);
|
|
190
201
|
|
|
202
|
+
server.tool(
|
|
203
|
+
"bitbucket_canMergePullRequest",
|
|
204
|
+
"Check whether a pull request can be merged. Read-only: returns canMerge, whether the pull request is conflicted, and any merge-check vetoes (e.g. missing approvals, unresolved tasks, failed builds). Use this before bitbucket_mergePullRequest to see what is blocking a merge.",
|
|
205
|
+
bitbucketToolSchemas.canMergePullRequest,
|
|
206
|
+
async ({ projectKey, repositorySlug, pullRequestId }) => {
|
|
207
|
+
const result = await bitbucketService.getPullRequestMergeability(projectKey, repositorySlug, pullRequestId);
|
|
208
|
+
return formatToolResponse(result);
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const mergeGateway = resolveMergeGateway();
|
|
213
|
+
|
|
214
|
+
// Merging lands code on a shared branch and cannot be undone through the API, so the tool is
|
|
215
|
+
// only registered when the operator has explicitly enabled it for specific repositories.
|
|
216
|
+
if (mergeGateway.enabled) {
|
|
217
|
+
const scope = `Allowed on this server: ${mergeGateway.repos.join(', ')}`
|
|
218
|
+
+ (mergeGateway.targetRefs.length > 0 ? `; target branches: ${mergeGateway.targetRefs.join(', ')}` : '')
|
|
219
|
+
+ '.';
|
|
220
|
+
server.tool(
|
|
221
|
+
"bitbucket_mergePullRequest",
|
|
222
|
+
`Merge a pull request. IMPORTANT: you MUST first call bitbucket_getPullRequest to get the current 'version' — it is required for optimistic locking and a stale version is rejected. The merge is refused if the pull request is conflicted or a merge check vetoes it; use bitbucket_canMergePullRequest to inspect blockers first. This operation is not reversible through the API. ${scope}`,
|
|
223
|
+
bitbucketToolSchemas.mergePullRequest,
|
|
224
|
+
async ({ projectKey, repositorySlug, pullRequestId, version, strategyId, message, output }) => {
|
|
225
|
+
const result = await bitbucketService.mergePullRequest({
|
|
226
|
+
projectKey,
|
|
227
|
+
repositorySlug,
|
|
228
|
+
pullRequestId,
|
|
229
|
+
version,
|
|
230
|
+
strategyId,
|
|
231
|
+
message,
|
|
232
|
+
output,
|
|
233
|
+
gateway: mergeGateway,
|
|
234
|
+
});
|
|
235
|
+
return formatToolResponse(result);
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
191
240
|
server.tool(
|
|
192
241
|
"bitbucket_getRequiredReviewers",
|
|
193
242
|
"Get required reviewers for pull request creation. Returns a set of users who are required reviewers for pull requests created from the given source repository and ref to the given target ref in this repository.",
|