@forge-glance/sdk 0.1.1 → 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 +14 -3
- package/dist/GitHubProvider.js +95 -2
- package/dist/GitLabProvider.d.ts +20 -3
- package/dist/GitLabProvider.js +185 -10
- package/dist/GitProvider.d.ts +64 -1
- package/dist/index.d.ts +14 -14
- package/dist/index.js +280 -12
- package/dist/providers.js +280 -12
- package/dist/types.d.ts +56 -1
- package/package.json +1 -1
- package/src/GitHubProvider.ts +424 -154
- package/src/GitLabProvider.ts +426 -88
- package/src/GitProvider.ts +113 -21
- package/src/index.ts +22 -15
- package/src/types.ts +59 -1
package/dist/index.js
CHANGED
|
@@ -245,8 +245,20 @@ class GitLabProvider {
|
|
|
245
245
|
this.baseURL = baseURL.replace(/\/$/, "");
|
|
246
246
|
this.token = token;
|
|
247
247
|
this.log = options.logger ?? noopLogger;
|
|
248
|
-
this.mrDetailFetcher = new MRDetailFetcher(this.baseURL, token, {
|
|
248
|
+
this.mrDetailFetcher = new MRDetailFetcher(this.baseURL, token, {
|
|
249
|
+
logger: this.log
|
|
250
|
+
});
|
|
249
251
|
}
|
|
252
|
+
capabilities = {
|
|
253
|
+
canMerge: true,
|
|
254
|
+
canApprove: true,
|
|
255
|
+
canUnapprove: true,
|
|
256
|
+
canRebase: true,
|
|
257
|
+
canAutoMerge: true,
|
|
258
|
+
canResolveDiscussions: true,
|
|
259
|
+
canRetryPipeline: true,
|
|
260
|
+
canRequestReReview: true
|
|
261
|
+
};
|
|
250
262
|
async validateToken() {
|
|
251
263
|
const url = `${this.baseURL}/api/v4/user`;
|
|
252
264
|
const res = await fetch(url, {
|
|
@@ -352,7 +364,11 @@ class GitLabProvider {
|
|
|
352
364
|
headers: { "PRIVATE-TOKEN": this.token }
|
|
353
365
|
});
|
|
354
366
|
if (!res.ok) {
|
|
355
|
-
this.log.warn("fetchPullRequestByBranch failed", {
|
|
367
|
+
this.log.warn("fetchPullRequestByBranch failed", {
|
|
368
|
+
projectPath,
|
|
369
|
+
sourceBranch,
|
|
370
|
+
status: res.status
|
|
371
|
+
});
|
|
356
372
|
return null;
|
|
357
373
|
}
|
|
358
374
|
const mrs = await res.json();
|
|
@@ -390,10 +406,7 @@ class GitLabProvider {
|
|
|
390
406
|
throw new Error(`createPullRequest failed: ${res.status} ${text}`);
|
|
391
407
|
}
|
|
392
408
|
const created = await res.json();
|
|
393
|
-
|
|
394
|
-
if (!pr)
|
|
395
|
-
throw new Error("Created MR but failed to fetch it back");
|
|
396
|
-
return pr;
|
|
409
|
+
return this.fetchSingleMRWithRetry(input.projectPath, created.iid, "Created MR but failed to fetch it back");
|
|
397
410
|
}
|
|
398
411
|
async updatePullRequest(projectPath, mrIid, input) {
|
|
399
412
|
const encoded = encodeURIComponent(projectPath);
|
|
@@ -426,10 +439,7 @@ class GitLabProvider {
|
|
|
426
439
|
const text = await res.text();
|
|
427
440
|
throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
|
|
428
441
|
}
|
|
429
|
-
|
|
430
|
-
if (!pr)
|
|
431
|
-
throw new Error("Updated MR but failed to fetch it back");
|
|
432
|
-
return pr;
|
|
442
|
+
return this.fetchSingleMRWithRetry(projectPath, mrIid, "Updated MR but failed to fetch it back");
|
|
433
443
|
}
|
|
434
444
|
async restRequest(method, path, body) {
|
|
435
445
|
const url = `${this.baseURL}${path}`;
|
|
@@ -445,6 +455,171 @@ class GitLabProvider {
|
|
|
445
455
|
body: body !== undefined ? JSON.stringify(body) : undefined
|
|
446
456
|
});
|
|
447
457
|
}
|
|
458
|
+
async mergePullRequest(projectPath, mrIid, input) {
|
|
459
|
+
const encoded = encodeURIComponent(projectPath);
|
|
460
|
+
const body = {};
|
|
461
|
+
if (input?.commitMessage != null)
|
|
462
|
+
body.merge_commit_message = input.commitMessage;
|
|
463
|
+
if (input?.squashCommitMessage != null)
|
|
464
|
+
body.squash_commit_message = input.squashCommitMessage;
|
|
465
|
+
if (input?.squash != null)
|
|
466
|
+
body.squash = input.squash;
|
|
467
|
+
if (input?.shouldRemoveSourceBranch != null)
|
|
468
|
+
body.should_remove_source_branch = input.shouldRemoveSourceBranch;
|
|
469
|
+
if (input?.sha != null)
|
|
470
|
+
body.sha = input.sha;
|
|
471
|
+
if (input?.mergeMethod === "squash" && input?.squash == null)
|
|
472
|
+
body.squash = true;
|
|
473
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/merge`, {
|
|
474
|
+
method: "PUT",
|
|
475
|
+
headers: {
|
|
476
|
+
"PRIVATE-TOKEN": this.token,
|
|
477
|
+
"Content-Type": "application/json"
|
|
478
|
+
},
|
|
479
|
+
body: JSON.stringify(body)
|
|
480
|
+
});
|
|
481
|
+
if (!res.ok) {
|
|
482
|
+
const text = await res.text();
|
|
483
|
+
throw new Error(`mergePullRequest failed: ${res.status} ${text}`);
|
|
484
|
+
}
|
|
485
|
+
return this.fetchSingleMRWithRetry(projectPath, mrIid, "Merged MR but failed to fetch it back");
|
|
486
|
+
}
|
|
487
|
+
async approvePullRequest(projectPath, mrIid) {
|
|
488
|
+
const encoded = encodeURIComponent(projectPath);
|
|
489
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/approve`, {
|
|
490
|
+
method: "POST",
|
|
491
|
+
headers: { "PRIVATE-TOKEN": this.token }
|
|
492
|
+
});
|
|
493
|
+
if (!res.ok) {
|
|
494
|
+
const text = await res.text().catch(() => "");
|
|
495
|
+
throw new Error(`approvePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async unapprovePullRequest(projectPath, mrIid) {
|
|
499
|
+
const encoded = encodeURIComponent(projectPath);
|
|
500
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/unapprove`, {
|
|
501
|
+
method: "POST",
|
|
502
|
+
headers: { "PRIVATE-TOKEN": this.token }
|
|
503
|
+
});
|
|
504
|
+
if (!res.ok) {
|
|
505
|
+
const text = await res.text().catch(() => "");
|
|
506
|
+
throw new Error(`unapprovePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async rebasePullRequest(projectPath, mrIid) {
|
|
510
|
+
const encoded = encodeURIComponent(projectPath);
|
|
511
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/rebase`, {
|
|
512
|
+
method: "PUT",
|
|
513
|
+
headers: { "PRIVATE-TOKEN": this.token }
|
|
514
|
+
});
|
|
515
|
+
if (!res.ok) {
|
|
516
|
+
const text = await res.text().catch(() => "");
|
|
517
|
+
throw new Error(`rebasePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async setAutoMerge(projectPath, mrIid) {
|
|
521
|
+
const encoded = encodeURIComponent(projectPath);
|
|
522
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/merge`, {
|
|
523
|
+
method: "PUT",
|
|
524
|
+
headers: {
|
|
525
|
+
"PRIVATE-TOKEN": this.token,
|
|
526
|
+
"Content-Type": "application/json"
|
|
527
|
+
},
|
|
528
|
+
body: JSON.stringify({ merge_when_pipeline_succeeds: true })
|
|
529
|
+
});
|
|
530
|
+
if (!res.ok) {
|
|
531
|
+
const text = await res.text().catch(() => "");
|
|
532
|
+
throw new Error(`setAutoMerge failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async cancelAutoMerge(projectPath, mrIid) {
|
|
536
|
+
const encoded = encodeURIComponent(projectPath);
|
|
537
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/cancel_merge_when_pipeline_succeeds`, {
|
|
538
|
+
method: "POST",
|
|
539
|
+
headers: { "PRIVATE-TOKEN": this.token }
|
|
540
|
+
});
|
|
541
|
+
if (!res.ok) {
|
|
542
|
+
const text = await res.text().catch(() => "");
|
|
543
|
+
throw new Error(`cancelAutoMerge failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async resolveDiscussion(projectPath, mrIid, discussionId) {
|
|
547
|
+
const encoded = encodeURIComponent(projectPath);
|
|
548
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/discussions/${discussionId}`, {
|
|
549
|
+
method: "PUT",
|
|
550
|
+
headers: {
|
|
551
|
+
"PRIVATE-TOKEN": this.token,
|
|
552
|
+
"Content-Type": "application/json"
|
|
553
|
+
},
|
|
554
|
+
body: JSON.stringify({ resolved: true })
|
|
555
|
+
});
|
|
556
|
+
if (!res.ok) {
|
|
557
|
+
const text = await res.text().catch(() => "");
|
|
558
|
+
throw new Error(`resolveDiscussion failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async unresolveDiscussion(projectPath, mrIid, discussionId) {
|
|
562
|
+
const encoded = encodeURIComponent(projectPath);
|
|
563
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/discussions/${discussionId}`, {
|
|
564
|
+
method: "PUT",
|
|
565
|
+
headers: {
|
|
566
|
+
"PRIVATE-TOKEN": this.token,
|
|
567
|
+
"Content-Type": "application/json"
|
|
568
|
+
},
|
|
569
|
+
body: JSON.stringify({ resolved: false })
|
|
570
|
+
});
|
|
571
|
+
if (!res.ok) {
|
|
572
|
+
const text = await res.text().catch(() => "");
|
|
573
|
+
throw new Error(`unresolveDiscussion failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async retryPipeline(projectPath, pipelineId) {
|
|
577
|
+
const encoded = encodeURIComponent(projectPath);
|
|
578
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/pipelines/${pipelineId}/retry`, {
|
|
579
|
+
method: "POST",
|
|
580
|
+
headers: { "PRIVATE-TOKEN": this.token }
|
|
581
|
+
});
|
|
582
|
+
if (!res.ok) {
|
|
583
|
+
const text = await res.text().catch(() => "");
|
|
584
|
+
throw new Error(`retryPipeline failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
async requestReReview(projectPath, mrIid, _reviewerUsernames) {
|
|
588
|
+
const encoded = encodeURIComponent(projectPath);
|
|
589
|
+
const mrRes = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}`, { headers: { "PRIVATE-TOKEN": this.token } });
|
|
590
|
+
if (!mrRes.ok) {
|
|
591
|
+
const text = await mrRes.text().catch(() => "");
|
|
592
|
+
throw new Error(`requestReReview: failed to fetch MR: ${mrRes.status}${text ? ` — ${text}` : ""}`);
|
|
593
|
+
}
|
|
594
|
+
const mr = await mrRes.json();
|
|
595
|
+
const reviewerIds = mr.reviewers?.map((r) => r.id) ?? [];
|
|
596
|
+
if (reviewerIds.length === 0) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}`, {
|
|
600
|
+
method: "PUT",
|
|
601
|
+
headers: {
|
|
602
|
+
"PRIVATE-TOKEN": this.token,
|
|
603
|
+
"Content-Type": "application/json"
|
|
604
|
+
},
|
|
605
|
+
body: JSON.stringify({ reviewer_ids: reviewerIds })
|
|
606
|
+
});
|
|
607
|
+
if (!res.ok) {
|
|
608
|
+
const text = await res.text().catch(() => "");
|
|
609
|
+
throw new Error(`requestReReview failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async fetchSingleMRWithRetry(projectPath, mrIid, errorMessage) {
|
|
613
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
614
|
+
if (attempt > 0) {
|
|
615
|
+
await new Promise((r) => setTimeout(r, attempt * 300));
|
|
616
|
+
}
|
|
617
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
618
|
+
if (pr)
|
|
619
|
+
return pr;
|
|
620
|
+
}
|
|
621
|
+
throw new Error(errorMessage);
|
|
622
|
+
}
|
|
448
623
|
async runQuery(query, variables) {
|
|
449
624
|
const url = `${this.baseURL}/api/graphql`;
|
|
450
625
|
const body = JSON.stringify({ query, variables: variables ?? {} });
|
|
@@ -560,6 +735,16 @@ class GitHubProvider {
|
|
|
560
735
|
this.apiBase = `${this.baseURL}/api/v3`;
|
|
561
736
|
}
|
|
562
737
|
}
|
|
738
|
+
capabilities = {
|
|
739
|
+
canMerge: true,
|
|
740
|
+
canApprove: true,
|
|
741
|
+
canUnapprove: false,
|
|
742
|
+
canRebase: false,
|
|
743
|
+
canAutoMerge: false,
|
|
744
|
+
canResolveDiscussions: false,
|
|
745
|
+
canRetryPipeline: true,
|
|
746
|
+
canRequestReReview: true
|
|
747
|
+
};
|
|
563
748
|
async validateToken() {
|
|
564
749
|
const res = await this.api("GET", "/user");
|
|
565
750
|
if (!res.ok) {
|
|
@@ -601,7 +786,9 @@ class GitHubProvider {
|
|
|
601
786
|
]);
|
|
602
787
|
return this.toPullRequest(pr, prRoles, reviews, checkRuns);
|
|
603
788
|
}));
|
|
604
|
-
this.log.debug("GitHubProvider.fetchPullRequests", {
|
|
789
|
+
this.log.debug("GitHubProvider.fetchPullRequests", {
|
|
790
|
+
count: results.length
|
|
791
|
+
});
|
|
605
792
|
return results;
|
|
606
793
|
}
|
|
607
794
|
async fetchSingleMR(projectPath, mrIid, _currentUserNumericId) {
|
|
@@ -714,7 +901,11 @@ class GitHubProvider {
|
|
|
714
901
|
async fetchPullRequestByBranch(projectPath, sourceBranch) {
|
|
715
902
|
const res = await this.api("GET", `/repos/${projectPath}/pulls?head=${projectPath.split("/")[0]}:${encodeURIComponent(sourceBranch)}&state=open&per_page=1`);
|
|
716
903
|
if (!res.ok) {
|
|
717
|
-
this.log.warn("fetchPullRequestByBranch failed", {
|
|
904
|
+
this.log.warn("fetchPullRequestByBranch failed", {
|
|
905
|
+
projectPath,
|
|
906
|
+
sourceBranch,
|
|
907
|
+
status: res.status
|
|
908
|
+
});
|
|
718
909
|
return null;
|
|
719
910
|
}
|
|
720
911
|
const prs = await res.json();
|
|
@@ -798,6 +989,83 @@ class GitHubProvider {
|
|
|
798
989
|
async restRequest(method, path, body) {
|
|
799
990
|
return this.api(method, path, body);
|
|
800
991
|
}
|
|
992
|
+
async mergePullRequest(projectPath, mrIid, input) {
|
|
993
|
+
const body = {};
|
|
994
|
+
if (input?.commitMessage != null)
|
|
995
|
+
body.commit_title = input.commitMessage;
|
|
996
|
+
if (input?.squashCommitMessage != null)
|
|
997
|
+
body.commit_title = input.squashCommitMessage;
|
|
998
|
+
if (input?.shouldRemoveSourceBranch != null)
|
|
999
|
+
body.delete_branch = input.shouldRemoveSourceBranch;
|
|
1000
|
+
if (input?.sha != null)
|
|
1001
|
+
body.sha = input.sha;
|
|
1002
|
+
if (input?.mergeMethod) {
|
|
1003
|
+
body.merge_method = input.mergeMethod;
|
|
1004
|
+
} else if (input?.squash) {
|
|
1005
|
+
body.merge_method = "squash";
|
|
1006
|
+
}
|
|
1007
|
+
const res = await this.api("PUT", `/repos/${projectPath}/pulls/${mrIid}/merge`, body);
|
|
1008
|
+
if (!res.ok) {
|
|
1009
|
+
const text = await res.text();
|
|
1010
|
+
throw new Error(`mergePullRequest failed: ${res.status} ${text}`);
|
|
1011
|
+
}
|
|
1012
|
+
const pr = await this.fetchSingleMR(projectPath, mrIid, null);
|
|
1013
|
+
if (!pr)
|
|
1014
|
+
throw new Error("Merged PR but failed to fetch it back");
|
|
1015
|
+
return pr;
|
|
1016
|
+
}
|
|
1017
|
+
async approvePullRequest(projectPath, mrIid) {
|
|
1018
|
+
const res = await this.api("POST", `/repos/${projectPath}/pulls/${mrIid}/reviews`, {
|
|
1019
|
+
event: "APPROVE"
|
|
1020
|
+
});
|
|
1021
|
+
if (!res.ok) {
|
|
1022
|
+
const text = await res.text().catch(() => "");
|
|
1023
|
+
throw new Error(`approvePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
async unapprovePullRequest(_projectPath, _mrIid) {
|
|
1027
|
+
throw new Error("unapprovePullRequest is not supported by GitHub. " + "Check provider.capabilities.canUnapprove before calling.");
|
|
1028
|
+
}
|
|
1029
|
+
async rebasePullRequest(_projectPath, _mrIid) {
|
|
1030
|
+
throw new Error("rebasePullRequest is not supported by GitHub. " + "Check provider.capabilities.canRebase before calling.");
|
|
1031
|
+
}
|
|
1032
|
+
async setAutoMerge(_projectPath, _mrIid) {
|
|
1033
|
+
throw new Error("setAutoMerge is not supported by the GitHub REST API. " + "Check provider.capabilities.canAutoMerge before calling.");
|
|
1034
|
+
}
|
|
1035
|
+
async cancelAutoMerge(_projectPath, _mrIid) {
|
|
1036
|
+
throw new Error("cancelAutoMerge is not supported by the GitHub REST API. " + "Check provider.capabilities.canAutoMerge before calling.");
|
|
1037
|
+
}
|
|
1038
|
+
async resolveDiscussion(_projectPath, _mrIid, _discussionId) {
|
|
1039
|
+
throw new Error("resolveDiscussion is not supported by the GitHub REST API. " + "Check provider.capabilities.canResolveDiscussions before calling.");
|
|
1040
|
+
}
|
|
1041
|
+
async unresolveDiscussion(_projectPath, _mrIid, _discussionId) {
|
|
1042
|
+
throw new Error("unresolveDiscussion is not supported by the GitHub REST API. " + "Check provider.capabilities.canResolveDiscussions before calling.");
|
|
1043
|
+
}
|
|
1044
|
+
async retryPipeline(projectPath, pipelineId) {
|
|
1045
|
+
const res = await this.api("POST", `/repos/${projectPath}/actions/runs/${pipelineId}/rerun`);
|
|
1046
|
+
if (!res.ok) {
|
|
1047
|
+
const text = await res.text().catch(() => "");
|
|
1048
|
+
throw new Error(`retryPipeline failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
async requestReReview(projectPath, mrIid, reviewerUsernames) {
|
|
1052
|
+
if (!reviewerUsernames?.length) {
|
|
1053
|
+
const prRes = await this.api("GET", `/repos/${projectPath}/pulls/${mrIid}`);
|
|
1054
|
+
if (!prRes.ok) {
|
|
1055
|
+
throw new Error(`requestReReview: failed to fetch PR: ${prRes.status}`);
|
|
1056
|
+
}
|
|
1057
|
+
const pr = await prRes.json();
|
|
1058
|
+
reviewerUsernames = pr.requested_reviewers.map((r) => r.login);
|
|
1059
|
+
if (!reviewerUsernames.length) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const res = await this.api("POST", `/repos/${projectPath}/pulls/${mrIid}/requested_reviewers`, { reviewers: reviewerUsernames });
|
|
1064
|
+
if (!res.ok) {
|
|
1065
|
+
const text = await res.text().catch(() => "");
|
|
1066
|
+
throw new Error(`requestReReview failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
801
1069
|
async api(method, path, body) {
|
|
802
1070
|
const url = `${this.apiBase}${path}`;
|
|
803
1071
|
const headers = {
|