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.
@@ -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
+ }