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,157 @@
|
|
|
1
|
+
# Full-featured Bitbucket Pipeline for Gemini Code Review
|
|
2
|
+
# Includes webhook triggers for @gemini mentions
|
|
3
|
+
|
|
4
|
+
image: node:20
|
|
5
|
+
|
|
6
|
+
definitions:
|
|
7
|
+
caches:
|
|
8
|
+
bun: ~/.bun
|
|
9
|
+
|
|
10
|
+
scripts:
|
|
11
|
+
install-bun: &install-bun |
|
|
12
|
+
curl -fsSL https://bun.sh/install | bash
|
|
13
|
+
export BUN_INSTALL="$HOME/.bun"
|
|
14
|
+
export PATH="$BUN_INSTALL/bin:$PATH"
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- step: &setup-gemini-action
|
|
18
|
+
name: Setup Gemini Action
|
|
19
|
+
caches:
|
|
20
|
+
- bun
|
|
21
|
+
- node
|
|
22
|
+
artifacts:
|
|
23
|
+
- .gemini-action/**
|
|
24
|
+
script:
|
|
25
|
+
- *install-bun
|
|
26
|
+
- |
|
|
27
|
+
if [ ! -d ".gemini-action" ]; then
|
|
28
|
+
git clone --depth 1 https://github.com/your-org/bitbucket-gemini-action.git .gemini-action
|
|
29
|
+
fi
|
|
30
|
+
- cd .gemini-action && bun install
|
|
31
|
+
|
|
32
|
+
- step: &run-gemini-prepare
|
|
33
|
+
name: Prepare Review
|
|
34
|
+
caches:
|
|
35
|
+
- bun
|
|
36
|
+
script:
|
|
37
|
+
- *install-bun
|
|
38
|
+
- bun run .gemini-action/src/entrypoints/prepare.ts
|
|
39
|
+
artifacts:
|
|
40
|
+
- .gemini-action-output.json
|
|
41
|
+
|
|
42
|
+
- step: &run-gemini-execute
|
|
43
|
+
name: Execute Review
|
|
44
|
+
caches:
|
|
45
|
+
- bun
|
|
46
|
+
script:
|
|
47
|
+
- *install-bun
|
|
48
|
+
- |
|
|
49
|
+
if [ -f ".gemini-action-output.json" ]; then
|
|
50
|
+
SHOULD_CONTINUE=$(cat .gemini-action-output.json | jq -r '.containsTrigger')
|
|
51
|
+
if [ "$SHOULD_CONTINUE" = "true" ]; then
|
|
52
|
+
bun run .gemini-action/src/entrypoints/execute.ts
|
|
53
|
+
else
|
|
54
|
+
echo "Skipping: No trigger detected"
|
|
55
|
+
fi
|
|
56
|
+
else
|
|
57
|
+
echo "Skipping: No prepare output found"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
pipelines:
|
|
61
|
+
# Automatic review on all PRs
|
|
62
|
+
pull-requests:
|
|
63
|
+
'**':
|
|
64
|
+
- step: *setup-gemini-action
|
|
65
|
+
- step:
|
|
66
|
+
<<: *run-gemini-prepare
|
|
67
|
+
name: Check for @gemini mentions
|
|
68
|
+
- step:
|
|
69
|
+
<<: *run-gemini-execute
|
|
70
|
+
name: Run Gemini Review
|
|
71
|
+
|
|
72
|
+
# Webhook-triggered review (for comment events)
|
|
73
|
+
custom:
|
|
74
|
+
# Triggered by webhook when @gemini is mentioned
|
|
75
|
+
on-mention:
|
|
76
|
+
- variables:
|
|
77
|
+
- name: WEBHOOK_PAYLOAD
|
|
78
|
+
- name: TRIGGER_EVENT
|
|
79
|
+
default: "pullrequest:comment_created"
|
|
80
|
+
- step: *setup-gemini-action
|
|
81
|
+
- step: *run-gemini-prepare
|
|
82
|
+
- step: *run-gemini-execute
|
|
83
|
+
|
|
84
|
+
# Manual full review
|
|
85
|
+
full-review:
|
|
86
|
+
- variables:
|
|
87
|
+
- name: PR_ID
|
|
88
|
+
description: "Pull Request ID to review"
|
|
89
|
+
- name: REVIEW_TYPE
|
|
90
|
+
default: "comprehensive"
|
|
91
|
+
allowed-values:
|
|
92
|
+
- "quick"
|
|
93
|
+
- "comprehensive"
|
|
94
|
+
- "security"
|
|
95
|
+
- step: *setup-gemini-action
|
|
96
|
+
- step:
|
|
97
|
+
name: Run Full Review
|
|
98
|
+
caches:
|
|
99
|
+
- bun
|
|
100
|
+
script:
|
|
101
|
+
- *install-bun
|
|
102
|
+
- |
|
|
103
|
+
case "$REVIEW_TYPE" in
|
|
104
|
+
"quick")
|
|
105
|
+
PROMPT="Quick review: Check for obvious bugs and issues"
|
|
106
|
+
;;
|
|
107
|
+
"security")
|
|
108
|
+
PROMPT="Security-focused review: Check for vulnerabilities, injection risks, auth issues"
|
|
109
|
+
;;
|
|
110
|
+
*)
|
|
111
|
+
PROMPT="Comprehensive review: bugs, security, performance, code quality, best practices"
|
|
112
|
+
;;
|
|
113
|
+
esac
|
|
114
|
+
export PROMPT
|
|
115
|
+
export MODE="agent"
|
|
116
|
+
export BITBUCKET_PR_ID="$PR_ID"
|
|
117
|
+
- bun run .gemini-action/src/entrypoints/prepare.ts
|
|
118
|
+
- bun run .gemini-action/src/entrypoints/execute.ts
|
|
119
|
+
|
|
120
|
+
# Scheduled review of open PRs
|
|
121
|
+
scheduled-review:
|
|
122
|
+
- step:
|
|
123
|
+
name: Review Stale PRs
|
|
124
|
+
script:
|
|
125
|
+
- *install-bun
|
|
126
|
+
- |
|
|
127
|
+
# Get list of open PRs older than 3 days
|
|
128
|
+
STALE_PRS=$(curl -s -u "$BITBUCKET_USERNAME:$BITBUCKET_APP_PASSWORD" \
|
|
129
|
+
"https://api.bitbucket.org/2.0/repositories/$BITBUCKET_WORKSPACE/$BITBUCKET_REPO_SLUG/pullrequests?state=OPEN" \
|
|
130
|
+
| jq -r '.values[] | select(.created_on < (now - 259200 | todate)) | .id')
|
|
131
|
+
|
|
132
|
+
for PR_ID in $STALE_PRS; do
|
|
133
|
+
echo "Reviewing stale PR #$PR_ID"
|
|
134
|
+
export BITBUCKET_PR_ID="$PR_ID"
|
|
135
|
+
export MODE="agent"
|
|
136
|
+
export PROMPT="This PR has been open for a while. Please review and suggest how to move it forward."
|
|
137
|
+
bun run .gemini-action/src/entrypoints/prepare.ts
|
|
138
|
+
bun run .gemini-action/src/entrypoints/execute.ts
|
|
139
|
+
done
|
|
140
|
+
|
|
141
|
+
# Branch-specific configurations
|
|
142
|
+
branches:
|
|
143
|
+
main:
|
|
144
|
+
- step:
|
|
145
|
+
name: Security Review
|
|
146
|
+
script:
|
|
147
|
+
- *install-bun
|
|
148
|
+
- |
|
|
149
|
+
export MODE="agent"
|
|
150
|
+
export PROMPT="Security review for main branch merge"
|
|
151
|
+
- bun run .gemini-action/src/entrypoints/prepare.ts
|
|
152
|
+
- bun run .gemini-action/src/entrypoints/execute.ts
|
|
153
|
+
|
|
154
|
+
# Schedule daily review of stale PRs
|
|
155
|
+
schedules:
|
|
156
|
+
- cron: "0 9 * * 1-5" # 9 AM weekdays
|
|
157
|
+
pipeline: custom/scheduled-review
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Minimal Bitbucket Pipeline for Gemini Code Review
|
|
2
|
+
# Copy this to your repository's bitbucket-pipelines.yml
|
|
3
|
+
|
|
4
|
+
image: node:20
|
|
5
|
+
|
|
6
|
+
pipelines:
|
|
7
|
+
pull-requests:
|
|
8
|
+
'**':
|
|
9
|
+
- step:
|
|
10
|
+
name: AI Code Review
|
|
11
|
+
script:
|
|
12
|
+
# Install Bun
|
|
13
|
+
- curl -fsSL https://bun.sh/install | bash
|
|
14
|
+
- export BUN_INSTALL="$HOME/.bun"
|
|
15
|
+
- export PATH="$BUN_INSTALL/bin:$PATH"
|
|
16
|
+
|
|
17
|
+
# Install and run gemini action
|
|
18
|
+
- npx bitbucket-gemini-action
|
|
19
|
+
|
|
20
|
+
# Required repository variables (Settings > Repository settings > Repository variables):
|
|
21
|
+
# - GEMINI_API_KEY: Your Google Gemini API key
|
|
22
|
+
# - BITBUCKET_ACCESS_TOKEN: Token with PR read/write permissions
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bitbucket-gemini-action",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bitbucket Pipeline action for AI-powered code review using Google Gemini",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "bun build src/entrypoints/prepare.ts --outdir=dist --target=node",
|
|
9
|
+
"test": "bun test",
|
|
10
|
+
"format": "prettier --write .",
|
|
11
|
+
"format:check": "prettier --check .",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"dev": "bun run src/entrypoints/prepare.ts"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@google/generative-ai": "^0.21.0",
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
18
|
+
"node-fetch": "^3.3.2",
|
|
19
|
+
"zod": "^3.24.4"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/bun": "^1.1.14",
|
|
23
|
+
"@types/node": "^22.10.2",
|
|
24
|
+
"prettier": "^3.4.2",
|
|
25
|
+
"typescript": "^5.7.2"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"author": "",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
|
|
33
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitbucket Cloud API Client
|
|
3
|
+
* REST API v2.0 implementation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
BitbucketPullRequest,
|
|
8
|
+
BitbucketComment,
|
|
9
|
+
BitbucketDiffStat,
|
|
10
|
+
BitbucketCommit,
|
|
11
|
+
BitbucketBranch,
|
|
12
|
+
BitbucketIssue,
|
|
13
|
+
BitbucketRepository,
|
|
14
|
+
PaginatedResponse,
|
|
15
|
+
} from "../types.js";
|
|
16
|
+
|
|
17
|
+
const BITBUCKET_API_BASE = "https://api.bitbucket.org/2.0";
|
|
18
|
+
|
|
19
|
+
export interface BitbucketClientOptions {
|
|
20
|
+
username?: string;
|
|
21
|
+
appPassword?: string;
|
|
22
|
+
accessToken?: string;
|
|
23
|
+
baseUrl?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class BitbucketClient {
|
|
27
|
+
private baseUrl: string;
|
|
28
|
+
private authHeader: string;
|
|
29
|
+
|
|
30
|
+
constructor(options: BitbucketClientOptions) {
|
|
31
|
+
this.baseUrl = options.baseUrl || BITBUCKET_API_BASE;
|
|
32
|
+
|
|
33
|
+
if (options.accessToken) {
|
|
34
|
+
this.authHeader = `Bearer ${options.accessToken}`;
|
|
35
|
+
} else if (options.username && options.appPassword) {
|
|
36
|
+
const credentials = Buffer.from(
|
|
37
|
+
`${options.username}:${options.appPassword}`
|
|
38
|
+
).toString("base64");
|
|
39
|
+
this.authHeader = `Basic ${credentials}`;
|
|
40
|
+
} else {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"BitbucketClient requires either accessToken or username/appPassword"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async request<T>(
|
|
48
|
+
endpoint: string,
|
|
49
|
+
options: RequestInit = {}
|
|
50
|
+
): Promise<T> {
|
|
51
|
+
const url = endpoint.startsWith("http")
|
|
52
|
+
? endpoint
|
|
53
|
+
: `${this.baseUrl}${endpoint}`;
|
|
54
|
+
|
|
55
|
+
const response = await fetch(url, {
|
|
56
|
+
...options,
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: this.authHeader,
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
Accept: "application/json",
|
|
61
|
+
...options.headers,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const errorBody = await response.text();
|
|
67
|
+
throw new BitbucketApiError(
|
|
68
|
+
`Bitbucket API error: ${response.status} ${response.statusText}`,
|
|
69
|
+
response.status,
|
|
70
|
+
errorBody
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (response.status === 204) {
|
|
75
|
+
return {} as T;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return response.json() as Promise<T>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async paginatedRequest<T>(
|
|
82
|
+
endpoint: string,
|
|
83
|
+
maxPages = 10
|
|
84
|
+
): Promise<T[]> {
|
|
85
|
+
const results: T[] = [];
|
|
86
|
+
let nextUrl: string | undefined = endpoint;
|
|
87
|
+
let pageCount = 0;
|
|
88
|
+
|
|
89
|
+
while (nextUrl && pageCount < maxPages) {
|
|
90
|
+
const page: PaginatedResponse<T> = await this.request<PaginatedResponse<T>>(nextUrl);
|
|
91
|
+
results.push(...page.values);
|
|
92
|
+
nextUrl = page.next;
|
|
93
|
+
pageCount++;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Repository operations
|
|
100
|
+
async getRepository(
|
|
101
|
+
workspace: string,
|
|
102
|
+
repoSlug: string
|
|
103
|
+
): Promise<BitbucketRepository> {
|
|
104
|
+
return this.request<BitbucketRepository>(
|
|
105
|
+
`/repositories/${workspace}/${repoSlug}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Pull Request operations
|
|
110
|
+
async getPullRequest(
|
|
111
|
+
workspace: string,
|
|
112
|
+
repoSlug: string,
|
|
113
|
+
prId: number
|
|
114
|
+
): Promise<BitbucketPullRequest> {
|
|
115
|
+
return this.request<BitbucketPullRequest>(
|
|
116
|
+
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async getPullRequestDiff(
|
|
121
|
+
workspace: string,
|
|
122
|
+
repoSlug: string,
|
|
123
|
+
prId: number
|
|
124
|
+
): Promise<string> {
|
|
125
|
+
const url = `${this.baseUrl}/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/diff`;
|
|
126
|
+
const response = await fetch(url, {
|
|
127
|
+
headers: {
|
|
128
|
+
Authorization: this.authHeader,
|
|
129
|
+
Accept: "text/plain",
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new BitbucketApiError(
|
|
135
|
+
`Failed to get PR diff: ${response.status}`,
|
|
136
|
+
response.status
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return response.text();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async getPullRequestDiffStat(
|
|
144
|
+
workspace: string,
|
|
145
|
+
repoSlug: string,
|
|
146
|
+
prId: number
|
|
147
|
+
): Promise<BitbucketDiffStat[]> {
|
|
148
|
+
return this.paginatedRequest<BitbucketDiffStat>(
|
|
149
|
+
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/diffstat`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async getPullRequestComments(
|
|
154
|
+
workspace: string,
|
|
155
|
+
repoSlug: string,
|
|
156
|
+
prId: number
|
|
157
|
+
): Promise<BitbucketComment[]> {
|
|
158
|
+
return this.paginatedRequest<BitbucketComment>(
|
|
159
|
+
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/comments`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async getPullRequestCommits(
|
|
164
|
+
workspace: string,
|
|
165
|
+
repoSlug: string,
|
|
166
|
+
prId: number
|
|
167
|
+
): Promise<BitbucketCommit[]> {
|
|
168
|
+
return this.paginatedRequest<BitbucketCommit>(
|
|
169
|
+
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/commits`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async createPullRequestComment(
|
|
174
|
+
workspace: string,
|
|
175
|
+
repoSlug: string,
|
|
176
|
+
prId: number,
|
|
177
|
+
content: string,
|
|
178
|
+
inline?: { path: string; line: number }
|
|
179
|
+
): Promise<BitbucketComment> {
|
|
180
|
+
const body: Record<string, unknown> = {
|
|
181
|
+
content: {
|
|
182
|
+
raw: content,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (inline) {
|
|
187
|
+
body.inline = {
|
|
188
|
+
to: inline.line,
|
|
189
|
+
path: inline.path,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return this.request<BitbucketComment>(
|
|
194
|
+
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/comments`,
|
|
195
|
+
{
|
|
196
|
+
method: "POST",
|
|
197
|
+
body: JSON.stringify(body),
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async updatePullRequestComment(
|
|
203
|
+
workspace: string,
|
|
204
|
+
repoSlug: string,
|
|
205
|
+
prId: number,
|
|
206
|
+
commentId: number,
|
|
207
|
+
content: string
|
|
208
|
+
): Promise<BitbucketComment> {
|
|
209
|
+
return this.request<BitbucketComment>(
|
|
210
|
+
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/comments/${commentId}`,
|
|
211
|
+
{
|
|
212
|
+
method: "PUT",
|
|
213
|
+
body: JSON.stringify({
|
|
214
|
+
content: {
|
|
215
|
+
raw: content,
|
|
216
|
+
},
|
|
217
|
+
}),
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async deletePullRequestComment(
|
|
223
|
+
workspace: string,
|
|
224
|
+
repoSlug: string,
|
|
225
|
+
prId: number,
|
|
226
|
+
commentId: number
|
|
227
|
+
): Promise<void> {
|
|
228
|
+
await this.request<void>(
|
|
229
|
+
`/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/comments/${commentId}`,
|
|
230
|
+
{
|
|
231
|
+
method: "DELETE",
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Branch operations
|
|
237
|
+
async getBranch(
|
|
238
|
+
workspace: string,
|
|
239
|
+
repoSlug: string,
|
|
240
|
+
branchName: string
|
|
241
|
+
): Promise<BitbucketBranch> {
|
|
242
|
+
return this.request<BitbucketBranch>(
|
|
243
|
+
`/repositories/${workspace}/${repoSlug}/refs/branches/${encodeURIComponent(branchName)}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async createBranch(
|
|
248
|
+
workspace: string,
|
|
249
|
+
repoSlug: string,
|
|
250
|
+
branchName: string,
|
|
251
|
+
targetCommit: string
|
|
252
|
+
): Promise<BitbucketBranch> {
|
|
253
|
+
return this.request<BitbucketBranch>(
|
|
254
|
+
`/repositories/${workspace}/${repoSlug}/refs/branches`,
|
|
255
|
+
{
|
|
256
|
+
method: "POST",
|
|
257
|
+
body: JSON.stringify({
|
|
258
|
+
name: branchName,
|
|
259
|
+
target: {
|
|
260
|
+
hash: targetCommit,
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async deleteBranch(
|
|
268
|
+
workspace: string,
|
|
269
|
+
repoSlug: string,
|
|
270
|
+
branchName: string
|
|
271
|
+
): Promise<void> {
|
|
272
|
+
await this.request<void>(
|
|
273
|
+
`/repositories/${workspace}/${repoSlug}/refs/branches/${encodeURIComponent(branchName)}`,
|
|
274
|
+
{
|
|
275
|
+
method: "DELETE",
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Commit operations
|
|
281
|
+
async getCommit(
|
|
282
|
+
workspace: string,
|
|
283
|
+
repoSlug: string,
|
|
284
|
+
commitHash: string
|
|
285
|
+
): Promise<BitbucketCommit> {
|
|
286
|
+
return this.request<BitbucketCommit>(
|
|
287
|
+
`/repositories/${workspace}/${repoSlug}/commit/${commitHash}`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async getFileContent(
|
|
292
|
+
workspace: string,
|
|
293
|
+
repoSlug: string,
|
|
294
|
+
commitHash: string,
|
|
295
|
+
filePath: string
|
|
296
|
+
): Promise<string> {
|
|
297
|
+
const url = `${this.baseUrl}/repositories/${workspace}/${repoSlug}/src/${commitHash}/${encodeURIComponent(filePath)}`;
|
|
298
|
+
const response = await fetch(url, {
|
|
299
|
+
headers: {
|
|
300
|
+
Authorization: this.authHeader,
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (!response.ok) {
|
|
305
|
+
throw new BitbucketApiError(
|
|
306
|
+
`Failed to get file content: ${response.status}`,
|
|
307
|
+
response.status
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return response.text();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Issue operations (if issue tracker is enabled)
|
|
315
|
+
async getIssue(
|
|
316
|
+
workspace: string,
|
|
317
|
+
repoSlug: string,
|
|
318
|
+
issueId: number
|
|
319
|
+
): Promise<BitbucketIssue> {
|
|
320
|
+
return this.request<BitbucketIssue>(
|
|
321
|
+
`/repositories/${workspace}/${repoSlug}/issues/${issueId}`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async getIssueComments(
|
|
326
|
+
workspace: string,
|
|
327
|
+
repoSlug: string,
|
|
328
|
+
issueId: number
|
|
329
|
+
): Promise<BitbucketComment[]> {
|
|
330
|
+
return this.paginatedRequest<BitbucketComment>(
|
|
331
|
+
`/repositories/${workspace}/${repoSlug}/issues/${issueId}/comments`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async createIssueComment(
|
|
336
|
+
workspace: string,
|
|
337
|
+
repoSlug: string,
|
|
338
|
+
issueId: number,
|
|
339
|
+
content: string
|
|
340
|
+
): Promise<BitbucketComment> {
|
|
341
|
+
return this.request<BitbucketComment>(
|
|
342
|
+
`/repositories/${workspace}/${repoSlug}/issues/${issueId}/comments`,
|
|
343
|
+
{
|
|
344
|
+
method: "POST",
|
|
345
|
+
body: JSON.stringify({
|
|
346
|
+
content: {
|
|
347
|
+
raw: content,
|
|
348
|
+
},
|
|
349
|
+
}),
|
|
350
|
+
}
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// User operations
|
|
355
|
+
async getCurrentUser(): Promise<{
|
|
356
|
+
uuid: string;
|
|
357
|
+
display_name: string;
|
|
358
|
+
account_id: string;
|
|
359
|
+
}> {
|
|
360
|
+
return this.request("/user");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async checkUserPermissions(
|
|
364
|
+
workspace: string,
|
|
365
|
+
repoSlug: string,
|
|
366
|
+
userId: string
|
|
367
|
+
): Promise<{ permission: "read" | "write" | "admin" }> {
|
|
368
|
+
const response = await this.request<{
|
|
369
|
+
values: Array<{ permission: "read" | "write" | "admin" }>;
|
|
370
|
+
}>(
|
|
371
|
+
`/repositories/${workspace}/${repoSlug}/permissions-config/users/${userId}`
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
return { permission: response.values[0]?.permission || "read" };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export class BitbucketApiError extends Error {
|
|
379
|
+
constructor(
|
|
380
|
+
message: string,
|
|
381
|
+
public statusCode: number,
|
|
382
|
+
public responseBody?: string
|
|
383
|
+
) {
|
|
384
|
+
super(message);
|
|
385
|
+
this.name = "BitbucketApiError";
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Factory function for creating client from environment variables
|
|
390
|
+
export function createBitbucketClientFromEnv(): BitbucketClient {
|
|
391
|
+
const accessToken = process.env.BITBUCKET_ACCESS_TOKEN;
|
|
392
|
+
const username = process.env.BITBUCKET_USERNAME;
|
|
393
|
+
const appPassword = process.env.BITBUCKET_APP_PASSWORD;
|
|
394
|
+
|
|
395
|
+
if (accessToken) {
|
|
396
|
+
return new BitbucketClient({ accessToken });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (username && appPassword) {
|
|
400
|
+
return new BitbucketClient({ username, appPassword });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
throw new Error(
|
|
404
|
+
"Missing Bitbucket credentials. Set BITBUCKET_ACCESS_TOKEN or BITBUCKET_USERNAME and BITBUCKET_APP_PASSWORD"
|
|
405
|
+
);
|
|
406
|
+
}
|