@atlassian-dc-mcp/bitbucket 0.28.0 → 0.30.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 +22 -0
- package/README.md +53 -1
- package/build/__tests__/bitbucket-service.test.js +25 -1
- 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 +53 -9
- package/build/bitbucket-service.d.ts.map +1 -1
- package/build/bitbucket-service.js +70 -15
- package/build/bitbucket-service.js.map +1 -1
- package/build/index.js +30 -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 +78 -1
- 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 +75 -14
- package/src/index.ts +45 -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
|
@@ -140,7 +140,8 @@ describe('PR Comment Mapper', () => {
|
|
|
140
140
|
},
|
|
141
141
|
comments: [],
|
|
142
142
|
threadResolved: false,
|
|
143
|
-
state: "OPEN"
|
|
143
|
+
state: "OPEN",
|
|
144
|
+
severity: "NORMAL"
|
|
144
145
|
}
|
|
145
146
|
},
|
|
146
147
|
{
|
|
@@ -160,13 +161,85 @@ describe('PR Comment Mapper', () => {
|
|
|
160
161
|
displayName: "User B"
|
|
161
162
|
},
|
|
162
163
|
commentCount: 1,
|
|
163
|
-
unresolvedCount: 1
|
|
164
|
+
unresolvedCount: 1,
|
|
165
|
+
blockerCount: 0,
|
|
166
|
+
unresolvedBlockerCount: 0
|
|
164
167
|
}
|
|
165
168
|
};
|
|
166
169
|
|
|
167
170
|
expect(result).toEqual(expectedResult);
|
|
168
171
|
});
|
|
169
172
|
|
|
173
|
+
it('should expose BLOCKER severity so tasks are distinguishable from normal comments', () => {
|
|
174
|
+
const blockerResponse: BitbucketPRApiResponse = {
|
|
175
|
+
...validPRResponse,
|
|
176
|
+
values: [
|
|
177
|
+
{
|
|
178
|
+
id: 1001,
|
|
179
|
+
createdDate: 1600000000000,
|
|
180
|
+
user: createUser('reviewer1', 106, 'Reviewer One'),
|
|
181
|
+
action: 'COMMENTED',
|
|
182
|
+
commentAction: 'ADDED',
|
|
183
|
+
comment: createComment({
|
|
184
|
+
id: 2010,
|
|
185
|
+
text: 'Blocking: fix this',
|
|
186
|
+
severity: 'BLOCKER',
|
|
187
|
+
comments: [
|
|
188
|
+
createComment({
|
|
189
|
+
id: 2011,
|
|
190
|
+
text: 'Reply keeps its own severity',
|
|
191
|
+
anchor: undefined,
|
|
192
|
+
severity: 'NORMAL'
|
|
193
|
+
})
|
|
194
|
+
]
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const result = simplifyBitbucketPRComments(blockerResponse) as SimplifiedPRResponse;
|
|
201
|
+
expect(result.activities[0].comment?.severity).toBe('BLOCKER');
|
|
202
|
+
expect(result.activities[0].comment?.comments[0].severity).toBe('NORMAL');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should count blockers and unresolved blockers by task state in the summary', () => {
|
|
206
|
+
const blockerResponse: BitbucketPRApiResponse = {
|
|
207
|
+
...validPRResponse,
|
|
208
|
+
values: [
|
|
209
|
+
openedActivity!,
|
|
210
|
+
{
|
|
211
|
+
id: 1010,
|
|
212
|
+
createdDate: 1600000003000,
|
|
213
|
+
user: createUser('reviewer1', 106, 'Reviewer One'),
|
|
214
|
+
action: 'COMMENTED',
|
|
215
|
+
commentAction: 'ADDED',
|
|
216
|
+
comment: createComment({ id: 2020, text: 'Unresolved task', severity: 'BLOCKER', state: 'OPEN' })
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: 1011,
|
|
220
|
+
createdDate: 1600000004000,
|
|
221
|
+
user: createUser('reviewer1', 106, 'Reviewer One'),
|
|
222
|
+
action: 'COMMENTED',
|
|
223
|
+
commentAction: 'ADDED',
|
|
224
|
+
comment: createComment({ id: 2021, text: 'Ticked task', severity: 'BLOCKER', state: 'RESOLVED' })
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 1012,
|
|
228
|
+
createdDate: 1600000005000,
|
|
229
|
+
user: createUser('reviewer1', 106, 'Reviewer One'),
|
|
230
|
+
action: 'COMMENTED',
|
|
231
|
+
commentAction: 'ADDED',
|
|
232
|
+
comment: createComment({ id: 2022, text: 'Just a note', severity: 'NORMAL', state: 'OPEN' })
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const result = simplifyBitbucketPRComments(blockerResponse) as SimplifiedPRResponse;
|
|
238
|
+
expect(result.summary.commentCount).toBe(3);
|
|
239
|
+
expect(result.summary.blockerCount).toBe(2);
|
|
240
|
+
expect(result.summary.unresolvedBlockerCount).toBe(1);
|
|
241
|
+
});
|
|
242
|
+
|
|
170
243
|
it('should preserve nested replies recursively', () => {
|
|
171
244
|
const threadedResponse: BitbucketPRApiResponse = {
|
|
172
245
|
...validPRResponse,
|
|
@@ -220,11 +293,13 @@ describe('PR Comment Mapper', () => {
|
|
|
220
293
|
createdDate: 1600000000000,
|
|
221
294
|
comments: [],
|
|
222
295
|
threadResolved: false,
|
|
223
|
-
state: "OPEN"
|
|
296
|
+
state: "OPEN",
|
|
297
|
+
severity: "NORMAL"
|
|
224
298
|
}
|
|
225
299
|
],
|
|
226
300
|
threadResolved: false,
|
|
227
|
-
state: "OPEN"
|
|
301
|
+
state: "OPEN",
|
|
302
|
+
severity: "NORMAL"
|
|
228
303
|
}
|
|
229
304
|
]);
|
|
230
305
|
});
|
|
@@ -294,7 +369,8 @@ describe('PR Comment Mapper', () => {
|
|
|
294
369
|
},
|
|
295
370
|
comments: [],
|
|
296
371
|
threadResolved: false,
|
|
297
|
-
state: 'OPEN'
|
|
372
|
+
state: 'OPEN',
|
|
373
|
+
severity: 'NORMAL'
|
|
298
374
|
}
|
|
299
375
|
}
|
|
300
376
|
]);
|
|
@@ -355,11 +431,13 @@ describe('PR Comment Mapper', () => {
|
|
|
355
431
|
createdDate: 1600000000000,
|
|
356
432
|
comments: [],
|
|
357
433
|
threadResolved: true,
|
|
358
|
-
state: 'OPEN'
|
|
434
|
+
state: 'OPEN',
|
|
435
|
+
severity: 'NORMAL'
|
|
359
436
|
}
|
|
360
437
|
],
|
|
361
438
|
threadResolved: true,
|
|
362
|
-
state: 'OPEN'
|
|
439
|
+
state: 'OPEN',
|
|
440
|
+
severity: 'NORMAL'
|
|
363
441
|
});
|
|
364
442
|
expect(getCommentSummary(resolvedThreadResponse, { includeResolved: true })).toEqual([
|
|
365
443
|
'User A on config.yml:6: Resolved thread root'
|
|
@@ -402,7 +480,8 @@ describe('PR Comment Mapper', () => {
|
|
|
402
480
|
createdDate: 1600000000000,
|
|
403
481
|
comments: [],
|
|
404
482
|
threadResolved: false,
|
|
405
|
-
state: "OPEN"
|
|
483
|
+
state: "OPEN",
|
|
484
|
+
severity: "NORMAL"
|
|
406
485
|
}
|
|
407
486
|
]);
|
|
408
487
|
});
|
|
@@ -447,7 +526,8 @@ describe('PR Comment Mapper', () => {
|
|
|
447
526
|
createdDate: 1600000000000,
|
|
448
527
|
comments: [],
|
|
449
528
|
threadResolved: false,
|
|
450
|
-
state: "OPEN"
|
|
529
|
+
state: "OPEN",
|
|
530
|
+
severity: "NORMAL"
|
|
451
531
|
}
|
|
452
532
|
]);
|
|
453
533
|
|
|
@@ -465,7 +545,9 @@ describe('PR Comment Mapper', () => {
|
|
|
465
545
|
summary: {
|
|
466
546
|
totalActivities: 0,
|
|
467
547
|
commentCount: 0,
|
|
468
|
-
unresolvedCount: 0
|
|
548
|
+
unresolvedCount: 0,
|
|
549
|
+
blockerCount: 0,
|
|
550
|
+
unresolvedBlockerCount: 0
|
|
469
551
|
}
|
|
470
552
|
};
|
|
471
553
|
|
|
@@ -489,7 +571,9 @@ describe('PR Comment Mapper', () => {
|
|
|
489
571
|
summary: {
|
|
490
572
|
totalActivities: 0,
|
|
491
573
|
commentCount: 0,
|
|
492
|
-
unresolvedCount: 0
|
|
574
|
+
unresolvedCount: 0,
|
|
575
|
+
blockerCount: 0,
|
|
576
|
+
unresolvedBlockerCount: 0
|
|
493
577
|
}
|
|
494
578
|
};
|
|
495
579
|
|
|
@@ -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,
|
|
@@ -460,18 +461,20 @@ export class BitbucketService {
|
|
|
460
461
|
}
|
|
461
462
|
|
|
462
463
|
/**
|
|
463
|
-
* Update an existing pull request comment. Use this to edit text, change severity,
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
464
|
+
* Update an existing pull request comment. Use this to edit text, change severity, change state, or
|
|
465
|
+
* resolve/reopen a comment thread. State and thread resolution are independent: `state` is the task
|
|
466
|
+
* state of a BLOCKER comment ('RESOLVED' ticks the task, 'OPEN' un-ticks it), while `threadResolved`
|
|
467
|
+
* toggles the thread-level "Resolve" button. A root BLOCKER comment can hold these independently, so
|
|
468
|
+
* they are sent as separate fields.
|
|
467
469
|
* @param projectKey The project key
|
|
468
470
|
* @param repositorySlug The repository slug
|
|
469
471
|
* @param pullRequestId The pull request ID
|
|
470
472
|
* @param commentId The comment ID to update
|
|
471
473
|
* @param version The current version of the comment (required for optimistic locking)
|
|
472
474
|
* @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.
|
|
475
|
+
* @param state Optional new task state. On a BLOCKER comment, 'RESOLVED' ticks the task and 'OPEN' un-ticks it.
|
|
474
476
|
* @param severity Optional new severity. 'BLOCKER' converts a comment into a task, 'NORMAL' converts a task back to a regular comment.
|
|
477
|
+
* @param threadResolved Optional thread resolution. `true` resolves the comment thread, `false` reopens it.
|
|
475
478
|
* @returns Promise with updated comment data
|
|
476
479
|
*/
|
|
477
480
|
async updatePullRequestComment(
|
|
@@ -483,6 +486,7 @@ export class BitbucketService {
|
|
|
483
486
|
text?: string,
|
|
484
487
|
state?: 'OPEN' | 'RESOLVED',
|
|
485
488
|
severity?: 'NORMAL' | 'BLOCKER',
|
|
489
|
+
threadResolved?: boolean,
|
|
486
490
|
output: BitbucketMutationOutputMode = 'ack'
|
|
487
491
|
) {
|
|
488
492
|
projectKey = projectKey.toUpperCase();
|
|
@@ -501,6 +505,10 @@ export class BitbucketService {
|
|
|
501
505
|
comment.severity = severity;
|
|
502
506
|
}
|
|
503
507
|
|
|
508
|
+
if (threadResolved !== undefined) {
|
|
509
|
+
comment.threadResolved = threadResolved;
|
|
510
|
+
}
|
|
511
|
+
|
|
504
512
|
const result = await handleApiOperation(
|
|
505
513
|
() => PullRequestsService.updateComment2(
|
|
506
514
|
projectKey,
|
|
@@ -522,6 +530,36 @@ export class BitbucketService {
|
|
|
522
530
|
return result;
|
|
523
531
|
}
|
|
524
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Check whether a pull request can be merged. Read-only: reports conflicts and any
|
|
535
|
+
* merge-check vetoes without changing anything.
|
|
536
|
+
* @param projectKey The project key
|
|
537
|
+
* @param repositorySlug The repository slug
|
|
538
|
+
* @param pullRequestId The pull request ID
|
|
539
|
+
* @returns Promise with the mergeability status
|
|
540
|
+
*/
|
|
541
|
+
async getPullRequestMergeability(projectKey: string, repositorySlug: string, pullRequestId: string) {
|
|
542
|
+
return fetchMergeability(projectKey.toUpperCase(), repositorySlug.toLowerCase(), pullRequestId);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Merge a pull request. Only permitted for repositories (and target branches) the operator
|
|
547
|
+
* allowed through the merge gateway; see merge-gateway.ts. A conflicted or vetoed pull
|
|
548
|
+
* request is refused before the merge request is sent.
|
|
549
|
+
* @param params.version The current pull request version, required for optimistic locking
|
|
550
|
+
* @param params.strategyId Optional merge strategy id (e.g. 'no-ff', 'squash', 'ff')
|
|
551
|
+
* @param params.message Optional merge commit message
|
|
552
|
+
* @param params.gateway The resolved operator merge policy
|
|
553
|
+
* @returns Promise with the merged pull request
|
|
554
|
+
*/
|
|
555
|
+
async mergePullRequest(params: MergePullRequestParams) {
|
|
556
|
+
return mergePullRequest({
|
|
557
|
+
...params,
|
|
558
|
+
projectKey: params.projectKey.toUpperCase(),
|
|
559
|
+
repositorySlug: params.repositorySlug.toLowerCase(),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
525
563
|
/**
|
|
526
564
|
* Get a user by slug, or search for users by name/email filter
|
|
527
565
|
* @param userSlug Optional exact slug to look up a specific user
|
|
@@ -649,8 +687,8 @@ export class BitbucketService {
|
|
|
649
687
|
|
|
650
688
|
/**
|
|
651
689
|
* Create a pull request
|
|
652
|
-
* @param projectKey The project key
|
|
653
|
-
* @param repositorySlug The repository slug
|
|
690
|
+
* @param projectKey The project key (also used as the destination repository's project)
|
|
691
|
+
* @param repositorySlug The repository slug (also used as the destination repository's slug)
|
|
654
692
|
* @param title The pull request title
|
|
655
693
|
* @param description Optional pull request description
|
|
656
694
|
* @param fromRefId The source branch (e.g., 'refs/heads/feature-branch')
|
|
@@ -658,6 +696,10 @@ export class BitbucketService {
|
|
|
658
696
|
* @param reviewers Optional array of reviewer usernames
|
|
659
697
|
* @param draft Optional flag to create the pull request as a draft
|
|
660
698
|
* @param output Return a compact acknowledgement or the full API response. Defaults to 'ack'.
|
|
699
|
+
* @param fromProjectKey Optional source repository's project key, when it differs from
|
|
700
|
+
* projectKey (fork-based pull requests). Defaults to projectKey.
|
|
701
|
+
* @param fromRepositorySlug Optional source repository's slug, when it differs from
|
|
702
|
+
* repositorySlug (fork-based pull requests). Defaults to repositorySlug.
|
|
661
703
|
* @returns Promise with created pull request data
|
|
662
704
|
*/
|
|
663
705
|
async createPullRequest(
|
|
@@ -669,7 +711,9 @@ export class BitbucketService {
|
|
|
669
711
|
toRefId: string,
|
|
670
712
|
reviewers?: string[],
|
|
671
713
|
draft?: boolean,
|
|
672
|
-
output: BitbucketMutationOutputMode = 'ack'
|
|
714
|
+
output: BitbucketMutationOutputMode = 'ack',
|
|
715
|
+
fromProjectKey?: string,
|
|
716
|
+
fromRepositorySlug?: string
|
|
673
717
|
) {
|
|
674
718
|
projectKey = projectKey.toUpperCase();
|
|
675
719
|
repositorySlug = repositorySlug.toLowerCase();
|
|
@@ -679,9 +723,9 @@ export class BitbucketService {
|
|
|
679
723
|
fromRef: {
|
|
680
724
|
id: fromRefId,
|
|
681
725
|
repository: {
|
|
682
|
-
slug: repositorySlug,
|
|
726
|
+
slug: (fromRepositorySlug ?? repositorySlug).toLowerCase(),
|
|
683
727
|
project: {
|
|
684
|
-
key: projectKey
|
|
728
|
+
key: (fromProjectKey ?? projectKey).toUpperCase()
|
|
685
729
|
}
|
|
686
730
|
}
|
|
687
731
|
},
|
|
@@ -1035,8 +1079,9 @@ export const bitbucketToolSchemas = {
|
|
|
1035
1079
|
commentId: z.string().describe("The ID of the comment to update"),
|
|
1036
1080
|
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
1081
|
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
|
|
1082
|
+
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
1083
|
severity: z.enum(['NORMAL', 'BLOCKER']).optional().describe("New severity. Use 'BLOCKER' to convert a comment into a task, 'NORMAL' to convert it back."),
|
|
1084
|
+
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
1085
|
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
|
|
1041
1086
|
},
|
|
1042
1087
|
getUser: {
|
|
@@ -1064,15 +1109,17 @@ export const bitbucketToolSchemas = {
|
|
|
1064
1109
|
whitespace: z.string().optional().describe("Optional whitespace flag which can be set to 'ignore-all'")
|
|
1065
1110
|
},
|
|
1066
1111
|
createPullRequest: {
|
|
1067
|
-
projectKey: z.string().describe("The project key"),
|
|
1068
|
-
repositorySlug: z.string().describe("The repository slug"),
|
|
1112
|
+
projectKey: z.string().describe("The destination repository's project key"),
|
|
1113
|
+
repositorySlug: z.string().describe("The destination repository's slug"),
|
|
1069
1114
|
title: z.string().describe("The pull request title"),
|
|
1070
1115
|
description: z.string().optional().describe("The pull request description"),
|
|
1071
1116
|
fromRefId: z.string().describe("The source branch reference ID (e.g., 'refs/heads/feature-branch')"),
|
|
1072
1117
|
toRefId: z.string().describe("The destination branch reference ID (e.g., 'refs/heads/main')"),
|
|
1073
1118
|
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
1119
|
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.")
|
|
1120
|
+
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack."),
|
|
1121
|
+
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."),
|
|
1122
|
+
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
1123
|
},
|
|
1077
1124
|
updatePullRequest: {
|
|
1078
1125
|
projectKey: z.string().describe("The project key"),
|
|
@@ -1085,6 +1132,20 @@ export const bitbucketToolSchemas = {
|
|
|
1085
1132
|
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
1133
|
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
|
|
1087
1134
|
},
|
|
1135
|
+
canMergePullRequest: {
|
|
1136
|
+
projectKey: z.string().describe("The project key"),
|
|
1137
|
+
repositorySlug: z.string().describe("The repository slug"),
|
|
1138
|
+
pullRequestId: z.string().describe("The pull request ID")
|
|
1139
|
+
},
|
|
1140
|
+
mergePullRequest: {
|
|
1141
|
+
projectKey: z.string().describe("The project key"),
|
|
1142
|
+
repositorySlug: z.string().describe("The repository slug"),
|
|
1143
|
+
pullRequestId: z.string().describe("The pull request ID"),
|
|
1144
|
+
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."),
|
|
1145
|
+
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."),
|
|
1146
|
+
message: z.string().optional().describe("Commit message for the merge commit. Omit to let Bitbucket generate it."),
|
|
1147
|
+
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
|
|
1148
|
+
},
|
|
1088
1149
|
getRequiredReviewers: {
|
|
1089
1150
|
projectKey: z.string().describe("The project key"),
|
|
1090
1151
|
repositorySlug: z.string().describe("The repository slug"),
|