@atlassian-dc-mcp/bitbucket 0.34.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +43 -1
  3. package/build/__tests__/bitbucket-service.test.js +31 -1
  4. package/build/__tests__/bitbucket-service.test.js.map +1 -1
  5. package/build/__tests__/pr-merge.test.js +5 -4
  6. package/build/__tests__/pr-merge.test.js.map +1 -1
  7. package/build/__tests__/pr-state.test.d.ts +2 -0
  8. package/build/__tests__/pr-state.test.d.ts.map +1 -0
  9. package/build/__tests__/pr-state.test.js +118 -0
  10. package/build/__tests__/pr-state.test.js.map +1 -0
  11. package/build/__tests__/repo-gateway.test.d.ts +2 -0
  12. package/build/__tests__/repo-gateway.test.d.ts.map +1 -0
  13. package/build/__tests__/{merge-gateway.test.js → repo-gateway.test.js} +70 -10
  14. package/build/__tests__/repo-gateway.test.js.map +1 -0
  15. package/build/bitbucket-service.d.ts +31 -0
  16. package/build/bitbucket-service.d.ts.map +1 -1
  17. package/build/bitbucket-service.js +43 -0
  18. package/build/bitbucket-service.js.map +1 -1
  19. package/build/index.js +29 -1
  20. package/build/index.js.map +1 -1
  21. package/build/pr-merge.d.ts +2 -2
  22. package/build/pr-merge.d.ts.map +1 -1
  23. package/build/pr-merge.js +2 -2
  24. package/build/pr-merge.js.map +1 -1
  25. package/build/pr-state.d.ts +33 -0
  26. package/build/pr-state.d.ts.map +1 -0
  27. package/build/pr-state.js +37 -0
  28. package/build/pr-state.js.map +1 -0
  29. package/build/repo-gateway.d.ts +57 -0
  30. package/build/repo-gateway.d.ts.map +1 -0
  31. package/build/repo-gateway.js +122 -0
  32. package/build/repo-gateway.js.map +1 -0
  33. package/package.json +3 -3
  34. package/src/__tests__/bitbucket-service.test.ts +37 -1
  35. package/src/__tests__/pr-merge.test.ts +5 -5
  36. package/src/__tests__/pr-state.test.ts +150 -0
  37. package/src/__tests__/{merge-gateway.test.ts → repo-gateway.test.ts} +83 -11
  38. package/src/bitbucket-service.ts +50 -0
  39. package/src/index.ts +42 -1
  40. package/src/pr-merge.ts +3 -3
  41. package/src/pr-state.ts +68 -0
  42. package/src/repo-gateway.ts +183 -0
  43. package/tsconfig.tsbuildinfo +1 -1
  44. package/build/__tests__/merge-gateway.test.d.ts +0 -2
  45. package/build/__tests__/merge-gateway.test.d.ts.map +0 -1
  46. package/build/__tests__/merge-gateway.test.js.map +0 -1
  47. package/build/merge-gateway.d.ts +0 -34
  48. package/build/merge-gateway.d.ts.map +0 -1
  49. package/build/merge-gateway.js +0 -86
  50. package/build/merge-gateway.js.map +0 -1
  51. package/src/merge-gateway.ts +0 -132
