paperclip-github-plugin 0.2.3 → 0.3.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/README.md +13 -1
- package/dist/manifest.js +18 -1
- package/dist/ui/index.js +30791 -3457
- package/dist/ui/index.js.map +4 -4
- package/dist/worker.js +3668 -250
- package/package.json +6 -2
package/dist/worker.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
// src/worker.ts
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
5
7
|
import { Octokit } from "@octokit/rest";
|
|
6
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
definePlugin,
|
|
10
|
+
startWorkerRpcHost
|
|
11
|
+
} from "@paperclipai/plugin-sdk";
|
|
7
12
|
|
|
8
13
|
// src/github-agent-tools.ts
|
|
9
14
|
var repositoryProperty = {
|
|
@@ -529,6 +534,16 @@ var DEFAULT_IGNORED_GITHUB_ISSUE_USERNAMES = ["renovate"];
|
|
|
529
534
|
var GITHUB_API_VERSION = "2026-03-10";
|
|
530
535
|
var DEFAULT_PAPERCLIP_LABEL_COLOR = "#6366f1";
|
|
531
536
|
var PAPERCLIP_LABEL_PAGE_SIZE = 100;
|
|
537
|
+
var PROJECT_PULL_REQUEST_SUMMARY_CONCURRENCY = 8;
|
|
538
|
+
var PROJECT_PULL_REQUEST_PAGE_SIZE = 10;
|
|
539
|
+
var PROJECT_PULL_REQUEST_METRICS_BATCH_SIZE = 100;
|
|
540
|
+
var PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS = 30 * 6e4;
|
|
541
|
+
var PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS = 60 * 6e4;
|
|
542
|
+
var PROJECT_PULL_REQUEST_DETAIL_CACHE_TTL_MS = 30 * 6e4;
|
|
543
|
+
var PROJECT_PULL_REQUEST_ISSUE_LOOKUP_CACHE_TTL_MS = 60 * 6e4;
|
|
544
|
+
var PROJECT_PULL_REQUEST_GITHUB_INSIGHT_CACHE_TTL_MS = 60 * 6e4;
|
|
545
|
+
var PROJECT_PULL_REQUEST_BRANCH_COMPARE_CACHE_TTL_MS = 30 * 6e4;
|
|
546
|
+
var GITHUB_TOKEN_PERMISSION_AUDIT_CACHE_TTL_MS = 5 * 6e4;
|
|
532
547
|
var MANUAL_SYNC_RESPONSE_GRACE_PERIOD_MS = 500;
|
|
533
548
|
var RUNNING_SYNC_MESSAGE = "GitHub sync is running in the background. This page will update when it finishes.";
|
|
534
549
|
var CANCELLING_SYNC_MESSAGE = "Cancellation requested. GitHub sync will stop after the current step finishes.";
|
|
@@ -542,6 +557,7 @@ var MISSING_MAPPING_SYNC_ACTION = "Open settings, add a repository mapping, let
|
|
|
542
557
|
var MISSING_BOARD_ACCESS_SYNC_MESSAGE = "Connect Paperclip board access before running sync on this authenticated deployment.";
|
|
543
558
|
var MISSING_BOARD_ACCESS_SYNC_ACTION = "Open plugin settings for each mapped company that sync will touch, connect Paperclip board access, approve the flow, and then run sync again.";
|
|
544
559
|
var ISSUE_LINK_ENTITY_TYPE = "paperclip-github-plugin.issue-link";
|
|
560
|
+
var PULL_REQUEST_LINK_ENTITY_TYPE = "paperclip-github-plugin.pull-request-link";
|
|
545
561
|
var COMMENT_ANNOTATION_ENTITY_TYPE = "paperclip-github-plugin.comment-annotation";
|
|
546
562
|
var EXTERNAL_CONFIG_FILE_PATH_SEGMENTS = [".paperclip", "plugins", "github-sync", "config.json"];
|
|
547
563
|
var AI_AUTHORED_COMMENT_FOOTER_PREFIX = "Created by a Paperclip AI agent using ";
|
|
@@ -552,6 +568,26 @@ var activeSyncPromise = null;
|
|
|
552
568
|
var activeRunningSyncState = null;
|
|
553
569
|
var activePaperclipApiAuthTokensByCompanyId = null;
|
|
554
570
|
var activeExternalConfigWarningKey = null;
|
|
571
|
+
var activeProjectPullRequestPageCache = /* @__PURE__ */ new Map();
|
|
572
|
+
var activeProjectPullRequestCountCache = /* @__PURE__ */ new Map();
|
|
573
|
+
var activeProjectPullRequestCountPromiseCache = /* @__PURE__ */ new Map();
|
|
574
|
+
var activeProjectPullRequestMetricsCache = /* @__PURE__ */ new Map();
|
|
575
|
+
var activeProjectPullRequestMetricsPromiseCache = /* @__PURE__ */ new Map();
|
|
576
|
+
var activeProjectPullRequestSummaryCache = /* @__PURE__ */ new Map();
|
|
577
|
+
var activeProjectPullRequestSummaryPromiseCache = /* @__PURE__ */ new Map();
|
|
578
|
+
var activeProjectPullRequestSummaryRecordCache = /* @__PURE__ */ new Map();
|
|
579
|
+
var activeProjectPullRequestDetailCache = /* @__PURE__ */ new Map();
|
|
580
|
+
var activeProjectPullRequestIssueLookupCache = /* @__PURE__ */ new Map();
|
|
581
|
+
var activeGitHubPullRequestStatusSnapshotCache = /* @__PURE__ */ new Map();
|
|
582
|
+
var activeGitHubPullRequestStatusSnapshotPromiseCache = /* @__PURE__ */ new Map();
|
|
583
|
+
var activeGitHubPullRequestReviewSummaryCache = /* @__PURE__ */ new Map();
|
|
584
|
+
var activeGitHubPullRequestReviewSummaryPromiseCache = /* @__PURE__ */ new Map();
|
|
585
|
+
var activeGitHubPullRequestReviewThreadSummaryCache = /* @__PURE__ */ new Map();
|
|
586
|
+
var activeGitHubPullRequestReviewThreadSummaryPromiseCache = /* @__PURE__ */ new Map();
|
|
587
|
+
var activeGitHubPullRequestBehindCountCache = /* @__PURE__ */ new Map();
|
|
588
|
+
var activeGitHubPullRequestBehindCountPromiseCache = /* @__PURE__ */ new Map();
|
|
589
|
+
var activeGitHubRepositoryTokenCapabilityAuditCache = /* @__PURE__ */ new Map();
|
|
590
|
+
var activeGitHubRepositoryTokenCapabilityAuditPromiseCache = /* @__PURE__ */ new Map();
|
|
555
591
|
var PaperclipLabelSyncError = class extends Error {
|
|
556
592
|
name = "PaperclipLabelSyncError";
|
|
557
593
|
status;
|
|
@@ -634,23 +670,6 @@ var GITHUB_ISSUE_STATUS_SNAPSHOT_QUERY = `
|
|
|
634
670
|
}
|
|
635
671
|
}
|
|
636
672
|
`;
|
|
637
|
-
var GITHUB_PULL_REQUEST_REVIEW_THREADS_QUERY = `
|
|
638
|
-
query GitHubPullRequestReviewThreads($owner: String!, $repo: String!, $pullRequestNumber: Int!, $after: String) {
|
|
639
|
-
repository(owner: $owner, name: $repo) {
|
|
640
|
-
pullRequest(number: $pullRequestNumber) {
|
|
641
|
-
reviewThreads(first: 100, after: $after) {
|
|
642
|
-
pageInfo {
|
|
643
|
-
hasNextPage
|
|
644
|
-
endCursor
|
|
645
|
-
}
|
|
646
|
-
nodes {
|
|
647
|
-
isResolved
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
`;
|
|
654
673
|
var GITHUB_PULL_REQUEST_CI_CONTEXTS_QUERY = `
|
|
655
674
|
query GitHubPullRequestCiContexts($owner: String!, $repo: String!, $pullRequestNumber: Int!, $after: String) {
|
|
656
675
|
repository(owner: $owner, name: $repo) {
|
|
@@ -750,6 +769,235 @@ var GITHUB_REPOSITORY_OPEN_PULL_REQUEST_STATUSES_QUERY = `
|
|
|
750
769
|
}
|
|
751
770
|
}
|
|
752
771
|
`;
|
|
772
|
+
var GITHUB_PROJECT_PULL_REQUEST_BASE_FIELDS = `
|
|
773
|
+
id
|
|
774
|
+
number
|
|
775
|
+
title
|
|
776
|
+
url
|
|
777
|
+
state
|
|
778
|
+
mergeable
|
|
779
|
+
mergeStateStatus
|
|
780
|
+
createdAt
|
|
781
|
+
updatedAt
|
|
782
|
+
baseRefName
|
|
783
|
+
headRefName
|
|
784
|
+
headRepositoryOwner {
|
|
785
|
+
login
|
|
786
|
+
}
|
|
787
|
+
changedFiles
|
|
788
|
+
commits {
|
|
789
|
+
totalCount
|
|
790
|
+
}
|
|
791
|
+
author {
|
|
792
|
+
login
|
|
793
|
+
url
|
|
794
|
+
avatarUrl
|
|
795
|
+
}
|
|
796
|
+
labels(first: 20) {
|
|
797
|
+
nodes {
|
|
798
|
+
name
|
|
799
|
+
color
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
comments {
|
|
803
|
+
totalCount
|
|
804
|
+
}
|
|
805
|
+
closingIssuesReferences(first: 10) {
|
|
806
|
+
nodes {
|
|
807
|
+
number
|
|
808
|
+
url
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
`;
|
|
812
|
+
var GITHUB_PROJECT_PULL_REQUEST_INSIGHT_FIELDS = `
|
|
813
|
+
reviews(first: 100) {
|
|
814
|
+
pageInfo {
|
|
815
|
+
hasNextPage
|
|
816
|
+
endCursor
|
|
817
|
+
}
|
|
818
|
+
nodes {
|
|
819
|
+
state
|
|
820
|
+
author {
|
|
821
|
+
login
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
reviewThreads(first: 100) {
|
|
826
|
+
totalCount
|
|
827
|
+
pageInfo {
|
|
828
|
+
hasNextPage
|
|
829
|
+
endCursor
|
|
830
|
+
}
|
|
831
|
+
nodes {
|
|
832
|
+
isResolved
|
|
833
|
+
comments(first: 1) {
|
|
834
|
+
nodes {
|
|
835
|
+
author {
|
|
836
|
+
login
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
statusCheckRollup {
|
|
843
|
+
contexts(first: 100) {
|
|
844
|
+
pageInfo {
|
|
845
|
+
hasNextPage
|
|
846
|
+
endCursor
|
|
847
|
+
}
|
|
848
|
+
nodes {
|
|
849
|
+
__typename
|
|
850
|
+
... on CheckRun {
|
|
851
|
+
status
|
|
852
|
+
conclusion
|
|
853
|
+
}
|
|
854
|
+
... on StatusContext {
|
|
855
|
+
state
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
`;
|
|
861
|
+
var GITHUB_PROJECT_PULL_REQUEST_SUMMARY_FIELDS = `${GITHUB_PROJECT_PULL_REQUEST_BASE_FIELDS}${GITHUB_PROJECT_PULL_REQUEST_INSIGHT_FIELDS}`;
|
|
862
|
+
var GITHUB_PROJECT_PULL_REQUEST_METRICS_FIELDS = `
|
|
863
|
+
number
|
|
864
|
+
mergeable
|
|
865
|
+
reviews(first: 100) {
|
|
866
|
+
pageInfo {
|
|
867
|
+
hasNextPage
|
|
868
|
+
endCursor
|
|
869
|
+
}
|
|
870
|
+
nodes {
|
|
871
|
+
state
|
|
872
|
+
author {
|
|
873
|
+
login
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
reviewThreads(first: 100) {
|
|
878
|
+
pageInfo {
|
|
879
|
+
hasNextPage
|
|
880
|
+
endCursor
|
|
881
|
+
}
|
|
882
|
+
nodes {
|
|
883
|
+
isResolved
|
|
884
|
+
comments(first: 1) {
|
|
885
|
+
nodes {
|
|
886
|
+
author {
|
|
887
|
+
login
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
statusCheckRollup {
|
|
894
|
+
contexts(first: 100) {
|
|
895
|
+
pageInfo {
|
|
896
|
+
hasNextPage
|
|
897
|
+
endCursor
|
|
898
|
+
}
|
|
899
|
+
nodes {
|
|
900
|
+
__typename
|
|
901
|
+
... on CheckRun {
|
|
902
|
+
status
|
|
903
|
+
conclusion
|
|
904
|
+
}
|
|
905
|
+
... on StatusContext {
|
|
906
|
+
state
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
`;
|
|
912
|
+
var GITHUB_PROJECT_PULL_REQUESTS_QUERY = `
|
|
913
|
+
query GitHubProjectPullRequests($owner: String!, $repo: String!, $after: String, $first: Int!) {
|
|
914
|
+
repository(owner: $owner, name: $repo) {
|
|
915
|
+
nameWithOwner
|
|
916
|
+
url
|
|
917
|
+
defaultBranchRef {
|
|
918
|
+
name
|
|
919
|
+
}
|
|
920
|
+
pullRequests(first: $first, after: $after, states: [OPEN], orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
921
|
+
totalCount
|
|
922
|
+
pageInfo {
|
|
923
|
+
hasNextPage
|
|
924
|
+
endCursor
|
|
925
|
+
}
|
|
926
|
+
nodes {
|
|
927
|
+
${GITHUB_PROJECT_PULL_REQUEST_SUMMARY_FIELDS}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
`;
|
|
933
|
+
var GITHUB_PROJECT_PULL_REQUEST_METRICS_QUERY = `
|
|
934
|
+
query GitHubProjectPullRequestMetrics($owner: String!, $repo: String!, $after: String, $first: Int!) {
|
|
935
|
+
repository(owner: $owner, name: $repo) {
|
|
936
|
+
defaultBranchRef {
|
|
937
|
+
name
|
|
938
|
+
}
|
|
939
|
+
pullRequests(first: $first, after: $after, states: [OPEN], orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
940
|
+
totalCount
|
|
941
|
+
pageInfo {
|
|
942
|
+
hasNextPage
|
|
943
|
+
endCursor
|
|
944
|
+
}
|
|
945
|
+
nodes {
|
|
946
|
+
${GITHUB_PROJECT_PULL_REQUEST_METRICS_FIELDS}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
`;
|
|
952
|
+
function buildGitHubProjectPullRequestByNumberAlias(pullRequestNumber) {
|
|
953
|
+
return `pr_${Math.max(1, Math.floor(pullRequestNumber))}`;
|
|
954
|
+
}
|
|
955
|
+
function buildGitHubProjectPullRequestsByNumberQuery(pullRequestNumbers) {
|
|
956
|
+
const normalizedNumbers = [
|
|
957
|
+
...new Set(
|
|
958
|
+
pullRequestNumbers.map((value) => Math.floor(value)).filter((value) => Number.isFinite(value) && value > 0)
|
|
959
|
+
)
|
|
960
|
+
];
|
|
961
|
+
if (normalizedNumbers.length === 0) {
|
|
962
|
+
throw new Error("At least one pull request number is required.");
|
|
963
|
+
}
|
|
964
|
+
const selections = normalizedNumbers.map(
|
|
965
|
+
(pullRequestNumber) => `
|
|
966
|
+
${buildGitHubProjectPullRequestByNumberAlias(pullRequestNumber)}: pullRequest(number: ${pullRequestNumber}) {
|
|
967
|
+
${GITHUB_PROJECT_PULL_REQUEST_BASE_FIELDS}
|
|
968
|
+
}`
|
|
969
|
+
).join("\n");
|
|
970
|
+
return `
|
|
971
|
+
query GitHubProjectPullRequestsByNumber($owner: String!, $repo: String!) {
|
|
972
|
+
repository(owner: $owner, name: $repo) {
|
|
973
|
+
${selections}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
`;
|
|
977
|
+
}
|
|
978
|
+
var GITHUB_PROJECT_OPEN_PULL_REQUEST_COUNT_QUERY = `
|
|
979
|
+
query GitHubProjectOpenPullRequestCount($owner: String!, $repo: String!) {
|
|
980
|
+
repository(owner: $owner, name: $repo) {
|
|
981
|
+
pullRequests(first: 1, states: [OPEN]) {
|
|
982
|
+
totalCount
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
`;
|
|
987
|
+
var GITHUB_PULL_REQUEST_CLOSING_ISSUES_QUERY = `
|
|
988
|
+
query GitHubPullRequestClosingIssues($owner: String!, $repo: String!, $pullRequestNumber: Int!) {
|
|
989
|
+
repository(owner: $owner, name: $repo) {
|
|
990
|
+
pullRequest(number: $pullRequestNumber) {
|
|
991
|
+
closingIssuesReferences(first: 10) {
|
|
992
|
+
nodes {
|
|
993
|
+
number
|
|
994
|
+
url
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
`;
|
|
753
1001
|
var GITHUB_PULL_REQUEST_REVIEW_THREADS_DETAILED_QUERY = `
|
|
754
1002
|
query GitHubPullRequestReviewThreadsDetailed($owner: String!, $repo: String!, $pullRequestNumber: Int!, $after: String) {
|
|
755
1003
|
repository(owner: $owner, name: $repo) {
|
|
@@ -860,6 +1108,30 @@ var GITHUB_MARK_PULL_REQUEST_READY_FOR_REVIEW_MUTATION = `
|
|
|
860
1108
|
}
|
|
861
1109
|
}
|
|
862
1110
|
`;
|
|
1111
|
+
var GITHUB_REQUEST_PULL_REQUEST_COPILOT_REVIEW_MUTATION = `
|
|
1112
|
+
mutation GitHubRequestPullRequestCopilotReview($pullRequestId: ID!, $botLogins: [String!]!) {
|
|
1113
|
+
requestReviews(input: {
|
|
1114
|
+
pullRequestId: $pullRequestId
|
|
1115
|
+
botLogins: $botLogins
|
|
1116
|
+
}) {
|
|
1117
|
+
pullRequest {
|
|
1118
|
+
id
|
|
1119
|
+
number
|
|
1120
|
+
url
|
|
1121
|
+
}
|
|
1122
|
+
requestedReviewers(first: 10) {
|
|
1123
|
+
edges {
|
|
1124
|
+
node {
|
|
1125
|
+
__typename
|
|
1126
|
+
... on Bot {
|
|
1127
|
+
login
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
`;
|
|
863
1135
|
var DEFAULT_SETTINGS = {
|
|
864
1136
|
mappings: [],
|
|
865
1137
|
syncState: {
|
|
@@ -959,6 +1231,118 @@ function getErrorResponseDataMessage(error) {
|
|
|
959
1231
|
const message = data.message;
|
|
960
1232
|
return typeof message === "string" && message.trim() ? message.trim() : void 0;
|
|
961
1233
|
}
|
|
1234
|
+
function getErrorResponseDataErrors(error) {
|
|
1235
|
+
if (!error || typeof error !== "object" || !("response" in error)) {
|
|
1236
|
+
return [];
|
|
1237
|
+
}
|
|
1238
|
+
const response = error.response;
|
|
1239
|
+
if (!response || typeof response !== "object" || !("data" in response)) {
|
|
1240
|
+
return [];
|
|
1241
|
+
}
|
|
1242
|
+
const data = response.data;
|
|
1243
|
+
if (!data || typeof data !== "object" || !("errors" in data)) {
|
|
1244
|
+
return [];
|
|
1245
|
+
}
|
|
1246
|
+
const errors = data.errors;
|
|
1247
|
+
return Array.isArray(errors) ? errors : [];
|
|
1248
|
+
}
|
|
1249
|
+
function getGitHubValidationErrorSummary(error) {
|
|
1250
|
+
const entries = getErrorResponseDataErrors(error);
|
|
1251
|
+
if (entries.length === 0) {
|
|
1252
|
+
return void 0;
|
|
1253
|
+
}
|
|
1254
|
+
const summaries = /* @__PURE__ */ new Set();
|
|
1255
|
+
for (const entry of entries) {
|
|
1256
|
+
if (typeof entry === "string" && entry.trim()) {
|
|
1257
|
+
summaries.add(entry.trim());
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (!entry || typeof entry !== "object") {
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
const explicitMessage = "message" in entry && typeof entry.message === "string" ? entry.message.trim() : "";
|
|
1264
|
+
if (explicitMessage) {
|
|
1265
|
+
summaries.add(explicitMessage);
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
const resource = "resource" in entry && typeof entry.resource === "string" ? entry.resource.trim() : "";
|
|
1269
|
+
const field = "field" in entry && typeof entry.field === "string" ? entry.field.trim() : "";
|
|
1270
|
+
const code = "code" in entry && typeof entry.code === "string" ? entry.code.trim().replace(/_/g, " ") : "";
|
|
1271
|
+
const parts = [
|
|
1272
|
+
resource ? resource.replace(/([a-z])([A-Z])/g, "$1 $2") : "",
|
|
1273
|
+
field ? `field "${field}"` : "",
|
|
1274
|
+
code
|
|
1275
|
+
].filter(Boolean);
|
|
1276
|
+
if (parts.length > 0) {
|
|
1277
|
+
summaries.add(parts.join(" "));
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return summaries.size > 0 ? [...summaries].join("; ") : void 0;
|
|
1281
|
+
}
|
|
1282
|
+
function formatGitHubPermissionsHeader(value) {
|
|
1283
|
+
if (!value?.trim()) {
|
|
1284
|
+
return void 0;
|
|
1285
|
+
}
|
|
1286
|
+
const parts = value.replace(/;/g, ",").split(",").map((entry) => entry.trim()).filter(Boolean).map((entry) => {
|
|
1287
|
+
const [name, level] = entry.split("=").map((part) => part.trim());
|
|
1288
|
+
if (!name) {
|
|
1289
|
+
return "";
|
|
1290
|
+
}
|
|
1291
|
+
const normalizedName = name.replace(/_/g, " ");
|
|
1292
|
+
return level ? `${normalizedName}: ${level}` : normalizedName;
|
|
1293
|
+
}).filter(Boolean);
|
|
1294
|
+
return parts.length > 0 ? parts.join(", ") : void 0;
|
|
1295
|
+
}
|
|
1296
|
+
function getAcceptedGitHubPermissionsSummary(error) {
|
|
1297
|
+
const headers = getErrorResponseHeaders(error);
|
|
1298
|
+
return formatGitHubPermissionsHeader(headers["x-accepted-github-permissions"]) ?? formatGitHubPermissionsHeader(headers["x-accepted-oauth-scopes"]);
|
|
1299
|
+
}
|
|
1300
|
+
function buildGitHubPullRequestWriteActionError(params) {
|
|
1301
|
+
const rateLimitPause = getGitHubRateLimitPauseDetails(params.error);
|
|
1302
|
+
if (rateLimitPause) {
|
|
1303
|
+
const resourceLabel = formatGitHubRateLimitResource(rateLimitPause.resource) ?? "GitHub API";
|
|
1304
|
+
return new Error(`${resourceLabel} rate limit reached. Wait until ${formatUtcTimestamp(rateLimitPause.resetAt)} before retrying.`);
|
|
1305
|
+
}
|
|
1306
|
+
const actionLabel = params.action === "comment" ? "comment" : params.action === "review" ? "review" : "branch update";
|
|
1307
|
+
const rawMessage = getErrorMessage(params.error).trim();
|
|
1308
|
+
const responseMessage = getErrorResponseDataMessage(params.error);
|
|
1309
|
+
const validationSummary = getGitHubValidationErrorSummary(params.error);
|
|
1310
|
+
const permissionsSummary = getAcceptedGitHubPermissionsSummary(params.error);
|
|
1311
|
+
const status = getErrorStatus(params.error);
|
|
1312
|
+
const combinedMessage = [rawMessage, responseMessage].filter((value) => Boolean(value?.trim())).join(" ").toLowerCase();
|
|
1313
|
+
if (params.action === "review" && params.reviewType === "REQUEST_CHANGES" && !params.body?.trim() && status === 422) {
|
|
1314
|
+
return new Error("Add a review summary before requesting changes. GitHub requires a comment for this action.");
|
|
1315
|
+
}
|
|
1316
|
+
if ((status === 403 || status === 404) && (combinedMessage.includes("resource not accessible") || combinedMessage.includes("not accessible by"))) {
|
|
1317
|
+
const requiredAccess = params.action === "comment" ? "Issues: write" : "Pull requests: write";
|
|
1318
|
+
const permissionSuffix = permissionsSummary ? ` GitHub reported required permissions: ${permissionsSummary}.` : "";
|
|
1319
|
+
return new Error(
|
|
1320
|
+
`GitHub rejected this ${actionLabel} because the configured token cannot write to ${params.repositoryLabel}. Reconnect a token with ${requiredAccess} access and repository visibility for this repo, then retry.${permissionSuffix}`
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
if (params.action === "update_branch" && status === 422) {
|
|
1324
|
+
if (combinedMessage.includes("expected head sha") || validationSummary?.toLowerCase().includes("expected_head_sha")) {
|
|
1325
|
+
return new Error("This pull request changed while the branch update was being requested. Refresh the queue and try again.");
|
|
1326
|
+
}
|
|
1327
|
+
if (combinedMessage.includes("merge conflict") || combinedMessage.includes("conflict")) {
|
|
1328
|
+
return new Error("This pull request needs conflict resolution before it can be updated with the base branch.");
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (responseMessage === "Validation Failed" && validationSummary) {
|
|
1332
|
+
return new Error(`GitHub rejected this ${actionLabel}: ${validationSummary}.`);
|
|
1333
|
+
}
|
|
1334
|
+
if (responseMessage && responseMessage !== rawMessage) {
|
|
1335
|
+
const validationSuffix = validationSummary ? ` ${validationSummary}.` : "";
|
|
1336
|
+
return new Error(`GitHub rejected this ${actionLabel}: ${responseMessage}.${validationSuffix}`);
|
|
1337
|
+
}
|
|
1338
|
+
if (validationSummary) {
|
|
1339
|
+
return new Error(`GitHub rejected this ${actionLabel}: ${validationSummary}.`);
|
|
1340
|
+
}
|
|
1341
|
+
if (rawMessage) {
|
|
1342
|
+
return new Error(rawMessage);
|
|
1343
|
+
}
|
|
1344
|
+
return new Error(`GitHub rejected this ${actionLabel}.`);
|
|
1345
|
+
}
|
|
962
1346
|
function parsePositiveInteger(value) {
|
|
963
1347
|
if (!value?.trim()) {
|
|
964
1348
|
return void 0;
|
|
@@ -980,6 +1364,58 @@ function parseRetryAfterTimestamp(value, now = Date.now()) {
|
|
|
980
1364
|
const timestamp = Date.parse(value);
|
|
981
1365
|
return Number.isFinite(timestamp) ? timestamp : void 0;
|
|
982
1366
|
}
|
|
1367
|
+
function buildGitHubRepositoryTokenCapabilityAuditCacheKey(repository, samplePullRequestNumber) {
|
|
1368
|
+
return `${repository.url.toLowerCase()}::${typeof samplePullRequestNumber === "number" ? samplePullRequestNumber : "none"}`;
|
|
1369
|
+
}
|
|
1370
|
+
function clearGitHubRepositoryTokenCapabilityAudits() {
|
|
1371
|
+
activeGitHubRepositoryTokenCapabilityAuditCache.clear();
|
|
1372
|
+
activeGitHubRepositoryTokenCapabilityAuditPromiseCache.clear();
|
|
1373
|
+
}
|
|
1374
|
+
function getGitHubCapabilityMissingPermissionLabel(capability) {
|
|
1375
|
+
switch (capability) {
|
|
1376
|
+
case "comment":
|
|
1377
|
+
return "Issues: write or Pull requests: write";
|
|
1378
|
+
case "review":
|
|
1379
|
+
case "close":
|
|
1380
|
+
case "update_branch":
|
|
1381
|
+
return "Pull requests: write";
|
|
1382
|
+
case "merge":
|
|
1383
|
+
return "Contents: write";
|
|
1384
|
+
case "rerun_ci":
|
|
1385
|
+
return "Checks: write";
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
function classifyGitHubCapabilityProbeError(error, options = {}) {
|
|
1389
|
+
const status = getErrorStatus(error);
|
|
1390
|
+
if (status && options.grantedStatuses?.includes(status)) {
|
|
1391
|
+
return "granted";
|
|
1392
|
+
}
|
|
1393
|
+
if (status === 404 && options.allowNotFoundAsGranted) {
|
|
1394
|
+
return "granted";
|
|
1395
|
+
}
|
|
1396
|
+
if (status === 401 || status === 403 || status === 404) {
|
|
1397
|
+
return "missing";
|
|
1398
|
+
}
|
|
1399
|
+
return "unknown";
|
|
1400
|
+
}
|
|
1401
|
+
function buildGitHubRepositoryTokenCapabilityAudit(params) {
|
|
1402
|
+
const warnings = [...new Set((params.warnings ?? []).map((warning) => warning.trim()).filter(Boolean))];
|
|
1403
|
+
return {
|
|
1404
|
+
repositoryUrl: params.repository.url,
|
|
1405
|
+
repositoryLabel: formatRepositoryLabel(params.repository),
|
|
1406
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1407
|
+
status: params.missingPermissions.length > 0 ? "missing_permissions" : warnings.length > 0 ? "unverifiable" : "verified",
|
|
1408
|
+
...typeof params.samplePullRequestNumber === "number" ? { samplePullRequestNumber: params.samplePullRequestNumber } : {},
|
|
1409
|
+
canComment: params.canComment,
|
|
1410
|
+
canReview: params.canReview,
|
|
1411
|
+
canClose: params.canClose,
|
|
1412
|
+
canUpdateBranch: params.canUpdateBranch,
|
|
1413
|
+
canMerge: params.canMerge,
|
|
1414
|
+
canRerunCi: params.canRerunCi,
|
|
1415
|
+
missingPermissions: [...new Set(params.missingPermissions)].sort((left, right) => left.localeCompare(right)),
|
|
1416
|
+
warnings
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
983
1419
|
function normalizeSecretRef(value) {
|
|
984
1420
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
985
1421
|
}
|
|
@@ -1025,7 +1461,10 @@ function getGitHubRateLimitPauseDetails(error, now = Date.now()) {
|
|
|
1025
1461
|
const retryAfterTimestamp = parseRetryAfterTimestamp(headers["retry-after"], now);
|
|
1026
1462
|
const responseMessage = getErrorResponseDataMessage(error);
|
|
1027
1463
|
const rawMessage = [getErrorMessage(error), responseMessage].filter((value) => Boolean(value?.trim())).join(" ").toLowerCase();
|
|
1028
|
-
const
|
|
1464
|
+
const hasPrimaryLimitHeaders = remaining === "0" && resetAtSeconds !== void 0;
|
|
1465
|
+
const hasRetryAfterLimitHint = retryAfterTimestamp !== void 0;
|
|
1466
|
+
const hasRateLimitMessage = rawMessage.includes("rate limit");
|
|
1467
|
+
const looksRateLimited = status === 429 || hasPrimaryLimitHeaders || hasRetryAfterLimitHint || hasRateLimitMessage;
|
|
1029
1468
|
if (!looksRateLimited) {
|
|
1030
1469
|
return null;
|
|
1031
1470
|
}
|
|
@@ -1442,27 +1881,117 @@ function getSyncableMappingsForTarget(mappings, target) {
|
|
|
1442
1881
|
return syncableMappings;
|
|
1443
1882
|
}
|
|
1444
1883
|
}
|
|
1445
|
-
function
|
|
1446
|
-
if (
|
|
1447
|
-
return
|
|
1884
|
+
function normalizeProjectNameForComparison(value) {
|
|
1885
|
+
if (typeof value !== "string") {
|
|
1886
|
+
return "";
|
|
1448
1887
|
}
|
|
1449
|
-
|
|
1450
|
-
return target.githubIssueId !== void 0 && issue.id === target.githubIssueId || target.githubIssueNumber !== void 0 && issue.number === target.githubIssueNumber || target.githubIssueUrl !== void 0 && normalizedIssueUrl === target.githubIssueUrl;
|
|
1888
|
+
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
1451
1889
|
}
|
|
1452
|
-
function
|
|
1453
|
-
|
|
1454
|
-
|
|
1890
|
+
function hydrateResolvedProjectMapping(mapping, context) {
|
|
1891
|
+
const resolvedProjectName = mapping.paperclipProjectName.trim() || context.projectName?.trim() || mapping.paperclipProjectName;
|
|
1892
|
+
return {
|
|
1893
|
+
...mapping,
|
|
1894
|
+
companyId: mapping.companyId ?? context.companyId,
|
|
1895
|
+
paperclipProjectId: mapping.paperclipProjectId ?? context.projectId,
|
|
1896
|
+
paperclipProjectName: resolvedProjectName
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
function getProjectRepositoryUrlFromProjectRecord(project) {
|
|
1900
|
+
if (!project) {
|
|
1901
|
+
return void 0;
|
|
1455
1902
|
}
|
|
1456
|
-
|
|
1903
|
+
const repositoryCandidates = [
|
|
1904
|
+
project.codebase?.repoUrl,
|
|
1905
|
+
project.primaryWorkspace?.repoUrl,
|
|
1906
|
+
...Array.isArray(project.workspaces) ? project.workspaces.map((workspace) => workspace?.repoUrl) : []
|
|
1907
|
+
];
|
|
1908
|
+
for (const repositoryUrl of repositoryCandidates) {
|
|
1909
|
+
if (typeof repositoryUrl !== "string" || !repositoryUrl.trim()) {
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
const normalizedRepositoryUrl = parseRepositoryReference(repositoryUrl)?.url ?? repositoryUrl.trim();
|
|
1913
|
+
if (parseRepositoryReference(normalizedRepositoryUrl)) {
|
|
1914
|
+
return normalizedRepositoryUrl;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
return void 0;
|
|
1457
1918
|
}
|
|
1458
|
-
async function
|
|
1459
|
-
const
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1919
|
+
async function resolveProjectScopedMappings(ctx, mappings, params) {
|
|
1920
|
+
const companyId = normalizeCompanyId(params.companyId);
|
|
1921
|
+
const projectId = typeof params.projectId === "string" && params.projectId.trim() ? params.projectId.trim() : void 0;
|
|
1922
|
+
if (!companyId || !projectId) {
|
|
1923
|
+
return [];
|
|
1924
|
+
}
|
|
1925
|
+
const candidateMappings = mappings.filter((mapping) => mapping.repositoryUrl.trim());
|
|
1926
|
+
const exactMatches = candidateMappings.filter(
|
|
1927
|
+
(mapping) => mapping.paperclipProjectId === projectId && (!mapping.companyId || mapping.companyId === companyId)
|
|
1928
|
+
).map((mapping) => hydrateResolvedProjectMapping(mapping, {
|
|
1929
|
+
companyId,
|
|
1930
|
+
projectId
|
|
1931
|
+
}));
|
|
1932
|
+
if (exactMatches.length > 0) {
|
|
1933
|
+
return exactMatches;
|
|
1934
|
+
}
|
|
1935
|
+
const namedFallbackCandidates = candidateMappings.filter(
|
|
1936
|
+
(mapping) => !mapping.paperclipProjectId && mapping.companyId === companyId && Boolean(normalizeProjectNameForComparison(mapping.paperclipProjectName))
|
|
1937
|
+
);
|
|
1938
|
+
let projectName = "";
|
|
1939
|
+
let projectRepositoryUrl;
|
|
1940
|
+
try {
|
|
1941
|
+
const project = await ctx.projects.get(projectId, companyId);
|
|
1942
|
+
projectName = typeof project?.name === "string" ? project.name.trim() : "";
|
|
1943
|
+
projectRepositoryUrl = getProjectRepositoryUrlFromProjectRecord(project);
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
ctx.logger.warn("Unable to resolve Paperclip project metadata for GitHub project mapping fallback.", {
|
|
1946
|
+
companyId,
|
|
1947
|
+
projectId,
|
|
1948
|
+
error: getErrorMessage(error)
|
|
1949
|
+
});
|
|
1950
|
+
return [];
|
|
1951
|
+
}
|
|
1952
|
+
const normalizedProjectName = normalizeProjectNameForComparison(projectName);
|
|
1953
|
+
if (normalizedProjectName) {
|
|
1954
|
+
const namedFallbackMatches = namedFallbackCandidates.filter((mapping) => normalizeProjectNameForComparison(mapping.paperclipProjectName) === normalizedProjectName).map((mapping) => hydrateResolvedProjectMapping(mapping, {
|
|
1955
|
+
companyId,
|
|
1956
|
+
projectId,
|
|
1957
|
+
projectName
|
|
1958
|
+
}));
|
|
1959
|
+
if (namedFallbackMatches.length > 0) {
|
|
1960
|
+
return namedFallbackMatches;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
if (!projectRepositoryUrl) {
|
|
1964
|
+
return [];
|
|
1965
|
+
}
|
|
1966
|
+
return [{
|
|
1967
|
+
id: `project-repo:${companyId}:${projectId}`,
|
|
1968
|
+
repositoryUrl: projectRepositoryUrl,
|
|
1969
|
+
paperclipProjectName: projectName || "Project",
|
|
1970
|
+
paperclipProjectId: projectId,
|
|
1971
|
+
companyId
|
|
1972
|
+
}];
|
|
1973
|
+
}
|
|
1974
|
+
function doesGitHubIssueMatchTarget(issue, target) {
|
|
1975
|
+
if (!target || target.kind !== "issue") {
|
|
1976
|
+
return true;
|
|
1977
|
+
}
|
|
1978
|
+
const normalizedIssueUrl = normalizeGitHubIssueHtmlUrl(issue.htmlUrl) ?? issue.htmlUrl;
|
|
1979
|
+
return target.githubIssueId !== void 0 && issue.id === target.githubIssueId || target.githubIssueNumber !== void 0 && issue.number === target.githubIssueNumber || target.githubIssueUrl !== void 0 && normalizedIssueUrl === target.githubIssueUrl;
|
|
1980
|
+
}
|
|
1981
|
+
function doesImportedIssueMatchTarget(issue, target) {
|
|
1982
|
+
if (!target || target.kind !== "issue") {
|
|
1983
|
+
return true;
|
|
1984
|
+
}
|
|
1985
|
+
return target.issueId !== void 0 && issue.paperclipIssueId === target.issueId || target.githubIssueId !== void 0 && issue.githubIssueId === target.githubIssueId || target.githubIssueNumber !== void 0 && issue.githubIssueNumber === target.githubIssueNumber;
|
|
1986
|
+
}
|
|
1987
|
+
async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId) {
|
|
1988
|
+
const linkRecords = await listGitHubIssueLinkRecords(ctx, {
|
|
1989
|
+
paperclipIssueId: issueId
|
|
1990
|
+
});
|
|
1991
|
+
const entityMatch = linkRecords.find((record) => !record.data.companyId || record.data.companyId === companyId);
|
|
1992
|
+
if (entityMatch) {
|
|
1993
|
+
return {
|
|
1994
|
+
source: "entity",
|
|
1466
1995
|
companyId: entityMatch.data.companyId,
|
|
1467
1996
|
paperclipProjectId: entityMatch.data.paperclipProjectId,
|
|
1468
1997
|
repositoryUrl: entityMatch.data.repositoryUrl,
|
|
@@ -1624,7 +2153,7 @@ function extractGitHubLinksFromCommentBody(body) {
|
|
|
1624
2153
|
return [...links.values()];
|
|
1625
2154
|
}
|
|
1626
2155
|
async function buildToolbarSyncState(ctx, input) {
|
|
1627
|
-
const settings =
|
|
2156
|
+
const settings = await getActiveOrCurrentSyncState(ctx);
|
|
1628
2157
|
const config = await getResolvedConfig(ctx);
|
|
1629
2158
|
const githubTokenConfigured = hasConfiguredGithubToken(settings, config);
|
|
1630
2159
|
const companyId = typeof input.companyId === "string" && input.companyId.trim() ? input.companyId.trim() : void 0;
|
|
@@ -1872,6 +2401,93 @@ async function listAvailableAssignees(ctx, companyId) {
|
|
|
1872
2401
|
return [];
|
|
1873
2402
|
}
|
|
1874
2403
|
}
|
|
2404
|
+
async function resolvePaperclipIssueDrawerAgents(ctx, companyId, agentIds) {
|
|
2405
|
+
const normalizedAgentIds = [
|
|
2406
|
+
...new Set(
|
|
2407
|
+
agentIds.filter((agentId) => typeof agentId === "string" && agentId.trim().length > 0).map((agentId) => agentId.trim())
|
|
2408
|
+
)
|
|
2409
|
+
];
|
|
2410
|
+
const agentsById = /* @__PURE__ */ new Map();
|
|
2411
|
+
if (normalizedAgentIds.length === 0 || !ctx.agents || typeof ctx.agents.get !== "function") {
|
|
2412
|
+
return agentsById;
|
|
2413
|
+
}
|
|
2414
|
+
await Promise.all(
|
|
2415
|
+
normalizedAgentIds.map(async (agentId) => {
|
|
2416
|
+
try {
|
|
2417
|
+
const agent = await ctx.agents.get(agentId, companyId);
|
|
2418
|
+
if (!agent) {
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
agentsById.set(agent.id, {
|
|
2422
|
+
id: agent.id,
|
|
2423
|
+
name: agent.name,
|
|
2424
|
+
...agent.title?.trim() ? { title: agent.title.trim() } : {}
|
|
2425
|
+
});
|
|
2426
|
+
} catch (error) {
|
|
2427
|
+
ctx.logger.warn("Unable to load Paperclip agent for pull request issue drawer.", {
|
|
2428
|
+
companyId,
|
|
2429
|
+
agentId,
|
|
2430
|
+
error: getErrorMessage(error)
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
})
|
|
2434
|
+
);
|
|
2435
|
+
return agentsById;
|
|
2436
|
+
}
|
|
2437
|
+
async function buildProjectPullRequestPaperclipIssueData(ctx, input) {
|
|
2438
|
+
const issueId = typeof input.issueId === "string" && input.issueId.trim() ? input.issueId.trim() : void 0;
|
|
2439
|
+
const companyId = typeof input.companyId === "string" && input.companyId.trim() ? input.companyId.trim() : void 0;
|
|
2440
|
+
if (!issueId || !companyId) {
|
|
2441
|
+
return null;
|
|
2442
|
+
}
|
|
2443
|
+
const issue = await ctx.issues.get(issueId, companyId);
|
|
2444
|
+
if (!issue) {
|
|
2445
|
+
return null;
|
|
2446
|
+
}
|
|
2447
|
+
const comments = ctx.issues && typeof ctx.issues.listComments === "function" ? await ctx.issues.listComments(issue.id, companyId) : [];
|
|
2448
|
+
const agentsById = await resolvePaperclipIssueDrawerAgents(ctx, companyId, [
|
|
2449
|
+
issue.assigneeAgentId,
|
|
2450
|
+
issue.createdByAgentId,
|
|
2451
|
+
...comments.map((comment) => comment.authorAgentId)
|
|
2452
|
+
]);
|
|
2453
|
+
const assignee = issue.assigneeAgentId ? agentsById.get(issue.assigneeAgentId) ?? null : null;
|
|
2454
|
+
const orderedComments = [...comments].sort(
|
|
2455
|
+
(left, right) => coerceDate(left.createdAt).getTime() - coerceDate(right.createdAt).getTime()
|
|
2456
|
+
);
|
|
2457
|
+
return {
|
|
2458
|
+
issueId: issue.id,
|
|
2459
|
+
...issue.identifier?.trim() ? { issueIdentifier: issue.identifier.trim() } : {},
|
|
2460
|
+
title: issue.title,
|
|
2461
|
+
description: issue.description ?? "",
|
|
2462
|
+
status: issue.status,
|
|
2463
|
+
priority: issue.priority,
|
|
2464
|
+
projectName: issue.project?.name ?? void 0,
|
|
2465
|
+
createdAt: coerceDate(issue.createdAt).toISOString(),
|
|
2466
|
+
updatedAt: coerceDate(issue.updatedAt).toISOString(),
|
|
2467
|
+
labels: Array.isArray(issue.labels) ? issue.labels.map((label) => ({
|
|
2468
|
+
name: label.name,
|
|
2469
|
+
color: label.color
|
|
2470
|
+
})).filter((label) => label.name.trim().length > 0) : [],
|
|
2471
|
+
assignee: assignee ? {
|
|
2472
|
+
id: assignee.id,
|
|
2473
|
+
name: assignee.name,
|
|
2474
|
+
...assignee.title ? { title: assignee.title } : {}
|
|
2475
|
+
} : null,
|
|
2476
|
+
commentCount: orderedComments.length,
|
|
2477
|
+
comments: orderedComments.map((comment) => {
|
|
2478
|
+
const author = comment.authorAgentId ? agentsById.get(comment.authorAgentId) : null;
|
|
2479
|
+
return {
|
|
2480
|
+
id: comment.id,
|
|
2481
|
+
body: comment.body ?? "",
|
|
2482
|
+
createdAt: coerceDate(comment.createdAt).toISOString(),
|
|
2483
|
+
updatedAt: coerceDate(comment.updatedAt).toISOString(),
|
|
2484
|
+
authorLabel: author?.name ?? (comment.authorUserId ? "Team member" : "Paperclip"),
|
|
2485
|
+
authorKind: author ? "agent" : comment.authorUserId ? "user" : "system",
|
|
2486
|
+
...author?.title ? { authorTitle: author.title } : {}
|
|
2487
|
+
};
|
|
2488
|
+
})
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
1875
2491
|
function createSetupConfigurationErrorSyncState(issue, trigger) {
|
|
1876
2492
|
switch (issue) {
|
|
1877
2493
|
case "missing_token":
|
|
@@ -1955,7 +2571,11 @@ async function saveSettingsSyncState(ctx, settings, syncState) {
|
|
|
1955
2571
|
return next;
|
|
1956
2572
|
}
|
|
1957
2573
|
async function setSyncCancellationRequest(ctx, request) {
|
|
1958
|
-
|
|
2574
|
+
if (request) {
|
|
2575
|
+
await ctx.state.set(SYNC_CANCELLATION_SCOPE, request);
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
await ctx.state.delete(SYNC_CANCELLATION_SCOPE);
|
|
1959
2579
|
}
|
|
1960
2580
|
async function getSyncCancellationRequest(ctx) {
|
|
1961
2581
|
const activeRequestedAt = activeRunningSyncState?.syncState.cancelRequestedAt?.trim();
|
|
@@ -2007,8 +2627,8 @@ async function waitForSyncResultWithinGracePeriod(promise, timeoutMs) {
|
|
|
2007
2627
|
try {
|
|
2008
2628
|
return await Promise.race([
|
|
2009
2629
|
promise,
|
|
2010
|
-
new Promise((
|
|
2011
|
-
timer = globalThis.setTimeout(() =>
|
|
2630
|
+
new Promise((resolve2) => {
|
|
2631
|
+
timer = globalThis.setTimeout(() => resolve2(null), timeoutMs);
|
|
2012
2632
|
})
|
|
2013
2633
|
]);
|
|
2014
2634
|
} finally {
|
|
@@ -2018,11 +2638,14 @@ async function waitForSyncResultWithinGracePeriod(promise, timeoutMs) {
|
|
|
2018
2638
|
}
|
|
2019
2639
|
}
|
|
2020
2640
|
async function getActiveOrCurrentSyncState(ctx) {
|
|
2641
|
+
if (activeRunningSyncState?.syncState.status === "running") {
|
|
2642
|
+
return activeRunningSyncState;
|
|
2643
|
+
}
|
|
2021
2644
|
const current = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
2022
2645
|
if (current.syncState.status === "running") {
|
|
2023
2646
|
return current;
|
|
2024
2647
|
}
|
|
2025
|
-
return
|
|
2648
|
+
return current;
|
|
2026
2649
|
}
|
|
2027
2650
|
function updateSyncFailureContext(current, next) {
|
|
2028
2651
|
if ("phase" in next) {
|
|
@@ -2216,6 +2839,22 @@ function normalizeGitHubUsername(value) {
|
|
|
2216
2839
|
const trimmed = value.trim().replace(/^@+/, "");
|
|
2217
2840
|
return trimmed ? trimmed.toLowerCase() : void 0;
|
|
2218
2841
|
}
|
|
2842
|
+
function buildGitHubUsernameAliases(value) {
|
|
2843
|
+
const normalized = normalizeGitHubUsername(value);
|
|
2844
|
+
if (!normalized) {
|
|
2845
|
+
return [];
|
|
2846
|
+
}
|
|
2847
|
+
const aliases = /* @__PURE__ */ new Set([normalized]);
|
|
2848
|
+
if (normalized.endsWith("[bot]")) {
|
|
2849
|
+
const withoutBotSuffix = normalized.slice(0, -"[bot]".length);
|
|
2850
|
+
if (withoutBotSuffix) {
|
|
2851
|
+
aliases.add(withoutBotSuffix);
|
|
2852
|
+
}
|
|
2853
|
+
} else {
|
|
2854
|
+
aliases.add(`${normalized}[bot]`);
|
|
2855
|
+
}
|
|
2856
|
+
return [...aliases];
|
|
2857
|
+
}
|
|
2219
2858
|
function parseIgnoredIssueAuthorUsernames(value) {
|
|
2220
2859
|
return value.split(/[\s,]+/g).map((entry) => normalizeGitHubUsername(entry)).filter((entry) => Boolean(entry));
|
|
2221
2860
|
}
|
|
@@ -2535,6 +3174,286 @@ function getPageCursor(pageInfo) {
|
|
|
2535
3174
|
}
|
|
2536
3175
|
return pageInfo.endCursor;
|
|
2537
3176
|
}
|
|
3177
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
3178
|
+
if (items.length === 0) {
|
|
3179
|
+
return [];
|
|
3180
|
+
}
|
|
3181
|
+
const limit = Math.max(1, Math.floor(concurrency));
|
|
3182
|
+
const results = new Array(items.length);
|
|
3183
|
+
let nextIndex = 0;
|
|
3184
|
+
async function worker() {
|
|
3185
|
+
while (nextIndex < items.length) {
|
|
3186
|
+
const currentIndex = nextIndex;
|
|
3187
|
+
nextIndex += 1;
|
|
3188
|
+
results[currentIndex] = await mapper(items[currentIndex], currentIndex);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
await Promise.all(
|
|
3192
|
+
Array.from(
|
|
3193
|
+
{ length: Math.min(limit, items.length) },
|
|
3194
|
+
async () => worker()
|
|
3195
|
+
)
|
|
3196
|
+
);
|
|
3197
|
+
return results;
|
|
3198
|
+
}
|
|
3199
|
+
function getFreshCacheValue(cache, key, now = Date.now()) {
|
|
3200
|
+
const entry = cache.get(key);
|
|
3201
|
+
if (!entry) {
|
|
3202
|
+
return null;
|
|
3203
|
+
}
|
|
3204
|
+
if (entry.expiresAt <= now) {
|
|
3205
|
+
cache.delete(key);
|
|
3206
|
+
return null;
|
|
3207
|
+
}
|
|
3208
|
+
return entry.value;
|
|
3209
|
+
}
|
|
3210
|
+
function getFreshCacheEntry(cache, key, now = Date.now()) {
|
|
3211
|
+
const entry = cache.get(key);
|
|
3212
|
+
if (!entry) {
|
|
3213
|
+
return null;
|
|
3214
|
+
}
|
|
3215
|
+
if (entry.expiresAt <= now) {
|
|
3216
|
+
cache.delete(key);
|
|
3217
|
+
return null;
|
|
3218
|
+
}
|
|
3219
|
+
return entry;
|
|
3220
|
+
}
|
|
3221
|
+
function setCacheValue(cache, key, value, ttlMs, now = Date.now()) {
|
|
3222
|
+
cache.set(key, {
|
|
3223
|
+
expiresAt: now + ttlMs,
|
|
3224
|
+
value
|
|
3225
|
+
});
|
|
3226
|
+
return value;
|
|
3227
|
+
}
|
|
3228
|
+
function normalizeProjectPullRequestFilter(value) {
|
|
3229
|
+
switch (value) {
|
|
3230
|
+
case "mergeable":
|
|
3231
|
+
case "reviewable":
|
|
3232
|
+
case "failing":
|
|
3233
|
+
return value;
|
|
3234
|
+
default:
|
|
3235
|
+
return "all";
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
function normalizeProjectPullRequestPageIndex(value) {
|
|
3239
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
3240
|
+
return Math.floor(value);
|
|
3241
|
+
}
|
|
3242
|
+
if (typeof value === "string" && value.trim()) {
|
|
3243
|
+
const parsed = Number(value.trim());
|
|
3244
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
3245
|
+
return Math.floor(parsed);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
return 0;
|
|
3249
|
+
}
|
|
3250
|
+
function isCopilotActorLogin(value) {
|
|
3251
|
+
const normalizedLogin = normalizeGitHubUsername(value);
|
|
3252
|
+
return Boolean(normalizedLogin && normalizedLogin.includes("copilot"));
|
|
3253
|
+
}
|
|
3254
|
+
function getProjectPullRequestReviewThreadStarterLogin(node) {
|
|
3255
|
+
return normalizeGitHubUsername(
|
|
3256
|
+
node?.comments?.nodes?.find((comment) => comment?.author?.login)?.author?.login
|
|
3257
|
+
);
|
|
3258
|
+
}
|
|
3259
|
+
function getDetailedPullRequestReviewThreadStarterLogin(thread) {
|
|
3260
|
+
const rootComment = thread.comments.find((comment) => !comment.replyToId) ?? thread.comments[0];
|
|
3261
|
+
return normalizeGitHubUsername(rootComment?.authorLogin);
|
|
3262
|
+
}
|
|
3263
|
+
function summarizeProjectPullRequestReviewThreadsFromConnection(connection) {
|
|
3264
|
+
let unresolvedReviewThreads = 0;
|
|
3265
|
+
let copilotUnresolvedReviewThreads = 0;
|
|
3266
|
+
for (const thread of connection?.nodes ?? []) {
|
|
3267
|
+
if (thread?.isResolved !== false) {
|
|
3268
|
+
continue;
|
|
3269
|
+
}
|
|
3270
|
+
unresolvedReviewThreads += 1;
|
|
3271
|
+
if (isCopilotActorLogin(getProjectPullRequestReviewThreadStarterLogin(thread))) {
|
|
3272
|
+
copilotUnresolvedReviewThreads += 1;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
return {
|
|
3276
|
+
unresolvedReviewThreads,
|
|
3277
|
+
copilotUnresolvedReviewThreads
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
function summarizeDetailedPullRequestReviewThreads(threads) {
|
|
3281
|
+
let unresolvedReviewThreads = 0;
|
|
3282
|
+
let copilotUnresolvedReviewThreads = 0;
|
|
3283
|
+
for (const thread of threads) {
|
|
3284
|
+
if (thread.isResolved) {
|
|
3285
|
+
continue;
|
|
3286
|
+
}
|
|
3287
|
+
unresolvedReviewThreads += 1;
|
|
3288
|
+
if (isCopilotActorLogin(getDetailedPullRequestReviewThreadStarterLogin(thread))) {
|
|
3289
|
+
copilotUnresolvedReviewThreads += 1;
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
return {
|
|
3293
|
+
unresolvedReviewThreads,
|
|
3294
|
+
copilotUnresolvedReviewThreads
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
function normalizeProjectPullRequestClosingIssues(repository, nodes) {
|
|
3298
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3299
|
+
const issues = [];
|
|
3300
|
+
for (const node of nodes ?? []) {
|
|
3301
|
+
const issueNumber = typeof node?.number === "number" && node.number > 0 ? Math.floor(node.number) : void 0;
|
|
3302
|
+
const issueUrl = typeof node?.url === "string" && node.url.trim() ? normalizeGitHubIssueHtmlUrl(node.url) : issueNumber !== void 0 ? buildGitHubIssueUrlFromRepository(repository.url, issueNumber) : void 0;
|
|
3303
|
+
if (issueNumber === void 0 || !issueUrl || seen.has(issueUrl)) {
|
|
3304
|
+
continue;
|
|
3305
|
+
}
|
|
3306
|
+
seen.add(issueUrl);
|
|
3307
|
+
issues.push({
|
|
3308
|
+
number: issueNumber,
|
|
3309
|
+
url: issueUrl
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
return issues;
|
|
3313
|
+
}
|
|
3314
|
+
function resolveProjectPullRequestReviewable(record) {
|
|
3315
|
+
return record.githubMergeable === true && record.checksStatus === "passed" && typeof record.copilotUnresolvedReviewThreads === "number" && record.copilotUnresolvedReviewThreads === 0;
|
|
3316
|
+
}
|
|
3317
|
+
function resolveProjectPullRequestMergeable(record) {
|
|
3318
|
+
return record.githubMergeable === true && record.checksStatus === "passed" && typeof record.reviewApprovals === "number" && record.reviewApprovals > 0 && typeof record.unresolvedReviewThreads === "number" && record.unresolvedReviewThreads === 0;
|
|
3319
|
+
}
|
|
3320
|
+
function resolveProjectPullRequestUpToDateStatus(record) {
|
|
3321
|
+
const mergeStateStatus = typeof record.mergeStateStatus === "string" ? record.mergeStateStatus : null;
|
|
3322
|
+
if (mergeStateStatus === "DIRTY" || record.mergeable === "CONFLICTING") {
|
|
3323
|
+
return "conflicts";
|
|
3324
|
+
}
|
|
3325
|
+
if (typeof record.behindBy === "number" && Number.isFinite(record.behindBy) && record.behindBy > 0) {
|
|
3326
|
+
return "can_update";
|
|
3327
|
+
}
|
|
3328
|
+
if (typeof record.behindBy === "number" && Number.isFinite(record.behindBy) && record.behindBy === 0) {
|
|
3329
|
+
return "up_to_date";
|
|
3330
|
+
}
|
|
3331
|
+
if (mergeStateStatus === "BEHIND") {
|
|
3332
|
+
return "can_update";
|
|
3333
|
+
}
|
|
3334
|
+
return "unknown";
|
|
3335
|
+
}
|
|
3336
|
+
function normalizeProjectPullRequestCopilotAction(value) {
|
|
3337
|
+
switch (typeof value === "string" ? value.trim().toLowerCase() : "") {
|
|
3338
|
+
case "fix_ci":
|
|
3339
|
+
return "fix_ci";
|
|
3340
|
+
case "rebase":
|
|
3341
|
+
return "rebase";
|
|
3342
|
+
case "address_review_feedback":
|
|
3343
|
+
return "address_review_feedback";
|
|
3344
|
+
case "review":
|
|
3345
|
+
return "review";
|
|
3346
|
+
default:
|
|
3347
|
+
return null;
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
function getProjectPullRequestCopilotActionLabel(action) {
|
|
3351
|
+
switch (action) {
|
|
3352
|
+
case "fix_ci":
|
|
3353
|
+
return "Fix CI";
|
|
3354
|
+
case "rebase":
|
|
3355
|
+
return "Rebase";
|
|
3356
|
+
case "address_review_feedback":
|
|
3357
|
+
return "Address review feedback";
|
|
3358
|
+
case "review":
|
|
3359
|
+
return "Review";
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
function getProjectPullRequestNumber(record) {
|
|
3363
|
+
const number = record.number;
|
|
3364
|
+
if (typeof number !== "number" || !Number.isFinite(number) || number <= 0) {
|
|
3365
|
+
return null;
|
|
3366
|
+
}
|
|
3367
|
+
return Math.floor(number);
|
|
3368
|
+
}
|
|
3369
|
+
function getProjectPullRequestUpdatedAtTimestamp(record) {
|
|
3370
|
+
const updatedAt = typeof record.updatedAt === "string" ? Date.parse(record.updatedAt) : Number.NaN;
|
|
3371
|
+
return Number.isFinite(updatedAt) ? updatedAt : 0;
|
|
3372
|
+
}
|
|
3373
|
+
function sortProjectPullRequestRecordsByUpdatedAt(records) {
|
|
3374
|
+
return [...records].sort((left, right) => {
|
|
3375
|
+
const timestampDelta = getProjectPullRequestUpdatedAtTimestamp(right) - getProjectPullRequestUpdatedAtTimestamp(left);
|
|
3376
|
+
if (timestampDelta !== 0) {
|
|
3377
|
+
return timestampDelta;
|
|
3378
|
+
}
|
|
3379
|
+
return (getProjectPullRequestNumber(right) ?? 0) - (getProjectPullRequestNumber(left) ?? 0);
|
|
3380
|
+
});
|
|
3381
|
+
}
|
|
3382
|
+
function getLinkedPaperclipIssueFromProjectPullRequestRecord(record) {
|
|
3383
|
+
const paperclipIssueId = typeof record.paperclipIssueId === "string" && record.paperclipIssueId.trim() ? record.paperclipIssueId.trim() : void 0;
|
|
3384
|
+
if (!paperclipIssueId) {
|
|
3385
|
+
return void 0;
|
|
3386
|
+
}
|
|
3387
|
+
return {
|
|
3388
|
+
paperclipIssueId,
|
|
3389
|
+
...typeof record.paperclipIssueKey === "string" && record.paperclipIssueKey.trim() ? { paperclipIssueKey: record.paperclipIssueKey.trim() } : {}
|
|
3390
|
+
};
|
|
3391
|
+
}
|
|
3392
|
+
function buildProjectPullRequestMetrics(pullRequests, totalOpenPullRequests, defaultBranchName) {
|
|
3393
|
+
const mergeablePullRequestNumbers = [];
|
|
3394
|
+
const reviewablePullRequestNumbers = [];
|
|
3395
|
+
const failingPullRequestNumbers = [];
|
|
3396
|
+
for (const pullRequest of pullRequests) {
|
|
3397
|
+
const number = getProjectPullRequestNumber(pullRequest);
|
|
3398
|
+
if (number === null) {
|
|
3399
|
+
continue;
|
|
3400
|
+
}
|
|
3401
|
+
if (pullRequest.mergeable === true) {
|
|
3402
|
+
mergeablePullRequestNumbers.push(number);
|
|
3403
|
+
}
|
|
3404
|
+
if (pullRequest.reviewable === true) {
|
|
3405
|
+
reviewablePullRequestNumbers.push(number);
|
|
3406
|
+
}
|
|
3407
|
+
if (pullRequest.checksStatus === "failed") {
|
|
3408
|
+
failingPullRequestNumbers.push(number);
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
return {
|
|
3412
|
+
totalOpenPullRequests,
|
|
3413
|
+
...defaultBranchName ? { defaultBranchName } : {},
|
|
3414
|
+
mergeablePullRequests: mergeablePullRequestNumbers.length,
|
|
3415
|
+
reviewablePullRequests: reviewablePullRequestNumbers.length,
|
|
3416
|
+
failingPullRequests: failingPullRequestNumbers.length,
|
|
3417
|
+
mergeablePullRequestNumbers,
|
|
3418
|
+
reviewablePullRequestNumbers,
|
|
3419
|
+
failingPullRequestNumbers
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
function getPublicProjectPullRequestMetrics(metrics) {
|
|
3423
|
+
return {
|
|
3424
|
+
totalOpenPullRequests: metrics.totalOpenPullRequests,
|
|
3425
|
+
...metrics.defaultBranchName ? { defaultBranchName: metrics.defaultBranchName } : {},
|
|
3426
|
+
mergeablePullRequests: metrics.mergeablePullRequests,
|
|
3427
|
+
reviewablePullRequests: metrics.reviewablePullRequests,
|
|
3428
|
+
failingPullRequests: metrics.failingPullRequests
|
|
3429
|
+
};
|
|
3430
|
+
}
|
|
3431
|
+
function sliceProjectPullRequestRecords(pullRequests, pageIndex, pageSize = PROJECT_PULL_REQUEST_PAGE_SIZE) {
|
|
3432
|
+
const effectivePageSize = Math.max(1, Math.floor(pageSize));
|
|
3433
|
+
const maxPageIndex = Math.max(0, Math.ceil(pullRequests.length / effectivePageSize) - 1);
|
|
3434
|
+
const normalizedPageIndex = Math.min(Math.max(0, Math.floor(pageIndex)), maxPageIndex);
|
|
3435
|
+
const start = normalizedPageIndex * effectivePageSize;
|
|
3436
|
+
const end = start + effectivePageSize;
|
|
3437
|
+
return {
|
|
3438
|
+
pullRequests: pullRequests.slice(start, end),
|
|
3439
|
+
pageIndex: normalizedPageIndex,
|
|
3440
|
+
hasNextPage: end < pullRequests.length,
|
|
3441
|
+
hasPreviousPage: normalizedPageIndex > 0
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
function sliceProjectPullRequestNumbers(pullRequestNumbers, pageIndex, pageSize = PROJECT_PULL_REQUEST_PAGE_SIZE) {
|
|
3445
|
+
const effectivePageSize = Math.max(1, Math.floor(pageSize));
|
|
3446
|
+
const maxPageIndex = Math.max(0, Math.ceil(pullRequestNumbers.length / effectivePageSize) - 1);
|
|
3447
|
+
const normalizedPageIndex = Math.min(Math.max(0, Math.floor(pageIndex)), maxPageIndex);
|
|
3448
|
+
const start = normalizedPageIndex * effectivePageSize;
|
|
3449
|
+
const end = start + effectivePageSize;
|
|
3450
|
+
return {
|
|
3451
|
+
pullRequestNumbers: pullRequestNumbers.slice(start, end),
|
|
3452
|
+
pageIndex: normalizedPageIndex,
|
|
3453
|
+
hasNextPage: end < pullRequestNumbers.length,
|
|
3454
|
+
hasPreviousPage: normalizedPageIndex > 0
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
2538
3457
|
function classifyGitHubPullRequestCiState(contexts) {
|
|
2539
3458
|
if (contexts.length === 0) {
|
|
2540
3459
|
return "unfinished";
|
|
@@ -2814,17 +3733,24 @@ function tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node) {
|
|
|
2814
3733
|
if (typeof node.number !== "number") {
|
|
2815
3734
|
return null;
|
|
2816
3735
|
}
|
|
2817
|
-
const
|
|
2818
|
-
const
|
|
2819
|
-
if (
|
|
3736
|
+
const reviewThreadSummary = node.reviewThreads?.pageInfo?.hasNextPage ? null : summarizeProjectPullRequestReviewThreadsFromConnection(node.reviewThreads);
|
|
3737
|
+
const ciState = tryBuildGitHubPullRequestCiStateFromBatchNode(node);
|
|
3738
|
+
if (!reviewThreadSummary || !ciState) {
|
|
2820
3739
|
return null;
|
|
2821
3740
|
}
|
|
2822
3741
|
return {
|
|
2823
3742
|
number: node.number,
|
|
2824
|
-
hasUnresolvedReviewThreads:
|
|
2825
|
-
ciState
|
|
3743
|
+
hasUnresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads > 0,
|
|
3744
|
+
ciState
|
|
2826
3745
|
};
|
|
2827
3746
|
}
|
|
3747
|
+
function tryBuildGitHubPullRequestCiStateFromBatchNode(node) {
|
|
3748
|
+
const ciContexts = node.statusCheckRollup?.contexts;
|
|
3749
|
+
if (ciContexts?.pageInfo?.hasNextPage) {
|
|
3750
|
+
return null;
|
|
3751
|
+
}
|
|
3752
|
+
return classifyGitHubPullRequestCiState(extractGitHubCiContextRecords(ciContexts?.nodes ?? []));
|
|
3753
|
+
}
|
|
2828
3754
|
async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullRequestNumbers, pullRequestStatusCache) {
|
|
2829
3755
|
if (targetPullRequestNumbers.size === 0) {
|
|
2830
3756
|
return;
|
|
@@ -2854,6 +3780,7 @@ async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullR
|
|
|
2854
3780
|
const snapshot = tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node);
|
|
2855
3781
|
if (snapshot) {
|
|
2856
3782
|
pullRequestStatusCache.set(node.number, snapshot);
|
|
3783
|
+
cacheGitHubPullRequestStatusSnapshot(repository, snapshot);
|
|
2857
3784
|
}
|
|
2858
3785
|
}
|
|
2859
3786
|
if (remainingNumbers.size === 0) {
|
|
@@ -2862,27 +3789,6 @@ async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullR
|
|
|
2862
3789
|
after = getPageCursor(pullRequests?.pageInfo);
|
|
2863
3790
|
} while (after);
|
|
2864
3791
|
}
|
|
2865
|
-
async function hasGitHubPullRequestUnresolvedReviewThreads(octokit, repository, pullRequestNumber) {
|
|
2866
|
-
let after;
|
|
2867
|
-
do {
|
|
2868
|
-
const response = await octokit.graphql(
|
|
2869
|
-
GITHUB_PULL_REQUEST_REVIEW_THREADS_QUERY,
|
|
2870
|
-
{
|
|
2871
|
-
owner: repository.owner,
|
|
2872
|
-
repo: repository.repo,
|
|
2873
|
-
pullRequestNumber,
|
|
2874
|
-
after
|
|
2875
|
-
}
|
|
2876
|
-
);
|
|
2877
|
-
const reviewThreads = response.repository?.pullRequest?.reviewThreads;
|
|
2878
|
-
const nodes = reviewThreads?.nodes ?? [];
|
|
2879
|
-
if (nodes.some((node) => node?.isResolved === false)) {
|
|
2880
|
-
return true;
|
|
2881
|
-
}
|
|
2882
|
-
after = getPageCursor(reviewThreads?.pageInfo);
|
|
2883
|
-
} while (after);
|
|
2884
|
-
return false;
|
|
2885
|
-
}
|
|
2886
3792
|
async function getGitHubPullRequestCiState(octokit, repository, pullRequestNumber) {
|
|
2887
3793
|
const contexts = [];
|
|
2888
3794
|
let after;
|
|
@@ -2918,22 +3824,53 @@ async function getGitHubPullRequestCiState(octokit, repository, pullRequestNumbe
|
|
|
2918
3824
|
} while (after);
|
|
2919
3825
|
return classifyGitHubPullRequestCiState(contexts);
|
|
2920
3826
|
}
|
|
2921
|
-
async function getGitHubPullRequestStatusSnapshot(octokit, repository, pullRequestNumber, pullRequestStatusCache) {
|
|
3827
|
+
async function getGitHubPullRequestStatusSnapshot(octokit, repository, pullRequestNumber, pullRequestStatusCache, options) {
|
|
2922
3828
|
const cached = pullRequestStatusCache.get(pullRequestNumber);
|
|
2923
3829
|
if (cached) {
|
|
2924
3830
|
return cached;
|
|
2925
3831
|
}
|
|
2926
|
-
const
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
3832
|
+
const cacheKey = buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "status");
|
|
3833
|
+
const cachedSnapshot = getFreshCacheValue(activeGitHubPullRequestStatusSnapshotCache, cacheKey);
|
|
3834
|
+
if (cachedSnapshot) {
|
|
3835
|
+
pullRequestStatusCache.set(pullRequestNumber, cachedSnapshot);
|
|
3836
|
+
return cachedSnapshot;
|
|
3837
|
+
}
|
|
3838
|
+
if (options?.reviewThreadSummary && options.ciState) {
|
|
3839
|
+
const snapshot = cacheGitHubPullRequestStatusSnapshot(repository, {
|
|
3840
|
+
number: pullRequestNumber,
|
|
3841
|
+
hasUnresolvedReviewThreads: options.reviewThreadSummary.unresolvedReviewThreads > 0,
|
|
3842
|
+
ciState: options.ciState
|
|
3843
|
+
});
|
|
3844
|
+
pullRequestStatusCache.set(pullRequestNumber, snapshot);
|
|
3845
|
+
return snapshot;
|
|
3846
|
+
}
|
|
3847
|
+
const inFlightSnapshot = activeGitHubPullRequestStatusSnapshotPromiseCache.get(cacheKey);
|
|
3848
|
+
if (inFlightSnapshot) {
|
|
3849
|
+
const snapshot = await inFlightSnapshot;
|
|
3850
|
+
pullRequestStatusCache.set(pullRequestNumber, snapshot);
|
|
3851
|
+
return snapshot;
|
|
3852
|
+
}
|
|
3853
|
+
const loadSnapshotPromise = (async () => {
|
|
3854
|
+
const [reviewThreadSummary, ciState] = await Promise.all([
|
|
3855
|
+
options?.reviewThreadSummary ?? getOrLoadCachedGitHubPullRequestReviewThreadSummary(octokit, repository, pullRequestNumber),
|
|
3856
|
+
options?.ciState ?? getGitHubPullRequestCiState(octokit, repository, pullRequestNumber)
|
|
3857
|
+
]);
|
|
3858
|
+
return cacheGitHubPullRequestStatusSnapshot(repository, {
|
|
3859
|
+
number: pullRequestNumber,
|
|
3860
|
+
hasUnresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads > 0,
|
|
3861
|
+
ciState
|
|
3862
|
+
});
|
|
3863
|
+
})();
|
|
3864
|
+
activeGitHubPullRequestStatusSnapshotPromiseCache.set(cacheKey, loadSnapshotPromise);
|
|
3865
|
+
try {
|
|
3866
|
+
const snapshot = await loadSnapshotPromise;
|
|
3867
|
+
pullRequestStatusCache.set(pullRequestNumber, snapshot);
|
|
3868
|
+
return snapshot;
|
|
3869
|
+
} finally {
|
|
3870
|
+
if (activeGitHubPullRequestStatusSnapshotPromiseCache.get(cacheKey) === loadSnapshotPromise) {
|
|
3871
|
+
activeGitHubPullRequestStatusSnapshotPromiseCache.delete(cacheKey);
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
2937
3874
|
}
|
|
2938
3875
|
async function getGitHubIssueStatusSnapshot(octokit, repository, issueNumber, githubIssue, linkedPullRequestsByIssueNumber, issueStatusSnapshotCache, pullRequestStatusCache) {
|
|
2939
3876
|
if (issueStatusSnapshotCache.has(issueNumber)) {
|
|
@@ -3340,6 +4277,33 @@ function normalizeGitHubIssueLinkEntityData(value) {
|
|
|
3340
4277
|
syncedAt
|
|
3341
4278
|
};
|
|
3342
4279
|
}
|
|
4280
|
+
function normalizeGitHubPullRequestLinkEntityData(value) {
|
|
4281
|
+
if (!value || typeof value !== "object") {
|
|
4282
|
+
return null;
|
|
4283
|
+
}
|
|
4284
|
+
const record = value;
|
|
4285
|
+
const repositoryUrl = typeof record.repositoryUrl === "string" && record.repositoryUrl.trim() ? getNormalizedMappingRepositoryUrl({
|
|
4286
|
+
repositoryUrl: record.repositoryUrl
|
|
4287
|
+
}) : void 0;
|
|
4288
|
+
const githubPullRequestNumber = typeof record.githubPullRequestNumber === "number" && record.githubPullRequestNumber > 0 ? Math.floor(record.githubPullRequestNumber) : void 0;
|
|
4289
|
+
const githubPullRequestUrl = typeof record.githubPullRequestUrl === "string" && record.githubPullRequestUrl.trim() ? record.githubPullRequestUrl.trim() : void 0;
|
|
4290
|
+
const githubPullRequestState = record.githubPullRequestState === "closed" ? "closed" : record.githubPullRequestState === "open" ? "open" : void 0;
|
|
4291
|
+
const title = typeof record.title === "string" && record.title.trim() ? record.title.trim() : void 0;
|
|
4292
|
+
const syncedAt = typeof record.syncedAt === "string" && record.syncedAt.trim() ? record.syncedAt.trim() : void 0;
|
|
4293
|
+
if (!repositoryUrl || githubPullRequestNumber === void 0 || !githubPullRequestUrl || !githubPullRequestState || !syncedAt) {
|
|
4294
|
+
return null;
|
|
4295
|
+
}
|
|
4296
|
+
return {
|
|
4297
|
+
...typeof record.companyId === "string" && record.companyId.trim() ? { companyId: record.companyId.trim() } : {},
|
|
4298
|
+
...typeof record.paperclipProjectId === "string" && record.paperclipProjectId.trim() ? { paperclipProjectId: record.paperclipProjectId.trim() } : {},
|
|
4299
|
+
repositoryUrl,
|
|
4300
|
+
githubPullRequestNumber,
|
|
4301
|
+
githubPullRequestUrl,
|
|
4302
|
+
githubPullRequestState,
|
|
4303
|
+
...title ? { title } : {},
|
|
4304
|
+
syncedAt
|
|
4305
|
+
};
|
|
4306
|
+
}
|
|
3343
4307
|
function normalizeStoredStatusTransitionCommentAnnotation(value) {
|
|
3344
4308
|
if (!value || typeof value !== "object") {
|
|
3345
4309
|
return null;
|
|
@@ -3414,34 +4378,80 @@ async function listGitHubIssueLinkRecords(ctx, query = {}) {
|
|
|
3414
4378
|
}
|
|
3415
4379
|
return records;
|
|
3416
4380
|
}
|
|
3417
|
-
async function
|
|
3418
|
-
const
|
|
3419
|
-
const
|
|
4381
|
+
async function listGitHubPullRequestLinkRecords(ctx, query = {}) {
|
|
4382
|
+
const records = [];
|
|
4383
|
+
const requestedIssueId = query.paperclipIssueId?.trim() || void 0;
|
|
4384
|
+
const requestedExternalId = query.externalId?.trim() || void 0;
|
|
3420
4385
|
for (let offset = 0; ; ) {
|
|
3421
4386
|
const page = await ctx.entities.list({
|
|
3422
|
-
entityType:
|
|
4387
|
+
entityType: PULL_REQUEST_LINK_ENTITY_TYPE,
|
|
3423
4388
|
scopeKind: "issue",
|
|
3424
|
-
scopeId:
|
|
3425
|
-
externalId:
|
|
4389
|
+
...requestedIssueId ? { scopeId: requestedIssueId } : {},
|
|
4390
|
+
...requestedExternalId ? { externalId: requestedExternalId } : {},
|
|
3426
4391
|
limit: PAPERCLIP_LABEL_PAGE_SIZE,
|
|
3427
4392
|
offset
|
|
3428
4393
|
});
|
|
3429
4394
|
if (page.length === 0) {
|
|
3430
4395
|
break;
|
|
3431
4396
|
}
|
|
3432
|
-
const
|
|
3433
|
-
if (entry.scopeKind !== "issue" || entry.scopeId
|
|
3434
|
-
|
|
4397
|
+
for (const entry of page) {
|
|
4398
|
+
if (entry.scopeKind !== "issue" || !entry.scopeId) {
|
|
4399
|
+
continue;
|
|
3435
4400
|
}
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
4401
|
+
if (requestedIssueId && entry.scopeId !== requestedIssueId) {
|
|
4402
|
+
continue;
|
|
4403
|
+
}
|
|
4404
|
+
if (requestedExternalId && entry.externalId !== requestedExternalId) {
|
|
4405
|
+
continue;
|
|
4406
|
+
}
|
|
4407
|
+
const data = normalizeGitHubPullRequestLinkEntityData(entry.data);
|
|
4408
|
+
if (!data) {
|
|
4409
|
+
continue;
|
|
4410
|
+
}
|
|
4411
|
+
records.push({
|
|
4412
|
+
paperclipIssueId: entry.scopeId,
|
|
4413
|
+
...typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {},
|
|
4414
|
+
...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {},
|
|
4415
|
+
...typeof entry.title === "string" && entry.title.trim() ? { title: entry.title.trim() } : {},
|
|
4416
|
+
...typeof entry.status === "string" && entry.status.trim() ? { status: entry.status.trim() } : {},
|
|
4417
|
+
data
|
|
4418
|
+
});
|
|
4419
|
+
}
|
|
4420
|
+
if (page.length < PAPERCLIP_LABEL_PAGE_SIZE || requestedIssueId && records.length > 0 || requestedExternalId && page.length < PAPERCLIP_LABEL_PAGE_SIZE) {
|
|
4421
|
+
break;
|
|
4422
|
+
}
|
|
4423
|
+
offset += page.length;
|
|
4424
|
+
}
|
|
4425
|
+
return records;
|
|
4426
|
+
}
|
|
4427
|
+
async function findStoredStatusTransitionCommentAnnotation(ctx, params) {
|
|
4428
|
+
const issueId = params.issueId.trim();
|
|
4429
|
+
const commentId = params.commentId.trim();
|
|
4430
|
+
for (let offset = 0; ; ) {
|
|
4431
|
+
const page = await ctx.entities.list({
|
|
4432
|
+
entityType: COMMENT_ANNOTATION_ENTITY_TYPE,
|
|
4433
|
+
scopeKind: "issue",
|
|
4434
|
+
scopeId: issueId,
|
|
4435
|
+
externalId: commentId,
|
|
4436
|
+
limit: PAPERCLIP_LABEL_PAGE_SIZE,
|
|
4437
|
+
offset
|
|
4438
|
+
});
|
|
4439
|
+
if (page.length === 0) {
|
|
4440
|
+
break;
|
|
4441
|
+
}
|
|
4442
|
+
const match = page.find((entry) => {
|
|
4443
|
+
if (entry.scopeKind !== "issue" || entry.scopeId !== issueId) {
|
|
4444
|
+
return false;
|
|
4445
|
+
}
|
|
4446
|
+
const externalId = "externalId" in entry && typeof entry.externalId === "string" ? entry.externalId : void 0;
|
|
4447
|
+
return externalId === commentId;
|
|
4448
|
+
});
|
|
4449
|
+
const annotation = match ? normalizeStoredStatusTransitionCommentAnnotation(match.data) : null;
|
|
4450
|
+
if (annotation) {
|
|
4451
|
+
return annotation;
|
|
4452
|
+
}
|
|
4453
|
+
if (page.length < PAPERCLIP_LABEL_PAGE_SIZE) {
|
|
4454
|
+
break;
|
|
3445
4455
|
}
|
|
3446
4456
|
offset += page.length;
|
|
3447
4457
|
}
|
|
@@ -3472,6 +4482,28 @@ async function upsertGitHubIssueLinkRecord(ctx, mapping, issueId, githubIssue, l
|
|
|
3472
4482
|
}
|
|
3473
4483
|
});
|
|
3474
4484
|
}
|
|
4485
|
+
async function upsertGitHubPullRequestLinkRecord(ctx, params) {
|
|
4486
|
+
await ctx.entities.upsert({
|
|
4487
|
+
entityType: PULL_REQUEST_LINK_ENTITY_TYPE,
|
|
4488
|
+
scopeKind: "issue",
|
|
4489
|
+
scopeId: params.issueId,
|
|
4490
|
+
externalId: params.pullRequestUrl,
|
|
4491
|
+
title: `GitHub pull request #${params.pullRequestNumber}`,
|
|
4492
|
+
status: params.pullRequestState,
|
|
4493
|
+
data: {
|
|
4494
|
+
companyId: params.companyId,
|
|
4495
|
+
paperclipProjectId: params.projectId,
|
|
4496
|
+
repositoryUrl: getNormalizedMappingRepositoryUrl({
|
|
4497
|
+
repositoryUrl: params.repositoryUrl
|
|
4498
|
+
}),
|
|
4499
|
+
githubPullRequestNumber: params.pullRequestNumber,
|
|
4500
|
+
githubPullRequestUrl: params.pullRequestUrl,
|
|
4501
|
+
githubPullRequestState: params.pullRequestState,
|
|
4502
|
+
title: params.pullRequestTitle,
|
|
4503
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4504
|
+
}
|
|
4505
|
+
});
|
|
4506
|
+
}
|
|
3475
4507
|
async function upsertStatusTransitionCommentAnnotation(ctx, params) {
|
|
3476
4508
|
const { issueId, commentId, annotation } = params;
|
|
3477
4509
|
await ctx.entities.upsert({
|
|
@@ -4412,7 +5444,13 @@ async function listRepositoryIssuesForImport(allIssues) {
|
|
|
4412
5444
|
return sortIssuesForImport(allIssues.filter((issue) => issue.state === "open"));
|
|
4413
5445
|
}
|
|
4414
5446
|
function shouldIgnoreGitHubIssue(issue, advancedSettings) {
|
|
4415
|
-
|
|
5447
|
+
if (!issue.authorLogin || advancedSettings.ignoredIssueAuthorUsernames.length === 0) {
|
|
5448
|
+
return false;
|
|
5449
|
+
}
|
|
5450
|
+
const issueAuthorAliases = new Set(buildGitHubUsernameAliases(issue.authorLogin));
|
|
5451
|
+
return advancedSettings.ignoredIssueAuthorUsernames.some(
|
|
5452
|
+
(ignoredUsername) => buildGitHubUsernameAliases(ignoredUsername).some((alias) => issueAuthorAliases.has(alias))
|
|
5453
|
+
);
|
|
4416
5454
|
}
|
|
4417
5455
|
async function applyDefaultAssigneeToPaperclipIssue(ctx, params) {
|
|
4418
5456
|
const { companyId, issueId, defaultAssigneeAgentId } = params;
|
|
@@ -4967,188 +6005,2493 @@ function sanitizeRepositoryScopedSearchQuery(query) {
|
|
|
4967
6005
|
}
|
|
4968
6006
|
function buildToolSuccessResult(content, data) {
|
|
4969
6007
|
return {
|
|
4970
|
-
content,
|
|
4971
|
-
data
|
|
6008
|
+
content,
|
|
6009
|
+
data
|
|
6010
|
+
};
|
|
6011
|
+
}
|
|
6012
|
+
function buildToolErrorResult(error) {
|
|
6013
|
+
const rateLimitPause = getGitHubRateLimitPauseDetails(error);
|
|
6014
|
+
if (rateLimitPause) {
|
|
6015
|
+
const resourceLabel = formatGitHubRateLimitResource(rateLimitPause.resource) ?? "GitHub API";
|
|
6016
|
+
return {
|
|
6017
|
+
error: `${resourceLabel} rate limit reached. Wait until ${formatUtcTimestamp(rateLimitPause.resetAt)} before retrying.`
|
|
6018
|
+
};
|
|
6019
|
+
}
|
|
6020
|
+
return {
|
|
6021
|
+
error: getErrorMessage(error)
|
|
6022
|
+
};
|
|
6023
|
+
}
|
|
6024
|
+
async function executeGitHubTool(fn) {
|
|
6025
|
+
try {
|
|
6026
|
+
return await fn();
|
|
6027
|
+
} catch (error) {
|
|
6028
|
+
return buildToolErrorResult(error);
|
|
6029
|
+
}
|
|
6030
|
+
}
|
|
6031
|
+
async function createGitHubToolOctokit(ctx) {
|
|
6032
|
+
const token = (await resolveGithubToken(ctx)).trim();
|
|
6033
|
+
if (!token) {
|
|
6034
|
+
throw new Error(MISSING_GITHUB_TOKEN_SYNC_MESSAGE);
|
|
6035
|
+
}
|
|
6036
|
+
return new Octokit({ auth: token });
|
|
6037
|
+
}
|
|
6038
|
+
async function listGitHubRepositoryOpenPullRequestNumbers(octokit, repository) {
|
|
6039
|
+
const response = await octokit.rest.pulls.list({
|
|
6040
|
+
owner: repository.owner,
|
|
6041
|
+
repo: repository.repo,
|
|
6042
|
+
state: "open",
|
|
6043
|
+
per_page: 1,
|
|
6044
|
+
headers: {
|
|
6045
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6046
|
+
}
|
|
6047
|
+
});
|
|
6048
|
+
return (response.data ?? []).map((pullRequest) => typeof pullRequest.number === "number" && pullRequest.number > 0 ? Math.floor(pullRequest.number) : null).filter((number) => number !== null);
|
|
6049
|
+
}
|
|
6050
|
+
async function probeGitHubRepositoryTokenCapability(fn, options = {}) {
|
|
6051
|
+
try {
|
|
6052
|
+
await fn();
|
|
6053
|
+
return "granted";
|
|
6054
|
+
} catch (error) {
|
|
6055
|
+
return classifyGitHubCapabilityProbeError(error, options);
|
|
6056
|
+
}
|
|
6057
|
+
}
|
|
6058
|
+
async function loadGitHubRepositoryTokenCapabilityAudit(octokit, repository, options) {
|
|
6059
|
+
try {
|
|
6060
|
+
await octokit.rest.repos.get({
|
|
6061
|
+
owner: repository.owner,
|
|
6062
|
+
repo: repository.repo,
|
|
6063
|
+
headers: {
|
|
6064
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6065
|
+
}
|
|
6066
|
+
});
|
|
6067
|
+
} catch (error) {
|
|
6068
|
+
const status = getErrorStatus(error);
|
|
6069
|
+
return buildGitHubRepositoryTokenCapabilityAudit({
|
|
6070
|
+
repository,
|
|
6071
|
+
canComment: false,
|
|
6072
|
+
canReview: false,
|
|
6073
|
+
canClose: false,
|
|
6074
|
+
canUpdateBranch: false,
|
|
6075
|
+
canMerge: false,
|
|
6076
|
+
canRerunCi: false,
|
|
6077
|
+
missingPermissions: status === 401 || status === 403 || status === 404 ? [
|
|
6078
|
+
"Metadata: read",
|
|
6079
|
+
"Issues: write or Pull requests: write",
|
|
6080
|
+
"Pull requests: write",
|
|
6081
|
+
"Contents: write",
|
|
6082
|
+
"Checks: write"
|
|
6083
|
+
] : [],
|
|
6084
|
+
warnings: [
|
|
6085
|
+
status === 401 || status === 403 || status === 404 ? `GitHub Sync could not confirm repository access for ${formatRepositoryLabel(repository)} with the configured token.` : `GitHub Sync could not verify repository permissions for ${formatRepositoryLabel(repository)} because GitHub returned an unexpected error.`
|
|
6086
|
+
]
|
|
6087
|
+
});
|
|
6088
|
+
}
|
|
6089
|
+
let samplePullRequestNumber = typeof options?.samplePullRequestNumber === "number" && options.samplePullRequestNumber > 0 ? Math.floor(options.samplePullRequestNumber) : void 0;
|
|
6090
|
+
if (!samplePullRequestNumber) {
|
|
6091
|
+
try {
|
|
6092
|
+
samplePullRequestNumber = (await listGitHubRepositoryOpenPullRequestNumbers(octokit, repository))[0];
|
|
6093
|
+
} catch {
|
|
6094
|
+
return buildGitHubRepositoryTokenCapabilityAudit({
|
|
6095
|
+
repository,
|
|
6096
|
+
canComment: false,
|
|
6097
|
+
canReview: false,
|
|
6098
|
+
canClose: false,
|
|
6099
|
+
canUpdateBranch: false,
|
|
6100
|
+
canMerge: false,
|
|
6101
|
+
canRerunCi: false,
|
|
6102
|
+
missingPermissions: [],
|
|
6103
|
+
warnings: [
|
|
6104
|
+
`GitHub Sync could not verify write permissions for ${formatRepositoryLabel(repository)} because GitHub did not return a sample pull request.`
|
|
6105
|
+
]
|
|
6106
|
+
});
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
if (!samplePullRequestNumber) {
|
|
6110
|
+
return buildGitHubRepositoryTokenCapabilityAudit({
|
|
6111
|
+
repository,
|
|
6112
|
+
canComment: false,
|
|
6113
|
+
canReview: false,
|
|
6114
|
+
canClose: false,
|
|
6115
|
+
canUpdateBranch: false,
|
|
6116
|
+
canMerge: false,
|
|
6117
|
+
canRerunCi: false,
|
|
6118
|
+
missingPermissions: [],
|
|
6119
|
+
warnings: [
|
|
6120
|
+
`GitHub Sync could not verify write permissions for ${formatRepositoryLabel(repository)} because the repository has no open pull requests to probe safely.`
|
|
6121
|
+
]
|
|
6122
|
+
});
|
|
6123
|
+
}
|
|
6124
|
+
const impossibleSha = "0000000000000000000000000000000000000000";
|
|
6125
|
+
const [
|
|
6126
|
+
commentProbe,
|
|
6127
|
+
reviewProbe,
|
|
6128
|
+
closeProbe,
|
|
6129
|
+
updateBranchProbe,
|
|
6130
|
+
mergeProbe,
|
|
6131
|
+
rerunCiProbe
|
|
6132
|
+
] = await Promise.all([
|
|
6133
|
+
probeGitHubRepositoryTokenCapability(
|
|
6134
|
+
() => octokit.rest.issues.createComment({
|
|
6135
|
+
owner: repository.owner,
|
|
6136
|
+
repo: repository.repo,
|
|
6137
|
+
issue_number: samplePullRequestNumber,
|
|
6138
|
+
body: "",
|
|
6139
|
+
headers: {
|
|
6140
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6141
|
+
}
|
|
6142
|
+
}),
|
|
6143
|
+
{ grantedStatuses: [422] }
|
|
6144
|
+
),
|
|
6145
|
+
probeGitHubRepositoryTokenCapability(
|
|
6146
|
+
() => octokit.rest.pulls.createReview({
|
|
6147
|
+
owner: repository.owner,
|
|
6148
|
+
repo: repository.repo,
|
|
6149
|
+
pull_number: samplePullRequestNumber,
|
|
6150
|
+
event: "REQUEST_CHANGES",
|
|
6151
|
+
headers: {
|
|
6152
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6153
|
+
}
|
|
6154
|
+
}),
|
|
6155
|
+
{ grantedStatuses: [422] }
|
|
6156
|
+
),
|
|
6157
|
+
probeGitHubRepositoryTokenCapability(
|
|
6158
|
+
() => octokit.request("PATCH /repos/{owner}/{repo}/pulls/{pull_number}", {
|
|
6159
|
+
owner: repository.owner,
|
|
6160
|
+
repo: repository.repo,
|
|
6161
|
+
pull_number: samplePullRequestNumber,
|
|
6162
|
+
state: "__paperclip_invalid_state__",
|
|
6163
|
+
headers: {
|
|
6164
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6165
|
+
}
|
|
6166
|
+
}),
|
|
6167
|
+
{ grantedStatuses: [422] }
|
|
6168
|
+
),
|
|
6169
|
+
probeGitHubRepositoryTokenCapability(
|
|
6170
|
+
() => octokit.request("PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch", {
|
|
6171
|
+
owner: repository.owner,
|
|
6172
|
+
repo: repository.repo,
|
|
6173
|
+
pull_number: samplePullRequestNumber,
|
|
6174
|
+
expected_head_sha: impossibleSha,
|
|
6175
|
+
headers: {
|
|
6176
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6177
|
+
}
|
|
6178
|
+
}),
|
|
6179
|
+
{ grantedStatuses: [409, 422] }
|
|
6180
|
+
),
|
|
6181
|
+
probeGitHubRepositoryTokenCapability(
|
|
6182
|
+
() => octokit.rest.pulls.merge({
|
|
6183
|
+
owner: repository.owner,
|
|
6184
|
+
repo: repository.repo,
|
|
6185
|
+
pull_number: samplePullRequestNumber,
|
|
6186
|
+
sha: impossibleSha,
|
|
6187
|
+
headers: {
|
|
6188
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6189
|
+
}
|
|
6190
|
+
}),
|
|
6191
|
+
{ grantedStatuses: [405, 409, 422] }
|
|
6192
|
+
),
|
|
6193
|
+
probeGitHubRepositoryTokenCapability(
|
|
6194
|
+
() => octokit.rest.checks.rerequestSuite({
|
|
6195
|
+
owner: repository.owner,
|
|
6196
|
+
repo: repository.repo,
|
|
6197
|
+
check_suite_id: 0,
|
|
6198
|
+
headers: {
|
|
6199
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6200
|
+
}
|
|
6201
|
+
}),
|
|
6202
|
+
{ allowNotFoundAsGranted: true }
|
|
6203
|
+
)
|
|
6204
|
+
]);
|
|
6205
|
+
const missingPermissions = [
|
|
6206
|
+
...commentProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("comment")] : [],
|
|
6207
|
+
...reviewProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("review")] : [],
|
|
6208
|
+
...closeProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("close")] : [],
|
|
6209
|
+
...updateBranchProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("update_branch")] : [],
|
|
6210
|
+
...mergeProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("merge")] : [],
|
|
6211
|
+
...rerunCiProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("rerun_ci")] : []
|
|
6212
|
+
];
|
|
6213
|
+
const warnings = [
|
|
6214
|
+
...commentProbe === "unknown" ? ["GitHub Sync could not verify comment permissions."] : [],
|
|
6215
|
+
...reviewProbe === "unknown" ? ["GitHub Sync could not verify review permissions."] : [],
|
|
6216
|
+
...closeProbe === "unknown" ? ["GitHub Sync could not verify close permissions."] : [],
|
|
6217
|
+
...updateBranchProbe === "unknown" ? ["GitHub Sync could not verify update-branch permissions."] : [],
|
|
6218
|
+
...mergeProbe === "unknown" ? ["GitHub Sync could not verify merge permissions."] : [],
|
|
6219
|
+
...rerunCiProbe === "unknown" ? ["GitHub Sync could not verify re-run CI permissions."] : []
|
|
6220
|
+
];
|
|
6221
|
+
return buildGitHubRepositoryTokenCapabilityAudit({
|
|
6222
|
+
repository,
|
|
6223
|
+
samplePullRequestNumber,
|
|
6224
|
+
canComment: commentProbe === "granted",
|
|
6225
|
+
canReview: reviewProbe === "granted",
|
|
6226
|
+
canClose: closeProbe === "granted",
|
|
6227
|
+
canUpdateBranch: updateBranchProbe === "granted",
|
|
6228
|
+
canMerge: mergeProbe === "granted",
|
|
6229
|
+
canRerunCi: rerunCiProbe === "granted",
|
|
6230
|
+
missingPermissions,
|
|
6231
|
+
warnings
|
|
6232
|
+
});
|
|
6233
|
+
}
|
|
6234
|
+
async function getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, repository, options) {
|
|
6235
|
+
const cacheKey = buildGitHubRepositoryTokenCapabilityAuditCacheKey(repository, options?.samplePullRequestNumber);
|
|
6236
|
+
const cachedAudit = getFreshCacheValue(activeGitHubRepositoryTokenCapabilityAuditCache, cacheKey);
|
|
6237
|
+
if (cachedAudit) {
|
|
6238
|
+
return cachedAudit;
|
|
6239
|
+
}
|
|
6240
|
+
const inFlightAudit = activeGitHubRepositoryTokenCapabilityAuditPromiseCache.get(cacheKey);
|
|
6241
|
+
if (inFlightAudit) {
|
|
6242
|
+
return inFlightAudit;
|
|
6243
|
+
}
|
|
6244
|
+
const loadAuditPromise = (async () => {
|
|
6245
|
+
const audit = await loadGitHubRepositoryTokenCapabilityAudit(octokit, repository, options);
|
|
6246
|
+
return setCacheValue(
|
|
6247
|
+
activeGitHubRepositoryTokenCapabilityAuditCache,
|
|
6248
|
+
cacheKey,
|
|
6249
|
+
audit,
|
|
6250
|
+
GITHUB_TOKEN_PERMISSION_AUDIT_CACHE_TTL_MS
|
|
6251
|
+
);
|
|
6252
|
+
})();
|
|
6253
|
+
activeGitHubRepositoryTokenCapabilityAuditPromiseCache.set(cacheKey, loadAuditPromise);
|
|
6254
|
+
try {
|
|
6255
|
+
return await loadAuditPromise;
|
|
6256
|
+
} finally {
|
|
6257
|
+
if (activeGitHubRepositoryTokenCapabilityAuditPromiseCache.get(cacheKey) === loadAuditPromise) {
|
|
6258
|
+
activeGitHubRepositoryTokenCapabilityAuditPromiseCache.delete(cacheKey);
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
}
|
|
6262
|
+
async function resolveRepositoryFromRunContext(ctx, runCtx) {
|
|
6263
|
+
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
6264
|
+
const mappings = getSyncableMappingsForTarget(settings.mappings, {
|
|
6265
|
+
kind: "project",
|
|
6266
|
+
companyId: runCtx.companyId,
|
|
6267
|
+
projectId: runCtx.projectId,
|
|
6268
|
+
displayLabel: "project"
|
|
6269
|
+
});
|
|
6270
|
+
const repositories = [
|
|
6271
|
+
...new Map(
|
|
6272
|
+
mappings.map((mapping) => {
|
|
6273
|
+
const repository = parseRepositoryReference(mapping.repositoryUrl);
|
|
6274
|
+
return repository ? [repository.url, repository] : null;
|
|
6275
|
+
}).filter((entry) => entry !== null)
|
|
6276
|
+
).values()
|
|
6277
|
+
];
|
|
6278
|
+
if (repositories.length === 1) {
|
|
6279
|
+
return repositories[0];
|
|
6280
|
+
}
|
|
6281
|
+
if (repositories.length === 0) {
|
|
6282
|
+
throw new Error("No GitHub repository is mapped to the current Paperclip project. Pass repository explicitly.");
|
|
6283
|
+
}
|
|
6284
|
+
throw new Error("Multiple GitHub repositories are mapped to the current Paperclip project. Pass repository explicitly.");
|
|
6285
|
+
}
|
|
6286
|
+
async function resolveGitHubToolRepository(ctx, runCtx, input) {
|
|
6287
|
+
const explicitRepository = normalizeOptionalToolString(input.repository);
|
|
6288
|
+
if (explicitRepository) {
|
|
6289
|
+
return requireRepositoryReference(explicitRepository);
|
|
6290
|
+
}
|
|
6291
|
+
const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
|
|
6292
|
+
if (paperclipIssueId) {
|
|
6293
|
+
const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
|
|
6294
|
+
if (!link) {
|
|
6295
|
+
throw new Error("This Paperclip issue is not linked to a GitHub issue yet. Pass repository explicitly.");
|
|
6296
|
+
}
|
|
6297
|
+
return requireRepositoryReference(link.repositoryUrl);
|
|
6298
|
+
}
|
|
6299
|
+
return resolveRepositoryFromRunContext(ctx, runCtx);
|
|
6300
|
+
}
|
|
6301
|
+
async function resolveGitHubIssueToolTarget(ctx, runCtx, input) {
|
|
6302
|
+
const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
|
|
6303
|
+
if (paperclipIssueId) {
|
|
6304
|
+
const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
|
|
6305
|
+
if (!link) {
|
|
6306
|
+
throw new Error("This Paperclip issue is not linked to a GitHub issue yet.");
|
|
6307
|
+
}
|
|
6308
|
+
const repository2 = assertExplicitRepositoryMatchesLinkedRepository(
|
|
6309
|
+
input.repository,
|
|
6310
|
+
link.repositoryUrl,
|
|
6311
|
+
"The provided repository does not match the linked GitHub repository for this Paperclip issue."
|
|
6312
|
+
);
|
|
6313
|
+
const explicitIssueNumber = normalizeToolPositiveInteger(input.issueNumber);
|
|
6314
|
+
if (explicitIssueNumber !== void 0 && explicitIssueNumber !== link.githubIssueNumber) {
|
|
6315
|
+
throw new Error("The provided issue number does not match the linked GitHub issue for this Paperclip issue.");
|
|
6316
|
+
}
|
|
6317
|
+
return {
|
|
6318
|
+
repository: repository2,
|
|
6319
|
+
issueNumber: link.githubIssueNumber,
|
|
6320
|
+
paperclipIssueId,
|
|
6321
|
+
githubIssueId: link.githubIssueId,
|
|
6322
|
+
githubIssueUrl: link.githubIssueUrl
|
|
6323
|
+
};
|
|
6324
|
+
}
|
|
6325
|
+
const repository = await resolveGitHubToolRepository(ctx, runCtx, input);
|
|
6326
|
+
const issueNumber = normalizeToolPositiveInteger(input.issueNumber);
|
|
6327
|
+
if (issueNumber === void 0) {
|
|
6328
|
+
throw new Error("issueNumber is required when paperclipIssueId is not provided.");
|
|
6329
|
+
}
|
|
6330
|
+
return {
|
|
6331
|
+
repository,
|
|
6332
|
+
issueNumber
|
|
6333
|
+
};
|
|
6334
|
+
}
|
|
6335
|
+
async function resolveGitHubPullRequestToolTarget(ctx, runCtx, input) {
|
|
6336
|
+
const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
|
|
6337
|
+
if (paperclipIssueId) {
|
|
6338
|
+
const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
|
|
6339
|
+
if (!link) {
|
|
6340
|
+
throw new Error("This Paperclip issue is not linked to GitHub yet.");
|
|
6341
|
+
}
|
|
6342
|
+
const repository2 = assertExplicitRepositoryMatchesLinkedRepository(
|
|
6343
|
+
input.repository,
|
|
6344
|
+
link.repositoryUrl,
|
|
6345
|
+
"repository must match the GitHub repository linked to the provided Paperclip issue."
|
|
6346
|
+
);
|
|
6347
|
+
const explicitPullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
6348
|
+
if (explicitPullRequestNumber !== void 0) {
|
|
6349
|
+
return {
|
|
6350
|
+
repository: repository2,
|
|
6351
|
+
pullRequestNumber: explicitPullRequestNumber,
|
|
6352
|
+
paperclipIssueId
|
|
6353
|
+
};
|
|
6354
|
+
}
|
|
6355
|
+
if (link.linkedPullRequestNumbers.length === 1) {
|
|
6356
|
+
return {
|
|
6357
|
+
repository: repository2,
|
|
6358
|
+
pullRequestNumber: link.linkedPullRequestNumbers[0],
|
|
6359
|
+
paperclipIssueId
|
|
6360
|
+
};
|
|
6361
|
+
}
|
|
6362
|
+
throw new Error("pullRequestNumber is required unless the linked Paperclip issue has exactly one linked pull request.");
|
|
6363
|
+
}
|
|
6364
|
+
const repository = await resolveGitHubToolRepository(ctx, runCtx, input);
|
|
6365
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
6366
|
+
if (pullRequestNumber === void 0) {
|
|
6367
|
+
throw new Error("pullRequestNumber is required when paperclipIssueId is not provided.");
|
|
6368
|
+
}
|
|
6369
|
+
return {
|
|
6370
|
+
repository,
|
|
6371
|
+
pullRequestNumber
|
|
6372
|
+
};
|
|
6373
|
+
}
|
|
6374
|
+
function formatAiAuthorshipFooter(llmModel) {
|
|
6375
|
+
return `
|
|
6376
|
+
|
|
6377
|
+
---
|
|
6378
|
+
${AI_AUTHORED_COMMENT_FOOTER_PREFIX}${llmModel.trim()}.`;
|
|
6379
|
+
}
|
|
6380
|
+
function appendAiAuthorshipFooter(body, llmModel) {
|
|
6381
|
+
const trimmedBody = body.trim();
|
|
6382
|
+
if (!trimmedBody) {
|
|
6383
|
+
throw new Error("Comment body cannot be empty.");
|
|
6384
|
+
}
|
|
6385
|
+
const trimmedModel = llmModel.trim();
|
|
6386
|
+
if (!trimmedModel) {
|
|
6387
|
+
throw new Error("llmModel is required when posting a GitHub comment.");
|
|
6388
|
+
}
|
|
6389
|
+
return `${trimmedBody}${formatAiAuthorshipFooter(trimmedModel)}`;
|
|
6390
|
+
}
|
|
6391
|
+
async function listAllGitHubIssueComments(octokit, repository, issueNumber) {
|
|
6392
|
+
const comments = [];
|
|
6393
|
+
for await (const response of octokit.paginate.iterator(octokit.rest.issues.listComments, {
|
|
6394
|
+
owner: repository.owner,
|
|
6395
|
+
repo: repository.repo,
|
|
6396
|
+
issue_number: issueNumber,
|
|
6397
|
+
per_page: 100,
|
|
6398
|
+
headers: {
|
|
6399
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6400
|
+
}
|
|
6401
|
+
})) {
|
|
6402
|
+
for (const comment of response.data) {
|
|
6403
|
+
comments.push({
|
|
6404
|
+
id: comment.id,
|
|
6405
|
+
body: comment.body ?? "",
|
|
6406
|
+
url: comment.html_url ?? void 0,
|
|
6407
|
+
authorLogin: normalizeGitHubUserLogin(comment.user?.login),
|
|
6408
|
+
authorUrl: comment.user?.html_url ?? void 0,
|
|
6409
|
+
authorAvatarUrl: comment.user?.avatar_url ?? void 0,
|
|
6410
|
+
createdAt: comment.created_at ?? void 0,
|
|
6411
|
+
updatedAt: comment.updated_at ?? void 0
|
|
6412
|
+
});
|
|
6413
|
+
}
|
|
6414
|
+
}
|
|
6415
|
+
return comments;
|
|
6416
|
+
}
|
|
6417
|
+
async function listPaperclipIssuesForProject(ctx, companyId, projectId) {
|
|
6418
|
+
const issues = [];
|
|
6419
|
+
for (let offset = 0; ; ) {
|
|
6420
|
+
const page = await ctx.issues.list({
|
|
6421
|
+
companyId,
|
|
6422
|
+
projectId,
|
|
6423
|
+
limit: PAPERCLIP_LABEL_PAGE_SIZE,
|
|
6424
|
+
offset
|
|
6425
|
+
});
|
|
6426
|
+
if (page.length === 0) {
|
|
6427
|
+
break;
|
|
6428
|
+
}
|
|
6429
|
+
issues.push(...page);
|
|
6430
|
+
if (page.length < PAPERCLIP_LABEL_PAGE_SIZE) {
|
|
6431
|
+
break;
|
|
6432
|
+
}
|
|
6433
|
+
offset += page.length;
|
|
6434
|
+
}
|
|
6435
|
+
return issues;
|
|
6436
|
+
}
|
|
6437
|
+
function buildProjectPullRequestPerson(input) {
|
|
6438
|
+
const normalizedLogin = normalizeGitHubUsername(input.login) ?? "unknown";
|
|
6439
|
+
const displayName = typeof input.name === "string" && input.name.trim() ? input.name.trim() : normalizedLogin;
|
|
6440
|
+
const profileUrl = typeof input.url === "string" && input.url.trim() ? input.url.trim() : `https://github.com/${normalizedLogin}`;
|
|
6441
|
+
const avatarUrl = typeof input.avatarUrl === "string" && input.avatarUrl.trim() ? input.avatarUrl.trim() : void 0;
|
|
6442
|
+
return {
|
|
6443
|
+
name: displayName,
|
|
6444
|
+
handle: `@${normalizedLogin}`,
|
|
6445
|
+
profileUrl,
|
|
6446
|
+
...avatarUrl ? { avatarUrl } : {}
|
|
6447
|
+
};
|
|
6448
|
+
}
|
|
6449
|
+
function normalizeProjectPullRequestLabels(nodes) {
|
|
6450
|
+
if (!Array.isArray(nodes)) {
|
|
6451
|
+
return [];
|
|
6452
|
+
}
|
|
6453
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6454
|
+
const labels = [];
|
|
6455
|
+
for (const entry of nodes) {
|
|
6456
|
+
const name = typeof entry?.name === "string" ? entry.name.trim() : "";
|
|
6457
|
+
if (!name) {
|
|
6458
|
+
continue;
|
|
6459
|
+
}
|
|
6460
|
+
const key = normalizeLabelName(name);
|
|
6461
|
+
if (seen.has(key)) {
|
|
6462
|
+
continue;
|
|
6463
|
+
}
|
|
6464
|
+
seen.add(key);
|
|
6465
|
+
labels.push({
|
|
6466
|
+
name,
|
|
6467
|
+
...normalizeHexColor(entry?.color) ? { color: normalizeHexColor(entry?.color) } : {}
|
|
6468
|
+
});
|
|
6469
|
+
}
|
|
6470
|
+
return labels;
|
|
6471
|
+
}
|
|
6472
|
+
function summarizeGitHubPullRequestReviewsFromEntries(entries) {
|
|
6473
|
+
const latestStateByReviewer = /* @__PURE__ */ new Map();
|
|
6474
|
+
let anonymousApprovals = 0;
|
|
6475
|
+
let anonymousChangesRequested = 0;
|
|
6476
|
+
for (const entry of entries) {
|
|
6477
|
+
const state = typeof entry.state === "string" ? entry.state.trim().toUpperCase() : "";
|
|
6478
|
+
const authorLogin = normalizeGitHubUsername(entry.authorLogin);
|
|
6479
|
+
if (state === "APPROVED") {
|
|
6480
|
+
if (authorLogin) {
|
|
6481
|
+
latestStateByReviewer.set(authorLogin, "APPROVED");
|
|
6482
|
+
} else {
|
|
6483
|
+
anonymousApprovals += 1;
|
|
6484
|
+
}
|
|
6485
|
+
continue;
|
|
6486
|
+
}
|
|
6487
|
+
if (state === "CHANGES_REQUESTED") {
|
|
6488
|
+
if (authorLogin) {
|
|
6489
|
+
latestStateByReviewer.set(authorLogin, "CHANGES_REQUESTED");
|
|
6490
|
+
} else {
|
|
6491
|
+
anonymousChangesRequested += 1;
|
|
6492
|
+
}
|
|
6493
|
+
continue;
|
|
6494
|
+
}
|
|
6495
|
+
if (state === "DISMISSED" && authorLogin) {
|
|
6496
|
+
latestStateByReviewer.delete(authorLogin);
|
|
6497
|
+
}
|
|
6498
|
+
}
|
|
6499
|
+
let approvals = anonymousApprovals;
|
|
6500
|
+
let changesRequested = anonymousChangesRequested;
|
|
6501
|
+
for (const state of latestStateByReviewer.values()) {
|
|
6502
|
+
if (state === "APPROVED") {
|
|
6503
|
+
approvals += 1;
|
|
6504
|
+
} else if (state === "CHANGES_REQUESTED") {
|
|
6505
|
+
changesRequested += 1;
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
return {
|
|
6509
|
+
approvals,
|
|
6510
|
+
changesRequested
|
|
6511
|
+
};
|
|
6512
|
+
}
|
|
6513
|
+
function summarizeGitHubPullRequestReviewNodes(nodes) {
|
|
6514
|
+
const entries = (nodes ?? []).map((node) => ({
|
|
6515
|
+
state: node?.state ?? void 0,
|
|
6516
|
+
authorLogin: node?.author?.login ?? void 0
|
|
6517
|
+
}));
|
|
6518
|
+
return summarizeGitHubPullRequestReviewsFromEntries(entries);
|
|
6519
|
+
}
|
|
6520
|
+
async function listGitHubPullRequestReviewSummary(octokit, repository, pullRequestNumber) {
|
|
6521
|
+
const entries = [];
|
|
6522
|
+
for await (const response of octokit.paginate.iterator(octokit.rest.pulls.listReviews, {
|
|
6523
|
+
owner: repository.owner,
|
|
6524
|
+
repo: repository.repo,
|
|
6525
|
+
pull_number: pullRequestNumber,
|
|
6526
|
+
per_page: 100,
|
|
6527
|
+
headers: {
|
|
6528
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6529
|
+
}
|
|
6530
|
+
})) {
|
|
6531
|
+
for (const review of response.data) {
|
|
6532
|
+
entries.push({
|
|
6533
|
+
state: review.state ?? void 0,
|
|
6534
|
+
authorLogin: review.user?.login ?? void 0
|
|
6535
|
+
});
|
|
6536
|
+
}
|
|
6537
|
+
}
|
|
6538
|
+
return summarizeGitHubPullRequestReviewsFromEntries(entries);
|
|
6539
|
+
}
|
|
6540
|
+
function buildProjectPullRequestScopeCacheKey(params) {
|
|
6541
|
+
return `${params.companyId}:${params.projectId}:${getNormalizedMappingRepositoryUrl({
|
|
6542
|
+
repositoryUrl: params.repositoryUrl
|
|
6543
|
+
})}`;
|
|
6544
|
+
}
|
|
6545
|
+
function buildRepositoryPullRequestCacheScopeKey(repository) {
|
|
6546
|
+
return `${repository.owner.toLowerCase()}/${repository.repo.toLowerCase()}`;
|
|
6547
|
+
}
|
|
6548
|
+
function buildRepositoryPullRequestCollectionCacheKey(repository, suffix) {
|
|
6549
|
+
return `${buildRepositoryPullRequestCacheScopeKey(repository)}:${suffix}`;
|
|
6550
|
+
}
|
|
6551
|
+
function buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, suffix) {
|
|
6552
|
+
return `${buildRepositoryPullRequestCollectionCacheKey(repository, "pull-request")}:${Math.max(1, Math.floor(pullRequestNumber))}:${suffix}`;
|
|
6553
|
+
}
|
|
6554
|
+
function buildRepositoryPullRequestCompareCacheKey(repository, baseBranch, headBranch, headRepositoryOwner) {
|
|
6555
|
+
const normalizedBaseBranch = baseBranch.trim();
|
|
6556
|
+
const normalizedHeadBranch = headBranch.trim();
|
|
6557
|
+
const normalizedHeadRepositoryOwner = typeof headRepositoryOwner === "string" && headRepositoryOwner.trim() ? headRepositoryOwner.trim() : repository.owner;
|
|
6558
|
+
const compareHeadRef = normalizedHeadRepositoryOwner.toLowerCase() === repository.owner.toLowerCase() ? normalizedHeadBranch : `${normalizedHeadRepositoryOwner}:${normalizedHeadBranch}`;
|
|
6559
|
+
return `${buildRepositoryPullRequestCollectionCacheKey(repository, "compare")}:${encodeURIComponent(`${normalizedBaseBranch}...${compareHeadRef}`)}`;
|
|
6560
|
+
}
|
|
6561
|
+
function buildProjectPullRequestPageCacheKey(scope, filter, pageIndex, cursor) {
|
|
6562
|
+
return `${buildProjectPullRequestScopeCacheKey({
|
|
6563
|
+
companyId: scope.companyId,
|
|
6564
|
+
projectId: scope.projectId,
|
|
6565
|
+
repositoryUrl: scope.repository.url
|
|
6566
|
+
})}:page:${filter}:${pageIndex}:${cursor ?? ""}`;
|
|
6567
|
+
}
|
|
6568
|
+
function buildProjectPullRequestDetailCacheKey(scope, pullRequestNumber) {
|
|
6569
|
+
return `${buildProjectPullRequestScopeCacheKey({
|
|
6570
|
+
companyId: scope.companyId,
|
|
6571
|
+
projectId: scope.projectId,
|
|
6572
|
+
repositoryUrl: scope.repository.url
|
|
6573
|
+
})}:detail:${pullRequestNumber}`;
|
|
6574
|
+
}
|
|
6575
|
+
function buildProjectPullRequestSummaryRecordCacheKey(scope, pullRequestNumber) {
|
|
6576
|
+
return `${buildProjectPullRequestScopeCacheKey({
|
|
6577
|
+
companyId: scope.companyId,
|
|
6578
|
+
projectId: scope.projectId,
|
|
6579
|
+
repositoryUrl: scope.repository.url
|
|
6580
|
+
})}:summary-record:${Math.max(1, Math.floor(pullRequestNumber))}`;
|
|
6581
|
+
}
|
|
6582
|
+
function invalidateProjectPullRequestCaches(scope) {
|
|
6583
|
+
const cacheKeyPrefix = `${buildProjectPullRequestScopeCacheKey({
|
|
6584
|
+
companyId: scope.companyId,
|
|
6585
|
+
projectId: scope.projectId,
|
|
6586
|
+
repositoryUrl: scope.repository.url
|
|
6587
|
+
})}:`;
|
|
6588
|
+
const repositoryCacheKeyPrefix = `${buildRepositoryPullRequestCacheScopeKey(scope.repository)}:`;
|
|
6589
|
+
for (const cache of [
|
|
6590
|
+
activeProjectPullRequestPageCache,
|
|
6591
|
+
activeProjectPullRequestSummaryCache,
|
|
6592
|
+
activeProjectPullRequestSummaryRecordCache,
|
|
6593
|
+
activeProjectPullRequestDetailCache,
|
|
6594
|
+
activeProjectPullRequestIssueLookupCache
|
|
6595
|
+
]) {
|
|
6596
|
+
for (const key of cache.keys()) {
|
|
6597
|
+
if (key.startsWith(cacheKeyPrefix)) {
|
|
6598
|
+
cache.delete(key);
|
|
6599
|
+
}
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
6602
|
+
for (const cache of [
|
|
6603
|
+
activeProjectPullRequestCountCache,
|
|
6604
|
+
activeProjectPullRequestMetricsCache,
|
|
6605
|
+
activeGitHubPullRequestBehindCountCache,
|
|
6606
|
+
activeGitHubPullRequestStatusSnapshotCache,
|
|
6607
|
+
activeGitHubPullRequestReviewSummaryCache,
|
|
6608
|
+
activeGitHubPullRequestReviewThreadSummaryCache
|
|
6609
|
+
]) {
|
|
6610
|
+
for (const key of cache.keys()) {
|
|
6611
|
+
if (key.startsWith(repositoryCacheKeyPrefix)) {
|
|
6612
|
+
cache.delete(key);
|
|
6613
|
+
}
|
|
6614
|
+
}
|
|
6615
|
+
}
|
|
6616
|
+
for (const key of activeProjectPullRequestSummaryPromiseCache.keys()) {
|
|
6617
|
+
if (key.startsWith(cacheKeyPrefix)) {
|
|
6618
|
+
activeProjectPullRequestSummaryPromiseCache.delete(key);
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
for (const promiseCache of [
|
|
6622
|
+
activeProjectPullRequestCountPromiseCache,
|
|
6623
|
+
activeProjectPullRequestMetricsPromiseCache,
|
|
6624
|
+
activeGitHubPullRequestBehindCountPromiseCache,
|
|
6625
|
+
activeGitHubPullRequestStatusSnapshotPromiseCache,
|
|
6626
|
+
activeGitHubPullRequestReviewSummaryPromiseCache,
|
|
6627
|
+
activeGitHubPullRequestReviewThreadSummaryPromiseCache
|
|
6628
|
+
]) {
|
|
6629
|
+
for (const key of promiseCache.keys()) {
|
|
6630
|
+
if (key.startsWith(repositoryCacheKeyPrefix)) {
|
|
6631
|
+
promiseCache.delete(key);
|
|
6632
|
+
}
|
|
6633
|
+
}
|
|
6634
|
+
}
|
|
6635
|
+
}
|
|
6636
|
+
async function getGitHubPullRequestBehindCount(octokit, repository, options) {
|
|
6637
|
+
const baseBranch = typeof options.baseBranch === "string" ? options.baseBranch.trim() : "";
|
|
6638
|
+
const headBranch = typeof options.headBranch === "string" ? options.headBranch.trim() : "";
|
|
6639
|
+
if (!baseBranch || !headBranch) {
|
|
6640
|
+
return null;
|
|
6641
|
+
}
|
|
6642
|
+
const headRepositoryOwner = typeof options.headRepositoryOwner === "string" && options.headRepositoryOwner.trim() ? options.headRepositoryOwner.trim() : repository.owner;
|
|
6643
|
+
const compareHeadRef = headRepositoryOwner.toLowerCase() === repository.owner.toLowerCase() ? headBranch : `${headRepositoryOwner}:${headBranch}`;
|
|
6644
|
+
const cacheKey = buildRepositoryPullRequestCompareCacheKey(repository, baseBranch, headBranch, headRepositoryOwner);
|
|
6645
|
+
const cachedBehindCountEntry = getFreshCacheEntry(activeGitHubPullRequestBehindCountCache, cacheKey);
|
|
6646
|
+
if (cachedBehindCountEntry) {
|
|
6647
|
+
return cachedBehindCountEntry.value;
|
|
6648
|
+
}
|
|
6649
|
+
const inFlightBehindCount = activeGitHubPullRequestBehindCountPromiseCache.get(cacheKey);
|
|
6650
|
+
if (inFlightBehindCount) {
|
|
6651
|
+
return inFlightBehindCount;
|
|
6652
|
+
}
|
|
6653
|
+
const loadBehindCountPromise = (async () => {
|
|
6654
|
+
try {
|
|
6655
|
+
const response = await octokit.request("GET /repos/{owner}/{repo}/compare/{basehead}", {
|
|
6656
|
+
owner: repository.owner,
|
|
6657
|
+
repo: repository.repo,
|
|
6658
|
+
basehead: `${baseBranch}...${compareHeadRef}`
|
|
6659
|
+
});
|
|
6660
|
+
const behindBy = response.data?.behind_by;
|
|
6661
|
+
return setCacheValue(
|
|
6662
|
+
activeGitHubPullRequestBehindCountCache,
|
|
6663
|
+
cacheKey,
|
|
6664
|
+
typeof behindBy === "number" && behindBy >= 0 ? Math.floor(behindBy) : null,
|
|
6665
|
+
PROJECT_PULL_REQUEST_BRANCH_COMPARE_CACHE_TTL_MS
|
|
6666
|
+
);
|
|
6667
|
+
} catch {
|
|
6668
|
+
return setCacheValue(
|
|
6669
|
+
activeGitHubPullRequestBehindCountCache,
|
|
6670
|
+
cacheKey,
|
|
6671
|
+
null,
|
|
6672
|
+
PROJECT_PULL_REQUEST_BRANCH_COMPARE_CACHE_TTL_MS
|
|
6673
|
+
);
|
|
6674
|
+
}
|
|
6675
|
+
})();
|
|
6676
|
+
activeGitHubPullRequestBehindCountPromiseCache.set(cacheKey, loadBehindCountPromise);
|
|
6677
|
+
try {
|
|
6678
|
+
return await loadBehindCountPromise;
|
|
6679
|
+
} finally {
|
|
6680
|
+
if (activeGitHubPullRequestBehindCountPromiseCache.get(cacheKey) === loadBehindCountPromise) {
|
|
6681
|
+
activeGitHubPullRequestBehindCountPromiseCache.delete(cacheKey);
|
|
6682
|
+
}
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
function cacheGitHubPullRequestReviewSummary(repository, pullRequestNumber, summary) {
|
|
6686
|
+
return setCacheValue(
|
|
6687
|
+
activeGitHubPullRequestReviewSummaryCache,
|
|
6688
|
+
buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "review-summary"),
|
|
6689
|
+
summary,
|
|
6690
|
+
PROJECT_PULL_REQUEST_GITHUB_INSIGHT_CACHE_TTL_MS
|
|
6691
|
+
);
|
|
6692
|
+
}
|
|
6693
|
+
function cacheGitHubPullRequestReviewThreadSummary(repository, pullRequestNumber, summary) {
|
|
6694
|
+
return setCacheValue(
|
|
6695
|
+
activeGitHubPullRequestReviewThreadSummaryCache,
|
|
6696
|
+
buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "review-threads"),
|
|
6697
|
+
summary,
|
|
6698
|
+
PROJECT_PULL_REQUEST_GITHUB_INSIGHT_CACHE_TTL_MS
|
|
6699
|
+
);
|
|
6700
|
+
}
|
|
6701
|
+
function cacheGitHubPullRequestStatusSnapshot(repository, snapshot) {
|
|
6702
|
+
return setCacheValue(
|
|
6703
|
+
activeGitHubPullRequestStatusSnapshotCache,
|
|
6704
|
+
buildRepositoryPullRequestRecordCacheKey(repository, snapshot.number, "status"),
|
|
6705
|
+
snapshot,
|
|
6706
|
+
PROJECT_PULL_REQUEST_GITHUB_INSIGHT_CACHE_TTL_MS
|
|
6707
|
+
);
|
|
6708
|
+
}
|
|
6709
|
+
async function getOrLoadCachedGitHubPullRequestReviewSummary(octokit, repository, pullRequestNumber, inlineSummary) {
|
|
6710
|
+
const cacheKey = buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "review-summary");
|
|
6711
|
+
const cachedSummary = getFreshCacheValue(activeGitHubPullRequestReviewSummaryCache, cacheKey);
|
|
6712
|
+
if (cachedSummary) {
|
|
6713
|
+
return cachedSummary;
|
|
6714
|
+
}
|
|
6715
|
+
if (inlineSummary) {
|
|
6716
|
+
return cacheGitHubPullRequestReviewSummary(repository, pullRequestNumber, inlineSummary);
|
|
6717
|
+
}
|
|
6718
|
+
const inFlightSummary = activeGitHubPullRequestReviewSummaryPromiseCache.get(cacheKey);
|
|
6719
|
+
if (inFlightSummary) {
|
|
6720
|
+
return inFlightSummary;
|
|
6721
|
+
}
|
|
6722
|
+
const loadSummaryPromise = (async () => cacheGitHubPullRequestReviewSummary(
|
|
6723
|
+
repository,
|
|
6724
|
+
pullRequestNumber,
|
|
6725
|
+
await listGitHubPullRequestReviewSummary(octokit, repository, pullRequestNumber)
|
|
6726
|
+
))();
|
|
6727
|
+
activeGitHubPullRequestReviewSummaryPromiseCache.set(cacheKey, loadSummaryPromise);
|
|
6728
|
+
try {
|
|
6729
|
+
return await loadSummaryPromise;
|
|
6730
|
+
} finally {
|
|
6731
|
+
if (activeGitHubPullRequestReviewSummaryPromiseCache.get(cacheKey) === loadSummaryPromise) {
|
|
6732
|
+
activeGitHubPullRequestReviewSummaryPromiseCache.delete(cacheKey);
|
|
6733
|
+
}
|
|
6734
|
+
}
|
|
6735
|
+
}
|
|
6736
|
+
async function getOrLoadCachedGitHubPullRequestReviewThreadSummary(octokit, repository, pullRequestNumber, inlineSummary) {
|
|
6737
|
+
const cacheKey = buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "review-threads");
|
|
6738
|
+
const cachedSummary = getFreshCacheValue(activeGitHubPullRequestReviewThreadSummaryCache, cacheKey);
|
|
6739
|
+
if (cachedSummary) {
|
|
6740
|
+
return cachedSummary;
|
|
6741
|
+
}
|
|
6742
|
+
if (inlineSummary) {
|
|
6743
|
+
return cacheGitHubPullRequestReviewThreadSummary(repository, pullRequestNumber, inlineSummary);
|
|
6744
|
+
}
|
|
6745
|
+
const inFlightSummary = activeGitHubPullRequestReviewThreadSummaryPromiseCache.get(cacheKey);
|
|
6746
|
+
if (inFlightSummary) {
|
|
6747
|
+
return inFlightSummary;
|
|
6748
|
+
}
|
|
6749
|
+
const loadSummaryPromise = (async () => cacheGitHubPullRequestReviewThreadSummary(
|
|
6750
|
+
repository,
|
|
6751
|
+
pullRequestNumber,
|
|
6752
|
+
summarizeDetailedPullRequestReviewThreads(
|
|
6753
|
+
await listDetailedPullRequestReviewThreads(octokit, repository, pullRequestNumber)
|
|
6754
|
+
)
|
|
6755
|
+
))();
|
|
6756
|
+
activeGitHubPullRequestReviewThreadSummaryPromiseCache.set(cacheKey, loadSummaryPromise);
|
|
6757
|
+
try {
|
|
6758
|
+
return await loadSummaryPromise;
|
|
6759
|
+
} finally {
|
|
6760
|
+
if (activeGitHubPullRequestReviewThreadSummaryPromiseCache.get(cacheKey) === loadSummaryPromise) {
|
|
6761
|
+
activeGitHubPullRequestReviewThreadSummaryPromiseCache.delete(cacheKey);
|
|
6762
|
+
}
|
|
6763
|
+
}
|
|
6764
|
+
}
|
|
6765
|
+
function getProjectPullRequestNumbersForFilter(metrics, filter) {
|
|
6766
|
+
switch (filter) {
|
|
6767
|
+
case "mergeable":
|
|
6768
|
+
return metrics.mergeablePullRequestNumbers;
|
|
6769
|
+
case "reviewable":
|
|
6770
|
+
return metrics.reviewablePullRequestNumbers;
|
|
6771
|
+
case "failing":
|
|
6772
|
+
return metrics.failingPullRequestNumbers;
|
|
6773
|
+
}
|
|
6774
|
+
}
|
|
6775
|
+
function cacheProjectPullRequestSummaryRecords(scope, pullRequests, ttlMs = PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS) {
|
|
6776
|
+
const now = Date.now();
|
|
6777
|
+
for (const pullRequest of pullRequests) {
|
|
6778
|
+
const pullRequestNumber = getProjectPullRequestNumber(pullRequest);
|
|
6779
|
+
if (pullRequestNumber === null) {
|
|
6780
|
+
continue;
|
|
6781
|
+
}
|
|
6782
|
+
setCacheValue(
|
|
6783
|
+
activeProjectPullRequestSummaryRecordCache,
|
|
6784
|
+
buildProjectPullRequestSummaryRecordCacheKey(scope, pullRequestNumber),
|
|
6785
|
+
pullRequest,
|
|
6786
|
+
ttlMs,
|
|
6787
|
+
now
|
|
6788
|
+
);
|
|
6789
|
+
}
|
|
6790
|
+
}
|
|
6791
|
+
function selectImportedPaperclipIssueReference(current, next) {
|
|
6792
|
+
if (!current) {
|
|
6793
|
+
return next;
|
|
6794
|
+
}
|
|
6795
|
+
return compareImportedPaperclipIssueCreatedAt(
|
|
6796
|
+
{
|
|
6797
|
+
id: next.paperclipIssueId,
|
|
6798
|
+
createdAt: next.createdAt
|
|
6799
|
+
},
|
|
6800
|
+
{
|
|
6801
|
+
id: current.paperclipIssueId,
|
|
6802
|
+
createdAt: current.createdAt
|
|
6803
|
+
}
|
|
6804
|
+
) < 0 ? next : current;
|
|
6805
|
+
}
|
|
6806
|
+
async function buildProjectPullRequestIssueLookup(ctx, scope) {
|
|
6807
|
+
const cacheKey = `${buildProjectPullRequestScopeCacheKey({
|
|
6808
|
+
companyId: scope.companyId,
|
|
6809
|
+
projectId: scope.projectId,
|
|
6810
|
+
repositoryUrl: scope.repository.url
|
|
6811
|
+
})}:issue-lookup`;
|
|
6812
|
+
const cachedLookup = getFreshCacheValue(activeProjectPullRequestIssueLookupCache, cacheKey);
|
|
6813
|
+
if (cachedLookup) {
|
|
6814
|
+
return cachedLookup;
|
|
6815
|
+
}
|
|
6816
|
+
const [projectIssues, issueLinks, pullRequestLinks] = await Promise.all([
|
|
6817
|
+
listPaperclipIssuesForProject(ctx, scope.companyId, scope.projectId),
|
|
6818
|
+
listGitHubIssueLinkRecords(ctx),
|
|
6819
|
+
listGitHubPullRequestLinkRecords(ctx)
|
|
6820
|
+
]);
|
|
6821
|
+
const issuesById = new Map(projectIssues.map((issue) => [issue.id, issue]));
|
|
6822
|
+
const normalizedRepositoryUrl = scope.repository.url;
|
|
6823
|
+
const linkedIssuesByGitHubIssueUrl = /* @__PURE__ */ new Map();
|
|
6824
|
+
for (const record of issueLinks) {
|
|
6825
|
+
if (record.data.repositoryUrl !== normalizedRepositoryUrl) {
|
|
6826
|
+
continue;
|
|
6827
|
+
}
|
|
6828
|
+
if (record.data.companyId && record.data.companyId !== scope.companyId) {
|
|
6829
|
+
continue;
|
|
6830
|
+
}
|
|
6831
|
+
if (record.data.paperclipProjectId && record.data.paperclipProjectId !== scope.projectId) {
|
|
6832
|
+
continue;
|
|
6833
|
+
}
|
|
6834
|
+
const linkedIssue = issuesById.get(record.paperclipIssueId);
|
|
6835
|
+
linkedIssuesByGitHubIssueUrl.set(
|
|
6836
|
+
record.data.githubIssueUrl,
|
|
6837
|
+
selectImportedPaperclipIssueReference(
|
|
6838
|
+
linkedIssuesByGitHubIssueUrl.get(record.data.githubIssueUrl),
|
|
6839
|
+
{
|
|
6840
|
+
paperclipIssueId: record.paperclipIssueId,
|
|
6841
|
+
...linkedIssue?.identifier ? { paperclipIssueKey: linkedIssue.identifier } : {},
|
|
6842
|
+
createdAt: record.createdAt
|
|
6843
|
+
}
|
|
6844
|
+
)
|
|
6845
|
+
);
|
|
6846
|
+
}
|
|
6847
|
+
for (const issue of projectIssues) {
|
|
6848
|
+
const githubIssueUrl = extractImportedGitHubIssueUrlFromDescription(issue.description);
|
|
6849
|
+
if (!githubIssueUrl) {
|
|
6850
|
+
continue;
|
|
6851
|
+
}
|
|
6852
|
+
linkedIssuesByGitHubIssueUrl.set(
|
|
6853
|
+
githubIssueUrl,
|
|
6854
|
+
selectImportedPaperclipIssueReference(
|
|
6855
|
+
linkedIssuesByGitHubIssueUrl.get(githubIssueUrl),
|
|
6856
|
+
{
|
|
6857
|
+
paperclipIssueId: issue.id,
|
|
6858
|
+
...issue.identifier ? { paperclipIssueKey: issue.identifier } : {},
|
|
6859
|
+
createdAt: issue.createdAt
|
|
6860
|
+
}
|
|
6861
|
+
)
|
|
6862
|
+
);
|
|
6863
|
+
}
|
|
6864
|
+
const fallbackIssuesByPullRequestNumber = /* @__PURE__ */ new Map();
|
|
6865
|
+
const sortedLinks = [...pullRequestLinks].sort((left, right) => {
|
|
6866
|
+
const rightTimestamp = Date.parse(right.updatedAt ?? right.createdAt ?? "");
|
|
6867
|
+
const leftTimestamp = Date.parse(left.updatedAt ?? left.createdAt ?? "");
|
|
6868
|
+
const safeRightTimestamp = Number.isFinite(rightTimestamp) ? rightTimestamp : 0;
|
|
6869
|
+
const safeLeftTimestamp = Number.isFinite(leftTimestamp) ? leftTimestamp : 0;
|
|
6870
|
+
return safeRightTimestamp - safeLeftTimestamp;
|
|
6871
|
+
});
|
|
6872
|
+
for (const record of sortedLinks) {
|
|
6873
|
+
if (record.data.repositoryUrl !== normalizedRepositoryUrl) {
|
|
6874
|
+
continue;
|
|
6875
|
+
}
|
|
6876
|
+
if (record.data.companyId && record.data.companyId !== scope.companyId) {
|
|
6877
|
+
continue;
|
|
6878
|
+
}
|
|
6879
|
+
if (record.data.paperclipProjectId && record.data.paperclipProjectId !== scope.projectId) {
|
|
6880
|
+
continue;
|
|
6881
|
+
}
|
|
6882
|
+
if (fallbackIssuesByPullRequestNumber.has(record.data.githubPullRequestNumber)) {
|
|
6883
|
+
continue;
|
|
6884
|
+
}
|
|
6885
|
+
const linkedIssue = issuesById.get(record.paperclipIssueId);
|
|
6886
|
+
fallbackIssuesByPullRequestNumber.set(record.data.githubPullRequestNumber, {
|
|
6887
|
+
paperclipIssueId: record.paperclipIssueId,
|
|
6888
|
+
...linkedIssue?.identifier ? { paperclipIssueKey: linkedIssue.identifier } : {}
|
|
6889
|
+
});
|
|
6890
|
+
}
|
|
6891
|
+
return setCacheValue(
|
|
6892
|
+
activeProjectPullRequestIssueLookupCache,
|
|
6893
|
+
cacheKey,
|
|
6894
|
+
{
|
|
6895
|
+
linkedIssuesByGitHubIssueUrl: new Map(
|
|
6896
|
+
[...linkedIssuesByGitHubIssueUrl.entries()].map(([githubIssueUrl, value]) => [
|
|
6897
|
+
githubIssueUrl,
|
|
6898
|
+
{
|
|
6899
|
+
paperclipIssueId: value.paperclipIssueId,
|
|
6900
|
+
...value.paperclipIssueKey ? { paperclipIssueKey: value.paperclipIssueKey } : {}
|
|
6901
|
+
}
|
|
6902
|
+
])
|
|
6903
|
+
),
|
|
6904
|
+
fallbackIssuesByPullRequestNumber
|
|
6905
|
+
},
|
|
6906
|
+
PROJECT_PULL_REQUEST_ISSUE_LOOKUP_CACHE_TTL_MS
|
|
6907
|
+
);
|
|
6908
|
+
}
|
|
6909
|
+
function resolveLinkedPaperclipIssueForPullRequest(pullRequestNumber, closingIssues, issueLookup) {
|
|
6910
|
+
for (const closingIssue of closingIssues) {
|
|
6911
|
+
const linkedIssue = issueLookup.linkedIssuesByGitHubIssueUrl.get(closingIssue.url);
|
|
6912
|
+
if (linkedIssue) {
|
|
6913
|
+
return linkedIssue;
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6916
|
+
return issueLookup.fallbackIssuesByPullRequestNumber.get(pullRequestNumber);
|
|
6917
|
+
}
|
|
6918
|
+
function getProjectPullRequestStatus(state) {
|
|
6919
|
+
switch (state) {
|
|
6920
|
+
case "MERGED":
|
|
6921
|
+
return "merged";
|
|
6922
|
+
case "CLOSED":
|
|
6923
|
+
return "closed";
|
|
6924
|
+
default:
|
|
6925
|
+
return "open";
|
|
6926
|
+
}
|
|
6927
|
+
}
|
|
6928
|
+
async function buildProjectPullRequestSummaryRecord(octokit, repository, node, issueLookup, pullRequestStatusCache) {
|
|
6929
|
+
if (!node || typeof node.number !== "number" || !node.url || !node.title?.trim()) {
|
|
6930
|
+
return null;
|
|
6931
|
+
}
|
|
6932
|
+
const inlineReviewThreadSummary = node.reviewThreads?.pageInfo?.hasNextPage ? null : summarizeProjectPullRequestReviewThreadsFromConnection(node.reviewThreads);
|
|
6933
|
+
const inlineReviewSummary = node.reviews?.pageInfo?.hasNextPage ? null : summarizeGitHubPullRequestReviewNodes(node.reviews?.nodes);
|
|
6934
|
+
const inlineCiState = tryBuildGitHubPullRequestCiStateFromBatchNode({
|
|
6935
|
+
statusCheckRollup: node.statusCheckRollup
|
|
6936
|
+
});
|
|
6937
|
+
const [reviewThreadSummary, reviewSummary, statusSnapshot, behindBy] = await Promise.all([
|
|
6938
|
+
getOrLoadCachedGitHubPullRequestReviewThreadSummary(
|
|
6939
|
+
octokit,
|
|
6940
|
+
repository,
|
|
6941
|
+
node.number,
|
|
6942
|
+
inlineReviewThreadSummary
|
|
6943
|
+
),
|
|
6944
|
+
getOrLoadCachedGitHubPullRequestReviewSummary(
|
|
6945
|
+
octokit,
|
|
6946
|
+
repository,
|
|
6947
|
+
node.number,
|
|
6948
|
+
inlineReviewSummary
|
|
6949
|
+
),
|
|
6950
|
+
getGitHubPullRequestStatusSnapshot(octokit, repository, node.number, pullRequestStatusCache, {
|
|
6951
|
+
reviewThreadSummary: inlineReviewThreadSummary,
|
|
6952
|
+
ciState: inlineCiState
|
|
6953
|
+
}),
|
|
6954
|
+
getGitHubPullRequestBehindCount(octokit, repository, {
|
|
6955
|
+
baseBranch: node.baseRefName,
|
|
6956
|
+
headBranch: node.headRefName,
|
|
6957
|
+
headRepositoryOwner: node.headRepositoryOwner?.login
|
|
6958
|
+
})
|
|
6959
|
+
]);
|
|
6960
|
+
const closingIssues = normalizeProjectPullRequestClosingIssues(repository, node.closingIssuesReferences?.nodes);
|
|
6961
|
+
const linkedIssue = resolveLinkedPaperclipIssueForPullRequest(node.number, closingIssues, issueLookup);
|
|
6962
|
+
const author = buildProjectPullRequestPerson({
|
|
6963
|
+
login: node.author?.login,
|
|
6964
|
+
url: node.author?.url,
|
|
6965
|
+
avatarUrl: node.author?.avatarUrl
|
|
6966
|
+
});
|
|
6967
|
+
const checksStatus = statusSnapshot.ciState === "green" ? "passed" : statusSnapshot.ciState === "red" ? "failed" : "pending";
|
|
6968
|
+
const githubMergeable = node.mergeable === "MERGEABLE";
|
|
6969
|
+
const reviewable = resolveProjectPullRequestReviewable({
|
|
6970
|
+
checksStatus,
|
|
6971
|
+
copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
|
|
6972
|
+
githubMergeable
|
|
6973
|
+
});
|
|
6974
|
+
const mergeable = resolveProjectPullRequestMergeable({
|
|
6975
|
+
checksStatus,
|
|
6976
|
+
reviewApprovals: reviewSummary.approvals,
|
|
6977
|
+
unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
|
|
6978
|
+
githubMergeable
|
|
6979
|
+
});
|
|
6980
|
+
const upToDateStatus = resolveProjectPullRequestUpToDateStatus({
|
|
6981
|
+
mergeStateStatus: node.mergeStateStatus,
|
|
6982
|
+
mergeable: node.mergeable,
|
|
6983
|
+
behindBy
|
|
6984
|
+
});
|
|
6985
|
+
return {
|
|
6986
|
+
id: node.id ?? `github-pull-request-${repository.owner}-${repository.repo}-${node.number}`,
|
|
6987
|
+
number: node.number,
|
|
6988
|
+
title: node.title.trim(),
|
|
6989
|
+
labels: normalizeProjectPullRequestLabels(node.labels?.nodes),
|
|
6990
|
+
author,
|
|
6991
|
+
assignees: [],
|
|
6992
|
+
checksStatus,
|
|
6993
|
+
upToDateStatus,
|
|
6994
|
+
githubMergeable,
|
|
6995
|
+
reviewable,
|
|
6996
|
+
reviewApprovals: reviewSummary.approvals,
|
|
6997
|
+
reviewChangesRequested: reviewSummary.changesRequested,
|
|
6998
|
+
reviewCommentCount: 0,
|
|
6999
|
+
unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
|
|
7000
|
+
copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
|
|
7001
|
+
commentsCount: typeof node.comments?.totalCount === "number" && node.comments.totalCount >= 0 ? Math.floor(node.comments.totalCount) : 0,
|
|
7002
|
+
createdAt: node.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
7003
|
+
updatedAt: node.updatedAt ?? node.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
7004
|
+
...linkedIssue?.paperclipIssueId ? { paperclipIssueId: linkedIssue.paperclipIssueId } : {},
|
|
7005
|
+
...linkedIssue?.paperclipIssueKey ? { paperclipIssueKey: linkedIssue.paperclipIssueKey } : {},
|
|
7006
|
+
mergeable,
|
|
7007
|
+
status: getProjectPullRequestStatus(node.state),
|
|
7008
|
+
githubUrl: node.url,
|
|
7009
|
+
checksUrl: `${node.url}/checks`,
|
|
7010
|
+
reviewsUrl: `${node.url}/files`,
|
|
7011
|
+
reviewThreadsUrl: `${node.url}/files`,
|
|
7012
|
+
commentsUrl: node.url,
|
|
7013
|
+
baseBranch: node.baseRefName ?? "",
|
|
7014
|
+
headBranch: node.headRefName ?? "",
|
|
7015
|
+
commits: typeof node.commits?.totalCount === "number" && node.commits.totalCount >= 0 ? Math.floor(node.commits.totalCount) : 0,
|
|
7016
|
+
changedFiles: typeof node.changedFiles === "number" && node.changedFiles >= 0 ? Math.floor(node.changedFiles) : 0
|
|
7017
|
+
};
|
|
7018
|
+
}
|
|
7019
|
+
async function listProjectPullRequestSummaryRecords(ctx, octokit, scope, options) {
|
|
7020
|
+
const issueLookup = await buildProjectPullRequestIssueLookup(ctx, scope);
|
|
7021
|
+
const pullRequestStatusCache = /* @__PURE__ */ new Map();
|
|
7022
|
+
const pullRequests = [];
|
|
7023
|
+
const first = Math.max(1, Math.floor(options?.first ?? PROJECT_PULL_REQUEST_PAGE_SIZE));
|
|
7024
|
+
let after = typeof options?.after === "string" && options.after.trim() ? options.after.trim() : void 0;
|
|
7025
|
+
let totalOpenPullRequests = 0;
|
|
7026
|
+
let defaultBranchName;
|
|
7027
|
+
let hasNextPage = false;
|
|
7028
|
+
let nextCursor;
|
|
7029
|
+
do {
|
|
7030
|
+
const response = await octokit.graphql(
|
|
7031
|
+
GITHUB_PROJECT_PULL_REQUESTS_QUERY,
|
|
7032
|
+
{
|
|
7033
|
+
owner: scope.repository.owner,
|
|
7034
|
+
repo: scope.repository.repo,
|
|
7035
|
+
first,
|
|
7036
|
+
after
|
|
7037
|
+
}
|
|
7038
|
+
);
|
|
7039
|
+
const connection = response.repository?.pullRequests;
|
|
7040
|
+
if (typeof connection?.totalCount === "number" && connection.totalCount >= 0) {
|
|
7041
|
+
totalOpenPullRequests = Math.floor(connection.totalCount);
|
|
7042
|
+
}
|
|
7043
|
+
defaultBranchName ??= normalizeOptionalString2(response.repository?.defaultBranchRef?.name);
|
|
7044
|
+
const pageNodes = (connection?.nodes ?? []).filter((node) => node !== null);
|
|
7045
|
+
const pageRecords = await mapWithConcurrency(
|
|
7046
|
+
pageNodes,
|
|
7047
|
+
PROJECT_PULL_REQUEST_SUMMARY_CONCURRENCY,
|
|
7048
|
+
async (node) => buildProjectPullRequestSummaryRecord(
|
|
7049
|
+
octokit,
|
|
7050
|
+
scope.repository,
|
|
7051
|
+
node,
|
|
7052
|
+
issueLookup,
|
|
7053
|
+
pullRequestStatusCache
|
|
7054
|
+
)
|
|
7055
|
+
);
|
|
7056
|
+
pullRequests.push(...pageRecords.filter((record) => Boolean(record)));
|
|
7057
|
+
nextCursor = getPageCursor(connection?.pageInfo);
|
|
7058
|
+
hasNextPage = Boolean(connection?.pageInfo?.hasNextPage && nextCursor);
|
|
7059
|
+
if (!options?.collectAll) {
|
|
7060
|
+
break;
|
|
7061
|
+
}
|
|
7062
|
+
after = nextCursor;
|
|
7063
|
+
} while (after);
|
|
7064
|
+
return {
|
|
7065
|
+
pullRequests: sortProjectPullRequestRecordsByUpdatedAt(pullRequests),
|
|
7066
|
+
totalOpenPullRequests,
|
|
7067
|
+
...defaultBranchName ? { defaultBranchName } : {},
|
|
7068
|
+
hasNextPage,
|
|
7069
|
+
...nextCursor ? { nextCursor } : {}
|
|
7070
|
+
};
|
|
7071
|
+
}
|
|
7072
|
+
async function buildProjectPullRequestMetricCounts(octokit, repository, node, pullRequestStatusCache) {
|
|
7073
|
+
if (!node || typeof node.number !== "number") {
|
|
7074
|
+
return {
|
|
7075
|
+
pullRequestNumber: null,
|
|
7076
|
+
mergeablePullRequests: 0,
|
|
7077
|
+
reviewablePullRequests: 0,
|
|
7078
|
+
failingPullRequests: 0
|
|
7079
|
+
};
|
|
7080
|
+
}
|
|
7081
|
+
const inlineReviewThreadSummary = node.reviewThreads?.pageInfo?.hasNextPage ? null : summarizeProjectPullRequestReviewThreadsFromConnection(node.reviewThreads);
|
|
7082
|
+
const inlineReviewSummary = node.reviews?.pageInfo?.hasNextPage ? null : summarizeGitHubPullRequestReviewNodes(node.reviews?.nodes);
|
|
7083
|
+
const inlineCiState = tryBuildGitHubPullRequestCiStateFromBatchNode({
|
|
7084
|
+
statusCheckRollup: node.statusCheckRollup
|
|
7085
|
+
});
|
|
7086
|
+
const [reviewThreadSummary, reviewSummary, statusSnapshot] = await Promise.all([
|
|
7087
|
+
getOrLoadCachedGitHubPullRequestReviewThreadSummary(
|
|
7088
|
+
octokit,
|
|
7089
|
+
repository,
|
|
7090
|
+
node.number,
|
|
7091
|
+
inlineReviewThreadSummary
|
|
7092
|
+
),
|
|
7093
|
+
getOrLoadCachedGitHubPullRequestReviewSummary(
|
|
7094
|
+
octokit,
|
|
7095
|
+
repository,
|
|
7096
|
+
node.number,
|
|
7097
|
+
inlineReviewSummary
|
|
7098
|
+
),
|
|
7099
|
+
getGitHubPullRequestStatusSnapshot(octokit, repository, node.number, pullRequestStatusCache, {
|
|
7100
|
+
reviewThreadSummary: inlineReviewThreadSummary,
|
|
7101
|
+
ciState: inlineCiState
|
|
7102
|
+
})
|
|
7103
|
+
]);
|
|
7104
|
+
const checksStatus = statusSnapshot.ciState === "green" ? "passed" : statusSnapshot.ciState === "red" ? "failed" : "pending";
|
|
7105
|
+
const githubMergeable = node.mergeable === "MERGEABLE";
|
|
7106
|
+
const reviewable = resolveProjectPullRequestReviewable({
|
|
7107
|
+
checksStatus,
|
|
7108
|
+
copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
|
|
7109
|
+
githubMergeable
|
|
7110
|
+
});
|
|
7111
|
+
const mergeable = resolveProjectPullRequestMergeable({
|
|
7112
|
+
checksStatus,
|
|
7113
|
+
reviewApprovals: reviewSummary.approvals,
|
|
7114
|
+
unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
|
|
7115
|
+
githubMergeable
|
|
7116
|
+
});
|
|
7117
|
+
return {
|
|
7118
|
+
pullRequestNumber: Math.floor(node.number),
|
|
7119
|
+
mergeablePullRequests: mergeable ? 1 : 0,
|
|
7120
|
+
reviewablePullRequests: reviewable ? 1 : 0,
|
|
7121
|
+
failingPullRequests: checksStatus === "failed" ? 1 : 0
|
|
7122
|
+
};
|
|
7123
|
+
}
|
|
7124
|
+
async function listProjectPullRequestCount(octokit, scope) {
|
|
7125
|
+
const response = await octokit.graphql(
|
|
7126
|
+
GITHUB_PROJECT_OPEN_PULL_REQUEST_COUNT_QUERY,
|
|
7127
|
+
{
|
|
7128
|
+
owner: scope.repository.owner,
|
|
7129
|
+
repo: scope.repository.repo
|
|
7130
|
+
}
|
|
7131
|
+
);
|
|
7132
|
+
const totalCount = response.repository?.pullRequests?.totalCount;
|
|
7133
|
+
return typeof totalCount === "number" && totalCount >= 0 ? Math.floor(totalCount) : 0;
|
|
7134
|
+
}
|
|
7135
|
+
async function listProjectPullRequestMetrics(octokit, scope) {
|
|
7136
|
+
const pullRequestStatusCache = /* @__PURE__ */ new Map();
|
|
7137
|
+
let totalOpenPullRequests = 0;
|
|
7138
|
+
let defaultBranchName;
|
|
7139
|
+
let mergeablePullRequests = 0;
|
|
7140
|
+
let reviewablePullRequests = 0;
|
|
7141
|
+
let failingPullRequests = 0;
|
|
7142
|
+
const mergeablePullRequestNumbers = [];
|
|
7143
|
+
const reviewablePullRequestNumbers = [];
|
|
7144
|
+
const failingPullRequestNumbers = [];
|
|
7145
|
+
let after;
|
|
7146
|
+
do {
|
|
7147
|
+
const response = await octokit.graphql(
|
|
7148
|
+
GITHUB_PROJECT_PULL_REQUEST_METRICS_QUERY,
|
|
7149
|
+
{
|
|
7150
|
+
owner: scope.repository.owner,
|
|
7151
|
+
repo: scope.repository.repo,
|
|
7152
|
+
first: PROJECT_PULL_REQUEST_METRICS_BATCH_SIZE,
|
|
7153
|
+
after
|
|
7154
|
+
}
|
|
7155
|
+
);
|
|
7156
|
+
const connection = response.repository?.pullRequests;
|
|
7157
|
+
if (typeof connection?.totalCount === "number" && connection.totalCount >= 0) {
|
|
7158
|
+
totalOpenPullRequests = Math.floor(connection.totalCount);
|
|
7159
|
+
}
|
|
7160
|
+
defaultBranchName ??= normalizeOptionalString2(response.repository?.defaultBranchRef?.name);
|
|
7161
|
+
const pageNodes = (connection?.nodes ?? []).filter((node) => node !== null);
|
|
7162
|
+
const pageMetrics = await mapWithConcurrency(
|
|
7163
|
+
pageNodes,
|
|
7164
|
+
PROJECT_PULL_REQUEST_SUMMARY_CONCURRENCY,
|
|
7165
|
+
async (node) => buildProjectPullRequestMetricCounts(octokit, scope.repository, node, pullRequestStatusCache)
|
|
7166
|
+
);
|
|
7167
|
+
for (const pageMetric of pageMetrics) {
|
|
7168
|
+
mergeablePullRequests += pageMetric.mergeablePullRequests;
|
|
7169
|
+
reviewablePullRequests += pageMetric.reviewablePullRequests;
|
|
7170
|
+
failingPullRequests += pageMetric.failingPullRequests;
|
|
7171
|
+
if (pageMetric.pullRequestNumber !== null && pageMetric.mergeablePullRequests > 0) {
|
|
7172
|
+
mergeablePullRequestNumbers.push(pageMetric.pullRequestNumber);
|
|
7173
|
+
}
|
|
7174
|
+
if (pageMetric.pullRequestNumber !== null && pageMetric.reviewablePullRequests > 0) {
|
|
7175
|
+
reviewablePullRequestNumbers.push(pageMetric.pullRequestNumber);
|
|
7176
|
+
}
|
|
7177
|
+
if (pageMetric.pullRequestNumber !== null && pageMetric.failingPullRequests > 0) {
|
|
7178
|
+
failingPullRequestNumbers.push(pageMetric.pullRequestNumber);
|
|
7179
|
+
}
|
|
7180
|
+
}
|
|
7181
|
+
after = getPageCursor(connection?.pageInfo);
|
|
7182
|
+
} while (after);
|
|
7183
|
+
return {
|
|
7184
|
+
totalOpenPullRequests,
|
|
7185
|
+
...defaultBranchName ? { defaultBranchName } : {},
|
|
7186
|
+
mergeablePullRequests,
|
|
7187
|
+
reviewablePullRequests,
|
|
7188
|
+
failingPullRequests,
|
|
7189
|
+
mergeablePullRequestNumbers,
|
|
7190
|
+
reviewablePullRequestNumbers,
|
|
7191
|
+
failingPullRequestNumbers
|
|
7192
|
+
};
|
|
7193
|
+
}
|
|
7194
|
+
function buildProjectPullRequestMetricsCacheKey(scope) {
|
|
7195
|
+
return buildRepositoryPullRequestCollectionCacheKey(scope.repository, "metrics");
|
|
7196
|
+
}
|
|
7197
|
+
function buildProjectPullRequestSummaryCacheKey(scope) {
|
|
7198
|
+
return `${buildProjectPullRequestScopeCacheKey({
|
|
7199
|
+
companyId: scope.companyId,
|
|
7200
|
+
projectId: scope.projectId,
|
|
7201
|
+
repositoryUrl: scope.repository.url
|
|
7202
|
+
})}:summary`;
|
|
7203
|
+
}
|
|
7204
|
+
function buildProjectPullRequestCountCacheKey(scope) {
|
|
7205
|
+
return buildRepositoryPullRequestCollectionCacheKey(scope.repository, "count");
|
|
7206
|
+
}
|
|
7207
|
+
function getCachedProjectPullRequestSummarySeed(scope) {
|
|
7208
|
+
const cachedPage = getFreshCacheValue(
|
|
7209
|
+
activeProjectPullRequestPageCache,
|
|
7210
|
+
buildProjectPullRequestPageCacheKey(scope, "all", 0)
|
|
7211
|
+
);
|
|
7212
|
+
if (!cachedPage || cachedPage.status !== "ready") {
|
|
7213
|
+
return null;
|
|
7214
|
+
}
|
|
7215
|
+
const totalOpenPullRequests = typeof cachedPage.totalOpenPullRequests === "number" && cachedPage.totalOpenPullRequests >= 0 ? Math.floor(cachedPage.totalOpenPullRequests) : null;
|
|
7216
|
+
const pullRequests = Array.isArray(cachedPage.pullRequests) ? cachedPage.pullRequests.filter(
|
|
7217
|
+
(record) => Boolean(record) && typeof record === "object" && !Array.isArray(record)
|
|
7218
|
+
) : null;
|
|
7219
|
+
if (totalOpenPullRequests === null || !pullRequests) {
|
|
7220
|
+
return null;
|
|
7221
|
+
}
|
|
7222
|
+
const hasNextPage = cachedPage.hasNextPage === true;
|
|
7223
|
+
const nextCursor = typeof cachedPage.nextCursor === "string" && cachedPage.nextCursor.trim() ? cachedPage.nextCursor.trim() : void 0;
|
|
7224
|
+
if (hasNextPage && !nextCursor) {
|
|
7225
|
+
return null;
|
|
7226
|
+
}
|
|
7227
|
+
const defaultBranchName = normalizeOptionalString2(cachedPage.defaultBranchName);
|
|
7228
|
+
return {
|
|
7229
|
+
totalOpenPullRequests,
|
|
7230
|
+
...defaultBranchName ? { defaultBranchName } : {},
|
|
7231
|
+
pullRequests: sortProjectPullRequestRecordsByUpdatedAt(pullRequests),
|
|
7232
|
+
hasNextPage,
|
|
7233
|
+
...nextCursor ? { nextCursor } : {}
|
|
7234
|
+
};
|
|
7235
|
+
}
|
|
7236
|
+
function cacheProjectPullRequestSummary(scope, summary) {
|
|
7237
|
+
const pullRequests = sortProjectPullRequestRecordsByUpdatedAt(summary.pullRequests);
|
|
7238
|
+
const metrics = buildProjectPullRequestMetrics(
|
|
7239
|
+
pullRequests,
|
|
7240
|
+
summary.totalOpenPullRequests,
|
|
7241
|
+
summary.defaultBranchName
|
|
7242
|
+
);
|
|
7243
|
+
cacheProjectPullRequestSummaryRecords(scope, pullRequests);
|
|
7244
|
+
cacheProjectPullRequestMetricsEntry(scope, metrics);
|
|
7245
|
+
return setCacheValue(
|
|
7246
|
+
activeProjectPullRequestSummaryCache,
|
|
7247
|
+
buildProjectPullRequestSummaryCacheKey(scope),
|
|
7248
|
+
{
|
|
7249
|
+
totalOpenPullRequests: summary.totalOpenPullRequests,
|
|
7250
|
+
...summary.defaultBranchName ? { defaultBranchName: summary.defaultBranchName } : {},
|
|
7251
|
+
pullRequests,
|
|
7252
|
+
metrics
|
|
7253
|
+
},
|
|
7254
|
+
PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS
|
|
7255
|
+
);
|
|
7256
|
+
}
|
|
7257
|
+
function cacheProjectPullRequestCount(scope, totalOpenPullRequests, ttlMs = PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS) {
|
|
7258
|
+
return setCacheValue(
|
|
7259
|
+
activeProjectPullRequestCountCache,
|
|
7260
|
+
buildProjectPullRequestCountCacheKey(scope),
|
|
7261
|
+
Math.max(0, Math.floor(totalOpenPullRequests)),
|
|
7262
|
+
ttlMs
|
|
7263
|
+
);
|
|
7264
|
+
}
|
|
7265
|
+
function cacheProjectPullRequestMetricsEntry(scope, metrics, ttlMs = PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS) {
|
|
7266
|
+
cacheProjectPullRequestCount(scope, metrics.totalOpenPullRequests, ttlMs);
|
|
7267
|
+
return setCacheValue(
|
|
7268
|
+
activeProjectPullRequestMetricsCache,
|
|
7269
|
+
buildProjectPullRequestMetricsCacheKey(scope),
|
|
7270
|
+
metrics,
|
|
7271
|
+
ttlMs
|
|
7272
|
+
);
|
|
7273
|
+
}
|
|
7274
|
+
async function getOrLoadCachedProjectPullRequestMetricsEntry(ctx, scope, octokit) {
|
|
7275
|
+
const summaryCacheKey = buildProjectPullRequestSummaryCacheKey(scope);
|
|
7276
|
+
const cachedSummary = getFreshCacheValue(activeProjectPullRequestSummaryCache, summaryCacheKey);
|
|
7277
|
+
if (cachedSummary) {
|
|
7278
|
+
return cachedSummary.metrics;
|
|
7279
|
+
}
|
|
7280
|
+
const metricsCacheKey = buildProjectPullRequestMetricsCacheKey(scope);
|
|
7281
|
+
const cachedMetrics = getFreshCacheValue(activeProjectPullRequestMetricsCache, metricsCacheKey);
|
|
7282
|
+
if (cachedMetrics) {
|
|
7283
|
+
return cachedMetrics;
|
|
7284
|
+
}
|
|
7285
|
+
const inFlightMetrics = activeProjectPullRequestMetricsPromiseCache.get(metricsCacheKey);
|
|
7286
|
+
if (inFlightMetrics) {
|
|
7287
|
+
return inFlightMetrics;
|
|
7288
|
+
}
|
|
7289
|
+
const loadMetricsPromise = (async () => {
|
|
7290
|
+
const resolvedOctokit = octokit ?? await createGitHubToolOctokit(ctx);
|
|
7291
|
+
const metrics = await listProjectPullRequestMetrics(resolvedOctokit, scope);
|
|
7292
|
+
return cacheProjectPullRequestMetricsEntry(scope, metrics);
|
|
7293
|
+
})();
|
|
7294
|
+
activeProjectPullRequestMetricsPromiseCache.set(metricsCacheKey, loadMetricsPromise);
|
|
7295
|
+
try {
|
|
7296
|
+
return await loadMetricsPromise;
|
|
7297
|
+
} finally {
|
|
7298
|
+
if (activeProjectPullRequestMetricsPromiseCache.get(metricsCacheKey) === loadMetricsPromise) {
|
|
7299
|
+
activeProjectPullRequestMetricsPromiseCache.delete(metricsCacheKey);
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
7302
|
+
}
|
|
7303
|
+
async function getOrLoadCachedProjectPullRequestCount(ctx, scope, octokit) {
|
|
7304
|
+
const summaryCacheKey = buildProjectPullRequestSummaryCacheKey(scope);
|
|
7305
|
+
const cachedSummary = getFreshCacheValue(activeProjectPullRequestSummaryCache, summaryCacheKey);
|
|
7306
|
+
if (cachedSummary) {
|
|
7307
|
+
return cachedSummary.totalOpenPullRequests;
|
|
7308
|
+
}
|
|
7309
|
+
const metricsCacheKey = buildProjectPullRequestMetricsCacheKey(scope);
|
|
7310
|
+
const cachedMetrics = getFreshCacheValue(activeProjectPullRequestMetricsCache, metricsCacheKey);
|
|
7311
|
+
if (cachedMetrics) {
|
|
7312
|
+
return cachedMetrics.totalOpenPullRequests;
|
|
7313
|
+
}
|
|
7314
|
+
const countCacheKey = buildProjectPullRequestCountCacheKey(scope);
|
|
7315
|
+
const cachedCount = getFreshCacheValue(activeProjectPullRequestCountCache, countCacheKey);
|
|
7316
|
+
if (cachedCount !== null) {
|
|
7317
|
+
return cachedCount;
|
|
7318
|
+
}
|
|
7319
|
+
const cachedSummarySeed = getCachedProjectPullRequestSummarySeed(scope);
|
|
7320
|
+
if (cachedSummarySeed) {
|
|
7321
|
+
return cacheProjectPullRequestCount(scope, cachedSummarySeed.totalOpenPullRequests);
|
|
7322
|
+
}
|
|
7323
|
+
const inFlightCount = activeProjectPullRequestCountPromiseCache.get(countCacheKey);
|
|
7324
|
+
if (inFlightCount) {
|
|
7325
|
+
return inFlightCount;
|
|
7326
|
+
}
|
|
7327
|
+
const loadCountPromise = (async () => {
|
|
7328
|
+
const resolvedOctokit = octokit ?? await createGitHubToolOctokit(ctx);
|
|
7329
|
+
const totalOpenPullRequests = await listProjectPullRequestCount(resolvedOctokit, scope);
|
|
7330
|
+
return setCacheValue(
|
|
7331
|
+
activeProjectPullRequestCountCache,
|
|
7332
|
+
countCacheKey,
|
|
7333
|
+
totalOpenPullRequests,
|
|
7334
|
+
PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS
|
|
7335
|
+
);
|
|
7336
|
+
})();
|
|
7337
|
+
activeProjectPullRequestCountPromiseCache.set(countCacheKey, loadCountPromise);
|
|
7338
|
+
try {
|
|
7339
|
+
return await loadCountPromise;
|
|
7340
|
+
} finally {
|
|
7341
|
+
if (activeProjectPullRequestCountPromiseCache.get(countCacheKey) === loadCountPromise) {
|
|
7342
|
+
activeProjectPullRequestCountPromiseCache.delete(countCacheKey);
|
|
7343
|
+
}
|
|
7344
|
+
}
|
|
7345
|
+
}
|
|
7346
|
+
async function getOrLoadCachedProjectPullRequestMetrics(ctx, scope, octokit) {
|
|
7347
|
+
return getPublicProjectPullRequestMetrics(
|
|
7348
|
+
await getOrLoadCachedProjectPullRequestMetricsEntry(ctx, scope, octokit)
|
|
7349
|
+
);
|
|
7350
|
+
}
|
|
7351
|
+
async function listProjectPullRequestSummaryRecordsByNumbers(ctx, octokit, scope, pullRequestNumbers) {
|
|
7352
|
+
const normalizedPullRequestNumbers = [
|
|
7353
|
+
...new Set(
|
|
7354
|
+
pullRequestNumbers.map((value) => Math.floor(value)).filter((value) => Number.isFinite(value) && value > 0)
|
|
7355
|
+
)
|
|
7356
|
+
];
|
|
7357
|
+
if (normalizedPullRequestNumbers.length === 0) {
|
|
7358
|
+
return [];
|
|
7359
|
+
}
|
|
7360
|
+
const query = buildGitHubProjectPullRequestsByNumberQuery(normalizedPullRequestNumbers);
|
|
7361
|
+
const response = await octokit.graphql(
|
|
7362
|
+
query,
|
|
7363
|
+
{
|
|
7364
|
+
owner: scope.repository.owner,
|
|
7365
|
+
repo: scope.repository.repo
|
|
7366
|
+
}
|
|
7367
|
+
);
|
|
7368
|
+
const repository = response.repository && typeof response.repository === "object" ? response.repository : {};
|
|
7369
|
+
const issueLookup = await buildProjectPullRequestIssueLookup(ctx, scope);
|
|
7370
|
+
const pullRequestStatusCache = /* @__PURE__ */ new Map();
|
|
7371
|
+
const recordsByNumber = /* @__PURE__ */ new Map();
|
|
7372
|
+
const builtRecords = await mapWithConcurrency(
|
|
7373
|
+
normalizedPullRequestNumbers,
|
|
7374
|
+
PROJECT_PULL_REQUEST_SUMMARY_CONCURRENCY,
|
|
7375
|
+
async (pullRequestNumber) => {
|
|
7376
|
+
const node = repository[buildGitHubProjectPullRequestByNumberAlias(pullRequestNumber)];
|
|
7377
|
+
if (!node) {
|
|
7378
|
+
return null;
|
|
7379
|
+
}
|
|
7380
|
+
const record = await buildProjectPullRequestSummaryRecord(
|
|
7381
|
+
octokit,
|
|
7382
|
+
scope.repository,
|
|
7383
|
+
node,
|
|
7384
|
+
issueLookup,
|
|
7385
|
+
pullRequestStatusCache
|
|
7386
|
+
);
|
|
7387
|
+
if (!record) {
|
|
7388
|
+
return null;
|
|
7389
|
+
}
|
|
7390
|
+
return {
|
|
7391
|
+
pullRequestNumber,
|
|
7392
|
+
record
|
|
7393
|
+
};
|
|
7394
|
+
}
|
|
7395
|
+
);
|
|
7396
|
+
for (const builtRecord of builtRecords) {
|
|
7397
|
+
if (!builtRecord) {
|
|
7398
|
+
continue;
|
|
7399
|
+
}
|
|
7400
|
+
recordsByNumber.set(builtRecord.pullRequestNumber, builtRecord.record);
|
|
7401
|
+
}
|
|
7402
|
+
return normalizedPullRequestNumbers.map((pullRequestNumber) => recordsByNumber.get(pullRequestNumber)).filter((record) => Boolean(record));
|
|
7403
|
+
}
|
|
7404
|
+
async function getOrLoadProjectPullRequestSummaryRecordsForNumbers(ctx, scope, pullRequestNumbers, octokit) {
|
|
7405
|
+
const normalizedPullRequestNumbers = [
|
|
7406
|
+
...new Set(
|
|
7407
|
+
pullRequestNumbers.map((value) => Math.floor(value)).filter((value) => Number.isFinite(value) && value > 0)
|
|
7408
|
+
)
|
|
7409
|
+
];
|
|
7410
|
+
if (normalizedPullRequestNumbers.length === 0) {
|
|
7411
|
+
return [];
|
|
7412
|
+
}
|
|
7413
|
+
const cachedSummary = getFreshCacheValue(
|
|
7414
|
+
activeProjectPullRequestSummaryCache,
|
|
7415
|
+
buildProjectPullRequestSummaryCacheKey(scope)
|
|
7416
|
+
);
|
|
7417
|
+
const summaryRecordsByNumber = cachedSummary ? new Map(
|
|
7418
|
+
cachedSummary.pullRequests.map((pullRequest) => {
|
|
7419
|
+
const pullRequestNumber = getProjectPullRequestNumber(pullRequest);
|
|
7420
|
+
return pullRequestNumber === null ? null : [pullRequestNumber, pullRequest];
|
|
7421
|
+
}).filter((entry) => Boolean(entry))
|
|
7422
|
+
) : null;
|
|
7423
|
+
const recordsByNumber = /* @__PURE__ */ new Map();
|
|
7424
|
+
for (const pullRequestNumber of normalizedPullRequestNumbers) {
|
|
7425
|
+
const cachedSummaryRecord = summaryRecordsByNumber?.get(pullRequestNumber);
|
|
7426
|
+
if (cachedSummaryRecord) {
|
|
7427
|
+
recordsByNumber.set(pullRequestNumber, cachedSummaryRecord);
|
|
7428
|
+
continue;
|
|
7429
|
+
}
|
|
7430
|
+
const cachedRecord = getFreshCacheValue(
|
|
7431
|
+
activeProjectPullRequestSummaryRecordCache,
|
|
7432
|
+
buildProjectPullRequestSummaryRecordCacheKey(scope, pullRequestNumber)
|
|
7433
|
+
);
|
|
7434
|
+
if (cachedRecord) {
|
|
7435
|
+
recordsByNumber.set(pullRequestNumber, cachedRecord);
|
|
7436
|
+
}
|
|
7437
|
+
}
|
|
7438
|
+
const missingPullRequestNumbers = normalizedPullRequestNumbers.filter((pullRequestNumber) => !recordsByNumber.has(pullRequestNumber));
|
|
7439
|
+
if (missingPullRequestNumbers.length > 0) {
|
|
7440
|
+
const resolvedOctokit = octokit ?? await createGitHubToolOctokit(ctx);
|
|
7441
|
+
const loadedRecords = await listProjectPullRequestSummaryRecordsByNumbers(
|
|
7442
|
+
ctx,
|
|
7443
|
+
resolvedOctokit,
|
|
7444
|
+
scope,
|
|
7445
|
+
missingPullRequestNumbers
|
|
7446
|
+
);
|
|
7447
|
+
cacheProjectPullRequestSummaryRecords(scope, loadedRecords);
|
|
7448
|
+
for (const loadedRecord of loadedRecords) {
|
|
7449
|
+
const pullRequestNumber = getProjectPullRequestNumber(loadedRecord);
|
|
7450
|
+
if (pullRequestNumber === null) {
|
|
7451
|
+
continue;
|
|
7452
|
+
}
|
|
7453
|
+
recordsByNumber.set(pullRequestNumber, loadedRecord);
|
|
7454
|
+
}
|
|
7455
|
+
}
|
|
7456
|
+
return normalizedPullRequestNumbers.map((pullRequestNumber) => recordsByNumber.get(pullRequestNumber)).filter((record) => Boolean(record));
|
|
7457
|
+
}
|
|
7458
|
+
function buildPaperclipIssueDescriptionFromPullRequest(params) {
|
|
7459
|
+
const importLine = `Imported from GitHub pull request [#${params.pullRequestNumber}](${params.pullRequestUrl}) in ${formatRepositoryLabel(params.repository)}.`;
|
|
7460
|
+
const body = typeof params.body === "string" ? params.body.trim() : "";
|
|
7461
|
+
return body ? `${importLine}
|
|
7462
|
+
|
|
7463
|
+
${body}` : importLine;
|
|
7464
|
+
}
|
|
7465
|
+
async function requireProjectPullRequestScope(ctx, input, resolvedProjectMappings) {
|
|
7466
|
+
const companyId = normalizeCompanyId(input.companyId);
|
|
7467
|
+
const projectId = typeof input.projectId === "string" && input.projectId.trim() ? input.projectId.trim() : void 0;
|
|
7468
|
+
if (!companyId || !projectId) {
|
|
7469
|
+
throw new Error("A company id and project id are required to load project pull requests.");
|
|
7470
|
+
}
|
|
7471
|
+
const mappings = resolvedProjectMappings ?? await resolveProjectScopedMappings(
|
|
7472
|
+
ctx,
|
|
7473
|
+
normalizeSettings(await ctx.state.get(SETTINGS_SCOPE)).mappings,
|
|
7474
|
+
{
|
|
7475
|
+
companyId,
|
|
7476
|
+
projectId
|
|
7477
|
+
}
|
|
7478
|
+
);
|
|
7479
|
+
if (mappings.length === 0) {
|
|
7480
|
+
throw new Error("No saved GitHub repository mapping matches this Paperclip project.");
|
|
7481
|
+
}
|
|
7482
|
+
const requestedRepositoryUrl = typeof input.repositoryUrl === "string" && input.repositoryUrl.trim() ? getNormalizedMappingRepositoryUrl({
|
|
7483
|
+
repositoryUrl: input.repositoryUrl
|
|
7484
|
+
}) : void 0;
|
|
7485
|
+
const sortedMappings = [...mappings].sort(
|
|
7486
|
+
(left, right) => getNormalizedMappingRepositoryUrl(left).localeCompare(getNormalizedMappingRepositoryUrl(right))
|
|
7487
|
+
);
|
|
7488
|
+
const mapping = requestedRepositoryUrl ? sortedMappings.find((entry) => getNormalizedMappingRepositoryUrl(entry) === requestedRepositoryUrl) : sortedMappings[0];
|
|
7489
|
+
if (!mapping) {
|
|
7490
|
+
throw new Error("This Paperclip project is not mapped to the requested GitHub repository.");
|
|
7491
|
+
}
|
|
7492
|
+
return {
|
|
7493
|
+
companyId,
|
|
7494
|
+
projectId,
|
|
7495
|
+
projectLabel: mapping.paperclipProjectName.trim() || "Project",
|
|
7496
|
+
mapping,
|
|
7497
|
+
repository: requireRepositoryReference(mapping.repositoryUrl),
|
|
7498
|
+
mappingCount: sortedMappings.length
|
|
7499
|
+
};
|
|
7500
|
+
}
|
|
7501
|
+
async function buildProjectPullRequestsPageData(ctx, input) {
|
|
7502
|
+
const filter = normalizeProjectPullRequestFilter(input.filter);
|
|
7503
|
+
const pageIndex = normalizeProjectPullRequestPageIndex(input.pageIndex);
|
|
7504
|
+
const cursor = typeof input.cursor === "string" && input.cursor.trim() ? input.cursor.trim() : void 0;
|
|
7505
|
+
const companyId = normalizeCompanyId(input.companyId);
|
|
7506
|
+
const projectId = typeof input.projectId === "string" && input.projectId.trim() ? input.projectId.trim() : null;
|
|
7507
|
+
if (!companyId || !projectId) {
|
|
7508
|
+
return {
|
|
7509
|
+
status: "missing_project",
|
|
7510
|
+
projectId,
|
|
7511
|
+
projectLabel: "Project",
|
|
7512
|
+
repositoryLabel: "",
|
|
7513
|
+
repositoryUrl: "",
|
|
7514
|
+
repositoryDescription: "",
|
|
7515
|
+
filter,
|
|
7516
|
+
pageIndex: 0,
|
|
7517
|
+
pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
|
|
7518
|
+
hasNextPage: false,
|
|
7519
|
+
hasPreviousPage: false,
|
|
7520
|
+
totalFilteredPullRequests: 0,
|
|
7521
|
+
pullRequests: [],
|
|
7522
|
+
message: "Open this page from a mapped Paperclip project."
|
|
7523
|
+
};
|
|
7524
|
+
}
|
|
7525
|
+
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
7526
|
+
const projectMappings = await resolveProjectScopedMappings(ctx, settings.mappings, {
|
|
7527
|
+
companyId,
|
|
7528
|
+
projectId
|
|
7529
|
+
});
|
|
7530
|
+
if (projectMappings.length === 0) {
|
|
7531
|
+
return {
|
|
7532
|
+
status: "unmapped",
|
|
7533
|
+
projectId,
|
|
7534
|
+
projectLabel: "Project",
|
|
7535
|
+
repositoryLabel: "",
|
|
7536
|
+
repositoryUrl: "",
|
|
7537
|
+
repositoryDescription: "",
|
|
7538
|
+
filter,
|
|
7539
|
+
pageIndex: 0,
|
|
7540
|
+
pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
|
|
7541
|
+
hasNextPage: false,
|
|
7542
|
+
hasPreviousPage: false,
|
|
7543
|
+
totalFilteredPullRequests: 0,
|
|
7544
|
+
pullRequests: [],
|
|
7545
|
+
message: "No GitHub repository is mapped to this project yet."
|
|
7546
|
+
};
|
|
7547
|
+
}
|
|
7548
|
+
const scope = await requireProjectPullRequestScope(ctx, input, projectMappings);
|
|
7549
|
+
const config = await getResolvedConfig(ctx);
|
|
7550
|
+
if (!hasConfiguredGithubToken(settings, config)) {
|
|
7551
|
+
return {
|
|
7552
|
+
status: "missing_token",
|
|
7553
|
+
projectId,
|
|
7554
|
+
projectLabel: scope.projectLabel,
|
|
7555
|
+
repositoryLabel: formatRepositoryLabel(scope.repository),
|
|
7556
|
+
repositoryUrl: scope.repository.url,
|
|
7557
|
+
repositoryDescription: "",
|
|
7558
|
+
filter,
|
|
7559
|
+
pageIndex: 0,
|
|
7560
|
+
pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
|
|
7561
|
+
hasNextPage: false,
|
|
7562
|
+
hasPreviousPage: false,
|
|
7563
|
+
totalFilteredPullRequests: 0,
|
|
7564
|
+
pullRequests: [],
|
|
7565
|
+
message: "Configure a GitHub token before opening pull requests."
|
|
7566
|
+
};
|
|
7567
|
+
}
|
|
7568
|
+
try {
|
|
7569
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
7570
|
+
const pageCacheKey = buildProjectPullRequestPageCacheKey(scope, filter, pageIndex, cursor);
|
|
7571
|
+
const cachedPage = getFreshCacheValue(activeProjectPullRequestPageCache, pageCacheKey);
|
|
7572
|
+
if (cachedPage) {
|
|
7573
|
+
return cachedPage;
|
|
7574
|
+
}
|
|
7575
|
+
if (filter !== "all") {
|
|
7576
|
+
const metrics = await getOrLoadCachedProjectPullRequestMetricsEntry(ctx, scope, octokit);
|
|
7577
|
+
const filteredPullRequestNumbers = getProjectPullRequestNumbersForFilter(metrics, filter);
|
|
7578
|
+
const page = sliceProjectPullRequestNumbers(filteredPullRequestNumbers, pageIndex, PROJECT_PULL_REQUEST_PAGE_SIZE);
|
|
7579
|
+
const pullRequests = sortProjectPullRequestRecordsByUpdatedAt(
|
|
7580
|
+
await getOrLoadProjectPullRequestSummaryRecordsForNumbers(
|
|
7581
|
+
ctx,
|
|
7582
|
+
scope,
|
|
7583
|
+
page.pullRequestNumbers,
|
|
7584
|
+
octokit
|
|
7585
|
+
)
|
|
7586
|
+
);
|
|
7587
|
+
const tokenPermissionAudit2 = await getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, scope.repository, {
|
|
7588
|
+
samplePullRequestNumber: pullRequests[0] ? getProjectPullRequestNumber(pullRequests[0]) ?? void 0 : void 0
|
|
7589
|
+
});
|
|
7590
|
+
cacheProjectPullRequestCount(scope, metrics.totalOpenPullRequests);
|
|
7591
|
+
return setCacheValue(
|
|
7592
|
+
activeProjectPullRequestPageCache,
|
|
7593
|
+
pageCacheKey,
|
|
7594
|
+
{
|
|
7595
|
+
status: "ready",
|
|
7596
|
+
projectId,
|
|
7597
|
+
projectLabel: scope.projectLabel,
|
|
7598
|
+
repositoryLabel: formatRepositoryLabel(scope.repository),
|
|
7599
|
+
repositoryUrl: scope.repository.url,
|
|
7600
|
+
repositoryDescription: "",
|
|
7601
|
+
...metrics.defaultBranchName ? { defaultBranchName: metrics.defaultBranchName } : {},
|
|
7602
|
+
filter,
|
|
7603
|
+
pageIndex: page.pageIndex,
|
|
7604
|
+
pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
|
|
7605
|
+
hasNextPage: page.hasNextPage,
|
|
7606
|
+
hasPreviousPage: page.hasPreviousPage,
|
|
7607
|
+
totalFilteredPullRequests: filteredPullRequestNumbers.length,
|
|
7608
|
+
totalOpenPullRequests: metrics.totalOpenPullRequests,
|
|
7609
|
+
pullRequests,
|
|
7610
|
+
tokenPermissionAudit: tokenPermissionAudit2
|
|
7611
|
+
},
|
|
7612
|
+
PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS
|
|
7613
|
+
);
|
|
7614
|
+
}
|
|
7615
|
+
const cachedFullSummary = getFreshCacheValue(
|
|
7616
|
+
activeProjectPullRequestSummaryCache,
|
|
7617
|
+
buildProjectPullRequestSummaryCacheKey(scope)
|
|
7618
|
+
);
|
|
7619
|
+
if (cachedFullSummary) {
|
|
7620
|
+
cacheProjectPullRequestCount(scope, cachedFullSummary.totalOpenPullRequests);
|
|
7621
|
+
const page = sliceProjectPullRequestRecords(
|
|
7622
|
+
cachedFullSummary.pullRequests,
|
|
7623
|
+
pageIndex,
|
|
7624
|
+
PROJECT_PULL_REQUEST_PAGE_SIZE
|
|
7625
|
+
);
|
|
7626
|
+
const tokenPermissionAudit2 = await getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, scope.repository, {
|
|
7627
|
+
samplePullRequestNumber: page.pullRequests[0] ? getProjectPullRequestNumber(page.pullRequests[0]) ?? void 0 : void 0
|
|
7628
|
+
});
|
|
7629
|
+
return setCacheValue(
|
|
7630
|
+
activeProjectPullRequestPageCache,
|
|
7631
|
+
pageCacheKey,
|
|
7632
|
+
{
|
|
7633
|
+
status: "ready",
|
|
7634
|
+
projectId,
|
|
7635
|
+
projectLabel: scope.projectLabel,
|
|
7636
|
+
repositoryLabel: formatRepositoryLabel(scope.repository),
|
|
7637
|
+
repositoryUrl: scope.repository.url,
|
|
7638
|
+
repositoryDescription: "",
|
|
7639
|
+
...cachedFullSummary.defaultBranchName ? { defaultBranchName: cachedFullSummary.defaultBranchName } : {},
|
|
7640
|
+
filter,
|
|
7641
|
+
pageIndex: page.pageIndex,
|
|
7642
|
+
pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
|
|
7643
|
+
hasNextPage: page.hasNextPage,
|
|
7644
|
+
hasPreviousPage: page.hasPreviousPage,
|
|
7645
|
+
totalFilteredPullRequests: cachedFullSummary.totalOpenPullRequests,
|
|
7646
|
+
totalOpenPullRequests: cachedFullSummary.totalOpenPullRequests,
|
|
7647
|
+
pullRequests: page.pullRequests,
|
|
7648
|
+
tokenPermissionAudit: tokenPermissionAudit2
|
|
7649
|
+
},
|
|
7650
|
+
PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS
|
|
7651
|
+
);
|
|
7652
|
+
}
|
|
7653
|
+
const summary = await listProjectPullRequestSummaryRecords(ctx, octokit, scope, {
|
|
7654
|
+
after: cursor,
|
|
7655
|
+
first: PROJECT_PULL_REQUEST_PAGE_SIZE
|
|
7656
|
+
});
|
|
7657
|
+
cacheProjectPullRequestCount(scope, summary.totalOpenPullRequests);
|
|
7658
|
+
cacheProjectPullRequestSummaryRecords(scope, summary.pullRequests, PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS);
|
|
7659
|
+
if (pageIndex === 0 && !summary.hasNextPage) {
|
|
7660
|
+
cacheProjectPullRequestSummary(scope, {
|
|
7661
|
+
totalOpenPullRequests: summary.totalOpenPullRequests,
|
|
7662
|
+
...summary.defaultBranchName ? { defaultBranchName: summary.defaultBranchName } : {},
|
|
7663
|
+
pullRequests: summary.pullRequests
|
|
7664
|
+
});
|
|
7665
|
+
}
|
|
7666
|
+
const tokenPermissionAudit = await getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, scope.repository, {
|
|
7667
|
+
samplePullRequestNumber: summary.pullRequests[0] ? getProjectPullRequestNumber(summary.pullRequests[0]) ?? void 0 : void 0
|
|
7668
|
+
});
|
|
7669
|
+
return setCacheValue(
|
|
7670
|
+
activeProjectPullRequestPageCache,
|
|
7671
|
+
pageCacheKey,
|
|
7672
|
+
{
|
|
7673
|
+
status: "ready",
|
|
7674
|
+
projectId,
|
|
7675
|
+
projectLabel: scope.projectLabel,
|
|
7676
|
+
repositoryLabel: formatRepositoryLabel(scope.repository),
|
|
7677
|
+
repositoryUrl: scope.repository.url,
|
|
7678
|
+
repositoryDescription: "",
|
|
7679
|
+
...summary.defaultBranchName ? { defaultBranchName: summary.defaultBranchName } : {},
|
|
7680
|
+
filter,
|
|
7681
|
+
pageIndex,
|
|
7682
|
+
pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
|
|
7683
|
+
hasNextPage: summary.hasNextPage,
|
|
7684
|
+
hasPreviousPage: pageIndex > 0,
|
|
7685
|
+
totalFilteredPullRequests: summary.totalOpenPullRequests,
|
|
7686
|
+
totalOpenPullRequests: summary.totalOpenPullRequests,
|
|
7687
|
+
...summary.nextCursor ? { nextCursor: summary.nextCursor } : {},
|
|
7688
|
+
pullRequests: summary.pullRequests,
|
|
7689
|
+
tokenPermissionAudit
|
|
7690
|
+
},
|
|
7691
|
+
PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS
|
|
7692
|
+
);
|
|
7693
|
+
} catch (error) {
|
|
7694
|
+
return {
|
|
7695
|
+
status: "error",
|
|
7696
|
+
projectId,
|
|
7697
|
+
projectLabel: scope.projectLabel,
|
|
7698
|
+
repositoryLabel: formatRepositoryLabel(scope.repository),
|
|
7699
|
+
repositoryUrl: scope.repository.url,
|
|
7700
|
+
repositoryDescription: "",
|
|
7701
|
+
filter,
|
|
7702
|
+
pageIndex,
|
|
7703
|
+
pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
|
|
7704
|
+
hasNextPage: false,
|
|
7705
|
+
hasPreviousPage: pageIndex > 0,
|
|
7706
|
+
totalFilteredPullRequests: 0,
|
|
7707
|
+
pullRequests: [],
|
|
7708
|
+
message: getErrorMessage(error)
|
|
7709
|
+
};
|
|
7710
|
+
}
|
|
7711
|
+
}
|
|
7712
|
+
async function buildProjectPullRequestMetricsData(ctx, input) {
|
|
7713
|
+
const companyId = normalizeCompanyId(input.companyId);
|
|
7714
|
+
const projectId = typeof input.projectId === "string" && input.projectId.trim() ? input.projectId.trim() : null;
|
|
7715
|
+
if (!companyId || !projectId) {
|
|
7716
|
+
return {
|
|
7717
|
+
status: "missing_project",
|
|
7718
|
+
projectId,
|
|
7719
|
+
totalOpenPullRequests: 0,
|
|
7720
|
+
mergeablePullRequests: 0,
|
|
7721
|
+
reviewablePullRequests: 0,
|
|
7722
|
+
failingPullRequests: 0
|
|
7723
|
+
};
|
|
7724
|
+
}
|
|
7725
|
+
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
7726
|
+
const projectMappings = await resolveProjectScopedMappings(ctx, settings.mappings, {
|
|
7727
|
+
companyId,
|
|
7728
|
+
projectId
|
|
7729
|
+
});
|
|
7730
|
+
if (projectMappings.length === 0) {
|
|
7731
|
+
return {
|
|
7732
|
+
status: "unmapped",
|
|
7733
|
+
projectId,
|
|
7734
|
+
totalOpenPullRequests: 0,
|
|
7735
|
+
mergeablePullRequests: 0,
|
|
7736
|
+
reviewablePullRequests: 0,
|
|
7737
|
+
failingPullRequests: 0
|
|
7738
|
+
};
|
|
7739
|
+
}
|
|
7740
|
+
const config = await getResolvedConfig(ctx);
|
|
7741
|
+
if (!hasConfiguredGithubToken(settings, config)) {
|
|
7742
|
+
return {
|
|
7743
|
+
status: "missing_token",
|
|
7744
|
+
projectId,
|
|
7745
|
+
totalOpenPullRequests: 0,
|
|
7746
|
+
mergeablePullRequests: 0,
|
|
7747
|
+
reviewablePullRequests: 0,
|
|
7748
|
+
failingPullRequests: 0
|
|
7749
|
+
};
|
|
7750
|
+
}
|
|
7751
|
+
try {
|
|
7752
|
+
const scope = await requireProjectPullRequestScope(ctx, input, projectMappings);
|
|
7753
|
+
const metrics = await getOrLoadCachedProjectPullRequestMetrics(ctx, scope);
|
|
7754
|
+
return {
|
|
7755
|
+
status: "ready",
|
|
7756
|
+
projectId,
|
|
7757
|
+
...metrics
|
|
7758
|
+
};
|
|
7759
|
+
} catch (error) {
|
|
7760
|
+
return {
|
|
7761
|
+
status: "error",
|
|
7762
|
+
projectId,
|
|
7763
|
+
message: getErrorMessage(error)
|
|
7764
|
+
};
|
|
7765
|
+
}
|
|
7766
|
+
}
|
|
7767
|
+
async function buildProjectPullRequestCountData(ctx, input) {
|
|
7768
|
+
const companyId = normalizeCompanyId(input.companyId);
|
|
7769
|
+
const projectId = typeof input.projectId === "string" && input.projectId.trim() ? input.projectId.trim() : null;
|
|
7770
|
+
if (!companyId || !projectId) {
|
|
7771
|
+
return {
|
|
7772
|
+
status: "missing_project",
|
|
7773
|
+
projectId,
|
|
7774
|
+
totalOpenPullRequests: 0
|
|
7775
|
+
};
|
|
7776
|
+
}
|
|
7777
|
+
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
7778
|
+
const projectMappings = await resolveProjectScopedMappings(ctx, settings.mappings, {
|
|
7779
|
+
companyId,
|
|
7780
|
+
projectId
|
|
7781
|
+
});
|
|
7782
|
+
if (projectMappings.length === 0) {
|
|
7783
|
+
return {
|
|
7784
|
+
status: "unmapped",
|
|
7785
|
+
projectId,
|
|
7786
|
+
totalOpenPullRequests: 0
|
|
7787
|
+
};
|
|
7788
|
+
}
|
|
7789
|
+
const config = await getResolvedConfig(ctx);
|
|
7790
|
+
if (!hasConfiguredGithubToken(settings, config)) {
|
|
7791
|
+
return {
|
|
7792
|
+
status: "missing_token",
|
|
7793
|
+
projectId,
|
|
7794
|
+
totalOpenPullRequests: 0
|
|
7795
|
+
};
|
|
7796
|
+
}
|
|
7797
|
+
try {
|
|
7798
|
+
const scope = await requireProjectPullRequestScope(ctx, input, projectMappings);
|
|
7799
|
+
const totalOpenPullRequests = await getOrLoadCachedProjectPullRequestCount(ctx, scope);
|
|
7800
|
+
return {
|
|
7801
|
+
status: "ready",
|
|
7802
|
+
projectId,
|
|
7803
|
+
totalOpenPullRequests
|
|
7804
|
+
};
|
|
7805
|
+
} catch (error) {
|
|
7806
|
+
return {
|
|
7807
|
+
status: "error",
|
|
7808
|
+
projectId,
|
|
7809
|
+
totalOpenPullRequests: 0,
|
|
7810
|
+
message: getErrorMessage(error)
|
|
7811
|
+
};
|
|
7812
|
+
}
|
|
7813
|
+
}
|
|
7814
|
+
async function buildSettingsTokenPermissionAuditData(ctx, input) {
|
|
7815
|
+
const requestedCompanyId = normalizeCompanyId(input.companyId);
|
|
7816
|
+
if (!requestedCompanyId) {
|
|
7817
|
+
return {
|
|
7818
|
+
status: "ready",
|
|
7819
|
+
allRequiredPermissionsGranted: true,
|
|
7820
|
+
repositories: [],
|
|
7821
|
+
missingPermissions: [],
|
|
7822
|
+
warnings: ["Open a company to audit token permissions for its mapped repositories."]
|
|
7823
|
+
};
|
|
7824
|
+
}
|
|
7825
|
+
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
7826
|
+
const config = await getResolvedConfig(ctx);
|
|
7827
|
+
if (!hasConfiguredGithubToken(settings, config)) {
|
|
7828
|
+
return {
|
|
7829
|
+
status: "missing_token",
|
|
7830
|
+
allRequiredPermissionsGranted: false,
|
|
7831
|
+
repositories: [],
|
|
7832
|
+
missingPermissions: [],
|
|
7833
|
+
warnings: [],
|
|
7834
|
+
message: "Save a GitHub token before auditing repository permissions."
|
|
7835
|
+
};
|
|
7836
|
+
}
|
|
7837
|
+
const scopedMappings = getSyncableMappings(filterMappingsByCompany(settings.mappings, requestedCompanyId));
|
|
7838
|
+
if (scopedMappings.length === 0) {
|
|
7839
|
+
return {
|
|
7840
|
+
status: "ready",
|
|
7841
|
+
allRequiredPermissionsGranted: true,
|
|
7842
|
+
repositories: [],
|
|
7843
|
+
missingPermissions: [],
|
|
7844
|
+
warnings: ["Add at least one mapped repository in this company to audit token permissions."]
|
|
7845
|
+
};
|
|
7846
|
+
}
|
|
7847
|
+
try {
|
|
7848
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
7849
|
+
const repositories = await Promise.all(
|
|
7850
|
+
[
|
|
7851
|
+
...new Map(
|
|
7852
|
+
scopedMappings.map((mapping) => {
|
|
7853
|
+
const repository = parseRepositoryReference(mapping.repositoryUrl);
|
|
7854
|
+
return repository ? [repository.url, repository] : null;
|
|
7855
|
+
}).filter((entry) => entry !== null)
|
|
7856
|
+
).values()
|
|
7857
|
+
].map((repository) => getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, repository))
|
|
7858
|
+
);
|
|
7859
|
+
const missingPermissions = [
|
|
7860
|
+
...new Set(repositories.flatMap((repository) => repository.missingPermissions))
|
|
7861
|
+
].sort((left, right) => left.localeCompare(right));
|
|
7862
|
+
const warnings = repositories.flatMap((repository) => repository.warnings);
|
|
7863
|
+
return {
|
|
7864
|
+
status: "ready",
|
|
7865
|
+
allRequiredPermissionsGranted: repositories.length > 0 && repositories.every((repository) => repository.status === "verified"),
|
|
7866
|
+
repositories,
|
|
7867
|
+
missingPermissions,
|
|
7868
|
+
warnings
|
|
7869
|
+
};
|
|
7870
|
+
} catch (error) {
|
|
7871
|
+
return {
|
|
7872
|
+
status: "error",
|
|
7873
|
+
allRequiredPermissionsGranted: false,
|
|
7874
|
+
repositories: [],
|
|
7875
|
+
missingPermissions: [],
|
|
7876
|
+
warnings: [],
|
|
7877
|
+
message: getErrorMessage(error)
|
|
7878
|
+
};
|
|
7879
|
+
}
|
|
7880
|
+
}
|
|
7881
|
+
async function listProjectPullRequestClosingIssues(octokit, repository, pullRequestNumber) {
|
|
7882
|
+
const response = await octokit.graphql(
|
|
7883
|
+
GITHUB_PULL_REQUEST_CLOSING_ISSUES_QUERY,
|
|
7884
|
+
{
|
|
7885
|
+
owner: repository.owner,
|
|
7886
|
+
repo: repository.repo,
|
|
7887
|
+
pullRequestNumber
|
|
7888
|
+
}
|
|
7889
|
+
);
|
|
7890
|
+
return normalizeProjectPullRequestClosingIssues(
|
|
7891
|
+
repository,
|
|
7892
|
+
response.repository?.pullRequest?.closingIssuesReferences?.nodes
|
|
7893
|
+
);
|
|
7894
|
+
}
|
|
7895
|
+
function getPullRequestApiState(value) {
|
|
7896
|
+
if (value.merged === true) {
|
|
7897
|
+
return "merged";
|
|
7898
|
+
}
|
|
7899
|
+
return value.state === "closed" ? "closed" : "open";
|
|
7900
|
+
}
|
|
7901
|
+
async function buildProjectPullRequestDetailData(ctx, input) {
|
|
7902
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
7903
|
+
if (!pullRequestNumber) {
|
|
7904
|
+
return null;
|
|
7905
|
+
}
|
|
7906
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
7907
|
+
const detailCacheKey = buildProjectPullRequestDetailCacheKey(scope, pullRequestNumber);
|
|
7908
|
+
const cachedDetail = getFreshCacheValue(activeProjectPullRequestDetailCache, detailCacheKey);
|
|
7909
|
+
if (cachedDetail !== null) {
|
|
7910
|
+
return cachedDetail;
|
|
7911
|
+
}
|
|
7912
|
+
const cachedSummaryRecord = getFreshCacheValue(
|
|
7913
|
+
activeProjectPullRequestSummaryRecordCache,
|
|
7914
|
+
buildProjectPullRequestSummaryRecordCacheKey(scope, pullRequestNumber)
|
|
7915
|
+
);
|
|
7916
|
+
const cachedLinkedIssue = cachedSummaryRecord ? getLinkedPaperclipIssueFromProjectPullRequestRecord(cachedSummaryRecord) : void 0;
|
|
7917
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
7918
|
+
const response = await octokit.rest.pulls.get({
|
|
7919
|
+
owner: scope.repository.owner,
|
|
7920
|
+
repo: scope.repository.repo,
|
|
7921
|
+
pull_number: pullRequestNumber,
|
|
7922
|
+
headers: {
|
|
7923
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
7924
|
+
}
|
|
7925
|
+
});
|
|
7926
|
+
const pullRequest = response.data;
|
|
7927
|
+
const reviewSummaryPromise = getOrLoadCachedGitHubPullRequestReviewSummary(octokit, scope.repository, pullRequestNumber);
|
|
7928
|
+
const reviewThreadSummaryPromise = getOrLoadCachedGitHubPullRequestReviewThreadSummary(octokit, scope.repository, pullRequestNumber);
|
|
7929
|
+
const statusSnapshotPromise = reviewThreadSummaryPromise.then(
|
|
7930
|
+
(reviewThreadSummary2) => getGitHubPullRequestStatusSnapshot(octokit, scope.repository, pullRequestNumber, /* @__PURE__ */ new Map(), {
|
|
7931
|
+
reviewThreadSummary: reviewThreadSummary2
|
|
7932
|
+
})
|
|
7933
|
+
);
|
|
7934
|
+
const [reviewSummary, reviewThreadSummary, comments, linkedIssue, statusSnapshot] = await Promise.all([
|
|
7935
|
+
reviewSummaryPromise,
|
|
7936
|
+
reviewThreadSummaryPromise,
|
|
7937
|
+
listAllGitHubIssueComments(octokit, scope.repository, pullRequestNumber),
|
|
7938
|
+
cachedLinkedIssue ? Promise.resolve(cachedLinkedIssue) : (async () => {
|
|
7939
|
+
const issueLookup = await buildProjectPullRequestIssueLookup(ctx, scope);
|
|
7940
|
+
return resolveLinkedPaperclipIssueForPullRequest(
|
|
7941
|
+
pullRequestNumber,
|
|
7942
|
+
await listProjectPullRequestClosingIssues(octokit, scope.repository, pullRequestNumber),
|
|
7943
|
+
issueLookup
|
|
7944
|
+
);
|
|
7945
|
+
})(),
|
|
7946
|
+
statusSnapshotPromise
|
|
7947
|
+
]);
|
|
7948
|
+
const author = buildProjectPullRequestPerson({
|
|
7949
|
+
login: pullRequest.user?.login,
|
|
7950
|
+
url: pullRequest.user?.html_url,
|
|
7951
|
+
avatarUrl: pullRequest.user?.avatar_url
|
|
7952
|
+
});
|
|
7953
|
+
const timeline = [];
|
|
7954
|
+
const trimmedBody = pullRequest.body?.trim() ?? "";
|
|
7955
|
+
if (trimmedBody) {
|
|
7956
|
+
timeline.push({
|
|
7957
|
+
id: `github-pull-request-${pullRequestNumber}-description`,
|
|
7958
|
+
kind: "description",
|
|
7959
|
+
author,
|
|
7960
|
+
createdAt: pullRequest.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
7961
|
+
body: trimmedBody
|
|
7962
|
+
});
|
|
7963
|
+
}
|
|
7964
|
+
for (const comment of comments) {
|
|
7965
|
+
timeline.push({
|
|
7966
|
+
id: `github-pull-request-${pullRequestNumber}-comment-${comment.id}`,
|
|
7967
|
+
kind: "comment",
|
|
7968
|
+
author: buildProjectPullRequestPerson({
|
|
7969
|
+
login: comment.authorLogin,
|
|
7970
|
+
url: comment.authorUrl,
|
|
7971
|
+
avatarUrl: comment.authorAvatarUrl
|
|
7972
|
+
}),
|
|
7973
|
+
createdAt: comment.createdAt ?? comment.updatedAt ?? pullRequest.updated_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
7974
|
+
body: comment.body
|
|
7975
|
+
});
|
|
7976
|
+
}
|
|
7977
|
+
timeline.sort(
|
|
7978
|
+
(left, right) => Date.parse(String(left.createdAt ?? "")) - Date.parse(String(right.createdAt ?? ""))
|
|
7979
|
+
);
|
|
7980
|
+
const checksStatus = statusSnapshot.ciState === "green" ? "passed" : statusSnapshot.ciState === "red" ? "failed" : "pending";
|
|
7981
|
+
const githubMergeable = pullRequest.mergeable === true;
|
|
7982
|
+
const reviewable = resolveProjectPullRequestReviewable({
|
|
7983
|
+
checksStatus,
|
|
7984
|
+
copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
|
|
7985
|
+
githubMergeable
|
|
7986
|
+
});
|
|
7987
|
+
const mergeable = resolveProjectPullRequestMergeable({
|
|
7988
|
+
checksStatus,
|
|
7989
|
+
reviewApprovals: reviewSummary.approvals,
|
|
7990
|
+
unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
|
|
7991
|
+
githubMergeable
|
|
7992
|
+
});
|
|
7993
|
+
return setCacheValue(
|
|
7994
|
+
activeProjectPullRequestDetailCache,
|
|
7995
|
+
detailCacheKey,
|
|
7996
|
+
{
|
|
7997
|
+
id: `github-pull-request-${scope.repository.owner}-${scope.repository.repo}-${pullRequestNumber}`,
|
|
7998
|
+
number: pullRequest.number,
|
|
7999
|
+
title: pullRequest.title,
|
|
8000
|
+
labels: normalizeGitHubIssueLabels(pullRequest.labels),
|
|
8001
|
+
author,
|
|
8002
|
+
assignees: (pullRequest.assignees ?? []).map((assignee) => buildProjectPullRequestPerson({
|
|
8003
|
+
login: assignee?.login,
|
|
8004
|
+
url: assignee?.html_url,
|
|
8005
|
+
avatarUrl: assignee?.avatar_url
|
|
8006
|
+
})),
|
|
8007
|
+
checksStatus,
|
|
8008
|
+
githubMergeable,
|
|
8009
|
+
reviewable,
|
|
8010
|
+
reviewApprovals: reviewSummary.approvals,
|
|
8011
|
+
reviewChangesRequested: reviewSummary.changesRequested,
|
|
8012
|
+
reviewCommentCount: typeof pullRequest.review_comments === "number" && pullRequest.review_comments >= 0 ? Math.floor(pullRequest.review_comments) : 0,
|
|
8013
|
+
unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
|
|
8014
|
+
copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
|
|
8015
|
+
commentsCount: typeof pullRequest.comments === "number" && pullRequest.comments >= 0 ? Math.floor(pullRequest.comments) : comments.length,
|
|
8016
|
+
createdAt: pullRequest.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
8017
|
+
updatedAt: pullRequest.updated_at ?? pullRequest.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
8018
|
+
...linkedIssue?.paperclipIssueId ? { paperclipIssueId: linkedIssue.paperclipIssueId } : {},
|
|
8019
|
+
...linkedIssue?.paperclipIssueKey ? { paperclipIssueKey: linkedIssue.paperclipIssueKey } : {},
|
|
8020
|
+
mergeable,
|
|
8021
|
+
status: getPullRequestApiState({
|
|
8022
|
+
state: pullRequest.state,
|
|
8023
|
+
merged: pullRequest.merged
|
|
8024
|
+
}),
|
|
8025
|
+
githubUrl: pullRequest.html_url,
|
|
8026
|
+
checksUrl: `${pullRequest.html_url}/checks`,
|
|
8027
|
+
reviewsUrl: `${pullRequest.html_url}/files`,
|
|
8028
|
+
reviewThreadsUrl: `${pullRequest.html_url}/files`,
|
|
8029
|
+
commentsUrl: pullRequest.html_url,
|
|
8030
|
+
baseBranch: pullRequest.base.ref,
|
|
8031
|
+
headBranch: pullRequest.head.ref,
|
|
8032
|
+
commits: typeof pullRequest.commits === "number" && pullRequest.commits >= 0 ? pullRequest.commits : 0,
|
|
8033
|
+
changedFiles: typeof pullRequest.changed_files === "number" && pullRequest.changed_files >= 0 ? pullRequest.changed_files : 0,
|
|
8034
|
+
timeline
|
|
8035
|
+
},
|
|
8036
|
+
PROJECT_PULL_REQUEST_DETAIL_CACHE_TTL_MS
|
|
8037
|
+
);
|
|
8038
|
+
}
|
|
8039
|
+
async function createProjectPullRequestPaperclipIssue(ctx, input) {
|
|
8040
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8041
|
+
if (!pullRequestNumber) {
|
|
8042
|
+
throw new Error("pullRequestNumber is required.");
|
|
8043
|
+
}
|
|
8044
|
+
if (!ctx.issues || typeof ctx.issues.create !== "function") {
|
|
8045
|
+
throw new Error("This Paperclip runtime does not expose plugin issue creation yet.");
|
|
8046
|
+
}
|
|
8047
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8048
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8049
|
+
const pullRequestResponse = await octokit.rest.pulls.get({
|
|
8050
|
+
owner: scope.repository.owner,
|
|
8051
|
+
repo: scope.repository.repo,
|
|
8052
|
+
pull_number: pullRequestNumber,
|
|
8053
|
+
headers: {
|
|
8054
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8055
|
+
}
|
|
8056
|
+
});
|
|
8057
|
+
const pullRequest = pullRequestResponse.data;
|
|
8058
|
+
const pullRequestUrl = pullRequest.html_url;
|
|
8059
|
+
const existingLinks = await listGitHubPullRequestLinkRecords(ctx, {
|
|
8060
|
+
externalId: pullRequestUrl
|
|
8061
|
+
});
|
|
8062
|
+
const existingLink = existingLinks.find(
|
|
8063
|
+
(record) => record.data.githubPullRequestNumber === pullRequestNumber && record.data.repositoryUrl === scope.repository.url && (!record.data.companyId || record.data.companyId === scope.companyId) && (!record.data.paperclipProjectId || record.data.paperclipProjectId === scope.projectId)
|
|
8064
|
+
);
|
|
8065
|
+
const existingIssue = existingLink ? await ctx.issues.get(existingLink.paperclipIssueId, scope.companyId) : null;
|
|
8066
|
+
if (existingLink && existingIssue) {
|
|
8067
|
+
return {
|
|
8068
|
+
paperclipIssueId: existingIssue.id,
|
|
8069
|
+
...existingIssue.identifier ? { paperclipIssueKey: existingIssue.identifier } : {},
|
|
8070
|
+
alreadyLinked: true
|
|
8071
|
+
};
|
|
8072
|
+
}
|
|
8073
|
+
const requestedTitle = typeof input.title === "string" && input.title.trim() ? input.title.trim() : pullRequest.title.trim();
|
|
8074
|
+
const createdIssue = await ctx.issues.create({
|
|
8075
|
+
companyId: scope.companyId,
|
|
8076
|
+
projectId: scope.projectId,
|
|
8077
|
+
title: requestedTitle,
|
|
8078
|
+
description: buildPaperclipIssueDescriptionFromPullRequest({
|
|
8079
|
+
repository: scope.repository,
|
|
8080
|
+
pullRequestNumber,
|
|
8081
|
+
pullRequestUrl,
|
|
8082
|
+
body: pullRequest.body
|
|
8083
|
+
})
|
|
8084
|
+
});
|
|
8085
|
+
const resolvedIssue = await ctx.issues.get(createdIssue.id, scope.companyId) ?? createdIssue;
|
|
8086
|
+
await upsertGitHubPullRequestLinkRecord(ctx, {
|
|
8087
|
+
companyId: scope.companyId,
|
|
8088
|
+
projectId: scope.projectId,
|
|
8089
|
+
issueId: resolvedIssue.id,
|
|
8090
|
+
repositoryUrl: scope.repository.url,
|
|
8091
|
+
pullRequestNumber,
|
|
8092
|
+
pullRequestUrl,
|
|
8093
|
+
pullRequestTitle: pullRequest.title,
|
|
8094
|
+
pullRequestState: getPullRequestApiState({
|
|
8095
|
+
state: pullRequest.state,
|
|
8096
|
+
merged: pullRequest.merged
|
|
8097
|
+
}) === "open" ? "open" : "closed"
|
|
8098
|
+
});
|
|
8099
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8100
|
+
return {
|
|
8101
|
+
paperclipIssueId: resolvedIssue.id,
|
|
8102
|
+
...resolvedIssue.identifier ? { paperclipIssueKey: resolvedIssue.identifier } : {},
|
|
8103
|
+
alreadyLinked: false
|
|
8104
|
+
};
|
|
8105
|
+
}
|
|
8106
|
+
async function refreshProjectPullRequests(ctx, input) {
|
|
8107
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8108
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8109
|
+
return {
|
|
8110
|
+
status: "refreshed",
|
|
8111
|
+
projectId: scope.projectId,
|
|
8112
|
+
repositoryUrl: scope.repository.url,
|
|
8113
|
+
refreshedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8114
|
+
};
|
|
8115
|
+
}
|
|
8116
|
+
async function updateProjectPullRequestBranch(ctx, input) {
|
|
8117
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8118
|
+
if (!pullRequestNumber) {
|
|
8119
|
+
throw new Error("pullRequestNumber is required.");
|
|
8120
|
+
}
|
|
8121
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8122
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8123
|
+
const pullRequestResponse = await octokit.rest.pulls.get({
|
|
8124
|
+
owner: scope.repository.owner,
|
|
8125
|
+
repo: scope.repository.repo,
|
|
8126
|
+
pull_number: pullRequestNumber,
|
|
8127
|
+
headers: {
|
|
8128
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8129
|
+
}
|
|
8130
|
+
});
|
|
8131
|
+
const pullRequest = pullRequestResponse.data;
|
|
8132
|
+
const githubUrl = pullRequest.html_url ?? `${scope.repository.url}/pull/${pullRequestNumber}`;
|
|
8133
|
+
const pullRequestState = getPullRequestApiState({
|
|
8134
|
+
state: pullRequest.state,
|
|
8135
|
+
merged: pullRequest.merged
|
|
8136
|
+
});
|
|
8137
|
+
if (pullRequestState !== "open") {
|
|
8138
|
+
throw new Error("Only open pull requests can be updated with the base branch.");
|
|
8139
|
+
}
|
|
8140
|
+
const behindBy = await getGitHubPullRequestBehindCount(octokit, scope.repository, {
|
|
8141
|
+
baseBranch: pullRequest.base.ref,
|
|
8142
|
+
headBranch: pullRequest.head.ref,
|
|
8143
|
+
headRepositoryOwner: pullRequest.head.repo?.owner?.login
|
|
8144
|
+
});
|
|
8145
|
+
if (typeof behindBy === "number" && behindBy <= 0) {
|
|
8146
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8147
|
+
return {
|
|
8148
|
+
githubUrl,
|
|
8149
|
+
status: "already_up_to_date"
|
|
8150
|
+
};
|
|
8151
|
+
}
|
|
8152
|
+
const mergeableState = typeof pullRequest.mergeable_state === "string" ? pullRequest.mergeable_state.trim().toLowerCase() : "";
|
|
8153
|
+
if ((mergeableState === "dirty" || pullRequest.mergeable === false) && (behindBy === null || behindBy > 0)) {
|
|
8154
|
+
throw new Error("This pull request needs conflict resolution before it can be updated with the base branch.");
|
|
8155
|
+
}
|
|
8156
|
+
try {
|
|
8157
|
+
await octokit.request("PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch", {
|
|
8158
|
+
owner: scope.repository.owner,
|
|
8159
|
+
repo: scope.repository.repo,
|
|
8160
|
+
pull_number: pullRequestNumber,
|
|
8161
|
+
...typeof pullRequest.head.sha === "string" && pullRequest.head.sha.trim() ? { expected_head_sha: pullRequest.head.sha.trim() } : {},
|
|
8162
|
+
headers: {
|
|
8163
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8164
|
+
}
|
|
8165
|
+
});
|
|
8166
|
+
} catch (error) {
|
|
8167
|
+
throw buildGitHubPullRequestWriteActionError({
|
|
8168
|
+
action: "update_branch",
|
|
8169
|
+
error,
|
|
8170
|
+
repositoryLabel: `${scope.repository.owner}/${scope.repository.repo}`
|
|
8171
|
+
});
|
|
8172
|
+
}
|
|
8173
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8174
|
+
return {
|
|
8175
|
+
githubUrl,
|
|
8176
|
+
status: "update_requested"
|
|
8177
|
+
};
|
|
8178
|
+
}
|
|
8179
|
+
async function mergeProjectPullRequest(ctx, input) {
|
|
8180
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8181
|
+
if (!pullRequestNumber) {
|
|
8182
|
+
throw new Error("pullRequestNumber is required.");
|
|
8183
|
+
}
|
|
8184
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8185
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8186
|
+
const response = await octokit.rest.pulls.merge({
|
|
8187
|
+
owner: scope.repository.owner,
|
|
8188
|
+
repo: scope.repository.repo,
|
|
8189
|
+
pull_number: pullRequestNumber,
|
|
8190
|
+
headers: {
|
|
8191
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8192
|
+
}
|
|
8193
|
+
});
|
|
8194
|
+
if (response.data.merged !== true) {
|
|
8195
|
+
throw new Error(response.data.message ?? `GitHub did not merge pull request #${pullRequestNumber}.`);
|
|
8196
|
+
}
|
|
8197
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8198
|
+
return {
|
|
8199
|
+
githubUrl: `${scope.repository.url}/pull/${pullRequestNumber}`,
|
|
8200
|
+
status: "merged"
|
|
8201
|
+
};
|
|
8202
|
+
}
|
|
8203
|
+
async function closeProjectPullRequest(ctx, input) {
|
|
8204
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8205
|
+
if (!pullRequestNumber) {
|
|
8206
|
+
throw new Error("pullRequestNumber is required.");
|
|
8207
|
+
}
|
|
8208
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8209
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8210
|
+
const response = await octokit.rest.pulls.update({
|
|
8211
|
+
owner: scope.repository.owner,
|
|
8212
|
+
repo: scope.repository.repo,
|
|
8213
|
+
pull_number: pullRequestNumber,
|
|
8214
|
+
state: "closed",
|
|
8215
|
+
headers: {
|
|
8216
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8217
|
+
}
|
|
8218
|
+
});
|
|
8219
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8220
|
+
return {
|
|
8221
|
+
githubUrl: response.data.html_url ?? `${scope.repository.url}/pull/${pullRequestNumber}`,
|
|
8222
|
+
status: getPullRequestApiState({
|
|
8223
|
+
state: response.data.state,
|
|
8224
|
+
merged: response.data.merged
|
|
8225
|
+
})
|
|
4972
8226
|
};
|
|
4973
8227
|
}
|
|
4974
|
-
function
|
|
4975
|
-
const
|
|
4976
|
-
if (
|
|
4977
|
-
|
|
4978
|
-
return {
|
|
4979
|
-
error: `${resourceLabel} rate limit reached. Wait until ${formatUtcTimestamp(rateLimitPause.resetAt)} before retrying.`
|
|
4980
|
-
};
|
|
8228
|
+
async function addProjectPullRequestComment(ctx, input) {
|
|
8229
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8230
|
+
if (!pullRequestNumber) {
|
|
8231
|
+
throw new Error("pullRequestNumber is required.");
|
|
4981
8232
|
}
|
|
8233
|
+
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
8234
|
+
if (!body) {
|
|
8235
|
+
throw new Error("Comment body cannot be empty.");
|
|
8236
|
+
}
|
|
8237
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8238
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8239
|
+
const response = await createProjectPullRequestGitHubComment(octokit, scope, pullRequestNumber, body);
|
|
8240
|
+
invalidateProjectPullRequestCaches(scope);
|
|
4982
8241
|
return {
|
|
4983
|
-
|
|
8242
|
+
commentId: response.id,
|
|
8243
|
+
commentUrl: response.htmlUrl ?? `${scope.repository.url}/pull/${pullRequestNumber}`
|
|
4984
8244
|
};
|
|
4985
8245
|
}
|
|
4986
|
-
async function
|
|
8246
|
+
async function createProjectPullRequestGitHubComment(octokit, scope, pullRequestNumber, body) {
|
|
8247
|
+
let response;
|
|
4987
8248
|
try {
|
|
4988
|
-
|
|
8249
|
+
response = await octokit.rest.issues.createComment({
|
|
8250
|
+
owner: scope.repository.owner,
|
|
8251
|
+
repo: scope.repository.repo,
|
|
8252
|
+
issue_number: pullRequestNumber,
|
|
8253
|
+
body,
|
|
8254
|
+
headers: {
|
|
8255
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8256
|
+
}
|
|
8257
|
+
});
|
|
4989
8258
|
} catch (error) {
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
if (!token) {
|
|
4996
|
-
throw new Error(MISSING_GITHUB_TOKEN_SYNC_MESSAGE);
|
|
8259
|
+
throw buildGitHubPullRequestWriteActionError({
|
|
8260
|
+
action: "comment",
|
|
8261
|
+
error,
|
|
8262
|
+
repositoryLabel: `${scope.repository.owner}/${scope.repository.repo}`
|
|
8263
|
+
});
|
|
4997
8264
|
}
|
|
4998
|
-
return
|
|
8265
|
+
return {
|
|
8266
|
+
id: response.data.id,
|
|
8267
|
+
...response.data.html_url ? { htmlUrl: response.data.html_url } : {}
|
|
8268
|
+
};
|
|
4999
8269
|
}
|
|
5000
|
-
|
|
5001
|
-
const
|
|
5002
|
-
const
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
8270
|
+
function buildProjectPullRequestCopilotComment(action, options) {
|
|
8271
|
+
const baseBranch = typeof options?.baseBranch === "string" ? options.baseBranch.trim() : "";
|
|
8272
|
+
const baseBranchLabel = baseBranch ? `\`${baseBranch}\`` : "the base branch";
|
|
8273
|
+
switch (action) {
|
|
8274
|
+
case "fix_ci":
|
|
8275
|
+
return "@copilot Please investigate the failing CI on this pull request, push the smallest fix needed to this branch, and summarize the root cause and changes.";
|
|
8276
|
+
case "rebase":
|
|
8277
|
+
return `@copilot This pull request is behind ${baseBranchLabel} and needs conflict resolution. Please bring this branch up to date with ${baseBranchLabel}, resolve the conflicts, push the updated branch, and summarize any non-trivial conflict decisions.`;
|
|
8278
|
+
case "address_review_feedback":
|
|
8279
|
+
return "@copilot Please address the unresolved review feedback on this pull request, push the necessary updates to this branch, and summarize what you changed.";
|
|
8280
|
+
case "review":
|
|
8281
|
+
return "@copilot Please review this pull request and leave feedback as GitHub review comments. Focus on correctness, regressions, and missing tests.";
|
|
8282
|
+
}
|
|
8283
|
+
}
|
|
8284
|
+
var COPILOT_PULL_REQUEST_REVIEWER_LOGIN = "copilot-pull-request-reviewer[bot]";
|
|
8285
|
+
async function requestProjectPullRequestCopilotAction(ctx, input) {
|
|
8286
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8287
|
+
if (!pullRequestNumber) {
|
|
8288
|
+
throw new Error("pullRequestNumber is required.");
|
|
8289
|
+
}
|
|
8290
|
+
const action = normalizeProjectPullRequestCopilotAction(input.action);
|
|
8291
|
+
if (!action) {
|
|
8292
|
+
throw new Error('action must be one of "fix_ci", "rebase", "address_review_feedback", or "review".');
|
|
8293
|
+
}
|
|
8294
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8295
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8296
|
+
const pullRequestResponse = await octokit.rest.pulls.get({
|
|
8297
|
+
owner: scope.repository.owner,
|
|
8298
|
+
repo: scope.repository.repo,
|
|
8299
|
+
pull_number: pullRequestNumber,
|
|
8300
|
+
headers: {
|
|
8301
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8302
|
+
}
|
|
5007
8303
|
});
|
|
5008
|
-
const
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
async function resolveGitHubToolRepository(ctx, runCtx, input) {
|
|
5025
|
-
const explicitRepository = normalizeOptionalToolString(input.repository);
|
|
5026
|
-
if (explicitRepository) {
|
|
5027
|
-
return requireRepositoryReference(explicitRepository);
|
|
5028
|
-
}
|
|
5029
|
-
const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
|
|
5030
|
-
if (paperclipIssueId) {
|
|
5031
|
-
const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
|
|
5032
|
-
if (!link) {
|
|
5033
|
-
throw new Error("This Paperclip issue is not linked to a GitHub issue yet. Pass repository explicitly.");
|
|
8304
|
+
const pullRequest = pullRequestResponse.data;
|
|
8305
|
+
const githubUrl = pullRequest.html_url ?? `${scope.repository.url}/pull/${pullRequestNumber}`;
|
|
8306
|
+
const pullRequestState = getPullRequestApiState({
|
|
8307
|
+
state: pullRequest.state,
|
|
8308
|
+
merged: pullRequest.merged
|
|
8309
|
+
});
|
|
8310
|
+
if (pullRequestState !== "open") {
|
|
8311
|
+
throw new Error("Only open pull requests can request Copilot actions.");
|
|
8312
|
+
}
|
|
8313
|
+
switch (action) {
|
|
8314
|
+
case "fix_ci": {
|
|
8315
|
+
const ciState = await getGitHubPullRequestCiState(octokit, scope.repository, pullRequestNumber);
|
|
8316
|
+
if (ciState !== "red") {
|
|
8317
|
+
throw new Error("This pull request does not currently have failing checks.");
|
|
8318
|
+
}
|
|
8319
|
+
break;
|
|
5034
8320
|
}
|
|
5035
|
-
|
|
8321
|
+
case "rebase": {
|
|
8322
|
+
const behindBy = await getGitHubPullRequestBehindCount(octokit, scope.repository, {
|
|
8323
|
+
baseBranch: pullRequest.base.ref,
|
|
8324
|
+
headBranch: pullRequest.head.ref,
|
|
8325
|
+
headRepositoryOwner: pullRequest.head.repo?.owner?.login
|
|
8326
|
+
});
|
|
8327
|
+
if (typeof behindBy === "number" && behindBy <= 0) {
|
|
8328
|
+
throw new Error("This pull request is already up to date with the base branch.");
|
|
8329
|
+
}
|
|
8330
|
+
const mergeableState = typeof pullRequest.mergeable_state === "string" ? pullRequest.mergeable_state.trim().toLowerCase() : "";
|
|
8331
|
+
const needsConflictResolution = (mergeableState === "dirty" || pullRequest.mergeable === false) && (behindBy === null || behindBy > 0);
|
|
8332
|
+
if (!needsConflictResolution) {
|
|
8333
|
+
throw new Error("This pull request can be updated without Copilot. Use Update branch instead.");
|
|
8334
|
+
}
|
|
8335
|
+
break;
|
|
8336
|
+
}
|
|
8337
|
+
case "address_review_feedback": {
|
|
8338
|
+
const reviewThreadSummary = await getOrLoadCachedGitHubPullRequestReviewThreadSummary(
|
|
8339
|
+
octokit,
|
|
8340
|
+
scope.repository,
|
|
8341
|
+
pullRequestNumber
|
|
8342
|
+
);
|
|
8343
|
+
if (reviewThreadSummary.unresolvedReviewThreads <= 0) {
|
|
8344
|
+
throw new Error("This pull request does not currently have unresolved review threads.");
|
|
8345
|
+
}
|
|
8346
|
+
break;
|
|
8347
|
+
}
|
|
8348
|
+
case "review":
|
|
8349
|
+
break;
|
|
5036
8350
|
}
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
if (paperclipIssueId) {
|
|
5042
|
-
const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
|
|
5043
|
-
if (!link) {
|
|
5044
|
-
throw new Error("This Paperclip issue is not linked to a GitHub issue yet.");
|
|
8351
|
+
if (action === "review") {
|
|
8352
|
+
const pullRequestId = typeof pullRequest.node_id === "string" && pullRequest.node_id.trim() ? pullRequest.node_id.trim() : null;
|
|
8353
|
+
if (!pullRequestId) {
|
|
8354
|
+
throw new Error("GitHub did not return a pull request node id for this review request.");
|
|
5045
8355
|
}
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
8356
|
+
let response;
|
|
8357
|
+
try {
|
|
8358
|
+
response = await octokit.graphql(
|
|
8359
|
+
GITHUB_REQUEST_PULL_REQUEST_COPILOT_REVIEW_MUTATION,
|
|
8360
|
+
{
|
|
8361
|
+
pullRequestId,
|
|
8362
|
+
botLogins: [COPILOT_PULL_REQUEST_REVIEWER_LOGIN]
|
|
8363
|
+
}
|
|
8364
|
+
);
|
|
8365
|
+
} catch (error) {
|
|
8366
|
+
throw buildGitHubPullRequestWriteActionError({
|
|
8367
|
+
action: "review",
|
|
8368
|
+
error,
|
|
8369
|
+
repositoryLabel: `${scope.repository.owner}/${scope.repository.repo}`
|
|
8370
|
+
});
|
|
5054
8371
|
}
|
|
8372
|
+
const requestedReviewers = (response.requestReviews?.requestedReviewers?.edges ?? []).map((edge) => edge?.node?.login?.trim() ?? "").filter(Boolean);
|
|
8373
|
+
invalidateProjectPullRequestCaches(scope);
|
|
5055
8374
|
return {
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
githubIssueUrl: link.githubIssueUrl
|
|
8375
|
+
action,
|
|
8376
|
+
actionLabel: getProjectPullRequestCopilotActionLabel(action),
|
|
8377
|
+
requestedReviewer: requestedReviewers[0] ?? COPILOT_PULL_REQUEST_REVIEWER_LOGIN,
|
|
8378
|
+
githubUrl: response.requestReviews?.pullRequest?.url ?? githubUrl
|
|
5061
8379
|
};
|
|
5062
8380
|
}
|
|
5063
|
-
const
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
8381
|
+
const comment = await createProjectPullRequestGitHubComment(
|
|
8382
|
+
octokit,
|
|
8383
|
+
scope,
|
|
8384
|
+
pullRequestNumber,
|
|
8385
|
+
buildProjectPullRequestCopilotComment(action, {
|
|
8386
|
+
baseBranch: pullRequest.base.ref
|
|
8387
|
+
})
|
|
8388
|
+
);
|
|
8389
|
+
invalidateProjectPullRequestCaches(scope);
|
|
5068
8390
|
return {
|
|
5069
|
-
|
|
5070
|
-
|
|
8391
|
+
action,
|
|
8392
|
+
actionLabel: getProjectPullRequestCopilotActionLabel(action),
|
|
8393
|
+
commentId: comment.id,
|
|
8394
|
+
commentUrl: comment.htmlUrl ?? githubUrl,
|
|
8395
|
+
githubUrl
|
|
5071
8396
|
};
|
|
5072
8397
|
}
|
|
5073
|
-
async function
|
|
5074
|
-
const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
|
|
5075
|
-
if (paperclipIssueId) {
|
|
5076
|
-
const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
|
|
5077
|
-
if (!link) {
|
|
5078
|
-
throw new Error("This Paperclip issue is not linked to GitHub yet.");
|
|
5079
|
-
}
|
|
5080
|
-
const repository2 = assertExplicitRepositoryMatchesLinkedRepository(
|
|
5081
|
-
input.repository,
|
|
5082
|
-
link.repositoryUrl,
|
|
5083
|
-
"repository must match the GitHub repository linked to the provided Paperclip issue."
|
|
5084
|
-
);
|
|
5085
|
-
const explicitPullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
5086
|
-
if (explicitPullRequestNumber !== void 0) {
|
|
5087
|
-
return {
|
|
5088
|
-
repository: repository2,
|
|
5089
|
-
pullRequestNumber: explicitPullRequestNumber,
|
|
5090
|
-
paperclipIssueId
|
|
5091
|
-
};
|
|
5092
|
-
}
|
|
5093
|
-
if (link.linkedPullRequestNumbers.length === 1) {
|
|
5094
|
-
return {
|
|
5095
|
-
repository: repository2,
|
|
5096
|
-
pullRequestNumber: link.linkedPullRequestNumbers[0],
|
|
5097
|
-
paperclipIssueId
|
|
5098
|
-
};
|
|
5099
|
-
}
|
|
5100
|
-
throw new Error("pullRequestNumber is required unless the linked Paperclip issue has exactly one linked pull request.");
|
|
5101
|
-
}
|
|
5102
|
-
const repository = await resolveGitHubToolRepository(ctx, runCtx, input);
|
|
8398
|
+
async function reviewProjectPullRequest(ctx, input) {
|
|
5103
8399
|
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
5104
|
-
if (pullRequestNumber
|
|
5105
|
-
throw new Error("pullRequestNumber is required
|
|
8400
|
+
if (!pullRequestNumber) {
|
|
8401
|
+
throw new Error("pullRequestNumber is required.");
|
|
8402
|
+
}
|
|
8403
|
+
const reviewType = input.review === "approve" ? "APPROVE" : input.review === "request_changes" ? "REQUEST_CHANGES" : void 0;
|
|
8404
|
+
if (!reviewType) {
|
|
8405
|
+
throw new Error('review must be "approve" or "request_changes".');
|
|
8406
|
+
}
|
|
8407
|
+
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
8408
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8409
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8410
|
+
let response;
|
|
8411
|
+
try {
|
|
8412
|
+
response = await octokit.rest.pulls.createReview({
|
|
8413
|
+
owner: scope.repository.owner,
|
|
8414
|
+
repo: scope.repository.repo,
|
|
8415
|
+
pull_number: pullRequestNumber,
|
|
8416
|
+
event: reviewType,
|
|
8417
|
+
...body ? { body } : {},
|
|
8418
|
+
headers: {
|
|
8419
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8420
|
+
}
|
|
8421
|
+
});
|
|
8422
|
+
} catch (error) {
|
|
8423
|
+
throw buildGitHubPullRequestWriteActionError({
|
|
8424
|
+
action: "review",
|
|
8425
|
+
error,
|
|
8426
|
+
repositoryLabel: `${scope.repository.owner}/${scope.repository.repo}`,
|
|
8427
|
+
reviewType,
|
|
8428
|
+
body
|
|
8429
|
+
});
|
|
5106
8430
|
}
|
|
8431
|
+
invalidateProjectPullRequestCaches(scope);
|
|
5107
8432
|
return {
|
|
5108
|
-
|
|
5109
|
-
|
|
8433
|
+
reviewId: response.data.id,
|
|
8434
|
+
review: reviewType === "APPROVE" ? "approved" : "changes_requested",
|
|
8435
|
+
reviewUrl: response.data.html_url ?? `${scope.repository.url}/pull/${pullRequestNumber}`
|
|
5110
8436
|
};
|
|
5111
8437
|
}
|
|
5112
|
-
function
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
}
|
|
5123
|
-
const trimmedModel = llmModel.trim();
|
|
5124
|
-
if (!trimmedModel) {
|
|
5125
|
-
throw new Error("llmModel is required when posting a GitHub comment.");
|
|
8438
|
+
function isFailedCheckSuiteConclusion(value) {
|
|
8439
|
+
switch (value?.trim().toLowerCase()) {
|
|
8440
|
+
case "action_required":
|
|
8441
|
+
case "cancelled":
|
|
8442
|
+
case "failure":
|
|
8443
|
+
case "stale":
|
|
8444
|
+
case "timed_out":
|
|
8445
|
+
return true;
|
|
8446
|
+
default:
|
|
8447
|
+
return false;
|
|
5126
8448
|
}
|
|
5127
|
-
return `${trimmedBody}${formatAiAuthorshipFooter(trimmedModel)}`;
|
|
5128
8449
|
}
|
|
5129
|
-
async function
|
|
5130
|
-
const
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
8450
|
+
async function rerunProjectPullRequestCi(ctx, input) {
|
|
8451
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8452
|
+
if (!pullRequestNumber) {
|
|
8453
|
+
throw new Error("pullRequestNumber is required.");
|
|
8454
|
+
}
|
|
8455
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8456
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8457
|
+
const pullRequestResponse = await octokit.rest.pulls.get({
|
|
8458
|
+
owner: scope.repository.owner,
|
|
8459
|
+
repo: scope.repository.repo,
|
|
8460
|
+
pull_number: pullRequestNumber,
|
|
5136
8461
|
headers: {
|
|
5137
8462
|
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
5138
8463
|
}
|
|
5139
|
-
})
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
updatedAt: comment.updated_at ?? void 0
|
|
5148
|
-
});
|
|
8464
|
+
});
|
|
8465
|
+
const checkSuitesResponse = await octokit.rest.checks.listSuitesForRef({
|
|
8466
|
+
owner: scope.repository.owner,
|
|
8467
|
+
repo: scope.repository.repo,
|
|
8468
|
+
ref: pullRequestResponse.data.head.sha,
|
|
8469
|
+
per_page: 100,
|
|
8470
|
+
headers: {
|
|
8471
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
5149
8472
|
}
|
|
8473
|
+
});
|
|
8474
|
+
const rerunnableSuites = checkSuitesResponse.data.check_suites.filter(
|
|
8475
|
+
(suite) => suite.status === "completed" && isFailedCheckSuiteConclusion(suite.conclusion)
|
|
8476
|
+
);
|
|
8477
|
+
if (rerunnableSuites.length === 0) {
|
|
8478
|
+
throw new Error("No failed GitHub check suites are available to re-run for this pull request.");
|
|
8479
|
+
}
|
|
8480
|
+
for (const suite of rerunnableSuites) {
|
|
8481
|
+
await octokit.rest.checks.rerequestSuite({
|
|
8482
|
+
owner: scope.repository.owner,
|
|
8483
|
+
repo: scope.repository.repo,
|
|
8484
|
+
check_suite_id: suite.id,
|
|
8485
|
+
headers: {
|
|
8486
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8487
|
+
}
|
|
8488
|
+
});
|
|
5150
8489
|
}
|
|
5151
|
-
|
|
8490
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8491
|
+
return {
|
|
8492
|
+
rerunCheckSuiteCount: rerunnableSuites.length,
|
|
8493
|
+
githubUrl: `${scope.repository.url}/pull/${pullRequestNumber}/checks`
|
|
8494
|
+
};
|
|
5152
8495
|
}
|
|
5153
8496
|
async function listAllPullRequestFiles(octokit, repository, pullRequestNumber) {
|
|
5154
8497
|
const files = [];
|
|
@@ -6576,6 +9919,17 @@ function registerGitHubAgentTools(ctx) {
|
|
|
6576
9919
|
})
|
|
6577
9920
|
);
|
|
6578
9921
|
}
|
|
9922
|
+
function shouldStartWorkerHost(moduleUrl, entry = process.argv[1]) {
|
|
9923
|
+
if (typeof entry !== "string" || !entry.trim()) {
|
|
9924
|
+
return false;
|
|
9925
|
+
}
|
|
9926
|
+
const modulePath = fileURLToPath(moduleUrl);
|
|
9927
|
+
try {
|
|
9928
|
+
return realpathSync(entry) === realpathSync(modulePath);
|
|
9929
|
+
} catch {
|
|
9930
|
+
return resolve(entry) === resolve(modulePath);
|
|
9931
|
+
}
|
|
9932
|
+
}
|
|
6579
9933
|
var plugin = definePlugin({
|
|
6580
9934
|
async setup(ctx) {
|
|
6581
9935
|
ctx.data.register("settings.registration", async (input) => {
|
|
@@ -6619,6 +9973,30 @@ var plugin = definePlugin({
|
|
|
6619
9973
|
const record = input && typeof input === "object" ? input : {};
|
|
6620
9974
|
return buildToolbarSyncState(ctx, record);
|
|
6621
9975
|
});
|
|
9976
|
+
ctx.data.register("settings.tokenPermissionAudit", async (input) => {
|
|
9977
|
+
const record = input && typeof input === "object" ? input : {};
|
|
9978
|
+
return buildSettingsTokenPermissionAuditData(ctx, record);
|
|
9979
|
+
});
|
|
9980
|
+
ctx.data.register("project.pullRequests.page", async (input) => {
|
|
9981
|
+
const record = input && typeof input === "object" ? input : {};
|
|
9982
|
+
return buildProjectPullRequestsPageData(ctx, record);
|
|
9983
|
+
});
|
|
9984
|
+
ctx.data.register("project.pullRequests.metrics", async (input) => {
|
|
9985
|
+
const record = input && typeof input === "object" ? input : {};
|
|
9986
|
+
return buildProjectPullRequestMetricsData(ctx, record);
|
|
9987
|
+
});
|
|
9988
|
+
ctx.data.register("project.pullRequests.count", async (input) => {
|
|
9989
|
+
const record = input && typeof input === "object" ? input : {};
|
|
9990
|
+
return buildProjectPullRequestCountData(ctx, record);
|
|
9991
|
+
});
|
|
9992
|
+
ctx.data.register("project.pullRequests.detail", async (input) => {
|
|
9993
|
+
const record = input && typeof input === "object" ? input : {};
|
|
9994
|
+
return buildProjectPullRequestDetailData(ctx, record);
|
|
9995
|
+
});
|
|
9996
|
+
ctx.data.register("project.pullRequests.paperclipIssue", async (input) => {
|
|
9997
|
+
const record = input && typeof input === "object" ? input : {};
|
|
9998
|
+
return buildProjectPullRequestPaperclipIssueData(ctx, record);
|
|
9999
|
+
});
|
|
6622
10000
|
ctx.data.register("issue.githubDetails", async (input) => {
|
|
6623
10001
|
const record = input && typeof input === "object" ? input : {};
|
|
6624
10002
|
return buildIssueGitHubDetails(ctx, record);
|
|
@@ -6684,6 +10062,7 @@ var plugin = definePlugin({
|
|
|
6684
10062
|
});
|
|
6685
10063
|
await ctx.state.set(SETTINGS_SCOPE, next);
|
|
6686
10064
|
await ctx.state.set(SYNC_STATE_SCOPE, next.syncState);
|
|
10065
|
+
clearGitHubRepositoryTokenCapabilityAudits();
|
|
6687
10066
|
return {
|
|
6688
10067
|
...getPublicSettingsForScope(next, requestedCompanyId),
|
|
6689
10068
|
availableAssignees: requestedCompanyId ? await listAvailableAssignees(ctx, requestedCompanyId) : []
|
|
@@ -6733,6 +10112,42 @@ var plugin = definePlugin({
|
|
|
6733
10112
|
}
|
|
6734
10113
|
return validateGithubToken(trimmedToken);
|
|
6735
10114
|
});
|
|
10115
|
+
ctx.actions.register("project.pullRequests.createIssue", async (input) => {
|
|
10116
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10117
|
+
return createProjectPullRequestPaperclipIssue(ctx, record);
|
|
10118
|
+
});
|
|
10119
|
+
ctx.actions.register("project.pullRequests.refresh", async (input) => {
|
|
10120
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10121
|
+
return refreshProjectPullRequests(ctx, record);
|
|
10122
|
+
});
|
|
10123
|
+
ctx.actions.register("project.pullRequests.updateBranch", async (input) => {
|
|
10124
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10125
|
+
return updateProjectPullRequestBranch(ctx, record);
|
|
10126
|
+
});
|
|
10127
|
+
ctx.actions.register("project.pullRequests.requestCopilotAction", async (input) => {
|
|
10128
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10129
|
+
return requestProjectPullRequestCopilotAction(ctx, record);
|
|
10130
|
+
});
|
|
10131
|
+
ctx.actions.register("project.pullRequests.merge", async (input) => {
|
|
10132
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10133
|
+
return mergeProjectPullRequest(ctx, record);
|
|
10134
|
+
});
|
|
10135
|
+
ctx.actions.register("project.pullRequests.close", async (input) => {
|
|
10136
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10137
|
+
return closeProjectPullRequest(ctx, record);
|
|
10138
|
+
});
|
|
10139
|
+
ctx.actions.register("project.pullRequests.addComment", async (input) => {
|
|
10140
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10141
|
+
return addProjectPullRequestComment(ctx, record);
|
|
10142
|
+
});
|
|
10143
|
+
ctx.actions.register("project.pullRequests.review", async (input) => {
|
|
10144
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10145
|
+
return reviewProjectPullRequest(ctx, record);
|
|
10146
|
+
});
|
|
10147
|
+
ctx.actions.register("project.pullRequests.rerunCi", async (input) => {
|
|
10148
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10149
|
+
return rerunProjectPullRequestCi(ctx, record);
|
|
10150
|
+
});
|
|
6736
10151
|
ctx.actions.register("sync.runNow", async (input) => {
|
|
6737
10152
|
const waitForCompletion = input && typeof input === "object" && "waitForCompletion" in input ? Boolean(input.waitForCompletion) : false;
|
|
6738
10153
|
const paperclipApiBaseUrl = input && typeof input === "object" && "paperclipApiBaseUrl" in input ? input.paperclipApiBaseUrl : void 0;
|
|
@@ -6788,7 +10203,10 @@ var plugin = definePlugin({
|
|
|
6788
10203
|
}
|
|
6789
10204
|
});
|
|
6790
10205
|
var worker_default = plugin;
|
|
6791
|
-
|
|
10206
|
+
if (shouldStartWorkerHost(import.meta.url)) {
|
|
10207
|
+
startWorkerRpcHost({ plugin });
|
|
10208
|
+
}
|
|
6792
10209
|
export {
|
|
6793
|
-
worker_default as default
|
|
10210
|
+
worker_default as default,
|
|
10211
|
+
shouldStartWorkerHost
|
|
6794
10212
|
};
|