@forge-glance/sdk 0.1.1 → 0.2.1

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/index.js CHANGED
@@ -101,6 +101,10 @@ var MR_DASHBOARD_FRAGMENT = `
101
101
  approvalsLeft
102
102
  resolvableDiscussionsCount
103
103
  resolvedDiscussionsCount
104
+ autoMergeEnabled
105
+ autoMergeStrategy
106
+ mergeUser { id username name avatarUrl }
107
+ mergeAfter
104
108
  headPipeline {
105
109
  id iid status
106
110
  createdAt
@@ -221,7 +225,11 @@ function toMR(gql, role, baseURL) {
221
225
  approved: gql.approved ?? false,
222
226
  approvedBy: gql.approvedBy.nodes.map(toUserRef),
223
227
  diffStats,
224
- detailedMergeStatus: gql.detailedMergeStatus ?? null
228
+ detailedMergeStatus: gql.detailedMergeStatus ?? null,
229
+ autoMergeEnabled: gql.autoMergeEnabled ?? false,
230
+ autoMergeStrategy: gql.autoMergeStrategy ?? null,
231
+ mergeUser: gql.mergeUser ? toUserRef(gql.mergeUser) : null,
232
+ mergeAfter: gql.mergeAfter ?? null
225
233
  };
226
234
  }
227
235
  var MR_DETAIL_QUERY = `
@@ -245,8 +253,20 @@ class GitLabProvider {
245
253
  this.baseURL = baseURL.replace(/\/$/, "");
246
254
  this.token = token;
247
255
  this.log = options.logger ?? noopLogger;
248
- this.mrDetailFetcher = new MRDetailFetcher(this.baseURL, token, { logger: this.log });
256
+ this.mrDetailFetcher = new MRDetailFetcher(this.baseURL, token, {
257
+ logger: this.log
258
+ });
249
259
  }
260
+ capabilities = {
261
+ canMerge: true,
262
+ canApprove: true,
263
+ canUnapprove: true,
264
+ canRebase: true,
265
+ canAutoMerge: true,
266
+ canResolveDiscussions: true,
267
+ canRetryPipeline: true,
268
+ canRequestReReview: true
269
+ };
250
270
  async validateToken() {
251
271
  const url = `${this.baseURL}/api/v4/user`;
252
272
  const res = await fetch(url, {
@@ -352,7 +372,11 @@ class GitLabProvider {
352
372
  headers: { "PRIVATE-TOKEN": this.token }
353
373
  });
354
374
  if (!res.ok) {
355
- this.log.warn("fetchPullRequestByBranch failed", { projectPath, sourceBranch, status: res.status });
375
+ this.log.warn("fetchPullRequestByBranch failed", {
376
+ projectPath,
377
+ sourceBranch,
378
+ status: res.status
379
+ });
356
380
  return null;
357
381
  }
358
382
  const mrs = await res.json();
@@ -390,10 +414,7 @@ class GitLabProvider {
390
414
  throw new Error(`createPullRequest failed: ${res.status} ${text}`);
391
415
  }
392
416
  const created = await res.json();
393
- const pr = await this.fetchSingleMR(input.projectPath, created.iid, null);
394
- if (!pr)
395
- throw new Error("Created MR but failed to fetch it back");
396
- return pr;
417
+ return this.fetchSingleMRWithRetry(input.projectPath, created.iid, "Created MR but failed to fetch it back");
397
418
  }
398
419
  async updatePullRequest(projectPath, mrIid, input) {
399
420
  const encoded = encodeURIComponent(projectPath);
@@ -426,10 +447,7 @@ class GitLabProvider {
426
447
  const text = await res.text();
427
448
  throw new Error(`updatePullRequest failed: ${res.status} ${text}`);
428
449
  }
429
- const pr = await this.fetchSingleMR(projectPath, mrIid, null);
430
- if (!pr)
431
- throw new Error("Updated MR but failed to fetch it back");
432
- return pr;
450
+ return this.fetchSingleMRWithRetry(projectPath, mrIid, "Updated MR but failed to fetch it back");
433
451
  }
434
452
  async restRequest(method, path, body) {
435
453
  const url = `${this.baseURL}${path}`;
@@ -445,6 +463,171 @@ class GitLabProvider {
445
463
  body: body !== undefined ? JSON.stringify(body) : undefined
446
464
  });
447
465
  }
