@forge-glance/sdk 0.1.0 → 0.2.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/dist/GitHubProvider.d.ts +19 -3
- package/dist/GitHubProvider.js +217 -1
- package/dist/GitLabProvider.d.ts +25 -3
- package/dist/GitLabProvider.js +285 -1
- package/dist/GitProvider.d.ts +89 -1
- package/dist/index.d.ts +14 -14
- package/dist/index.js +502 -2
- package/dist/providers.js +502 -2
- package/dist/types.d.ts +103 -0
- package/package.json +1 -1
- package/src/GitHubProvider.ts +534 -115
- package/src/GitLabProvider.ts +519 -56
- package/src/GitProvider.ts +155 -13
- package/src/index.ts +25 -15
- package/src/types.ts +111 -0
package/dist/GitHubProvider.d.ts
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* Base URL: "https://api.github.com" for github.com; for GHES, the user
|
|
12
12
|
* provides the instance URL and we append "/api/v3".
|
|
13
13
|
*/
|
|
14
|
-
import type { GitProvider } from
|
|
15
|
-
import type { MRDetail, PullRequest, UserRef } from
|
|
16
|
-
import { type ForgeLogger } from
|
|
14
|
+
import type { GitProvider } from './GitProvider.ts';
|
|
15
|
+
import type { BranchProtectionRule, CreatePullRequestInput, MergePullRequestInput, MRDetail, ProviderCapabilities, PullRequest, UpdatePullRequestInput, UserRef } from './types.ts';
|
|
16
|
+
import { type ForgeLogger } from './logger.ts';
|
|
17
17
|
export declare class GitHubProvider implements GitProvider {
|
|
18
18
|
readonly providerName: "github";
|
|
19
19
|
readonly baseURL: string;
|
|
@@ -29,11 +29,27 @@ export declare class GitHubProvider implements GitProvider {
|
|
|
29
29
|
constructor(baseURL: string, token: string, options?: {
|
|
30
30
|
logger?: ForgeLogger;
|
|
31
31
|
});
|
|
32
|
+
readonly capabilities: ProviderCapabilities;
|
|
32
33
|
validateToken(): Promise<UserRef>;
|
|
33
34
|
fetchPullRequests(): Promise<PullRequest[]>;
|
|
34
35
|
fetchSingleMR(projectPath: string, mrIid: number, _currentUserNumericId: number | null): Promise<PullRequest | null>;
|
|
35
36
|
fetchMRDiscussions(repositoryId: string, mrIid: number): Promise<MRDetail>;
|
|
37
|
+
fetchBranchProtectionRules(projectPath: string): Promise<BranchProtectionRule[]>;
|
|
38
|
+
deleteBranch(projectPath: string, branch: string): Promise<void>;
|
|
39
|
+
fetchPullRequestByBranch(projectPath: string, sourceBranch: string): Promise<PullRequest | null>;
|
|
40
|
+
createPullRequest(input: CreatePullRequestInput): Promise<PullRequest>;
|
|
41
|
+
updatePullRequest(projectPath: string, mrIid: number, input: UpdatePullRequestInput): Promise<PullRequest>;
|
|
36
42
|
restRequest(method: string, path: string, body?: unknown): Promise<Response>;
|
|
43
|
+
mergePullRequest(projectPath: string, mrIid: number, input?: MergePullRequestInput): Promise<PullRequest>;
|
|
44
|
+
approvePullRequest(projectPath: string, mrIid: number): Promise<void>;
|
|
45
|
+
unapprovePullRequest(_projectPath: string, _mrIid: number): Promise<void>;
|
|
46
|
+
rebasePullRequest(_projectPath: string, _mrIid: number): Promise<void>;
|
|
47
|
+
setAutoMerge(_projectPath: string, _mrIid: number): Promise<void>;
|
|
48
|
+
cancelAutoMerge(_projectPath: string, _mrIid: number): Promise<void>;
|
|
49
|
+
resolveDiscussion(_projectPath: string, _mrIid: number, _discussionId: string): Promise<void>;
|
|
50
|
+
unresolveDiscussion(_projectPath: string, _mrIid: number, _discussionId: string): Promise<void>;
|
|
51
|
+
retryPipeline(projectPath: string, pipelineId: number): Promise<void>;
|
|
52
|
+
requestReReview(projectPath: string, mrIid: number, reviewerUsernames?: string[]): Promise<void>;
|
|
37
53
|
private api;
|
|
38
54
|
/**
|
|
39
55
|
* Search for PRs using the GitHub search API.
|
package/dist/GitHubProvider.js
CHANGED
|
@@ -95,6 +95,16 @@ class GitHubProvider {
|
|
|
95
95
|
this.apiBase = `${this.baseURL}/api/v3`;
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
+
capabilities = {
|
|
99
|
+
canMerge: true,
|
|
100
|
+
canApprove: true,
|
|
101
|
+
canUnapprove: false,
|
|
102
|
+
canRebase: false,
|
|
103
|
+
canAutoMerge: false,
|
|
104
|
+
canResolveDiscussions: false,
|
|
105
|
+
canRetryPipeline: true,
|
|
106
|
+
canRequestReReview: true
|
|
107
|
+
};
|
|
98
108
|
async validateToken() {
|
|
99
109
|
const res = await this.api("GET", "/user");
|
|
100
110
|
if (!res.ok) {
|
|
@@ -136,7 +146,9 @@ class GitHubProvider {
|
|
|
136
146
|
]);
|
|
137
147
|
return this.toPullRequest(pr, prRoles, reviews, checkRuns);
|
|
138
148
|
}));
|
|
139
|
-
this.log.debug("GitHubProvider.fetchPullRequests", {
|
|
149
|
+
this.log.debug("GitHubProvider.fetchPullRequests", {
|
|
150
|
+
count: results.length
|
|
151
|
+
});
|
|
140
152
|
return results;
|
|
141
153
|
}
|
|
142
154
|
async fetchSingleMR(projectPath, mrIid, _currentUserNumericId) {
|
|
@@ -207,9 +219,213 @@ class GitHubProvider {
|
|
|
207
219
|
}
|
|
208
220
|
return { mrIid, repositoryId, discussions };
|
|
209
221
|
}
|
|
222
|
+
async fetchBranchProtectionRules(projectPath) {
|
|
223
|
+
const res = await this.api("GET", `/repos/${projectPath}/branches?protected=true&per_page=100`);
|
|
224
|
+
if (!res.ok) {
|
|
225
|
+
throw new Error(`fetchBranchProtectionRules failed: ${res.status} ${await res.text()}`);
|
|
226
|
+
}
|
|
227
|
+
const branches = await res.json();
|
|
228
|
+
const rules = [];
|
|
229
|
+
for (const b of branches) {
|
|
230
|
+
if (!b.protected)
|
|
231
|
+
continue;
|
|
232
|
+
const detailRes = await this.api("GET", `/repos/${projectPath}/branches/${encodeURIComponent(b.name)}/protection`);
|
|
233
|
+
if (!detailRes.ok) {
|
|
234
|
+
rules.push({
|
|
235
|
+
pattern: b.name,
|
|
236
|
+
allowForcePush: false,
|
|
237
|
+
allowDeletion: false,
|
|
238
|
+
requiredApprovals: 0,
|
|
239
|
+
requireStatusChecks: false
|
|
240
|
+
});
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const detail = await detailRes.json();
|
|
244
|
+
rules.push({
|
|
245
|
+
pattern: b.name,
|
|
246
|
+
allowForcePush: detail.allow_force_pushes?.enabled ?? false,
|
|
247
|
+
allowDeletion: detail.allow_deletions?.enabled ?? false,
|
|
248
|
+
requiredApprovals: detail.required_pull_request_reviews?.required_approving_review_count ?? 0,
|
|
249
|
+
requireStatusChecks: detail.required_status_checks !== null && detail.required_status_checks !== undefined,
|
|
250
|
+
raw: detail
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return rules;
|
|
254
|
+
}
|
|
255
|
+
async deleteBranch(projectPath, branch) {
|
|
256
|
+
const res = await this.api("DELETE", `/repos/${projectPath}/git/refs/heads/${encodeURIComponent(branch)}`);
|
|
257
|
+
if (!res.ok) {
|
|
258
|
+
throw new Error(`deleteBranch failed: ${res.status} ${await res.text()}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async fetchPullRequestByBranch(projectPath, sourceBranch) {
|
|
262
|
+
const res = await this.api("GET", `/repos/${projectPath}/pulls?head=${projectPath.split("/")[0]}:${encodeURIComponent(sourceBranch)}&state=open&per_page=1`);
|
|
263
|
+
if (!res.ok) {
|
|
264
|
+
this.log.warn("fetchPullRequestByBranch failed", {
|
|
265
|
+
projectPath,
|
|
266
|
+
sourceBranch,
|
|
267
|
+
status: res.status
|
|
268
|
+
});
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const prs = await res.json();
|
|
272
|
+
if (!prs[0])
|
|
273
|
+
return null;
|
|
274
|
+
return this.fetchSingleMR(projectPath, prs[0].number, null);
|
|
275
|
+
}
|
|
276
|
+
async createPullRequest(input) {
|
|
277
|
+
const body = {
|
|
278
|
+
head: input.sourceBranch,
|
|
279
|
+
base: input.targetBranch,
|
|
280
|
+
title: input.title
|
|
281
|
+
};
|
|
282
|
+
if (input.description != null)
|
|
283
|
+
body.body = input.description;
|
|
284
|
+
if (input.draft != null)
|
|
285
|
+
body.draft = input.draft;
|
|
286
|
+
const res = await this.api("POST", `/repos/${input.projectPath}/pulls`, body);
|
|
287
|
+
if (!res.ok) {
|
|
288
|
+
const text = await res.text();
|
|
289
|
+
throw new Error(`createPullRequest failed: ${res.status} ${text}`);
|
|
290
|
+
}
|
|
291
|
+
const created = await res.json();
|
|
292
|
+
if (input.reviewers?.length) {
|
|
293
|
+
await this.api("POST", `/repos/${input.projectPath}/pulls/${created.number}/requested_reviewers`, {
|
|
294
|
+
reviewers: input.reviewers
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (input.assignees?.length) {
|
|
298
|
+
await this.api("POST", `/repos/${input.projectPath}/issues/${created.number}/assignees`, {
|
|
299
|
+
assignees: input.assignees
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
if (input.labels?.length) {
|
|
303
|
+
await this.api("POST", `/repos/${input.projectPath}/issues/${created.number}/labels`, {
|
|
304
|
+
labels: input.labels
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const pr = await this.fetchSingleMR(input.projectPath, created.number, null);
|
|
308
|
+
if (!pr)
|
|
309
|
+
throw new Error("Created PR but failed to fetch it back");
|
|
310
|
+
return pr;
|
|
311
|
+
}
|
|
312
|
+
async updatePullRequest(projectPath, mrIid, input) {
|
|
313
|
+
const body = {};
|
|
314
|
+
if (input.title != null)
|
|
315
|
+
body.title = input.title;
|
|
316
|
+
if (input.description != null)
|
|
317
|
+
body.body = input.description;
|
|
318
|
+
if (input.draft != null)
|
|
319
|
+
body.draft = input.draft;
|
|
320
|
+
if (input.targetBranch != null)
|
|
321
|
+
body.base = input.targetBranch;
|
|
322
|
+
if (input.stateEvent)
|
|
323
|
+
body.state = input.stateEvent === "close" ? "closed" : "open";
|
|
324
|
+
const res = await this.api("PATCH", `/repos/${projectPath}/pulls/${mrIid}`, body);
|
|
325
|
+
if (!res.ok) {
|
|
326
|
+
const text = await res.text();
|
|
327
|
+
throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
|
|
328
|
+
}
|
|
329
|
+
if (input.reviewers) {
|
|
330
|
+
await this.api("POST", `/repos/${projectPath}/pulls/${mrIid}/requested_reviewers`, {
|
|
331
|
+
reviewers: input.reviewers
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (input.assignees) {
|
|
335
|
+
await this.api("POST", `/repos/${projectPath}/issues/${mrIid}/assignees`, {
|
|
336
|
+
assignees: input.assignees
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (input.labels) {
|
|
340
|
+
await this.api("PUT", `/repos/${projectPath}/issues/${mrIid}/labels`, {
|
|
341
|
+
labels: input.labels
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
345
|
+
if (!pr)
|
|
346
|
+
throw new Error("Updated PR but failed to fetch it back");
|
|
347
|
+
return pr;
|
|
348
|
+
}
|
|
210
349
|
async restRequest(method, path, body) {
|
|
211
350
|
return this.api(method, path, body);
|
|
212
351
|
}
|
|
352
|
+
async mergePullRequest(projectPath, mrIid, input) {
|
|
353
|
+
const body = {};
|
|
354
|
+
if (input?.commitMessage != null)
|
|
355
|
+
body.commit_title = input.commitMessage;
|
|
356
|
+
if (input?.squashCommitMessage != null)
|
|
357
|
+
body.commit_title = input.squashCommitMessage;
|
|
358
|
+
if (input?.shouldRemoveSourceBranch != null)
|
|
359
|
+
body.delete_branch = input.shouldRemoveSourceBranch;
|
|
360
|
+
if (input?.sha != null)
|
|
361
|
+
body.sha = input.sha;
|
|
362
|
+
if (input?.mergeMethod) {
|
|
363
|
+
body.merge_method = input.mergeMethod;
|
|
364
|
+
} else if (input?.squash) {
|
|
365
|
+
body.merge_method = "squash";
|
|
366
|
+
}
|
|
367
|
+
const res = await this.api("PUT", `/repos/${projectPath}/pulls/${mrIid}/merge`, body);
|
|
368
|
+
if (!res.ok) {
|
|
369
|
+
const text = await res.text();
|
|
370
|
+
throw new Error(`mergePullRequest failed: ${res.status} ${text}`);
|
|
371
|
+
}
|
|
372
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
373
|
+
if (!pr)
|
|
374
|
+
throw new Error("Merged PR but failed to fetch it back");
|
|
375
|
+
return pr;
|
|
376
|
+
}
|
|
377
|
+
async approvePullRequest(projectPath, mrIid) {
|
|
378
|
+
const res = await this.api("POST", `/repos/${projectPath}/pulls/${mrIid}/reviews`, {
|
|
379
|
+
event: "APPROVE"
|
|
380
|
+
});
|
|
381
|
+
if (!res.ok) {
|
|
382
|
+
const text = await res.text().catch(() => "");
|
|
383
|
+
throw new Error(`approvePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async unapprovePullRequest(_projectPath, _mrIid) {
|
|
387
|
+
throw new Error("unapprovePullRequest is not supported by GitHub. " + "Check provider.capabilities.canUnapprove before calling.");
|
|
388
|
+
}
|
|
389
|
+
async rebasePullRequest(_projectPath, _mrIid) {
|
|
390
|
+
throw new Error("rebasePullRequest is not supported by GitHub. " + "Check provider.capabilities.canRebase before calling.");
|
|
391
|
+
}
|
|
392
|
+
async setAutoMerge(_projectPath, _mrIid) {
|
|
393
|
+
throw new Error("setAutoMerge is not supported by the GitHub REST API. " + "Check provider.capabilities.canAutoMerge before calling.");
|
|
394
|
+
}
|
|
395
|
+
async cancelAutoMerge(_projectPath, _mrIid) {
|
|
396
|
+
throw new Error("cancelAutoMerge is not supported by the GitHub REST API. " + "Check provider.capabilities.canAutoMerge before calling.");
|
|
397
|
+
}
|
|
398
|
+
async resolveDiscussion(_projectPath, _mrIid, _discussionId) {
|
|
399
|
+
throw new Error("resolveDiscussion is not supported by the GitHub REST API. " + "Check provider.capabilities.canResolveDiscussions before calling.");
|
|
400
|
+
}
|
|
401
|
+
async unresolveDiscussion(_projectPath, _mrIid, _discussionId) {
|
|
402
|
+
throw new Error("unresolveDiscussion is not supported by the GitHub REST API. " + "Check provider.capabilities.canResolveDiscussions before calling.");
|
|
403
|
+
}
|
|
404
|
+
async retryPipeline(projectPath, pipelineId) {
|
|
405
|
+
const res = await this.api("POST", `/repos/${projectPath}/actions/runs/${pipelineId}/rerun`);
|
|
406
|
+
if (!res.ok) {
|
|
407
|
+
const text = await res.text().catch(() => "");
|
|
408
|
+
throw new Error(`retryPipeline failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async requestReReview(projectPath, mrIid, reviewerUsernames) {
|
|
412
|
+
if (!reviewerUsernames?.length) {
|
|
413
|
+
const prRes = await this.api("GET", `/repos/${projectPath}/pulls/${mrIid}`);
|
|
414
|
+
if (!prRes.ok) {
|
|
415
|
+
throw new Error(`requestReReview: failed to fetch PR: ${prRes.status}`);
|
|
416
|
+
}
|
|
417
|
+
const pr = await prRes.json();
|
|
418
|
+
reviewerUsernames = pr.requested_reviewers.map((r) => r.login);
|
|
419
|
+
if (!reviewerUsernames.length) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const res = await this.api("POST", `/repos/${projectPath}/pulls/${mrIid}/requested_reviewers`, { reviewers: reviewerUsernames });
|
|
424
|
+
if (!res.ok) {
|
|
425
|
+
const text = await res.text().catch(() => "");
|
|
426
|
+
throw new Error(`requestReReview failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
213
429
|
async api(method, path, body) {
|
|
214
430
|
const url = `${this.apiBase}${path}`;
|
|
215
431
|
const headers = {
|
package/dist/GitLabProvider.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { GitProvider } from
|
|
2
|
-
import type { MRDetail, PullRequest, UserRef } from
|
|
3
|
-
import { type ForgeLogger } from
|
|
1
|
+
import type { GitProvider } from './GitProvider.ts';
|
|
2
|
+
import type { BranchProtectionRule, CreatePullRequestInput, MergePullRequestInput, MRDetail, ProviderCapabilities, PullRequest, UpdatePullRequestInput, UserRef } from './types.ts';
|
|
3
|
+
import { type ForgeLogger } from './logger.ts';
|
|
4
4
|
/**
|
|
5
5
|
* Strips the provider prefix from a scoped repositoryId and returns the
|
|
6
6
|
* numeric GitLab project ID needed for REST API calls.
|
|
@@ -17,6 +17,7 @@ export declare class GitLabProvider implements GitProvider {
|
|
|
17
17
|
constructor(baseURL: string, token: string, options?: {
|
|
18
18
|
logger?: ForgeLogger;
|
|
19
19
|
});
|
|
20
|
+
readonly capabilities: ProviderCapabilities;
|
|
20
21
|
validateToken(): Promise<UserRef>;
|
|
21
22
|
/**
|
|
22
23
|
* Fetch a single MR by project path and IID.
|
|
@@ -29,6 +30,27 @@ export declare class GitLabProvider implements GitProvider {
|
|
|
29
30
|
fetchSingleMR(projectPath: string, mrIid: number, currentUserNumericId: number | null): Promise<PullRequest | null>;
|
|
30
31
|
fetchPullRequests(): Promise<PullRequest[]>;
|
|
31
32
|
fetchMRDiscussions(repositoryId: string, mrIid: number): Promise<MRDetail>;
|
|
33
|
+
fetchBranchProtectionRules(projectPath: string): Promise<BranchProtectionRule[]>;
|
|
34
|
+
deleteBranch(projectPath: string, branch: string): Promise<void>;
|
|
35
|
+
fetchPullRequestByBranch(projectPath: string, sourceBranch: string): Promise<PullRequest | null>;
|
|
36
|
+
createPullRequest(input: CreatePullRequestInput): Promise<PullRequest>;
|
|
37
|
+
updatePullRequest(projectPath: string, mrIid: number, input: UpdatePullRequestInput): Promise<PullRequest>;
|
|
32
38
|
restRequest(method: string, path: string, body?: unknown): Promise<Response>;
|
|
39
|
+
mergePullRequest(projectPath: string, mrIid: number, input?: MergePullRequestInput): Promise<PullRequest>;
|
|
40
|
+
approvePullRequest(projectPath: string, mrIid: number): Promise<void>;
|
|
41
|
+
unapprovePullRequest(projectPath: string, mrIid: number): Promise<void>;
|
|
42
|
+
rebasePullRequest(projectPath: string, mrIid: number): Promise<void>;
|
|
43
|
+
setAutoMerge(projectPath: string, mrIid: number): Promise<void>;
|
|
44
|
+
cancelAutoMerge(projectPath: string, mrIid: number): Promise<void>;
|
|
45
|
+
resolveDiscussion(projectPath: string, mrIid: number, discussionId: string): Promise<void>;
|
|
46
|
+
unresolveDiscussion(projectPath: string, mrIid: number, discussionId: string): Promise<void>;
|
|
47
|
+
retryPipeline(projectPath: string, pipelineId: number): Promise<void>;
|
|
48
|
+
requestReReview(projectPath: string, mrIid: number, _reviewerUsernames?: string[]): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Retry `fetchSingleMR` with exponential backoff to handle REST→GraphQL
|
|
51
|
+
* eventual consistency. GitLab's GraphQL may not immediately reflect
|
|
52
|
+
* changes made via REST. 3 attempts: 0ms, 300ms, 600ms delay.
|
|
53
|
+
*/
|
|
54
|
+
private fetchSingleMRWithRetry;
|
|
33
55
|
private runQuery;
|
|
34
56
|
}
|
package/dist/GitLabProvider.js
CHANGED
|
@@ -236,8 +236,20 @@ class GitLabProvider {
|
|
|
236
236
|
this.baseURL = baseURL.replace(/\/$/, "");
|
|
237
237
|
this.token = token;
|
|
238
238
|
this.log = options.logger ?? noopLogger;
|
|
239
|
-
this.mrDetailFetcher = new MRDetailFetcher(this.baseURL, token, {
|
|
239
|
+
this.mrDetailFetcher = new MRDetailFetcher(this.baseURL, token, {
|
|
240
|
+
logger: this.log
|
|
241
|
+
});
|
|
240
242
|
}
|
|
243
|
+
capabilities = {
|
|
244
|
+
canMerge: true,
|
|
245
|
+
canApprove: true,
|
|
246
|
+
canUnapprove: true,
|
|
247
|
+
canRebase: true,
|
|
248
|
+
canAutoMerge: true,
|
|
249
|
+
canResolveDiscussions: true,
|
|
250
|
+
canRetryPipeline: true,
|
|
251
|
+
canRequestReReview: true
|
|
252
|
+
};
|
|
241
253
|
async validateToken() {
|
|
242
254
|
const url = `${this.baseURL}/api/v4/user`;
|
|
243
255
|
const res = await fetch(url, {
|
|
@@ -313,6 +325,113 @@ class GitLabProvider {
|
|
|
313
325
|
const projectId = parseGitLabRepoId(repositoryId);
|
|
314
326
|
return this.mrDetailFetcher.fetchDetail(projectId, mrIid);
|
|
315
327
|
}
|
|
328
|
+
async fetchBranchProtectionRules(projectPath) {
|
|
329
|
+
const encoded = encodeURIComponent(projectPath);
|
|
330
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/protected_branches?per_page=100`, { headers: { "PRIVATE-TOKEN": this.token } });
|
|
331
|
+
if (!res.ok) {
|
|
332
|
+
throw new Error(`fetchBranchProtectionRules failed: ${res.status} ${await res.text()}`);
|
|
333
|
+
}
|
|
334
|
+
const branches = await res.json();
|
|
335
|
+
return branches.map((b) => ({
|
|
336
|
+
pattern: b.name,
|
|
337
|
+
allowForcePush: b.allow_force_push,
|
|
338
|
+
allowDeletion: false,
|
|
339
|
+
requiredApprovals: 0,
|
|
340
|
+
requireStatusChecks: false,
|
|
341
|
+
raw: b
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
async deleteBranch(projectPath, branch) {
|
|
345
|
+
const encoded = encodeURIComponent(projectPath);
|
|
346
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/repository/branches/${encodeURIComponent(branch)}`, { method: "DELETE", headers: { "PRIVATE-TOKEN": this.token } });
|
|
347
|
+
if (!res.ok) {
|
|
348
|
+
throw new Error(`deleteBranch failed: ${res.status} ${await res.text()}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async fetchPullRequestByBranch(projectPath, sourceBranch) {
|
|
352
|
+
const encoded = encodeURIComponent(projectPath);
|
|
353
|
+
const url = `${this.baseURL}/api/v4/projects/${encoded}/merge_requests?source_branch=${encodeURIComponent(sourceBranch)}&state=opened&per_page=1`;
|
|
354
|
+
const res = await fetch(url, {
|
|
355
|
+
headers: { "PRIVATE-TOKEN": this.token }
|
|
356
|
+
});
|
|
357
|
+
if (!res.ok) {
|
|
358
|
+
this.log.warn("fetchPullRequestByBranch failed", {
|
|
359
|
+
projectPath,
|
|
360
|
+
sourceBranch,
|
|
361
|
+
status: res.status
|
|
362
|
+
});
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
const mrs = await res.json();
|
|
366
|
+
if (!mrs[0])
|
|
367
|
+
return null;
|
|
368
|
+
return this.fetchSingleMR(projectPath, mrs[0].iid, null);
|
|
369
|
+
}
|
|
370
|
+
async createPullRequest(input) {
|
|
371
|
+
const encoded = encodeURIComponent(input.projectPath);
|
|
372
|
+
const body = {
|
|
373
|
+
source_branch: input.sourceBranch,
|
|
374
|
+
target_branch: input.targetBranch,
|
|
375
|
+
title: input.title
|
|
376
|
+
};
|
|
377
|
+
if (input.description != null)
|
|
378
|
+
body.description = input.description;
|
|
379
|
+
if (input.draft != null)
|
|
380
|
+
body.draft = input.draft;
|
|
381
|
+
if (input.labels?.length)
|
|
382
|
+
body.labels = input.labels.join(",");
|
|
383
|
+
if (input.assignees?.length)
|
|
384
|
+
body.assignee_ids = input.assignees;
|
|
385
|
+
if (input.reviewers?.length)
|
|
386
|
+
body.reviewer_ids = input.reviewers;
|
|
387
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests`, {
|
|
388
|
+
method: "POST",
|
|
389
|
+
headers: {
|
|
390
|
+
"PRIVATE-TOKEN": this.token,
|
|
391
|
+
"Content-Type": "application/json"
|
|
392
|
+
},
|
|
393
|
+
body: JSON.stringify(body)
|
|
394
|
+
});
|
|
395
|
+
if (!res.ok) {
|
|
396
|
+
const text = await res.text();
|
|
397
|
+
throw new Error(`createPullRequest failed: ${res.status} ${text}`);
|
|
398
|
+
}
|
|
399
|
+
const created = await res.json();
|
|
400
|
+
return this.fetchSingleMRWithRetry(input.projectPath, created.iid, "Created MR but failed to fetch it back");
|
|
401
|
+
}
|
|
402
|
+
async updatePullRequest(projectPath, mrIid, input) {
|
|
403
|
+
const encoded = encodeURIComponent(projectPath);
|
|
404
|
+
const body = {};
|
|
405
|
+
if (input.title != null)
|
|
406
|
+
body.title = input.title;
|
|
407
|
+
if (input.description != null)
|
|
408
|
+
body.description = input.description;
|
|
409
|
+
if (input.draft != null)
|
|
410
|
+
body.draft = input.draft;
|
|
411
|
+
if (input.targetBranch != null)
|
|
412
|
+
body.target_branch = input.targetBranch;
|
|
413
|
+
if (input.labels)
|
|
414
|
+
body.labels = input.labels.join(",");
|
|
415
|
+
if (input.assignees)
|
|
416
|
+
body.assignee_ids = input.assignees;
|
|
417
|
+
if (input.reviewers)
|
|
418
|
+
body.reviewer_ids = input.reviewers;
|
|
419
|
+
if (input.stateEvent)
|
|
420
|
+
body.state_event = input.stateEvent;
|
|
421
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}`, {
|
|
422
|
+
method: "PUT",
|
|
423
|
+
headers: {
|
|
424
|
+
"PRIVATE-TOKEN": this.token,
|
|
425
|
+
"Content-Type": "application/json"
|
|
426
|
+
},
|
|
427
|
+
body: JSON.stringify(body)
|
|
428
|
+
});
|
|
429
|
+
if (!res.ok) {
|
|
430
|
+
const text = await res.text();
|
|
431
|
+
throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
|
|
432
|
+
}
|
|
433
|
+
return this.fetchSingleMRWithRetry(projectPath, mrIid, "Updated MR but failed to fetch it back");
|
|
434
|
+
}
|
|
316
435
|
async restRequest(method, path, body) {
|
|
317
436
|
const url = `${this.baseURL}${path}`;
|
|
318
437
|
const headers = {
|
|
@@ -327,6 +446,171 @@ class GitLabProvider {
|
|
|
327
446
|
body: body !== undefined ? JSON.stringify(body) : undefined
|
|
328
447
|
});
|
|
329
448
|
}
|
|
449
|
+
async mergePullRequest(projectPath, mrIid, input) {
|
|
450
|
+
const encoded = encodeURIComponent(projectPath);
|
|
451
|
+
const body = {};
|
|
452
|
+
if (input?.commitMessage != null)
|
|
453
|
+
body.merge_commit_message = input.commitMessage;
|
|
454
|
+
if (input?.squashCommitMessage != null)
|
|
455
|
+
body.squash_commit_message = input.squashCommitMessage;
|
|
456
|
+
if (input?.squash != null)
|
|
457
|
+
body.squash = input.squash;
|
|
458
|
+
if (input?.shouldRemoveSourceBranch != null)
|
|
459
|
+
body.should_remove_source_branch = input.shouldRemoveSourceBranch;
|
|
460
|
+
if (input?.sha != null)
|
|
461
|
+
body.sha = input.sha;
|
|
462
|
+
if (input?.mergeMethod === "squash" && input?.squash == null)
|
|
463
|
+
body.squash = true;
|
|
464
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/merge`, {
|
|
465
|
+
method: "PUT",
|
|
466
|
+
headers: {
|
|
467
|
+
"PRIVATE-TOKEN": this.token,
|
|
468
|
+
"Content-Type": "application/json"
|
|
469
|
+
},
|
|
470
|
+
body: JSON.stringify(body)
|
|
471
|
+
});
|
|
472
|
+
if (!res.ok) {
|
|
473
|
+
const text = await res.text();
|
|
474
|
+
throw new Error(`mergePullRequest failed: ${res.status} ${text}`);
|
|
475
|
+
}
|
|
476
|
+
return this.fetchSingleMRWithRetry(projectPath, mrIid, "Merged MR but failed to fetch it back");
|
|
477
|
+
}
|
|
478
|
+
async approvePullRequest(projectPath, mrIid) {
|
|
479
|
+
const encoded = encodeURIComponent(projectPath);
|
|
480
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/approve`, {
|
|
481
|
+
method: "POST",
|
|
482
|
+
headers: { "PRIVATE-TOKEN": this.token }
|
|
483
|
+
});
|
|
484
|
+
if (!res.ok) {
|
|
485
|
+
const text = await res.text().catch(() => "");
|
|
486
|
+
throw new Error(`approvePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async unapprovePullRequest(projectPath, mrIid) {
|
|
490
|
+
const encoded = encodeURIComponent(projectPath);
|
|
491
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/unapprove`, {
|
|
492
|
+
method: "POST",
|
|
493
|
+
headers: { "PRIVATE-TOKEN": this.token }
|
|
494
|
+
});
|
|
495
|
+
if (!res.ok) {
|
|
496
|
+
const text = await res.text().catch(() => "");
|
|
497
|
+
throw new Error(`unapprovePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async rebasePullRequest(projectPath, mrIid) {
|
|
501
|
+
const encoded = encodeURIComponent(projectPath);
|
|
502
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/rebase`, {
|
|
503
|
+
method: "PUT",
|
|
504
|
+
headers: { "PRIVATE-TOKEN": this.token }
|
|
505
|
+
});
|
|
506
|
+
if (!res.ok) {
|
|
507
|
+
const text = await res.text().catch(() => "");
|
|
508
|
+
throw new Error(`rebasePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async setAutoMerge(projectPath, mrIid) {
|
|
512
|
+
const encoded = encodeURIComponent(projectPath);
|
|
513
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/merge`, {
|
|
514
|
+
method: "PUT",
|
|
515
|
+
headers: {
|
|
516
|
+
"PRIVATE-TOKEN": this.token,
|
|
517
|
+
"Content-Type": "application/json"
|
|
518
|
+
},
|
|
519
|
+
body: JSON.stringify({ merge_when_pipeline_succeeds: true })
|
|
520
|
+
});
|
|
521
|
+
if (!res.ok) {
|
|
522
|
+
const text = await res.text().catch(() => "");
|
|
523
|
+
throw new Error(`setAutoMerge failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async cancelAutoMerge(projectPath, mrIid) {
|
|
527
|
+
const encoded = encodeURIComponent(projectPath);
|
|
528
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/cancel_merge_when_pipeline_succeeds`, {
|
|
529
|
+
method: "POST",
|
|
530
|
+
headers: { "PRIVATE-TOKEN": this.token }
|
|
531
|
+
});
|
|
532
|
+
if (!res.ok) {
|
|
533
|
+
const text = await res.text().catch(() => "");
|
|
534
|
+
throw new Error(`cancelAutoMerge failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async resolveDiscussion(projectPath, mrIid, discussionId) {
|
|
538
|
+
const encoded = encodeURIComponent(projectPath);
|
|
539
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/discussions/${discussionId}`, {
|
|
540
|
+
method: "PUT",
|
|
541
|
+
headers: {
|
|
542
|
+
"PRIVATE-TOKEN": this.token,
|
|
543
|
+
"Content-Type": "application/json"
|
|
544
|
+
},
|
|
545
|
+
body: JSON.stringify({ resolved: true })
|
|
546
|
+
});
|
|
547
|
+
if (!res.ok) {
|
|
548
|
+
const text = await res.text().catch(() => "");
|
|
549
|
+
throw new Error(`resolveDiscussion failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
async unresolveDiscussion(projectPath, mrIid, discussionId) {
|
|
553
|
+
const encoded = encodeURIComponent(projectPath);
|
|
554
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/discussions/${discussionId}`, {
|
|
555
|
+
method: "PUT",
|
|
556
|
+
headers: {
|
|
557
|
+
"PRIVATE-TOKEN": this.token,
|
|
558
|
+
"Content-Type": "application/json"
|
|
559
|
+
},
|
|
560
|
+
body: JSON.stringify({ resolved: false })
|
|
561
|
+
});
|
|
562
|
+
if (!res.ok) {
|
|
563
|
+
const text = await res.text().catch(() => "");
|
|
564
|
+
throw new Error(`unresolveDiscussion failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async retryPipeline(projectPath, pipelineId) {
|
|
568
|
+
const encoded = encodeURIComponent(projectPath);
|
|
569
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/pipelines/${pipelineId}/retry`, {
|
|
570
|
+
method: "POST",
|
|
571
|
+
headers: { "PRIVATE-TOKEN": this.token }
|
|
572
|
+
});
|
|
573
|
+
if (!res.ok) {
|
|
574
|
+
const text = await res.text().catch(() => "");
|
|
575
|
+
throw new Error(`retryPipeline failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async requestReReview(projectPath, mrIid, _reviewerUsernames) {
|
|
579
|
+
const encoded = encodeURIComponent(projectPath);
|
|
580
|
+
const mrRes = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}`, { headers: { "PRIVATE-TOKEN": this.token } });
|
|
581
|
+
if (!mrRes.ok) {
|
|
582
|
+
const text = await mrRes.text().catch(() => "");
|
|
583
|
+
throw new Error(`requestReReview: failed to fetch MR: ${mrRes.status}${text ? ` — ${text}` : ""}`);
|
|
584
|
+
}
|
|
585
|
+
const mr = await mrRes.json();
|
|
586
|
+
const reviewerIds = mr.reviewers?.map((r) => r.id) ?? [];
|
|
587
|
+
if (reviewerIds.length === 0) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}`, {
|
|
591
|
+
method: "PUT",
|
|
592
|
+
headers: {
|
|
593
|
+
"PRIVATE-TOKEN": this.token,
|
|
594
|
+
"Content-Type": "application/json"
|
|
595
|
+
},
|
|
596
|
+
body: JSON.stringify({ reviewer_ids: reviewerIds })
|
|
597
|
+
});
|
|
598
|
+
if (!res.ok) {
|
|
599
|
+
const text = await res.text().catch(() => "");
|
|
600
|
+
throw new Error(`requestReReview failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
async fetchSingleMRWithRetry(projectPath, mrIid, errorMessage) {
|
|
604
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
605
|
+
if (attempt > 0) {
|
|
606
|
+
await new Promise((r) => setTimeout(r, attempt * 300));
|
|
607
|
+
}
|
|
608
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
609
|
+
if (pr)
|
|
610
|
+
return pr;
|
|
611
|
+
}
|
|
612
|
+
throw new Error(errorMessage);
|
|
613
|
+
}
|
|
330
614
|
async runQuery(query, variables) {
|
|
331
615
|
const url = `${this.baseURL}/api/graphql`;
|
|
332
616
|
const body = JSON.stringify({ query, variables: variables ?? {} });
|