bitbucket-gemini-action 1.0.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/.claude/settings.local.json +8 -0
- package/.prettierrc +8 -0
- package/CLAUDE.md +150 -0
- package/README.md +375 -0
- package/bitbucket-pipelines.yml +95 -0
- package/bun.lock +227 -0
- package/dist/prepare.js +7111 -0
- package/examples/bitbucket-pipelines-full.yml +157 -0
- package/examples/bitbucket-pipelines-minimal.yml +22 -0
- package/package.json +33 -0
- package/src/bitbucket/api/client.ts +406 -0
- package/src/bitbucket/context.ts +196 -0
- package/src/bitbucket/data/fetcher.ts +195 -0
- package/src/bitbucket/data/formatter.ts +221 -0
- package/src/bitbucket/operations/comments.ts +236 -0
- package/src/bitbucket/types.ts +262 -0
- package/src/bitbucket/validation/permissions.ts +154 -0
- package/src/bitbucket/validation/trigger.ts +175 -0
- package/src/entrypoints/execute.ts +349 -0
- package/src/entrypoints/prepare.ts +216 -0
- package/src/gemini/client.ts +263 -0
- package/src/gemini/presets.ts +2130 -0
- package/src/gemini/prompts.ts +331 -0
- package/src/gemini/tools.ts +226 -0
- package/src/index.ts +71 -0
- package/src/modes/agent/index.ts +119 -0
- package/src/modes/registry.ts +118 -0
- package/src/modes/tag/index.ts +172 -0
- package/src/modes/types.ts +95 -0
- package/src/utils/env.ts +190 -0
- package/src/utils/retry.ts +149 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitbucket Comment Operations
|
|
3
|
+
* Handles creating and updating PR comments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BitbucketClient } from "../api/client.js";
|
|
7
|
+
import type { BitbucketComment } from "../types.js";
|
|
8
|
+
|
|
9
|
+
export interface TrackingCommentContent {
|
|
10
|
+
status: "pending" | "in_progress" | "completed" | "failed";
|
|
11
|
+
message?: string;
|
|
12
|
+
summary?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
inlineCommentsCount?: number;
|
|
15
|
+
pipelineUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create initial tracking comment
|
|
20
|
+
*/
|
|
21
|
+
export async function createTrackingComment(
|
|
22
|
+
client: BitbucketClient,
|
|
23
|
+
workspace: string,
|
|
24
|
+
repoSlug: string,
|
|
25
|
+
prId: number,
|
|
26
|
+
pipelineUrl?: string
|
|
27
|
+
): Promise<BitbucketComment> {
|
|
28
|
+
const content = formatTrackingComment({
|
|
29
|
+
status: "pending",
|
|
30
|
+
message: "Starting review...",
|
|
31
|
+
pipelineUrl,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return client.createPullRequestComment(workspace, repoSlug, prId, content);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Update tracking comment with progress
|
|
39
|
+
*/
|
|
40
|
+
export async function updateTrackingComment(
|
|
41
|
+
client: BitbucketClient,
|
|
42
|
+
workspace: string,
|
|
43
|
+
repoSlug: string,
|
|
44
|
+
prId: number,
|
|
45
|
+
commentId: number,
|
|
46
|
+
content: TrackingCommentContent
|
|
47
|
+
): Promise<BitbucketComment> {
|
|
48
|
+
const formattedContent = formatTrackingComment(content);
|
|
49
|
+
return client.updatePullRequestComment(
|
|
50
|
+
workspace,
|
|
51
|
+
repoSlug,
|
|
52
|
+
prId,
|
|
53
|
+
commentId,
|
|
54
|
+
formattedContent
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create inline comment on specific code line
|
|
60
|
+
*/
|
|
61
|
+
export async function createInlineComment(
|
|
62
|
+
client: BitbucketClient,
|
|
63
|
+
workspace: string,
|
|
64
|
+
repoSlug: string,
|
|
65
|
+
prId: number,
|
|
66
|
+
filePath: string,
|
|
67
|
+
line: number,
|
|
68
|
+
content: string
|
|
69
|
+
): Promise<BitbucketComment> {
|
|
70
|
+
return client.createPullRequestComment(workspace, repoSlug, prId, content, {
|
|
71
|
+
path: filePath,
|
|
72
|
+
line,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create general PR comment
|
|
78
|
+
*/
|
|
79
|
+
export async function createPRComment(
|
|
80
|
+
client: BitbucketClient,
|
|
81
|
+
workspace: string,
|
|
82
|
+
repoSlug: string,
|
|
83
|
+
prId: number,
|
|
84
|
+
content: string
|
|
85
|
+
): Promise<BitbucketComment> {
|
|
86
|
+
return client.createPullRequestComment(workspace, repoSlug, prId, content);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Format tracking comment content
|
|
91
|
+
*/
|
|
92
|
+
function formatTrackingComment(content: TrackingCommentContent): string {
|
|
93
|
+
const statusEmoji = getStatusEmoji(content.status);
|
|
94
|
+
const statusText = getStatusText(content.status);
|
|
95
|
+
|
|
96
|
+
let markdown = `## 🤖 Gemini Code Review
|
|
97
|
+
|
|
98
|
+
${statusEmoji} **Status**: ${statusText}
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
if (content.message) {
|
|
102
|
+
markdown += `\n${content.message}\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (content.summary) {
|
|
106
|
+
markdown += `\n### Summary\n${content.summary}\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (content.inlineCommentsCount !== undefined) {
|
|
110
|
+
markdown += `\n📝 **Inline comments**: ${content.inlineCommentsCount}\n`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (content.error) {
|
|
114
|
+
markdown += `\n### ❌ Error\n\`\`\`\n${content.error}\n\`\`\`\n`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (content.pipelineUrl) {
|
|
118
|
+
markdown += `\n---\n[View Pipeline](${content.pipelineUrl})\n`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
markdown += `\n---\n_Powered by Gemini AI_`;
|
|
122
|
+
|
|
123
|
+
return markdown;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getStatusEmoji(status: TrackingCommentContent["status"]): string {
|
|
127
|
+
switch (status) {
|
|
128
|
+
case "pending":
|
|
129
|
+
return "⏳";
|
|
130
|
+
case "in_progress":
|
|
131
|
+
return "🔄";
|
|
132
|
+
case "completed":
|
|
133
|
+
return "✅";
|
|
134
|
+
case "failed":
|
|
135
|
+
return "❌";
|
|
136
|
+
default:
|
|
137
|
+
return "❓";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getStatusText(status: TrackingCommentContent["status"]): string {
|
|
142
|
+
switch (status) {
|
|
143
|
+
case "pending":
|
|
144
|
+
return "Pending";
|
|
145
|
+
case "in_progress":
|
|
146
|
+
return "In Progress";
|
|
147
|
+
case "completed":
|
|
148
|
+
return "Completed";
|
|
149
|
+
case "failed":
|
|
150
|
+
return "Failed";
|
|
151
|
+
default:
|
|
152
|
+
return "Unknown";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Format review summary
|
|
158
|
+
*/
|
|
159
|
+
export function formatReviewSummary(
|
|
160
|
+
findings: Array<{
|
|
161
|
+
type: "bug" | "security" | "performance" | "style" | "suggestion";
|
|
162
|
+
severity: "critical" | "major" | "minor" | "info";
|
|
163
|
+
message: string;
|
|
164
|
+
file?: string;
|
|
165
|
+
line?: number;
|
|
166
|
+
}>
|
|
167
|
+
): string {
|
|
168
|
+
if (findings.length === 0) {
|
|
169
|
+
return "No issues found. The code looks good! 👍";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const grouped = {
|
|
173
|
+
critical: findings.filter((f) => f.severity === "critical"),
|
|
174
|
+
major: findings.filter((f) => f.severity === "major"),
|
|
175
|
+
minor: findings.filter((f) => f.severity === "minor"),
|
|
176
|
+
info: findings.filter((f) => f.severity === "info"),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
let summary = "";
|
|
180
|
+
|
|
181
|
+
if (grouped.critical.length > 0) {
|
|
182
|
+
summary += `\n#### 🔴 Critical Issues (${grouped.critical.length})\n`;
|
|
183
|
+
summary += grouped.critical.map((f) => formatFinding(f)).join("\n");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (grouped.major.length > 0) {
|
|
187
|
+
summary += `\n#### 🟠 Major Issues (${grouped.major.length})\n`;
|
|
188
|
+
summary += grouped.major.map((f) => formatFinding(f)).join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (grouped.minor.length > 0) {
|
|
192
|
+
summary += `\n#### 🟡 Minor Issues (${grouped.minor.length})\n`;
|
|
193
|
+
summary += grouped.minor.map((f) => formatFinding(f)).join("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (grouped.info.length > 0) {
|
|
197
|
+
summary += `\n#### 💡 Suggestions (${grouped.info.length})\n`;
|
|
198
|
+
summary += grouped.info.map((f) => formatFinding(f)).join("\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return summary;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function formatFinding(finding: {
|
|
205
|
+
type: string;
|
|
206
|
+
message: string;
|
|
207
|
+
file?: string;
|
|
208
|
+
line?: number;
|
|
209
|
+
}): string {
|
|
210
|
+
const location =
|
|
211
|
+
finding.file && finding.line
|
|
212
|
+
? ` in \`${finding.file}:${finding.line}\``
|
|
213
|
+
: finding.file
|
|
214
|
+
? ` in \`${finding.file}\``
|
|
215
|
+
: "";
|
|
216
|
+
const typeEmoji = getTypeEmoji(finding.type);
|
|
217
|
+
|
|
218
|
+
return `- ${typeEmoji} ${finding.message}${location}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getTypeEmoji(type: string): string {
|
|
222
|
+
switch (type) {
|
|
223
|
+
case "bug":
|
|
224
|
+
return "🐛";
|
|
225
|
+
case "security":
|
|
226
|
+
return "🔒";
|
|
227
|
+
case "performance":
|
|
228
|
+
return "⚡";
|
|
229
|
+
case "style":
|
|
230
|
+
return "🎨";
|
|
231
|
+
case "suggestion":
|
|
232
|
+
return "💡";
|
|
233
|
+
default:
|
|
234
|
+
return "•";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitbucket API Types
|
|
3
|
+
* Based on Bitbucket Cloud REST API v2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface BitbucketRepository {
|
|
7
|
+
uuid: string;
|
|
8
|
+
name: string;
|
|
9
|
+
full_name: string;
|
|
10
|
+
workspace: {
|
|
11
|
+
uuid: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
name: string;
|
|
14
|
+
};
|
|
15
|
+
project?: {
|
|
16
|
+
uuid: string;
|
|
17
|
+
key: string;
|
|
18
|
+
name: string;
|
|
19
|
+
};
|
|
20
|
+
is_private: boolean;
|
|
21
|
+
scm: "git" | "hg";
|
|
22
|
+
mainbranch?: {
|
|
23
|
+
name: string;
|
|
24
|
+
type: string;
|
|
25
|
+
};
|
|
26
|
+
links: {
|
|
27
|
+
self: { href: string };
|
|
28
|
+
html: { href: string };
|
|
29
|
+
clone: Array<{ href: string; name: string }>;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BitbucketUser {
|
|
34
|
+
uuid: string;
|
|
35
|
+
account_id: string;
|
|
36
|
+
display_name: string;
|
|
37
|
+
nickname: string;
|
|
38
|
+
type: "user" | "team";
|
|
39
|
+
links: {
|
|
40
|
+
self: { href: string };
|
|
41
|
+
html: { href: string };
|
|
42
|
+
avatar: { href: string };
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BitbucketPullRequest {
|
|
47
|
+
id: number;
|
|
48
|
+
title: string;
|
|
49
|
+
description: string;
|
|
50
|
+
state: "OPEN" | "MERGED" | "DECLINED" | "SUPERSEDED";
|
|
51
|
+
author: BitbucketUser;
|
|
52
|
+
source: {
|
|
53
|
+
branch: { name: string };
|
|
54
|
+
commit: { hash: string };
|
|
55
|
+
repository: BitbucketRepository;
|
|
56
|
+
};
|
|
57
|
+
destination: {
|
|
58
|
+
branch: { name: string };
|
|
59
|
+
commit: { hash: string };
|
|
60
|
+
repository: BitbucketRepository;
|
|
61
|
+
};
|
|
62
|
+
merge_commit?: { hash: string };
|
|
63
|
+
close_source_branch: boolean;
|
|
64
|
+
created_on: string;
|
|
65
|
+
updated_on: string;
|
|
66
|
+
reason: string;
|
|
67
|
+
reviewers: BitbucketUser[];
|
|
68
|
+
participants: Array<{
|
|
69
|
+
user: BitbucketUser;
|
|
70
|
+
role: "PARTICIPANT" | "REVIEWER";
|
|
71
|
+
approved: boolean;
|
|
72
|
+
state: "approved" | "changes_requested" | null;
|
|
73
|
+
participated_on: string;
|
|
74
|
+
}>;
|
|
75
|
+
links: {
|
|
76
|
+
self: { href: string };
|
|
77
|
+
html: { href: string };
|
|
78
|
+
commits: { href: string };
|
|
79
|
+
approve: { href: string };
|
|
80
|
+
diff: { href: string };
|
|
81
|
+
diffstat: { href: string };
|
|
82
|
+
comments: { href: string };
|
|
83
|
+
activity: { href: string };
|
|
84
|
+
merge: { href: string };
|
|
85
|
+
decline: { href: string };
|
|
86
|
+
};
|
|
87
|
+
task_count: number;
|
|
88
|
+
comment_count: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface BitbucketComment {
|
|
92
|
+
id: number;
|
|
93
|
+
content: {
|
|
94
|
+
raw: string;
|
|
95
|
+
markup: "markdown" | "creole" | "plaintext";
|
|
96
|
+
html: string;
|
|
97
|
+
};
|
|
98
|
+
user: BitbucketUser;
|
|
99
|
+
created_on: string;
|
|
100
|
+
updated_on: string;
|
|
101
|
+
deleted: boolean;
|
|
102
|
+
pending: boolean;
|
|
103
|
+
type: "pullrequest_comment";
|
|
104
|
+
parent?: { id: number };
|
|
105
|
+
inline?: {
|
|
106
|
+
from: number | null;
|
|
107
|
+
to: number | null;
|
|
108
|
+
path: string;
|
|
109
|
+
};
|
|
110
|
+
links: {
|
|
111
|
+
self: { href: string };
|
|
112
|
+
html: { href: string };
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface BitbucketDiffStat {
|
|
117
|
+
type: "diffstat";
|
|
118
|
+
status: "added" | "removed" | "modified" | "renamed";
|
|
119
|
+
lines_added: number;
|
|
120
|
+
lines_removed: number;
|
|
121
|
+
old?: { path: string; type: string };
|
|
122
|
+
new?: { path: string; type: string };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface BitbucketCommit {
|
|
126
|
+
hash: string;
|
|
127
|
+
date: string;
|
|
128
|
+
author: {
|
|
129
|
+
raw: string;
|
|
130
|
+
user?: BitbucketUser;
|
|
131
|
+
};
|
|
132
|
+
message: string;
|
|
133
|
+
summary: {
|
|
134
|
+
raw: string;
|
|
135
|
+
markup: string;
|
|
136
|
+
html: string;
|
|
137
|
+
};
|
|
138
|
+
parents: Array<{ hash: string }>;
|
|
139
|
+
links: {
|
|
140
|
+
self: { href: string };
|
|
141
|
+
html: { href: string };
|
|
142
|
+
diff: { href: string };
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface BitbucketBranch {
|
|
147
|
+
name: string;
|
|
148
|
+
target: BitbucketCommit;
|
|
149
|
+
type: "branch";
|
|
150
|
+
links: {
|
|
151
|
+
self: { href: string };
|
|
152
|
+
commits: { href: string };
|
|
153
|
+
html: { href: string };
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface BitbucketIssue {
|
|
158
|
+
id: number;
|
|
159
|
+
title: string;
|
|
160
|
+
content: {
|
|
161
|
+
raw: string;
|
|
162
|
+
markup: string;
|
|
163
|
+
html: string;
|
|
164
|
+
};
|
|
165
|
+
reporter: BitbucketUser;
|
|
166
|
+
assignee?: BitbucketUser;
|
|
167
|
+
state: "new" | "open" | "resolved" | "on hold" | "invalid" | "duplicate" | "wontfix" | "closed";
|
|
168
|
+
kind: "bug" | "enhancement" | "proposal" | "task";
|
|
169
|
+
priority: "trivial" | "minor" | "major" | "critical" | "blocker";
|
|
170
|
+
milestone?: { name: string };
|
|
171
|
+
version?: { name: string };
|
|
172
|
+
component?: { name: string };
|
|
173
|
+
votes: number;
|
|
174
|
+
watches: number;
|
|
175
|
+
created_on: string;
|
|
176
|
+
updated_on: string;
|
|
177
|
+
links: {
|
|
178
|
+
self: { href: string };
|
|
179
|
+
html: { href: string };
|
|
180
|
+
comments: { href: string };
|
|
181
|
+
attachments: { href: string };
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface BitbucketPipelineVariable {
|
|
186
|
+
key: string;
|
|
187
|
+
value: string;
|
|
188
|
+
secured: boolean;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface BitbucketWebhookPayload {
|
|
192
|
+
actor: BitbucketUser;
|
|
193
|
+
repository: BitbucketRepository;
|
|
194
|
+
pullrequest?: BitbucketPullRequest;
|
|
195
|
+
comment?: BitbucketComment;
|
|
196
|
+
issue?: BitbucketIssue;
|
|
197
|
+
commit?: BitbucketCommit;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface PaginatedResponse<T> {
|
|
201
|
+
size: number;
|
|
202
|
+
page: number;
|
|
203
|
+
pagelen: number;
|
|
204
|
+
next?: string;
|
|
205
|
+
previous?: string;
|
|
206
|
+
values: T[];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Pipeline-specific types
|
|
210
|
+
export interface PipelineContext {
|
|
211
|
+
workspace: string;
|
|
212
|
+
repoSlug: string;
|
|
213
|
+
pipelineUuid: string;
|
|
214
|
+
stepUuid: string;
|
|
215
|
+
buildNumber: number;
|
|
216
|
+
branch: string;
|
|
217
|
+
commit: string;
|
|
218
|
+
prId?: number;
|
|
219
|
+
trigger: PipelineTrigger;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export type PipelineTrigger =
|
|
223
|
+
| "push"
|
|
224
|
+
| "pull-request"
|
|
225
|
+
| "pull-request:created"
|
|
226
|
+
| "pull-request:updated"
|
|
227
|
+
| "pull-request:comment:created"
|
|
228
|
+
| "pull-request:comment:updated"
|
|
229
|
+
| "manual"
|
|
230
|
+
| "schedule"
|
|
231
|
+
| "api";
|
|
232
|
+
|
|
233
|
+
// Parsed context for internal use
|
|
234
|
+
export interface ParsedBitbucketContext {
|
|
235
|
+
eventType: BitbucketEventType;
|
|
236
|
+
workspace: string;
|
|
237
|
+
repoSlug: string;
|
|
238
|
+
repository: {
|
|
239
|
+
workspace: string;
|
|
240
|
+
slug: string;
|
|
241
|
+
fullName: string;
|
|
242
|
+
};
|
|
243
|
+
actor: BitbucketUser;
|
|
244
|
+
pullRequest?: BitbucketPullRequest;
|
|
245
|
+
comment?: BitbucketComment;
|
|
246
|
+
issue?: BitbucketIssue;
|
|
247
|
+
isPR: boolean;
|
|
248
|
+
entityNumber?: number;
|
|
249
|
+
triggerTimestamp: string;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export type BitbucketEventType =
|
|
253
|
+
| "pullrequest:created"
|
|
254
|
+
| "pullrequest:updated"
|
|
255
|
+
| "pullrequest:comment_created"
|
|
256
|
+
| "pullrequest:comment_updated"
|
|
257
|
+
| "issue:created"
|
|
258
|
+
| "issue:updated"
|
|
259
|
+
| "issue:comment_created"
|
|
260
|
+
| "repo:push"
|
|
261
|
+
| "manual"
|
|
262
|
+
| "schedule";
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission Validation
|
|
3
|
+
* Validates user permissions for Bitbucket operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BitbucketClient } from "../api/client.js";
|
|
7
|
+
import type { BitbucketUser } from "../types.js";
|
|
8
|
+
|
|
9
|
+
export type Permission = "read" | "write" | "admin";
|
|
10
|
+
|
|
11
|
+
export interface PermissionCheckResult {
|
|
12
|
+
hasPermission: boolean;
|
|
13
|
+
permission: Permission;
|
|
14
|
+
reason?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if user has write permission on the repository
|
|
19
|
+
*/
|
|
20
|
+
export async function checkWritePermission(
|
|
21
|
+
client: BitbucketClient,
|
|
22
|
+
workspace: string,
|
|
23
|
+
repoSlug: string,
|
|
24
|
+
userId: string
|
|
25
|
+
): Promise<PermissionCheckResult> {
|
|
26
|
+
try {
|
|
27
|
+
const result = await client.checkUserPermissions(
|
|
28
|
+
workspace,
|
|
29
|
+
repoSlug,
|
|
30
|
+
userId
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const hasWrite =
|
|
34
|
+
result.permission === "write" || result.permission === "admin";
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
hasPermission: hasWrite,
|
|
38
|
+
permission: result.permission,
|
|
39
|
+
reason: hasWrite
|
|
40
|
+
? undefined
|
|
41
|
+
: `User has ${result.permission} permission, write required`,
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// If we can't check permissions, assume no permission
|
|
45
|
+
return {
|
|
46
|
+
hasPermission: false,
|
|
47
|
+
permission: "read",
|
|
48
|
+
reason: `Failed to check permissions: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if user is a bot
|
|
55
|
+
*/
|
|
56
|
+
export function isBot(user: BitbucketUser): boolean {
|
|
57
|
+
// Common bot indicators
|
|
58
|
+
const botIndicators = [
|
|
59
|
+
"bot",
|
|
60
|
+
"ci",
|
|
61
|
+
"automation",
|
|
62
|
+
"pipeline",
|
|
63
|
+
"service",
|
|
64
|
+
"app",
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const nickname = user.nickname.toLowerCase();
|
|
68
|
+
const displayName = user.display_name.toLowerCase();
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
user.type === "team" ||
|
|
72
|
+
botIndicators.some(
|
|
73
|
+
(indicator) =>
|
|
74
|
+
nickname.includes(indicator) || displayName.includes(indicator)
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if user is allowed based on allowlist
|
|
81
|
+
*/
|
|
82
|
+
export function isUserAllowed(
|
|
83
|
+
user: BitbucketUser,
|
|
84
|
+
allowedUsers?: string[]
|
|
85
|
+
): boolean {
|
|
86
|
+
if (!allowedUsers || allowedUsers.length === 0) {
|
|
87
|
+
return true; // No restriction
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
allowedUsers.includes(user.account_id) ||
|
|
92
|
+
allowedUsers.includes(user.nickname) ||
|
|
93
|
+
allowedUsers.includes(user.uuid)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validate actor can trigger the action
|
|
99
|
+
*/
|
|
100
|
+
export async function validateActor(
|
|
101
|
+
client: BitbucketClient,
|
|
102
|
+
workspace: string,
|
|
103
|
+
repoSlug: string,
|
|
104
|
+
actor: BitbucketUser,
|
|
105
|
+
options: {
|
|
106
|
+
requireWritePermission?: boolean;
|
|
107
|
+
allowBots?: boolean;
|
|
108
|
+
allowedUsers?: string[];
|
|
109
|
+
} = {}
|
|
110
|
+
): Promise<{
|
|
111
|
+
valid: boolean;
|
|
112
|
+
reason?: string;
|
|
113
|
+
}> {
|
|
114
|
+
const {
|
|
115
|
+
requireWritePermission = true,
|
|
116
|
+
allowBots = false,
|
|
117
|
+
allowedUsers,
|
|
118
|
+
} = options;
|
|
119
|
+
|
|
120
|
+
// Check if bot
|
|
121
|
+
if (!allowBots && isBot(actor)) {
|
|
122
|
+
return {
|
|
123
|
+
valid: false,
|
|
124
|
+
reason: "Bot users are not allowed to trigger this action",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check allowlist
|
|
129
|
+
if (!isUserAllowed(actor, allowedUsers)) {
|
|
130
|
+
return {
|
|
131
|
+
valid: false,
|
|
132
|
+
reason: "User is not in the allowed users list",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check write permission if required
|
|
137
|
+
if (requireWritePermission) {
|
|
138
|
+
const permCheck = await checkWritePermission(
|
|
139
|
+
client,
|
|
140
|
+
workspace,
|
|
141
|
+
repoSlug,
|
|
142
|
+
actor.account_id
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (!permCheck.hasPermission) {
|
|
146
|
+
return {
|
|
147
|
+
valid: false,
|
|
148
|
+
reason: permCheck.reason,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { valid: true };
|
|
154
|
+
}
|