paperclip-github-plugin 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/dist/manifest.js +18 -1
- package/dist/ui/index.js +29746 -2072
- package/dist/ui/index.js.map +4 -4
- package/dist/worker.js +3862 -211
- package/package.json +6 -2
package/dist/worker.js
CHANGED
|
@@ -3,7 +3,10 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { Octokit } from "@octokit/rest";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
definePlugin,
|
|
8
|
+
runWorker
|
|
9
|
+
} from "@paperclipai/plugin-sdk";
|
|
7
10
|
|
|
8
11
|
// src/github-agent-tools.ts
|
|
9
12
|
var repositoryProperty = {
|
|
@@ -515,6 +518,10 @@ var SYNC_STATE_SCOPE = {
|
|
|
515
518
|
scopeKind: "instance",
|
|
516
519
|
stateKey: "paperclip-github-plugin-last-sync"
|
|
517
520
|
};
|
|
521
|
+
var SYNC_CANCELLATION_SCOPE = {
|
|
522
|
+
scopeKind: "instance",
|
|
523
|
+
stateKey: "paperclip-github-plugin-sync-cancel-request"
|
|
524
|
+
};
|
|
518
525
|
var IMPORT_REGISTRY_SCOPE = {
|
|
519
526
|
scopeKind: "instance",
|
|
520
527
|
stateKey: "paperclip-github-plugin-import-registry"
|
|
@@ -525,9 +532,21 @@ var DEFAULT_IGNORED_GITHUB_ISSUE_USERNAMES = ["renovate"];
|
|
|
525
532
|
var GITHUB_API_VERSION = "2026-03-10";
|
|
526
533
|
var DEFAULT_PAPERCLIP_LABEL_COLOR = "#6366f1";
|
|
527
534
|
var PAPERCLIP_LABEL_PAGE_SIZE = 100;
|
|
535
|
+
var PROJECT_PULL_REQUEST_SUMMARY_CONCURRENCY = 8;
|
|
536
|
+
var PROJECT_PULL_REQUEST_PAGE_SIZE = 10;
|
|
537
|
+
var PROJECT_PULL_REQUEST_METRICS_BATCH_SIZE = 100;
|
|
538
|
+
var PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS = 30 * 6e4;
|
|
539
|
+
var PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS = 60 * 6e4;
|
|
540
|
+
var PROJECT_PULL_REQUEST_DETAIL_CACHE_TTL_MS = 30 * 6e4;
|
|
541
|
+
var PROJECT_PULL_REQUEST_ISSUE_LOOKUP_CACHE_TTL_MS = 60 * 6e4;
|
|
542
|
+
var PROJECT_PULL_REQUEST_GITHUB_INSIGHT_CACHE_TTL_MS = 60 * 6e4;
|
|
543
|
+
var PROJECT_PULL_REQUEST_BRANCH_COMPARE_CACHE_TTL_MS = 30 * 6e4;
|
|
544
|
+
var GITHUB_TOKEN_PERMISSION_AUDIT_CACHE_TTL_MS = 5 * 6e4;
|
|
528
545
|
var MANUAL_SYNC_RESPONSE_GRACE_PERIOD_MS = 500;
|
|
529
546
|
var RUNNING_SYNC_MESSAGE = "GitHub sync is running in the background. This page will update when it finishes.";
|
|
547
|
+
var CANCELLING_SYNC_MESSAGE = "Cancellation requested. GitHub sync will stop after the current step finishes.";
|
|
530
548
|
var SYNC_PROGRESS_PERSIST_INTERVAL_MS = 250;
|
|
549
|
+
var MAX_SYNC_FAILURE_LOG_ENTRIES = 25;
|
|
531
550
|
var GITHUB_SECONDARY_RATE_LIMIT_FALLBACK_MS = 6e4;
|
|
532
551
|
var MISSING_GITHUB_TOKEN_SYNC_MESSAGE = "Configure a GitHub token before running sync.";
|
|
533
552
|
var MISSING_GITHUB_TOKEN_SYNC_ACTION = 'Open settings and save a GitHub token secret, or create ~/.paperclip/plugins/github-sync/config.json with a "githubToken" value, and then run sync again.';
|
|
@@ -536,6 +555,7 @@ var MISSING_MAPPING_SYNC_ACTION = "Open settings, add a repository mapping, let
|
|
|
536
555
|
var MISSING_BOARD_ACCESS_SYNC_MESSAGE = "Connect Paperclip board access before running sync on this authenticated deployment.";
|
|
537
556
|
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.";
|
|
538
557
|
var ISSUE_LINK_ENTITY_TYPE = "paperclip-github-plugin.issue-link";
|
|
558
|
+
var PULL_REQUEST_LINK_ENTITY_TYPE = "paperclip-github-plugin.pull-request-link";
|
|
539
559
|
var COMMENT_ANNOTATION_ENTITY_TYPE = "paperclip-github-plugin.comment-annotation";
|
|
540
560
|
var EXTERNAL_CONFIG_FILE_PATH_SEGMENTS = [".paperclip", "plugins", "github-sync", "config.json"];
|
|
541
561
|
var AI_AUTHORED_COMMENT_FOOTER_PREFIX = "Created by a Paperclip AI agent using ";
|
|
@@ -546,6 +566,26 @@ var activeSyncPromise = null;
|
|
|
546
566
|
var activeRunningSyncState = null;
|
|
547
567
|
var activePaperclipApiAuthTokensByCompanyId = null;
|
|
548
568
|
var activeExternalConfigWarningKey = null;
|
|
569
|
+
var activeProjectPullRequestPageCache = /* @__PURE__ */ new Map();
|
|
570
|
+
var activeProjectPullRequestCountCache = /* @__PURE__ */ new Map();
|
|
571
|
+
var activeProjectPullRequestCountPromiseCache = /* @__PURE__ */ new Map();
|
|
572
|
+
var activeProjectPullRequestMetricsCache = /* @__PURE__ */ new Map();
|
|
573
|
+
var activeProjectPullRequestMetricsPromiseCache = /* @__PURE__ */ new Map();
|
|
574
|
+
var activeProjectPullRequestSummaryCache = /* @__PURE__ */ new Map();
|
|
575
|
+
var activeProjectPullRequestSummaryPromiseCache = /* @__PURE__ */ new Map();
|
|
576
|
+
var activeProjectPullRequestSummaryRecordCache = /* @__PURE__ */ new Map();
|
|
577
|
+
var activeProjectPullRequestDetailCache = /* @__PURE__ */ new Map();
|
|
578
|
+
var activeProjectPullRequestIssueLookupCache = /* @__PURE__ */ new Map();
|
|
579
|
+
var activeGitHubPullRequestStatusSnapshotCache = /* @__PURE__ */ new Map();
|
|
580
|
+
var activeGitHubPullRequestStatusSnapshotPromiseCache = /* @__PURE__ */ new Map();
|
|
581
|
+
var activeGitHubPullRequestReviewSummaryCache = /* @__PURE__ */ new Map();
|
|
582
|
+
var activeGitHubPullRequestReviewSummaryPromiseCache = /* @__PURE__ */ new Map();
|
|
583
|
+
var activeGitHubPullRequestReviewThreadSummaryCache = /* @__PURE__ */ new Map();
|
|
584
|
+
var activeGitHubPullRequestReviewThreadSummaryPromiseCache = /* @__PURE__ */ new Map();
|
|
585
|
+
var activeGitHubPullRequestBehindCountCache = /* @__PURE__ */ new Map();
|
|
586
|
+
var activeGitHubPullRequestBehindCountPromiseCache = /* @__PURE__ */ new Map();
|
|
587
|
+
var activeGitHubRepositoryTokenCapabilityAuditCache = /* @__PURE__ */ new Map();
|
|
588
|
+
var activeGitHubRepositoryTokenCapabilityAuditPromiseCache = /* @__PURE__ */ new Map();
|
|
549
589
|
var PaperclipLabelSyncError = class extends Error {
|
|
550
590
|
name = "PaperclipLabelSyncError";
|
|
551
591
|
status;
|
|
@@ -577,6 +617,14 @@ var PaperclipLabelSyncError = class extends Error {
|
|
|
577
617
|
this.labelNames = labelNames;
|
|
578
618
|
}
|
|
579
619
|
};
|
|
620
|
+
var SyncCancellationError = class extends Error {
|
|
621
|
+
name = "SyncCancellationError";
|
|
622
|
+
requestedAt;
|
|
623
|
+
constructor(requestedAt) {
|
|
624
|
+
super(CANCELLING_SYNC_MESSAGE);
|
|
625
|
+
this.requestedAt = requestedAt;
|
|
626
|
+
}
|
|
627
|
+
};
|
|
580
628
|
var SUCCESSFUL_CHECK_RUN_CONCLUSIONS = /* @__PURE__ */ new Set(["SUCCESS", "NEUTRAL", "SKIPPED"]);
|
|
581
629
|
var FAILED_CHECK_RUN_CONCLUSIONS = /* @__PURE__ */ new Set([
|
|
582
630
|
"ACTION_REQUIRED",
|
|
@@ -620,23 +668,6 @@ var GITHUB_ISSUE_STATUS_SNAPSHOT_QUERY = `
|
|
|
620
668
|
}
|
|
621
669
|
}
|
|
622
670
|
`;
|
|
623
|
-
var GITHUB_PULL_REQUEST_REVIEW_THREADS_QUERY = `
|
|
624
|
-
query GitHubPullRequestReviewThreads($owner: String!, $repo: String!, $pullRequestNumber: Int!, $after: String) {
|
|
625
|
-
repository(owner: $owner, name: $repo) {
|
|
626
|
-
pullRequest(number: $pullRequestNumber) {
|
|
627
|
-
reviewThreads(first: 100, after: $after) {
|
|
628
|
-
pageInfo {
|
|
629
|
-
hasNextPage
|
|
630
|
-
endCursor
|
|
631
|
-
}
|
|
632
|
-
nodes {
|
|
633
|
-
isResolved
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
`;
|
|
640
671
|
var GITHUB_PULL_REQUEST_CI_CONTEXTS_QUERY = `
|
|
641
672
|
query GitHubPullRequestCiContexts($owner: String!, $repo: String!, $pullRequestNumber: Int!, $after: String) {
|
|
642
673
|
repository(owner: $owner, name: $repo) {
|
|
@@ -736,6 +767,235 @@ var GITHUB_REPOSITORY_OPEN_PULL_REQUEST_STATUSES_QUERY = `
|
|
|
736
767
|
}
|
|
737
768
|
}
|
|
738
769
|
`;
|
|
770
|
+
var GITHUB_PROJECT_PULL_REQUEST_BASE_FIELDS = `
|
|
771
|
+
id
|
|
772
|
+
number
|
|
773
|
+
title
|
|
774
|
+
url
|
|
775
|
+
state
|
|
776
|
+
mergeable
|
|
777
|
+
mergeStateStatus
|
|
778
|
+
createdAt
|
|
779
|
+
updatedAt
|
|
780
|
+
baseRefName
|
|
781
|
+
headRefName
|
|
782
|
+
headRepositoryOwner {
|
|
783
|
+
login
|
|
784
|
+
}
|
|
785
|
+
changedFiles
|
|
786
|
+
commits {
|
|
787
|
+
totalCount
|
|
788
|
+
}
|
|
789
|
+
author {
|
|
790
|
+
login
|
|
791
|
+
url
|
|
792
|
+
avatarUrl
|
|
793
|
+
}
|
|
794
|
+
labels(first: 20) {
|
|
795
|
+
nodes {
|
|
796
|
+
name
|
|
797
|
+
color
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
comments {
|
|
801
|
+
totalCount
|
|
802
|
+
}
|
|
803
|
+
closingIssuesReferences(first: 10) {
|
|
804
|
+
nodes {
|
|
805
|
+
number
|
|
806
|
+
url
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
`;
|
|
810
|
+
var GITHUB_PROJECT_PULL_REQUEST_INSIGHT_FIELDS = `
|
|
811
|
+
reviews(first: 100) {
|
|
812
|
+
pageInfo {
|
|
813
|
+
hasNextPage
|
|
814
|
+
endCursor
|
|
815
|
+
}
|
|
816
|
+
nodes {
|
|
817
|
+
state
|
|
818
|
+
author {
|
|
819
|
+
login
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
reviewThreads(first: 100) {
|
|
824
|
+
totalCount
|
|
825
|
+
pageInfo {
|
|
826
|
+
hasNextPage
|
|
827
|
+
endCursor
|
|
828
|
+
}
|
|
829
|
+
nodes {
|
|
830
|
+
isResolved
|
|
831
|
+
comments(first: 1) {
|
|
832
|
+
nodes {
|
|
833
|
+
author {
|
|
834
|
+
login
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
statusCheckRollup {
|
|
841
|
+
contexts(first: 100) {
|
|
842
|
+
pageInfo {
|
|
843
|
+
hasNextPage
|
|
844
|
+
endCursor
|
|
845
|
+
}
|
|
846
|
+
nodes {
|
|
847
|
+
__typename
|
|
848
|
+
... on CheckRun {
|
|
849
|
+
status
|
|
850
|
+
conclusion
|
|
851
|
+
}
|
|
852
|
+
... on StatusContext {
|
|
853
|
+
state
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
`;
|
|
859
|
+
var GITHUB_PROJECT_PULL_REQUEST_SUMMARY_FIELDS = `${GITHUB_PROJECT_PULL_REQUEST_BASE_FIELDS}${GITHUB_PROJECT_PULL_REQUEST_INSIGHT_FIELDS}`;
|
|
860
|
+
var GITHUB_PROJECT_PULL_REQUEST_METRICS_FIELDS = `
|
|
861
|
+
number
|
|
862
|
+
mergeable
|
|
863
|
+
reviews(first: 100) {
|
|
864
|
+
pageInfo {
|
|
865
|
+
hasNextPage
|
|
866
|
+
endCursor
|
|
867
|
+
}
|
|
868
|
+
nodes {
|
|
869
|
+
state
|
|
870
|
+
author {
|
|
871
|
+
login
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
reviewThreads(first: 100) {
|
|
876
|
+
pageInfo {
|
|
877
|
+
hasNextPage
|
|
878
|
+
endCursor
|
|
879
|
+
}
|
|
880
|
+
nodes {
|
|
881
|
+
isResolved
|
|
882
|
+
comments(first: 1) {
|
|
883
|
+
nodes {
|
|
884
|
+
author {
|
|
885
|
+
login
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
statusCheckRollup {
|
|
892
|
+
contexts(first: 100) {
|
|
893
|
+
pageInfo {
|
|
894
|
+
hasNextPage
|
|
895
|
+
endCursor
|
|
896
|
+
}
|
|
897
|
+
nodes {
|
|
898
|
+
__typename
|
|
899
|
+
... on CheckRun {
|
|
900
|
+
status
|
|
901
|
+
conclusion
|
|
902
|
+
}
|
|
903
|
+
... on StatusContext {
|
|
904
|
+
state
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
`;
|
|
910
|
+
var GITHUB_PROJECT_PULL_REQUESTS_QUERY = `
|
|
911
|
+
query GitHubProjectPullRequests($owner: String!, $repo: String!, $after: String, $first: Int!) {
|
|
912
|
+
repository(owner: $owner, name: $repo) {
|
|
913
|
+
nameWithOwner
|
|
914
|
+
url
|
|
915
|
+
defaultBranchRef {
|
|
916
|
+
name
|
|
917
|
+
}
|
|
918
|
+
pullRequests(first: $first, after: $after, states: [OPEN], orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
919
|
+
totalCount
|
|
920
|
+
pageInfo {
|
|
921
|
+
hasNextPage
|
|
922
|
+
endCursor
|
|
923
|
+
}
|
|
924
|
+
nodes {
|
|
925
|
+
${GITHUB_PROJECT_PULL_REQUEST_SUMMARY_FIELDS}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
`;
|
|
931
|
+
var GITHUB_PROJECT_PULL_REQUEST_METRICS_QUERY = `
|
|
932
|
+
query GitHubProjectPullRequestMetrics($owner: String!, $repo: String!, $after: String, $first: Int!) {
|
|
933
|
+
repository(owner: $owner, name: $repo) {
|
|
934
|
+
defaultBranchRef {
|
|
935
|
+
name
|
|
936
|
+
}
|
|
937
|
+
pullRequests(first: $first, after: $after, states: [OPEN], orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
938
|
+
totalCount
|
|
939
|
+
pageInfo {
|
|
940
|
+
hasNextPage
|
|
941
|
+
endCursor
|
|
942
|
+
}
|
|
943
|
+
nodes {
|
|
944
|
+
${GITHUB_PROJECT_PULL_REQUEST_METRICS_FIELDS}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
`;
|
|
950
|
+
function buildGitHubProjectPullRequestByNumberAlias(pullRequestNumber) {
|
|
951
|
+
return `pr_${Math.max(1, Math.floor(pullRequestNumber))}`;
|
|
952
|
+
}
|
|
953
|
+
function buildGitHubProjectPullRequestsByNumberQuery(pullRequestNumbers) {
|
|
954
|
+
const normalizedNumbers = [
|
|
955
|
+
...new Set(
|
|
956
|
+
pullRequestNumbers.map((value) => Math.floor(value)).filter((value) => Number.isFinite(value) && value > 0)
|
|
957
|
+
)
|
|
958
|
+
];
|
|
959
|
+
if (normalizedNumbers.length === 0) {
|
|
960
|
+
throw new Error("At least one pull request number is required.");
|
|
961
|
+
}
|
|
962
|
+
const selections = normalizedNumbers.map(
|
|
963
|
+
(pullRequestNumber) => `
|
|
964
|
+
${buildGitHubProjectPullRequestByNumberAlias(pullRequestNumber)}: pullRequest(number: ${pullRequestNumber}) {
|
|
965
|
+
${GITHUB_PROJECT_PULL_REQUEST_BASE_FIELDS}
|
|
966
|
+
}`
|
|
967
|
+
).join("\n");
|
|
968
|
+
return `
|
|
969
|
+
query GitHubProjectPullRequestsByNumber($owner: String!, $repo: String!) {
|
|
970
|
+
repository(owner: $owner, name: $repo) {
|
|
971
|
+
${selections}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
`;
|
|
975
|
+
}
|
|
976
|
+
var GITHUB_PROJECT_OPEN_PULL_REQUEST_COUNT_QUERY = `
|
|
977
|
+
query GitHubProjectOpenPullRequestCount($owner: String!, $repo: String!) {
|
|
978
|
+
repository(owner: $owner, name: $repo) {
|
|
979
|
+
pullRequests(first: 1, states: [OPEN]) {
|
|
980
|
+
totalCount
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
`;
|
|
985
|
+
var GITHUB_PULL_REQUEST_CLOSING_ISSUES_QUERY = `
|
|
986
|
+
query GitHubPullRequestClosingIssues($owner: String!, $repo: String!, $pullRequestNumber: Int!) {
|
|
987
|
+
repository(owner: $owner, name: $repo) {
|
|
988
|
+
pullRequest(number: $pullRequestNumber) {
|
|
989
|
+
closingIssuesReferences(first: 10) {
|
|
990
|
+
nodes {
|
|
991
|
+
number
|
|
992
|
+
url
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
`;
|
|
739
999
|
var GITHUB_PULL_REQUEST_REVIEW_THREADS_DETAILED_QUERY = `
|
|
740
1000
|
query GitHubPullRequestReviewThreadsDetailed($owner: String!, $repo: String!, $pullRequestNumber: Int!, $after: String) {
|
|
741
1001
|
repository(owner: $owner, name: $repo) {
|
|
@@ -846,6 +1106,30 @@ var GITHUB_MARK_PULL_REQUEST_READY_FOR_REVIEW_MUTATION = `
|
|
|
846
1106
|
}
|
|
847
1107
|
}
|
|
848
1108
|
`;
|
|
1109
|
+
var GITHUB_REQUEST_PULL_REQUEST_COPILOT_REVIEW_MUTATION = `
|
|
1110
|
+
mutation GitHubRequestPullRequestCopilotReview($pullRequestId: ID!, $botLogins: [String!]!) {
|
|
1111
|
+
requestReviews(input: {
|
|
1112
|
+
pullRequestId: $pullRequestId
|
|
1113
|
+
botLogins: $botLogins
|
|
1114
|
+
}) {
|
|
1115
|
+
pullRequest {
|
|
1116
|
+
id
|
|
1117
|
+
number
|
|
1118
|
+
url
|
|
1119
|
+
}
|
|
1120
|
+
requestedReviewers(first: 10) {
|
|
1121
|
+
edges {
|
|
1122
|
+
node {
|
|
1123
|
+
__typename
|
|
1124
|
+
... on Bot {
|
|
1125
|
+
login
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
`;
|
|
849
1133
|
var DEFAULT_SETTINGS = {
|
|
850
1134
|
mappings: [],
|
|
851
1135
|
syncState: {
|
|
@@ -884,6 +1168,20 @@ function createIdleSyncState() {
|
|
|
884
1168
|
status: "idle"
|
|
885
1169
|
};
|
|
886
1170
|
}
|
|
1171
|
+
function createCancelledSyncState(params) {
|
|
1172
|
+
const { message, trigger, syncedIssuesCount, createdIssuesCount, skippedIssuesCount, erroredIssuesCount, progress } = params;
|
|
1173
|
+
return {
|
|
1174
|
+
status: "cancelled",
|
|
1175
|
+
message,
|
|
1176
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1177
|
+
syncedIssuesCount,
|
|
1178
|
+
createdIssuesCount,
|
|
1179
|
+
skippedIssuesCount,
|
|
1180
|
+
erroredIssuesCount,
|
|
1181
|
+
lastRunTrigger: trigger,
|
|
1182
|
+
...progress ? { progress: normalizeSyncProgress(progress) } : {}
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
887
1185
|
function formatGitHubIssueCountLabel(count) {
|
|
888
1186
|
const normalizedCount = Math.max(0, Math.floor(count));
|
|
889
1187
|
return `${normalizedCount} GitHub ${normalizedCount === 1 ? "issue" : "issues"}`;
|
|
@@ -931,6 +1229,118 @@ function getErrorResponseDataMessage(error) {
|
|
|
931
1229
|
const message = data.message;
|
|
932
1230
|
return typeof message === "string" && message.trim() ? message.trim() : void 0;
|
|
933
1231
|
}
|
|
1232
|
+
function getErrorResponseDataErrors(error) {
|
|
1233
|
+
if (!error || typeof error !== "object" || !("response" in error)) {
|
|
1234
|
+
return [];
|
|
1235
|
+
}
|
|
1236
|
+
const response = error.response;
|
|
1237
|
+
if (!response || typeof response !== "object" || !("data" in response)) {
|
|
1238
|
+
return [];
|
|
1239
|
+
}
|
|
1240
|
+
const data = response.data;
|
|
1241
|
+
if (!data || typeof data !== "object" || !("errors" in data)) {
|
|
1242
|
+
return [];
|
|
1243
|
+
}
|
|
1244
|
+
const errors = data.errors;
|
|
1245
|
+
return Array.isArray(errors) ? errors : [];
|
|
1246
|
+
}
|
|
1247
|
+
function getGitHubValidationErrorSummary(error) {
|
|
1248
|
+
const entries = getErrorResponseDataErrors(error);
|
|
1249
|
+
if (entries.length === 0) {
|
|
1250
|
+
return void 0;
|
|
1251
|
+
}
|
|
1252
|
+
const summaries = /* @__PURE__ */ new Set();
|
|
1253
|
+
for (const entry of entries) {
|
|
1254
|
+
if (typeof entry === "string" && entry.trim()) {
|
|
1255
|
+
summaries.add(entry.trim());
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
if (!entry || typeof entry !== "object") {
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
const explicitMessage = "message" in entry && typeof entry.message === "string" ? entry.message.trim() : "";
|
|
1262
|
+
if (explicitMessage) {
|
|
1263
|
+
summaries.add(explicitMessage);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
const resource = "resource" in entry && typeof entry.resource === "string" ? entry.resource.trim() : "";
|
|
1267
|
+
const field = "field" in entry && typeof entry.field === "string" ? entry.field.trim() : "";
|
|
1268
|
+
const code = "code" in entry && typeof entry.code === "string" ? entry.code.trim().replace(/_/g, " ") : "";
|
|
1269
|
+
const parts = [
|
|
1270
|
+
resource ? resource.replace(/([a-z])([A-Z])/g, "$1 $2") : "",
|
|
1271
|
+
field ? `field "${field}"` : "",
|
|
1272
|
+
code
|
|
1273
|
+
].filter(Boolean);
|
|
1274
|
+
if (parts.length > 0) {
|
|
1275
|
+
summaries.add(parts.join(" "));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return summaries.size > 0 ? [...summaries].join("; ") : void 0;
|
|
1279
|
+
}
|
|
1280
|
+
function formatGitHubPermissionsHeader(value) {
|
|
1281
|
+
if (!value?.trim()) {
|
|
1282
|
+
return void 0;
|
|
1283
|
+
}
|
|
1284
|
+
const parts = value.replace(/;/g, ",").split(",").map((entry) => entry.trim()).filter(Boolean).map((entry) => {
|
|
1285
|
+
const [name, level] = entry.split("=").map((part) => part.trim());
|
|
1286
|
+
if (!name) {
|
|
1287
|
+
return "";
|
|
1288
|
+
}
|
|
1289
|
+
const normalizedName = name.replace(/_/g, " ");
|
|
1290
|
+
return level ? `${normalizedName}: ${level}` : normalizedName;
|
|
1291
|
+
}).filter(Boolean);
|
|
1292
|
+
return parts.length > 0 ? parts.join(", ") : void 0;
|
|
1293
|
+
}
|
|
1294
|
+
function getAcceptedGitHubPermissionsSummary(error) {
|
|
1295
|
+
const headers = getErrorResponseHeaders(error);
|
|
1296
|
+
return formatGitHubPermissionsHeader(headers["x-accepted-github-permissions"]) ?? formatGitHubPermissionsHeader(headers["x-accepted-oauth-scopes"]);
|
|
1297
|
+
}
|
|
1298
|
+
function buildGitHubPullRequestWriteActionError(params) {
|
|
1299
|
+
const rateLimitPause = getGitHubRateLimitPauseDetails(params.error);
|
|
1300
|
+
if (rateLimitPause) {
|
|
1301
|
+
const resourceLabel = formatGitHubRateLimitResource(rateLimitPause.resource) ?? "GitHub API";
|
|
1302
|
+
return new Error(`${resourceLabel} rate limit reached. Wait until ${formatUtcTimestamp(rateLimitPause.resetAt)} before retrying.`);
|
|
1303
|
+
}
|
|
1304
|
+
const actionLabel = params.action === "comment" ? "comment" : params.action === "review" ? "review" : "branch update";
|
|
1305
|
+
const rawMessage = getErrorMessage(params.error).trim();
|
|
1306
|
+
const responseMessage = getErrorResponseDataMessage(params.error);
|
|
1307
|
+
const validationSummary = getGitHubValidationErrorSummary(params.error);
|
|
1308
|
+
const permissionsSummary = getAcceptedGitHubPermissionsSummary(params.error);
|
|
1309
|
+
const status = getErrorStatus(params.error);
|
|
1310
|
+
const combinedMessage = [rawMessage, responseMessage].filter((value) => Boolean(value?.trim())).join(" ").toLowerCase();
|
|
1311
|
+
if (params.action === "review" && params.reviewType === "REQUEST_CHANGES" && !params.body?.trim() && status === 422) {
|
|
1312
|
+
return new Error("Add a review summary before requesting changes. GitHub requires a comment for this action.");
|
|
1313
|
+
}
|
|
1314
|
+
if ((status === 403 || status === 404) && (combinedMessage.includes("resource not accessible") || combinedMessage.includes("not accessible by"))) {
|
|
1315
|
+
const requiredAccess = params.action === "comment" ? "Issues: write" : "Pull requests: write";
|
|
1316
|
+
const permissionSuffix = permissionsSummary ? ` GitHub reported required permissions: ${permissionsSummary}.` : "";
|
|
1317
|
+
return new Error(
|
|
1318
|
+
`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}`
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
if (params.action === "update_branch" && status === 422) {
|
|
1322
|
+
if (combinedMessage.includes("expected head sha") || validationSummary?.toLowerCase().includes("expected_head_sha")) {
|
|
1323
|
+
return new Error("This pull request changed while the branch update was being requested. Refresh the queue and try again.");
|
|
1324
|
+
}
|
|
1325
|
+
if (combinedMessage.includes("merge conflict") || combinedMessage.includes("conflict")) {
|
|
1326
|
+
return new Error("This pull request needs conflict resolution before it can be updated with the base branch.");
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
if (responseMessage === "Validation Failed" && validationSummary) {
|
|
1330
|
+
return new Error(`GitHub rejected this ${actionLabel}: ${validationSummary}.`);
|
|
1331
|
+
}
|
|
1332
|
+
if (responseMessage && responseMessage !== rawMessage) {
|
|
1333
|
+
const validationSuffix = validationSummary ? ` ${validationSummary}.` : "";
|
|
1334
|
+
return new Error(`GitHub rejected this ${actionLabel}: ${responseMessage}.${validationSuffix}`);
|
|
1335
|
+
}
|
|
1336
|
+
if (validationSummary) {
|
|
1337
|
+
return new Error(`GitHub rejected this ${actionLabel}: ${validationSummary}.`);
|
|
1338
|
+
}
|
|
1339
|
+
if (rawMessage) {
|
|
1340
|
+
return new Error(rawMessage);
|
|
1341
|
+
}
|
|
1342
|
+
return new Error(`GitHub rejected this ${actionLabel}.`);
|
|
1343
|
+
}
|
|
934
1344
|
function parsePositiveInteger(value) {
|
|
935
1345
|
if (!value?.trim()) {
|
|
936
1346
|
return void 0;
|
|
@@ -952,6 +1362,58 @@ function parseRetryAfterTimestamp(value, now = Date.now()) {
|
|
|
952
1362
|
const timestamp = Date.parse(value);
|
|
953
1363
|
return Number.isFinite(timestamp) ? timestamp : void 0;
|
|
954
1364
|
}
|
|
1365
|
+
function buildGitHubRepositoryTokenCapabilityAuditCacheKey(repository, samplePullRequestNumber) {
|
|
1366
|
+
return `${repository.url.toLowerCase()}::${typeof samplePullRequestNumber === "number" ? samplePullRequestNumber : "none"}`;
|
|
1367
|
+
}
|
|
1368
|
+
function clearGitHubRepositoryTokenCapabilityAudits() {
|
|
1369
|
+
activeGitHubRepositoryTokenCapabilityAuditCache.clear();
|
|
1370
|
+
activeGitHubRepositoryTokenCapabilityAuditPromiseCache.clear();
|
|
1371
|
+
}
|
|
1372
|
+
function getGitHubCapabilityMissingPermissionLabel(capability) {
|
|
1373
|
+
switch (capability) {
|
|
1374
|
+
case "comment":
|
|
1375
|
+
return "Issues: write or Pull requests: write";
|
|
1376
|
+
case "review":
|
|
1377
|
+
case "close":
|
|
1378
|
+
case "update_branch":
|
|
1379
|
+
return "Pull requests: write";
|
|
1380
|
+
case "merge":
|
|
1381
|
+
return "Contents: write";
|
|
1382
|
+
case "rerun_ci":
|
|
1383
|
+
return "Checks: write";
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
function classifyGitHubCapabilityProbeError(error, options = {}) {
|
|
1387
|
+
const status = getErrorStatus(error);
|
|
1388
|
+
if (status && options.grantedStatuses?.includes(status)) {
|
|
1389
|
+
return "granted";
|
|
1390
|
+
}
|
|
1391
|
+
if (status === 404 && options.allowNotFoundAsGranted) {
|
|
1392
|
+
return "granted";
|
|
1393
|
+
}
|
|
1394
|
+
if (status === 401 || status === 403 || status === 404) {
|
|
1395
|
+
return "missing";
|
|
1396
|
+
}
|
|
1397
|
+
return "unknown";
|
|
1398
|
+
}
|
|
1399
|
+
function buildGitHubRepositoryTokenCapabilityAudit(params) {
|
|
1400
|
+
const warnings = [...new Set((params.warnings ?? []).map((warning) => warning.trim()).filter(Boolean))];
|
|
1401
|
+
return {
|
|
1402
|
+
repositoryUrl: params.repository.url,
|
|
1403
|
+
repositoryLabel: formatRepositoryLabel(params.repository),
|
|
1404
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1405
|
+
status: params.missingPermissions.length > 0 ? "missing_permissions" : warnings.length > 0 ? "unverifiable" : "verified",
|
|
1406
|
+
...typeof params.samplePullRequestNumber === "number" ? { samplePullRequestNumber: params.samplePullRequestNumber } : {},
|
|
1407
|
+
canComment: params.canComment,
|
|
1408
|
+
canReview: params.canReview,
|
|
1409
|
+
canClose: params.canClose,
|
|
1410
|
+
canUpdateBranch: params.canUpdateBranch,
|
|
1411
|
+
canMerge: params.canMerge,
|
|
1412
|
+
canRerunCi: params.canRerunCi,
|
|
1413
|
+
missingPermissions: [...new Set(params.missingPermissions)].sort((left, right) => left.localeCompare(right)),
|
|
1414
|
+
warnings
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
955
1417
|
function normalizeSecretRef(value) {
|
|
956
1418
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
957
1419
|
}
|
|
@@ -997,7 +1459,10 @@ function getGitHubRateLimitPauseDetails(error, now = Date.now()) {
|
|
|
997
1459
|
const retryAfterTimestamp = parseRetryAfterTimestamp(headers["retry-after"], now);
|
|
998
1460
|
const responseMessage = getErrorResponseDataMessage(error);
|
|
999
1461
|
const rawMessage = [getErrorMessage(error), responseMessage].filter((value) => Boolean(value?.trim())).join(" ").toLowerCase();
|
|
1000
|
-
const
|
|
1462
|
+
const hasPrimaryLimitHeaders = remaining === "0" && resetAtSeconds !== void 0;
|
|
1463
|
+
const hasRetryAfterLimitHint = retryAfterTimestamp !== void 0;
|
|
1464
|
+
const hasRateLimitMessage = rawMessage.includes("rate limit");
|
|
1465
|
+
const looksRateLimited = status === 429 || hasPrimaryLimitHeaders || hasRetryAfterLimitHint || hasRateLimitMessage;
|
|
1001
1466
|
if (!looksRateLimited) {
|
|
1002
1467
|
return null;
|
|
1003
1468
|
}
|
|
@@ -1047,6 +1512,7 @@ function normalizeSyncConfigurationIssue(value) {
|
|
|
1047
1512
|
switch (value) {
|
|
1048
1513
|
case "missing_token":
|
|
1049
1514
|
case "missing_mapping":
|
|
1515
|
+
case "missing_board_access":
|
|
1050
1516
|
return value;
|
|
1051
1517
|
default:
|
|
1052
1518
|
return void 0;
|
|
@@ -1120,19 +1586,43 @@ function normalizeSyncErrorDetails(value) {
|
|
|
1120
1586
|
...rateLimitResource ? { rateLimitResource } : {}
|
|
1121
1587
|
};
|
|
1122
1588
|
}
|
|
1123
|
-
function
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1589
|
+
function normalizeSyncFailureLogEntry(value) {
|
|
1590
|
+
if (!value || typeof value !== "object") {
|
|
1591
|
+
return void 0;
|
|
1592
|
+
}
|
|
1593
|
+
const record = value;
|
|
1594
|
+
const details = normalizeSyncErrorDetails(record);
|
|
1595
|
+
const message = typeof record.message === "string" && record.message.trim() ? record.message.trim() : void 0;
|
|
1596
|
+
const occurredAt = typeof record.occurredAt === "string" && record.occurredAt.trim() ? record.occurredAt.trim() : void 0;
|
|
1597
|
+
if (!message || !occurredAt) {
|
|
1598
|
+
return void 0;
|
|
1599
|
+
}
|
|
1600
|
+
return {
|
|
1601
|
+
message,
|
|
1602
|
+
occurredAt,
|
|
1603
|
+
...details ?? {}
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
function normalizeSyncFailureLogEntries(value) {
|
|
1607
|
+
if (!Array.isArray(value)) {
|
|
1608
|
+
return void 0;
|
|
1609
|
+
}
|
|
1610
|
+
const entries = value.map((entry) => normalizeSyncFailureLogEntry(entry)).filter((entry) => entry !== void 0).slice(-MAX_SYNC_FAILURE_LOG_ENTRIES);
|
|
1611
|
+
return entries.length > 0 ? entries : void 0;
|
|
1612
|
+
}
|
|
1613
|
+
function formatSyncFailurePhase(phase) {
|
|
1614
|
+
switch (phase) {
|
|
1615
|
+
case "configuration":
|
|
1616
|
+
return "checking sync configuration";
|
|
1617
|
+
case "loading_paperclip_labels":
|
|
1618
|
+
return "loading Paperclip labels";
|
|
1619
|
+
case "listing_github_issues":
|
|
1620
|
+
return "listing GitHub issues";
|
|
1621
|
+
case "building_import_plan":
|
|
1622
|
+
return "building the GitHub import plan";
|
|
1623
|
+
case "importing_issue":
|
|
1624
|
+
return "importing a GitHub issue";
|
|
1625
|
+
case "syncing_labels":
|
|
1136
1626
|
return "syncing issue labels";
|
|
1137
1627
|
case "syncing_description":
|
|
1138
1628
|
return "syncing issue descriptions";
|
|
@@ -1274,8 +1764,49 @@ function buildSyncErrorDetails(error, context) {
|
|
|
1274
1764
|
...rateLimitPause?.resource ? { rateLimitResource: rateLimitPause.resource } : {}
|
|
1275
1765
|
};
|
|
1276
1766
|
}
|
|
1767
|
+
function createSyncFailureLogEntry(params) {
|
|
1768
|
+
const message = params.message.trim();
|
|
1769
|
+
const occurredAt = typeof params.occurredAt === "string" && params.occurredAt.trim() ? params.occurredAt.trim() : (/* @__PURE__ */ new Date()).toISOString();
|
|
1770
|
+
const errorDetails = normalizeSyncErrorDetails(params.errorDetails);
|
|
1771
|
+
if (!message) {
|
|
1772
|
+
return void 0;
|
|
1773
|
+
}
|
|
1774
|
+
return {
|
|
1775
|
+
message,
|
|
1776
|
+
occurredAt,
|
|
1777
|
+
...errorDetails ?? {}
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
function buildSyncFailureLogEntry(error, context, occurredAt) {
|
|
1781
|
+
return createSyncFailureLogEntry({
|
|
1782
|
+
message: buildSyncFailureMessage(error, context),
|
|
1783
|
+
occurredAt,
|
|
1784
|
+
errorDetails: buildSyncErrorDetails(error, context)
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
function buildRecentSyncFailureLogEntries(failures) {
|
|
1788
|
+
const entries = failures.slice(-MAX_SYNC_FAILURE_LOG_ENTRIES).map((failure) => buildSyncFailureLogEntry(failure.error, failure.context, failure.occurredAt)).filter((entry) => entry !== void 0);
|
|
1789
|
+
return entries.length > 0 ? entries : void 0;
|
|
1790
|
+
}
|
|
1791
|
+
function appendRecentSyncFailureLogEntry(entries, entry) {
|
|
1792
|
+
if (!entry) {
|
|
1793
|
+
return entries;
|
|
1794
|
+
}
|
|
1795
|
+
return [...entries ?? [], entry].slice(-MAX_SYNC_FAILURE_LOG_ENTRIES);
|
|
1796
|
+
}
|
|
1277
1797
|
function createErrorSyncState(params) {
|
|
1278
|
-
const {
|
|
1798
|
+
const {
|
|
1799
|
+
message,
|
|
1800
|
+
trigger,
|
|
1801
|
+
syncedIssuesCount,
|
|
1802
|
+
createdIssuesCount,
|
|
1803
|
+
skippedIssuesCount,
|
|
1804
|
+
erroredIssuesCount,
|
|
1805
|
+
progress,
|
|
1806
|
+
errorDetails,
|
|
1807
|
+
recentFailures
|
|
1808
|
+
} = params;
|
|
1809
|
+
const normalizedRecentFailures = normalizeSyncFailureLogEntries(recentFailures);
|
|
1279
1810
|
return {
|
|
1280
1811
|
status: "error",
|
|
1281
1812
|
message,
|
|
@@ -1286,20 +1817,27 @@ function createErrorSyncState(params) {
|
|
|
1286
1817
|
erroredIssuesCount,
|
|
1287
1818
|
lastRunTrigger: trigger,
|
|
1288
1819
|
...progress ? { progress: normalizeSyncProgress(progress) } : {},
|
|
1289
|
-
...errorDetails ? { errorDetails } : {}
|
|
1820
|
+
...errorDetails ? { errorDetails } : {},
|
|
1821
|
+
...normalizedRecentFailures ? { recentFailures: normalizedRecentFailures } : {}
|
|
1290
1822
|
};
|
|
1291
1823
|
}
|
|
1292
1824
|
function createRunningSyncState(previous, trigger, options = {}) {
|
|
1825
|
+
const previousRunningState = previous.status === "running" ? previous : void 0;
|
|
1826
|
+
const nextMessage = options.message ?? previousRunningState?.message ?? RUNNING_SYNC_MESSAGE;
|
|
1827
|
+
const nextCancelRequestedAt = options.cancelRequestedAt ?? previousRunningState?.cancelRequestedAt;
|
|
1828
|
+
const normalizedRecentFailures = normalizeSyncFailureLogEntries(options.recentFailures);
|
|
1293
1829
|
return {
|
|
1294
1830
|
status: "running",
|
|
1295
|
-
message:
|
|
1831
|
+
message: nextMessage,
|
|
1296
1832
|
checkedAt: previous.checkedAt,
|
|
1297
1833
|
syncedIssuesCount: options.syncedIssuesCount ?? 0,
|
|
1298
1834
|
createdIssuesCount: options.createdIssuesCount ?? 0,
|
|
1299
1835
|
skippedIssuesCount: options.skippedIssuesCount ?? 0,
|
|
1300
1836
|
erroredIssuesCount: options.erroredIssuesCount ?? 0,
|
|
1301
1837
|
lastRunTrigger: trigger,
|
|
1302
|
-
...
|
|
1838
|
+
...nextCancelRequestedAt ? { cancelRequestedAt: nextCancelRequestedAt } : {},
|
|
1839
|
+
...options.progress ? { progress: normalizeSyncProgress(options.progress) } : {},
|
|
1840
|
+
...normalizedRecentFailures ? { recentFailures: normalizedRecentFailures } : {}
|
|
1303
1841
|
};
|
|
1304
1842
|
}
|
|
1305
1843
|
function getSyncableMappings(mappings) {
|
|
@@ -1341,6 +1879,96 @@ function getSyncableMappingsForTarget(mappings, target) {
|
|
|
1341
1879
|
return syncableMappings;
|
|
1342
1880
|
}
|
|
1343
1881
|
}
|
|
1882
|
+
function normalizeProjectNameForComparison(value) {
|
|
1883
|
+
if (typeof value !== "string") {
|
|
1884
|
+
return "";
|
|
1885
|
+
}
|
|
1886
|
+
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
1887
|
+
}
|
|
1888
|
+
function hydrateResolvedProjectMapping(mapping, context) {
|
|
1889
|
+
const resolvedProjectName = mapping.paperclipProjectName.trim() || context.projectName?.trim() || mapping.paperclipProjectName;
|
|
1890
|
+
return {
|
|
1891
|
+
...mapping,
|
|
1892
|
+
companyId: mapping.companyId ?? context.companyId,
|
|
1893
|
+
paperclipProjectId: mapping.paperclipProjectId ?? context.projectId,
|
|
1894
|
+
paperclipProjectName: resolvedProjectName
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
function getProjectRepositoryUrlFromProjectRecord(project) {
|
|
1898
|
+
if (!project) {
|
|
1899
|
+
return void 0;
|
|
1900
|
+
}
|
|
1901
|
+
const repositoryCandidates = [
|
|
1902
|
+
project.codebase?.repoUrl,
|
|
1903
|
+
project.primaryWorkspace?.repoUrl,
|
|
1904
|
+
...Array.isArray(project.workspaces) ? project.workspaces.map((workspace) => workspace?.repoUrl) : []
|
|
1905
|
+
];
|
|
1906
|
+
for (const repositoryUrl of repositoryCandidates) {
|
|
1907
|
+
if (typeof repositoryUrl !== "string" || !repositoryUrl.trim()) {
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
const normalizedRepositoryUrl = parseRepositoryReference(repositoryUrl)?.url ?? repositoryUrl.trim();
|
|
1911
|
+
if (parseRepositoryReference(normalizedRepositoryUrl)) {
|
|
1912
|
+
return normalizedRepositoryUrl;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
return void 0;
|
|
1916
|
+
}
|
|
1917
|
+
async function resolveProjectScopedMappings(ctx, mappings, params) {
|
|
1918
|
+
const companyId = normalizeCompanyId(params.companyId);
|
|
1919
|
+
const projectId = typeof params.projectId === "string" && params.projectId.trim() ? params.projectId.trim() : void 0;
|
|
1920
|
+
if (!companyId || !projectId) {
|
|
1921
|
+
return [];
|
|
1922
|
+
}
|
|
1923
|
+
const candidateMappings = mappings.filter((mapping) => mapping.repositoryUrl.trim());
|
|
1924
|
+
const exactMatches = candidateMappings.filter(
|
|
1925
|
+
(mapping) => mapping.paperclipProjectId === projectId && (!mapping.companyId || mapping.companyId === companyId)
|
|
1926
|
+
).map((mapping) => hydrateResolvedProjectMapping(mapping, {
|
|
1927
|
+
companyId,
|
|
1928
|
+
projectId
|
|
1929
|
+
}));
|
|
1930
|
+
if (exactMatches.length > 0) {
|
|
1931
|
+
return exactMatches;
|
|
1932
|
+
}
|
|
1933
|
+
const namedFallbackCandidates = candidateMappings.filter(
|
|
1934
|
+
(mapping) => !mapping.paperclipProjectId && mapping.companyId === companyId && Boolean(normalizeProjectNameForComparison(mapping.paperclipProjectName))
|
|
1935
|
+
);
|
|
1936
|
+
let projectName = "";
|
|
1937
|
+
let projectRepositoryUrl;
|
|
1938
|
+
try {
|
|
1939
|
+
const project = await ctx.projects.get(projectId, companyId);
|
|
1940
|
+
projectName = typeof project?.name === "string" ? project.name.trim() : "";
|
|
1941
|
+
projectRepositoryUrl = getProjectRepositoryUrlFromProjectRecord(project);
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
ctx.logger.warn("Unable to resolve Paperclip project metadata for GitHub project mapping fallback.", {
|
|
1944
|
+
companyId,
|
|
1945
|
+
projectId,
|
|
1946
|
+
error: getErrorMessage(error)
|
|
1947
|
+
});
|
|
1948
|
+
return [];
|
|
1949
|
+
}
|
|
1950
|
+
const normalizedProjectName = normalizeProjectNameForComparison(projectName);
|
|
1951
|
+
if (normalizedProjectName) {
|
|
1952
|
+
const namedFallbackMatches = namedFallbackCandidates.filter((mapping) => normalizeProjectNameForComparison(mapping.paperclipProjectName) === normalizedProjectName).map((mapping) => hydrateResolvedProjectMapping(mapping, {
|
|
1953
|
+
companyId,
|
|
1954
|
+
projectId,
|
|
1955
|
+
projectName
|
|
1956
|
+
}));
|
|
1957
|
+
if (namedFallbackMatches.length > 0) {
|
|
1958
|
+
return namedFallbackMatches;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if (!projectRepositoryUrl) {
|
|
1962
|
+
return [];
|
|
1963
|
+
}
|
|
1964
|
+
return [{
|
|
1965
|
+
id: `project-repo:${companyId}:${projectId}`,
|
|
1966
|
+
repositoryUrl: projectRepositoryUrl,
|
|
1967
|
+
paperclipProjectName: projectName || "Project",
|
|
1968
|
+
paperclipProjectId: projectId,
|
|
1969
|
+
companyId
|
|
1970
|
+
}];
|
|
1971
|
+
}
|
|
1344
1972
|
function doesGitHubIssueMatchTarget(issue, target) {
|
|
1345
1973
|
if (!target || target.kind !== "issue") {
|
|
1346
1974
|
return true;
|
|
@@ -1523,7 +2151,7 @@ function extractGitHubLinksFromCommentBody(body) {
|
|
|
1523
2151
|
return [...links.values()];
|
|
1524
2152
|
}
|
|
1525
2153
|
async function buildToolbarSyncState(ctx, input) {
|
|
1526
|
-
const settings =
|
|
2154
|
+
const settings = await getActiveOrCurrentSyncState(ctx);
|
|
1527
2155
|
const config = await getResolvedConfig(ctx);
|
|
1528
2156
|
const githubTokenConfigured = hasConfiguredGithubToken(settings, config);
|
|
1529
2157
|
const companyId = typeof input.companyId === "string" && input.companyId.trim() ? input.companyId.trim() : void 0;
|
|
@@ -1771,6 +2399,93 @@ async function listAvailableAssignees(ctx, companyId) {
|
|
|
1771
2399
|
return [];
|
|
1772
2400
|
}
|
|
1773
2401
|
}
|
|
2402
|
+
async function resolvePaperclipIssueDrawerAgents(ctx, companyId, agentIds) {
|
|
2403
|
+
const normalizedAgentIds = [
|
|
2404
|
+
...new Set(
|
|
2405
|
+
agentIds.filter((agentId) => typeof agentId === "string" && agentId.trim().length > 0).map((agentId) => agentId.trim())
|
|
2406
|
+
)
|
|
2407
|
+
];
|
|
2408
|
+
const agentsById = /* @__PURE__ */ new Map();
|
|
2409
|
+
if (normalizedAgentIds.length === 0 || !ctx.agents || typeof ctx.agents.get !== "function") {
|
|
2410
|
+
return agentsById;
|
|
2411
|
+
}
|
|
2412
|
+
await Promise.all(
|
|
2413
|
+
normalizedAgentIds.map(async (agentId) => {
|
|
2414
|
+
try {
|
|
2415
|
+
const agent = await ctx.agents.get(agentId, companyId);
|
|
2416
|
+
if (!agent) {
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
agentsById.set(agent.id, {
|
|
2420
|
+
id: agent.id,
|
|
2421
|
+
name: agent.name,
|
|
2422
|
+
...agent.title?.trim() ? { title: agent.title.trim() } : {}
|
|
2423
|
+
});
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
ctx.logger.warn("Unable to load Paperclip agent for pull request issue drawer.", {
|
|
2426
|
+
companyId,
|
|
2427
|
+
agentId,
|
|
2428
|
+
error: getErrorMessage(error)
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
})
|
|
2432
|
+
);
|
|
2433
|
+
return agentsById;
|
|
2434
|
+
}
|
|
2435
|
+
async function buildProjectPullRequestPaperclipIssueData(ctx, input) {
|
|
2436
|
+
const issueId = typeof input.issueId === "string" && input.issueId.trim() ? input.issueId.trim() : void 0;
|
|
2437
|
+
const companyId = typeof input.companyId === "string" && input.companyId.trim() ? input.companyId.trim() : void 0;
|
|
2438
|
+
if (!issueId || !companyId) {
|
|
2439
|
+
return null;
|
|
2440
|
+
}
|
|
2441
|
+
const issue = await ctx.issues.get(issueId, companyId);
|
|
2442
|
+
if (!issue) {
|
|
2443
|
+
return null;
|
|
2444
|
+
}
|
|
2445
|
+
const comments = ctx.issues && typeof ctx.issues.listComments === "function" ? await ctx.issues.listComments(issue.id, companyId) : [];
|
|
2446
|
+
const agentsById = await resolvePaperclipIssueDrawerAgents(ctx, companyId, [
|
|
2447
|
+
issue.assigneeAgentId,
|
|
2448
|
+
issue.createdByAgentId,
|
|
2449
|
+
...comments.map((comment) => comment.authorAgentId)
|
|
2450
|
+
]);
|
|
2451
|
+
const assignee = issue.assigneeAgentId ? agentsById.get(issue.assigneeAgentId) ?? null : null;
|
|
2452
|
+
const orderedComments = [...comments].sort(
|
|
2453
|
+
(left, right) => coerceDate(left.createdAt).getTime() - coerceDate(right.createdAt).getTime()
|
|
2454
|
+
);
|
|
2455
|
+
return {
|
|
2456
|
+
issueId: issue.id,
|
|
2457
|
+
...issue.identifier?.trim() ? { issueIdentifier: issue.identifier.trim() } : {},
|
|
2458
|
+
title: issue.title,
|
|
2459
|
+
description: issue.description ?? "",
|
|
2460
|
+
status: issue.status,
|
|
2461
|
+
priority: issue.priority,
|
|
2462
|
+
projectName: issue.project?.name ?? void 0,
|
|
2463
|
+
createdAt: coerceDate(issue.createdAt).toISOString(),
|
|
2464
|
+
updatedAt: coerceDate(issue.updatedAt).toISOString(),
|
|
2465
|
+
labels: Array.isArray(issue.labels) ? issue.labels.map((label) => ({
|
|
2466
|
+
name: label.name,
|
|
2467
|
+
color: label.color
|
|
2468
|
+
})).filter((label) => label.name.trim().length > 0) : [],
|
|
2469
|
+
assignee: assignee ? {
|
|
2470
|
+
id: assignee.id,
|
|
2471
|
+
name: assignee.name,
|
|
2472
|
+
...assignee.title ? { title: assignee.title } : {}
|
|
2473
|
+
} : null,
|
|
2474
|
+
commentCount: orderedComments.length,
|
|
2475
|
+
comments: orderedComments.map((comment) => {
|
|
2476
|
+
const author = comment.authorAgentId ? agentsById.get(comment.authorAgentId) : null;
|
|
2477
|
+
return {
|
|
2478
|
+
id: comment.id,
|
|
2479
|
+
body: comment.body ?? "",
|
|
2480
|
+
createdAt: coerceDate(comment.createdAt).toISOString(),
|
|
2481
|
+
updatedAt: coerceDate(comment.updatedAt).toISOString(),
|
|
2482
|
+
authorLabel: author?.name ?? (comment.authorUserId ? "Team member" : "Paperclip"),
|
|
2483
|
+
authorKind: author ? "agent" : comment.authorUserId ? "user" : "system",
|
|
2484
|
+
...author?.title ? { authorTitle: author.title } : {}
|
|
2485
|
+
};
|
|
2486
|
+
})
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
1774
2489
|
function createSetupConfigurationErrorSyncState(issue, trigger) {
|
|
1775
2490
|
switch (issue) {
|
|
1776
2491
|
case "missing_token":
|
|
@@ -1785,7 +2500,16 @@ function createSetupConfigurationErrorSyncState(issue, trigger) {
|
|
|
1785
2500
|
phase: "configuration",
|
|
1786
2501
|
configurationIssue: "missing_token",
|
|
1787
2502
|
suggestedAction: MISSING_GITHUB_TOKEN_SYNC_ACTION
|
|
1788
|
-
}
|
|
2503
|
+
},
|
|
2504
|
+
recentFailures: [
|
|
2505
|
+
{
|
|
2506
|
+
message: MISSING_GITHUB_TOKEN_SYNC_MESSAGE,
|
|
2507
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2508
|
+
phase: "configuration",
|
|
2509
|
+
configurationIssue: "missing_token",
|
|
2510
|
+
suggestedAction: MISSING_GITHUB_TOKEN_SYNC_ACTION
|
|
2511
|
+
}
|
|
2512
|
+
]
|
|
1789
2513
|
});
|
|
1790
2514
|
case "missing_mapping":
|
|
1791
2515
|
return createErrorSyncState({
|
|
@@ -1799,7 +2523,16 @@ function createSetupConfigurationErrorSyncState(issue, trigger) {
|
|
|
1799
2523
|
phase: "configuration",
|
|
1800
2524
|
configurationIssue: "missing_mapping",
|
|
1801
2525
|
suggestedAction: MISSING_MAPPING_SYNC_ACTION
|
|
1802
|
-
}
|
|
2526
|
+
},
|
|
2527
|
+
recentFailures: [
|
|
2528
|
+
{
|
|
2529
|
+
message: MISSING_MAPPING_SYNC_MESSAGE,
|
|
2530
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2531
|
+
phase: "configuration",
|
|
2532
|
+
configurationIssue: "missing_mapping",
|
|
2533
|
+
suggestedAction: MISSING_MAPPING_SYNC_ACTION
|
|
2534
|
+
}
|
|
2535
|
+
]
|
|
1803
2536
|
});
|
|
1804
2537
|
case "missing_board_access":
|
|
1805
2538
|
return createErrorSyncState({
|
|
@@ -1813,7 +2546,16 @@ function createSetupConfigurationErrorSyncState(issue, trigger) {
|
|
|
1813
2546
|
phase: "configuration",
|
|
1814
2547
|
configurationIssue: "missing_board_access",
|
|
1815
2548
|
suggestedAction: MISSING_BOARD_ACCESS_SYNC_ACTION
|
|
1816
|
-
}
|
|
2549
|
+
},
|
|
2550
|
+
recentFailures: [
|
|
2551
|
+
{
|
|
2552
|
+
message: MISSING_BOARD_ACCESS_SYNC_MESSAGE,
|
|
2553
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2554
|
+
phase: "configuration",
|
|
2555
|
+
configurationIssue: "missing_board_access",
|
|
2556
|
+
suggestedAction: MISSING_BOARD_ACCESS_SYNC_ACTION
|
|
2557
|
+
}
|
|
2558
|
+
]
|
|
1817
2559
|
});
|
|
1818
2560
|
}
|
|
1819
2561
|
}
|
|
@@ -1826,6 +2568,29 @@ async function saveSettingsSyncState(ctx, settings, syncState) {
|
|
|
1826
2568
|
await ctx.state.set(SYNC_STATE_SCOPE, next.syncState);
|
|
1827
2569
|
return next;
|
|
1828
2570
|
}
|
|
2571
|
+
async function setSyncCancellationRequest(ctx, request) {
|
|
2572
|
+
if (request) {
|
|
2573
|
+
await ctx.state.set(SYNC_CANCELLATION_SCOPE, request);
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
await ctx.state.delete(SYNC_CANCELLATION_SCOPE);
|
|
2577
|
+
}
|
|
2578
|
+
async function getSyncCancellationRequest(ctx) {
|
|
2579
|
+
const activeRequestedAt = activeRunningSyncState?.syncState.cancelRequestedAt?.trim();
|
|
2580
|
+
if (activeRunningSyncState?.syncState.status === "running" && activeRequestedAt) {
|
|
2581
|
+
return {
|
|
2582
|
+
requestedAt: activeRequestedAt
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
return normalizeSyncCancellationRequest(await ctx.state.get(SYNC_CANCELLATION_SCOPE));
|
|
2586
|
+
}
|
|
2587
|
+
function buildCancelledSyncMessage(target, progress) {
|
|
2588
|
+
const completedIssueCount = typeof progress?.completedIssueCount === "number" ? Math.max(0, progress.completedIssueCount) : void 0;
|
|
2589
|
+
const totalIssueCount = typeof progress?.totalIssueCount === "number" ? Math.max(0, progress.totalIssueCount) : void 0;
|
|
2590
|
+
const scopeLabel = target ? `GitHub sync for ${target.displayLabel}` : "GitHub sync";
|
|
2591
|
+
const completionSummary = completedIssueCount !== void 0 && totalIssueCount !== void 0 ? ` Completed ${Math.min(completedIssueCount, totalIssueCount)} of ${totalIssueCount} issues before stopping.` : "";
|
|
2592
|
+
return `${scopeLabel} was cancelled before it finished.${completionSummary}`;
|
|
2593
|
+
}
|
|
1829
2594
|
async function createUnexpectedSyncErrorResult(ctx, trigger, error) {
|
|
1830
2595
|
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
1831
2596
|
const errorDetails = buildSyncErrorDetails(error, {
|
|
@@ -1844,7 +2609,14 @@ async function createUnexpectedSyncErrorResult(ctx, trigger, error) {
|
|
|
1844
2609
|
createdIssuesCount: settings.syncState.createdIssuesCount ?? 0,
|
|
1845
2610
|
skippedIssuesCount: settings.syncState.skippedIssuesCount ?? 0,
|
|
1846
2611
|
erroredIssuesCount: 0,
|
|
1847
|
-
errorDetails
|
|
2612
|
+
errorDetails,
|
|
2613
|
+
recentFailures: appendRecentSyncFailureLogEntry(
|
|
2614
|
+
void 0,
|
|
2615
|
+
createSyncFailureLogEntry({
|
|
2616
|
+
message,
|
|
2617
|
+
errorDetails
|
|
2618
|
+
})
|
|
2619
|
+
)
|
|
1848
2620
|
})
|
|
1849
2621
|
);
|
|
1850
2622
|
}
|
|
@@ -1864,11 +2636,14 @@ async function waitForSyncResultWithinGracePeriod(promise, timeoutMs) {
|
|
|
1864
2636
|
}
|
|
1865
2637
|
}
|
|
1866
2638
|
async function getActiveOrCurrentSyncState(ctx) {
|
|
2639
|
+
if (activeRunningSyncState?.syncState.status === "running") {
|
|
2640
|
+
return activeRunningSyncState;
|
|
2641
|
+
}
|
|
1867
2642
|
const current = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
1868
2643
|
if (current.syncState.status === "running") {
|
|
1869
2644
|
return current;
|
|
1870
2645
|
}
|
|
1871
|
-
return
|
|
2646
|
+
return current;
|
|
1872
2647
|
}
|
|
1873
2648
|
function updateSyncFailureContext(current, next) {
|
|
1874
2649
|
if ("phase" in next) {
|
|
@@ -1892,7 +2667,8 @@ function recordRecoverableSyncFailure(ctx, failures, error, context) {
|
|
|
1892
2667
|
const snapshot = cloneSyncFailureContext(context);
|
|
1893
2668
|
failures.push({
|
|
1894
2669
|
error,
|
|
1895
|
-
context: snapshot
|
|
2670
|
+
context: snapshot,
|
|
2671
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1896
2672
|
});
|
|
1897
2673
|
ctx.logger.warn("GitHub sync skipped a failed item and continued.", {
|
|
1898
2674
|
phase: snapshot.phase,
|
|
@@ -1998,6 +2774,13 @@ function normalizePaperclipBoardApiTokenRefs(value) {
|
|
|
1998
2774
|
}
|
|
1999
2775
|
return Object.fromEntries(entries);
|
|
2000
2776
|
}
|
|
2777
|
+
function normalizeSyncCancellationRequest(value) {
|
|
2778
|
+
if (!value || typeof value !== "object") {
|
|
2779
|
+
return null;
|
|
2780
|
+
}
|
|
2781
|
+
const requestedAt = typeof value.requestedAt === "string" ? value.requestedAt.trim() : "";
|
|
2782
|
+
return requestedAt ? { requestedAt } : null;
|
|
2783
|
+
}
|
|
2001
2784
|
function normalizeSyncState(value) {
|
|
2002
2785
|
if (!value || typeof value !== "object") {
|
|
2003
2786
|
return DEFAULT_SETTINGS.syncState;
|
|
@@ -2007,8 +2790,9 @@ function normalizeSyncState(value) {
|
|
|
2007
2790
|
const lastRunTrigger = record.lastRunTrigger;
|
|
2008
2791
|
const progress = normalizeSyncProgress(record.progress);
|
|
2009
2792
|
const errorDetails = normalizeSyncErrorDetails(record.errorDetails);
|
|
2793
|
+
const recentFailures = normalizeSyncFailureLogEntries(record.recentFailures);
|
|
2010
2794
|
return {
|
|
2011
|
-
status: status === "running" || status === "success" || status === "error" ? status : "idle",
|
|
2795
|
+
status: status === "running" || status === "success" || status === "error" || status === "cancelled" ? status : "idle",
|
|
2012
2796
|
message: typeof record.message === "string" ? record.message : void 0,
|
|
2013
2797
|
checkedAt: typeof record.checkedAt === "string" ? record.checkedAt : void 0,
|
|
2014
2798
|
syncedIssuesCount: typeof record.syncedIssuesCount === "number" ? record.syncedIssuesCount : void 0,
|
|
@@ -2016,8 +2800,10 @@ function normalizeSyncState(value) {
|
|
|
2016
2800
|
skippedIssuesCount: typeof record.skippedIssuesCount === "number" ? record.skippedIssuesCount : void 0,
|
|
2017
2801
|
erroredIssuesCount: typeof record.erroredIssuesCount === "number" ? record.erroredIssuesCount : void 0,
|
|
2018
2802
|
lastRunTrigger: lastRunTrigger === "manual" || lastRunTrigger === "schedule" || lastRunTrigger === "retry" ? lastRunTrigger : void 0,
|
|
2803
|
+
cancelRequestedAt: typeof record.cancelRequestedAt === "string" ? record.cancelRequestedAt : void 0,
|
|
2019
2804
|
...progress ? { progress } : {},
|
|
2020
|
-
...errorDetails ? { errorDetails } : {}
|
|
2805
|
+
...errorDetails ? { errorDetails } : {},
|
|
2806
|
+
...recentFailures ? { recentFailures } : {}
|
|
2021
2807
|
};
|
|
2022
2808
|
}
|
|
2023
2809
|
function normalizeMappings(value) {
|
|
@@ -2051,6 +2837,22 @@ function normalizeGitHubUsername(value) {
|
|
|
2051
2837
|
const trimmed = value.trim().replace(/^@+/, "");
|
|
2052
2838
|
return trimmed ? trimmed.toLowerCase() : void 0;
|
|
2053
2839
|
}
|
|
2840
|
+
function buildGitHubUsernameAliases(value) {
|
|
2841
|
+
const normalized = normalizeGitHubUsername(value);
|
|
2842
|
+
if (!normalized) {
|
|
2843
|
+
return [];
|
|
2844
|
+
}
|
|
2845
|
+
const aliases = /* @__PURE__ */ new Set([normalized]);
|
|
2846
|
+
if (normalized.endsWith("[bot]")) {
|
|
2847
|
+
const withoutBotSuffix = normalized.slice(0, -"[bot]".length);
|
|
2848
|
+
if (withoutBotSuffix) {
|
|
2849
|
+
aliases.add(withoutBotSuffix);
|
|
2850
|
+
}
|
|
2851
|
+
} else {
|
|
2852
|
+
aliases.add(`${normalized}[bot]`);
|
|
2853
|
+
}
|
|
2854
|
+
return [...aliases];
|
|
2855
|
+
}
|
|
2054
2856
|
function parseIgnoredIssueAuthorUsernames(value) {
|
|
2055
2857
|
return value.split(/[\s,]+/g).map((entry) => normalizeGitHubUsername(entry)).filter((entry) => Boolean(entry));
|
|
2056
2858
|
}
|
|
@@ -2370,6 +3172,286 @@ function getPageCursor(pageInfo) {
|
|
|
2370
3172
|
}
|
|
2371
3173
|
return pageInfo.endCursor;
|
|
2372
3174
|
}
|
|
3175
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
3176
|
+
if (items.length === 0) {
|
|
3177
|
+
return [];
|
|
3178
|
+
}
|
|
3179
|
+
const limit = Math.max(1, Math.floor(concurrency));
|
|
3180
|
+
const results = new Array(items.length);
|
|
3181
|
+
let nextIndex = 0;
|
|
3182
|
+
async function worker() {
|
|
3183
|
+
while (nextIndex < items.length) {
|
|
3184
|
+
const currentIndex = nextIndex;
|
|
3185
|
+
nextIndex += 1;
|
|
3186
|
+
results[currentIndex] = await mapper(items[currentIndex], currentIndex);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
await Promise.all(
|
|
3190
|
+
Array.from(
|
|
3191
|
+
{ length: Math.min(limit, items.length) },
|
|
3192
|
+
async () => worker()
|
|
3193
|
+
)
|
|
3194
|
+
);
|
|
3195
|
+
return results;
|
|
3196
|
+
}
|
|
3197
|
+
function getFreshCacheValue(cache, key, now = Date.now()) {
|
|
3198
|
+
const entry = cache.get(key);
|
|
3199
|
+
if (!entry) {
|
|
3200
|
+
return null;
|
|
3201
|
+
}
|
|
3202
|
+
if (entry.expiresAt <= now) {
|
|
3203
|
+
cache.delete(key);
|
|
3204
|
+
return null;
|
|
3205
|
+
}
|
|
3206
|
+
return entry.value;
|
|
3207
|
+
}
|
|
3208
|
+
function getFreshCacheEntry(cache, key, now = Date.now()) {
|
|
3209
|
+
const entry = cache.get(key);
|
|
3210
|
+
if (!entry) {
|
|
3211
|
+
return null;
|
|
3212
|
+
}
|
|
3213
|
+
if (entry.expiresAt <= now) {
|
|
3214
|
+
cache.delete(key);
|
|
3215
|
+
return null;
|
|
3216
|
+
}
|
|
3217
|
+
return entry;
|
|
3218
|
+
}
|
|
3219
|
+
function setCacheValue(cache, key, value, ttlMs, now = Date.now()) {
|
|
3220
|
+
cache.set(key, {
|
|
3221
|
+
expiresAt: now + ttlMs,
|
|
3222
|
+
value
|
|
3223
|
+
});
|
|
3224
|
+
return value;
|
|
3225
|
+
}
|
|
3226
|
+
function normalizeProjectPullRequestFilter(value) {
|
|
3227
|
+
switch (value) {
|
|
3228
|
+
case "mergeable":
|
|
3229
|
+
case "reviewable":
|
|
3230
|
+
case "failing":
|
|
3231
|
+
return value;
|
|
3232
|
+
default:
|
|
3233
|
+
return "all";
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
function normalizeProjectPullRequestPageIndex(value) {
|
|
3237
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
3238
|
+
return Math.floor(value);
|
|
3239
|
+
}
|
|
3240
|
+
if (typeof value === "string" && value.trim()) {
|
|
3241
|
+
const parsed = Number(value.trim());
|
|
3242
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
3243
|
+
return Math.floor(parsed);
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
return 0;
|
|
3247
|
+
}
|
|
3248
|
+
function isCopilotActorLogin(value) {
|
|
3249
|
+
const normalizedLogin = normalizeGitHubUsername(value);
|
|
3250
|
+
return Boolean(normalizedLogin && normalizedLogin.includes("copilot"));
|
|
3251
|
+
}
|
|
3252
|
+
function getProjectPullRequestReviewThreadStarterLogin(node) {
|
|
3253
|
+
return normalizeGitHubUsername(
|
|
3254
|
+
node?.comments?.nodes?.find((comment) => comment?.author?.login)?.author?.login
|
|
3255
|
+
);
|
|
3256
|
+
}
|
|
3257
|
+
function getDetailedPullRequestReviewThreadStarterLogin(thread) {
|
|
3258
|
+
const rootComment = thread.comments.find((comment) => !comment.replyToId) ?? thread.comments[0];
|
|
3259
|
+
return normalizeGitHubUsername(rootComment?.authorLogin);
|
|
3260
|
+
}
|
|
3261
|
+
function summarizeProjectPullRequestReviewThreadsFromConnection(connection) {
|
|
3262
|
+
let unresolvedReviewThreads = 0;
|
|
3263
|
+
let copilotUnresolvedReviewThreads = 0;
|
|
3264
|
+
for (const thread of connection?.nodes ?? []) {
|
|
3265
|
+
if (thread?.isResolved !== false) {
|
|
3266
|
+
continue;
|
|
3267
|
+
}
|
|
3268
|
+
unresolvedReviewThreads += 1;
|
|
3269
|
+
if (isCopilotActorLogin(getProjectPullRequestReviewThreadStarterLogin(thread))) {
|
|
3270
|
+
copilotUnresolvedReviewThreads += 1;
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
return {
|
|
3274
|
+
unresolvedReviewThreads,
|
|
3275
|
+
copilotUnresolvedReviewThreads
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
function summarizeDetailedPullRequestReviewThreads(threads) {
|
|
3279
|
+
let unresolvedReviewThreads = 0;
|
|
3280
|
+
let copilotUnresolvedReviewThreads = 0;
|
|
3281
|
+
for (const thread of threads) {
|
|
3282
|
+
if (thread.isResolved) {
|
|
3283
|
+
continue;
|
|
3284
|
+
}
|
|
3285
|
+
unresolvedReviewThreads += 1;
|
|
3286
|
+
if (isCopilotActorLogin(getDetailedPullRequestReviewThreadStarterLogin(thread))) {
|
|
3287
|
+
copilotUnresolvedReviewThreads += 1;
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
return {
|
|
3291
|
+
unresolvedReviewThreads,
|
|
3292
|
+
copilotUnresolvedReviewThreads
|
|
3293
|
+
};
|
|
3294
|
+
}
|
|
3295
|
+
function normalizeProjectPullRequestClosingIssues(repository, nodes) {
|
|
3296
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3297
|
+
const issues = [];
|
|
3298
|
+
for (const node of nodes ?? []) {
|
|
3299
|
+
const issueNumber = typeof node?.number === "number" && node.number > 0 ? Math.floor(node.number) : void 0;
|
|
3300
|
+
const issueUrl = typeof node?.url === "string" && node.url.trim() ? normalizeGitHubIssueHtmlUrl(node.url) : issueNumber !== void 0 ? buildGitHubIssueUrlFromRepository(repository.url, issueNumber) : void 0;
|
|
3301
|
+
if (issueNumber === void 0 || !issueUrl || seen.has(issueUrl)) {
|
|
3302
|
+
continue;
|
|
3303
|
+
}
|
|
3304
|
+
seen.add(issueUrl);
|
|
3305
|
+
issues.push({
|
|
3306
|
+
number: issueNumber,
|
|
3307
|
+
url: issueUrl
|
|
3308
|
+
});
|
|
3309
|
+
}
|
|
3310
|
+
return issues;
|
|
3311
|
+
}
|
|
3312
|
+
function resolveProjectPullRequestReviewable(record) {
|
|
3313
|
+
return record.githubMergeable === true && record.checksStatus === "passed" && typeof record.copilotUnresolvedReviewThreads === "number" && record.copilotUnresolvedReviewThreads === 0;
|
|
3314
|
+
}
|
|
3315
|
+
function resolveProjectPullRequestMergeable(record) {
|
|
3316
|
+
return record.githubMergeable === true && record.checksStatus === "passed" && typeof record.reviewApprovals === "number" && record.reviewApprovals > 0 && typeof record.unresolvedReviewThreads === "number" && record.unresolvedReviewThreads === 0;
|
|
3317
|
+
}
|
|
3318
|
+
function resolveProjectPullRequestUpToDateStatus(record) {
|
|
3319
|
+
const mergeStateStatus = typeof record.mergeStateStatus === "string" ? record.mergeStateStatus : null;
|
|
3320
|
+
if (mergeStateStatus === "DIRTY" || record.mergeable === "CONFLICTING") {
|
|
3321
|
+
return "conflicts";
|
|
3322
|
+
}
|
|
3323
|
+
if (typeof record.behindBy === "number" && Number.isFinite(record.behindBy) && record.behindBy > 0) {
|
|
3324
|
+
return "can_update";
|
|
3325
|
+
}
|
|
3326
|
+
if (typeof record.behindBy === "number" && Number.isFinite(record.behindBy) && record.behindBy === 0) {
|
|
3327
|
+
return "up_to_date";
|
|
3328
|
+
}
|
|
3329
|
+
if (mergeStateStatus === "BEHIND") {
|
|
3330
|
+
return "can_update";
|
|
3331
|
+
}
|
|
3332
|
+
return "unknown";
|
|
3333
|
+
}
|
|
3334
|
+
function normalizeProjectPullRequestCopilotAction(value) {
|
|
3335
|
+
switch (typeof value === "string" ? value.trim().toLowerCase() : "") {
|
|
3336
|
+
case "fix_ci":
|
|
3337
|
+
return "fix_ci";
|
|
3338
|
+
case "rebase":
|
|
3339
|
+
return "rebase";
|
|
3340
|
+
case "address_review_feedback":
|
|
3341
|
+
return "address_review_feedback";
|
|
3342
|
+
case "review":
|
|
3343
|
+
return "review";
|
|
3344
|
+
default:
|
|
3345
|
+
return null;
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
function getProjectPullRequestCopilotActionLabel(action) {
|
|
3349
|
+
switch (action) {
|
|
3350
|
+
case "fix_ci":
|
|
3351
|
+
return "Fix CI";
|
|
3352
|
+
case "rebase":
|
|
3353
|
+
return "Rebase";
|
|
3354
|
+
case "address_review_feedback":
|
|
3355
|
+
return "Address review feedback";
|
|
3356
|
+
case "review":
|
|
3357
|
+
return "Review";
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
function getProjectPullRequestNumber(record) {
|
|
3361
|
+
const number = record.number;
|
|
3362
|
+
if (typeof number !== "number" || !Number.isFinite(number) || number <= 0) {
|
|
3363
|
+
return null;
|
|
3364
|
+
}
|
|
3365
|
+
return Math.floor(number);
|
|
3366
|
+
}
|
|
3367
|
+
function getProjectPullRequestUpdatedAtTimestamp(record) {
|
|
3368
|
+
const updatedAt = typeof record.updatedAt === "string" ? Date.parse(record.updatedAt) : Number.NaN;
|
|
3369
|
+
return Number.isFinite(updatedAt) ? updatedAt : 0;
|
|
3370
|
+
}
|
|
3371
|
+
function sortProjectPullRequestRecordsByUpdatedAt(records) {
|
|
3372
|
+
return [...records].sort((left, right) => {
|
|
3373
|
+
const timestampDelta = getProjectPullRequestUpdatedAtTimestamp(right) - getProjectPullRequestUpdatedAtTimestamp(left);
|
|
3374
|
+
if (timestampDelta !== 0) {
|
|
3375
|
+
return timestampDelta;
|
|
3376
|
+
}
|
|
3377
|
+
return (getProjectPullRequestNumber(right) ?? 0) - (getProjectPullRequestNumber(left) ?? 0);
|
|
3378
|
+
});
|
|
3379
|
+
}
|
|
3380
|
+
function getLinkedPaperclipIssueFromProjectPullRequestRecord(record) {
|
|
3381
|
+
const paperclipIssueId = typeof record.paperclipIssueId === "string" && record.paperclipIssueId.trim() ? record.paperclipIssueId.trim() : void 0;
|
|
3382
|
+
if (!paperclipIssueId) {
|
|
3383
|
+
return void 0;
|
|
3384
|
+
}
|
|
3385
|
+
return {
|
|
3386
|
+
paperclipIssueId,
|
|
3387
|
+
...typeof record.paperclipIssueKey === "string" && record.paperclipIssueKey.trim() ? { paperclipIssueKey: record.paperclipIssueKey.trim() } : {}
|
|
3388
|
+
};
|
|
3389
|
+
}
|
|
3390
|
+
function buildProjectPullRequestMetrics(pullRequests, totalOpenPullRequests, defaultBranchName) {
|
|
3391
|
+
const mergeablePullRequestNumbers = [];
|
|
3392
|
+
const reviewablePullRequestNumbers = [];
|
|
3393
|
+
const failingPullRequestNumbers = [];
|
|
3394
|
+
for (const pullRequest of pullRequests) {
|
|
3395
|
+
const number = getProjectPullRequestNumber(pullRequest);
|
|
3396
|
+
if (number === null) {
|
|
3397
|
+
continue;
|
|
3398
|
+
}
|
|
3399
|
+
if (pullRequest.mergeable === true) {
|
|
3400
|
+
mergeablePullRequestNumbers.push(number);
|
|
3401
|
+
}
|
|
3402
|
+
if (pullRequest.reviewable === true) {
|
|
3403
|
+
reviewablePullRequestNumbers.push(number);
|
|
3404
|
+
}
|
|
3405
|
+
if (pullRequest.checksStatus === "failed") {
|
|
3406
|
+
failingPullRequestNumbers.push(number);
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
return {
|
|
3410
|
+
totalOpenPullRequests,
|
|
3411
|
+
...defaultBranchName ? { defaultBranchName } : {},
|
|
3412
|
+
mergeablePullRequests: mergeablePullRequestNumbers.length,
|
|
3413
|
+
reviewablePullRequests: reviewablePullRequestNumbers.length,
|
|
3414
|
+
failingPullRequests: failingPullRequestNumbers.length,
|
|
3415
|
+
mergeablePullRequestNumbers,
|
|
3416
|
+
reviewablePullRequestNumbers,
|
|
3417
|
+
failingPullRequestNumbers
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
3420
|
+
function getPublicProjectPullRequestMetrics(metrics) {
|
|
3421
|
+
return {
|
|
3422
|
+
totalOpenPullRequests: metrics.totalOpenPullRequests,
|
|
3423
|
+
...metrics.defaultBranchName ? { defaultBranchName: metrics.defaultBranchName } : {},
|
|
3424
|
+
mergeablePullRequests: metrics.mergeablePullRequests,
|
|
3425
|
+
reviewablePullRequests: metrics.reviewablePullRequests,
|
|
3426
|
+
failingPullRequests: metrics.failingPullRequests
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
function sliceProjectPullRequestRecords(pullRequests, pageIndex, pageSize = PROJECT_PULL_REQUEST_PAGE_SIZE) {
|
|
3430
|
+
const effectivePageSize = Math.max(1, Math.floor(pageSize));
|
|
3431
|
+
const maxPageIndex = Math.max(0, Math.ceil(pullRequests.length / effectivePageSize) - 1);
|
|
3432
|
+
const normalizedPageIndex = Math.min(Math.max(0, Math.floor(pageIndex)), maxPageIndex);
|
|
3433
|
+
const start = normalizedPageIndex * effectivePageSize;
|
|
3434
|
+
const end = start + effectivePageSize;
|
|
3435
|
+
return {
|
|
3436
|
+
pullRequests: pullRequests.slice(start, end),
|
|
3437
|
+
pageIndex: normalizedPageIndex,
|
|
3438
|
+
hasNextPage: end < pullRequests.length,
|
|
3439
|
+
hasPreviousPage: normalizedPageIndex > 0
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
function sliceProjectPullRequestNumbers(pullRequestNumbers, pageIndex, pageSize = PROJECT_PULL_REQUEST_PAGE_SIZE) {
|
|
3443
|
+
const effectivePageSize = Math.max(1, Math.floor(pageSize));
|
|
3444
|
+
const maxPageIndex = Math.max(0, Math.ceil(pullRequestNumbers.length / effectivePageSize) - 1);
|
|
3445
|
+
const normalizedPageIndex = Math.min(Math.max(0, Math.floor(pageIndex)), maxPageIndex);
|
|
3446
|
+
const start = normalizedPageIndex * effectivePageSize;
|
|
3447
|
+
const end = start + effectivePageSize;
|
|
3448
|
+
return {
|
|
3449
|
+
pullRequestNumbers: pullRequestNumbers.slice(start, end),
|
|
3450
|
+
pageIndex: normalizedPageIndex,
|
|
3451
|
+
hasNextPage: end < pullRequestNumbers.length,
|
|
3452
|
+
hasPreviousPage: normalizedPageIndex > 0
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
2373
3455
|
function classifyGitHubPullRequestCiState(contexts) {
|
|
2374
3456
|
if (contexts.length === 0) {
|
|
2375
3457
|
return "unfinished";
|
|
@@ -2649,17 +3731,24 @@ function tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node) {
|
|
|
2649
3731
|
if (typeof node.number !== "number") {
|
|
2650
3732
|
return null;
|
|
2651
3733
|
}
|
|
2652
|
-
const
|
|
2653
|
-
const
|
|
2654
|
-
if (
|
|
3734
|
+
const reviewThreadSummary = node.reviewThreads?.pageInfo?.hasNextPage ? null : summarizeProjectPullRequestReviewThreadsFromConnection(node.reviewThreads);
|
|
3735
|
+
const ciState = tryBuildGitHubPullRequestCiStateFromBatchNode(node);
|
|
3736
|
+
if (!reviewThreadSummary || !ciState) {
|
|
2655
3737
|
return null;
|
|
2656
3738
|
}
|
|
2657
3739
|
return {
|
|
2658
3740
|
number: node.number,
|
|
2659
|
-
hasUnresolvedReviewThreads:
|
|
2660
|
-
ciState
|
|
3741
|
+
hasUnresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads > 0,
|
|
3742
|
+
ciState
|
|
2661
3743
|
};
|
|
2662
3744
|
}
|
|
3745
|
+
function tryBuildGitHubPullRequestCiStateFromBatchNode(node) {
|
|
3746
|
+
const ciContexts = node.statusCheckRollup?.contexts;
|
|
3747
|
+
if (ciContexts?.pageInfo?.hasNextPage) {
|
|
3748
|
+
return null;
|
|
3749
|
+
}
|
|
3750
|
+
return classifyGitHubPullRequestCiState(extractGitHubCiContextRecords(ciContexts?.nodes ?? []));
|
|
3751
|
+
}
|
|
2663
3752
|
async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullRequestNumbers, pullRequestStatusCache) {
|
|
2664
3753
|
if (targetPullRequestNumbers.size === 0) {
|
|
2665
3754
|
return;
|
|
@@ -2689,6 +3778,7 @@ async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullR
|
|
|
2689
3778
|
const snapshot = tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node);
|
|
2690
3779
|
if (snapshot) {
|
|
2691
3780
|
pullRequestStatusCache.set(node.number, snapshot);
|
|
3781
|
+
cacheGitHubPullRequestStatusSnapshot(repository, snapshot);
|
|
2692
3782
|
}
|
|
2693
3783
|
}
|
|
2694
3784
|
if (remainingNumbers.size === 0) {
|
|
@@ -2697,29 +3787,8 @@ async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullR
|
|
|
2697
3787
|
after = getPageCursor(pullRequests?.pageInfo);
|
|
2698
3788
|
} while (after);
|
|
2699
3789
|
}
|
|
2700
|
-
async function
|
|
2701
|
-
|
|
2702
|
-
do {
|
|
2703
|
-
const response = await octokit.graphql(
|
|
2704
|
-
GITHUB_PULL_REQUEST_REVIEW_THREADS_QUERY,
|
|
2705
|
-
{
|
|
2706
|
-
owner: repository.owner,
|
|
2707
|
-
repo: repository.repo,
|
|
2708
|
-
pullRequestNumber,
|
|
2709
|
-
after
|
|
2710
|
-
}
|
|
2711
|
-
);
|
|
2712
|
-
const reviewThreads = response.repository?.pullRequest?.reviewThreads;
|
|
2713
|
-
const nodes = reviewThreads?.nodes ?? [];
|
|
2714
|
-
if (nodes.some((node) => node?.isResolved === false)) {
|
|
2715
|
-
return true;
|
|
2716
|
-
}
|
|
2717
|
-
after = getPageCursor(reviewThreads?.pageInfo);
|
|
2718
|
-
} while (after);
|
|
2719
|
-
return false;
|
|
2720
|
-
}
|
|
2721
|
-
async function getGitHubPullRequestCiState(octokit, repository, pullRequestNumber) {
|
|
2722
|
-
const contexts = [];
|
|
3790
|
+
async function getGitHubPullRequestCiState(octokit, repository, pullRequestNumber) {
|
|
3791
|
+
const contexts = [];
|
|
2723
3792
|
let after;
|
|
2724
3793
|
do {
|
|
2725
3794
|
const response = await octokit.graphql(GITHUB_PULL_REQUEST_CI_CONTEXTS_QUERY, {
|
|
@@ -2753,22 +3822,53 @@ async function getGitHubPullRequestCiState(octokit, repository, pullRequestNumbe
|
|
|
2753
3822
|
} while (after);
|
|
2754
3823
|
return classifyGitHubPullRequestCiState(contexts);
|
|
2755
3824
|
}
|
|
2756
|
-
async function getGitHubPullRequestStatusSnapshot(octokit, repository, pullRequestNumber, pullRequestStatusCache) {
|
|
3825
|
+
async function getGitHubPullRequestStatusSnapshot(octokit, repository, pullRequestNumber, pullRequestStatusCache, options) {
|
|
2757
3826
|
const cached = pullRequestStatusCache.get(pullRequestNumber);
|
|
2758
3827
|
if (cached) {
|
|
2759
3828
|
return cached;
|
|
2760
3829
|
}
|
|
2761
|
-
const
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
3830
|
+
const cacheKey = buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "status");
|
|
3831
|
+
const cachedSnapshot = getFreshCacheValue(activeGitHubPullRequestStatusSnapshotCache, cacheKey);
|
|
3832
|
+
if (cachedSnapshot) {
|
|
3833
|
+
pullRequestStatusCache.set(pullRequestNumber, cachedSnapshot);
|
|
3834
|
+
return cachedSnapshot;
|
|
3835
|
+
}
|
|
3836
|
+
if (options?.reviewThreadSummary && options.ciState) {
|
|
3837
|
+
const snapshot = cacheGitHubPullRequestStatusSnapshot(repository, {
|
|
3838
|
+
number: pullRequestNumber,
|
|
3839
|
+
hasUnresolvedReviewThreads: options.reviewThreadSummary.unresolvedReviewThreads > 0,
|
|
3840
|
+
ciState: options.ciState
|
|
3841
|
+
});
|
|
3842
|
+
pullRequestStatusCache.set(pullRequestNumber, snapshot);
|
|
3843
|
+
return snapshot;
|
|
3844
|
+
}
|
|
3845
|
+
const inFlightSnapshot = activeGitHubPullRequestStatusSnapshotPromiseCache.get(cacheKey);
|
|
3846
|
+
if (inFlightSnapshot) {
|
|
3847
|
+
const snapshot = await inFlightSnapshot;
|
|
3848
|
+
pullRequestStatusCache.set(pullRequestNumber, snapshot);
|
|
3849
|
+
return snapshot;
|
|
3850
|
+
}
|
|
3851
|
+
const loadSnapshotPromise = (async () => {
|
|
3852
|
+
const [reviewThreadSummary, ciState] = await Promise.all([
|
|
3853
|
+
options?.reviewThreadSummary ?? getOrLoadCachedGitHubPullRequestReviewThreadSummary(octokit, repository, pullRequestNumber),
|
|
3854
|
+
options?.ciState ?? getGitHubPullRequestCiState(octokit, repository, pullRequestNumber)
|
|
3855
|
+
]);
|
|
3856
|
+
return cacheGitHubPullRequestStatusSnapshot(repository, {
|
|
3857
|
+
number: pullRequestNumber,
|
|
3858
|
+
hasUnresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads > 0,
|
|
3859
|
+
ciState
|
|
3860
|
+
});
|
|
3861
|
+
})();
|
|
3862
|
+
activeGitHubPullRequestStatusSnapshotPromiseCache.set(cacheKey, loadSnapshotPromise);
|
|
3863
|
+
try {
|
|
3864
|
+
const snapshot = await loadSnapshotPromise;
|
|
3865
|
+
pullRequestStatusCache.set(pullRequestNumber, snapshot);
|
|
3866
|
+
return snapshot;
|
|
3867
|
+
} finally {
|
|
3868
|
+
if (activeGitHubPullRequestStatusSnapshotPromiseCache.get(cacheKey) === loadSnapshotPromise) {
|
|
3869
|
+
activeGitHubPullRequestStatusSnapshotPromiseCache.delete(cacheKey);
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
2772
3872
|
}
|
|
2773
3873
|
async function getGitHubIssueStatusSnapshot(octokit, repository, issueNumber, githubIssue, linkedPullRequestsByIssueNumber, issueStatusSnapshotCache, pullRequestStatusCache) {
|
|
2774
3874
|
if (issueStatusSnapshotCache.has(issueNumber)) {
|
|
@@ -3175,6 +4275,33 @@ function normalizeGitHubIssueLinkEntityData(value) {
|
|
|
3175
4275
|
syncedAt
|
|
3176
4276
|
};
|
|
3177
4277
|
}
|
|
4278
|
+
function normalizeGitHubPullRequestLinkEntityData(value) {
|
|
4279
|
+
if (!value || typeof value !== "object") {
|
|
4280
|
+
return null;
|
|
4281
|
+
}
|
|
4282
|
+
const record = value;
|
|
4283
|
+
const repositoryUrl = typeof record.repositoryUrl === "string" && record.repositoryUrl.trim() ? getNormalizedMappingRepositoryUrl({
|
|
4284
|
+
repositoryUrl: record.repositoryUrl
|
|
4285
|
+
}) : void 0;
|
|
4286
|
+
const githubPullRequestNumber = typeof record.githubPullRequestNumber === "number" && record.githubPullRequestNumber > 0 ? Math.floor(record.githubPullRequestNumber) : void 0;
|
|
4287
|
+
const githubPullRequestUrl = typeof record.githubPullRequestUrl === "string" && record.githubPullRequestUrl.trim() ? record.githubPullRequestUrl.trim() : void 0;
|
|
4288
|
+
const githubPullRequestState = record.githubPullRequestState === "closed" ? "closed" : record.githubPullRequestState === "open" ? "open" : void 0;
|
|
4289
|
+
const title = typeof record.title === "string" && record.title.trim() ? record.title.trim() : void 0;
|
|
4290
|
+
const syncedAt = typeof record.syncedAt === "string" && record.syncedAt.trim() ? record.syncedAt.trim() : void 0;
|
|
4291
|
+
if (!repositoryUrl || githubPullRequestNumber === void 0 || !githubPullRequestUrl || !githubPullRequestState || !syncedAt) {
|
|
4292
|
+
return null;
|
|
4293
|
+
}
|
|
4294
|
+
return {
|
|
4295
|
+
...typeof record.companyId === "string" && record.companyId.trim() ? { companyId: record.companyId.trim() } : {},
|
|
4296
|
+
...typeof record.paperclipProjectId === "string" && record.paperclipProjectId.trim() ? { paperclipProjectId: record.paperclipProjectId.trim() } : {},
|
|
4297
|
+
repositoryUrl,
|
|
4298
|
+
githubPullRequestNumber,
|
|
4299
|
+
githubPullRequestUrl,
|
|
4300
|
+
githubPullRequestState,
|
|
4301
|
+
...title ? { title } : {},
|
|
4302
|
+
syncedAt
|
|
4303
|
+
};
|
|
4304
|
+
}
|
|
3178
4305
|
function normalizeStoredStatusTransitionCommentAnnotation(value) {
|
|
3179
4306
|
if (!value || typeof value !== "object") {
|
|
3180
4307
|
return null;
|
|
@@ -3249,6 +4376,52 @@ async function listGitHubIssueLinkRecords(ctx, query = {}) {
|
|
|
3249
4376
|
}
|
|
3250
4377
|
return records;
|
|
3251
4378
|
}
|
|
4379
|
+
async function listGitHubPullRequestLinkRecords(ctx, query = {}) {
|
|
4380
|
+
const records = [];
|
|
4381
|
+
const requestedIssueId = query.paperclipIssueId?.trim() || void 0;
|
|
4382
|
+
const requestedExternalId = query.externalId?.trim() || void 0;
|
|
4383
|
+
for (let offset = 0; ; ) {
|
|
4384
|
+
const page = await ctx.entities.list({
|
|
4385
|
+
entityType: PULL_REQUEST_LINK_ENTITY_TYPE,
|
|
4386
|
+
scopeKind: "issue",
|
|
4387
|
+
...requestedIssueId ? { scopeId: requestedIssueId } : {},
|
|
4388
|
+
...requestedExternalId ? { externalId: requestedExternalId } : {},
|
|
4389
|
+
limit: PAPERCLIP_LABEL_PAGE_SIZE,
|
|
4390
|
+
offset
|
|
4391
|
+
});
|
|
4392
|
+
if (page.length === 0) {
|
|
4393
|
+
break;
|
|
4394
|
+
}
|
|
4395
|
+
for (const entry of page) {
|
|
4396
|
+
if (entry.scopeKind !== "issue" || !entry.scopeId) {
|
|
4397
|
+
continue;
|
|
4398
|
+
}
|
|
4399
|
+
if (requestedIssueId && entry.scopeId !== requestedIssueId) {
|
|
4400
|
+
continue;
|
|
4401
|
+
}
|
|
4402
|
+
if (requestedExternalId && entry.externalId !== requestedExternalId) {
|
|
4403
|
+
continue;
|
|
4404
|
+
}
|
|
4405
|
+
const data = normalizeGitHubPullRequestLinkEntityData(entry.data);
|
|
4406
|
+
if (!data) {
|
|
4407
|
+
continue;
|
|
4408
|
+
}
|
|
4409
|
+
records.push({
|
|
4410
|
+
paperclipIssueId: entry.scopeId,
|
|
4411
|
+
...typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {},
|
|
4412
|
+
...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {},
|
|
4413
|
+
...typeof entry.title === "string" && entry.title.trim() ? { title: entry.title.trim() } : {},
|
|
4414
|
+
...typeof entry.status === "string" && entry.status.trim() ? { status: entry.status.trim() } : {},
|
|
4415
|
+
data
|
|
4416
|
+
});
|
|
4417
|
+
}
|
|
4418
|
+
if (page.length < PAPERCLIP_LABEL_PAGE_SIZE || requestedIssueId && records.length > 0 || requestedExternalId && page.length < PAPERCLIP_LABEL_PAGE_SIZE) {
|
|
4419
|
+
break;
|
|
4420
|
+
}
|
|
4421
|
+
offset += page.length;
|
|
4422
|
+
}
|
|
4423
|
+
return records;
|
|
4424
|
+
}
|
|
3252
4425
|
async function findStoredStatusTransitionCommentAnnotation(ctx, params) {
|
|
3253
4426
|
const issueId = params.issueId.trim();
|
|
3254
4427
|
const commentId = params.commentId.trim();
|
|
@@ -3307,6 +4480,28 @@ async function upsertGitHubIssueLinkRecord(ctx, mapping, issueId, githubIssue, l
|
|
|
3307
4480
|
}
|
|
3308
4481
|
});
|
|
3309
4482
|
}
|
|
4483
|
+
async function upsertGitHubPullRequestLinkRecord(ctx, params) {
|
|
4484
|
+
await ctx.entities.upsert({
|
|
4485
|
+
entityType: PULL_REQUEST_LINK_ENTITY_TYPE,
|
|
4486
|
+
scopeKind: "issue",
|
|
4487
|
+
scopeId: params.issueId,
|
|
4488
|
+
externalId: params.pullRequestUrl,
|
|
4489
|
+
title: `GitHub pull request #${params.pullRequestNumber}`,
|
|
4490
|
+
status: params.pullRequestState,
|
|
4491
|
+
data: {
|
|
4492
|
+
companyId: params.companyId,
|
|
4493
|
+
paperclipProjectId: params.projectId,
|
|
4494
|
+
repositoryUrl: getNormalizedMappingRepositoryUrl({
|
|
4495
|
+
repositoryUrl: params.repositoryUrl
|
|
4496
|
+
}),
|
|
4497
|
+
githubPullRequestNumber: params.pullRequestNumber,
|
|
4498
|
+
githubPullRequestUrl: params.pullRequestUrl,
|
|
4499
|
+
githubPullRequestState: params.pullRequestState,
|
|
4500
|
+
title: params.pullRequestTitle,
|
|
4501
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4502
|
+
}
|
|
4503
|
+
});
|
|
4504
|
+
}
|
|
3310
4505
|
async function upsertStatusTransitionCommentAnnotation(ctx, params) {
|
|
3311
4506
|
const { issueId, commentId, annotation } = params;
|
|
3312
4507
|
await ctx.entities.upsert({
|
|
@@ -4247,7 +5442,13 @@ async function listRepositoryIssuesForImport(allIssues) {
|
|
|
4247
5442
|
return sortIssuesForImport(allIssues.filter((issue) => issue.state === "open"));
|
|
4248
5443
|
}
|
|
4249
5444
|
function shouldIgnoreGitHubIssue(issue, advancedSettings) {
|
|
4250
|
-
|
|
5445
|
+
if (!issue.authorLogin || advancedSettings.ignoredIssueAuthorUsernames.length === 0) {
|
|
5446
|
+
return false;
|
|
5447
|
+
}
|
|
5448
|
+
const issueAuthorAliases = new Set(buildGitHubUsernameAliases(issue.authorLogin));
|
|
5449
|
+
return advancedSettings.ignoredIssueAuthorUsernames.some(
|
|
5450
|
+
(ignoredUsername) => buildGitHubUsernameAliases(ignoredUsername).some((alias) => issueAuthorAliases.has(alias))
|
|
5451
|
+
);
|
|
4251
5452
|
}
|
|
4252
5453
|
async function applyDefaultAssigneeToPaperclipIssue(ctx, params) {
|
|
4253
5454
|
const { companyId, issueId, defaultAssigneeAgentId } = params;
|
|
@@ -4318,25 +5519,30 @@ async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, avail
|
|
|
4318
5519
|
createdIssueDescription = createdIssue.description;
|
|
4319
5520
|
createPath = "sdk";
|
|
4320
5521
|
}
|
|
5522
|
+
const ensuredCreatedIssueId = createdIssueId;
|
|
5523
|
+
if (!ensuredCreatedIssueId) {
|
|
5524
|
+
throw new Error("GitHub sync could not resolve the created Paperclip issue id.");
|
|
5525
|
+
}
|
|
5526
|
+
const normalizedCreatedIssueDescription = createdIssueDescription ?? void 0;
|
|
4321
5527
|
if (createPath !== "sdk") {
|
|
4322
5528
|
await applyDefaultAssigneeToPaperclipIssue(ctx, {
|
|
4323
5529
|
companyId: mapping.companyId,
|
|
4324
|
-
issueId:
|
|
5530
|
+
issueId: ensuredCreatedIssueId,
|
|
4325
5531
|
defaultAssigneeAgentId: advancedSettings.defaultAssigneeAgentId
|
|
4326
5532
|
});
|
|
4327
5533
|
}
|
|
4328
|
-
if (normalizeIssueDescriptionValue(
|
|
5534
|
+
if (normalizeIssueDescriptionValue(normalizedCreatedIssueDescription) !== description) {
|
|
4329
5535
|
logIssueDescriptionDiagnostic(
|
|
4330
5536
|
ctx,
|
|
4331
5537
|
"warn",
|
|
4332
5538
|
"GitHub sync detected a missing or mismatched Paperclip issue description immediately after issue creation.",
|
|
4333
5539
|
{
|
|
4334
5540
|
companyId: mapping.companyId,
|
|
4335
|
-
issueId:
|
|
5541
|
+
issueId: ensuredCreatedIssueId,
|
|
4336
5542
|
paperclipApiBaseUrl,
|
|
4337
5543
|
githubIssue: issue,
|
|
4338
5544
|
linkedPullRequestNumbers: [],
|
|
4339
|
-
currentDescription:
|
|
5545
|
+
currentDescription: normalizedCreatedIssueDescription,
|
|
4340
5546
|
nextDescription: description,
|
|
4341
5547
|
reason: "create_response_mismatch",
|
|
4342
5548
|
createPath
|
|
@@ -4346,8 +5552,8 @@ async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, avail
|
|
|
4346
5552
|
ctx,
|
|
4347
5553
|
{
|
|
4348
5554
|
companyId: mapping.companyId,
|
|
4349
|
-
issueId:
|
|
4350
|
-
currentDescription:
|
|
5555
|
+
issueId: ensuredCreatedIssueId,
|
|
5556
|
+
currentDescription: normalizedCreatedIssueDescription,
|
|
4351
5557
|
githubIssue: issue,
|
|
4352
5558
|
linkedPullRequestNumbers: [],
|
|
4353
5559
|
paperclipApiBaseUrl,
|
|
@@ -4355,7 +5561,7 @@ async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, avail
|
|
|
4355
5561
|
}
|
|
4356
5562
|
);
|
|
4357
5563
|
}
|
|
4358
|
-
await upsertGitHubIssueLinkRecord(ctx, mapping,
|
|
5564
|
+
await upsertGitHubIssueLinkRecord(ctx, mapping, ensuredCreatedIssueId, issue, []);
|
|
4359
5565
|
if (syncFailureContext) {
|
|
4360
5566
|
updateSyncFailureContext(syncFailureContext, {
|
|
4361
5567
|
phase: "syncing_labels",
|
|
@@ -4373,11 +5579,11 @@ async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, avail
|
|
|
4373
5579
|
await applyPaperclipLabelsToIssue(
|
|
4374
5580
|
ctx,
|
|
4375
5581
|
mapping.companyId,
|
|
4376
|
-
|
|
5582
|
+
ensuredCreatedIssueId,
|
|
4377
5583
|
labelResolution.labels
|
|
4378
5584
|
);
|
|
4379
5585
|
return {
|
|
4380
|
-
id:
|
|
5586
|
+
id: ensuredCreatedIssueId,
|
|
4381
5587
|
unresolvedGitHubLabels: labelResolution.unresolvedGitHubLabels,
|
|
4382
5588
|
...labelResolution.failure ? { labelResolutionFailure: labelResolution.failure } : {}
|
|
4383
5589
|
};
|
|
@@ -4449,7 +5655,7 @@ async function ensurePaperclipIssueImported(ctx, mapping, advancedSettings, issu
|
|
|
4449
5655
|
}
|
|
4450
5656
|
return createdIssue.id;
|
|
4451
5657
|
}
|
|
4452
|
-
async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mapping, advancedSettings, allIssuesById, importedIssues, createdIssueIds, availableLabels, paperclipApiBaseUrl, linkedPullRequestsByIssueNumber, issueStatusSnapshotCache, pullRequestStatusCache, repositoryMaintainerCache, syncFailureContext, failures, onProgress) {
|
|
5658
|
+
async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mapping, advancedSettings, allIssuesById, importedIssues, createdIssueIds, availableLabels, paperclipApiBaseUrl, linkedPullRequestsByIssueNumber, issueStatusSnapshotCache, pullRequestStatusCache, repositoryMaintainerCache, syncFailureContext, failures, assertNotCancelled, onProgress) {
|
|
4453
5659
|
if (!mapping.companyId || !ctx.issues || typeof ctx.issues.get !== "function" || typeof ctx.issues.update !== "function") {
|
|
4454
5660
|
return {
|
|
4455
5661
|
updatedStatusesCount: 0,
|
|
@@ -4463,6 +5669,9 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
4463
5669
|
let completedIssueCount = 0;
|
|
4464
5670
|
const totalIssueCount = importedIssues.length;
|
|
4465
5671
|
for (const importedIssue of importedIssues) {
|
|
5672
|
+
if (assertNotCancelled) {
|
|
5673
|
+
await assertNotCancelled();
|
|
5674
|
+
}
|
|
4466
5675
|
const githubIssue = allIssuesById.get(importedIssue.githubIssueId);
|
|
4467
5676
|
try {
|
|
4468
5677
|
if (!githubIssue) {
|
|
@@ -4824,6 +6033,230 @@ async function createGitHubToolOctokit(ctx) {
|
|
|
4824
6033
|
}
|
|
4825
6034
|
return new Octokit({ auth: token });
|
|
4826
6035
|
}
|
|
6036
|
+
async function listGitHubRepositoryOpenPullRequestNumbers(octokit, repository) {
|
|
6037
|
+
const response = await octokit.rest.pulls.list({
|
|
6038
|
+
owner: repository.owner,
|
|
6039
|
+
repo: repository.repo,
|
|
6040
|
+
state: "open",
|
|
6041
|
+
per_page: 1,
|
|
6042
|
+
headers: {
|
|
6043
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6044
|
+
}
|
|
6045
|
+
});
|
|
6046
|
+
return (response.data ?? []).map((pullRequest) => typeof pullRequest.number === "number" && pullRequest.number > 0 ? Math.floor(pullRequest.number) : null).filter((number) => number !== null);
|
|
6047
|
+
}
|
|
6048
|
+
async function probeGitHubRepositoryTokenCapability(fn, options = {}) {
|
|
6049
|
+
try {
|
|
6050
|
+
await fn();
|
|
6051
|
+
return "granted";
|
|
6052
|
+
} catch (error) {
|
|
6053
|
+
return classifyGitHubCapabilityProbeError(error, options);
|
|
6054
|
+
}
|
|
6055
|
+
}
|
|
6056
|
+
async function loadGitHubRepositoryTokenCapabilityAudit(octokit, repository, options) {
|
|
6057
|
+
try {
|
|
6058
|
+
await octokit.rest.repos.get({
|
|
6059
|
+
owner: repository.owner,
|
|
6060
|
+
repo: repository.repo,
|
|
6061
|
+
headers: {
|
|
6062
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6063
|
+
}
|
|
6064
|
+
});
|
|
6065
|
+
} catch (error) {
|
|
6066
|
+
const status = getErrorStatus(error);
|
|
6067
|
+
return buildGitHubRepositoryTokenCapabilityAudit({
|
|
6068
|
+
repository,
|
|
6069
|
+
canComment: false,
|
|
6070
|
+
canReview: false,
|
|
6071
|
+
canClose: false,
|
|
6072
|
+
canUpdateBranch: false,
|
|
6073
|
+
canMerge: false,
|
|
6074
|
+
canRerunCi: false,
|
|
6075
|
+
missingPermissions: status === 401 || status === 403 || status === 404 ? [
|
|
6076
|
+
"Metadata: read",
|
|
6077
|
+
"Issues: write or Pull requests: write",
|
|
6078
|
+
"Pull requests: write",
|
|
6079
|
+
"Contents: write",
|
|
6080
|
+
"Checks: write"
|
|
6081
|
+
] : [],
|
|
6082
|
+
warnings: [
|
|
6083
|
+
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.`
|
|
6084
|
+
]
|
|
6085
|
+
});
|
|
6086
|
+
}
|
|
6087
|
+
let samplePullRequestNumber = typeof options?.samplePullRequestNumber === "number" && options.samplePullRequestNumber > 0 ? Math.floor(options.samplePullRequestNumber) : void 0;
|
|
6088
|
+
if (!samplePullRequestNumber) {
|
|
6089
|
+
try {
|
|
6090
|
+
samplePullRequestNumber = (await listGitHubRepositoryOpenPullRequestNumbers(octokit, repository))[0];
|
|
6091
|
+
} catch {
|
|
6092
|
+
return buildGitHubRepositoryTokenCapabilityAudit({
|
|
6093
|
+
repository,
|
|
6094
|
+
canComment: false,
|
|
6095
|
+
canReview: false,
|
|
6096
|
+
canClose: false,
|
|
6097
|
+
canUpdateBranch: false,
|
|
6098
|
+
canMerge: false,
|
|
6099
|
+
canRerunCi: false,
|
|
6100
|
+
missingPermissions: [],
|
|
6101
|
+
warnings: [
|
|
6102
|
+
`GitHub Sync could not verify write permissions for ${formatRepositoryLabel(repository)} because GitHub did not return a sample pull request.`
|
|
6103
|
+
]
|
|
6104
|
+
});
|
|
6105
|
+
}
|
|
6106
|
+
}
|
|
6107
|
+
if (!samplePullRequestNumber) {
|
|
6108
|
+
return buildGitHubRepositoryTokenCapabilityAudit({
|
|
6109
|
+
repository,
|
|
6110
|
+
canComment: false,
|
|
6111
|
+
canReview: false,
|
|
6112
|
+
canClose: false,
|
|
6113
|
+
canUpdateBranch: false,
|
|
6114
|
+
canMerge: false,
|
|
6115
|
+
canRerunCi: false,
|
|
6116
|
+
missingPermissions: [],
|
|
6117
|
+
warnings: [
|
|
6118
|
+
`GitHub Sync could not verify write permissions for ${formatRepositoryLabel(repository)} because the repository has no open pull requests to probe safely.`
|
|
6119
|
+
]
|
|
6120
|
+
});
|
|
6121
|
+
}
|
|
6122
|
+
const impossibleSha = "0000000000000000000000000000000000000000";
|
|
6123
|
+
const [
|
|
6124
|
+
commentProbe,
|
|
6125
|
+
reviewProbe,
|
|
6126
|
+
closeProbe,
|
|
6127
|
+
updateBranchProbe,
|
|
6128
|
+
mergeProbe,
|
|
6129
|
+
rerunCiProbe
|
|
6130
|
+
] = await Promise.all([
|
|
6131
|
+
probeGitHubRepositoryTokenCapability(
|
|
6132
|
+
() => octokit.rest.issues.createComment({
|
|
6133
|
+
owner: repository.owner,
|
|
6134
|
+
repo: repository.repo,
|
|
6135
|
+
issue_number: samplePullRequestNumber,
|
|
6136
|
+
body: "",
|
|
6137
|
+
headers: {
|
|
6138
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6139
|
+
}
|
|
6140
|
+
}),
|
|
6141
|
+
{ grantedStatuses: [422] }
|
|
6142
|
+
),
|
|
6143
|
+
probeGitHubRepositoryTokenCapability(
|
|
6144
|
+
() => octokit.rest.pulls.createReview({
|
|
6145
|
+
owner: repository.owner,
|
|
6146
|
+
repo: repository.repo,
|
|
6147
|
+
pull_number: samplePullRequestNumber,
|
|
6148
|
+
event: "REQUEST_CHANGES",
|
|
6149
|
+
headers: {
|
|
6150
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6151
|
+
}
|
|
6152
|
+
}),
|
|
6153
|
+
{ grantedStatuses: [422] }
|
|
6154
|
+
),
|
|
6155
|
+
probeGitHubRepositoryTokenCapability(
|
|
6156
|
+
() => octokit.request("PATCH /repos/{owner}/{repo}/pulls/{pull_number}", {
|
|
6157
|
+
owner: repository.owner,
|
|
6158
|
+
repo: repository.repo,
|
|
6159
|
+
pull_number: samplePullRequestNumber,
|
|
6160
|
+
state: "__paperclip_invalid_state__",
|
|
6161
|
+
headers: {
|
|
6162
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6163
|
+
}
|
|
6164
|
+
}),
|
|
6165
|
+
{ grantedStatuses: [422] }
|
|
6166
|
+
),
|
|
6167
|
+
probeGitHubRepositoryTokenCapability(
|
|
6168
|
+
() => octokit.request("PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch", {
|
|
6169
|
+
owner: repository.owner,
|
|
6170
|
+
repo: repository.repo,
|
|
6171
|
+
pull_number: samplePullRequestNumber,
|
|
6172
|
+
expected_head_sha: impossibleSha,
|
|
6173
|
+
headers: {
|
|
6174
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6175
|
+
}
|
|
6176
|
+
}),
|
|
6177
|
+
{ grantedStatuses: [409, 422] }
|
|
6178
|
+
),
|
|
6179
|
+
probeGitHubRepositoryTokenCapability(
|
|
6180
|
+
() => octokit.rest.pulls.merge({
|
|
6181
|
+
owner: repository.owner,
|
|
6182
|
+
repo: repository.repo,
|
|
6183
|
+
pull_number: samplePullRequestNumber,
|
|
6184
|
+
sha: impossibleSha,
|
|
6185
|
+
headers: {
|
|
6186
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6187
|
+
}
|
|
6188
|
+
}),
|
|
6189
|
+
{ grantedStatuses: [405, 409, 422] }
|
|
6190
|
+
),
|
|
6191
|
+
probeGitHubRepositoryTokenCapability(
|
|
6192
|
+
() => octokit.rest.checks.rerequestSuite({
|
|
6193
|
+
owner: repository.owner,
|
|
6194
|
+
repo: repository.repo,
|
|
6195
|
+
check_suite_id: 0,
|
|
6196
|
+
headers: {
|
|
6197
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6198
|
+
}
|
|
6199
|
+
}),
|
|
6200
|
+
{ allowNotFoundAsGranted: true }
|
|
6201
|
+
)
|
|
6202
|
+
]);
|
|
6203
|
+
const missingPermissions = [
|
|
6204
|
+
...commentProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("comment")] : [],
|
|
6205
|
+
...reviewProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("review")] : [],
|
|
6206
|
+
...closeProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("close")] : [],
|
|
6207
|
+
...updateBranchProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("update_branch")] : [],
|
|
6208
|
+
...mergeProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("merge")] : [],
|
|
6209
|
+
...rerunCiProbe === "missing" ? [getGitHubCapabilityMissingPermissionLabel("rerun_ci")] : []
|
|
6210
|
+
];
|
|
6211
|
+
const warnings = [
|
|
6212
|
+
...commentProbe === "unknown" ? ["GitHub Sync could not verify comment permissions."] : [],
|
|
6213
|
+
...reviewProbe === "unknown" ? ["GitHub Sync could not verify review permissions."] : [],
|
|
6214
|
+
...closeProbe === "unknown" ? ["GitHub Sync could not verify close permissions."] : [],
|
|
6215
|
+
...updateBranchProbe === "unknown" ? ["GitHub Sync could not verify update-branch permissions."] : [],
|
|
6216
|
+
...mergeProbe === "unknown" ? ["GitHub Sync could not verify merge permissions."] : [],
|
|
6217
|
+
...rerunCiProbe === "unknown" ? ["GitHub Sync could not verify re-run CI permissions."] : []
|
|
6218
|
+
];
|
|
6219
|
+
return buildGitHubRepositoryTokenCapabilityAudit({
|
|
6220
|
+
repository,
|
|
6221
|
+
samplePullRequestNumber,
|
|
6222
|
+
canComment: commentProbe === "granted",
|
|
6223
|
+
canReview: reviewProbe === "granted",
|
|
6224
|
+
canClose: closeProbe === "granted",
|
|
6225
|
+
canUpdateBranch: updateBranchProbe === "granted",
|
|
6226
|
+
canMerge: mergeProbe === "granted",
|
|
6227
|
+
canRerunCi: rerunCiProbe === "granted",
|
|
6228
|
+
missingPermissions,
|
|
6229
|
+
warnings
|
|
6230
|
+
});
|
|
6231
|
+
}
|
|
6232
|
+
async function getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, repository, options) {
|
|
6233
|
+
const cacheKey = buildGitHubRepositoryTokenCapabilityAuditCacheKey(repository, options?.samplePullRequestNumber);
|
|
6234
|
+
const cachedAudit = getFreshCacheValue(activeGitHubRepositoryTokenCapabilityAuditCache, cacheKey);
|
|
6235
|
+
if (cachedAudit) {
|
|
6236
|
+
return cachedAudit;
|
|
6237
|
+
}
|
|
6238
|
+
const inFlightAudit = activeGitHubRepositoryTokenCapabilityAuditPromiseCache.get(cacheKey);
|
|
6239
|
+
if (inFlightAudit) {
|
|
6240
|
+
return inFlightAudit;
|
|
6241
|
+
}
|
|
6242
|
+
const loadAuditPromise = (async () => {
|
|
6243
|
+
const audit = await loadGitHubRepositoryTokenCapabilityAudit(octokit, repository, options);
|
|
6244
|
+
return setCacheValue(
|
|
6245
|
+
activeGitHubRepositoryTokenCapabilityAuditCache,
|
|
6246
|
+
cacheKey,
|
|
6247
|
+
audit,
|
|
6248
|
+
GITHUB_TOKEN_PERMISSION_AUDIT_CACHE_TTL_MS
|
|
6249
|
+
);
|
|
6250
|
+
})();
|
|
6251
|
+
activeGitHubRepositoryTokenCapabilityAuditPromiseCache.set(cacheKey, loadAuditPromise);
|
|
6252
|
+
try {
|
|
6253
|
+
return await loadAuditPromise;
|
|
6254
|
+
} finally {
|
|
6255
|
+
if (activeGitHubRepositoryTokenCapabilityAuditPromiseCache.get(cacheKey) === loadAuditPromise) {
|
|
6256
|
+
activeGitHubRepositoryTokenCapabilityAuditPromiseCache.delete(cacheKey);
|
|
6257
|
+
}
|
|
6258
|
+
}
|
|
6259
|
+
}
|
|
4827
6260
|
async function resolveRepositoryFromRunContext(ctx, runCtx) {
|
|
4828
6261
|
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
4829
6262
|
const mappings = getSyncableMappingsForTarget(settings.mappings, {
|
|
@@ -4846,136 +6279,2217 @@ async function resolveRepositoryFromRunContext(ctx, runCtx) {
|
|
|
4846
6279
|
if (repositories.length === 0) {
|
|
4847
6280
|
throw new Error("No GitHub repository is mapped to the current Paperclip project. Pass repository explicitly.");
|
|
4848
6281
|
}
|
|
4849
|
-
throw new Error("Multiple GitHub repositories are mapped to the current Paperclip project. Pass repository explicitly.");
|
|
6282
|
+
throw new Error("Multiple GitHub repositories are mapped to the current Paperclip project. Pass repository explicitly.");
|
|
6283
|
+
}
|
|
6284
|
+
async function resolveGitHubToolRepository(ctx, runCtx, input) {
|
|
6285
|
+
const explicitRepository = normalizeOptionalToolString(input.repository);
|
|
6286
|
+
if (explicitRepository) {
|
|
6287
|
+
return requireRepositoryReference(explicitRepository);
|
|
6288
|
+
}
|
|
6289
|
+
const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
|
|
6290
|
+
if (paperclipIssueId) {
|
|
6291
|
+
const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
|
|
6292
|
+
if (!link) {
|
|
6293
|
+
throw new Error("This Paperclip issue is not linked to a GitHub issue yet. Pass repository explicitly.");
|
|
6294
|
+
}
|
|
6295
|
+
return requireRepositoryReference(link.repositoryUrl);
|
|
6296
|
+
}
|
|
6297
|
+
return resolveRepositoryFromRunContext(ctx, runCtx);
|
|
6298
|
+
}
|
|
6299
|
+
async function resolveGitHubIssueToolTarget(ctx, runCtx, input) {
|
|
6300
|
+
const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
|
|
6301
|
+
if (paperclipIssueId) {
|
|
6302
|
+
const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
|
|
6303
|
+
if (!link) {
|
|
6304
|
+
throw new Error("This Paperclip issue is not linked to a GitHub issue yet.");
|
|
6305
|
+
}
|
|
6306
|
+
const repository2 = assertExplicitRepositoryMatchesLinkedRepository(
|
|
6307
|
+
input.repository,
|
|
6308
|
+
link.repositoryUrl,
|
|
6309
|
+
"The provided repository does not match the linked GitHub repository for this Paperclip issue."
|
|
6310
|
+
);
|
|
6311
|
+
const explicitIssueNumber = normalizeToolPositiveInteger(input.issueNumber);
|
|
6312
|
+
if (explicitIssueNumber !== void 0 && explicitIssueNumber !== link.githubIssueNumber) {
|
|
6313
|
+
throw new Error("The provided issue number does not match the linked GitHub issue for this Paperclip issue.");
|
|
6314
|
+
}
|
|
6315
|
+
return {
|
|
6316
|
+
repository: repository2,
|
|
6317
|
+
issueNumber: link.githubIssueNumber,
|
|
6318
|
+
paperclipIssueId,
|
|
6319
|
+
githubIssueId: link.githubIssueId,
|
|
6320
|
+
githubIssueUrl: link.githubIssueUrl
|
|
6321
|
+
};
|
|
6322
|
+
}
|
|
6323
|
+
const repository = await resolveGitHubToolRepository(ctx, runCtx, input);
|
|
6324
|
+
const issueNumber = normalizeToolPositiveInteger(input.issueNumber);
|
|
6325
|
+
if (issueNumber === void 0) {
|
|
6326
|
+
throw new Error("issueNumber is required when paperclipIssueId is not provided.");
|
|
6327
|
+
}
|
|
6328
|
+
return {
|
|
6329
|
+
repository,
|
|
6330
|
+
issueNumber
|
|
6331
|
+
};
|
|
6332
|
+
}
|
|
6333
|
+
async function resolveGitHubPullRequestToolTarget(ctx, runCtx, input) {
|
|
6334
|
+
const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
|
|
6335
|
+
if (paperclipIssueId) {
|
|
6336
|
+
const link = await resolvePaperclipIssueGitHubLink(ctx, paperclipIssueId, runCtx.companyId);
|
|
6337
|
+
if (!link) {
|
|
6338
|
+
throw new Error("This Paperclip issue is not linked to GitHub yet.");
|
|
6339
|
+
}
|
|
6340
|
+
const repository2 = assertExplicitRepositoryMatchesLinkedRepository(
|
|
6341
|
+
input.repository,
|
|
6342
|
+
link.repositoryUrl,
|
|
6343
|
+
"repository must match the GitHub repository linked to the provided Paperclip issue."
|
|
6344
|
+
);
|
|
6345
|
+
const explicitPullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
6346
|
+
if (explicitPullRequestNumber !== void 0) {
|
|
6347
|
+
return {
|
|
6348
|
+
repository: repository2,
|
|
6349
|
+
pullRequestNumber: explicitPullRequestNumber,
|
|
6350
|
+
paperclipIssueId
|
|
6351
|
+
};
|
|
6352
|
+
}
|
|
6353
|
+
if (link.linkedPullRequestNumbers.length === 1) {
|
|
6354
|
+
return {
|
|
6355
|
+
repository: repository2,
|
|
6356
|
+
pullRequestNumber: link.linkedPullRequestNumbers[0],
|
|
6357
|
+
paperclipIssueId
|
|
6358
|
+
};
|
|
6359
|
+
}
|
|
6360
|
+
throw new Error("pullRequestNumber is required unless the linked Paperclip issue has exactly one linked pull request.");
|
|
6361
|
+
}
|
|
6362
|
+
const repository = await resolveGitHubToolRepository(ctx, runCtx, input);
|
|
6363
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
6364
|
+
if (pullRequestNumber === void 0) {
|
|
6365
|
+
throw new Error("pullRequestNumber is required when paperclipIssueId is not provided.");
|
|
6366
|
+
}
|
|
6367
|
+
return {
|
|
6368
|
+
repository,
|
|
6369
|
+
pullRequestNumber
|
|
6370
|
+
};
|
|
6371
|
+
}
|
|
6372
|
+
function formatAiAuthorshipFooter(llmModel) {
|
|
6373
|
+
return `
|
|
6374
|
+
|
|
6375
|
+
---
|
|
6376
|
+
${AI_AUTHORED_COMMENT_FOOTER_PREFIX}${llmModel.trim()}.`;
|
|
6377
|
+
}
|
|
6378
|
+
function appendAiAuthorshipFooter(body, llmModel) {
|
|
6379
|
+
const trimmedBody = body.trim();
|
|
6380
|
+
if (!trimmedBody) {
|
|
6381
|
+
throw new Error("Comment body cannot be empty.");
|
|
6382
|
+
}
|
|
6383
|
+
const trimmedModel = llmModel.trim();
|
|
6384
|
+
if (!trimmedModel) {
|
|
6385
|
+
throw new Error("llmModel is required when posting a GitHub comment.");
|
|
6386
|
+
}
|
|
6387
|
+
return `${trimmedBody}${formatAiAuthorshipFooter(trimmedModel)}`;
|
|
6388
|
+
}
|
|
6389
|
+
async function listAllGitHubIssueComments(octokit, repository, issueNumber) {
|
|
6390
|
+
const comments = [];
|
|
6391
|
+
for await (const response of octokit.paginate.iterator(octokit.rest.issues.listComments, {
|
|
6392
|
+
owner: repository.owner,
|
|
6393
|
+
repo: repository.repo,
|
|
6394
|
+
issue_number: issueNumber,
|
|
6395
|
+
per_page: 100,
|
|
6396
|
+
headers: {
|
|
6397
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6398
|
+
}
|
|
6399
|
+
})) {
|
|
6400
|
+
for (const comment of response.data) {
|
|
6401
|
+
comments.push({
|
|
6402
|
+
id: comment.id,
|
|
6403
|
+
body: comment.body ?? "",
|
|
6404
|
+
url: comment.html_url ?? void 0,
|
|
6405
|
+
authorLogin: normalizeGitHubUserLogin(comment.user?.login),
|
|
6406
|
+
authorUrl: comment.user?.html_url ?? void 0,
|
|
6407
|
+
authorAvatarUrl: comment.user?.avatar_url ?? void 0,
|
|
6408
|
+
createdAt: comment.created_at ?? void 0,
|
|
6409
|
+
updatedAt: comment.updated_at ?? void 0
|
|
6410
|
+
});
|
|
6411
|
+
}
|
|
6412
|
+
}
|
|
6413
|
+
return comments;
|
|
6414
|
+
}
|
|
6415
|
+
async function listPaperclipIssuesForProject(ctx, companyId, projectId) {
|
|
6416
|
+
const issues = [];
|
|
6417
|
+
for (let offset = 0; ; ) {
|
|
6418
|
+
const page = await ctx.issues.list({
|
|
6419
|
+
companyId,
|
|
6420
|
+
projectId,
|
|
6421
|
+
limit: PAPERCLIP_LABEL_PAGE_SIZE,
|
|
6422
|
+
offset
|
|
6423
|
+
});
|
|
6424
|
+
if (page.length === 0) {
|
|
6425
|
+
break;
|
|
6426
|
+
}
|
|
6427
|
+
issues.push(...page);
|
|
6428
|
+
if (page.length < PAPERCLIP_LABEL_PAGE_SIZE) {
|
|
6429
|
+
break;
|
|
6430
|
+
}
|
|
6431
|
+
offset += page.length;
|
|
6432
|
+
}
|
|
6433
|
+
return issues;
|
|
6434
|
+
}
|
|
6435
|
+
function buildProjectPullRequestPerson(input) {
|
|
6436
|
+
const normalizedLogin = normalizeGitHubUsername(input.login) ?? "unknown";
|
|
6437
|
+
const displayName = typeof input.name === "string" && input.name.trim() ? input.name.trim() : normalizedLogin;
|
|
6438
|
+
const profileUrl = typeof input.url === "string" && input.url.trim() ? input.url.trim() : `https://github.com/${normalizedLogin}`;
|
|
6439
|
+
const avatarUrl = typeof input.avatarUrl === "string" && input.avatarUrl.trim() ? input.avatarUrl.trim() : void 0;
|
|
6440
|
+
return {
|
|
6441
|
+
name: displayName,
|
|
6442
|
+
handle: `@${normalizedLogin}`,
|
|
6443
|
+
profileUrl,
|
|
6444
|
+
...avatarUrl ? { avatarUrl } : {}
|
|
6445
|
+
};
|
|
6446
|
+
}
|
|
6447
|
+
function normalizeProjectPullRequestLabels(nodes) {
|
|
6448
|
+
if (!Array.isArray(nodes)) {
|
|
6449
|
+
return [];
|
|
6450
|
+
}
|
|
6451
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6452
|
+
const labels = [];
|
|
6453
|
+
for (const entry of nodes) {
|
|
6454
|
+
const name = typeof entry?.name === "string" ? entry.name.trim() : "";
|
|
6455
|
+
if (!name) {
|
|
6456
|
+
continue;
|
|
6457
|
+
}
|
|
6458
|
+
const key = normalizeLabelName(name);
|
|
6459
|
+
if (seen.has(key)) {
|
|
6460
|
+
continue;
|
|
6461
|
+
}
|
|
6462
|
+
seen.add(key);
|
|
6463
|
+
labels.push({
|
|
6464
|
+
name,
|
|
6465
|
+
...normalizeHexColor(entry?.color) ? { color: normalizeHexColor(entry?.color) } : {}
|
|
6466
|
+
});
|
|
6467
|
+
}
|
|
6468
|
+
return labels;
|
|
6469
|
+
}
|
|
6470
|
+
function summarizeGitHubPullRequestReviewsFromEntries(entries) {
|
|
6471
|
+
const latestStateByReviewer = /* @__PURE__ */ new Map();
|
|
6472
|
+
let anonymousApprovals = 0;
|
|
6473
|
+
let anonymousChangesRequested = 0;
|
|
6474
|
+
for (const entry of entries) {
|
|
6475
|
+
const state = typeof entry.state === "string" ? entry.state.trim().toUpperCase() : "";
|
|
6476
|
+
const authorLogin = normalizeGitHubUsername(entry.authorLogin);
|
|
6477
|
+
if (state === "APPROVED") {
|
|
6478
|
+
if (authorLogin) {
|
|
6479
|
+
latestStateByReviewer.set(authorLogin, "APPROVED");
|
|
6480
|
+
} else {
|
|
6481
|
+
anonymousApprovals += 1;
|
|
6482
|
+
}
|
|
6483
|
+
continue;
|
|
6484
|
+
}
|
|
6485
|
+
if (state === "CHANGES_REQUESTED") {
|
|
6486
|
+
if (authorLogin) {
|
|
6487
|
+
latestStateByReviewer.set(authorLogin, "CHANGES_REQUESTED");
|
|
6488
|
+
} else {
|
|
6489
|
+
anonymousChangesRequested += 1;
|
|
6490
|
+
}
|
|
6491
|
+
continue;
|
|
6492
|
+
}
|
|
6493
|
+
if (state === "DISMISSED" && authorLogin) {
|
|
6494
|
+
latestStateByReviewer.delete(authorLogin);
|
|
6495
|
+
}
|
|
6496
|
+
}
|
|
6497
|
+
let approvals = anonymousApprovals;
|
|
6498
|
+
let changesRequested = anonymousChangesRequested;
|
|
6499
|
+
for (const state of latestStateByReviewer.values()) {
|
|
6500
|
+
if (state === "APPROVED") {
|
|
6501
|
+
approvals += 1;
|
|
6502
|
+
} else if (state === "CHANGES_REQUESTED") {
|
|
6503
|
+
changesRequested += 1;
|
|
6504
|
+
}
|
|
6505
|
+
}
|
|
6506
|
+
return {
|
|
6507
|
+
approvals,
|
|
6508
|
+
changesRequested
|
|
6509
|
+
};
|
|
6510
|
+
}
|
|
6511
|
+
function summarizeGitHubPullRequestReviewNodes(nodes) {
|
|
6512
|
+
const entries = (nodes ?? []).map((node) => ({
|
|
6513
|
+
state: node?.state ?? void 0,
|
|
6514
|
+
authorLogin: node?.author?.login ?? void 0
|
|
6515
|
+
}));
|
|
6516
|
+
return summarizeGitHubPullRequestReviewsFromEntries(entries);
|
|
6517
|
+
}
|
|
6518
|
+
async function listGitHubPullRequestReviewSummary(octokit, repository, pullRequestNumber) {
|
|
6519
|
+
const entries = [];
|
|
6520
|
+
for await (const response of octokit.paginate.iterator(octokit.rest.pulls.listReviews, {
|
|
6521
|
+
owner: repository.owner,
|
|
6522
|
+
repo: repository.repo,
|
|
6523
|
+
pull_number: pullRequestNumber,
|
|
6524
|
+
per_page: 100,
|
|
6525
|
+
headers: {
|
|
6526
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
6527
|
+
}
|
|
6528
|
+
})) {
|
|
6529
|
+
for (const review of response.data) {
|
|
6530
|
+
entries.push({
|
|
6531
|
+
state: review.state ?? void 0,
|
|
6532
|
+
authorLogin: review.user?.login ?? void 0
|
|
6533
|
+
});
|
|
6534
|
+
}
|
|
6535
|
+
}
|
|
6536
|
+
return summarizeGitHubPullRequestReviewsFromEntries(entries);
|
|
6537
|
+
}
|
|
6538
|
+
function buildProjectPullRequestScopeCacheKey(params) {
|
|
6539
|
+
return `${params.companyId}:${params.projectId}:${getNormalizedMappingRepositoryUrl({
|
|
6540
|
+
repositoryUrl: params.repositoryUrl
|
|
6541
|
+
})}`;
|
|
6542
|
+
}
|
|
6543
|
+
function buildRepositoryPullRequestCacheScopeKey(repository) {
|
|
6544
|
+
return `${repository.owner.toLowerCase()}/${repository.repo.toLowerCase()}`;
|
|
6545
|
+
}
|
|
6546
|
+
function buildRepositoryPullRequestCollectionCacheKey(repository, suffix) {
|
|
6547
|
+
return `${buildRepositoryPullRequestCacheScopeKey(repository)}:${suffix}`;
|
|
6548
|
+
}
|
|
6549
|
+
function buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, suffix) {
|
|
6550
|
+
return `${buildRepositoryPullRequestCollectionCacheKey(repository, "pull-request")}:${Math.max(1, Math.floor(pullRequestNumber))}:${suffix}`;
|
|
6551
|
+
}
|
|
6552
|
+
function buildRepositoryPullRequestCompareCacheKey(repository, baseBranch, headBranch, headRepositoryOwner) {
|
|
6553
|
+
const normalizedBaseBranch = baseBranch.trim();
|
|
6554
|
+
const normalizedHeadBranch = headBranch.trim();
|
|
6555
|
+
const normalizedHeadRepositoryOwner = typeof headRepositoryOwner === "string" && headRepositoryOwner.trim() ? headRepositoryOwner.trim() : repository.owner;
|
|
6556
|
+
const compareHeadRef = normalizedHeadRepositoryOwner.toLowerCase() === repository.owner.toLowerCase() ? normalizedHeadBranch : `${normalizedHeadRepositoryOwner}:${normalizedHeadBranch}`;
|
|
6557
|
+
return `${buildRepositoryPullRequestCollectionCacheKey(repository, "compare")}:${encodeURIComponent(`${normalizedBaseBranch}...${compareHeadRef}`)}`;
|
|
6558
|
+
}
|
|
6559
|
+
function buildProjectPullRequestPageCacheKey(scope, filter, pageIndex, cursor) {
|
|
6560
|
+
return `${buildProjectPullRequestScopeCacheKey({
|
|
6561
|
+
companyId: scope.companyId,
|
|
6562
|
+
projectId: scope.projectId,
|
|
6563
|
+
repositoryUrl: scope.repository.url
|
|
6564
|
+
})}:page:${filter}:${pageIndex}:${cursor ?? ""}`;
|
|
6565
|
+
}
|
|
6566
|
+
function buildProjectPullRequestDetailCacheKey(scope, pullRequestNumber) {
|
|
6567
|
+
return `${buildProjectPullRequestScopeCacheKey({
|
|
6568
|
+
companyId: scope.companyId,
|
|
6569
|
+
projectId: scope.projectId,
|
|
6570
|
+
repositoryUrl: scope.repository.url
|
|
6571
|
+
})}:detail:${pullRequestNumber}`;
|
|
6572
|
+
}
|
|
6573
|
+
function buildProjectPullRequestSummaryRecordCacheKey(scope, pullRequestNumber) {
|
|
6574
|
+
return `${buildProjectPullRequestScopeCacheKey({
|
|
6575
|
+
companyId: scope.companyId,
|
|
6576
|
+
projectId: scope.projectId,
|
|
6577
|
+
repositoryUrl: scope.repository.url
|
|
6578
|
+
})}:summary-record:${Math.max(1, Math.floor(pullRequestNumber))}`;
|
|
6579
|
+
}
|
|
6580
|
+
function invalidateProjectPullRequestCaches(scope) {
|
|
6581
|
+
const cacheKeyPrefix = `${buildProjectPullRequestScopeCacheKey({
|
|
6582
|
+
companyId: scope.companyId,
|
|
6583
|
+
projectId: scope.projectId,
|
|
6584
|
+
repositoryUrl: scope.repository.url
|
|
6585
|
+
})}:`;
|
|
6586
|
+
const repositoryCacheKeyPrefix = `${buildRepositoryPullRequestCacheScopeKey(scope.repository)}:`;
|
|
6587
|
+
for (const cache of [
|
|
6588
|
+
activeProjectPullRequestPageCache,
|
|
6589
|
+
activeProjectPullRequestSummaryCache,
|
|
6590
|
+
activeProjectPullRequestSummaryRecordCache,
|
|
6591
|
+
activeProjectPullRequestDetailCache,
|
|
6592
|
+
activeProjectPullRequestIssueLookupCache
|
|
6593
|
+
]) {
|
|
6594
|
+
for (const key of cache.keys()) {
|
|
6595
|
+
if (key.startsWith(cacheKeyPrefix)) {
|
|
6596
|
+
cache.delete(key);
|
|
6597
|
+
}
|
|
6598
|
+
}
|
|
6599
|
+
}
|
|
6600
|
+
for (const cache of [
|
|
6601
|
+
activeProjectPullRequestCountCache,
|
|
6602
|
+
activeProjectPullRequestMetricsCache,
|
|
6603
|
+
activeGitHubPullRequestBehindCountCache,
|
|
6604
|
+
activeGitHubPullRequestStatusSnapshotCache,
|
|
6605
|
+
activeGitHubPullRequestReviewSummaryCache,
|
|
6606
|
+
activeGitHubPullRequestReviewThreadSummaryCache
|
|
6607
|
+
]) {
|
|
6608
|
+
for (const key of cache.keys()) {
|
|
6609
|
+
if (key.startsWith(repositoryCacheKeyPrefix)) {
|
|
6610
|
+
cache.delete(key);
|
|
6611
|
+
}
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
6614
|
+
for (const key of activeProjectPullRequestSummaryPromiseCache.keys()) {
|
|
6615
|
+
if (key.startsWith(cacheKeyPrefix)) {
|
|
6616
|
+
activeProjectPullRequestSummaryPromiseCache.delete(key);
|
|
6617
|
+
}
|
|
6618
|
+
}
|
|
6619
|
+
for (const promiseCache of [
|
|
6620
|
+
activeProjectPullRequestCountPromiseCache,
|
|
6621
|
+
activeProjectPullRequestMetricsPromiseCache,
|
|
6622
|
+
activeGitHubPullRequestBehindCountPromiseCache,
|
|
6623
|
+
activeGitHubPullRequestStatusSnapshotPromiseCache,
|
|
6624
|
+
activeGitHubPullRequestReviewSummaryPromiseCache,
|
|
6625
|
+
activeGitHubPullRequestReviewThreadSummaryPromiseCache
|
|
6626
|
+
]) {
|
|
6627
|
+
for (const key of promiseCache.keys()) {
|
|
6628
|
+
if (key.startsWith(repositoryCacheKeyPrefix)) {
|
|
6629
|
+
promiseCache.delete(key);
|
|
6630
|
+
}
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
}
|
|
6634
|
+
async function getGitHubPullRequestBehindCount(octokit, repository, options) {
|
|
6635
|
+
const baseBranch = typeof options.baseBranch === "string" ? options.baseBranch.trim() : "";
|
|
6636
|
+
const headBranch = typeof options.headBranch === "string" ? options.headBranch.trim() : "";
|
|
6637
|
+
if (!baseBranch || !headBranch) {
|
|
6638
|
+
return null;
|
|
6639
|
+
}
|
|
6640
|
+
const headRepositoryOwner = typeof options.headRepositoryOwner === "string" && options.headRepositoryOwner.trim() ? options.headRepositoryOwner.trim() : repository.owner;
|
|
6641
|
+
const compareHeadRef = headRepositoryOwner.toLowerCase() === repository.owner.toLowerCase() ? headBranch : `${headRepositoryOwner}:${headBranch}`;
|
|
6642
|
+
const cacheKey = buildRepositoryPullRequestCompareCacheKey(repository, baseBranch, headBranch, headRepositoryOwner);
|
|
6643
|
+
const cachedBehindCountEntry = getFreshCacheEntry(activeGitHubPullRequestBehindCountCache, cacheKey);
|
|
6644
|
+
if (cachedBehindCountEntry) {
|
|
6645
|
+
return cachedBehindCountEntry.value;
|
|
6646
|
+
}
|
|
6647
|
+
const inFlightBehindCount = activeGitHubPullRequestBehindCountPromiseCache.get(cacheKey);
|
|
6648
|
+
if (inFlightBehindCount) {
|
|
6649
|
+
return inFlightBehindCount;
|
|
6650
|
+
}
|
|
6651
|
+
const loadBehindCountPromise = (async () => {
|
|
6652
|
+
try {
|
|
6653
|
+
const response = await octokit.request("GET /repos/{owner}/{repo}/compare/{basehead}", {
|
|
6654
|
+
owner: repository.owner,
|
|
6655
|
+
repo: repository.repo,
|
|
6656
|
+
basehead: `${baseBranch}...${compareHeadRef}`
|
|
6657
|
+
});
|
|
6658
|
+
const behindBy = response.data?.behind_by;
|
|
6659
|
+
return setCacheValue(
|
|
6660
|
+
activeGitHubPullRequestBehindCountCache,
|
|
6661
|
+
cacheKey,
|
|
6662
|
+
typeof behindBy === "number" && behindBy >= 0 ? Math.floor(behindBy) : null,
|
|
6663
|
+
PROJECT_PULL_REQUEST_BRANCH_COMPARE_CACHE_TTL_MS
|
|
6664
|
+
);
|
|
6665
|
+
} catch {
|
|
6666
|
+
return setCacheValue(
|
|
6667
|
+
activeGitHubPullRequestBehindCountCache,
|
|
6668
|
+
cacheKey,
|
|
6669
|
+
null,
|
|
6670
|
+
PROJECT_PULL_REQUEST_BRANCH_COMPARE_CACHE_TTL_MS
|
|
6671
|
+
);
|
|
6672
|
+
}
|
|
6673
|
+
})();
|
|
6674
|
+
activeGitHubPullRequestBehindCountPromiseCache.set(cacheKey, loadBehindCountPromise);
|
|
6675
|
+
try {
|
|
6676
|
+
return await loadBehindCountPromise;
|
|
6677
|
+
} finally {
|
|
6678
|
+
if (activeGitHubPullRequestBehindCountPromiseCache.get(cacheKey) === loadBehindCountPromise) {
|
|
6679
|
+
activeGitHubPullRequestBehindCountPromiseCache.delete(cacheKey);
|
|
6680
|
+
}
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
6683
|
+
function cacheGitHubPullRequestReviewSummary(repository, pullRequestNumber, summary) {
|
|
6684
|
+
return setCacheValue(
|
|
6685
|
+
activeGitHubPullRequestReviewSummaryCache,
|
|
6686
|
+
buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "review-summary"),
|
|
6687
|
+
summary,
|
|
6688
|
+
PROJECT_PULL_REQUEST_GITHUB_INSIGHT_CACHE_TTL_MS
|
|
6689
|
+
);
|
|
6690
|
+
}
|
|
6691
|
+
function cacheGitHubPullRequestReviewThreadSummary(repository, pullRequestNumber, summary) {
|
|
6692
|
+
return setCacheValue(
|
|
6693
|
+
activeGitHubPullRequestReviewThreadSummaryCache,
|
|
6694
|
+
buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "review-threads"),
|
|
6695
|
+
summary,
|
|
6696
|
+
PROJECT_PULL_REQUEST_GITHUB_INSIGHT_CACHE_TTL_MS
|
|
6697
|
+
);
|
|
6698
|
+
}
|
|
6699
|
+
function cacheGitHubPullRequestStatusSnapshot(repository, snapshot) {
|
|
6700
|
+
return setCacheValue(
|
|
6701
|
+
activeGitHubPullRequestStatusSnapshotCache,
|
|
6702
|
+
buildRepositoryPullRequestRecordCacheKey(repository, snapshot.number, "status"),
|
|
6703
|
+
snapshot,
|
|
6704
|
+
PROJECT_PULL_REQUEST_GITHUB_INSIGHT_CACHE_TTL_MS
|
|
6705
|
+
);
|
|
6706
|
+
}
|
|
6707
|
+
async function getOrLoadCachedGitHubPullRequestReviewSummary(octokit, repository, pullRequestNumber, inlineSummary) {
|
|
6708
|
+
const cacheKey = buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "review-summary");
|
|
6709
|
+
const cachedSummary = getFreshCacheValue(activeGitHubPullRequestReviewSummaryCache, cacheKey);
|
|
6710
|
+
if (cachedSummary) {
|
|
6711
|
+
return cachedSummary;
|
|
6712
|
+
}
|
|
6713
|
+
if (inlineSummary) {
|
|
6714
|
+
return cacheGitHubPullRequestReviewSummary(repository, pullRequestNumber, inlineSummary);
|
|
6715
|
+
}
|
|
6716
|
+
const inFlightSummary = activeGitHubPullRequestReviewSummaryPromiseCache.get(cacheKey);
|
|
6717
|
+
if (inFlightSummary) {
|
|
6718
|
+
return inFlightSummary;
|
|
6719
|
+
}
|
|
6720
|
+
const loadSummaryPromise = (async () => cacheGitHubPullRequestReviewSummary(
|
|
6721
|
+
repository,
|
|
6722
|
+
pullRequestNumber,
|
|
6723
|
+
await listGitHubPullRequestReviewSummary(octokit, repository, pullRequestNumber)
|
|
6724
|
+
))();
|
|
6725
|
+
activeGitHubPullRequestReviewSummaryPromiseCache.set(cacheKey, loadSummaryPromise);
|
|
6726
|
+
try {
|
|
6727
|
+
return await loadSummaryPromise;
|
|
6728
|
+
} finally {
|
|
6729
|
+
if (activeGitHubPullRequestReviewSummaryPromiseCache.get(cacheKey) === loadSummaryPromise) {
|
|
6730
|
+
activeGitHubPullRequestReviewSummaryPromiseCache.delete(cacheKey);
|
|
6731
|
+
}
|
|
6732
|
+
}
|
|
6733
|
+
}
|
|
6734
|
+
async function getOrLoadCachedGitHubPullRequestReviewThreadSummary(octokit, repository, pullRequestNumber, inlineSummary) {
|
|
6735
|
+
const cacheKey = buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "review-threads");
|
|
6736
|
+
const cachedSummary = getFreshCacheValue(activeGitHubPullRequestReviewThreadSummaryCache, cacheKey);
|
|
6737
|
+
if (cachedSummary) {
|
|
6738
|
+
return cachedSummary;
|
|
6739
|
+
}
|
|
6740
|
+
if (inlineSummary) {
|
|
6741
|
+
return cacheGitHubPullRequestReviewThreadSummary(repository, pullRequestNumber, inlineSummary);
|
|
6742
|
+
}
|
|
6743
|
+
const inFlightSummary = activeGitHubPullRequestReviewThreadSummaryPromiseCache.get(cacheKey);
|
|
6744
|
+
if (inFlightSummary) {
|
|
6745
|
+
return inFlightSummary;
|
|
6746
|
+
}
|
|
6747
|
+
const loadSummaryPromise = (async () => cacheGitHubPullRequestReviewThreadSummary(
|
|
6748
|
+
repository,
|
|
6749
|
+
pullRequestNumber,
|
|
6750
|
+
summarizeDetailedPullRequestReviewThreads(
|
|
6751
|
+
await listDetailedPullRequestReviewThreads(octokit, repository, pullRequestNumber)
|
|
6752
|
+
)
|
|
6753
|
+
))();
|
|
6754
|
+
activeGitHubPullRequestReviewThreadSummaryPromiseCache.set(cacheKey, loadSummaryPromise);
|
|
6755
|
+
try {
|
|
6756
|
+
return await loadSummaryPromise;
|
|
6757
|
+
} finally {
|
|
6758
|
+
if (activeGitHubPullRequestReviewThreadSummaryPromiseCache.get(cacheKey) === loadSummaryPromise) {
|
|
6759
|
+
activeGitHubPullRequestReviewThreadSummaryPromiseCache.delete(cacheKey);
|
|
6760
|
+
}
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
function getProjectPullRequestNumbersForFilter(metrics, filter) {
|
|
6764
|
+
switch (filter) {
|
|
6765
|
+
case "mergeable":
|
|
6766
|
+
return metrics.mergeablePullRequestNumbers;
|
|
6767
|
+
case "reviewable":
|
|
6768
|
+
return metrics.reviewablePullRequestNumbers;
|
|
6769
|
+
case "failing":
|
|
6770
|
+
return metrics.failingPullRequestNumbers;
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
function cacheProjectPullRequestSummaryRecords(scope, pullRequests, ttlMs = PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS) {
|
|
6774
|
+
const now = Date.now();
|
|
6775
|
+
for (const pullRequest of pullRequests) {
|
|
6776
|
+
const pullRequestNumber = getProjectPullRequestNumber(pullRequest);
|
|
6777
|
+
if (pullRequestNumber === null) {
|
|
6778
|
+
continue;
|
|
6779
|
+
}
|
|
6780
|
+
setCacheValue(
|
|
6781
|
+
activeProjectPullRequestSummaryRecordCache,
|
|
6782
|
+
buildProjectPullRequestSummaryRecordCacheKey(scope, pullRequestNumber),
|
|
6783
|
+
pullRequest,
|
|
6784
|
+
ttlMs,
|
|
6785
|
+
now
|
|
6786
|
+
);
|
|
6787
|
+
}
|
|
6788
|
+
}
|
|
6789
|
+
function selectImportedPaperclipIssueReference(current, next) {
|
|
6790
|
+
if (!current) {
|
|
6791
|
+
return next;
|
|
6792
|
+
}
|
|
6793
|
+
return compareImportedPaperclipIssueCreatedAt(
|
|
6794
|
+
{
|
|
6795
|
+
id: next.paperclipIssueId,
|
|
6796
|
+
createdAt: next.createdAt
|
|
6797
|
+
},
|
|
6798
|
+
{
|
|
6799
|
+
id: current.paperclipIssueId,
|
|
6800
|
+
createdAt: current.createdAt
|
|
6801
|
+
}
|
|
6802
|
+
) < 0 ? next : current;
|
|
6803
|
+
}
|
|
6804
|
+
async function buildProjectPullRequestIssueLookup(ctx, scope) {
|
|
6805
|
+
const cacheKey = `${buildProjectPullRequestScopeCacheKey({
|
|
6806
|
+
companyId: scope.companyId,
|
|
6807
|
+
projectId: scope.projectId,
|
|
6808
|
+
repositoryUrl: scope.repository.url
|
|
6809
|
+
})}:issue-lookup`;
|
|
6810
|
+
const cachedLookup = getFreshCacheValue(activeProjectPullRequestIssueLookupCache, cacheKey);
|
|
6811
|
+
if (cachedLookup) {
|
|
6812
|
+
return cachedLookup;
|
|
6813
|
+
}
|
|
6814
|
+
const [projectIssues, issueLinks, pullRequestLinks] = await Promise.all([
|
|
6815
|
+
listPaperclipIssuesForProject(ctx, scope.companyId, scope.projectId),
|
|
6816
|
+
listGitHubIssueLinkRecords(ctx),
|
|
6817
|
+
listGitHubPullRequestLinkRecords(ctx)
|
|
6818
|
+
]);
|
|
6819
|
+
const issuesById = new Map(projectIssues.map((issue) => [issue.id, issue]));
|
|
6820
|
+
const normalizedRepositoryUrl = scope.repository.url;
|
|
6821
|
+
const linkedIssuesByGitHubIssueUrl = /* @__PURE__ */ new Map();
|
|
6822
|
+
for (const record of issueLinks) {
|
|
6823
|
+
if (record.data.repositoryUrl !== normalizedRepositoryUrl) {
|
|
6824
|
+
continue;
|
|
6825
|
+
}
|
|
6826
|
+
if (record.data.companyId && record.data.companyId !== scope.companyId) {
|
|
6827
|
+
continue;
|
|
6828
|
+
}
|
|
6829
|
+
if (record.data.paperclipProjectId && record.data.paperclipProjectId !== scope.projectId) {
|
|
6830
|
+
continue;
|
|
6831
|
+
}
|
|
6832
|
+
const linkedIssue = issuesById.get(record.paperclipIssueId);
|
|
6833
|
+
linkedIssuesByGitHubIssueUrl.set(
|
|
6834
|
+
record.data.githubIssueUrl,
|
|
6835
|
+
selectImportedPaperclipIssueReference(
|
|
6836
|
+
linkedIssuesByGitHubIssueUrl.get(record.data.githubIssueUrl),
|
|
6837
|
+
{
|
|
6838
|
+
paperclipIssueId: record.paperclipIssueId,
|
|
6839
|
+
...linkedIssue?.identifier ? { paperclipIssueKey: linkedIssue.identifier } : {},
|
|
6840
|
+
createdAt: record.createdAt
|
|
6841
|
+
}
|
|
6842
|
+
)
|
|
6843
|
+
);
|
|
6844
|
+
}
|
|
6845
|
+
for (const issue of projectIssues) {
|
|
6846
|
+
const githubIssueUrl = extractImportedGitHubIssueUrlFromDescription(issue.description);
|
|
6847
|
+
if (!githubIssueUrl) {
|
|
6848
|
+
continue;
|
|
6849
|
+
}
|
|
6850
|
+
linkedIssuesByGitHubIssueUrl.set(
|
|
6851
|
+
githubIssueUrl,
|
|
6852
|
+
selectImportedPaperclipIssueReference(
|
|
6853
|
+
linkedIssuesByGitHubIssueUrl.get(githubIssueUrl),
|
|
6854
|
+
{
|
|
6855
|
+
paperclipIssueId: issue.id,
|
|
6856
|
+
...issue.identifier ? { paperclipIssueKey: issue.identifier } : {},
|
|
6857
|
+
createdAt: issue.createdAt
|
|
6858
|
+
}
|
|
6859
|
+
)
|
|
6860
|
+
);
|
|
6861
|
+
}
|
|
6862
|
+
const fallbackIssuesByPullRequestNumber = /* @__PURE__ */ new Map();
|
|
6863
|
+
const sortedLinks = [...pullRequestLinks].sort((left, right) => {
|
|
6864
|
+
const rightTimestamp = Date.parse(right.updatedAt ?? right.createdAt ?? "");
|
|
6865
|
+
const leftTimestamp = Date.parse(left.updatedAt ?? left.createdAt ?? "");
|
|
6866
|
+
const safeRightTimestamp = Number.isFinite(rightTimestamp) ? rightTimestamp : 0;
|
|
6867
|
+
const safeLeftTimestamp = Number.isFinite(leftTimestamp) ? leftTimestamp : 0;
|
|
6868
|
+
return safeRightTimestamp - safeLeftTimestamp;
|
|
6869
|
+
});
|
|
6870
|
+
for (const record of sortedLinks) {
|
|
6871
|
+
if (record.data.repositoryUrl !== normalizedRepositoryUrl) {
|
|
6872
|
+
continue;
|
|
6873
|
+
}
|
|
6874
|
+
if (record.data.companyId && record.data.companyId !== scope.companyId) {
|
|
6875
|
+
continue;
|
|
6876
|
+
}
|
|
6877
|
+
if (record.data.paperclipProjectId && record.data.paperclipProjectId !== scope.projectId) {
|
|
6878
|
+
continue;
|
|
6879
|
+
}
|
|
6880
|
+
if (fallbackIssuesByPullRequestNumber.has(record.data.githubPullRequestNumber)) {
|
|
6881
|
+
continue;
|
|
6882
|
+
}
|
|
6883
|
+
const linkedIssue = issuesById.get(record.paperclipIssueId);
|
|
6884
|
+
fallbackIssuesByPullRequestNumber.set(record.data.githubPullRequestNumber, {
|
|
6885
|
+
paperclipIssueId: record.paperclipIssueId,
|
|
6886
|
+
...linkedIssue?.identifier ? { paperclipIssueKey: linkedIssue.identifier } : {}
|
|
6887
|
+
});
|
|
6888
|
+
}
|
|
6889
|
+
return setCacheValue(
|
|
6890
|
+
activeProjectPullRequestIssueLookupCache,
|
|
6891
|
+
cacheKey,
|
|
6892
|
+
{
|
|
6893
|
+
linkedIssuesByGitHubIssueUrl: new Map(
|
|
6894
|
+
[...linkedIssuesByGitHubIssueUrl.entries()].map(([githubIssueUrl, value]) => [
|
|
6895
|
+
githubIssueUrl,
|
|
6896
|
+
{
|
|
6897
|
+
paperclipIssueId: value.paperclipIssueId,
|
|
6898
|
+
...value.paperclipIssueKey ? { paperclipIssueKey: value.paperclipIssueKey } : {}
|
|
6899
|
+
}
|
|
6900
|
+
])
|
|
6901
|
+
),
|
|
6902
|
+
fallbackIssuesByPullRequestNumber
|
|
6903
|
+
},
|
|
6904
|
+
PROJECT_PULL_REQUEST_ISSUE_LOOKUP_CACHE_TTL_MS
|
|
6905
|
+
);
|
|
6906
|
+
}
|
|
6907
|
+
function resolveLinkedPaperclipIssueForPullRequest(pullRequestNumber, closingIssues, issueLookup) {
|
|
6908
|
+
for (const closingIssue of closingIssues) {
|
|
6909
|
+
const linkedIssue = issueLookup.linkedIssuesByGitHubIssueUrl.get(closingIssue.url);
|
|
6910
|
+
if (linkedIssue) {
|
|
6911
|
+
return linkedIssue;
|
|
6912
|
+
}
|
|
6913
|
+
}
|
|
6914
|
+
return issueLookup.fallbackIssuesByPullRequestNumber.get(pullRequestNumber);
|
|
6915
|
+
}
|
|
6916
|
+
function getProjectPullRequestStatus(state) {
|
|
6917
|
+
switch (state) {
|
|
6918
|
+
case "MERGED":
|
|
6919
|
+
return "merged";
|
|
6920
|
+
case "CLOSED":
|
|
6921
|
+
return "closed";
|
|
6922
|
+
default:
|
|
6923
|
+
return "open";
|
|
6924
|
+
}
|
|
6925
|
+
}
|
|
6926
|
+
async function buildProjectPullRequestSummaryRecord(octokit, repository, node, issueLookup, pullRequestStatusCache) {
|
|
6927
|
+
if (!node || typeof node.number !== "number" || !node.url || !node.title?.trim()) {
|
|
6928
|
+
return null;
|
|
6929
|
+
}
|
|
6930
|
+
const inlineReviewThreadSummary = node.reviewThreads?.pageInfo?.hasNextPage ? null : summarizeProjectPullRequestReviewThreadsFromConnection(node.reviewThreads);
|
|
6931
|
+
const inlineReviewSummary = node.reviews?.pageInfo?.hasNextPage ? null : summarizeGitHubPullRequestReviewNodes(node.reviews?.nodes);
|
|
6932
|
+
const inlineCiState = tryBuildGitHubPullRequestCiStateFromBatchNode({
|
|
6933
|
+
statusCheckRollup: node.statusCheckRollup
|
|
6934
|
+
});
|
|
6935
|
+
const [reviewThreadSummary, reviewSummary, statusSnapshot, behindBy] = await Promise.all([
|
|
6936
|
+
getOrLoadCachedGitHubPullRequestReviewThreadSummary(
|
|
6937
|
+
octokit,
|
|
6938
|
+
repository,
|
|
6939
|
+
node.number,
|
|
6940
|
+
inlineReviewThreadSummary
|
|
6941
|
+
),
|
|
6942
|
+
getOrLoadCachedGitHubPullRequestReviewSummary(
|
|
6943
|
+
octokit,
|
|
6944
|
+
repository,
|
|
6945
|
+
node.number,
|
|
6946
|
+
inlineReviewSummary
|
|
6947
|
+
),
|
|
6948
|
+
getGitHubPullRequestStatusSnapshot(octokit, repository, node.number, pullRequestStatusCache, {
|
|
6949
|
+
reviewThreadSummary: inlineReviewThreadSummary,
|
|
6950
|
+
ciState: inlineCiState
|
|
6951
|
+
}),
|
|
6952
|
+
getGitHubPullRequestBehindCount(octokit, repository, {
|
|
6953
|
+
baseBranch: node.baseRefName,
|
|
6954
|
+
headBranch: node.headRefName,
|
|
6955
|
+
headRepositoryOwner: node.headRepositoryOwner?.login
|
|
6956
|
+
})
|
|
6957
|
+
]);
|
|
6958
|
+
const closingIssues = normalizeProjectPullRequestClosingIssues(repository, node.closingIssuesReferences?.nodes);
|
|
6959
|
+
const linkedIssue = resolveLinkedPaperclipIssueForPullRequest(node.number, closingIssues, issueLookup);
|
|
6960
|
+
const author = buildProjectPullRequestPerson({
|
|
6961
|
+
login: node.author?.login,
|
|
6962
|
+
url: node.author?.url,
|
|
6963
|
+
avatarUrl: node.author?.avatarUrl
|
|
6964
|
+
});
|
|
6965
|
+
const checksStatus = statusSnapshot.ciState === "green" ? "passed" : statusSnapshot.ciState === "red" ? "failed" : "pending";
|
|
6966
|
+
const githubMergeable = node.mergeable === "MERGEABLE";
|
|
6967
|
+
const reviewable = resolveProjectPullRequestReviewable({
|
|
6968
|
+
checksStatus,
|
|
6969
|
+
copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
|
|
6970
|
+
githubMergeable
|
|
6971
|
+
});
|
|
6972
|
+
const mergeable = resolveProjectPullRequestMergeable({
|
|
6973
|
+
checksStatus,
|
|
6974
|
+
reviewApprovals: reviewSummary.approvals,
|
|
6975
|
+
unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
|
|
6976
|
+
githubMergeable
|
|
6977
|
+
});
|
|
6978
|
+
const upToDateStatus = resolveProjectPullRequestUpToDateStatus({
|
|
6979
|
+
mergeStateStatus: node.mergeStateStatus,
|
|
6980
|
+
mergeable: node.mergeable,
|
|
6981
|
+
behindBy
|
|
6982
|
+
});
|
|
6983
|
+
return {
|
|
6984
|
+
id: node.id ?? `github-pull-request-${repository.owner}-${repository.repo}-${node.number}`,
|
|
6985
|
+
number: node.number,
|
|
6986
|
+
title: node.title.trim(),
|
|
6987
|
+
labels: normalizeProjectPullRequestLabels(node.labels?.nodes),
|
|
6988
|
+
author,
|
|
6989
|
+
assignees: [],
|
|
6990
|
+
checksStatus,
|
|
6991
|
+
upToDateStatus,
|
|
6992
|
+
githubMergeable,
|
|
6993
|
+
reviewable,
|
|
6994
|
+
reviewApprovals: reviewSummary.approvals,
|
|
6995
|
+
reviewChangesRequested: reviewSummary.changesRequested,
|
|
6996
|
+
reviewCommentCount: 0,
|
|
6997
|
+
unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
|
|
6998
|
+
copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
|
|
6999
|
+
commentsCount: typeof node.comments?.totalCount === "number" && node.comments.totalCount >= 0 ? Math.floor(node.comments.totalCount) : 0,
|
|
7000
|
+
createdAt: node.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
7001
|
+
updatedAt: node.updatedAt ?? node.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
7002
|
+
...linkedIssue?.paperclipIssueId ? { paperclipIssueId: linkedIssue.paperclipIssueId } : {},
|
|
7003
|
+
...linkedIssue?.paperclipIssueKey ? { paperclipIssueKey: linkedIssue.paperclipIssueKey } : {},
|
|
7004
|
+
mergeable,
|
|
7005
|
+
status: getProjectPullRequestStatus(node.state),
|
|
7006
|
+
githubUrl: node.url,
|
|
7007
|
+
checksUrl: `${node.url}/checks`,
|
|
7008
|
+
reviewsUrl: `${node.url}/files`,
|
|
7009
|
+
reviewThreadsUrl: `${node.url}/files`,
|
|
7010
|
+
commentsUrl: node.url,
|
|
7011
|
+
baseBranch: node.baseRefName ?? "",
|
|
7012
|
+
headBranch: node.headRefName ?? "",
|
|
7013
|
+
commits: typeof node.commits?.totalCount === "number" && node.commits.totalCount >= 0 ? Math.floor(node.commits.totalCount) : 0,
|
|
7014
|
+
changedFiles: typeof node.changedFiles === "number" && node.changedFiles >= 0 ? Math.floor(node.changedFiles) : 0
|
|
7015
|
+
};
|
|
7016
|
+
}
|
|
7017
|
+
async function listProjectPullRequestSummaryRecords(ctx, octokit, scope, options) {
|
|
7018
|
+
const issueLookup = await buildProjectPullRequestIssueLookup(ctx, scope);
|
|
7019
|
+
const pullRequestStatusCache = /* @__PURE__ */ new Map();
|
|
7020
|
+
const pullRequests = [];
|
|
7021
|
+
const first = Math.max(1, Math.floor(options?.first ?? PROJECT_PULL_REQUEST_PAGE_SIZE));
|
|
7022
|
+
let after = typeof options?.after === "string" && options.after.trim() ? options.after.trim() : void 0;
|
|
7023
|
+
let totalOpenPullRequests = 0;
|
|
7024
|
+
let defaultBranchName;
|
|
7025
|
+
let hasNextPage = false;
|
|
7026
|
+
let nextCursor;
|
|
7027
|
+
do {
|
|
7028
|
+
const response = await octokit.graphql(
|
|
7029
|
+
GITHUB_PROJECT_PULL_REQUESTS_QUERY,
|
|
7030
|
+
{
|
|
7031
|
+
owner: scope.repository.owner,
|
|
7032
|
+
repo: scope.repository.repo,
|
|
7033
|
+
first,
|
|
7034
|
+
after
|
|
7035
|
+
}
|
|
7036
|
+
);
|
|
7037
|
+
const connection = response.repository?.pullRequests;
|
|
7038
|
+
if (typeof connection?.totalCount === "number" && connection.totalCount >= 0) {
|
|
7039
|
+
totalOpenPullRequests = Math.floor(connection.totalCount);
|
|
7040
|
+
}
|
|
7041
|
+
defaultBranchName ??= normalizeOptionalString2(response.repository?.defaultBranchRef?.name);
|
|
7042
|
+
const pageNodes = (connection?.nodes ?? []).filter((node) => node !== null);
|
|
7043
|
+
const pageRecords = await mapWithConcurrency(
|
|
7044
|
+
pageNodes,
|
|
7045
|
+
PROJECT_PULL_REQUEST_SUMMARY_CONCURRENCY,
|
|
7046
|
+
async (node) => buildProjectPullRequestSummaryRecord(
|
|
7047
|
+
octokit,
|
|
7048
|
+
scope.repository,
|
|
7049
|
+
node,
|
|
7050
|
+
issueLookup,
|
|
7051
|
+
pullRequestStatusCache
|
|
7052
|
+
)
|
|
7053
|
+
);
|
|
7054
|
+
pullRequests.push(...pageRecords.filter((record) => Boolean(record)));
|
|
7055
|
+
nextCursor = getPageCursor(connection?.pageInfo);
|
|
7056
|
+
hasNextPage = Boolean(connection?.pageInfo?.hasNextPage && nextCursor);
|
|
7057
|
+
if (!options?.collectAll) {
|
|
7058
|
+
break;
|
|
7059
|
+
}
|
|
7060
|
+
after = nextCursor;
|
|
7061
|
+
} while (after);
|
|
7062
|
+
return {
|
|
7063
|
+
pullRequests: sortProjectPullRequestRecordsByUpdatedAt(pullRequests),
|
|
7064
|
+
totalOpenPullRequests,
|
|
7065
|
+
...defaultBranchName ? { defaultBranchName } : {},
|
|
7066
|
+
hasNextPage,
|
|
7067
|
+
...nextCursor ? { nextCursor } : {}
|
|
7068
|
+
};
|
|
7069
|
+
}
|
|
7070
|
+
async function buildProjectPullRequestMetricCounts(octokit, repository, node, pullRequestStatusCache) {
|
|
7071
|
+
if (!node || typeof node.number !== "number") {
|
|
7072
|
+
return {
|
|
7073
|
+
pullRequestNumber: null,
|
|
7074
|
+
mergeablePullRequests: 0,
|
|
7075
|
+
reviewablePullRequests: 0,
|
|
7076
|
+
failingPullRequests: 0
|
|
7077
|
+
};
|
|
7078
|
+
}
|
|
7079
|
+
const inlineReviewThreadSummary = node.reviewThreads?.pageInfo?.hasNextPage ? null : summarizeProjectPullRequestReviewThreadsFromConnection(node.reviewThreads);
|
|
7080
|
+
const inlineReviewSummary = node.reviews?.pageInfo?.hasNextPage ? null : summarizeGitHubPullRequestReviewNodes(node.reviews?.nodes);
|
|
7081
|
+
const inlineCiState = tryBuildGitHubPullRequestCiStateFromBatchNode({
|
|
7082
|
+
statusCheckRollup: node.statusCheckRollup
|
|
7083
|
+
});
|
|
7084
|
+
const [reviewThreadSummary, reviewSummary, statusSnapshot] = await Promise.all([
|
|
7085
|
+
getOrLoadCachedGitHubPullRequestReviewThreadSummary(
|
|
7086
|
+
octokit,
|
|
7087
|
+
repository,
|
|
7088
|
+
node.number,
|
|
7089
|
+
inlineReviewThreadSummary
|
|
7090
|
+
),
|
|
7091
|
+
getOrLoadCachedGitHubPullRequestReviewSummary(
|
|
7092
|
+
octokit,
|
|
7093
|
+
repository,
|
|
7094
|
+
node.number,
|
|
7095
|
+
inlineReviewSummary
|
|
7096
|
+
),
|
|
7097
|
+
getGitHubPullRequestStatusSnapshot(octokit, repository, node.number, pullRequestStatusCache, {
|
|
7098
|
+
reviewThreadSummary: inlineReviewThreadSummary,
|
|
7099
|
+
ciState: inlineCiState
|
|
7100
|
+
})
|
|
7101
|
+
]);
|
|
7102
|
+
const checksStatus = statusSnapshot.ciState === "green" ? "passed" : statusSnapshot.ciState === "red" ? "failed" : "pending";
|
|
7103
|
+
const githubMergeable = node.mergeable === "MERGEABLE";
|
|
7104
|
+
const reviewable = resolveProjectPullRequestReviewable({
|
|
7105
|
+
checksStatus,
|
|
7106
|
+
copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
|
|
7107
|
+
githubMergeable
|
|
7108
|
+
});
|
|
7109
|
+
const mergeable = resolveProjectPullRequestMergeable({
|
|
7110
|
+
checksStatus,
|
|
7111
|
+
reviewApprovals: reviewSummary.approvals,
|
|
7112
|
+
unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
|
|
7113
|
+
githubMergeable
|
|
7114
|
+
});
|
|
7115
|
+
return {
|
|
7116
|
+
pullRequestNumber: Math.floor(node.number),
|
|
7117
|
+
mergeablePullRequests: mergeable ? 1 : 0,
|
|
7118
|
+
reviewablePullRequests: reviewable ? 1 : 0,
|
|
7119
|
+
failingPullRequests: checksStatus === "failed" ? 1 : 0
|
|
7120
|
+
};
|
|
7121
|
+
}
|
|
7122
|
+
async function listProjectPullRequestCount(octokit, scope) {
|
|
7123
|
+
const response = await octokit.graphql(
|
|
7124
|
+
GITHUB_PROJECT_OPEN_PULL_REQUEST_COUNT_QUERY,
|
|
7125
|
+
{
|
|
7126
|
+
owner: scope.repository.owner,
|
|
7127
|
+
repo: scope.repository.repo
|
|
7128
|
+
}
|
|
7129
|
+
);
|
|
7130
|
+
const totalCount = response.repository?.pullRequests?.totalCount;
|
|
7131
|
+
return typeof totalCount === "number" && totalCount >= 0 ? Math.floor(totalCount) : 0;
|
|
7132
|
+
}
|
|
7133
|
+
async function listProjectPullRequestMetrics(octokit, scope) {
|
|
7134
|
+
const pullRequestStatusCache = /* @__PURE__ */ new Map();
|
|
7135
|
+
let totalOpenPullRequests = 0;
|
|
7136
|
+
let defaultBranchName;
|
|
7137
|
+
let mergeablePullRequests = 0;
|
|
7138
|
+
let reviewablePullRequests = 0;
|
|
7139
|
+
let failingPullRequests = 0;
|
|
7140
|
+
const mergeablePullRequestNumbers = [];
|
|
7141
|
+
const reviewablePullRequestNumbers = [];
|
|
7142
|
+
const failingPullRequestNumbers = [];
|
|
7143
|
+
let after;
|
|
7144
|
+
do {
|
|
7145
|
+
const response = await octokit.graphql(
|
|
7146
|
+
GITHUB_PROJECT_PULL_REQUEST_METRICS_QUERY,
|
|
7147
|
+
{
|
|
7148
|
+
owner: scope.repository.owner,
|
|
7149
|
+
repo: scope.repository.repo,
|
|
7150
|
+
first: PROJECT_PULL_REQUEST_METRICS_BATCH_SIZE,
|
|
7151
|
+
after
|
|
7152
|
+
}
|
|
7153
|
+
);
|
|
7154
|
+
const connection = response.repository?.pullRequests;
|
|
7155
|
+
if (typeof connection?.totalCount === "number" && connection.totalCount >= 0) {
|
|
7156
|
+
totalOpenPullRequests = Math.floor(connection.totalCount);
|
|
7157
|
+
}
|
|
7158
|
+
defaultBranchName ??= normalizeOptionalString2(response.repository?.defaultBranchRef?.name);
|
|
7159
|
+
const pageNodes = (connection?.nodes ?? []).filter((node) => node !== null);
|
|
7160
|
+
const pageMetrics = await mapWithConcurrency(
|
|
7161
|
+
pageNodes,
|
|
7162
|
+
PROJECT_PULL_REQUEST_SUMMARY_CONCURRENCY,
|
|
7163
|
+
async (node) => buildProjectPullRequestMetricCounts(octokit, scope.repository, node, pullRequestStatusCache)
|
|
7164
|
+
);
|
|
7165
|
+
for (const pageMetric of pageMetrics) {
|
|
7166
|
+
mergeablePullRequests += pageMetric.mergeablePullRequests;
|
|
7167
|
+
reviewablePullRequests += pageMetric.reviewablePullRequests;
|
|
7168
|
+
failingPullRequests += pageMetric.failingPullRequests;
|
|
7169
|
+
if (pageMetric.pullRequestNumber !== null && pageMetric.mergeablePullRequests > 0) {
|
|
7170
|
+
mergeablePullRequestNumbers.push(pageMetric.pullRequestNumber);
|
|
7171
|
+
}
|
|
7172
|
+
if (pageMetric.pullRequestNumber !== null && pageMetric.reviewablePullRequests > 0) {
|
|
7173
|
+
reviewablePullRequestNumbers.push(pageMetric.pullRequestNumber);
|
|
7174
|
+
}
|
|
7175
|
+
if (pageMetric.pullRequestNumber !== null && pageMetric.failingPullRequests > 0) {
|
|
7176
|
+
failingPullRequestNumbers.push(pageMetric.pullRequestNumber);
|
|
7177
|
+
}
|
|
7178
|
+
}
|
|
7179
|
+
after = getPageCursor(connection?.pageInfo);
|
|
7180
|
+
} while (after);
|
|
7181
|
+
return {
|
|
7182
|
+
totalOpenPullRequests,
|
|
7183
|
+
...defaultBranchName ? { defaultBranchName } : {},
|
|
7184
|
+
mergeablePullRequests,
|
|
7185
|
+
reviewablePullRequests,
|
|
7186
|
+
failingPullRequests,
|
|
7187
|
+
mergeablePullRequestNumbers,
|
|
7188
|
+
reviewablePullRequestNumbers,
|
|
7189
|
+
failingPullRequestNumbers
|
|
7190
|
+
};
|
|
7191
|
+
}
|
|
7192
|
+
function buildProjectPullRequestMetricsCacheKey(scope) {
|
|
7193
|
+
return buildRepositoryPullRequestCollectionCacheKey(scope.repository, "metrics");
|
|
7194
|
+
}
|
|
7195
|
+
function buildProjectPullRequestSummaryCacheKey(scope) {
|
|
7196
|
+
return `${buildProjectPullRequestScopeCacheKey({
|
|
7197
|
+
companyId: scope.companyId,
|
|
7198
|
+
projectId: scope.projectId,
|
|
7199
|
+
repositoryUrl: scope.repository.url
|
|
7200
|
+
})}:summary`;
|
|
7201
|
+
}
|
|
7202
|
+
function buildProjectPullRequestCountCacheKey(scope) {
|
|
7203
|
+
return buildRepositoryPullRequestCollectionCacheKey(scope.repository, "count");
|
|
7204
|
+
}
|
|
7205
|
+
function getCachedProjectPullRequestSummarySeed(scope) {
|
|
7206
|
+
const cachedPage = getFreshCacheValue(
|
|
7207
|
+
activeProjectPullRequestPageCache,
|
|
7208
|
+
buildProjectPullRequestPageCacheKey(scope, "all", 0)
|
|
7209
|
+
);
|
|
7210
|
+
if (!cachedPage || cachedPage.status !== "ready") {
|
|
7211
|
+
return null;
|
|
7212
|
+
}
|
|
7213
|
+
const totalOpenPullRequests = typeof cachedPage.totalOpenPullRequests === "number" && cachedPage.totalOpenPullRequests >= 0 ? Math.floor(cachedPage.totalOpenPullRequests) : null;
|
|
7214
|
+
const pullRequests = Array.isArray(cachedPage.pullRequests) ? cachedPage.pullRequests.filter(
|
|
7215
|
+
(record) => Boolean(record) && typeof record === "object" && !Array.isArray(record)
|
|
7216
|
+
) : null;
|
|
7217
|
+
if (totalOpenPullRequests === null || !pullRequests) {
|
|
7218
|
+
return null;
|
|
7219
|
+
}
|
|
7220
|
+
const hasNextPage = cachedPage.hasNextPage === true;
|
|
7221
|
+
const nextCursor = typeof cachedPage.nextCursor === "string" && cachedPage.nextCursor.trim() ? cachedPage.nextCursor.trim() : void 0;
|
|
7222
|
+
if (hasNextPage && !nextCursor) {
|
|
7223
|
+
return null;
|
|
7224
|
+
}
|
|
7225
|
+
const defaultBranchName = normalizeOptionalString2(cachedPage.defaultBranchName);
|
|
7226
|
+
return {
|
|
7227
|
+
totalOpenPullRequests,
|
|
7228
|
+
...defaultBranchName ? { defaultBranchName } : {},
|
|
7229
|
+
pullRequests: sortProjectPullRequestRecordsByUpdatedAt(pullRequests),
|
|
7230
|
+
hasNextPage,
|
|
7231
|
+
...nextCursor ? { nextCursor } : {}
|
|
7232
|
+
};
|
|
7233
|
+
}
|
|
7234
|
+
function cacheProjectPullRequestSummary(scope, summary) {
|
|
7235
|
+
const pullRequests = sortProjectPullRequestRecordsByUpdatedAt(summary.pullRequests);
|
|
7236
|
+
const metrics = buildProjectPullRequestMetrics(
|
|
7237
|
+
pullRequests,
|
|
7238
|
+
summary.totalOpenPullRequests,
|
|
7239
|
+
summary.defaultBranchName
|
|
7240
|
+
);
|
|
7241
|
+
cacheProjectPullRequestSummaryRecords(scope, pullRequests);
|
|
7242
|
+
cacheProjectPullRequestMetricsEntry(scope, metrics);
|
|
7243
|
+
return setCacheValue(
|
|
7244
|
+
activeProjectPullRequestSummaryCache,
|
|
7245
|
+
buildProjectPullRequestSummaryCacheKey(scope),
|
|
7246
|
+
{
|
|
7247
|
+
totalOpenPullRequests: summary.totalOpenPullRequests,
|
|
7248
|
+
...summary.defaultBranchName ? { defaultBranchName: summary.defaultBranchName } : {},
|
|
7249
|
+
pullRequests,
|
|
7250
|
+
metrics
|
|
7251
|
+
},
|
|
7252
|
+
PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS
|
|
7253
|
+
);
|
|
7254
|
+
}
|
|
7255
|
+
function cacheProjectPullRequestCount(scope, totalOpenPullRequests, ttlMs = PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS) {
|
|
7256
|
+
return setCacheValue(
|
|
7257
|
+
activeProjectPullRequestCountCache,
|
|
7258
|
+
buildProjectPullRequestCountCacheKey(scope),
|
|
7259
|
+
Math.max(0, Math.floor(totalOpenPullRequests)),
|
|
7260
|
+
ttlMs
|
|
7261
|
+
);
|
|
7262
|
+
}
|
|
7263
|
+
function cacheProjectPullRequestMetricsEntry(scope, metrics, ttlMs = PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS) {
|
|
7264
|
+
cacheProjectPullRequestCount(scope, metrics.totalOpenPullRequests, ttlMs);
|
|
7265
|
+
return setCacheValue(
|
|
7266
|
+
activeProjectPullRequestMetricsCache,
|
|
7267
|
+
buildProjectPullRequestMetricsCacheKey(scope),
|
|
7268
|
+
metrics,
|
|
7269
|
+
ttlMs
|
|
7270
|
+
);
|
|
7271
|
+
}
|
|
7272
|
+
async function getOrLoadCachedProjectPullRequestMetricsEntry(ctx, scope, octokit) {
|
|
7273
|
+
const summaryCacheKey = buildProjectPullRequestSummaryCacheKey(scope);
|
|
7274
|
+
const cachedSummary = getFreshCacheValue(activeProjectPullRequestSummaryCache, summaryCacheKey);
|
|
7275
|
+
if (cachedSummary) {
|
|
7276
|
+
return cachedSummary.metrics;
|
|
7277
|
+
}
|
|
7278
|
+
const metricsCacheKey = buildProjectPullRequestMetricsCacheKey(scope);
|
|
7279
|
+
const cachedMetrics = getFreshCacheValue(activeProjectPullRequestMetricsCache, metricsCacheKey);
|
|
7280
|
+
if (cachedMetrics) {
|
|
7281
|
+
return cachedMetrics;
|
|
7282
|
+
}
|
|
7283
|
+
const inFlightMetrics = activeProjectPullRequestMetricsPromiseCache.get(metricsCacheKey);
|
|
7284
|
+
if (inFlightMetrics) {
|
|
7285
|
+
return inFlightMetrics;
|
|
7286
|
+
}
|
|
7287
|
+
const loadMetricsPromise = (async () => {
|
|
7288
|
+
const resolvedOctokit = octokit ?? await createGitHubToolOctokit(ctx);
|
|
7289
|
+
const metrics = await listProjectPullRequestMetrics(resolvedOctokit, scope);
|
|
7290
|
+
return cacheProjectPullRequestMetricsEntry(scope, metrics);
|
|
7291
|
+
})();
|
|
7292
|
+
activeProjectPullRequestMetricsPromiseCache.set(metricsCacheKey, loadMetricsPromise);
|
|
7293
|
+
try {
|
|
7294
|
+
return await loadMetricsPromise;
|
|
7295
|
+
} finally {
|
|
7296
|
+
if (activeProjectPullRequestMetricsPromiseCache.get(metricsCacheKey) === loadMetricsPromise) {
|
|
7297
|
+
activeProjectPullRequestMetricsPromiseCache.delete(metricsCacheKey);
|
|
7298
|
+
}
|
|
7299
|
+
}
|
|
7300
|
+
}
|
|
7301
|
+
async function getOrLoadCachedProjectPullRequestCount(ctx, scope, octokit) {
|
|
7302
|
+
const summaryCacheKey = buildProjectPullRequestSummaryCacheKey(scope);
|
|
7303
|
+
const cachedSummary = getFreshCacheValue(activeProjectPullRequestSummaryCache, summaryCacheKey);
|
|
7304
|
+
if (cachedSummary) {
|
|
7305
|
+
return cachedSummary.totalOpenPullRequests;
|
|
7306
|
+
}
|
|
7307
|
+
const metricsCacheKey = buildProjectPullRequestMetricsCacheKey(scope);
|
|
7308
|
+
const cachedMetrics = getFreshCacheValue(activeProjectPullRequestMetricsCache, metricsCacheKey);
|
|
7309
|
+
if (cachedMetrics) {
|
|
7310
|
+
return cachedMetrics.totalOpenPullRequests;
|
|
7311
|
+
}
|
|
7312
|
+
const countCacheKey = buildProjectPullRequestCountCacheKey(scope);
|
|
7313
|
+
const cachedCount = getFreshCacheValue(activeProjectPullRequestCountCache, countCacheKey);
|
|
7314
|
+
if (cachedCount !== null) {
|
|
7315
|
+
return cachedCount;
|
|
7316
|
+
}
|
|
7317
|
+
const cachedSummarySeed = getCachedProjectPullRequestSummarySeed(scope);
|
|
7318
|
+
if (cachedSummarySeed) {
|
|
7319
|
+
return cacheProjectPullRequestCount(scope, cachedSummarySeed.totalOpenPullRequests);
|
|
7320
|
+
}
|
|
7321
|
+
const inFlightCount = activeProjectPullRequestCountPromiseCache.get(countCacheKey);
|
|
7322
|
+
if (inFlightCount) {
|
|
7323
|
+
return inFlightCount;
|
|
7324
|
+
}
|
|
7325
|
+
const loadCountPromise = (async () => {
|
|
7326
|
+
const resolvedOctokit = octokit ?? await createGitHubToolOctokit(ctx);
|
|
7327
|
+
const totalOpenPullRequests = await listProjectPullRequestCount(resolvedOctokit, scope);
|
|
7328
|
+
return setCacheValue(
|
|
7329
|
+
activeProjectPullRequestCountCache,
|
|
7330
|
+
countCacheKey,
|
|
7331
|
+
totalOpenPullRequests,
|
|
7332
|
+
PROJECT_PULL_REQUEST_SUMMARY_CACHE_TTL_MS
|
|
7333
|
+
);
|
|
7334
|
+
})();
|
|
7335
|
+
activeProjectPullRequestCountPromiseCache.set(countCacheKey, loadCountPromise);
|
|
7336
|
+
try {
|
|
7337
|
+
return await loadCountPromise;
|
|
7338
|
+
} finally {
|
|
7339
|
+
if (activeProjectPullRequestCountPromiseCache.get(countCacheKey) === loadCountPromise) {
|
|
7340
|
+
activeProjectPullRequestCountPromiseCache.delete(countCacheKey);
|
|
7341
|
+
}
|
|
7342
|
+
}
|
|
7343
|
+
}
|
|
7344
|
+
async function getOrLoadCachedProjectPullRequestMetrics(ctx, scope, octokit) {
|
|
7345
|
+
return getPublicProjectPullRequestMetrics(
|
|
7346
|
+
await getOrLoadCachedProjectPullRequestMetricsEntry(ctx, scope, octokit)
|
|
7347
|
+
);
|
|
7348
|
+
}
|
|
7349
|
+
async function listProjectPullRequestSummaryRecordsByNumbers(ctx, octokit, scope, pullRequestNumbers) {
|
|
7350
|
+
const normalizedPullRequestNumbers = [
|
|
7351
|
+
...new Set(
|
|
7352
|
+
pullRequestNumbers.map((value) => Math.floor(value)).filter((value) => Number.isFinite(value) && value > 0)
|
|
7353
|
+
)
|
|
7354
|
+
];
|
|
7355
|
+
if (normalizedPullRequestNumbers.length === 0) {
|
|
7356
|
+
return [];
|
|
7357
|
+
}
|
|
7358
|
+
const query = buildGitHubProjectPullRequestsByNumberQuery(normalizedPullRequestNumbers);
|
|
7359
|
+
const response = await octokit.graphql(
|
|
7360
|
+
query,
|
|
7361
|
+
{
|
|
7362
|
+
owner: scope.repository.owner,
|
|
7363
|
+
repo: scope.repository.repo
|
|
7364
|
+
}
|
|
7365
|
+
);
|
|
7366
|
+
const repository = response.repository && typeof response.repository === "object" ? response.repository : {};
|
|
7367
|
+
const issueLookup = await buildProjectPullRequestIssueLookup(ctx, scope);
|
|
7368
|
+
const pullRequestStatusCache = /* @__PURE__ */ new Map();
|
|
7369
|
+
const recordsByNumber = /* @__PURE__ */ new Map();
|
|
7370
|
+
const builtRecords = await mapWithConcurrency(
|
|
7371
|
+
normalizedPullRequestNumbers,
|
|
7372
|
+
PROJECT_PULL_REQUEST_SUMMARY_CONCURRENCY,
|
|
7373
|
+
async (pullRequestNumber) => {
|
|
7374
|
+
const node = repository[buildGitHubProjectPullRequestByNumberAlias(pullRequestNumber)];
|
|
7375
|
+
if (!node) {
|
|
7376
|
+
return null;
|
|
7377
|
+
}
|
|
7378
|
+
const record = await buildProjectPullRequestSummaryRecord(
|
|
7379
|
+
octokit,
|
|
7380
|
+
scope.repository,
|
|
7381
|
+
node,
|
|
7382
|
+
issueLookup,
|
|
7383
|
+
pullRequestStatusCache
|
|
7384
|
+
);
|
|
7385
|
+
if (!record) {
|
|
7386
|
+
return null;
|
|
7387
|
+
}
|
|
7388
|
+
return {
|
|
7389
|
+
pullRequestNumber,
|
|
7390
|
+
record
|
|
7391
|
+
};
|
|
7392
|
+
}
|
|
7393
|
+
);
|
|
7394
|
+
for (const builtRecord of builtRecords) {
|
|
7395
|
+
if (!builtRecord) {
|
|
7396
|
+
continue;
|
|
7397
|
+
}
|
|
7398
|
+
recordsByNumber.set(builtRecord.pullRequestNumber, builtRecord.record);
|
|
7399
|
+
}
|
|
7400
|
+
return normalizedPullRequestNumbers.map((pullRequestNumber) => recordsByNumber.get(pullRequestNumber)).filter((record) => Boolean(record));
|
|
7401
|
+
}
|
|
7402
|
+
async function getOrLoadProjectPullRequestSummaryRecordsForNumbers(ctx, scope, pullRequestNumbers, octokit) {
|
|
7403
|
+
const normalizedPullRequestNumbers = [
|
|
7404
|
+
...new Set(
|
|
7405
|
+
pullRequestNumbers.map((value) => Math.floor(value)).filter((value) => Number.isFinite(value) && value > 0)
|
|
7406
|
+
)
|
|
7407
|
+
];
|
|
7408
|
+
if (normalizedPullRequestNumbers.length === 0) {
|
|
7409
|
+
return [];
|
|
7410
|
+
}
|
|
7411
|
+
const cachedSummary = getFreshCacheValue(
|
|
7412
|
+
activeProjectPullRequestSummaryCache,
|
|
7413
|
+
buildProjectPullRequestSummaryCacheKey(scope)
|
|
7414
|
+
);
|
|
7415
|
+
const summaryRecordsByNumber = cachedSummary ? new Map(
|
|
7416
|
+
cachedSummary.pullRequests.map((pullRequest) => {
|
|
7417
|
+
const pullRequestNumber = getProjectPullRequestNumber(pullRequest);
|
|
7418
|
+
return pullRequestNumber === null ? null : [pullRequestNumber, pullRequest];
|
|
7419
|
+
}).filter((entry) => Boolean(entry))
|
|
7420
|
+
) : null;
|
|
7421
|
+
const recordsByNumber = /* @__PURE__ */ new Map();
|
|
7422
|
+
for (const pullRequestNumber of normalizedPullRequestNumbers) {
|
|
7423
|
+
const cachedSummaryRecord = summaryRecordsByNumber?.get(pullRequestNumber);
|
|
7424
|
+
if (cachedSummaryRecord) {
|
|
7425
|
+
recordsByNumber.set(pullRequestNumber, cachedSummaryRecord);
|
|
7426
|
+
continue;
|
|
7427
|
+
}
|
|
7428
|
+
const cachedRecord = getFreshCacheValue(
|
|
7429
|
+
activeProjectPullRequestSummaryRecordCache,
|
|
7430
|
+
buildProjectPullRequestSummaryRecordCacheKey(scope, pullRequestNumber)
|
|
7431
|
+
);
|
|
7432
|
+
if (cachedRecord) {
|
|
7433
|
+
recordsByNumber.set(pullRequestNumber, cachedRecord);
|
|
7434
|
+
}
|
|
7435
|
+
}
|
|
7436
|
+
const missingPullRequestNumbers = normalizedPullRequestNumbers.filter((pullRequestNumber) => !recordsByNumber.has(pullRequestNumber));
|
|
7437
|
+
if (missingPullRequestNumbers.length > 0) {
|
|
7438
|
+
const resolvedOctokit = octokit ?? await createGitHubToolOctokit(ctx);
|
|
7439
|
+
const loadedRecords = await listProjectPullRequestSummaryRecordsByNumbers(
|
|
7440
|
+
ctx,
|
|
7441
|
+
resolvedOctokit,
|
|
7442
|
+
scope,
|
|
7443
|
+
missingPullRequestNumbers
|
|
7444
|
+
);
|
|
7445
|
+
cacheProjectPullRequestSummaryRecords(scope, loadedRecords);
|
|
7446
|
+
for (const loadedRecord of loadedRecords) {
|
|
7447
|
+
const pullRequestNumber = getProjectPullRequestNumber(loadedRecord);
|
|
7448
|
+
if (pullRequestNumber === null) {
|
|
7449
|
+
continue;
|
|
7450
|
+
}
|
|
7451
|
+
recordsByNumber.set(pullRequestNumber, loadedRecord);
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7454
|
+
return normalizedPullRequestNumbers.map((pullRequestNumber) => recordsByNumber.get(pullRequestNumber)).filter((record) => Boolean(record));
|
|
7455
|
+
}
|
|
7456
|
+
function buildPaperclipIssueDescriptionFromPullRequest(params) {
|
|
7457
|
+
const importLine = `Imported from GitHub pull request [#${params.pullRequestNumber}](${params.pullRequestUrl}) in ${formatRepositoryLabel(params.repository)}.`;
|
|
7458
|
+
const body = typeof params.body === "string" ? params.body.trim() : "";
|
|
7459
|
+
return body ? `${importLine}
|
|
7460
|
+
|
|
7461
|
+
${body}` : importLine;
|
|
7462
|
+
}
|
|
7463
|
+
async function requireProjectPullRequestScope(ctx, input, resolvedProjectMappings) {
|
|
7464
|
+
const companyId = normalizeCompanyId(input.companyId);
|
|
7465
|
+
const projectId = typeof input.projectId === "string" && input.projectId.trim() ? input.projectId.trim() : void 0;
|
|
7466
|
+
if (!companyId || !projectId) {
|
|
7467
|
+
throw new Error("A company id and project id are required to load project pull requests.");
|
|
7468
|
+
}
|
|
7469
|
+
const mappings = resolvedProjectMappings ?? await resolveProjectScopedMappings(
|
|
7470
|
+
ctx,
|
|
7471
|
+
normalizeSettings(await ctx.state.get(SETTINGS_SCOPE)).mappings,
|
|
7472
|
+
{
|
|
7473
|
+
companyId,
|
|
7474
|
+
projectId
|
|
7475
|
+
}
|
|
7476
|
+
);
|
|
7477
|
+
if (mappings.length === 0) {
|
|
7478
|
+
throw new Error("No saved GitHub repository mapping matches this Paperclip project.");
|
|
7479
|
+
}
|
|
7480
|
+
const requestedRepositoryUrl = typeof input.repositoryUrl === "string" && input.repositoryUrl.trim() ? getNormalizedMappingRepositoryUrl({
|
|
7481
|
+
repositoryUrl: input.repositoryUrl
|
|
7482
|
+
}) : void 0;
|
|
7483
|
+
const sortedMappings = [...mappings].sort(
|
|
7484
|
+
(left, right) => getNormalizedMappingRepositoryUrl(left).localeCompare(getNormalizedMappingRepositoryUrl(right))
|
|
7485
|
+
);
|
|
7486
|
+
const mapping = requestedRepositoryUrl ? sortedMappings.find((entry) => getNormalizedMappingRepositoryUrl(entry) === requestedRepositoryUrl) : sortedMappings[0];
|
|
7487
|
+
if (!mapping) {
|
|
7488
|
+
throw new Error("This Paperclip project is not mapped to the requested GitHub repository.");
|
|
7489
|
+
}
|
|
7490
|
+
return {
|
|
7491
|
+
companyId,
|
|
7492
|
+
projectId,
|
|
7493
|
+
projectLabel: mapping.paperclipProjectName.trim() || "Project",
|
|
7494
|
+
mapping,
|
|
7495
|
+
repository: requireRepositoryReference(mapping.repositoryUrl),
|
|
7496
|
+
mappingCount: sortedMappings.length
|
|
7497
|
+
};
|
|
7498
|
+
}
|
|
7499
|
+
async function buildProjectPullRequestsPageData(ctx, input) {
|
|
7500
|
+
const filter = normalizeProjectPullRequestFilter(input.filter);
|
|
7501
|
+
const pageIndex = normalizeProjectPullRequestPageIndex(input.pageIndex);
|
|
7502
|
+
const cursor = typeof input.cursor === "string" && input.cursor.trim() ? input.cursor.trim() : void 0;
|
|
7503
|
+
const companyId = normalizeCompanyId(input.companyId);
|
|
7504
|
+
const projectId = typeof input.projectId === "string" && input.projectId.trim() ? input.projectId.trim() : null;
|
|
7505
|
+
if (!companyId || !projectId) {
|
|
7506
|
+
return {
|
|
7507
|
+
status: "missing_project",
|
|
7508
|
+
projectId,
|
|
7509
|
+
projectLabel: "Project",
|
|
7510
|
+
repositoryLabel: "",
|
|
7511
|
+
repositoryUrl: "",
|
|
7512
|
+
repositoryDescription: "",
|
|
7513
|
+
filter,
|
|
7514
|
+
pageIndex: 0,
|
|
7515
|
+
pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
|
|
7516
|
+
hasNextPage: false,
|
|
7517
|
+
hasPreviousPage: false,
|
|
7518
|
+
totalFilteredPullRequests: 0,
|
|
7519
|
+
pullRequests: [],
|
|
7520
|
+
message: "Open this page from a mapped Paperclip project."
|
|
7521
|
+
};
|
|
7522
|
+
}
|
|
7523
|
+
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
7524
|
+
const projectMappings = await resolveProjectScopedMappings(ctx, settings.mappings, {
|
|
7525
|
+
companyId,
|
|
7526
|
+
projectId
|
|
7527
|
+
});
|
|
7528
|
+
if (projectMappings.length === 0) {
|
|
7529
|
+
return {
|
|
7530
|
+
status: "unmapped",
|
|
7531
|
+
projectId,
|
|
7532
|
+
projectLabel: "Project",
|
|
7533
|
+
repositoryLabel: "",
|
|
7534
|
+
repositoryUrl: "",
|
|
7535
|
+
repositoryDescription: "",
|
|
7536
|
+
filter,
|
|
7537
|
+
pageIndex: 0,
|
|
7538
|
+
pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
|
|
7539
|
+
hasNextPage: false,
|
|
7540
|
+
hasPreviousPage: false,
|
|
7541
|
+
totalFilteredPullRequests: 0,
|
|
7542
|
+
pullRequests: [],
|
|
7543
|
+
message: "No GitHub repository is mapped to this project yet."
|
|
7544
|
+
};
|
|
7545
|
+
}
|
|
7546
|
+
const scope = await requireProjectPullRequestScope(ctx, input, projectMappings);
|
|
7547
|
+
const config = await getResolvedConfig(ctx);
|
|
7548
|
+
if (!hasConfiguredGithubToken(settings, config)) {
|
|
7549
|
+
return {
|
|
7550
|
+
status: "missing_token",
|
|
7551
|
+
projectId,
|
|
7552
|
+
projectLabel: scope.projectLabel,
|
|
7553
|
+
repositoryLabel: formatRepositoryLabel(scope.repository),
|
|
7554
|
+
repositoryUrl: scope.repository.url,
|
|
7555
|
+
repositoryDescription: "",
|
|
7556
|
+
filter,
|
|
7557
|
+
pageIndex: 0,
|
|
7558
|
+
pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
|
|
7559
|
+
hasNextPage: false,
|
|
7560
|
+
hasPreviousPage: false,
|
|
7561
|
+
totalFilteredPullRequests: 0,
|
|
7562
|
+
pullRequests: [],
|
|
7563
|
+
message: "Configure a GitHub token before opening pull requests."
|
|
7564
|
+
};
|
|
7565
|
+
}
|
|
7566
|
+
try {
|
|
7567
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
7568
|
+
const pageCacheKey = buildProjectPullRequestPageCacheKey(scope, filter, pageIndex, cursor);
|
|
7569
|
+
const cachedPage = getFreshCacheValue(activeProjectPullRequestPageCache, pageCacheKey);
|
|
7570
|
+
if (cachedPage) {
|
|
7571
|
+
return cachedPage;
|
|
7572
|
+
}
|
|
7573
|
+
if (filter !== "all") {
|
|
7574
|
+
const metrics = await getOrLoadCachedProjectPullRequestMetricsEntry(ctx, scope, octokit);
|
|
7575
|
+
const filteredPullRequestNumbers = getProjectPullRequestNumbersForFilter(metrics, filter);
|
|
7576
|
+
const page = sliceProjectPullRequestNumbers(filteredPullRequestNumbers, pageIndex, PROJECT_PULL_REQUEST_PAGE_SIZE);
|
|
7577
|
+
const pullRequests = sortProjectPullRequestRecordsByUpdatedAt(
|
|
7578
|
+
await getOrLoadProjectPullRequestSummaryRecordsForNumbers(
|
|
7579
|
+
ctx,
|
|
7580
|
+
scope,
|
|
7581
|
+
page.pullRequestNumbers,
|
|
7582
|
+
octokit
|
|
7583
|
+
)
|
|
7584
|
+
);
|
|
7585
|
+
const tokenPermissionAudit2 = await getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, scope.repository, {
|
|
7586
|
+
samplePullRequestNumber: pullRequests[0] ? getProjectPullRequestNumber(pullRequests[0]) ?? void 0 : void 0
|
|
7587
|
+
});
|
|
7588
|
+
cacheProjectPullRequestCount(scope, metrics.totalOpenPullRequests);
|
|
7589
|
+
return setCacheValue(
|
|
7590
|
+
activeProjectPullRequestPageCache,
|
|
7591
|
+
pageCacheKey,
|
|
7592
|
+
{
|
|
7593
|
+
status: "ready",
|
|
7594
|
+
projectId,
|
|
7595
|
+
projectLabel: scope.projectLabel,
|
|
7596
|
+
repositoryLabel: formatRepositoryLabel(scope.repository),
|
|
7597
|
+
repositoryUrl: scope.repository.url,
|
|
7598
|
+
repositoryDescription: "",
|
|
7599
|
+
...metrics.defaultBranchName ? { defaultBranchName: metrics.defaultBranchName } : {},
|
|
7600
|
+
filter,
|
|
7601
|
+
pageIndex: page.pageIndex,
|
|
7602
|
+
pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
|
|
7603
|
+
hasNextPage: page.hasNextPage,
|
|
7604
|
+
hasPreviousPage: page.hasPreviousPage,
|
|
7605
|
+
totalFilteredPullRequests: filteredPullRequestNumbers.length,
|
|
7606
|
+
totalOpenPullRequests: metrics.totalOpenPullRequests,
|
|
7607
|
+
pullRequests,
|
|
7608
|
+
tokenPermissionAudit: tokenPermissionAudit2
|
|
7609
|
+
},
|
|
7610
|
+
PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS
|
|
7611
|
+
);
|
|
7612
|
+
}
|
|
7613
|
+
const cachedFullSummary = getFreshCacheValue(
|
|
7614
|
+
activeProjectPullRequestSummaryCache,
|
|
7615
|
+
buildProjectPullRequestSummaryCacheKey(scope)
|
|
7616
|
+
);
|
|
7617
|
+
if (cachedFullSummary) {
|
|
7618
|
+
cacheProjectPullRequestCount(scope, cachedFullSummary.totalOpenPullRequests);
|
|
7619
|
+
const page = sliceProjectPullRequestRecords(
|
|
7620
|
+
cachedFullSummary.pullRequests,
|
|
7621
|
+
pageIndex,
|
|
7622
|
+
PROJECT_PULL_REQUEST_PAGE_SIZE
|
|
7623
|
+
);
|
|
7624
|
+
const tokenPermissionAudit2 = await getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, scope.repository, {
|
|
7625
|
+
samplePullRequestNumber: page.pullRequests[0] ? getProjectPullRequestNumber(page.pullRequests[0]) ?? void 0 : void 0
|
|
7626
|
+
});
|
|
7627
|
+
return setCacheValue(
|
|
7628
|
+
activeProjectPullRequestPageCache,
|
|
7629
|
+
pageCacheKey,
|
|
7630
|
+
{
|
|
7631
|
+
status: "ready",
|
|
7632
|
+
projectId,
|
|
7633
|
+
projectLabel: scope.projectLabel,
|
|
7634
|
+
repositoryLabel: formatRepositoryLabel(scope.repository),
|
|
7635
|
+
repositoryUrl: scope.repository.url,
|
|
7636
|
+
repositoryDescription: "",
|
|
7637
|
+
...cachedFullSummary.defaultBranchName ? { defaultBranchName: cachedFullSummary.defaultBranchName } : {},
|
|
7638
|
+
filter,
|
|
7639
|
+
pageIndex: page.pageIndex,
|
|
7640
|
+
pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
|
|
7641
|
+
hasNextPage: page.hasNextPage,
|
|
7642
|
+
hasPreviousPage: page.hasPreviousPage,
|
|
7643
|
+
totalFilteredPullRequests: cachedFullSummary.totalOpenPullRequests,
|
|
7644
|
+
totalOpenPullRequests: cachedFullSummary.totalOpenPullRequests,
|
|
7645
|
+
pullRequests: page.pullRequests,
|
|
7646
|
+
tokenPermissionAudit: tokenPermissionAudit2
|
|
7647
|
+
},
|
|
7648
|
+
PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS
|
|
7649
|
+
);
|
|
7650
|
+
}
|
|
7651
|
+
const summary = await listProjectPullRequestSummaryRecords(ctx, octokit, scope, {
|
|
7652
|
+
after: cursor,
|
|
7653
|
+
first: PROJECT_PULL_REQUEST_PAGE_SIZE
|
|
7654
|
+
});
|
|
7655
|
+
cacheProjectPullRequestCount(scope, summary.totalOpenPullRequests);
|
|
7656
|
+
cacheProjectPullRequestSummaryRecords(scope, summary.pullRequests, PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS);
|
|
7657
|
+
if (pageIndex === 0 && !summary.hasNextPage) {
|
|
7658
|
+
cacheProjectPullRequestSummary(scope, {
|
|
7659
|
+
totalOpenPullRequests: summary.totalOpenPullRequests,
|
|
7660
|
+
...summary.defaultBranchName ? { defaultBranchName: summary.defaultBranchName } : {},
|
|
7661
|
+
pullRequests: summary.pullRequests
|
|
7662
|
+
});
|
|
7663
|
+
}
|
|
7664
|
+
const tokenPermissionAudit = await getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, scope.repository, {
|
|
7665
|
+
samplePullRequestNumber: summary.pullRequests[0] ? getProjectPullRequestNumber(summary.pullRequests[0]) ?? void 0 : void 0
|
|
7666
|
+
});
|
|
7667
|
+
return setCacheValue(
|
|
7668
|
+
activeProjectPullRequestPageCache,
|
|
7669
|
+
pageCacheKey,
|
|
7670
|
+
{
|
|
7671
|
+
status: "ready",
|
|
7672
|
+
projectId,
|
|
7673
|
+
projectLabel: scope.projectLabel,
|
|
7674
|
+
repositoryLabel: formatRepositoryLabel(scope.repository),
|
|
7675
|
+
repositoryUrl: scope.repository.url,
|
|
7676
|
+
repositoryDescription: "",
|
|
7677
|
+
...summary.defaultBranchName ? { defaultBranchName: summary.defaultBranchName } : {},
|
|
7678
|
+
filter,
|
|
7679
|
+
pageIndex,
|
|
7680
|
+
pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
|
|
7681
|
+
hasNextPage: summary.hasNextPage,
|
|
7682
|
+
hasPreviousPage: pageIndex > 0,
|
|
7683
|
+
totalFilteredPullRequests: summary.totalOpenPullRequests,
|
|
7684
|
+
totalOpenPullRequests: summary.totalOpenPullRequests,
|
|
7685
|
+
...summary.nextCursor ? { nextCursor: summary.nextCursor } : {},
|
|
7686
|
+
pullRequests: summary.pullRequests,
|
|
7687
|
+
tokenPermissionAudit
|
|
7688
|
+
},
|
|
7689
|
+
PROJECT_PULL_REQUEST_PAGE_CACHE_TTL_MS
|
|
7690
|
+
);
|
|
7691
|
+
} catch (error) {
|
|
7692
|
+
return {
|
|
7693
|
+
status: "error",
|
|
7694
|
+
projectId,
|
|
7695
|
+
projectLabel: scope.projectLabel,
|
|
7696
|
+
repositoryLabel: formatRepositoryLabel(scope.repository),
|
|
7697
|
+
repositoryUrl: scope.repository.url,
|
|
7698
|
+
repositoryDescription: "",
|
|
7699
|
+
filter,
|
|
7700
|
+
pageIndex,
|
|
7701
|
+
pageSize: PROJECT_PULL_REQUEST_PAGE_SIZE,
|
|
7702
|
+
hasNextPage: false,
|
|
7703
|
+
hasPreviousPage: pageIndex > 0,
|
|
7704
|
+
totalFilteredPullRequests: 0,
|
|
7705
|
+
pullRequests: [],
|
|
7706
|
+
message: getErrorMessage(error)
|
|
7707
|
+
};
|
|
7708
|
+
}
|
|
7709
|
+
}
|
|
7710
|
+
async function buildProjectPullRequestMetricsData(ctx, input) {
|
|
7711
|
+
const companyId = normalizeCompanyId(input.companyId);
|
|
7712
|
+
const projectId = typeof input.projectId === "string" && input.projectId.trim() ? input.projectId.trim() : null;
|
|
7713
|
+
if (!companyId || !projectId) {
|
|
7714
|
+
return {
|
|
7715
|
+
status: "missing_project",
|
|
7716
|
+
projectId,
|
|
7717
|
+
totalOpenPullRequests: 0,
|
|
7718
|
+
mergeablePullRequests: 0,
|
|
7719
|
+
reviewablePullRequests: 0,
|
|
7720
|
+
failingPullRequests: 0
|
|
7721
|
+
};
|
|
7722
|
+
}
|
|
7723
|
+
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
7724
|
+
const projectMappings = await resolveProjectScopedMappings(ctx, settings.mappings, {
|
|
7725
|
+
companyId,
|
|
7726
|
+
projectId
|
|
7727
|
+
});
|
|
7728
|
+
if (projectMappings.length === 0) {
|
|
7729
|
+
return {
|
|
7730
|
+
status: "unmapped",
|
|
7731
|
+
projectId,
|
|
7732
|
+
totalOpenPullRequests: 0,
|
|
7733
|
+
mergeablePullRequests: 0,
|
|
7734
|
+
reviewablePullRequests: 0,
|
|
7735
|
+
failingPullRequests: 0
|
|
7736
|
+
};
|
|
7737
|
+
}
|
|
7738
|
+
const config = await getResolvedConfig(ctx);
|
|
7739
|
+
if (!hasConfiguredGithubToken(settings, config)) {
|
|
7740
|
+
return {
|
|
7741
|
+
status: "missing_token",
|
|
7742
|
+
projectId,
|
|
7743
|
+
totalOpenPullRequests: 0,
|
|
7744
|
+
mergeablePullRequests: 0,
|
|
7745
|
+
reviewablePullRequests: 0,
|
|
7746
|
+
failingPullRequests: 0
|
|
7747
|
+
};
|
|
7748
|
+
}
|
|
7749
|
+
try {
|
|
7750
|
+
const scope = await requireProjectPullRequestScope(ctx, input, projectMappings);
|
|
7751
|
+
const metrics = await getOrLoadCachedProjectPullRequestMetrics(ctx, scope);
|
|
7752
|
+
return {
|
|
7753
|
+
status: "ready",
|
|
7754
|
+
projectId,
|
|
7755
|
+
...metrics
|
|
7756
|
+
};
|
|
7757
|
+
} catch (error) {
|
|
7758
|
+
return {
|
|
7759
|
+
status: "error",
|
|
7760
|
+
projectId,
|
|
7761
|
+
message: getErrorMessage(error)
|
|
7762
|
+
};
|
|
7763
|
+
}
|
|
7764
|
+
}
|
|
7765
|
+
async function buildProjectPullRequestCountData(ctx, input) {
|
|
7766
|
+
const companyId = normalizeCompanyId(input.companyId);
|
|
7767
|
+
const projectId = typeof input.projectId === "string" && input.projectId.trim() ? input.projectId.trim() : null;
|
|
7768
|
+
if (!companyId || !projectId) {
|
|
7769
|
+
return {
|
|
7770
|
+
status: "missing_project",
|
|
7771
|
+
projectId,
|
|
7772
|
+
totalOpenPullRequests: 0
|
|
7773
|
+
};
|
|
7774
|
+
}
|
|
7775
|
+
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
7776
|
+
const projectMappings = await resolveProjectScopedMappings(ctx, settings.mappings, {
|
|
7777
|
+
companyId,
|
|
7778
|
+
projectId
|
|
7779
|
+
});
|
|
7780
|
+
if (projectMappings.length === 0) {
|
|
7781
|
+
return {
|
|
7782
|
+
status: "unmapped",
|
|
7783
|
+
projectId,
|
|
7784
|
+
totalOpenPullRequests: 0
|
|
7785
|
+
};
|
|
7786
|
+
}
|
|
7787
|
+
const config = await getResolvedConfig(ctx);
|
|
7788
|
+
if (!hasConfiguredGithubToken(settings, config)) {
|
|
7789
|
+
return {
|
|
7790
|
+
status: "missing_token",
|
|
7791
|
+
projectId,
|
|
7792
|
+
totalOpenPullRequests: 0
|
|
7793
|
+
};
|
|
7794
|
+
}
|
|
7795
|
+
try {
|
|
7796
|
+
const scope = await requireProjectPullRequestScope(ctx, input, projectMappings);
|
|
7797
|
+
const totalOpenPullRequests = await getOrLoadCachedProjectPullRequestCount(ctx, scope);
|
|
7798
|
+
return {
|
|
7799
|
+
status: "ready",
|
|
7800
|
+
projectId,
|
|
7801
|
+
totalOpenPullRequests
|
|
7802
|
+
};
|
|
7803
|
+
} catch (error) {
|
|
7804
|
+
return {
|
|
7805
|
+
status: "error",
|
|
7806
|
+
projectId,
|
|
7807
|
+
totalOpenPullRequests: 0,
|
|
7808
|
+
message: getErrorMessage(error)
|
|
7809
|
+
};
|
|
7810
|
+
}
|
|
7811
|
+
}
|
|
7812
|
+
async function buildSettingsTokenPermissionAuditData(ctx, input) {
|
|
7813
|
+
const requestedCompanyId = normalizeCompanyId(input.companyId);
|
|
7814
|
+
if (!requestedCompanyId) {
|
|
7815
|
+
return {
|
|
7816
|
+
status: "ready",
|
|
7817
|
+
allRequiredPermissionsGranted: true,
|
|
7818
|
+
repositories: [],
|
|
7819
|
+
missingPermissions: [],
|
|
7820
|
+
warnings: ["Open a company to audit token permissions for its mapped repositories."]
|
|
7821
|
+
};
|
|
7822
|
+
}
|
|
7823
|
+
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
7824
|
+
const config = await getResolvedConfig(ctx);
|
|
7825
|
+
if (!hasConfiguredGithubToken(settings, config)) {
|
|
7826
|
+
return {
|
|
7827
|
+
status: "missing_token",
|
|
7828
|
+
allRequiredPermissionsGranted: false,
|
|
7829
|
+
repositories: [],
|
|
7830
|
+
missingPermissions: [],
|
|
7831
|
+
warnings: [],
|
|
7832
|
+
message: "Save a GitHub token before auditing repository permissions."
|
|
7833
|
+
};
|
|
7834
|
+
}
|
|
7835
|
+
const scopedMappings = getSyncableMappings(filterMappingsByCompany(settings.mappings, requestedCompanyId));
|
|
7836
|
+
if (scopedMappings.length === 0) {
|
|
7837
|
+
return {
|
|
7838
|
+
status: "ready",
|
|
7839
|
+
allRequiredPermissionsGranted: true,
|
|
7840
|
+
repositories: [],
|
|
7841
|
+
missingPermissions: [],
|
|
7842
|
+
warnings: ["Add at least one mapped repository in this company to audit token permissions."]
|
|
7843
|
+
};
|
|
7844
|
+
}
|
|
7845
|
+
try {
|
|
7846
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
7847
|
+
const repositories = await Promise.all(
|
|
7848
|
+
[
|
|
7849
|
+
...new Map(
|
|
7850
|
+
scopedMappings.map((mapping) => {
|
|
7851
|
+
const repository = parseRepositoryReference(mapping.repositoryUrl);
|
|
7852
|
+
return repository ? [repository.url, repository] : null;
|
|
7853
|
+
}).filter((entry) => entry !== null)
|
|
7854
|
+
).values()
|
|
7855
|
+
].map((repository) => getOrLoadGitHubRepositoryTokenCapabilityAudit(octokit, repository))
|
|
7856
|
+
);
|
|
7857
|
+
const missingPermissions = [
|
|
7858
|
+
...new Set(repositories.flatMap((repository) => repository.missingPermissions))
|
|
7859
|
+
].sort((left, right) => left.localeCompare(right));
|
|
7860
|
+
const warnings = repositories.flatMap((repository) => repository.warnings);
|
|
7861
|
+
return {
|
|
7862
|
+
status: "ready",
|
|
7863
|
+
allRequiredPermissionsGranted: repositories.length > 0 && repositories.every((repository) => repository.status === "verified"),
|
|
7864
|
+
repositories,
|
|
7865
|
+
missingPermissions,
|
|
7866
|
+
warnings
|
|
7867
|
+
};
|
|
7868
|
+
} catch (error) {
|
|
7869
|
+
return {
|
|
7870
|
+
status: "error",
|
|
7871
|
+
allRequiredPermissionsGranted: false,
|
|
7872
|
+
repositories: [],
|
|
7873
|
+
missingPermissions: [],
|
|
7874
|
+
warnings: [],
|
|
7875
|
+
message: getErrorMessage(error)
|
|
7876
|
+
};
|
|
7877
|
+
}
|
|
7878
|
+
}
|
|
7879
|
+
async function listProjectPullRequestClosingIssues(octokit, repository, pullRequestNumber) {
|
|
7880
|
+
const response = await octokit.graphql(
|
|
7881
|
+
GITHUB_PULL_REQUEST_CLOSING_ISSUES_QUERY,
|
|
7882
|
+
{
|
|
7883
|
+
owner: repository.owner,
|
|
7884
|
+
repo: repository.repo,
|
|
7885
|
+
pullRequestNumber
|
|
7886
|
+
}
|
|
7887
|
+
);
|
|
7888
|
+
return normalizeProjectPullRequestClosingIssues(
|
|
7889
|
+
repository,
|
|
7890
|
+
response.repository?.pullRequest?.closingIssuesReferences?.nodes
|
|
7891
|
+
);
|
|
7892
|
+
}
|
|
7893
|
+
function getPullRequestApiState(value) {
|
|
7894
|
+
if (value.merged === true) {
|
|
7895
|
+
return "merged";
|
|
7896
|
+
}
|
|
7897
|
+
return value.state === "closed" ? "closed" : "open";
|
|
7898
|
+
}
|
|
7899
|
+
async function buildProjectPullRequestDetailData(ctx, input) {
|
|
7900
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
7901
|
+
if (!pullRequestNumber) {
|
|
7902
|
+
return null;
|
|
7903
|
+
}
|
|
7904
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
7905
|
+
const detailCacheKey = buildProjectPullRequestDetailCacheKey(scope, pullRequestNumber);
|
|
7906
|
+
const cachedDetail = getFreshCacheValue(activeProjectPullRequestDetailCache, detailCacheKey);
|
|
7907
|
+
if (cachedDetail !== null) {
|
|
7908
|
+
return cachedDetail;
|
|
7909
|
+
}
|
|
7910
|
+
const cachedSummaryRecord = getFreshCacheValue(
|
|
7911
|
+
activeProjectPullRequestSummaryRecordCache,
|
|
7912
|
+
buildProjectPullRequestSummaryRecordCacheKey(scope, pullRequestNumber)
|
|
7913
|
+
);
|
|
7914
|
+
const cachedLinkedIssue = cachedSummaryRecord ? getLinkedPaperclipIssueFromProjectPullRequestRecord(cachedSummaryRecord) : void 0;
|
|
7915
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
7916
|
+
const response = await octokit.rest.pulls.get({
|
|
7917
|
+
owner: scope.repository.owner,
|
|
7918
|
+
repo: scope.repository.repo,
|
|
7919
|
+
pull_number: pullRequestNumber,
|
|
7920
|
+
headers: {
|
|
7921
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
7922
|
+
}
|
|
7923
|
+
});
|
|
7924
|
+
const pullRequest = response.data;
|
|
7925
|
+
const reviewSummaryPromise = getOrLoadCachedGitHubPullRequestReviewSummary(octokit, scope.repository, pullRequestNumber);
|
|
7926
|
+
const reviewThreadSummaryPromise = getOrLoadCachedGitHubPullRequestReviewThreadSummary(octokit, scope.repository, pullRequestNumber);
|
|
7927
|
+
const statusSnapshotPromise = reviewThreadSummaryPromise.then(
|
|
7928
|
+
(reviewThreadSummary2) => getGitHubPullRequestStatusSnapshot(octokit, scope.repository, pullRequestNumber, /* @__PURE__ */ new Map(), {
|
|
7929
|
+
reviewThreadSummary: reviewThreadSummary2
|
|
7930
|
+
})
|
|
7931
|
+
);
|
|
7932
|
+
const [reviewSummary, reviewThreadSummary, comments, linkedIssue, statusSnapshot] = await Promise.all([
|
|
7933
|
+
reviewSummaryPromise,
|
|
7934
|
+
reviewThreadSummaryPromise,
|
|
7935
|
+
listAllGitHubIssueComments(octokit, scope.repository, pullRequestNumber),
|
|
7936
|
+
cachedLinkedIssue ? Promise.resolve(cachedLinkedIssue) : (async () => {
|
|
7937
|
+
const issueLookup = await buildProjectPullRequestIssueLookup(ctx, scope);
|
|
7938
|
+
return resolveLinkedPaperclipIssueForPullRequest(
|
|
7939
|
+
pullRequestNumber,
|
|
7940
|
+
await listProjectPullRequestClosingIssues(octokit, scope.repository, pullRequestNumber),
|
|
7941
|
+
issueLookup
|
|
7942
|
+
);
|
|
7943
|
+
})(),
|
|
7944
|
+
statusSnapshotPromise
|
|
7945
|
+
]);
|
|
7946
|
+
const author = buildProjectPullRequestPerson({
|
|
7947
|
+
login: pullRequest.user?.login,
|
|
7948
|
+
url: pullRequest.user?.html_url,
|
|
7949
|
+
avatarUrl: pullRequest.user?.avatar_url
|
|
7950
|
+
});
|
|
7951
|
+
const timeline = [];
|
|
7952
|
+
const trimmedBody = pullRequest.body?.trim() ?? "";
|
|
7953
|
+
if (trimmedBody) {
|
|
7954
|
+
timeline.push({
|
|
7955
|
+
id: `github-pull-request-${pullRequestNumber}-description`,
|
|
7956
|
+
kind: "description",
|
|
7957
|
+
author,
|
|
7958
|
+
createdAt: pullRequest.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
7959
|
+
body: trimmedBody
|
|
7960
|
+
});
|
|
7961
|
+
}
|
|
7962
|
+
for (const comment of comments) {
|
|
7963
|
+
timeline.push({
|
|
7964
|
+
id: `github-pull-request-${pullRequestNumber}-comment-${comment.id}`,
|
|
7965
|
+
kind: "comment",
|
|
7966
|
+
author: buildProjectPullRequestPerson({
|
|
7967
|
+
login: comment.authorLogin,
|
|
7968
|
+
url: comment.authorUrl,
|
|
7969
|
+
avatarUrl: comment.authorAvatarUrl
|
|
7970
|
+
}),
|
|
7971
|
+
createdAt: comment.createdAt ?? comment.updatedAt ?? pullRequest.updated_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
7972
|
+
body: comment.body
|
|
7973
|
+
});
|
|
7974
|
+
}
|
|
7975
|
+
timeline.sort(
|
|
7976
|
+
(left, right) => Date.parse(String(left.createdAt ?? "")) - Date.parse(String(right.createdAt ?? ""))
|
|
7977
|
+
);
|
|
7978
|
+
const checksStatus = statusSnapshot.ciState === "green" ? "passed" : statusSnapshot.ciState === "red" ? "failed" : "pending";
|
|
7979
|
+
const githubMergeable = pullRequest.mergeable === true;
|
|
7980
|
+
const reviewable = resolveProjectPullRequestReviewable({
|
|
7981
|
+
checksStatus,
|
|
7982
|
+
copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
|
|
7983
|
+
githubMergeable
|
|
7984
|
+
});
|
|
7985
|
+
const mergeable = resolveProjectPullRequestMergeable({
|
|
7986
|
+
checksStatus,
|
|
7987
|
+
reviewApprovals: reviewSummary.approvals,
|
|
7988
|
+
unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
|
|
7989
|
+
githubMergeable
|
|
7990
|
+
});
|
|
7991
|
+
return setCacheValue(
|
|
7992
|
+
activeProjectPullRequestDetailCache,
|
|
7993
|
+
detailCacheKey,
|
|
7994
|
+
{
|
|
7995
|
+
id: `github-pull-request-${scope.repository.owner}-${scope.repository.repo}-${pullRequestNumber}`,
|
|
7996
|
+
number: pullRequest.number,
|
|
7997
|
+
title: pullRequest.title,
|
|
7998
|
+
labels: normalizeGitHubIssueLabels(pullRequest.labels),
|
|
7999
|
+
author,
|
|
8000
|
+
assignees: (pullRequest.assignees ?? []).map((assignee) => buildProjectPullRequestPerson({
|
|
8001
|
+
login: assignee?.login,
|
|
8002
|
+
url: assignee?.html_url,
|
|
8003
|
+
avatarUrl: assignee?.avatar_url
|
|
8004
|
+
})),
|
|
8005
|
+
checksStatus,
|
|
8006
|
+
githubMergeable,
|
|
8007
|
+
reviewable,
|
|
8008
|
+
reviewApprovals: reviewSummary.approvals,
|
|
8009
|
+
reviewChangesRequested: reviewSummary.changesRequested,
|
|
8010
|
+
reviewCommentCount: typeof pullRequest.review_comments === "number" && pullRequest.review_comments >= 0 ? Math.floor(pullRequest.review_comments) : 0,
|
|
8011
|
+
unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
|
|
8012
|
+
copilotUnresolvedReviewThreads: reviewThreadSummary.copilotUnresolvedReviewThreads,
|
|
8013
|
+
commentsCount: typeof pullRequest.comments === "number" && pullRequest.comments >= 0 ? Math.floor(pullRequest.comments) : comments.length,
|
|
8014
|
+
createdAt: pullRequest.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
8015
|
+
updatedAt: pullRequest.updated_at ?? pullRequest.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
8016
|
+
...linkedIssue?.paperclipIssueId ? { paperclipIssueId: linkedIssue.paperclipIssueId } : {},
|
|
8017
|
+
...linkedIssue?.paperclipIssueKey ? { paperclipIssueKey: linkedIssue.paperclipIssueKey } : {},
|
|
8018
|
+
mergeable,
|
|
8019
|
+
status: getPullRequestApiState({
|
|
8020
|
+
state: pullRequest.state,
|
|
8021
|
+
merged: pullRequest.merged
|
|
8022
|
+
}),
|
|
8023
|
+
githubUrl: pullRequest.html_url,
|
|
8024
|
+
checksUrl: `${pullRequest.html_url}/checks`,
|
|
8025
|
+
reviewsUrl: `${pullRequest.html_url}/files`,
|
|
8026
|
+
reviewThreadsUrl: `${pullRequest.html_url}/files`,
|
|
8027
|
+
commentsUrl: pullRequest.html_url,
|
|
8028
|
+
baseBranch: pullRequest.base.ref,
|
|
8029
|
+
headBranch: pullRequest.head.ref,
|
|
8030
|
+
commits: typeof pullRequest.commits === "number" && pullRequest.commits >= 0 ? pullRequest.commits : 0,
|
|
8031
|
+
changedFiles: typeof pullRequest.changed_files === "number" && pullRequest.changed_files >= 0 ? pullRequest.changed_files : 0,
|
|
8032
|
+
timeline
|
|
8033
|
+
},
|
|
8034
|
+
PROJECT_PULL_REQUEST_DETAIL_CACHE_TTL_MS
|
|
8035
|
+
);
|
|
8036
|
+
}
|
|
8037
|
+
async function createProjectPullRequestPaperclipIssue(ctx, input) {
|
|
8038
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8039
|
+
if (!pullRequestNumber) {
|
|
8040
|
+
throw new Error("pullRequestNumber is required.");
|
|
8041
|
+
}
|
|
8042
|
+
if (!ctx.issues || typeof ctx.issues.create !== "function") {
|
|
8043
|
+
throw new Error("This Paperclip runtime does not expose plugin issue creation yet.");
|
|
8044
|
+
}
|
|
8045
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8046
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8047
|
+
const pullRequestResponse = await octokit.rest.pulls.get({
|
|
8048
|
+
owner: scope.repository.owner,
|
|
8049
|
+
repo: scope.repository.repo,
|
|
8050
|
+
pull_number: pullRequestNumber,
|
|
8051
|
+
headers: {
|
|
8052
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8053
|
+
}
|
|
8054
|
+
});
|
|
8055
|
+
const pullRequest = pullRequestResponse.data;
|
|
8056
|
+
const pullRequestUrl = pullRequest.html_url;
|
|
8057
|
+
const existingLinks = await listGitHubPullRequestLinkRecords(ctx, {
|
|
8058
|
+
externalId: pullRequestUrl
|
|
8059
|
+
});
|
|
8060
|
+
const existingLink = existingLinks.find(
|
|
8061
|
+
(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)
|
|
8062
|
+
);
|
|
8063
|
+
const existingIssue = existingLink ? await ctx.issues.get(existingLink.paperclipIssueId, scope.companyId) : null;
|
|
8064
|
+
if (existingLink && existingIssue) {
|
|
8065
|
+
return {
|
|
8066
|
+
paperclipIssueId: existingIssue.id,
|
|
8067
|
+
...existingIssue.identifier ? { paperclipIssueKey: existingIssue.identifier } : {},
|
|
8068
|
+
alreadyLinked: true
|
|
8069
|
+
};
|
|
8070
|
+
}
|
|
8071
|
+
const requestedTitle = typeof input.title === "string" && input.title.trim() ? input.title.trim() : pullRequest.title.trim();
|
|
8072
|
+
const createdIssue = await ctx.issues.create({
|
|
8073
|
+
companyId: scope.companyId,
|
|
8074
|
+
projectId: scope.projectId,
|
|
8075
|
+
title: requestedTitle,
|
|
8076
|
+
description: buildPaperclipIssueDescriptionFromPullRequest({
|
|
8077
|
+
repository: scope.repository,
|
|
8078
|
+
pullRequestNumber,
|
|
8079
|
+
pullRequestUrl,
|
|
8080
|
+
body: pullRequest.body
|
|
8081
|
+
})
|
|
8082
|
+
});
|
|
8083
|
+
const resolvedIssue = await ctx.issues.get(createdIssue.id, scope.companyId) ?? createdIssue;
|
|
8084
|
+
await upsertGitHubPullRequestLinkRecord(ctx, {
|
|
8085
|
+
companyId: scope.companyId,
|
|
8086
|
+
projectId: scope.projectId,
|
|
8087
|
+
issueId: resolvedIssue.id,
|
|
8088
|
+
repositoryUrl: scope.repository.url,
|
|
8089
|
+
pullRequestNumber,
|
|
8090
|
+
pullRequestUrl,
|
|
8091
|
+
pullRequestTitle: pullRequest.title,
|
|
8092
|
+
pullRequestState: getPullRequestApiState({
|
|
8093
|
+
state: pullRequest.state,
|
|
8094
|
+
merged: pullRequest.merged
|
|
8095
|
+
}) === "open" ? "open" : "closed"
|
|
8096
|
+
});
|
|
8097
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8098
|
+
return {
|
|
8099
|
+
paperclipIssueId: resolvedIssue.id,
|
|
8100
|
+
...resolvedIssue.identifier ? { paperclipIssueKey: resolvedIssue.identifier } : {},
|
|
8101
|
+
alreadyLinked: false
|
|
8102
|
+
};
|
|
8103
|
+
}
|
|
8104
|
+
async function refreshProjectPullRequests(ctx, input) {
|
|
8105
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8106
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8107
|
+
return {
|
|
8108
|
+
status: "refreshed",
|
|
8109
|
+
projectId: scope.projectId,
|
|
8110
|
+
repositoryUrl: scope.repository.url,
|
|
8111
|
+
refreshedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8112
|
+
};
|
|
8113
|
+
}
|
|
8114
|
+
async function updateProjectPullRequestBranch(ctx, input) {
|
|
8115
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8116
|
+
if (!pullRequestNumber) {
|
|
8117
|
+
throw new Error("pullRequestNumber is required.");
|
|
8118
|
+
}
|
|
8119
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8120
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8121
|
+
const pullRequestResponse = await octokit.rest.pulls.get({
|
|
8122
|
+
owner: scope.repository.owner,
|
|
8123
|
+
repo: scope.repository.repo,
|
|
8124
|
+
pull_number: pullRequestNumber,
|
|
8125
|
+
headers: {
|
|
8126
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8127
|
+
}
|
|
8128
|
+
});
|
|
8129
|
+
const pullRequest = pullRequestResponse.data;
|
|
8130
|
+
const githubUrl = pullRequest.html_url ?? `${scope.repository.url}/pull/${pullRequestNumber}`;
|
|
8131
|
+
const pullRequestState = getPullRequestApiState({
|
|
8132
|
+
state: pullRequest.state,
|
|
8133
|
+
merged: pullRequest.merged
|
|
8134
|
+
});
|
|
8135
|
+
if (pullRequestState !== "open") {
|
|
8136
|
+
throw new Error("Only open pull requests can be updated with the base branch.");
|
|
8137
|
+
}
|
|
8138
|
+
const behindBy = await getGitHubPullRequestBehindCount(octokit, scope.repository, {
|
|
8139
|
+
baseBranch: pullRequest.base.ref,
|
|
8140
|
+
headBranch: pullRequest.head.ref,
|
|
8141
|
+
headRepositoryOwner: pullRequest.head.repo?.owner?.login
|
|
8142
|
+
});
|
|
8143
|
+
if (typeof behindBy === "number" && behindBy <= 0) {
|
|
8144
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8145
|
+
return {
|
|
8146
|
+
githubUrl,
|
|
8147
|
+
status: "already_up_to_date"
|
|
8148
|
+
};
|
|
8149
|
+
}
|
|
8150
|
+
const mergeableState = typeof pullRequest.mergeable_state === "string" ? pullRequest.mergeable_state.trim().toLowerCase() : "";
|
|
8151
|
+
if ((mergeableState === "dirty" || pullRequest.mergeable === false) && (behindBy === null || behindBy > 0)) {
|
|
8152
|
+
throw new Error("This pull request needs conflict resolution before it can be updated with the base branch.");
|
|
8153
|
+
}
|
|
8154
|
+
try {
|
|
8155
|
+
await octokit.request("PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch", {
|
|
8156
|
+
owner: scope.repository.owner,
|
|
8157
|
+
repo: scope.repository.repo,
|
|
8158
|
+
pull_number: pullRequestNumber,
|
|
8159
|
+
...typeof pullRequest.head.sha === "string" && pullRequest.head.sha.trim() ? { expected_head_sha: pullRequest.head.sha.trim() } : {},
|
|
8160
|
+
headers: {
|
|
8161
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8162
|
+
}
|
|
8163
|
+
});
|
|
8164
|
+
} catch (error) {
|
|
8165
|
+
throw buildGitHubPullRequestWriteActionError({
|
|
8166
|
+
action: "update_branch",
|
|
8167
|
+
error,
|
|
8168
|
+
repositoryLabel: `${scope.repository.owner}/${scope.repository.repo}`
|
|
8169
|
+
});
|
|
8170
|
+
}
|
|
8171
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8172
|
+
return {
|
|
8173
|
+
githubUrl,
|
|
8174
|
+
status: "update_requested"
|
|
8175
|
+
};
|
|
4850
8176
|
}
|
|
4851
|
-
async function
|
|
4852
|
-
const
|
|
4853
|
-
if (
|
|
4854
|
-
|
|
4855
|
-
}
|
|
4856
|
-
const
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
8177
|
+
async function mergeProjectPullRequest(ctx, input) {
|
|
8178
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8179
|
+
if (!pullRequestNumber) {
|
|
8180
|
+
throw new Error("pullRequestNumber is required.");
|
|
8181
|
+
}
|
|
8182
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8183
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8184
|
+
const response = await octokit.rest.pulls.merge({
|
|
8185
|
+
owner: scope.repository.owner,
|
|
8186
|
+
repo: scope.repository.repo,
|
|
8187
|
+
pull_number: pullRequestNumber,
|
|
8188
|
+
headers: {
|
|
8189
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
4861
8190
|
}
|
|
4862
|
-
|
|
8191
|
+
});
|
|
8192
|
+
if (response.data.merged !== true) {
|
|
8193
|
+
throw new Error(response.data.message ?? `GitHub did not merge pull request #${pullRequestNumber}.`);
|
|
4863
8194
|
}
|
|
4864
|
-
|
|
8195
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8196
|
+
return {
|
|
8197
|
+
githubUrl: `${scope.repository.url}/pull/${pullRequestNumber}`,
|
|
8198
|
+
status: "merged"
|
|
8199
|
+
};
|
|
4865
8200
|
}
|
|
4866
|
-
async function
|
|
4867
|
-
const
|
|
4868
|
-
if (
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
throw new Error("The provided issue number does not match the linked GitHub issue for this Paperclip issue.");
|
|
8201
|
+
async function closeProjectPullRequest(ctx, input) {
|
|
8202
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8203
|
+
if (!pullRequestNumber) {
|
|
8204
|
+
throw new Error("pullRequestNumber is required.");
|
|
8205
|
+
}
|
|
8206
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8207
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8208
|
+
const response = await octokit.rest.pulls.update({
|
|
8209
|
+
owner: scope.repository.owner,
|
|
8210
|
+
repo: scope.repository.repo,
|
|
8211
|
+
pull_number: pullRequestNumber,
|
|
8212
|
+
state: "closed",
|
|
8213
|
+
headers: {
|
|
8214
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
4881
8215
|
}
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
8216
|
+
});
|
|
8217
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8218
|
+
return {
|
|
8219
|
+
githubUrl: response.data.html_url ?? `${scope.repository.url}/pull/${pullRequestNumber}`,
|
|
8220
|
+
status: getPullRequestApiState({
|
|
8221
|
+
state: response.data.state,
|
|
8222
|
+
merged: response.data.merged
|
|
8223
|
+
})
|
|
8224
|
+
};
|
|
8225
|
+
}
|
|
8226
|
+
async function addProjectPullRequestComment(ctx, input) {
|
|
8227
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8228
|
+
if (!pullRequestNumber) {
|
|
8229
|
+
throw new Error("pullRequestNumber is required.");
|
|
4889
8230
|
}
|
|
4890
|
-
const
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
throw new Error("issueNumber is required when paperclipIssueId is not provided.");
|
|
8231
|
+
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
8232
|
+
if (!body) {
|
|
8233
|
+
throw new Error("Comment body cannot be empty.");
|
|
4894
8234
|
}
|
|
8235
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8236
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8237
|
+
const response = await createProjectPullRequestGitHubComment(octokit, scope, pullRequestNumber, body);
|
|
8238
|
+
invalidateProjectPullRequestCaches(scope);
|
|
4895
8239
|
return {
|
|
4896
|
-
|
|
4897
|
-
|
|
8240
|
+
commentId: response.id,
|
|
8241
|
+
commentUrl: response.htmlUrl ?? `${scope.repository.url}/pull/${pullRequestNumber}`
|
|
4898
8242
|
};
|
|
4899
8243
|
}
|
|
4900
|
-
async function
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
8244
|
+
async function createProjectPullRequestGitHubComment(octokit, scope, pullRequestNumber, body) {
|
|
8245
|
+
let response;
|
|
8246
|
+
try {
|
|
8247
|
+
response = await octokit.rest.issues.createComment({
|
|
8248
|
+
owner: scope.repository.owner,
|
|
8249
|
+
repo: scope.repository.repo,
|
|
8250
|
+
issue_number: pullRequestNumber,
|
|
8251
|
+
body,
|
|
8252
|
+
headers: {
|
|
8253
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8254
|
+
}
|
|
8255
|
+
});
|
|
8256
|
+
} catch (error) {
|
|
8257
|
+
throw buildGitHubPullRequestWriteActionError({
|
|
8258
|
+
action: "comment",
|
|
8259
|
+
error,
|
|
8260
|
+
repositoryLabel: `${scope.repository.owner}/${scope.repository.repo}`
|
|
8261
|
+
});
|
|
8262
|
+
}
|
|
8263
|
+
return {
|
|
8264
|
+
id: response.data.id,
|
|
8265
|
+
...response.data.html_url ? { htmlUrl: response.data.html_url } : {}
|
|
8266
|
+
};
|
|
8267
|
+
}
|
|
8268
|
+
function buildProjectPullRequestCopilotComment(action, options) {
|
|
8269
|
+
const baseBranch = typeof options?.baseBranch === "string" ? options.baseBranch.trim() : "";
|
|
8270
|
+
const baseBranchLabel = baseBranch ? `\`${baseBranch}\`` : "the base branch";
|
|
8271
|
+
switch (action) {
|
|
8272
|
+
case "fix_ci":
|
|
8273
|
+
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.";
|
|
8274
|
+
case "rebase":
|
|
8275
|
+
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.`;
|
|
8276
|
+
case "address_review_feedback":
|
|
8277
|
+
return "@copilot Please address the unresolved review feedback on this pull request, push the necessary updates to this branch, and summarize what you changed.";
|
|
8278
|
+
case "review":
|
|
8279
|
+
return "@copilot Please review this pull request and leave feedback as GitHub review comments. Focus on correctness, regressions, and missing tests.";
|
|
8280
|
+
}
|
|
8281
|
+
}
|
|
8282
|
+
var COPILOT_PULL_REQUEST_REVIEWER_LOGIN = "copilot-pull-request-reviewer[bot]";
|
|
8283
|
+
async function requestProjectPullRequestCopilotAction(ctx, input) {
|
|
8284
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8285
|
+
if (!pullRequestNumber) {
|
|
8286
|
+
throw new Error("pullRequestNumber is required.");
|
|
8287
|
+
}
|
|
8288
|
+
const action = normalizeProjectPullRequestCopilotAction(input.action);
|
|
8289
|
+
if (!action) {
|
|
8290
|
+
throw new Error('action must be one of "fix_ci", "rebase", "address_review_feedback", or "review".');
|
|
8291
|
+
}
|
|
8292
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8293
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8294
|
+
const pullRequestResponse = await octokit.rest.pulls.get({
|
|
8295
|
+
owner: scope.repository.owner,
|
|
8296
|
+
repo: scope.repository.repo,
|
|
8297
|
+
pull_number: pullRequestNumber,
|
|
8298
|
+
headers: {
|
|
8299
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
4906
8300
|
}
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
8301
|
+
});
|
|
8302
|
+
const pullRequest = pullRequestResponse.data;
|
|
8303
|
+
const githubUrl = pullRequest.html_url ?? `${scope.repository.url}/pull/${pullRequestNumber}`;
|
|
8304
|
+
const pullRequestState = getPullRequestApiState({
|
|
8305
|
+
state: pullRequest.state,
|
|
8306
|
+
merged: pullRequest.merged
|
|
8307
|
+
});
|
|
8308
|
+
if (pullRequestState !== "open") {
|
|
8309
|
+
throw new Error("Only open pull requests can request Copilot actions.");
|
|
8310
|
+
}
|
|
8311
|
+
switch (action) {
|
|
8312
|
+
case "fix_ci": {
|
|
8313
|
+
const ciState = await getGitHubPullRequestCiState(octokit, scope.repository, pullRequestNumber);
|
|
8314
|
+
if (ciState !== "red") {
|
|
8315
|
+
throw new Error("This pull request does not currently have failing checks.");
|
|
8316
|
+
}
|
|
8317
|
+
break;
|
|
4919
8318
|
}
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
};
|
|
8319
|
+
case "rebase": {
|
|
8320
|
+
const behindBy = await getGitHubPullRequestBehindCount(octokit, scope.repository, {
|
|
8321
|
+
baseBranch: pullRequest.base.ref,
|
|
8322
|
+
headBranch: pullRequest.head.ref,
|
|
8323
|
+
headRepositoryOwner: pullRequest.head.repo?.owner?.login
|
|
8324
|
+
});
|
|
8325
|
+
if (typeof behindBy === "number" && behindBy <= 0) {
|
|
8326
|
+
throw new Error("This pull request is already up to date with the base branch.");
|
|
8327
|
+
}
|
|
8328
|
+
const mergeableState = typeof pullRequest.mergeable_state === "string" ? pullRequest.mergeable_state.trim().toLowerCase() : "";
|
|
8329
|
+
const needsConflictResolution = (mergeableState === "dirty" || pullRequest.mergeable === false) && (behindBy === null || behindBy > 0);
|
|
8330
|
+
if (!needsConflictResolution) {
|
|
8331
|
+
throw new Error("This pull request can be updated without Copilot. Use Update branch instead.");
|
|
8332
|
+
}
|
|
8333
|
+
break;
|
|
4926
8334
|
}
|
|
4927
|
-
|
|
8335
|
+
case "address_review_feedback": {
|
|
8336
|
+
const reviewThreadSummary = await getOrLoadCachedGitHubPullRequestReviewThreadSummary(
|
|
8337
|
+
octokit,
|
|
8338
|
+
scope.repository,
|
|
8339
|
+
pullRequestNumber
|
|
8340
|
+
);
|
|
8341
|
+
if (reviewThreadSummary.unresolvedReviewThreads <= 0) {
|
|
8342
|
+
throw new Error("This pull request does not currently have unresolved review threads.");
|
|
8343
|
+
}
|
|
8344
|
+
break;
|
|
8345
|
+
}
|
|
8346
|
+
case "review":
|
|
8347
|
+
break;
|
|
4928
8348
|
}
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
8349
|
+
if (action === "review") {
|
|
8350
|
+
const pullRequestId = typeof pullRequest.node_id === "string" && pullRequest.node_id.trim() ? pullRequest.node_id.trim() : null;
|
|
8351
|
+
if (!pullRequestId) {
|
|
8352
|
+
throw new Error("GitHub did not return a pull request node id for this review request.");
|
|
8353
|
+
}
|
|
8354
|
+
let response;
|
|
8355
|
+
try {
|
|
8356
|
+
response = await octokit.graphql(
|
|
8357
|
+
GITHUB_REQUEST_PULL_REQUEST_COPILOT_REVIEW_MUTATION,
|
|
8358
|
+
{
|
|
8359
|
+
pullRequestId,
|
|
8360
|
+
botLogins: [COPILOT_PULL_REQUEST_REVIEWER_LOGIN]
|
|
8361
|
+
}
|
|
8362
|
+
);
|
|
8363
|
+
} catch (error) {
|
|
8364
|
+
throw buildGitHubPullRequestWriteActionError({
|
|
8365
|
+
action: "review",
|
|
8366
|
+
error,
|
|
8367
|
+
repositoryLabel: `${scope.repository.owner}/${scope.repository.repo}`
|
|
8368
|
+
});
|
|
8369
|
+
}
|
|
8370
|
+
const requestedReviewers = (response.requestReviews?.requestedReviewers?.edges ?? []).map((edge) => edge?.node?.login?.trim() ?? "").filter(Boolean);
|
|
8371
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8372
|
+
return {
|
|
8373
|
+
action,
|
|
8374
|
+
actionLabel: getProjectPullRequestCopilotActionLabel(action),
|
|
8375
|
+
requestedReviewer: requestedReviewers[0] ?? COPILOT_PULL_REQUEST_REVIEWER_LOGIN,
|
|
8376
|
+
githubUrl: response.requestReviews?.pullRequest?.url ?? githubUrl
|
|
8377
|
+
};
|
|
4933
8378
|
}
|
|
8379
|
+
const comment = await createProjectPullRequestGitHubComment(
|
|
8380
|
+
octokit,
|
|
8381
|
+
scope,
|
|
8382
|
+
pullRequestNumber,
|
|
8383
|
+
buildProjectPullRequestCopilotComment(action, {
|
|
8384
|
+
baseBranch: pullRequest.base.ref
|
|
8385
|
+
})
|
|
8386
|
+
);
|
|
8387
|
+
invalidateProjectPullRequestCaches(scope);
|
|
4934
8388
|
return {
|
|
4935
|
-
|
|
4936
|
-
|
|
8389
|
+
action,
|
|
8390
|
+
actionLabel: getProjectPullRequestCopilotActionLabel(action),
|
|
8391
|
+
commentId: comment.id,
|
|
8392
|
+
commentUrl: comment.htmlUrl ?? githubUrl,
|
|
8393
|
+
githubUrl
|
|
4937
8394
|
};
|
|
4938
8395
|
}
|
|
4939
|
-
function
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
${AI_AUTHORED_COMMENT_FOOTER_PREFIX}${llmModel.trim()}.`;
|
|
4944
|
-
}
|
|
4945
|
-
function appendAiAuthorshipFooter(body, llmModel) {
|
|
4946
|
-
const trimmedBody = body.trim();
|
|
4947
|
-
if (!trimmedBody) {
|
|
4948
|
-
throw new Error("Comment body cannot be empty.");
|
|
8396
|
+
async function reviewProjectPullRequest(ctx, input) {
|
|
8397
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8398
|
+
if (!pullRequestNumber) {
|
|
8399
|
+
throw new Error("pullRequestNumber is required.");
|
|
4949
8400
|
}
|
|
4950
|
-
const
|
|
4951
|
-
if (!
|
|
4952
|
-
throw new Error(
|
|
8401
|
+
const reviewType = input.review === "approve" ? "APPROVE" : input.review === "request_changes" ? "REQUEST_CHANGES" : void 0;
|
|
8402
|
+
if (!reviewType) {
|
|
8403
|
+
throw new Error('review must be "approve" or "request_changes".');
|
|
4953
8404
|
}
|
|
4954
|
-
|
|
8405
|
+
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
8406
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8407
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8408
|
+
let response;
|
|
8409
|
+
try {
|
|
8410
|
+
response = await octokit.rest.pulls.createReview({
|
|
8411
|
+
owner: scope.repository.owner,
|
|
8412
|
+
repo: scope.repository.repo,
|
|
8413
|
+
pull_number: pullRequestNumber,
|
|
8414
|
+
event: reviewType,
|
|
8415
|
+
...body ? { body } : {},
|
|
8416
|
+
headers: {
|
|
8417
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8418
|
+
}
|
|
8419
|
+
});
|
|
8420
|
+
} catch (error) {
|
|
8421
|
+
throw buildGitHubPullRequestWriteActionError({
|
|
8422
|
+
action: "review",
|
|
8423
|
+
error,
|
|
8424
|
+
repositoryLabel: `${scope.repository.owner}/${scope.repository.repo}`,
|
|
8425
|
+
reviewType,
|
|
8426
|
+
body
|
|
8427
|
+
});
|
|
8428
|
+
}
|
|
8429
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8430
|
+
return {
|
|
8431
|
+
reviewId: response.data.id,
|
|
8432
|
+
review: reviewType === "APPROVE" ? "approved" : "changes_requested",
|
|
8433
|
+
reviewUrl: response.data.html_url ?? `${scope.repository.url}/pull/${pullRequestNumber}`
|
|
8434
|
+
};
|
|
4955
8435
|
}
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
8436
|
+
function isFailedCheckSuiteConclusion(value) {
|
|
8437
|
+
switch (value?.trim().toLowerCase()) {
|
|
8438
|
+
case "action_required":
|
|
8439
|
+
case "cancelled":
|
|
8440
|
+
case "failure":
|
|
8441
|
+
case "stale":
|
|
8442
|
+
case "timed_out":
|
|
8443
|
+
return true;
|
|
8444
|
+
default:
|
|
8445
|
+
return false;
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
async function rerunProjectPullRequestCi(ctx, input) {
|
|
8449
|
+
const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
|
|
8450
|
+
if (!pullRequestNumber) {
|
|
8451
|
+
throw new Error("pullRequestNumber is required.");
|
|
8452
|
+
}
|
|
8453
|
+
const scope = await requireProjectPullRequestScope(ctx, input);
|
|
8454
|
+
const octokit = await createGitHubToolOctokit(ctx);
|
|
8455
|
+
const pullRequestResponse = await octokit.rest.pulls.get({
|
|
8456
|
+
owner: scope.repository.owner,
|
|
8457
|
+
repo: scope.repository.repo,
|
|
8458
|
+
pull_number: pullRequestNumber,
|
|
4963
8459
|
headers: {
|
|
4964
8460
|
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
4965
8461
|
}
|
|
4966
|
-
})
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
updatedAt: comment.updated_at ?? void 0
|
|
4975
|
-
});
|
|
8462
|
+
});
|
|
8463
|
+
const checkSuitesResponse = await octokit.rest.checks.listSuitesForRef({
|
|
8464
|
+
owner: scope.repository.owner,
|
|
8465
|
+
repo: scope.repository.repo,
|
|
8466
|
+
ref: pullRequestResponse.data.head.sha,
|
|
8467
|
+
per_page: 100,
|
|
8468
|
+
headers: {
|
|
8469
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
4976
8470
|
}
|
|
8471
|
+
});
|
|
8472
|
+
const rerunnableSuites = checkSuitesResponse.data.check_suites.filter(
|
|
8473
|
+
(suite) => suite.status === "completed" && isFailedCheckSuiteConclusion(suite.conclusion)
|
|
8474
|
+
);
|
|
8475
|
+
if (rerunnableSuites.length === 0) {
|
|
8476
|
+
throw new Error("No failed GitHub check suites are available to re-run for this pull request.");
|
|
8477
|
+
}
|
|
8478
|
+
for (const suite of rerunnableSuites) {
|
|
8479
|
+
await octokit.rest.checks.rerequestSuite({
|
|
8480
|
+
owner: scope.repository.owner,
|
|
8481
|
+
repo: scope.repository.repo,
|
|
8482
|
+
check_suite_id: suite.id,
|
|
8483
|
+
headers: {
|
|
8484
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
8485
|
+
}
|
|
8486
|
+
});
|
|
4977
8487
|
}
|
|
4978
|
-
|
|
8488
|
+
invalidateProjectPullRequestCaches(scope);
|
|
8489
|
+
return {
|
|
8490
|
+
rerunCheckSuiteCount: rerunnableSuites.length,
|
|
8491
|
+
githubUrl: `${scope.repository.url}/pull/${pullRequestNumber}/checks`
|
|
8492
|
+
};
|
|
4979
8493
|
}
|
|
4980
8494
|
async function listAllPullRequestFiles(octokit, repository, pullRequestNumber) {
|
|
4981
8495
|
const files = [];
|
|
@@ -5163,6 +8677,10 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5163
8677
|
return next;
|
|
5164
8678
|
}
|
|
5165
8679
|
if (!ctx.issues || typeof ctx.issues.create !== "function") {
|
|
8680
|
+
const errorDetails = {
|
|
8681
|
+
phase: "configuration",
|
|
8682
|
+
suggestedAction: "Update Paperclip to a runtime that supports plugin issue creation, then retry sync."
|
|
8683
|
+
};
|
|
5166
8684
|
const next = {
|
|
5167
8685
|
...settings,
|
|
5168
8686
|
syncState: createErrorSyncState({
|
|
@@ -5172,10 +8690,14 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5172
8690
|
createdIssuesCount: 0,
|
|
5173
8691
|
skippedIssuesCount: 0,
|
|
5174
8692
|
erroredIssuesCount: 0,
|
|
5175
|
-
errorDetails
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
8693
|
+
errorDetails,
|
|
8694
|
+
recentFailures: appendRecentSyncFailureLogEntry(
|
|
8695
|
+
void 0,
|
|
8696
|
+
createSyncFailureLogEntry({
|
|
8697
|
+
message: "This Paperclip runtime does not expose plugin issue creation yet.",
|
|
8698
|
+
errorDetails
|
|
8699
|
+
})
|
|
8700
|
+
)
|
|
5179
8701
|
})
|
|
5180
8702
|
};
|
|
5181
8703
|
await ctx.state.set(SETTINGS_SCOPE, next);
|
|
@@ -5211,14 +8733,23 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5211
8733
|
erroredIssuesCount: recoverableFailures.length,
|
|
5212
8734
|
progress: currentProgress
|
|
5213
8735
|
});
|
|
8736
|
+
async function throwIfSyncCancelled() {
|
|
8737
|
+
const cancellationRequest = await getSyncCancellationRequest(ctx);
|
|
8738
|
+
if (!cancellationRequest) {
|
|
8739
|
+
return;
|
|
8740
|
+
}
|
|
8741
|
+
throw new SyncCancellationError(cancellationRequest.requestedAt);
|
|
8742
|
+
}
|
|
5214
8743
|
async function persistRunningProgress(force = false) {
|
|
5215
8744
|
const progress = normalizeSyncProgress(currentProgress);
|
|
8745
|
+
const recentFailures = buildRecentSyncFailureLogEntries(recoverableFailures);
|
|
5216
8746
|
const signature = JSON.stringify({
|
|
5217
8747
|
syncedIssuesCount,
|
|
5218
8748
|
createdIssuesCount,
|
|
5219
8749
|
skippedIssuesCount,
|
|
5220
8750
|
erroredIssuesCount: recoverableFailures.length,
|
|
5221
|
-
progress
|
|
8751
|
+
progress,
|
|
8752
|
+
recentFailures
|
|
5222
8753
|
});
|
|
5223
8754
|
const now = Date.now();
|
|
5224
8755
|
if (!force) {
|
|
@@ -5237,7 +8768,8 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5237
8768
|
createdIssuesCount,
|
|
5238
8769
|
skippedIssuesCount,
|
|
5239
8770
|
erroredIssuesCount: recoverableFailures.length,
|
|
5240
|
-
progress
|
|
8771
|
+
progress,
|
|
8772
|
+
recentFailures
|
|
5241
8773
|
})
|
|
5242
8774
|
);
|
|
5243
8775
|
activeRunningSyncState = currentSettings;
|
|
@@ -5254,7 +8786,9 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5254
8786
|
}
|
|
5255
8787
|
const repositoryPlans = [];
|
|
5256
8788
|
try {
|
|
8789
|
+
await throwIfSyncCancelled();
|
|
5257
8790
|
for (const [mappingIndex, mapping] of mappings.entries()) {
|
|
8791
|
+
await throwIfSyncCancelled();
|
|
5258
8792
|
try {
|
|
5259
8793
|
const repository = requireRepositoryReference(mapping.repositoryUrl);
|
|
5260
8794
|
const importedIssueRecords = nextRegistry.filter((entry) => doesImportedIssueRecordMatchMapping(entry, mapping)).filter((entry) => doesImportedIssueMatchTarget(entry, options.target));
|
|
@@ -5342,7 +8876,7 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5342
8876
|
trackedIssueCount
|
|
5343
8877
|
});
|
|
5344
8878
|
} catch (error) {
|
|
5345
|
-
if (isGitHubRateLimitError(error)) {
|
|
8879
|
+
if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
|
|
5346
8880
|
throw error;
|
|
5347
8881
|
}
|
|
5348
8882
|
recordRecoverableSyncFailure(ctx, recoverableFailures, error, failureContext);
|
|
@@ -5371,6 +8905,7 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5371
8905
|
}
|
|
5372
8906
|
await persistRunningProgress(true);
|
|
5373
8907
|
for (const plan of repositoryPlans) {
|
|
8908
|
+
await throwIfSyncCancelled();
|
|
5374
8909
|
try {
|
|
5375
8910
|
const { mapping, advancedSettings, repository, repositoryIndex, allIssuesById, issues } = plan;
|
|
5376
8911
|
const companyId = mapping.companyId;
|
|
@@ -5425,8 +8960,9 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5425
8960
|
openLinkedPullRequestNumbers,
|
|
5426
8961
|
pullRequestStatusCache
|
|
5427
8962
|
);
|
|
8963
|
+
await throwIfSyncCancelled();
|
|
5428
8964
|
} catch (error) {
|
|
5429
|
-
if (isGitHubRateLimitError(error)) {
|
|
8965
|
+
if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
|
|
5430
8966
|
throw error;
|
|
5431
8967
|
}
|
|
5432
8968
|
}
|
|
@@ -5441,6 +8977,7 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5441
8977
|
};
|
|
5442
8978
|
await persistRunningProgress(true);
|
|
5443
8979
|
for (const [issueIndex, issue] of issues.entries()) {
|
|
8980
|
+
await throwIfSyncCancelled();
|
|
5444
8981
|
const createdIssueCountBefore = createdIssueIds.size;
|
|
5445
8982
|
const skippedIssueCountBefore = skippedIssueIds.size;
|
|
5446
8983
|
try {
|
|
@@ -5489,6 +9026,7 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5489
9026
|
totalIssueCount: totalTrackedIssueCount
|
|
5490
9027
|
};
|
|
5491
9028
|
await persistRunningProgress(true);
|
|
9029
|
+
await throwIfSyncCancelled();
|
|
5492
9030
|
const synchronizationResult = await synchronizePaperclipIssueStatuses(
|
|
5493
9031
|
ctx,
|
|
5494
9032
|
octokit,
|
|
@@ -5506,6 +9044,7 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5506
9044
|
repositoryMaintainerCache,
|
|
5507
9045
|
failureContext,
|
|
5508
9046
|
recoverableFailures,
|
|
9047
|
+
throwIfSyncCancelled,
|
|
5509
9048
|
async (progress) => {
|
|
5510
9049
|
markTrackedIssueProcessed(mapping, progress.githubIssueId);
|
|
5511
9050
|
currentProgress = {
|
|
@@ -5524,7 +9063,7 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5524
9063
|
updatedLabelsCount += synchronizationResult.updatedLabelsCount;
|
|
5525
9064
|
updatedDescriptionsCount += synchronizationResult.updatedDescriptionsCount;
|
|
5526
9065
|
} catch (error) {
|
|
5527
|
-
if (isGitHubRateLimitError(error)) {
|
|
9066
|
+
if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
|
|
5528
9067
|
throw error;
|
|
5529
9068
|
}
|
|
5530
9069
|
recordRecoverableSyncFailure(ctx, recoverableFailures, error, failureContext);
|
|
@@ -5548,7 +9087,8 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5548
9087
|
skippedIssuesCount,
|
|
5549
9088
|
erroredIssuesCount: recoverableFailures.length,
|
|
5550
9089
|
progress: currentProgress,
|
|
5551
|
-
errorDetails
|
|
9090
|
+
errorDetails,
|
|
9091
|
+
recentFailures: buildRecentSyncFailureLogEntries(recoverableFailures)
|
|
5552
9092
|
})
|
|
5553
9093
|
};
|
|
5554
9094
|
await ctx.state.set(SETTINGS_SCOPE, next2);
|
|
@@ -5574,6 +9114,24 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5574
9114
|
await ctx.state.set(IMPORT_REGISTRY_SCOPE, nextRegistry);
|
|
5575
9115
|
return next;
|
|
5576
9116
|
} catch (error) {
|
|
9117
|
+
if (error instanceof SyncCancellationError) {
|
|
9118
|
+
const next2 = {
|
|
9119
|
+
...currentSettings,
|
|
9120
|
+
syncState: createCancelledSyncState({
|
|
9121
|
+
message: buildCancelledSyncMessage(options.target, currentProgress),
|
|
9122
|
+
trigger,
|
|
9123
|
+
syncedIssuesCount,
|
|
9124
|
+
createdIssuesCount,
|
|
9125
|
+
skippedIssuesCount,
|
|
9126
|
+
erroredIssuesCount: recoverableFailures.length,
|
|
9127
|
+
progress: currentProgress
|
|
9128
|
+
})
|
|
9129
|
+
};
|
|
9130
|
+
await ctx.state.set(SETTINGS_SCOPE, next2);
|
|
9131
|
+
await ctx.state.set(SYNC_STATE_SCOPE, next2.syncState);
|
|
9132
|
+
await ctx.state.set(IMPORT_REGISTRY_SCOPE, nextRegistry);
|
|
9133
|
+
return next2;
|
|
9134
|
+
}
|
|
5577
9135
|
const errorDetails = buildSyncErrorDetails(error, failureContext);
|
|
5578
9136
|
const next = {
|
|
5579
9137
|
...currentSettings,
|
|
@@ -5585,7 +9143,11 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
5585
9143
|
skippedIssuesCount,
|
|
5586
9144
|
erroredIssuesCount: recoverableFailures.length,
|
|
5587
9145
|
progress: currentProgress,
|
|
5588
|
-
errorDetails
|
|
9146
|
+
errorDetails,
|
|
9147
|
+
recentFailures: appendRecentSyncFailureLogEntry(
|
|
9148
|
+
buildRecentSyncFailureLogEntries(recoverableFailures),
|
|
9149
|
+
buildSyncFailureLogEntry(error, failureContext)
|
|
9150
|
+
)
|
|
5589
9151
|
})
|
|
5590
9152
|
};
|
|
5591
9153
|
await ctx.state.set(SETTINGS_SCOPE, next);
|
|
@@ -5639,6 +9201,7 @@ async function startSync(ctx, trigger, options = {}) {
|
|
|
5639
9201
|
if (trigger !== "manual" && !token.trim()) {
|
|
5640
9202
|
return currentSettings;
|
|
5641
9203
|
}
|
|
9204
|
+
await setSyncCancellationRequest(ctx, null);
|
|
5642
9205
|
const runningStatePromise = (async () => {
|
|
5643
9206
|
const syncableMappings = getSyncableMappingsForTarget(currentSettings.mappings, options.target);
|
|
5644
9207
|
const syncState = createRunningSyncState(currentSettings.syncState, trigger, {
|
|
@@ -5668,6 +9231,7 @@ async function startSync(ctx, trigger, options = {}) {
|
|
|
5668
9231
|
} catch (error) {
|
|
5669
9232
|
return await createUnexpectedSyncErrorResult(ctx, trigger, error);
|
|
5670
9233
|
} finally {
|
|
9234
|
+
await setSyncCancellationRequest(ctx, null);
|
|
5671
9235
|
activePaperclipApiAuthTokensByCompanyId = null;
|
|
5672
9236
|
activeRunningSyncState = null;
|
|
5673
9237
|
activeSyncPromise = null;
|
|
@@ -6396,6 +9960,30 @@ var plugin = definePlugin({
|
|
|
6396
9960
|
const record = input && typeof input === "object" ? input : {};
|
|
6397
9961
|
return buildToolbarSyncState(ctx, record);
|
|
6398
9962
|
});
|
|
9963
|
+
ctx.data.register("settings.tokenPermissionAudit", async (input) => {
|
|
9964
|
+
const record = input && typeof input === "object" ? input : {};
|
|
9965
|
+
return buildSettingsTokenPermissionAuditData(ctx, record);
|
|
9966
|
+
});
|
|
9967
|
+
ctx.data.register("project.pullRequests.page", async (input) => {
|
|
9968
|
+
const record = input && typeof input === "object" ? input : {};
|
|
9969
|
+
return buildProjectPullRequestsPageData(ctx, record);
|
|
9970
|
+
});
|
|
9971
|
+
ctx.data.register("project.pullRequests.metrics", async (input) => {
|
|
9972
|
+
const record = input && typeof input === "object" ? input : {};
|
|
9973
|
+
return buildProjectPullRequestMetricsData(ctx, record);
|
|
9974
|
+
});
|
|
9975
|
+
ctx.data.register("project.pullRequests.count", async (input) => {
|
|
9976
|
+
const record = input && typeof input === "object" ? input : {};
|
|
9977
|
+
return buildProjectPullRequestCountData(ctx, record);
|
|
9978
|
+
});
|
|
9979
|
+
ctx.data.register("project.pullRequests.detail", async (input) => {
|
|
9980
|
+
const record = input && typeof input === "object" ? input : {};
|
|
9981
|
+
return buildProjectPullRequestDetailData(ctx, record);
|
|
9982
|
+
});
|
|
9983
|
+
ctx.data.register("project.pullRequests.paperclipIssue", async (input) => {
|
|
9984
|
+
const record = input && typeof input === "object" ? input : {};
|
|
9985
|
+
return buildProjectPullRequestPaperclipIssueData(ctx, record);
|
|
9986
|
+
});
|
|
6399
9987
|
ctx.data.register("issue.githubDetails", async (input) => {
|
|
6400
9988
|
const record = input && typeof input === "object" ? input : {};
|
|
6401
9989
|
return buildIssueGitHubDetails(ctx, record);
|
|
@@ -6461,6 +10049,7 @@ var plugin = definePlugin({
|
|
|
6461
10049
|
});
|
|
6462
10050
|
await ctx.state.set(SETTINGS_SCOPE, next);
|
|
6463
10051
|
await ctx.state.set(SYNC_STATE_SCOPE, next.syncState);
|
|
10052
|
+
clearGitHubRepositoryTokenCapabilityAudits();
|
|
6464
10053
|
return {
|
|
6465
10054
|
...getPublicSettingsForScope(next, requestedCompanyId),
|
|
6466
10055
|
availableAssignees: requestedCompanyId ? await listAvailableAssignees(ctx, requestedCompanyId) : []
|
|
@@ -6510,6 +10099,42 @@ var plugin = definePlugin({
|
|
|
6510
10099
|
}
|
|
6511
10100
|
return validateGithubToken(trimmedToken);
|
|
6512
10101
|
});
|
|
10102
|
+
ctx.actions.register("project.pullRequests.createIssue", async (input) => {
|
|
10103
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10104
|
+
return createProjectPullRequestPaperclipIssue(ctx, record);
|
|
10105
|
+
});
|
|
10106
|
+
ctx.actions.register("project.pullRequests.refresh", async (input) => {
|
|
10107
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10108
|
+
return refreshProjectPullRequests(ctx, record);
|
|
10109
|
+
});
|
|
10110
|
+
ctx.actions.register("project.pullRequests.updateBranch", async (input) => {
|
|
10111
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10112
|
+
return updateProjectPullRequestBranch(ctx, record);
|
|
10113
|
+
});
|
|
10114
|
+
ctx.actions.register("project.pullRequests.requestCopilotAction", async (input) => {
|
|
10115
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10116
|
+
return requestProjectPullRequestCopilotAction(ctx, record);
|
|
10117
|
+
});
|
|
10118
|
+
ctx.actions.register("project.pullRequests.merge", async (input) => {
|
|
10119
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10120
|
+
return mergeProjectPullRequest(ctx, record);
|
|
10121
|
+
});
|
|
10122
|
+
ctx.actions.register("project.pullRequests.close", async (input) => {
|
|
10123
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10124
|
+
return closeProjectPullRequest(ctx, record);
|
|
10125
|
+
});
|
|
10126
|
+
ctx.actions.register("project.pullRequests.addComment", async (input) => {
|
|
10127
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10128
|
+
return addProjectPullRequestComment(ctx, record);
|
|
10129
|
+
});
|
|
10130
|
+
ctx.actions.register("project.pullRequests.review", async (input) => {
|
|
10131
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10132
|
+
return reviewProjectPullRequest(ctx, record);
|
|
10133
|
+
});
|
|
10134
|
+
ctx.actions.register("project.pullRequests.rerunCi", async (input) => {
|
|
10135
|
+
const record = input && typeof input === "object" ? input : {};
|
|
10136
|
+
return rerunProjectPullRequestCi(ctx, record);
|
|
10137
|
+
});
|
|
6513
10138
|
ctx.actions.register("sync.runNow", async (input) => {
|
|
6514
10139
|
const waitForCompletion = input && typeof input === "object" && "waitForCompletion" in input ? Boolean(input.waitForCompletion) : false;
|
|
6515
10140
|
const paperclipApiBaseUrl = input && typeof input === "object" && "paperclipApiBaseUrl" in input ? input.paperclipApiBaseUrl : void 0;
|
|
@@ -6528,6 +10153,32 @@ var plugin = definePlugin({
|
|
|
6528
10153
|
...target ? { target } : {}
|
|
6529
10154
|
});
|
|
6530
10155
|
});
|
|
10156
|
+
ctx.actions.register("sync.cancel", async () => {
|
|
10157
|
+
const currentSettings = await getActiveOrCurrentSyncState(ctx);
|
|
10158
|
+
if (currentSettings.syncState.status !== "running") {
|
|
10159
|
+
return currentSettings;
|
|
10160
|
+
}
|
|
10161
|
+
const existingRequest = currentSettings.syncState.cancelRequestedAt?.trim() ? { requestedAt: currentSettings.syncState.cancelRequestedAt.trim() } : await getSyncCancellationRequest(ctx);
|
|
10162
|
+
const cancellationRequest = existingRequest ?? {
|
|
10163
|
+
requestedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10164
|
+
};
|
|
10165
|
+
await setSyncCancellationRequest(ctx, cancellationRequest);
|
|
10166
|
+
const next = await saveSettingsSyncState(
|
|
10167
|
+
ctx,
|
|
10168
|
+
currentSettings,
|
|
10169
|
+
createRunningSyncState(currentSettings.syncState, currentSettings.syncState.lastRunTrigger ?? "manual", {
|
|
10170
|
+
syncedIssuesCount: currentSettings.syncState.syncedIssuesCount ?? 0,
|
|
10171
|
+
createdIssuesCount: currentSettings.syncState.createdIssuesCount ?? 0,
|
|
10172
|
+
skippedIssuesCount: currentSettings.syncState.skippedIssuesCount ?? 0,
|
|
10173
|
+
erroredIssuesCount: currentSettings.syncState.erroredIssuesCount ?? 0,
|
|
10174
|
+
progress: currentSettings.syncState.progress,
|
|
10175
|
+
message: CANCELLING_SYNC_MESSAGE,
|
|
10176
|
+
cancelRequestedAt: cancellationRequest.requestedAt
|
|
10177
|
+
})
|
|
10178
|
+
);
|
|
10179
|
+
activeRunningSyncState = next;
|
|
10180
|
+
return next;
|
|
10181
|
+
});
|
|
6531
10182
|
registerGitHubAgentTools(ctx);
|
|
6532
10183
|
ctx.jobs.register("sync.github-issues", async (job) => {
|
|
6533
10184
|
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|