@@ -0,0 +1,183 @@
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
+
13
+ /** Names an operation in refusal messages and points the operator at the variables that open it. */
14
+ export interface GatewayPolicy {
15
+ /** Short operation name, used to tag warnings. */
16
+ name: string;
17
+ /** Gerund that starts a refusal message, e.g. "Merging". */
18
+ gerund: string;
19
+ flagVar: string;
20
+ reposVar: string;
21
+ /** Omitted for operations that have no meaningful target-branch restriction. */
22
+ targetRefsVar?: string;
23
+ }
24
+
25
+ export const MERGE_POLICY: GatewayPolicy = {
26
+ name: 'merge',
27
+ gerund: 'Merging',
28
+ flagVar: 'BITBUCKET_MERGE_ENABLED',
29
+ reposVar: 'BITBUCKET_MERGE_ALLOWED_REPOS',
30
+ targetRefsVar: 'BITBUCKET_MERGE_ALLOWED_TARGET_REFS',
31
+ };
32
+
33
+ /**
34
+ * Declining has no target-ref restriction: which branch a pull request would have landed on
35
+ * says nothing about whether abandoning it is safe.
36
+ */
37
+ export const DECLINE_POLICY: GatewayPolicy = {
38
+ name: 'decline',
39
+ gerund: 'Declining',
40
+ flagVar: 'BITBUCKET_DECLINE_ENABLED',
41
+ reposVar: 'BITBUCKET_DECLINE_ALLOWED_REPOS',
42
+ };
43
+
44
+ export interface RepoGateway {
45
+ /** Whether the operation is enabled and at least one valid repository pattern resolved. */
46
+ enabled: boolean;
47
+ /** Allowed `PROJECT/repository-slug` targets; a `PROJECT/*` entry allows the whole project. */
48
+ repos: string[];
49
+ /** Allowed target refs; a trailing `*` matches a prefix. Empty means any ref in an allowed repository. */
50
+ targetRefs: string[];
51
+ policy: GatewayPolicy;
52
+ }
53
+
54
+ type Env = Record<string, string | undefined>;
55
+ type Warn = (message: string) => void;
56
+
57
+ export interface ResolveGatewayOptions {
58
+ env?: Env;
59
+ warn?: Warn;
60
+ }
61
+
62
+ const REPO_ENTRY_RE = /^[^\s/]+\/[^\s/]+$/;
63
+
64
+ function readBool(env: Env, name: string): boolean {
65
+ const value = env[name]?.trim().toLowerCase();
66
+ return value === 'true' || value === '1' || value === 'yes';
67
+ }
68
+
69
+ function parseList(raw: string | undefined): string[] {
70
+ return (raw ?? '').split(/[,;\s]+/).map(entry => entry.trim()).filter(Boolean);
71
+ }
72
+
73
+ function dedupe(values: string[]): string[] {
74
+ return [...new Set(values)];
75
+ }
76
+
77
+ /**
78
+ * Accepts `PROJECT/repository-slug` or `PROJECT/*`. The project key is upper-cased and
79
+ * the slug lower-cased to match the casing the REST API uses, so comparisons are exact.
80
+ */
81
+ function normalizeRepoEntry(entry: string, warn: Warn): string | undefined {
82
+ if (!REPO_ENTRY_RE.test(entry)) {
83
+ warn(`Ignoring repository entry that is not "PROJECT/repository-slug" or "PROJECT/*": "${entry}"`);
84
+ return undefined;
85
+ }
86
+ const [projectKey, slug] = entry.split('/');
87
+ if (projectKey === '*') {
88
+ warn(`Ignoring repository entry that would allow every project: "${entry}"`);
89
+ return undefined;
90
+ }
91
+ return `${projectKey.toUpperCase()}/${slug.toLowerCase()}`;
92
+ }
93
+
94
+ /** Bare branch names are expanded so operators can write `develop` instead of `refs/heads/develop`. */
95
+ function normalizeRefEntry(entry: string): string {
96
+ return entry.startsWith('refs/') ? entry : `refs/heads/${entry}`;
97
+ }
98
+
99
+ function matchesPattern(value: string, pattern: string): boolean {
100
+ return pattern.endsWith('*') ? value.startsWith(pattern.slice(0, -1)) : value === pattern;
101
+ }
102
+
103
+ function disabledGateway(policy: GatewayPolicy): RepoGateway {
104
+ return { enabled: false, repos: [], targetRefs: [], policy };
105
+ }
106
+
107
+ /**
108
+ * Reads one operation's gateway configuration from the environment. The operation only
109
+ * activates when its flag is set and at least one repository entry is valid; otherwise a
110
+ * warning is logged and the gateway stays disabled.
111
+ */
112
+ export function resolveGateway(policy: GatewayPolicy, options?: ResolveGatewayOptions): RepoGateway {
113
+ const env = options?.env ?? process.env;
114
+ const warn = options?.warn ?? ((message: string) => console.error(`[${policy.name}-gateway] ${message}`));
115
+
116
+ if (!readBool(env, policy.flagVar)) {
117
+ return disabledGateway(policy);
118
+ }
119
+
120
+ const repos = dedupe(
121
+ parseList(env[policy.reposVar])
122
+ .map(entry => normalizeRepoEntry(entry, warn))
123
+ .filter((entry): entry is string => Boolean(entry)),
124
+ );
125
+
126
+ if (repos.length === 0) {
127
+ warn(
128
+ `${policy.gerund} was enabled but no valid repository is configured (set ${policy.reposVar} ` +
129
+ 'to a list of "PROJECT/repository-slug" or "PROJECT/*" entries); the tool will stay disabled.',
130
+ );
131
+ return disabledGateway(policy);
132
+ }
133
+
134
+ const targetRefs = policy.targetRefsVar
135
+ ? dedupe(parseList(env[policy.targetRefsVar]).map(normalizeRefEntry))
136
+ : [];
137
+
138
+ return { enabled: true, repos, targetRefs, policy };
139
+ }
140
+
141
+ export function resolveMergeGateway(options?: ResolveGatewayOptions): RepoGateway {
142
+ return resolveGateway(MERGE_POLICY, options);
143
+ }
144
+
145
+ export function resolveDeclineGateway(options?: ResolveGatewayOptions): RepoGateway {
146
+ return resolveGateway(DECLINE_POLICY, options);
147
+ }
148
+
149
+ /** Throws unless the gateway allows its operation in this repository. No network call. */
150
+ export function assertRepoOperationAllowed(gateway: RepoGateway, projectKey: string, repositorySlug: string): void {
151
+ const { policy } = gateway;
152
+ if (!gateway.enabled) {
153
+ throw new Error(
154
+ `${policy.gerund} pull requests is disabled on this server. Enable it with ${policy.flagVar} ` +
155
+ `and list the allowed repositories in ${policy.reposVar}.`,
156
+ );
157
+ }
158
+ const target = `${projectKey.toUpperCase()}/${repositorySlug.toLowerCase()}`;
159
+ if (!gateway.repos.some(pattern => matchesPattern(target, pattern))) {
160
+ throw new Error(
161
+ `${policy.gerund} is not allowed in ${target} on this server. Allowed: ${gateway.repos.join(', ')}.`,
162
+ );
163
+ }
164
+ }
165
+
166
+ /** Throws unless the gateway allows merging into this target ref. A gateway with no ref restriction allows all. */
167
+ export function assertTargetRefMergeAllowed(gateway: RepoGateway, targetRefId: string | undefined): void {
168
+ if (gateway.targetRefs.length === 0) {
169
+ return;
170
+ }
171
+ if (!targetRefId) {
172
+ throw new Error(
173
+ 'Could not determine the target branch of the pull request, and this server restricts which ' +
174
+ `branches may be merged into (${gateway.policy.targetRefsVar}); refusing to merge.`,
175
+ );
176
+ }
177
+ if (!gateway.targetRefs.some(pattern => matchesPattern(targetRefId, pattern))) {
178
+ throw new Error(
179
+ `Merging into ${targetRefId} is not allowed on this server. ` +
180
+ `Allowed target refs: ${gateway.targetRefs.join(', ')}.`,
181
+ );
182
+ }
183
+ }