@atlassian-dc-mcp/bitbucket 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +56 -2
  3. package/build/__tests__/bitbucket-service.test.js +60 -2
  4. package/build/__tests__/bitbucket-service.test.js.map +1 -1
  5. package/build/__tests__/bitbucket-token-optimization.test.js +11 -0
  6. package/build/__tests__/bitbucket-token-optimization.test.js.map +1 -1
  7. package/build/__tests__/merge-gateway.test.d.ts +2 -0
  8. package/build/__tests__/merge-gateway.test.d.ts.map +1 -0
  9. package/build/__tests__/merge-gateway.test.js +98 -0
  10. package/build/__tests__/merge-gateway.test.js.map +1 -0
  11. package/build/__tests__/pr-comment-mapper.test.js +91 -11
  12. package/build/__tests__/pr-comment-mapper.test.js.map +1 -1
  13. package/build/__tests__/pr-merge.test.d.ts +2 -0
  14. package/build/__tests__/pr-merge.test.d.ts.map +1 -0
  15. package/build/__tests__/pr-merge.test.js +166 -0
  16. package/build/__tests__/pr-merge.test.js.map +1 -0
  17. package/build/bitbucket-response-mapper.d.ts +10 -0
  18. package/build/bitbucket-response-mapper.d.ts.map +1 -1
  19. package/build/bitbucket-response-mapper.js +16 -0
  20. package/build/bitbucket-response-mapper.js.map +1 -1
  21. package/build/bitbucket-service.d.ts +68 -9
  22. package/build/bitbucket-service.d.ts.map +1 -1
  23. package/build/bitbucket-service.js +91 -15
  24. package/build/bitbucket-service.js.map +1 -1
  25. package/build/index.js +34 -4
  26. package/build/index.js.map +1 -1
  27. package/build/merge-gateway.d.ts +34 -0
  28. package/build/merge-gateway.d.ts.map +1 -0
  29. package/build/merge-gateway.js +86 -0
  30. package/build/merge-gateway.js.map +1 -0
  31. package/build/pr-comment-mapper.d.ts +3 -0
  32. package/build/pr-comment-mapper.d.ts.map +1 -1
  33. package/build/pr-comment-mapper.js +8 -3
  34. package/build/pr-comment-mapper.js.map +1 -1
  35. package/build/pr-merge.d.ts +23 -0
  36. package/build/pr-merge.d.ts.map +1 -0
  37. package/build/pr-merge.js +59 -0
  38. package/build/pr-merge.js.map +1 -0
  39. package/package.json +3 -3
  40. package/src/__tests__/bitbucket-service.test.ts +154 -2
  41. package/src/__tests__/bitbucket-token-optimization.test.ts +11 -0
  42. package/src/__tests__/merge-gateway.test.ts +119 -0
  43. package/src/__tests__/pr-comment-mapper.test.ts +95 -11
  44. package/src/__tests__/pr-merge.test.ts +206 -0
  45. package/src/bitbucket-response-mapper.ts +27 -0
  46. package/src/bitbucket-service.ts +100 -14
  47. package/src/index.ts +55 -6
  48. package/src/merge-gateway.ts +132 -0
  49. package/src/pr-comment-mapper.ts +11 -3
  50. package/src/pr-merge.ts +94 -0
  51. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Operator-controlled gate for merging pull requests.
