@atlassian-dc-mcp/bitbucket 0.33.0 → 0.35.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 +20 -0
- package/README.md +55 -1
- package/build/__tests__/bitbucket-service.test.js +65 -2
- package/build/__tests__/bitbucket-service.test.js.map +1 -1
- package/build/__tests__/pr-merge.test.js +5 -4
- package/build/__tests__/pr-merge.test.js.map +1 -1
- package/build/__tests__/pr-state.test.d.ts +2 -0
- package/build/__tests__/pr-state.test.d.ts.map +1 -0
- package/build/__tests__/pr-state.test.js +118 -0
- package/build/__tests__/pr-state.test.js.map +1 -0
- package/build/__tests__/repo-gateway.test.d.ts +2 -0
- package/build/__tests__/repo-gateway.test.d.ts.map +1 -0
- package/build/__tests__/{merge-gateway.test.js → repo-gateway.test.js} +70 -10
- package/build/__tests__/repo-gateway.test.js.map +1 -0
- package/build/bitbucket-service.d.ts +46 -0
- package/build/bitbucket-service.d.ts.map +1 -1
- package/build/bitbucket-service.js +62 -0
- package/build/bitbucket-service.js.map +1 -1
- package/build/index.js +33 -1
- package/build/index.js.map +1 -1
- package/build/pr-merge.d.ts +2 -2
- package/build/pr-merge.d.ts.map +1 -1
- package/build/pr-merge.js +2 -2
- package/build/pr-merge.js.map +1 -1
- package/build/pr-state.d.ts +33 -0
- package/build/pr-state.d.ts.map +1 -0
- package/build/pr-state.js +37 -0
- package/build/pr-state.js.map +1 -0
- package/build/repo-gateway.d.ts +57 -0
- package/build/repo-gateway.d.ts.map +1 -0
- package/build/repo-gateway.js +122 -0
- package/build/repo-gateway.js.map +1 -0
- package/package.json +3 -3
- package/src/__tests__/bitbucket-service.test.ts +111 -2
- package/src/__tests__/pr-merge.test.ts +5 -5
- package/src/__tests__/pr-state.test.ts +150 -0
- package/src/__tests__/{merge-gateway.test.ts → repo-gateway.test.ts} +83 -11
- package/src/bitbucket-service.ts +73 -0
- package/src/index.ts +52 -1
- package/src/pr-merge.ts +3 -3
- package/src/pr-state.ts +68 -0
- package/src/repo-gateway.ts +183 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/build/__tests__/merge-gateway.test.d.ts +0 -2
- package/build/__tests__/merge-gateway.test.d.ts.map +0 -1
- package/build/__tests__/merge-gateway.test.js.map +0 -1
- package/build/merge-gateway.d.ts +0 -34
- package/build/merge-gateway.d.ts.map +0 -1
- package/build/merge-gateway.js +0 -86
- package/build/merge-gateway.js.map +0 -1
- package/src/merge-gateway.ts +0 -132
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PullRequestsService } from '../bitbucket-client/index.js';
|
|
2
2
|
import { fetchMergeability, mergePullRequest } from '../pr-merge.js';
|
|
3
|
-
import type
|
|
3
|
+
import { MERGE_POLICY, type RepoGateway } from '../repo-gateway.js';
|
|
4
4
|
|
|
5
5
|
jest.mock('../bitbucket-client/index.js', () => ({
|
|
6
6
|
PullRequestsService: {
|
|
@@ -15,7 +15,7 @@ const canMerge = PullRequestsService.canMerge as jest.Mock;
|
|
|
15
15
|
const merge = PullRequestsService.merge as jest.Mock;
|
|
16
16
|
const getPullRequest = PullRequestsService.get3 as jest.Mock;
|
|
17
17
|
|
|
18
|
-
const OPEN_GATEWAY:
|
|
18
|
+
const OPEN_GATEWAY: RepoGateway = { enabled: true, repos: ['PROJ/demo'], targetRefs: [], policy: MERGE_POLICY };
|
|
19
19
|
const CLEAN = { canMerge: true, conflicted: false, outcome: 'CLEAN', vetoes: [] };
|
|
20
20
|
const MERGED_PR = {
|
|
21
21
|
id: 42,
|
|
@@ -111,7 +111,7 @@ describe('mergePullRequest', () => {
|
|
|
111
111
|
it('refuses without any request when merging is disabled', async () => {
|
|
112
112
|
const result = await mergePullRequest({
|
|
113
113
|
...baseParams,
|
|
114
|
-
gateway: { enabled: false, repos: [], targetRefs: [] },
|
|
114
|
+
gateway: { enabled: false, repos: [], targetRefs: [], policy: MERGE_POLICY },
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
expect(result.success).toBe(false);
|
|
@@ -137,7 +137,7 @@ describe('mergePullRequest', () => {
|
|
|
137
137
|
|
|
138
138
|
const result = await mergePullRequest({
|
|
139
139
|
...baseParams,
|
|
140
|
-
gateway: { enabled: true, repos: ['PROJ/demo'], targetRefs: ['refs/heads/develop'] },
|
|
140
|
+
gateway: { enabled: true, repos: ['PROJ/demo'], targetRefs: ['refs/heads/develop'], policy: MERGE_POLICY },
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
expect(result.success).toBe(false);
|
|
@@ -153,7 +153,7 @@ describe('mergePullRequest', () => {
|
|
|
153
153
|
|
|
154
154
|
const result = await mergePullRequest({
|
|
155
155
|
...baseParams,
|
|
156
|
-
gateway: { enabled: true, repos: ['PROJ/demo'], targetRefs: ['refs/heads/release/*'] },
|
|
156
|
+
gateway: { enabled: true, repos: ['PROJ/demo'], targetRefs: ['refs/heads/release/*'], policy: MERGE_POLICY },
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
expect(result.success).toBe(true);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { PullRequestsService } from '../bitbucket-client/index.js';
|
|
2
|
+
import { declinePullRequest, reopenPullRequest } from '../pr-state.js';
|
|
3
|
+
import { DECLINE_POLICY, type RepoGateway } from '../repo-gateway.js';
|
|
4
|
+
|
|
5
|
+
jest.mock('../bitbucket-client/index.js', () => ({
|
|
6
|
+
PullRequestsService: {
|
|
7
|
+
decline: jest.fn(),
|
|
8
|
+
reopen: jest.fn(),
|
|
9
|
+
},
|
|
10
|
+
OpenAPI: { BASE: '', TOKEN: '', VERSION: '' },
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const decline = PullRequestsService.decline as jest.Mock;
|
|
14
|
+
const reopen = PullRequestsService.reopen as jest.Mock;
|
|
15
|
+
|
|
16
|
+
const OPEN_GATEWAY: RepoGateway = {
|
|
17
|
+
enabled: true,
|
|
18
|
+
repos: ['PROJ/demo'],
|
|
19
|
+
targetRefs: [],
|
|
20
|
+
policy: DECLINE_POLICY,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DECLINED_PR = {
|
|
24
|
+
id: 42,
|
|
25
|
+
version: 3,
|
|
26
|
+
title: 'Add decline tool',
|
|
27
|
+
state: 'DECLINED',
|
|
28
|
+
fromRef: { id: 'refs/heads/feature' },
|
|
29
|
+
toRef: { id: 'refs/heads/develop' },
|
|
30
|
+
reviewers: [],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const REOPENED_PR = { ...DECLINED_PR, version: 4, state: 'OPEN' };
|
|
34
|
+
|
|
35
|
+
const baseParams = {
|
|
36
|
+
projectKey: 'PROJ',
|
|
37
|
+
repositorySlug: 'demo',
|
|
38
|
+
pullRequestId: '42',
|
|
39
|
+
version: 2,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
describe('declinePullRequest', () => {
|
|
43
|
+
beforeEach(() => jest.clearAllMocks());
|
|
44
|
+
|
|
45
|
+
it('declines with the version as an optimistic lock and returns a compact ack', async () => {
|
|
46
|
+
decline.mockResolvedValue(DECLINED_PR);
|
|
47
|
+
|
|
48
|
+
const result = await declinePullRequest({ ...baseParams, gateway: OPEN_GATEWAY });
|
|
49
|
+
|
|
50
|
+
expect(decline).toHaveBeenCalledWith('PROJ', '42', 'demo', '2', { version: 2 });
|
|
51
|
+
expect(result.success).toBe(true);
|
|
52
|
+
expect(result.data).toEqual({
|
|
53
|
+
id: 42,
|
|
54
|
+
version: 3,
|
|
55
|
+
title: 'Add decline tool',
|
|
56
|
+
state: 'DECLINED',
|
|
57
|
+
fromRefId: 'refs/heads/feature',
|
|
58
|
+
toRefId: 'refs/heads/develop',
|
|
59
|
+
reviewerCount: 0,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('posts the reason atomically with the decline when a comment is given', async () => {
|
|
64
|
+
decline.mockResolvedValue(DECLINED_PR);
|
|
65
|
+
|
|
66
|
+
await declinePullRequest({
|
|
67
|
+
...baseParams,
|
|
68
|
+
gateway: OPEN_GATEWAY,
|
|
69
|
+
comment: 'Superseded by PR 99.',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(decline).toHaveBeenCalledWith('PROJ', '42', 'demo', '2', {
|
|
73
|
+
version: 2,
|
|
74
|
+
comment: 'Superseded by PR 99.',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('can return the full payload', async () => {
|
|
79
|
+
decline.mockResolvedValue(DECLINED_PR);
|
|
80
|
+
|
|
81
|
+
const result = await declinePullRequest({ ...baseParams, gateway: OPEN_GATEWAY, output: 'full' });
|
|
82
|
+
|
|
83
|
+
expect(result.data).toBe(DECLINED_PR);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('refuses without any request when declining is disabled', async () => {
|
|
87
|
+
const result = await declinePullRequest({
|
|
88
|
+
...baseParams,
|
|
89
|
+
gateway: { enabled: false, repos: [], targetRefs: [], policy: DECLINE_POLICY },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result.success).toBe(false);
|
|
93
|
+
expect(result.error).toMatch(/Declining pull requests is disabled on this server/);
|
|
94
|
+
expect(decline).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('refuses a repository outside the allowed list without any request', async () => {
|
|
98
|
+
const result = await declinePullRequest({
|
|
99
|
+
...baseParams,
|
|
100
|
+
repositorySlug: 'other-repo',
|
|
101
|
+
gateway: OPEN_GATEWAY,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(result.success).toBe(false);
|
|
105
|
+
expect(result.error).toMatch(/Declining is not allowed in PROJ\/other-repo/);
|
|
106
|
+
expect(decline).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('surfaces a server-side failure such as a stale version', async () => {
|
|
110
|
+
decline.mockRejectedValue({ status: 409, statusText: 'Conflict', body: { errors: [{ message: 'stale' }] } });
|
|
111
|
+
|
|
112
|
+
const result = await declinePullRequest({ ...baseParams, gateway: OPEN_GATEWAY });
|
|
113
|
+
|
|
114
|
+
expect(result.success).toBe(false);
|
|
115
|
+
expect(result.error).toBe('Error declining pull request: 409 Conflict');
|
|
116
|
+
expect(result.details).toEqual({ errors: [{ message: 'stale' }] });
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('reopenPullRequest', () => {
|
|
121
|
+
beforeEach(() => jest.clearAllMocks());
|
|
122
|
+
|
|
123
|
+
it('reopens with the version as an optimistic lock and returns a compact ack', async () => {
|
|
124
|
+
reopen.mockResolvedValue(REOPENED_PR);
|
|
125
|
+
|
|
126
|
+
const result = await reopenPullRequest(baseParams);
|
|
127
|
+
|
|
128
|
+
expect(reopen).toHaveBeenCalledWith('PROJ', '42', 'demo', '2', { version: 2 });
|
|
129
|
+
expect(result.success).toBe(true);
|
|
130
|
+
expect(result.data).toMatchObject({ id: 42, version: 4, state: 'OPEN' });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('can return the full payload', async () => {
|
|
134
|
+
reopen.mockResolvedValue(REOPENED_PR);
|
|
135
|
+
|
|
136
|
+
const result = await reopenPullRequest({ ...baseParams, output: 'full' });
|
|
137
|
+
|
|
138
|
+
expect(result.data).toBe(REOPENED_PR);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('surfaces a server-side failure such as a pull request that is not declined', async () => {
|
|
142
|
+
reopen.mockRejectedValue({ status: 409, statusText: 'Conflict', body: { errors: [{ message: 'not declined' }] } });
|
|
143
|
+
|
|
144
|
+
const result = await reopenPullRequest(baseParams);
|
|
145
|
+
|
|
146
|
+
expect(result.success).toBe(false);
|
|
147
|
+
expect(result.error).toBe('Error reopening pull request: 409 Conflict');
|
|
148
|
+
expect(result.details).toEqual({ errors: [{ message: 'not declined' }] });
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
DECLINE_POLICY,
|
|
3
|
+
MERGE_POLICY,
|
|
4
|
+
assertRepoOperationAllowed,
|
|
3
5
|
assertTargetRefMergeAllowed,
|
|
6
|
+
resolveDeclineGateway,
|
|
4
7
|
resolveMergeGateway,
|
|
5
|
-
|
|
8
|
+
type RepoGateway,
|
|
9
|
+
} from '../repo-gateway.js';
|
|
6
10
|
|
|
7
11
|
const silentWarn = () => undefined;
|
|
8
12
|
|
|
13
|
+
const disabled = (policy = MERGE_POLICY): RepoGateway => ({
|
|
14
|
+
enabled: false,
|
|
15
|
+
repos: [],
|
|
16
|
+
targetRefs: [],
|
|
17
|
+
policy,
|
|
18
|
+
});
|
|
19
|
+
|
|
9
20
|
describe('resolveMergeGateway', () => {
|
|
10
21
|
it('is disabled by default', () => {
|
|
11
22
|
const gateway = resolveMergeGateway({ env: {}, warn: silentWarn });
|
|
@@ -63,40 +74,101 @@ describe('resolveMergeGateway', () => {
|
|
|
63
74
|
});
|
|
64
75
|
});
|
|
65
76
|
|
|
66
|
-
describe('
|
|
77
|
+
describe('resolveDeclineGateway', () => {
|
|
78
|
+
it('is disabled by default', () => {
|
|
79
|
+
const gateway = resolveDeclineGateway({ env: {}, warn: silentWarn });
|
|
80
|
+
expect(gateway.enabled).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('reads its own environment variables rather than the merge ones', () => {
|
|
84
|
+
const gateway = resolveDeclineGateway({
|
|
85
|
+
env: {
|
|
86
|
+
BITBUCKET_DECLINE_ENABLED: 'true',
|
|
87
|
+
BITBUCKET_DECLINE_ALLOWED_REPOS: 'proj/Demo, OTHER/*',
|
|
88
|
+
},
|
|
89
|
+
warn: silentWarn,
|
|
90
|
+
});
|
|
91
|
+
expect(gateway.enabled).toBe(true);
|
|
92
|
+
expect(gateway.repos).toEqual(['PROJ/demo', 'OTHER/*']);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('stays disabled when only merging was enabled', () => {
|
|
96
|
+
const gateway = resolveDeclineGateway({
|
|
97
|
+
env: { BITBUCKET_MERGE_ENABLED: 'true', BITBUCKET_MERGE_ALLOWED_REPOS: 'PROJ/demo' },
|
|
98
|
+
warn: silentWarn,
|
|
99
|
+
});
|
|
100
|
+
expect(gateway.enabled).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('never restricts target refs, even when the merge target refs are configured', () => {
|
|
104
|
+
const gateway = resolveDeclineGateway({
|
|
105
|
+
env: {
|
|
106
|
+
BITBUCKET_DECLINE_ENABLED: 'true',
|
|
107
|
+
BITBUCKET_DECLINE_ALLOWED_REPOS: 'PROJ/demo',
|
|
108
|
+
BITBUCKET_MERGE_ALLOWED_TARGET_REFS: 'develop',
|
|
109
|
+
},
|
|
110
|
+
warn: silentWarn,
|
|
111
|
+
});
|
|
112
|
+
expect(gateway.targetRefs).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('names the decline environment variables when enabled with no valid repository', () => {
|
|
116
|
+
const warnings: string[] = [];
|
|
117
|
+
resolveDeclineGateway({ env: { BITBUCKET_DECLINE_ENABLED: 'true' }, warn: message => warnings.push(message) });
|
|
118
|
+
expect(warnings).toHaveLength(1);
|
|
119
|
+
expect(warnings[0]).toContain('BITBUCKET_DECLINE_ALLOWED_REPOS');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('assertRepoOperationAllowed', () => {
|
|
67
124
|
const gateway = resolveMergeGateway({
|
|
68
125
|
env: { BITBUCKET_MERGE_ENABLED: 'true', BITBUCKET_MERGE_ALLOWED_REPOS: 'PROJ/demo, OTHER/*' },
|
|
69
126
|
warn: silentWarn,
|
|
70
127
|
});
|
|
71
128
|
|
|
72
|
-
it('refuses everything when
|
|
73
|
-
expect(() =>
|
|
129
|
+
it('refuses everything when the operation is disabled', () => {
|
|
130
|
+
expect(() => assertRepoOperationAllowed(disabled(), 'PROJ', 'demo'))
|
|
74
131
|
.toThrow(/disabled on this server/);
|
|
75
132
|
});
|
|
76
133
|
|
|
134
|
+
it('names the environment variables of its own operation when disabled', () => {
|
|
135
|
+
expect(() => assertRepoOperationAllowed(disabled(DECLINE_POLICY), 'PROJ', 'demo'))
|
|
136
|
+
.toThrow(/Declining pull requests is disabled on this server\. Enable it with BITBUCKET_DECLINE_ENABLED and list the allowed repositories in BITBUCKET_DECLINE_ALLOWED_REPOS\./);
|
|
137
|
+
});
|
|
138
|
+
|
|
77
139
|
it('allows an exact repository regardless of the casing used by the caller', () => {
|
|
78
|
-
expect(() =>
|
|
140
|
+
expect(() => assertRepoOperationAllowed(gateway, 'proj', 'DEMO')).not.toThrow();
|
|
79
141
|
});
|
|
80
142
|
|
|
81
143
|
it('allows any repository in a wildcard project', () => {
|
|
82
|
-
expect(() =>
|
|
144
|
+
expect(() => assertRepoOperationAllowed(gateway, 'OTHER', 'anything')).not.toThrow();
|
|
83
145
|
});
|
|
84
146
|
|
|
85
147
|
it('refuses a repository outside the allowed list', () => {
|
|
86
|
-
expect(() =>
|
|
148
|
+
expect(() => assertRepoOperationAllowed(gateway, 'PROJ', 'other-repo')).toThrow(/Merging is not allowed in PROJ\/other-repo/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('names the refused operation in the message', () => {
|
|
152
|
+
const declineGateway = resolveDeclineGateway({
|
|
153
|
+
env: { BITBUCKET_DECLINE_ENABLED: 'true', BITBUCKET_DECLINE_ALLOWED_REPOS: 'PROJ/demo' },
|
|
154
|
+
warn: silentWarn,
|
|
155
|
+
});
|
|
156
|
+
expect(() => assertRepoOperationAllowed(declineGateway, 'PROJ', 'other-repo'))
|
|
157
|
+
.toThrow(/Declining is not allowed in PROJ\/other-repo/);
|
|
87
158
|
});
|
|
88
159
|
|
|
89
160
|
it('does not treat a wildcard project as a prefix of another project', () => {
|
|
90
|
-
expect(() =>
|
|
161
|
+
expect(() => assertRepoOperationAllowed(gateway, 'OTHERS', 'demo')).toThrow(/not allowed/);
|
|
91
162
|
});
|
|
92
163
|
});
|
|
93
164
|
|
|
94
165
|
describe('assertTargetRefMergeAllowed', () => {
|
|
95
|
-
const unrestricted = { enabled: true, repos: ['PROJ/demo'], targetRefs: [] };
|
|
96
|
-
const restricted = {
|
|
166
|
+
const unrestricted: RepoGateway = { enabled: true, repos: ['PROJ/demo'], targetRefs: [], policy: MERGE_POLICY };
|
|
167
|
+
const restricted: RepoGateway = {
|
|
97
168
|
enabled: true,
|
|
98
169
|
repos: ['PROJ/demo'],
|
|
99
170
|
targetRefs: ['refs/heads/develop', 'refs/heads/release/*'],
|
|
171
|
+
policy: MERGE_POLICY,
|
|
100
172
|
};
|
|
101
173
|
|
|
102
174
|
it('allows any ref when no target-ref restriction is configured', () => {
|
package/src/bitbucket-service.ts
CHANGED
|
@@ -6,6 +6,12 @@ import { simplifyInboxPullRequests } from './inbox-pr-mapper.js';
|
|
|
6
6
|
import { CompareDiffResponse, formatCompareDiffAsUnified } from './compare-diff-mapper.js';
|
|
7
7
|
import { BITBUCKET_PRODUCT, getDefaultPageSize, getMissingConfig } from './config.js';
|
|
8
8
|
import { fetchMergeability, mergePullRequest, type MergePullRequestParams } from './pr-merge.js';
|
|
9
|
+
import {
|
|
10
|
+
declinePullRequest,
|
|
11
|
+
reopenPullRequest,
|
|
12
|
+
type DeclinePullRequestParams,
|
|
13
|
+
type ReopenPullRequestParams,
|
|
14
|
+
} from './pr-state.js';
|
|
9
15
|
import {
|
|
10
16
|
BitbucketMutationOutputMode,
|
|
11
17
|
BitbucketOutputMode,
|
|
@@ -237,6 +243,23 @@ export class BitbucketService {
|
|
|
237
243
|
);
|
|
238
244
|
}
|
|
239
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Create a branch in a repository
|
|
248
|
+
* @param projectKey The project key
|
|
249
|
+
* @param repositorySlug The repository slug
|
|
250
|
+
* @param name The name of the branch to create
|
|
251
|
+
* @param startPoint The branch, tag or commit the new branch is forked from
|
|
252
|
+
* @returns Promise with the created branch
|
|
253
|
+
*/
|
|
254
|
+
async createBranch(projectKey: string, repositorySlug: string, name: string, startPoint: string) {
|
|
255
|
+
projectKey = projectKey.toUpperCase();
|
|
256
|
+
repositorySlug = repositorySlug.toLowerCase();
|
|
257
|
+
return handleApiOperation(
|
|
258
|
+
() => RepositoryService.createBranch(projectKey, repositorySlug, { name, startPoint }),
|
|
259
|
+
'Error creating branch'
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
240
263
|
/**
|
|
241
264
|
* Get pull requests for a repository
|
|
242
265
|
* @param projectKey The project key
|
|
@@ -582,6 +605,35 @@ export class BitbucketService {
|
|
|
582
605
|
});
|
|
583
606
|
}
|
|
584
607
|
|
|
608
|
+
/**
|
|
609
|
+
* Decline a pull request. Only permitted for repositories the operator allowed through the
|
|
610
|
+
* decline gateway; see repo-gateway.ts.
|
|
611
|
+
* @param params.version The current pull request version, required for optimistic locking
|
|
612
|
+
* @param params.comment Optional reason, posted as a pull request comment by the same request
|
|
613
|
+
* @param params.gateway The resolved operator decline policy
|
|
614
|
+
* @returns Promise with the declined pull request
|
|
615
|
+
*/
|
|
616
|
+
async declinePullRequest(params: DeclinePullRequestParams) {
|
|
617
|
+
return declinePullRequest({
|
|
618
|
+
...params,
|
|
619
|
+
projectKey: params.projectKey.toUpperCase(),
|
|
620
|
+
repositorySlug: params.repositorySlug.toLowerCase(),
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Reopen a declined pull request. Not gated: it restores the state a decline removed.
|
|
626
|
+
* @param params.version The current pull request version, required for optimistic locking
|
|
627
|
+
* @returns Promise with the reopened pull request
|
|
628
|
+
*/
|
|
629
|
+
async reopenPullRequest(params: ReopenPullRequestParams) {
|
|
630
|
+
return reopenPullRequest({
|
|
631
|
+
...params,
|
|
632
|
+
projectKey: params.projectKey.toUpperCase(),
|
|
633
|
+
repositorySlug: params.repositorySlug.toLowerCase(),
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
585
637
|
/**
|
|
586
638
|
* Get a user by slug, or search for users by name/email filter
|
|
587
639
|
* @param userSlug Optional exact slug to look up a specific user
|
|
@@ -1116,6 +1168,12 @@ export const bitbucketToolSchemas = {
|
|
|
1116
1168
|
path: z.string().describe("Path to the file in the repository (e.g. 'src/index.ts')"),
|
|
1117
1169
|
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")
|
|
1118
1170
|
},
|
|
1171
|
+
createBranch: {
|
|
1172
|
+
projectKey: z.string().describe("The project key"),
|
|
1173
|
+
repositorySlug: z.string().describe("The repository slug"),
|
|
1174
|
+
name: z.string().describe("The name of the branch to create, without the 'refs/heads/' prefix (e.g. 'feature/my-branch')"),
|
|
1175
|
+
startPoint: z.string().describe("The branch, tag or commit the new branch is forked from (e.g. 'master', 'refs/heads/master', or a commit id)")
|
|
1176
|
+
},
|
|
1119
1177
|
getCommits: {
|
|
1120
1178
|
projectKey: z.string().describe("The project key"),
|
|
1121
1179
|
repositorySlug: z.string().describe("The repository slug"),
|
|
@@ -1245,6 +1303,21 @@ export const bitbucketToolSchemas = {
|
|
|
1245
1303
|
message: z.string().optional().describe("Commit message for the merge commit. Omit to let Bitbucket generate it."),
|
|
1246
1304
|
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
|
|
1247
1305
|
},
|
|
1306
|
+
declinePullRequest: {
|
|
1307
|
+
projectKey: z.string().describe("The project key"),
|
|
1308
|
+
repositorySlug: z.string().describe("The repository slug"),
|
|
1309
|
+
pullRequestId: z.string().describe("The pull request ID"),
|
|
1310
|
+
version: z.number().describe("The current version of the pull request (required for optimistic locking). Fetch it with bitbucket_getPullRequest immediately before declining; a stale version is rejected with a 409, in which case refetch the version and retry once."),
|
|
1311
|
+
comment: z.string().optional().describe("Reason for declining, posted as a pull request comment by the same request. Prefer this over posting a separate comment first: sending both together means a rejected decline leaves no orphaned 'why this was abandoned' comment on a pull request that stayed open."),
|
|
1312
|
+
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
|
|
1313
|
+
},
|
|
1314
|
+
reopenPullRequest: {
|
|
1315
|
+
projectKey: z.string().describe("The project key"),
|
|
1316
|
+
repositorySlug: z.string().describe("The repository slug"),
|
|
1317
|
+
pullRequestId: z.string().describe("The pull request ID"),
|
|
1318
|
+
version: z.number().describe("The current version of the pull request (required for optimistic locking). Fetch it with bitbucket_getPullRequest immediately before reopening; a stale version is rejected with a 409, in which case refetch the version and retry once."),
|
|
1319
|
+
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
|
|
1320
|
+
},
|
|
1248
1321
|
getRequiredReviewers: {
|
|
1249
1322
|
projectKey: z.string().describe("The project key"),
|
|
1250
1323
|
repositorySlug: z.string().describe("The repository slug"),
|
package/src/index.ts
CHANGED
|
@@ -1,7 +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 './
|
|
4
|
+
import { resolveDeclineGateway, resolveMergeGateway } from './repo-gateway.js';
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
6
|
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
@@ -78,6 +78,16 @@ server.tool(
|
|
|
78
78
|
}
|
|
79
79
|
);
|
|
80
80
|
|
|
81
|
+
server.tool(
|
|
82
|
+
"bitbucket_createBranch",
|
|
83
|
+
"Create a branch in a Bitbucket repository, forked from a branch, tag or commit. Use this before bitbucket_createPullRequest when the source branch does not exist yet. The response contains the new branch's 'id' (e.g. 'refs/heads/feature/my-branch'), which is what bitbucket_createPullRequest expects as fromRefId.",
|
|
84
|
+
bitbucketToolSchemas.createBranch,
|
|
85
|
+
async ({ projectKey, repositorySlug, name, startPoint }) => {
|
|
86
|
+
const result = await bitbucketService.createBranch(projectKey, repositorySlug, name, startPoint);
|
|
87
|
+
return formatToolResponse(result);
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
|
|
81
91
|
server.tool(
|
|
82
92
|
"bitbucket_getCommits",
|
|
83
93
|
"Get commits for a Bitbucket repository",
|
|
@@ -247,6 +257,47 @@ if (mergeGateway.enabled) {
|
|
|
247
257
|
);
|
|
248
258
|
}
|
|
249
259
|
|
|
260
|
+
const declineGateway = resolveDeclineGateway();
|
|
261
|
+
|
|
262
|
+
// Declining is reviewer-visible the moment it lands and notifies everyone on the pull request, so
|
|
263
|
+
// it is registered only for the repositories the operator named. Reopening is the undo path and is
|
|
264
|
+
// always available.
|
|
265
|
+
if (declineGateway.enabled) {
|
|
266
|
+
server.tool(
|
|
267
|
+
"bitbucket_declinePullRequest",
|
|
268
|
+
`Decline (close without merging) 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. Pass the reason as 'comment' so it is posted with the decline in a single request. Reviewers are notified; undo with bitbucket_reopenPullRequest. Allowed on this server: ${declineGateway.repos.join(', ')}.`,
|
|
269
|
+
bitbucketToolSchemas.declinePullRequest,
|
|
270
|
+
async ({ projectKey, repositorySlug, pullRequestId, version, comment, output }) => {
|
|
271
|
+
const result = await bitbucketService.declinePullRequest({
|
|
272
|
+
projectKey,
|
|
273
|
+
repositorySlug,
|
|
274
|
+
pullRequestId,
|
|
275
|
+
version,
|
|
276
|
+
comment,
|
|
277
|
+
output,
|
|
278
|
+
gateway: declineGateway,
|
|
279
|
+
});
|
|
280
|
+
return formatToolResponse(result);
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
server.tool(
|
|
286
|
+
"bitbucket_reopenPullRequest",
|
|
287
|
+
"Reopen a declined pull request, restoring it to OPEN. 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 pull request must be in the DECLINED state; anything else is refused.",
|
|
288
|
+
bitbucketToolSchemas.reopenPullRequest,
|
|
289
|
+
async ({ projectKey, repositorySlug, pullRequestId, version, output }) => {
|
|
290
|
+
const result = await bitbucketService.reopenPullRequest({
|
|
291
|
+
projectKey,
|
|
292
|
+
repositorySlug,
|
|
293
|
+
pullRequestId,
|
|
294
|
+
version,
|
|
295
|
+
output,
|
|
296
|
+
});
|
|
297
|
+
return formatToolResponse(result);
|
|
298
|
+
}
|
|
299
|
+
);
|
|
300
|
+
|
|
250
301
|
server.tool(
|
|
251
302
|
"bitbucket_getRequiredReviewers",
|
|
252
303
|
"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.",
|
package/src/pr-merge.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
shapeMergeability,
|
|
6
6
|
shapePullRequestAck,
|
|
7
7
|
} from './bitbucket-response-mapper.js';
|
|
8
|
-
import {
|
|
8
|
+
import { assertRepoOperationAllowed, assertTargetRefMergeAllowed, type RepoGateway } from './repo-gateway.js';
|
|
9
9
|
|
|
10
10
|
export interface MergePullRequestParams {
|
|
11
11
|
projectKey: string;
|
|
@@ -13,7 +13,7 @@ export interface MergePullRequestParams {
|
|
|
13
13
|
pullRequestId: string;
|
|
14
14
|
/** Current PR version, required for optimistic locking. */
|
|
15
15
|
version: number;
|
|
16
|
-
gateway:
|
|
16
|
+
gateway: RepoGateway;
|
|
17
17
|
strategyId?: string;
|
|
18
18
|
message?: string;
|
|
19
19
|
output?: BitbucketMutationOutputMode;
|
|
@@ -75,7 +75,7 @@ export async function mergePullRequest(params: MergePullRequestParams) {
|
|
|
75
75
|
const { projectKey, repositorySlug, pullRequestId, version, gateway } = params;
|
|
76
76
|
|
|
77
77
|
const result = await handleApiOperation(async () => {
|
|
78
|
-
|
|
78
|
+
assertRepoOperationAllowed(gateway, projectKey, repositorySlug);
|
|
79
79
|
await assertTargetRefAllowed(params);
|
|
80
80
|
await assertMergeable(projectKey, repositorySlug, pullRequestId);
|
|
81
81
|
|
package/src/pr-state.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { handleApiOperation } from '@atlassian-dc-mcp/common';
|
|
2
|
+
import { PullRequestsService } from './bitbucket-client/index.js';
|
|
3
|
+
import { BitbucketMutationOutputMode, shapePullRequestAck } from './bitbucket-response-mapper.js';
|
|
4
|
+
import { assertRepoOperationAllowed, type RepoGateway } from './repo-gateway.js';
|
|
5
|
+
|
|
6
|
+
interface PullRequestStateParams {
|
|
7
|
+
projectKey: string;
|
|
8
|
+
repositorySlug: string;
|
|
9
|
+
pullRequestId: string;
|
|
10
|
+
/** Current PR version, required for optimistic locking. */
|
|
11
|
+
version: number;
|
|
12
|
+
output?: BitbucketMutationOutputMode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DeclinePullRequestParams extends PullRequestStateParams {
|
|
16
|
+
gateway: RepoGateway;
|
|
17
|
+
/**
|
|
18
|
+
* Reason for declining, posted as a pull request comment by the same request. Sending it with
|
|
19
|
+
* the decline keeps the two atomic: a rejected decline leaves no orphaned "why this was
|
|
20
|
+
* abandoned" comment on a pull request that stayed open.
|
|
21
|
+
*/
|
|
22
|
+
comment?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ReopenPullRequestParams = PullRequestStateParams;
|
|
26
|
+
|
|
27
|
+
/** The ack carries the new version, which is what a later reopen needs as its optimistic lock. */
|
|
28
|
+
function shapeResult<T extends { success: boolean; data?: unknown }>(result: T, output?: BitbucketMutationOutputMode) {
|
|
29
|
+
if (result.success && result.data && output !== 'full') {
|
|
30
|
+
return { ...result, data: shapePullRequestAck(result.data) };
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Decline a pull request under the operator's decline policy. The repository is checked before
|
|
37
|
+
* any request is issued. Bitbucket rejects a stale version and a pull request that is not OPEN
|
|
38
|
+
* with a 409, so no pre-flight read is needed.
|
|
39
|
+
*/
|
|
40
|
+
export async function declinePullRequest(params: DeclinePullRequestParams) {
|
|
41
|
+
const { projectKey, repositorySlug, pullRequestId, version, gateway, comment } = params;
|
|
42
|
+
|
|
43
|
+
const result = await handleApiOperation(async () => {
|
|
44
|
+
assertRepoOperationAllowed(gateway, projectKey, repositorySlug);
|
|
45
|
+
|
|
46
|
+
return PullRequestsService.decline(projectKey, pullRequestId, repositorySlug, String(version), {
|
|
47
|
+
version,
|
|
48
|
+
...(comment ? { comment } : {}),
|
|
49
|
+
});
|
|
50
|
+
}, 'Error declining pull request');
|
|
51
|
+
|
|
52
|
+
return shapeResult(result, params.output);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Reopen a declined pull request. Ungated: it restores the state a decline removed, so it is the
|
|
57
|
+
* undo path rather than a destructive one. A pull request that is not declined is rejected with a 409.
|
|
58
|
+
*/
|
|
59
|
+
export async function reopenPullRequest(params: ReopenPullRequestParams) {
|
|
60
|
+
const { projectKey, repositorySlug, pullRequestId, version } = params;
|
|
61
|
+
|
|
62
|
+
const result = await handleApiOperation(
|
|
63
|
+
() => PullRequestsService.reopen(projectKey, pullRequestId, repositorySlug, String(version), { version }),
|
|
64
|
+
'Error reopening pull request',
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return shapeResult(result, params.output);
|
|
68
|
+
}
|