466
+ async mergePullRequest(projectPath, mrIid, input) {
467
+ const encoded = encodeURIComponent(projectPath);
468
+ const body = {};
469
+ if (input?.commitMessage != null)
470
+ body.merge_commit_message = input.commitMessage;
471
+ if (input?.squashCommitMessage != null)
472
+ body.squash_commit_message = input.squashCommitMessage;
473
+ if (input?.squash != null)
474
+ body.squash = input.squash;
475
+ if (input?.shouldRemoveSourceBranch != null)
476
+ body.should_remove_source_branch = input.shouldRemoveSourceBranch;
477
+ if (input?.sha != null)
478
+ body.sha = input.sha;
479
+ if (input?.mergeMethod === "squash" && input?.squash == null)
480
+ body.squash = true;
481
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/merge`, {
482
+ method: "PUT",
483
+ headers: {
484
+ "PRIVATE-TOKEN": this.token,
485
+ "Content-Type": "application/json"
486
+ },
487
+ body: JSON.stringify(body)
488
+ });
489
+ if (!res.ok) {
490
+ const text = await res.text();
491
+ throw new Error(`mergePullRequest failed: ${res.status} ${text}`);
492
+ }
493
+ return this.fetchSingleMRWithRetry(projectPath, mrIid, "Merged MR but failed to fetch it back");
494
+ }
495
+ async approvePullRequest(projectPath, mrIid) {
496
+ const encoded = encodeURIComponent(projectPath);
497
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/approve`, {
498
+ method: "POST",
499
+ headers: { "PRIVATE-TOKEN": this.token }
500
+ });
501
+ if (!res.ok) {
502
+ const text = await res.text().catch(() => "");
503
+ throw new Error(`approvePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
504
+ }
505
+ }
506
+ async unapprovePullRequest(projectPath, mrIid) {
507
+ const encoded = encodeURIComponent(projectPath);
508
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/unapprove`, {
509
+ method: "POST",
510
+ headers: { "PRIVATE-TOKEN": this.token }
511
+ });
512
+ if (!res.ok) {
513
+ const text = await res.text().catch(() => "");
514
+ throw new Error(`unapprovePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
515
+ }
516
+ }
517
+ async rebasePullRequest(projectPath, mrIid) {
518
+ const encoded = encodeURIComponent(projectPath);
519
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/rebase`, {
520
+ method: "PUT",
521
+ headers: { "PRIVATE-TOKEN": this.token }
522
+ });
523
+ if (!res.ok) {
524
+ const text = await res.text().catch(() => "");
525
+ throw new Error(`rebasePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
526
+ }
527
+ }
528
+ async setAutoMerge(projectPath, mrIid) {
529
+ const encoded = encodeURIComponent(projectPath);
530
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/merge`, {
531
+ method: "PUT",
532
+ headers: {
533
+ "PRIVATE-TOKEN": this.token,
534
+ "Content-Type": "application/json"
535
+ },
536
+ body: JSON.stringify({ merge_when_pipeline_succeeds: true })
537
+ });
538
+ if (!res.ok) {
539
+ const text = await res.text().catch(() => "");
540
+ throw new Error(`setAutoMerge failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
541
+ }
542
+ }
543
+ async cancelAutoMerge(projectPath, mrIid) {
544
+ const encoded = encodeURIComponent(projectPath);
545
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/cancel_merge_when_pipeline_succeeds`, {
546
+ method: "POST",
547
+ headers: { "PRIVATE-TOKEN": this.token }
548
+ });
549
+ if (!res.ok) {
550
+ const text = await res.text().catch(() => "");
551
+ throw new Error(`cancelAutoMerge failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
552
+ }
553
+ }
554
+ async resolveDiscussion(projectPath, mrIid, discussionId) {
555
+ const encoded = encodeURIComponent(projectPath);
556
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/discussions/${discussionId}`, {
557
+ method: "PUT",
558
+ headers: {
559
+ "PRIVATE-TOKEN": this.token,
560
+ "Content-Type": "application/json"
561
+ },
562
+ body: JSON.stringify({ resolved: true })
563
+ });
564
+ if (!res.ok) {
565
+ const text = await res.text().catch(() => "");
566
+ throw new Error(`resolveDiscussion failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
567
+ }
568
+ }
569
+ async unresolveDiscussion(projectPath, mrIid, discussionId) {
570
+ const encoded = encodeURIComponent(projectPath);
571
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}/discussions/${discussionId}`, {
572
+ method: "PUT",
573
+ headers: {
574
+ "PRIVATE-TOKEN": this.token,
575
+ "Content-Type": "application/json"
576
+ },
577
+ body: JSON.stringify({ resolved: false })
578
+ });
579
+ if (!res.ok) {
580
+ const text = await res.text().catch(() => "");
581
+ throw new Error(`unresolveDiscussion failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
582
+ }
583
+ }
584
+ async retryPipeline(projectPath, pipelineId) {
585
+ const encoded = encodeURIComponent(projectPath);
586
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/pipelines/${pipelineId}/retry`, {
587
+ method: "POST",
588
+ headers: { "PRIVATE-TOKEN": this.token }
589
+ });
590
+ if (!res.ok) {
591
+ const text = await res.text().catch(() => "");
592
+ throw new Error(`retryPipeline failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
593
+ }
594
+ }
595
+ async requestReReview(projectPath, mrIid, _reviewerUsernames) {
596
+ const encoded = encodeURIComponent(projectPath);
597
+ const mrRes = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}`, { headers: { "PRIVATE-TOKEN": this.token } });
598
+ if (!mrRes.ok) {
599
+ const text = await mrRes.text().catch(() => "");
600
+ throw new Error(`requestReReview: failed to fetch MR: ${mrRes.status}${text ? ` — ${text}` : ""}`);
601
+ }
602
+ const mr = await mrRes.json();
603
+ const reviewerIds = mr.reviewers?.map((r) => r.id) ?? [];
604
+ if (reviewerIds.length === 0) {
605
+ return;
606
+ }
607
+ const res = await fetch(`${this.baseURL}/api/v4/projects/${encoded}/merge_requests/${mrIid}`, {
608
+ method: "PUT",
609
+ headers: {
610
+ "PRIVATE-TOKEN": this.token,
611
+ "Content-Type": "application/json"
612
+ },
613
+ body: JSON.stringify({ reviewer_ids: reviewerIds })
614
+ });
615
+ if (!res.ok) {
616
+ const text = await res.text().catch(() => "");
617
+ throw new Error(`requestReReview failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
618
+ }
619
+ }
620
+ async fetchSingleMRWithRetry(projectPath, mrIid, errorMessage) {
621
+ for (let attempt = 0;attempt < 3; attempt++) {
622
+ if (attempt > 0) {
623
+ await new Promise((r) => setTimeout(r, attempt * 300));
624
+ }
625
+ const pr = await this.fetchSingleMR(projectPath, mrIid, null);
626
+ if (pr)
627
+ return pr;
628
+ }
629
+ throw new Error(errorMessage);
630
+ }
448
631
  async runQuery(query, variables) {
449
632
  const url = `${this.baseURL}/api/graphql`;
450
633
  const body = JSON.stringify({ query, variables: variables ?? {} });
@@ -560,6 +743,16 @@ class GitHubProvider {
560
743
  this.apiBase = `${this.baseURL}/api/v3`;
561
744
  }
562
745
  }
746
+ capabilities = {
747
+ canMerge: true,
748
+ canApprove: true,
749
+ canUnapprove: false,
750
+ canRebase: false,
751
+ canAutoMerge: false,
752
+ canResolveDiscussions: false,
753
+ canRetryPipeline: true,
754
+ canRequestReReview: true
755
+ };
563
756
  async validateToken() {
564
757
  const res = await this.api("GET", "/user");
565
758
  if (!res.ok) {
@@ -601,7 +794,9 @@ class GitHubProvider {
601
794
  ]);
602
795
  return this.toPullRequest(pr, prRoles, reviews, checkRuns);
603
796
  }));
604
- this.log.debug("GitHubProvider.fetchPullRequests", { count: results.length });
797
+ this.log.debug("GitHubProvider.fetchPullRequests", {
798
+ count: results.length
799
+ });
605
800
  return results;
606
801
  }
607
802
  async fetchSingleMR(projectPath, mrIid, _currentUserNumericId) {
@@ -714,7 +909,11 @@ class GitHubProvider {
714
909
  async fetchPullRequestByBranch(projectPath, sourceBranch) {
715
910
  const res = await this.api("GET", `/repos/${projectPath}/pulls?head=${projectPath.split("/")[0]}:${encodeURIComponent(sourceBranch)}&state=open&per_page=1`);
716
911
  if (!res.ok) {
717
- this.log.warn("fetchPullRequestByBranch failed", { projectPath, sourceBranch, status: res.status });
912
+ this.log.warn("fetchPullRequestByBranch failed", {
913
+ projectPath,
914
+ sourceBranch,
915
+ status: res.status
916
+ });
718
917
  return null;
719
918
  }
720
919
  const prs = await res.json();
@@ -798,6 +997,83 @@ class GitHubProvider {
798
997
  async restRequest(method, path, body) {
799
998
  return this.api(method, path, body);
800
999
  }
1000
+ async mergePullRequest(projectPath, mrIid, input) {
1001
+ const body = {};
1002
+ if (input?.commitMessage != null)
1003
+ body.commit_title = input.commitMessage;
1004
+ if (input?.squashCommitMessage != null)
1005
+ body.commit_title = input.squashCommitMessage;
1006
+ if (input?.shouldRemoveSourceBranch != null)
1007
+ body.delete_branch = input.shouldRemoveSourceBranch;
1008
+ if (input?.sha != null)
1009
+ body.sha = input.sha;
1010
+ if (input?.mergeMethod) {
1011
+ body.merge_method = input.mergeMethod;
1012
+ } else if (input?.squash) {
1013
+ body.merge_method = "squash";
1014
+ }
1015
+ const res = await this.api("PUT", `/repos/${projectPath}/pulls/${mrIid}/merge`, body);
1016
+ if (!res.ok) {
1017
+ const text = await res.text();
1018
+ throw new Error(`mergePullRequest failed: ${res.status} ${text}`);
1019
+ }
1020
+ const pr = await this.fetchSingleMR(projectPath, mrIid, null);
1021
+ if (!pr)
1022
+ throw new Error("Merged PR but failed to fetch it back");
1023
+ return pr;
1024
+ }
1025
+ async approvePullRequest(projectPath, mrIid) {
1026
+ const res = await this.api("POST", `/repos/${projectPath}/pulls/${mrIid}/reviews`, {
1027
+ event: "APPROVE"
1028
+ });
1029
+ if (!res.ok) {
1030
+ const text = await res.text().catch(() => "");
1031
+ throw new Error(`approvePullRequest failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
1032
+ }
1033
+ }
1034
+ async unapprovePullRequest(_projectPath, _mrIid) {
1035
+ throw new Error("unapprovePullRequest is not supported by GitHub. " + "Check provider.capabilities.canUnapprove before calling.");
1036
+ }
1037
+ async rebasePullRequest(_projectPath, _mrIid) {
1038
+ throw new Error("rebasePullRequest is not supported by GitHub. " + "Check provider.capabilities.canRebase before calling.");
1039
+ }
1040
+ async setAutoMerge(_projectPath, _mrIid) {
1041
+ throw new Error("setAutoMerge is not supported by the GitHub REST API. " + "Check provider.capabilities.canAutoMerge before calling.");
1042
+ }
1043
+ async cancelAutoMerge(_projectPath, _mrIid) {
1044
+ throw new Error("cancelAutoMerge is not supported by the GitHub REST API. " + "Check provider.capabilities.canAutoMerge before calling.");
1045
+ }
1046
+ async resolveDiscussion(_projectPath, _mrIid, _discussionId) {
1047
+ throw new Error("resolveDiscussion is not supported by the GitHub REST API. " + "Check provider.capabilities.canResolveDiscussions before calling.");
1048
+ }
1049
+ async unresolveDiscussion(_projectPath, _mrIid, _discussionId) {
1050
+ throw new Error("unresolveDiscussion is not supported by the GitHub REST API. " + "Check provider.capabilities.canResolveDiscussions before calling.");
1051
+ }
1052
+ async retryPipeline(projectPath, pipelineId) {
1053
+ const res = await this.api("POST", `/repos/${projectPath}/actions/runs/${pipelineId}/rerun`);
1054
+ if (!res.ok) {
1055
+ const text = await res.text().catch(() => "");
1056
+ throw new Error(`retryPipeline failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
1057
+ }
1058
+ }
1059
+ async requestReReview(projectPath, mrIid, reviewerUsernames) {
1060
+ if (!reviewerUsernames?.length) {
1061
+ const prRes = await this.api("GET", `/repos/${projectPath}/pulls/${mrIid}`);
1062
+ if (!prRes.ok) {
1063
+ throw new Error(`requestReReview: failed to fetch PR: ${prRes.status}`);
1064
+ }
1065
+ const pr = await prRes.json();
1066
+ reviewerUsernames = pr.requested_reviewers.map((r) => r.login);
1067
+ if (!reviewerUsernames.length) {
1068
+ return;
1069
+ }
1070
+ }
1071
+ const res = await this.api("POST", `/repos/${projectPath}/pulls/${mrIid}/requested_reviewers`, { reviewers: reviewerUsernames });
1072
+ if (!res.ok) {
1073
+ const text = await res.text().catch(() => "");
1074
+ throw new Error(`requestReReview failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
1075
+ }
1076
+ }
801
1077
  async api(method, path, body) {
802
1078
  const url = `${this.apiBase}${path}`;
803
1079
  const headers = {
@@ -912,7 +1188,11 @@ class GitHubProvider {
912
1188
  approved: approvedBy.length > 0 && changesRequested === 0,
913
1189
  approvedBy,
914
1190
  diffStats,
915
- detailedMergeStatus: null
1191
+ detailedMergeStatus: null,
1192
+ autoMergeEnabled: pr.auto_merge != null,
1193
+ autoMergeStrategy: pr.auto_merge?.merge_method ?? null,
1194
+ mergeUser: pr.auto_merge ? toUserRef2(pr.auto_merge.enabled_by) : null,
1195
+ mergeAfter: null
916
1196
  };
917
1197
  }
918
1198
  }