3
+ *
4
+ * A merge writes to a shared branch and cannot be undone through the API, so it is
5
+ * disabled by default: `bitbucket_mergePullRequest` is not even registered unless the
6
+ * operator enables it and names the repositories it may merge in. The allowed scope is
7
+ * read from the environment once at startup, so the model can never select or widen it.
8
+ * Read-only mergeability checks are not gated.
9
+ */
10
+ export interface MergeGateway {
11
+ /** Whether merging is enabled and at least one valid repository pattern resolved. */
12
+ enabled: boolean;
13
+ /** Allowed `PROJECT/repository-slug` targets; a `PROJECT/*` entry allows the whole project. */
14
+ repos: string[];
15
+ /** Allowed target refs; a trailing `*` matches a prefix. Empty means any ref in an allowed repository. */
16
+ targetRefs: string[];
17
+ }
18
+
19
+ type Env = Record<string, string | undefined>;
20
+ type Warn = (message: string) => void;
21
+
22
+ const DISABLED: MergeGateway = { enabled: false, repos: [], targetRefs: [] };
23
+
24
+ const REPO_ENTRY_RE = /^[^\s/]+\/[^\s/]+$/;
25
+
26
+ function readBool(env: Env, name: string): boolean {
27
+ const value = env[name]?.trim().toLowerCase();
28
+ return value === 'true' || value === '1' || value === 'yes';
29
+ }
30
+
31
+ function parseList(raw: string | undefined): string[] {
32
+ return (raw ?? '').split(/[,;\s]+/).map(entry => entry.trim()).filter(Boolean);
33
+ }
34
+
35
+ function dedupe(values: string[]): string[] {
36
+ return [...new Set(values)];
37
+ }
38
+
39
+ /**
40
+ * Accepts `PROJECT/repository-slug` or `PROJECT/*`. The project key is upper-cased and
41
+ * the slug lower-cased to match the casing the REST API uses, so comparisons are exact.
42
+ */
43
+ function normalizeRepoEntry(entry: string, warn: Warn): string | undefined {
44
+ if (!REPO_ENTRY_RE.test(entry)) {
45
+ warn(`Ignoring merge repository entry that is not "PROJECT/repository-slug" or "PROJECT/*": "${entry}"`);
46
+ return undefined;
47
+ }
48
+ const [projectKey, slug] = entry.split('/');
49
+ if (projectKey === '*') {
50
+ warn(`Ignoring merge repository entry that would allow every project: "${entry}"`);
51
+ return undefined;
52
+ }
53
+ return `${projectKey.toUpperCase()}/${slug.toLowerCase()}`;
54
+ }
55
+
56
+ /** Bare branch names are expanded so operators can write `develop` instead of `refs/heads/develop`. */
57
+ function normalizeRefEntry(entry: string): string {
58
+ return entry.startsWith('refs/') ? entry : `refs/heads/${entry}`;
59
+ }
60
+
61
+ function matchesPattern(value: string, pattern: string): boolean {
62
+ return pattern.endsWith('*') ? value.startsWith(pattern.slice(0, -1)) : value === pattern;
63
+ }
64
+
65
+ /**
66
+ * Reads the merge gateway configuration from the environment. Merging only activates
67
+ * when the flag is set and at least one repository entry is valid; otherwise a warning
68
+ * is logged and the gateway stays disabled.
69
+ */
70
+ export function resolveMergeGateway(options?: { env?: Env; warn?: Warn }): MergeGateway {
71
+ const env = options?.env ?? process.env;
72
+ const warn = options?.warn ?? ((message: string) => console.error(`[merge-gateway] ${message}`));
73
+
74
+ if (!readBool(env, 'BITBUCKET_MERGE_ENABLED')) {
75
+ return DISABLED;
76
+ }
77
+
78
+ const repos = dedupe(
79
+ parseList(env.BITBUCKET_MERGE_ALLOWED_REPOS)
80
+ .map(entry => normalizeRepoEntry(entry, warn))
81
+ .filter((entry): entry is string => Boolean(entry)),
82
+ );
83
+
84
+ if (repos.length === 0) {
85
+ warn(
86
+ 'Merging was enabled but no valid repository is configured (set BITBUCKET_MERGE_ALLOWED_REPOS ' +
87
+ 'to a list of "PROJECT/repository-slug" or "PROJECT/*" entries); the merge tool will stay disabled.',
88
+ );
89
+ return DISABLED;
90
+ }
91
+
92
+ return {
93
+ enabled: true,
94
+ repos,
95
+ targetRefs: dedupe(parseList(env.BITBUCKET_MERGE_ALLOWED_TARGET_REFS).map(normalizeRefEntry)),
96
+ };
97
+ }
98
+
99
+ /** Throws unless the gateway allows merging in this repository. No network call. */
100
+ export function assertRepoMergeAllowed(gateway: MergeGateway, projectKey: string, repositorySlug: string): void {
101
+ if (!gateway.enabled) {
102
+ throw new Error(
103
+ 'Merging pull requests is disabled on this server. Enable it with BITBUCKET_MERGE_ENABLED ' +
104
+ 'and list the allowed repositories in BITBUCKET_MERGE_ALLOWED_REPOS.',
105
+ );
106
+ }
107
+ const target = `${projectKey.toUpperCase()}/${repositorySlug.toLowerCase()}`;
108
+ if (!gateway.repos.some(pattern => matchesPattern(target, pattern))) {
109
+ throw new Error(
110
+ `Merging is not allowed in ${target} on this server. Allowed: ${gateway.repos.join(', ')}.`,
111
+ );
112
+ }
113
+ }
114
+
115
+ /** Throws unless the gateway allows merging into this target ref. A gateway with no ref restriction allows all. */
116
+ export function assertTargetRefMergeAllowed(gateway: MergeGateway, targetRefId: string | undefined): void {
117
+ if (gateway.targetRefs.length === 0) {
118
+ return;
119
+ }
120
+ if (!targetRefId) {
121
+ throw new Error(
122
+ 'Could not determine the target branch of the pull request, and this server restricts which ' +
123
+ 'branches may be merged into (BITBUCKET_MERGE_ALLOWED_TARGET_REFS); refusing to merge.',
124
+ );
125
+ }
126
+ if (!gateway.targetRefs.some(pattern => matchesPattern(targetRefId, pattern))) {
127
+ throw new Error(
128
+ `Merging into ${targetRefId} is not allowed on this server. ` +
129
+ `Allowed target refs: ${gateway.targetRefs.join(', ')}.`,
130
+ );
131
+ }
132
+ }
@@ -148,6 +148,7 @@ interface SimplifiedComment {
148
148
  comments: SimplifiedComment[];
149
149
  threadResolved: boolean;
150
150
  state: string;
151
+ severity: string;
151
152
  }
