@atlassian-dc-mcp/bitbucket 0.34.0 → 0.36.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 -2
- package/build/__tests__/bitbucket-service.test.js +41 -2
- package/build/__tests__/bitbucket-service.test.js.map +1 -1
- package/build/__tests__/config.test.js +2 -1
- package/build/__tests__/config.test.js.map +1 -1
- package/build/__tests__/pr-builds.test.d.ts +2 -0
- package/build/__tests__/pr-builds.test.d.ts.map +1 -0
- package/build/__tests__/pr-builds.test.js +74 -0
- package/build/__tests__/pr-builds.test.js.map +1 -0
- 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 +44 -0
- package/build/bitbucket-service.d.ts.map +1 -1
- package/build/bitbucket-service.js +59 -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-builds.d.ts +36 -0
- package/build/pr-builds.d.ts.map +1 -0
- package/build/pr-builds.js +40 -0
- package/build/pr-builds.js.map +1 -0
- 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 +50 -2
- package/src/__tests__/config.test.ts +2 -1
- package/src/__tests__/pr-builds.test.ts +93 -0
- 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 +67 -0
- package/src/index.ts +52 -1
- package/src/pr-builds.ts +74 -0
- 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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator-controlled gate for pull request operations that change state on a shared branch
|
|
3
|
+
* or in front of other people.
|
|
4
|
+
*
|
|
5
|
+
* A merge cannot be undone through the API, and a decline is visible to every reviewer the
|
|
6
|
+
* moment it lands, so both are disabled by default: the tool is not even registered unless
|
|
7
|
+
* the operator enables it and names the repositories it may act on. Each operation reads its
|
|
8
|
+
* own environment variables, so allowing declines never implies allowing merges. The scope is
|
|
9
|
+
* read once at startup, so the model can never select or widen it. Read-only checks are not
|
|
10
|
+
* gated.
|
|
11
|
+
*/
|
|
12
|
+
/** Names an operation in refusal messages and points the operator at the variables that open it. */
|
|
13
|
+
export interface GatewayPolicy {
|
|
14
|
+
/** Short operation name, used to tag warnings. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Gerund that starts a refusal message, e.g. "Merging". */
|
|
17
|
+
gerund: string;
|
|
18
|
+
flagVar: string;
|
|
19
|
+
reposVar: string;
|
|
20
|
+
/** Omitted for operations that have no meaningful target-branch restriction. */
|
|
21
|
+
targetRefsVar?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare const MERGE_POLICY: GatewayPolicy;
|
|
24
|
+
/**
|
|
25
|
+
* Declining has no target-ref restriction: which branch a pull request would have landed on
|
|
26
|
+
* says nothing about whether abandoning it is safe.
|
|
27
|
+
*/
|
|
28
|
+
export declare const DECLINE_POLICY: GatewayPolicy;
|
|
29
|
+
export interface RepoGateway {
|
|
30
|
+
/** Whether the operation is enabled and at least one valid repository pattern resolved. */
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
/** Allowed `PROJECT/repository-slug` targets; a `PROJECT/*` entry allows the whole project. */
|
|
33
|
+
repos: string[];
|
|
34
|
+
/** Allowed target refs; a trailing `*` matches a prefix. Empty means any ref in an allowed repository. */
|
|
35
|
+
targetRefs: string[];
|
|
36
|
+
policy: GatewayPolicy;
|
|
37
|
+
}
|
|
38
|
+
type Env = Record<string, string | undefined>;
|
|
39
|
+
type Warn = (message: string) => void;
|
|
40
|
+
export interface ResolveGatewayOptions {
|
|
41
|
+
env?: Env;
|
|
42
|
+
warn?: Warn;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Reads one operation's gateway configuration from the environment. The operation only
|
|
46
|
+
* activates when its flag is set and at least one repository entry is valid; otherwise a
|
|
47
|
+
* warning is logged and the gateway stays disabled.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveGateway(policy: GatewayPolicy, options?: ResolveGatewayOptions): RepoGateway;
|
|
50
|
+
export declare function resolveMergeGateway(options?: ResolveGatewayOptions): RepoGateway;
|
|
51
|
+
export declare function resolveDeclineGateway(options?: ResolveGatewayOptions): RepoGateway;
|
|
52
|
+
/** Throws unless the gateway allows its operation in this repository. No network call. */
|
|
53
|
+
export declare function assertRepoOperationAllowed(gateway: RepoGateway, projectKey: string, repositorySlug: string): void;
|
|
54
|
+
/** Throws unless the gateway allows merging into this target ref. A gateway with no ref restriction allows all. */
|
|
55
|
+
export declare function assertTargetRefMergeAllowed(gateway: RepoGateway, targetRefId: string | undefined): void;
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=repo-gateway.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo-gateway.d.ts","sourceRoot":"","sources":["../src/repo-gateway.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,oGAAoG;AACpG,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,YAAY,EAAE,aAM1B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,aAK5B,CAAC;AAEF,MAAM,WAAW,WAAW;IAC1B,2FAA2F;IAC3F,OAAO,EAAE,OAAO,CAAC;IACjB,+FAA+F;IAC/F,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,0GAA0G;IAC1G,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAC9C,KAAK,IAAI,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAEtC,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,GAAG,CAAC;IACV,IAAI,CAAC,EAAE,IAAI,CAAC;CACb;AA+CD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,WAAW,CA2BlG;AAED,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,WAAW,CAEhF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,WAAW,CAElF;AAED,0FAA0F;AAC1F,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAcjH;AAED,mHAAmH;AACnH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAgBvG"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator-controlled gate for pull request operations that change state on a shared branch
|
|
3
|
+
* or in front of other people.
|
|
4
|
+
*
|
|
5
|
+
* A merge cannot be undone through the API, and a decline is visible to every reviewer the
|
|
6
|
+
* moment it lands, so both are disabled by default: the tool is not even registered unless
|
|
7
|
+
* the operator enables it and names the repositories it may act on. Each operation reads its
|
|
8
|
+
* own environment variables, so allowing declines never implies allowing merges. The scope is
|
|
9
|
+
* read once at startup, so the model can never select or widen it. Read-only checks are not
|
|
10
|
+
* gated.
|
|
11
|
+
*/
|
|
12
|
+
export const MERGE_POLICY = {
|
|
13
|
+
name: 'merge',
|
|
14
|
+
gerund: 'Merging',
|
|
15
|
+
flagVar: 'BITBUCKET_MERGE_ENABLED',
|
|
16
|
+
reposVar: 'BITBUCKET_MERGE_ALLOWED_REPOS',
|
|
17
|
+
targetRefsVar: 'BITBUCKET_MERGE_ALLOWED_TARGET_REFS',
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Declining has no target-ref restriction: which branch a pull request would have landed on
|
|
21
|
+
* says nothing about whether abandoning it is safe.
|
|
22
|
+
*/
|
|
23
|
+
export const DECLINE_POLICY = {
|
|
24
|
+
name: 'decline',
|
|
25
|
+
gerund: 'Declining',
|
|
26
|
+
flagVar: 'BITBUCKET_DECLINE_ENABLED',
|
|
27
|
+
reposVar: 'BITBUCKET_DECLINE_ALLOWED_REPOS',
|
|
28
|
+
};
|
|
29
|
+
const REPO_ENTRY_RE = /^[^\s/]+\/[^\s/]+$/;
|
|
30
|
+
function readBool(env, name) {
|
|
31
|
+
const value = env[name]?.trim().toLowerCase();
|
|
32
|
+
return value === 'true' || value === '1' || value === 'yes';
|
|
33
|
+
}
|
|
34
|
+
function parseList(raw) {
|
|
35
|
+
return (raw ?? '').split(/[,;\s]+/).map(entry => entry.trim()).filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
function dedupe(values) {
|
|
38
|
+
return [...new Set(values)];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Accepts `PROJECT/repository-slug` or `PROJECT/*`. The project key is upper-cased and
|
|
42
|
+
* the slug lower-cased to match the casing the REST API uses, so comparisons are exact.
|
|
43
|
+
*/
|
|
44
|
+
function normalizeRepoEntry(entry, warn) {
|
|
45
|
+
if (!REPO_ENTRY_RE.test(entry)) {
|
|
46
|
+
warn(`Ignoring repository entry that is not "PROJECT/repository-slug" or "PROJECT/*": "${entry}"`);
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const [projectKey, slug] = entry.split('/');
|
|
50
|
+
if (projectKey === '*') {
|
|
51
|
+
warn(`Ignoring repository entry that would allow every project: "${entry}"`);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return `${projectKey.toUpperCase()}/${slug.toLowerCase()}`;
|
|
55
|
+
}
|
|
56
|
+
/** Bare branch names are expanded so operators can write `develop` instead of `refs/heads/develop`. */
|
|
57
|
+
function normalizeRefEntry(entry) {
|
|
58
|
+
return entry.startsWith('refs/') ? entry : `refs/heads/${entry}`;
|
|
59
|
+
}
|
|
60
|
+
function matchesPattern(value, pattern) {
|
|
61
|
+
return pattern.endsWith('*') ? value.startsWith(pattern.slice(0, -1)) : value === pattern;
|
|
62
|
+
}
|
|
63
|
+
function disabledGateway(policy) {
|
|
64
|
+
return { enabled: false, repos: [], targetRefs: [], policy };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Reads one operation's gateway configuration from the environment. The operation only
|
|
68
|
+
* activates when its flag is set and at least one repository entry is valid; otherwise a
|
|
69
|
+
* warning is logged and the gateway stays disabled.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveGateway(policy, options) {
|
|
72
|
+
const env = options?.env ?? process.env;
|
|
73
|
+
const warn = options?.warn ?? ((message) => console.error(`[${policy.name}-gateway] ${message}`));
|
|
74
|
+
if (!readBool(env, policy.flagVar)) {
|
|
75
|
+
return disabledGateway(policy);
|
|
76
|
+
}
|
|
77
|
+
const repos = dedupe(parseList(env[policy.reposVar])
|
|
78
|
+
.map(entry => normalizeRepoEntry(entry, warn))
|
|
79
|
+
.filter((entry) => Boolean(entry)));
|
|
80
|
+
if (repos.length === 0) {
|
|
81
|
+
warn(`${policy.gerund} was enabled but no valid repository is configured (set ${policy.reposVar} ` +
|
|
82
|
+
'to a list of "PROJECT/repository-slug" or "PROJECT/*" entries); the tool will stay disabled.');
|
|
83
|
+
return disabledGateway(policy);
|
|
84
|
+
}
|
|
85
|
+
const targetRefs = policy.targetRefsVar
|
|
86
|
+
? dedupe(parseList(env[policy.targetRefsVar]).map(normalizeRefEntry))
|
|
87
|
+
: [];
|
|
88
|
+
return { enabled: true, repos, targetRefs, policy };
|
|
89
|
+
}
|
|
90
|
+
export function resolveMergeGateway(options) {
|
|
91
|
+
return resolveGateway(MERGE_POLICY, options);
|
|
92
|
+
}
|
|
93
|
+
export function resolveDeclineGateway(options) {
|
|
94
|
+
return resolveGateway(DECLINE_POLICY, options);
|
|
95
|
+
}
|
|
96
|
+
/** Throws unless the gateway allows its operation in this repository. No network call. */
|
|
97
|
+
export function assertRepoOperationAllowed(gateway, projectKey, repositorySlug) {
|
|
98
|
+
const { policy } = gateway;
|
|
99
|
+
if (!gateway.enabled) {
|
|
100
|
+
throw new Error(`${policy.gerund} pull requests is disabled on this server. Enable it with ${policy.flagVar} ` +
|
|
101
|
+
`and list the allowed repositories in ${policy.reposVar}.`);
|
|
102
|
+
}
|
|
103
|
+
const target = `${projectKey.toUpperCase()}/${repositorySlug.toLowerCase()}`;
|
|
104
|
+
if (!gateway.repos.some(pattern => matchesPattern(target, pattern))) {
|
|
105
|
+
throw new Error(`${policy.gerund} is not allowed in ${target} on this server. Allowed: ${gateway.repos.join(', ')}.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Throws unless the gateway allows merging into this target ref. A gateway with no ref restriction allows all. */
|
|
109
|
+
export function assertTargetRefMergeAllowed(gateway, targetRefId) {
|
|
110
|
+
if (gateway.targetRefs.length === 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!targetRefId) {
|
|
114
|
+
throw new Error('Could not determine the target branch of the pull request, and this server restricts which ' +
|
|
115
|
+
`branches may be merged into (${gateway.policy.targetRefsVar}); refusing to merge.`);
|
|
116
|
+
}
|
|
117
|
+
if (!gateway.targetRefs.some(pattern => matchesPattern(targetRefId, pattern))) {
|
|
118
|
+
throw new Error(`Merging into ${targetRefId} is not allowed on this server. ` +
|
|
119
|
+
`Allowed target refs: ${gateway.targetRefs.join(', ')}.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=repo-gateway.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo-gateway.js","sourceRoot":"","sources":["../src/repo-gateway.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlassian-dc-mcp/bitbucket",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./bin/run.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"test": "jest"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@atlassian-dc-mcp/common": "^0.
|
|
34
|
+
"@atlassian-dc-mcp/common": "^0.36.0",
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
36
36
|
"dotenv": "^16.4.7",
|
|
37
37
|
"zod": "^3.24.2"
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "f2ef2f49cce893cbf1ff2cad072cdecaae7ad28d"
|
|
52
52
|
}
|
|
@@ -3,7 +3,8 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { initializeRuntimeConfig } from '@atlassian-dc-mcp/common';
|
|
5
5
|
import { BitbucketService } from '../bitbucket-service.js';
|
|
6
|
-
import { PullRequestsService, RepositoryService } from '../bitbucket-client/index.js';
|
|
6
|
+
import { DeprecatedService, PullRequestsService, RepositoryService } from '../bitbucket-client/index.js';
|
|
7
|
+
import { DECLINE_POLICY, type RepoGateway } from '../repo-gateway.js';
|
|
7
8
|
import { request as mockRequest } from '../bitbucket-client/core/request.js';
|
|
8
9
|
|
|
9
10
|
// Mock the request function
|
|
@@ -23,12 +24,17 @@ jest.mock('../bitbucket-client/index.js', () => ({
|
|
|
23
24
|
updateStatus: jest.fn(),
|
|
24
25
|
getPage: jest.fn(),
|
|
25
26
|
getReviewers: jest.fn(),
|
|
26
|
-
get3: jest.fn()
|
|
27
|
+
get3: jest.fn(),
|
|
28
|
+
decline: jest.fn(),
|
|
29
|
+
reopen: jest.fn()
|
|
27
30
|
},
|
|
28
31
|
RepositoryService: {
|
|
29
32
|
streamRaw: jest.fn(),
|
|
30
33
|
createBranch: jest.fn()
|
|
31
34
|
},
|
|
35
|
+
DeprecatedService: {
|
|
36
|
+
getBuildStatus: jest.fn()
|
|
37
|
+
},
|
|
32
38
|
OpenAPI: {
|
|
33
39
|
BASE: '',
|
|
34
40
|
TOKEN: '',
|
|
@@ -2971,6 +2977,48 @@ describe('BitbucketService', () => {
|
|
|
2971
2977
|
'refs/heads/feature', 'refs/heads/main'
|
|
2972
2978
|
);
|
|
2973
2979
|
});
|
|
2980
|
+
|
|
2981
|
+
it('should uppercase projectKey and lowercase repositorySlug for declinePullRequest', async () => {
|
|
2982
|
+
const gateway: RepoGateway = {
|
|
2983
|
+
enabled: true,
|
|
2984
|
+
repos: ['TEST/test-repo'],
|
|
2985
|
+
targetRefs: [],
|
|
2986
|
+
policy: DECLINE_POLICY,
|
|
2987
|
+
};
|
|
2988
|
+
(PullRequestsService.decline as jest.Mock).mockResolvedValue({ id: 1, state: 'DECLINED' });
|
|
2989
|
+
|
|
2990
|
+
await bitbucketService.declinePullRequest({
|
|
2991
|
+
projectKey: 'test',
|
|
2992
|
+
repositorySlug: 'Test-Repo',
|
|
2993
|
+
pullRequestId: '1',
|
|
2994
|
+
version: 2,
|
|
2995
|
+
gateway,
|
|
2996
|
+
});
|
|
2997
|
+
|
|
2998
|
+
expect(PullRequestsService.decline).toHaveBeenCalledWith('TEST', '1', 'test-repo', '2', { version: 2 });
|
|
2999
|
+
});
|
|
3000
|
+
|
|
3001
|
+
it('should uppercase projectKey and lowercase repositorySlug for reopenPullRequest', async () => {
|
|
3002
|
+
(PullRequestsService.reopen as jest.Mock).mockResolvedValue({ id: 1, state: 'OPEN' });
|
|
3003
|
+
|
|
3004
|
+
await bitbucketService.reopenPullRequest({
|
|
3005
|
+
projectKey: 'test',
|
|
3006
|
+
repositorySlug: 'Test-Repo',
|
|
3007
|
+
pullRequestId: '1',
|
|
3008
|
+
version: 2,
|
|
3009
|
+
});
|
|
3010
|
+
|
|
3011
|
+
expect(PullRequestsService.reopen).toHaveBeenCalledWith('TEST', '1', 'test-repo', '2', { version: 2 });
|
|
3012
|
+
});
|
|
3013
|
+
|
|
3014
|
+
it('should uppercase projectKey and lowercase repositorySlug for getPullRequestBuilds', async () => {
|
|
3015
|
+
(PullRequestsService.get3 as jest.Mock).mockResolvedValue({ id: 1, fromRef: { latestCommit: 'abc123' } });
|
|
3016
|
+
(DeprecatedService.getBuildStatus as jest.Mock).mockResolvedValue({ values: [] });
|
|
3017
|
+
|
|
3018
|
+
await bitbucketService.getPullRequestBuilds('test', 'Test-Repo', '1');
|
|
3019
|
+
|
|
3020
|
+
expect(PullRequestsService.get3).toHaveBeenCalledWith('TEST', '1', 'test-repo');
|
|
3021
|
+
});
|
|
2974
3022
|
});
|
|
2975
3023
|
|
|
2976
3024
|
describe('searchCode', () => {
|
|
@@ -16,8 +16,9 @@ describe('Bitbucket config', () => {
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
afterEach(() => {
|
|
19
|
-
|
|
19
|
+
// Windows refuses to remove a directory that is still the process cwd.
|
|
20
20
|
process.chdir(originalCwd);
|
|
21
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
afterAll(() => {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { DeprecatedService, PullRequestsService } from '../bitbucket-client/index.js';
|
|
2
|
+
import { fetchPullRequestBuilds } from '../pr-builds.js';
|
|
3
|
+
|
|
4
|
+
jest.mock('../bitbucket-client/index.js', () => ({
|
|
5
|
+
PullRequestsService: {
|
|
6
|
+
get3: jest.fn(),
|
|
7
|
+
},
|
|
8
|
+
DeprecatedService: {
|
|
9
|
+
getBuildStatus: jest.fn(),
|
|
10
|
+
},
|
|
11
|
+
OpenAPI: { BASE: '', TOKEN: '', VERSION: '' },
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const getPullRequest = PullRequestsService.get3 as jest.Mock;
|
|
15
|
+
const getBuildStatus = DeprecatedService.getBuildStatus as jest.Mock;
|
|
16
|
+
|
|
17
|
+
const HEAD = '0780e1428c06abf1c96ab9cb995d129255511c93';
|
|
18
|
+
const OPEN_PR = { id: 42, fromRef: { id: 'refs/heads/feature', latestCommit: HEAD } };
|
|
19
|
+
|
|
20
|
+
const BUILD = {
|
|
21
|
+
key: 'a1',
|
|
22
|
+
name: 'Builds » demo » PR-42 #1',
|
|
23
|
+
state: 'SUCCESSFUL',
|
|
24
|
+
url: 'https://ci.example.com/job/demo/job/PR-42/1/',
|
|
25
|
+
description: 'This commit looks good.',
|
|
26
|
+
dateAdded: 1791464936354,
|
|
27
|
+
};
|
|
28
|
+
const E2E = { ...BUILD, key: 'b2', name: 'E2E » demo » PR-42 #1', state: 'FAILED' };
|
|
29
|
+
const REVIEW = { ...BUILD, key: 'c3', name: 'AI Code Review » demo » PR-42 #1', state: 'INPROGRESS' };
|
|
30
|
+
|
|
31
|
+
describe('fetchPullRequestBuilds', () => {
|
|
32
|
+
beforeEach(() => jest.clearAllMocks());
|
|
33
|
+
|
|
34
|
+
it('lists the build statuses of the latest source commit with per-state counts', async () => {
|
|
35
|
+
getPullRequest.mockResolvedValue(OPEN_PR);
|
|
36
|
+
getBuildStatus.mockResolvedValue({ size: 3, isLastPage: true, values: [BUILD, E2E, REVIEW] });
|
|
37
|
+
|
|
38
|
+
const result = await fetchPullRequestBuilds('PROJ', 'demo', '42');
|
|
39
|
+
|
|
40
|
+
expect(getPullRequest).toHaveBeenCalledWith('PROJ', '42', 'demo');
|
|
41
|
+
expect(getBuildStatus).toHaveBeenCalledWith(HEAD, 'NEWEST', 0, 100);
|
|
42
|
+
expect(result).toEqual({
|
|
43
|
+
success: true,
|
|
44
|
+
data: {
|
|
45
|
+
commitId: HEAD,
|
|
46
|
+
counts: { successful: 1, failed: 1, inProgress: 1, cancelled: 0, unknown: 0 },
|
|
47
|
+
builds: [BUILD, E2E, REVIEW],
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('counts a build without a recognised state as unknown', async () => {
|
|
53
|
+
getPullRequest.mockResolvedValue(OPEN_PR);
|
|
54
|
+
getBuildStatus.mockResolvedValue({ values: [{ key: 'x' }, { key: 'y', state: 'QUEUED' }] });
|
|
55
|
+
|
|
56
|
+
const result = await fetchPullRequestBuilds('PROJ', 'demo', '42');
|
|
57
|
+
|
|
58
|
+
expect(result.data?.counts).toEqual({ successful: 0, failed: 0, inProgress: 0, cancelled: 0, unknown: 2 });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns empty builds when no CI has reported on the commit', async () => {
|
|
62
|
+
getPullRequest.mockResolvedValue(OPEN_PR);
|
|
63
|
+
getBuildStatus.mockResolvedValue({ size: 0, isLastPage: true, values: [] });
|
|
64
|
+
|
|
65
|
+
const result = await fetchPullRequestBuilds('PROJ', 'demo', '42');
|
|
66
|
+
|
|
67
|
+
expect(result.data).toEqual({
|
|
68
|
+
commitId: HEAD,
|
|
69
|
+
counts: { successful: 0, failed: 0, inProgress: 0, cancelled: 0, unknown: 0 },
|
|
70
|
+
builds: [],
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('fails without querying builds when the pull request has no source commit', async () => {
|
|
75
|
+
getPullRequest.mockResolvedValue({ id: 42, fromRef: { id: 'refs/heads/feature' } });
|
|
76
|
+
|
|
77
|
+
const result = await fetchPullRequestBuilds('PROJ', 'demo', '42');
|
|
78
|
+
|
|
79
|
+
expect(result.success).toBe(false);
|
|
80
|
+
expect(result.error).toBe('Pull request 42 has no source commit');
|
|
81
|
+
expect(getBuildStatus).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('reports an API error with the status line', async () => {
|
|
85
|
+
getPullRequest.mockRejectedValue({ status: 404, statusText: 'Not Found', body: { errors: [] } });
|
|
86
|
+
|
|
87
|
+
const result = await fetchPullRequestBuilds('PROJ', 'demo', '42');
|
|
88
|
+
|
|
89
|
+
expect(result.success).toBe(false);
|
|
90
|
+
expect(result.error).toBe('Error fetching pull request builds: 404 Not Found');
|
|
91
|
+
expect(getBuildStatus).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -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
|
+
});
|