152
153
 
153
154
  interface SimplifiedActivity {
@@ -167,6 +168,8 @@ export interface SimplifiedPRResponse {
167
168
  prAuthor?: SimplifiedUser;
168
169
  commentCount: number;
169
170
  unresolvedCount: number;
171
+ blockerCount: number;
172
+ unresolvedBlockerCount: number;
170
173
  };
171
174
  }
172
175
 
@@ -271,7 +274,8 @@ function simplifyComment(comment: Comment, ancestorIds: Set<number> = new Set())
271
274
  .filter(childComment => !nextAncestorIds.has(childComment.id))
272
275
  .map(childComment => simplifyComment(childComment, nextAncestorIds)),
273
276
  threadResolved: comment.threadResolved,
274
- state: comment.state
277
+ state: comment.state,
278
+ severity: comment.severity
275
279
  };
276
280
  }
277
281
 
@@ -361,9 +365,11 @@ export function simplifyBitbucketPRComments(
361
365
  // Find PR author (usually the one who OPENED the PR)
362
366
  const prAuthor = activities.find(a => a.action === 'OPENED')?.user;
363
367
 
364
- // Count comments and unresolved threads
368
+ // Count comments, unresolved threads and blocker tasks
365
369
  const comments = activities.filter(a => a.action === 'COMMENTED' && a.comment);
366
370
  const unresolvedCount = comments.filter(a => a.comment && !a.comment.threadResolved).length;
371
+ const blockers = comments.filter(a => a.comment && a.comment.severity === 'BLOCKER');
372
+ const unresolvedBlockerCount = blockers.filter(a => a.comment && a.comment.state !== 'RESOLVED').length;
367
373
 
368
374
  return {
369
375
  isLastPage: filteredResponse.isLastPage ?? true,
@@ -372,7 +378,9 @@ export function simplifyBitbucketPRComments(
372
378
  totalActivities: activities.length,
373
379
  ...(prAuthor && { prAuthor }),
374
380
  commentCount: comments.length,
375
- unresolvedCount
381
+ unresolvedCount,
382
+ blockerCount: blockers.length,
383
+ unresolvedBlockerCount
376
384
  }
377
385
  };
378
386
  }
@@ -0,0 +1,94 @@
1
+ import { handleApiOperation } from '@atlassian-dc-mcp/common';
2
+ import { PullRequestsService } from './bitbucket-client/index.js';
3
+ import {
4
+ BitbucketMutationOutputMode,
5
+ shapeMergeability,
6
+ shapePullRequestAck,
7
+ } from './bitbucket-response-mapper.js';
8
+ import { assertRepoMergeAllowed, assertTargetRefMergeAllowed, type MergeGateway } from './merge-gateway.js';
9
+
10
+ export interface MergePullRequestParams {
11
+ projectKey: string;
12
+ repositorySlug: string;
13
+ pullRequestId: string;
14
+ /** Current PR version, required for optimistic locking. */
15
+ version: number;
16
+ gateway: MergeGateway;
17
+ strategyId?: string;
18
+ message?: string;
19
+ output?: BitbucketMutationOutputMode;
20
+ }
21
+
22
+ /** Read-only mergeability check: reports conflicts and merge-check vetoes. */
23
+ export async function fetchMergeability(projectKey: string, repositorySlug: string, pullRequestId: string) {
24
+ const result = await handleApiOperation(
25
+ () => PullRequestsService.canMerge(projectKey, pullRequestId, repositorySlug),
26
+ 'Error checking pull request mergeability',
27
+ );
28
+
29
+ if (result.success && result.data) {
30
+ return { ...result, data: shapeMergeability(result.data) };
31
+ }
32
+
33
+ return result;
34
+ }
35
+
36
+ function describeVetoes(vetoes: Array<{ summary?: string; detail?: string }>): string {
37
+ return vetoes
38
+ .map(veto => [veto.summary, veto.detail].filter(Boolean).join(' — '))
39
+ .filter(Boolean)
40
+ .join('; ');
41
+ }
42
+
43
+ async function assertMergeable(projectKey: string, repositorySlug: string, pullRequestId: string): Promise<void> {
44
+ const mergeability = shapeMergeability(
45
+ await PullRequestsService.canMerge(projectKey, pullRequestId, repositorySlug),
46
+ );
47
+ if (mergeability.canMerge) {
48
+ return;
49
+ }
50
+ const cause = mergeability.conflicted ? 'it has conflicts' : 'a merge check vetoed the merge';
51
+ const reasons = describeVetoes(mergeability.vetoes);
52
+ throw new Error(`Pull request cannot be merged: ${cause}${reasons ? `. ${reasons}` : ''}`);
53
+ }
54
+
55
+ /** Only fetches the pull request when the operator restricts target branches. */
56
+ async function assertTargetRefAllowed(params: MergePullRequestParams): Promise<void> {
57
+ if (params.gateway.targetRefs.length === 0) {
58
+ return;
59
+ }
60
+ const pullRequest: any = await PullRequestsService.get3(
61
+ params.projectKey,
62
+ params.pullRequestId,
63
+ params.repositorySlug,
64
+ );
65
+ assertTargetRefMergeAllowed(params.gateway, pullRequest?.toRef?.id);
66
+ }
67
+
68
+ /**
69
+ * Merge a pull request under the operator's merge policy. Order matters: the repository
70
+ * (and target branch, when restricted) is checked before any request, and the mergeability
71
+ * check runs before the POST so a conflicted or vetoed pull request is refused without
72
+ * issuing the write.
73
+ */
74
+ export async function mergePullRequest(params: MergePullRequestParams) {
75
+ const { projectKey, repositorySlug, pullRequestId, version, gateway } = params;
76
+
77
+ const result = await handleApiOperation(async () => {
78
+ assertRepoMergeAllowed(gateway, projectKey, repositorySlug);
79
+ await assertTargetRefAllowed(params);
80
+ await assertMergeable(projectKey, repositorySlug, pullRequestId);
81
+
82
+ return PullRequestsService.merge(projectKey, pullRequestId, repositorySlug, String(version), {
83
+ version,
84
+ ...(params.strategyId ? { strategyId: params.strategyId } : {}),
85
+ ...(params.message ? { message: params.message } : {}),
86
+ });
87
+ }, 'Error merging pull request');
88
+
89
+ if (result.success && result.data && params.output !== 'full') {
90
+ return { ...result, data: shapePullRequestAck(result.data) };
91
+ }
92
+
93
+ return result;
94
+ }