github-issue-tower-defence-management 1.124.0 → 1.125.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/CHANGELOG.md +7 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +267 -276
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +663 -328
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +454 -421
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +6 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
|
|
37
37
|
export const FULL_ISSUE_FETCH_INTERVAL_MS = 60 * 60 * 1000;
|
|
38
38
|
export const INCREMENTAL_FETCH_SKEW_BUFFER_MS = 5 * 60 * 1000;
|
|
39
|
+
export const REQUIRED_CHECKS_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
39
40
|
|
|
40
41
|
export type CachedProjectIssues = {
|
|
41
42
|
lastFetchedAt: string;
|
|
@@ -57,69 +58,6 @@ type TimelineItem = {
|
|
|
57
58
|
mergeable?: string;
|
|
58
59
|
headRefName?: string;
|
|
59
60
|
baseRefName?: string;
|
|
60
|
-
baseRepository?: {
|
|
61
|
-
branchProtectionRules?: {
|
|
62
|
-
nodes: Array<{
|
|
63
|
-
pattern: string;
|
|
64
|
-
requiredStatusCheckContexts: string[];
|
|
65
|
-
}>;
|
|
66
|
-
};
|
|
67
|
-
defaultBranchRef?: {
|
|
68
|
-
name: string;
|
|
69
|
-
} | null;
|
|
70
|
-
rulesets?: {
|
|
71
|
-
nodes: Array<{
|
|
72
|
-
name: string;
|
|
73
|
-
enforcement: string;
|
|
74
|
-
conditions: {
|
|
75
|
-
refName: {
|
|
76
|
-
include: string[];
|
|
77
|
-
exclude: string[];
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
rules: {
|
|
81
|
-
nodes: Array<{
|
|
82
|
-
type: string;
|
|
83
|
-
parameters:
|
|
84
|
-
| {
|
|
85
|
-
requiredStatusChecks: Array<{
|
|
86
|
-
context: string;
|
|
87
|
-
}>;
|
|
88
|
-
}
|
|
89
|
-
| Record<string, never>;
|
|
90
|
-
}>;
|
|
91
|
-
};
|
|
92
|
-
}>;
|
|
93
|
-
};
|
|
94
|
-
};
|
|
95
|
-
commits?: {
|
|
96
|
-
nodes: Array<{
|
|
97
|
-
commit: {
|
|
98
|
-
statusCheckRollup?: {
|
|
99
|
-
contexts?: {
|
|
100
|
-
nodes: Array<
|
|
101
|
-
| {
|
|
102
|
-
__typename: 'CheckRun';
|
|
103
|
-
name: string;
|
|
104
|
-
conclusion: string | null;
|
|
105
|
-
databaseId: number;
|
|
106
|
-
}
|
|
107
|
-
| {
|
|
108
|
-
__typename: 'StatusContext';
|
|
109
|
-
context: string;
|
|
110
|
-
state: string;
|
|
111
|
-
}
|
|
112
|
-
>;
|
|
113
|
-
};
|
|
114
|
-
} | null;
|
|
115
|
-
};
|
|
116
|
-
}>;
|
|
117
|
-
};
|
|
118
|
-
reviewThreads?: {
|
|
119
|
-
nodes: Array<{
|
|
120
|
-
isResolved: boolean;
|
|
121
|
-
}>;
|
|
122
|
-
};
|
|
123
61
|
baseRef?: {
|
|
124
62
|
name: string;
|
|
125
63
|
} | null;
|
|
@@ -143,88 +81,101 @@ type IssueTimelineResponse = {
|
|
|
143
81
|
errors?: Array<{ message: string }>;
|
|
144
82
|
};
|
|
145
83
|
|
|
84
|
+
type CiContextNode =
|
|
85
|
+
| {
|
|
86
|
+
__typename: 'CheckRun';
|
|
87
|
+
name: string;
|
|
88
|
+
conclusion: string | null;
|
|
89
|
+
databaseId: number;
|
|
90
|
+
}
|
|
91
|
+
| {
|
|
92
|
+
__typename: 'StatusContext';
|
|
93
|
+
context: string;
|
|
94
|
+
state: string;
|
|
95
|
+
};
|
|
96
|
+
|
|
146
97
|
type PrStatusComputationData = {
|
|
147
98
|
isDraft?: boolean;
|
|
148
99
|
mergeable?: string;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}>;
|
|
155
|
-
};
|
|
156
|
-
defaultBranchRef?: {
|
|
157
|
-
name: string;
|
|
158
|
-
} | null;
|
|
159
|
-
rulesets?: {
|
|
160
|
-
nodes: Array<{
|
|
161
|
-
name: string;
|
|
162
|
-
enforcement: string;
|
|
163
|
-
conditions: {
|
|
164
|
-
refName: {
|
|
165
|
-
include: string[];
|
|
166
|
-
exclude: string[];
|
|
167
|
-
};
|
|
168
|
-
};
|
|
169
|
-
rules: {
|
|
170
|
-
nodes: Array<{
|
|
171
|
-
type: string;
|
|
172
|
-
parameters:
|
|
173
|
-
| {
|
|
174
|
-
requiredStatusChecks: Array<{
|
|
175
|
-
context: string;
|
|
176
|
-
}>;
|
|
177
|
-
}
|
|
178
|
-
| Record<string, never>;
|
|
179
|
-
}>;
|
|
180
|
-
};
|
|
181
|
-
}>;
|
|
182
|
-
};
|
|
183
|
-
};
|
|
184
|
-
commits?: {
|
|
185
|
-
nodes: Array<{
|
|
186
|
-
commit: {
|
|
187
|
-
statusCheckRollup?: {
|
|
188
|
-
contexts?: {
|
|
189
|
-
nodes: Array<
|
|
190
|
-
| {
|
|
191
|
-
__typename: 'CheckRun';
|
|
192
|
-
name: string;
|
|
193
|
-
conclusion: string | null;
|
|
194
|
-
databaseId: number;
|
|
195
|
-
}
|
|
196
|
-
| {
|
|
197
|
-
__typename: 'StatusContext';
|
|
198
|
-
context: string;
|
|
199
|
-
state: string;
|
|
200
|
-
}
|
|
201
|
-
>;
|
|
202
|
-
};
|
|
203
|
-
} | null;
|
|
204
|
-
};
|
|
205
|
-
}>;
|
|
206
|
-
};
|
|
207
|
-
reviewThreads?: {
|
|
208
|
-
nodes: Array<{
|
|
209
|
-
isResolved: boolean;
|
|
210
|
-
}>;
|
|
211
|
-
};
|
|
100
|
+
requiredCheckNames: string[];
|
|
101
|
+
ciContexts: CiContextNode[];
|
|
102
|
+
reviewThreads: Array<{
|
|
103
|
+
isResolved: boolean;
|
|
104
|
+
}>;
|
|
212
105
|
};
|
|
213
106
|
|
|
214
|
-
type
|
|
107
|
+
type SlimPullRequestResponse = {
|
|
215
108
|
data?: {
|
|
216
109
|
repository?: {
|
|
217
110
|
pullRequest?: {
|
|
218
111
|
url: string;
|
|
219
112
|
state: string;
|
|
113
|
+
isDraft?: boolean;
|
|
220
114
|
headRefName?: string;
|
|
221
115
|
baseRefName?: string;
|
|
222
|
-
|
|
223
|
-
|
|
116
|
+
mergeable?: string;
|
|
117
|
+
headRefOid?: string;
|
|
118
|
+
reviewThreads?: {
|
|
119
|
+
pageInfo: {
|
|
120
|
+
endCursor: string | null;
|
|
121
|
+
hasNextPage: boolean;
|
|
122
|
+
};
|
|
123
|
+
nodes: Array<{
|
|
124
|
+
isResolved: boolean;
|
|
125
|
+
}>;
|
|
126
|
+
};
|
|
127
|
+
} | null;
|
|
128
|
+
} | null;
|
|
224
129
|
};
|
|
225
130
|
errors?: Array<{ message: string }>;
|
|
226
131
|
};
|
|
227
132
|
|
|
133
|
+
type SlimPullRequest = {
|
|
134
|
+
url: string;
|
|
135
|
+
state: string;
|
|
136
|
+
isDraft?: boolean;
|
|
137
|
+
headRefName?: string;
|
|
138
|
+
baseRefName?: string;
|
|
139
|
+
mergeable?: string;
|
|
140
|
+
headRefOid?: string;
|
|
141
|
+
reviewThreads: Array<{
|
|
142
|
+
isResolved: boolean;
|
|
143
|
+
}>;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
type BranchRulesResponseItem = {
|
|
147
|
+
type: string;
|
|
148
|
+
parameters?: {
|
|
149
|
+
required_status_checks?: Array<{
|
|
150
|
+
context: string;
|
|
151
|
+
}>;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
type BranchDetailResponse = {
|
|
156
|
+
protection?: {
|
|
157
|
+
required_status_checks?: {
|
|
158
|
+
contexts?: string[];
|
|
159
|
+
} | null;
|
|
160
|
+
} | null;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
type CheckRunsResponse = {
|
|
164
|
+
total_count: number;
|
|
165
|
+
check_runs: Array<{
|
|
166
|
+
id: number;
|
|
167
|
+
name: string;
|
|
168
|
+
conclusion: string | null;
|
|
169
|
+
}>;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
type CombinedStatusResponse = {
|
|
173
|
+
statuses: Array<{
|
|
174
|
+
context: string;
|
|
175
|
+
state: string;
|
|
176
|
+
}>;
|
|
177
|
+
};
|
|
178
|
+
|
|
228
179
|
type PullRequestMergeabilityResponse = {
|
|
229
180
|
data?: {
|
|
230
181
|
repository?: {
|
|
@@ -244,13 +195,36 @@ function isIssueTimelineResponse(
|
|
|
244
195
|
return true;
|
|
245
196
|
}
|
|
246
197
|
|
|
247
|
-
function
|
|
198
|
+
function isSlimPullRequestResponse(
|
|
248
199
|
value: unknown,
|
|
249
|
-
): value is
|
|
200
|
+
): value is SlimPullRequestResponse {
|
|
250
201
|
if (typeof value !== 'object' || value === null) return false;
|
|
251
202
|
return true;
|
|
252
203
|
}
|
|
253
204
|
|
|
205
|
+
function isBranchRulesResponse(
|
|
206
|
+
value: unknown,
|
|
207
|
+
): value is BranchRulesResponseItem[] {
|
|
208
|
+
return Array.isArray(value);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isBranchDetailResponse(value: unknown): value is BranchDetailResponse {
|
|
212
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isCheckRunsResponse(value: unknown): value is CheckRunsResponse {
|
|
217
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
218
|
+
return 'check_runs' in value && Array.isArray(value.check_runs);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isCombinedStatusResponse(
|
|
222
|
+
value: unknown,
|
|
223
|
+
): value is CombinedStatusResponse {
|
|
224
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
225
|
+
return 'statuses' in value && Array.isArray(value.statuses);
|
|
226
|
+
}
|
|
227
|
+
|
|
254
228
|
function isPullRequestMergeabilityResponse(
|
|
255
229
|
value: unknown,
|
|
256
230
|
): value is PullRequestMergeabilityResponse {
|
|
@@ -428,58 +402,6 @@ function isPullRequestCommitsResponse(
|
|
|
428
402
|
return Array.isArray(value) && value.every(isPullRequestCommitsResponseItem);
|
|
429
403
|
}
|
|
430
404
|
|
|
431
|
-
const fnmatch = (pattern: string, str: string): boolean => {
|
|
432
|
-
let regexStr = '^';
|
|
433
|
-
let i = 0;
|
|
434
|
-
while (i < pattern.length) {
|
|
435
|
-
const c = pattern[i];
|
|
436
|
-
if (c === '*') {
|
|
437
|
-
if (pattern[i + 1] === '*') {
|
|
438
|
-
regexStr += '.*';
|
|
439
|
-
i += 2;
|
|
440
|
-
if (pattern[i] === '/') {
|
|
441
|
-
i++;
|
|
442
|
-
}
|
|
443
|
-
} else {
|
|
444
|
-
regexStr += '[^/]*';
|
|
445
|
-
i++;
|
|
446
|
-
}
|
|
447
|
-
} else if (c === '?') {
|
|
448
|
-
regexStr += '[^/]';
|
|
449
|
-
i++;
|
|
450
|
-
} else if (c === '[') {
|
|
451
|
-
let j = i + 1;
|
|
452
|
-
while (j < pattern.length && pattern[j] !== ']') {
|
|
453
|
-
j++;
|
|
454
|
-
}
|
|
455
|
-
if (j >= pattern.length) {
|
|
456
|
-
regexStr += '\\[';
|
|
457
|
-
i++;
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
const content = pattern.slice(i + 1, j);
|
|
461
|
-
if (content.length > 0 && (content[0] === '!' || content[0] === '^')) {
|
|
462
|
-
const body = content.slice(1).replace(/\\/g, '\\\\');
|
|
463
|
-
regexStr += '[^' + body + ']';
|
|
464
|
-
} else {
|
|
465
|
-
const escapedContent = content.replace(/\\/g, '\\\\');
|
|
466
|
-
regexStr += '[' + escapedContent + ']';
|
|
467
|
-
}
|
|
468
|
-
i = j + 1;
|
|
469
|
-
} else {
|
|
470
|
-
regexStr += c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
471
|
-
i++;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
regexStr += '$';
|
|
475
|
-
try {
|
|
476
|
-
const regex = new RegExp(regexStr);
|
|
477
|
-
return regex.test(str);
|
|
478
|
-
} catch {
|
|
479
|
-
return pattern === str;
|
|
480
|
-
}
|
|
481
|
-
};
|
|
482
|
-
|
|
483
405
|
export class ApiV3CheerioRestIssueRepository
|
|
484
406
|
extends BaseGitHubRepository
|
|
485
407
|
implements IssueRepository
|
|
@@ -1000,73 +922,13 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1000
922
|
private computePrStatus = (
|
|
1001
923
|
prUrl: string,
|
|
1002
924
|
headRefName: string | undefined,
|
|
1003
|
-
baseRefName: string | undefined,
|
|
1004
925
|
data: PrStatusComputationData,
|
|
1005
926
|
): RelatedPullRequest => {
|
|
1006
927
|
const isConflicted = data.mergeable === 'CONFLICTING';
|
|
1007
|
-
const
|
|
1008
|
-
const
|
|
1009
|
-
const contexts = statusCheckRollup?.contexts?.nodes || [];
|
|
1010
|
-
|
|
1011
|
-
const branchProtectionRules =
|
|
1012
|
-
data.baseRepository?.branchProtectionRules?.nodes || [];
|
|
1013
|
-
const matchingRules = baseRefName
|
|
1014
|
-
? branchProtectionRules.filter(
|
|
1015
|
-
(rule) =>
|
|
1016
|
-
rule.pattern === baseRefName || fnmatch(rule.pattern, baseRefName),
|
|
1017
|
-
)
|
|
1018
|
-
: [];
|
|
1019
|
-
const requiredCheckNamesSet = new Set<string>();
|
|
1020
|
-
for (const rule of matchingRules) {
|
|
1021
|
-
for (const name of rule.requiredStatusCheckContexts) {
|
|
1022
|
-
requiredCheckNamesSet.add(name);
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
const rulesets = data.baseRepository?.rulesets?.nodes || [];
|
|
1027
|
-
const defaultBranchName = data.baseRepository?.defaultBranchRef?.name || '';
|
|
1028
|
-
for (const ruleset of rulesets) {
|
|
1029
|
-
if (ruleset.enforcement !== 'ACTIVE') continue;
|
|
1030
|
-
const refIncludes = ruleset.conditions.refName.include;
|
|
1031
|
-
const refExcludes = ruleset.conditions.refName.exclude;
|
|
1032
|
-
const matchesInclude =
|
|
1033
|
-
baseRefName !== undefined &&
|
|
1034
|
-
refIncludes.some((pattern) => {
|
|
1035
|
-
if (pattern === '~DEFAULT_BRANCH') {
|
|
1036
|
-
return baseRefName === defaultBranchName;
|
|
1037
|
-
}
|
|
1038
|
-
if (pattern === '~ALL') {
|
|
1039
|
-
return true;
|
|
1040
|
-
}
|
|
1041
|
-
const branchPattern = pattern.replace(/^refs\/heads\//, '');
|
|
1042
|
-
return (
|
|
1043
|
-
branchPattern === baseRefName || fnmatch(branchPattern, baseRefName)
|
|
1044
|
-
);
|
|
1045
|
-
});
|
|
1046
|
-
if (!matchesInclude) continue;
|
|
1047
|
-
const matchesExclude =
|
|
1048
|
-
baseRefName !== undefined &&
|
|
1049
|
-
refExcludes.some((pattern) => {
|
|
1050
|
-
if (pattern === '~DEFAULT_BRANCH') {
|
|
1051
|
-
return baseRefName === defaultBranchName;
|
|
1052
|
-
}
|
|
1053
|
-
const branchPattern = pattern.replace(/^refs\/heads\//, '');
|
|
1054
|
-
return (
|
|
1055
|
-
branchPattern === baseRefName || fnmatch(branchPattern, baseRefName)
|
|
1056
|
-
);
|
|
1057
|
-
});
|
|
1058
|
-
if (matchesExclude) continue;
|
|
1059
|
-
for (const rule of ruleset.rules.nodes) {
|
|
1060
|
-
if (rule.type !== 'REQUIRED_STATUS_CHECKS') continue;
|
|
1061
|
-
if ('requiredStatusChecks' in rule.parameters) {
|
|
1062
|
-
for (const check of rule.parameters.requiredStatusChecks) {
|
|
1063
|
-
requiredCheckNamesSet.add(check.context);
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
928
|
+
const hasStatusCheckRollup = data.ciContexts.length > 0;
|
|
929
|
+
const contexts = data.ciContexts;
|
|
1068
930
|
|
|
1069
|
-
const requiredCheckNames =
|
|
931
|
+
const requiredCheckNames = data.requiredCheckNames;
|
|
1070
932
|
const seenContextNames = new Set<string>();
|
|
1071
933
|
for (const ctx of contexts) {
|
|
1072
934
|
if ('name' in ctx) {
|
|
@@ -1106,7 +968,7 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1106
968
|
'STALE',
|
|
1107
969
|
]);
|
|
1108
970
|
const isCiStateSuccess = (() => {
|
|
1109
|
-
if (!
|
|
971
|
+
if (!hasStatusCheckRollup) return false;
|
|
1110
972
|
const latestRuns = [...latestCheckRunByName.values()];
|
|
1111
973
|
const statusContexts = contexts.filter(
|
|
1112
974
|
(
|
|
@@ -1132,7 +994,7 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1132
994
|
})();
|
|
1133
995
|
const isPassedAllCiJob = isCiStateSuccess && allRequiredChecksPassed;
|
|
1134
996
|
|
|
1135
|
-
const reviewThreads = data.reviewThreads
|
|
997
|
+
const reviewThreads = data.reviewThreads;
|
|
1136
998
|
const isResolvedAllReviewComments =
|
|
1137
999
|
reviewThreads.length === 0 ||
|
|
1138
1000
|
reviewThreads.every((thread) => thread.isResolved);
|
|
@@ -1152,6 +1014,310 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1152
1014
|
};
|
|
1153
1015
|
};
|
|
1154
1016
|
|
|
1017
|
+
private readonly requiredCheckNamesCache = new Map<
|
|
1018
|
+
string,
|
|
1019
|
+
{ fetchedAtMs: number; names: string[] }
|
|
1020
|
+
>();
|
|
1021
|
+
|
|
1022
|
+
private getRequiredCheckNames = async (
|
|
1023
|
+
owner: string,
|
|
1024
|
+
repo: string,
|
|
1025
|
+
branch: string,
|
|
1026
|
+
): Promise<string[]> => {
|
|
1027
|
+
const cacheKey = `${owner}/${repo}/${branch}`;
|
|
1028
|
+
const nowMs = (await this.dateRepository.now()).getTime();
|
|
1029
|
+
const cached = this.requiredCheckNamesCache.get(cacheKey);
|
|
1030
|
+
if (cached && nowMs - cached.fetchedAtMs < REQUIRED_CHECKS_CACHE_TTL_MS) {
|
|
1031
|
+
return cached.names;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const ownerSegment = encodeURIComponent(owner);
|
|
1035
|
+
const repoSegment = encodeURIComponent(repo);
|
|
1036
|
+
const branchSegment = encodeURIComponent(branch);
|
|
1037
|
+
const requiredCheckNamesSet = new Set<string>();
|
|
1038
|
+
|
|
1039
|
+
const rulesResponse = await this.fetchWithRateLimitRetry(() =>
|
|
1040
|
+
fetch(
|
|
1041
|
+
`https://api.github.com/repos/${ownerSegment}/${repoSegment}/rules/branches/${branchSegment}?per_page=100`,
|
|
1042
|
+
{
|
|
1043
|
+
method: 'GET',
|
|
1044
|
+
headers: {
|
|
1045
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1046
|
+
Accept: 'application/vnd.github+json',
|
|
1047
|
+
},
|
|
1048
|
+
},
|
|
1049
|
+
),
|
|
1050
|
+
);
|
|
1051
|
+
if (rulesResponse.ok) {
|
|
1052
|
+
const rulesBody: unknown = await rulesResponse.json();
|
|
1053
|
+
if (!isBranchRulesResponse(rulesBody)) {
|
|
1054
|
+
throw new Error(
|
|
1055
|
+
`Unexpected response shape when fetching branch rules: ${owner}/${repo}/${branch}`,
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
for (const rule of rulesBody) {
|
|
1059
|
+
if (rule.type !== 'required_status_checks') continue;
|
|
1060
|
+
for (const check of rule.parameters?.required_status_checks || []) {
|
|
1061
|
+
requiredCheckNamesSet.add(check.context);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
} else if (rulesResponse.status !== 404) {
|
|
1065
|
+
const reason = await this.formatGitHubErrorWithStatus(rulesResponse);
|
|
1066
|
+
throw new Error(
|
|
1067
|
+
`Failed to fetch branch rules for ${owner}/${repo}/${branch}: ${reason}`,
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const branchResponse = await this.fetchWithRateLimitRetry(() =>
|
|
1072
|
+
fetch(
|
|
1073
|
+
`https://api.github.com/repos/${ownerSegment}/${repoSegment}/branches/${branchSegment}`,
|
|
1074
|
+
{
|
|
1075
|
+
method: 'GET',
|
|
1076
|
+
headers: {
|
|
1077
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1078
|
+
Accept: 'application/vnd.github+json',
|
|
1079
|
+
},
|
|
1080
|
+
},
|
|
1081
|
+
),
|
|
1082
|
+
);
|
|
1083
|
+
if (branchResponse.ok) {
|
|
1084
|
+
const branchBody: unknown = await branchResponse.json();
|
|
1085
|
+
if (!isBranchDetailResponse(branchBody)) {
|
|
1086
|
+
throw new Error(
|
|
1087
|
+
`Unexpected response shape when fetching branch detail: ${owner}/${repo}/${branch}`,
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
for (const context of branchBody.protection?.required_status_checks
|
|
1091
|
+
?.contexts || []) {
|
|
1092
|
+
requiredCheckNamesSet.add(context);
|
|
1093
|
+
}
|
|
1094
|
+
} else if (branchResponse.status !== 404) {
|
|
1095
|
+
const reason = await this.formatGitHubErrorWithStatus(branchResponse);
|
|
1096
|
+
throw new Error(
|
|
1097
|
+
`Failed to fetch branch detail for ${owner}/${repo}/${branch}: ${reason}`,
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const names = Array.from(requiredCheckNamesSet);
|
|
1102
|
+
this.requiredCheckNamesCache.set(cacheKey, {
|
|
1103
|
+
fetchedAtMs: nowMs,
|
|
1104
|
+
names,
|
|
1105
|
+
});
|
|
1106
|
+
return names;
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
private getCommitCiContexts = async (
|
|
1110
|
+
owner: string,
|
|
1111
|
+
repo: string,
|
|
1112
|
+
commitSha: string,
|
|
1113
|
+
): Promise<CiContextNode[]> => {
|
|
1114
|
+
const ownerSegment = encodeURIComponent(owner);
|
|
1115
|
+
const repoSegment = encodeURIComponent(repo);
|
|
1116
|
+
const shaSegment = encodeURIComponent(commitSha);
|
|
1117
|
+
const contexts: CiContextNode[] = [];
|
|
1118
|
+
|
|
1119
|
+
const perPage = 100;
|
|
1120
|
+
let page = 1;
|
|
1121
|
+
let hasMore = true;
|
|
1122
|
+
while (hasMore) {
|
|
1123
|
+
const checkRunsResponse = await this.fetchWithRateLimitRetry(() =>
|
|
1124
|
+
fetch(
|
|
1125
|
+
`https://api.github.com/repos/${ownerSegment}/${repoSegment}/commits/${shaSegment}/check-runs?per_page=${perPage}&page=${page}`,
|
|
1126
|
+
{
|
|
1127
|
+
method: 'GET',
|
|
1128
|
+
headers: {
|
|
1129
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1130
|
+
Accept: 'application/vnd.github+json',
|
|
1131
|
+
},
|
|
1132
|
+
},
|
|
1133
|
+
),
|
|
1134
|
+
);
|
|
1135
|
+
if (!checkRunsResponse.ok) {
|
|
1136
|
+
const reason =
|
|
1137
|
+
await this.formatGitHubErrorWithStatus(checkRunsResponse);
|
|
1138
|
+
throw new Error(
|
|
1139
|
+
`Failed to fetch check runs for ${owner}/${repo}@${commitSha}: ${reason}`,
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
const checkRunsBody: unknown = await checkRunsResponse.json();
|
|
1143
|
+
if (!isCheckRunsResponse(checkRunsBody)) {
|
|
1144
|
+
throw new Error(
|
|
1145
|
+
`Unexpected response shape when fetching check runs: ${owner}/${repo}@${commitSha}`,
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
for (const checkRun of checkRunsBody.check_runs) {
|
|
1149
|
+
contexts.push({
|
|
1150
|
+
__typename: 'CheckRun',
|
|
1151
|
+
name: checkRun.name,
|
|
1152
|
+
conclusion: checkRun.conclusion
|
|
1153
|
+
? checkRun.conclusion.toUpperCase()
|
|
1154
|
+
: null,
|
|
1155
|
+
databaseId: checkRun.id,
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
if (
|
|
1159
|
+
checkRunsBody.check_runs.length < perPage ||
|
|
1160
|
+
page * perPage >= checkRunsBody.total_count
|
|
1161
|
+
) {
|
|
1162
|
+
hasMore = false;
|
|
1163
|
+
} else {
|
|
1164
|
+
page += 1;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const combinedStatusResponse = await this.fetchWithRateLimitRetry(() =>
|
|
1169
|
+
fetch(
|
|
1170
|
+
`https://api.github.com/repos/${ownerSegment}/${repoSegment}/commits/${shaSegment}/status?per_page=100`,
|
|
1171
|
+
{
|
|
1172
|
+
method: 'GET',
|
|
1173
|
+
headers: {
|
|
1174
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1175
|
+
Accept: 'application/vnd.github+json',
|
|
1176
|
+
},
|
|
1177
|
+
},
|
|
1178
|
+
),
|
|
1179
|
+
);
|
|
1180
|
+
if (!combinedStatusResponse.ok) {
|
|
1181
|
+
const reason = await this.formatGitHubErrorWithStatus(
|
|
1182
|
+
combinedStatusResponse,
|
|
1183
|
+
);
|
|
1184
|
+
throw new Error(
|
|
1185
|
+
`Failed to fetch combined status for ${owner}/${repo}@${commitSha}: ${reason}`,
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
const combinedStatusBody: unknown = await combinedStatusResponse.json();
|
|
1189
|
+
if (!isCombinedStatusResponse(combinedStatusBody)) {
|
|
1190
|
+
throw new Error(
|
|
1191
|
+
`Unexpected response shape when fetching combined status: ${owner}/${repo}@${commitSha}`,
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
for (const status of combinedStatusBody.statuses) {
|
|
1195
|
+
contexts.push({
|
|
1196
|
+
__typename: 'StatusContext',
|
|
1197
|
+
context: status.context,
|
|
1198
|
+
state: status.state.toUpperCase(),
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
return contexts;
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
private fetchSlimPullRequest = async (
|
|
1206
|
+
owner: string,
|
|
1207
|
+
repo: string,
|
|
1208
|
+
prNumber: number,
|
|
1209
|
+
): Promise<SlimPullRequest | null> => {
|
|
1210
|
+
const query = `
|
|
1211
|
+
query PullRequestSlimStatus($owner: String!, $repo: String!, $prNumber: Int!, $reviewThreadsAfter: String) {
|
|
1212
|
+
repository(owner: $owner, name: $repo) {
|
|
1213
|
+
pullRequest(number: $prNumber) {
|
|
1214
|
+
url
|
|
1215
|
+
state
|
|
1216
|
+
isDraft
|
|
1217
|
+
headRefName
|
|
1218
|
+
baseRefName
|
|
1219
|
+
mergeable
|
|
1220
|
+
headRefOid
|
|
1221
|
+
reviewThreads(first: 100, after: $reviewThreadsAfter) {
|
|
1222
|
+
pageInfo {
|
|
1223
|
+
endCursor
|
|
1224
|
+
hasNextPage
|
|
1225
|
+
}
|
|
1226
|
+
nodes {
|
|
1227
|
+
isResolved
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
`;
|
|
1234
|
+
|
|
1235
|
+
let slimPullRequest: SlimPullRequest | null = null;
|
|
1236
|
+
let reviewThreadsAfter: string | null = null;
|
|
1237
|
+
let hasNextPage = true;
|
|
1238
|
+
|
|
1239
|
+
while (hasNextPage) {
|
|
1240
|
+
const response = await this.fetchWithRateLimitRetry(() =>
|
|
1241
|
+
fetchGithubGraphql({
|
|
1242
|
+
ghToken: this.ghToken,
|
|
1243
|
+
query,
|
|
1244
|
+
variables: { owner, repo, prNumber, reviewThreadsAfter },
|
|
1245
|
+
}),
|
|
1246
|
+
);
|
|
1247
|
+
|
|
1248
|
+
if (!response.ok) {
|
|
1249
|
+
throw new Error(
|
|
1250
|
+
`Failed to fetch pull request from GitHub GraphQL API: HTTP ${response.status}`,
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const responseData: unknown = await response.json();
|
|
1255
|
+
if (!isSlimPullRequestResponse(responseData)) {
|
|
1256
|
+
throw new Error('Unexpected response shape when fetching pull request');
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (responseData.errors && responseData.errors.length > 0) {
|
|
1260
|
+
throw new Error(
|
|
1261
|
+
`GraphQL errors: ${JSON.stringify(responseData.errors)}`,
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const pr = responseData.data?.repository?.pullRequest;
|
|
1266
|
+
if (!pr) {
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (!slimPullRequest) {
|
|
1271
|
+
slimPullRequest = {
|
|
1272
|
+
url: pr.url,
|
|
1273
|
+
state: pr.state,
|
|
1274
|
+
isDraft: pr.isDraft,
|
|
1275
|
+
headRefName: pr.headRefName,
|
|
1276
|
+
baseRefName: pr.baseRefName,
|
|
1277
|
+
mergeable: pr.mergeable,
|
|
1278
|
+
headRefOid: pr.headRefOid,
|
|
1279
|
+
reviewThreads: [],
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
for (const thread of pr.reviewThreads?.nodes || []) {
|
|
1283
|
+
slimPullRequest.reviewThreads.push({ isResolved: thread.isResolved });
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
hasNextPage = pr.reviewThreads?.pageInfo.hasNextPage === true;
|
|
1287
|
+
reviewThreadsAfter = pr.reviewThreads?.pageInfo.endCursor ?? null;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
return slimPullRequest;
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
private buildRelatedPullRequestFromSlim = async (
|
|
1294
|
+
owner: string,
|
|
1295
|
+
repo: string,
|
|
1296
|
+
slimPullRequest: SlimPullRequest,
|
|
1297
|
+
): Promise<RelatedPullRequest> => {
|
|
1298
|
+
const requiredCheckNames = slimPullRequest.baseRefName
|
|
1299
|
+
? await this.getRequiredCheckNames(
|
|
1300
|
+
owner,
|
|
1301
|
+
repo,
|
|
1302
|
+
slimPullRequest.baseRefName,
|
|
1303
|
+
)
|
|
1304
|
+
: [];
|
|
1305
|
+
const ciContexts = slimPullRequest.headRefOid
|
|
1306
|
+
? await this.getCommitCiContexts(owner, repo, slimPullRequest.headRefOid)
|
|
1307
|
+
: [];
|
|
1308
|
+
return this.computePrStatus(
|
|
1309
|
+
slimPullRequest.url,
|
|
1310
|
+
slimPullRequest.headRefName,
|
|
1311
|
+
{
|
|
1312
|
+
isDraft: slimPullRequest.isDraft,
|
|
1313
|
+
mergeable: slimPullRequest.mergeable,
|
|
1314
|
+
requiredCheckNames,
|
|
1315
|
+
ciContexts,
|
|
1316
|
+
reviewThreads: slimPullRequest.reviewThreads,
|
|
1317
|
+
},
|
|
1318
|
+
);
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1155
1321
|
private resolveMergeabilityWithRetry = async (
|
|
1156
1322
|
owner: string,
|
|
1157
1323
|
repo: string,
|
|
@@ -1262,68 +1428,6 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1262
1428
|
mergeable
|
|
1263
1429
|
headRefName
|
|
1264
1430
|
baseRefName
|
|
1265
|
-
baseRepository {
|
|
1266
|
-
branchProtectionRules(first: 100) {
|
|
1267
|
-
nodes {
|
|
1268
|
-
pattern
|
|
1269
|
-
requiredStatusCheckContexts
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
defaultBranchRef {
|
|
1273
|
-
name
|
|
1274
|
-
}
|
|
1275
|
-
rulesets(first: 100) {
|
|
1276
|
-
nodes {
|
|
1277
|
-
name
|
|
1278
|
-
enforcement
|
|
1279
|
-
conditions {
|
|
1280
|
-
refName {
|
|
1281
|
-
include
|
|
1282
|
-
exclude
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
rules(first: 100) {
|
|
1286
|
-
nodes {
|
|
1287
|
-
type
|
|
1288
|
-
parameters {
|
|
1289
|
-
... on RequiredStatusChecksParameters {
|
|
1290
|
-
requiredStatusChecks {
|
|
1291
|
-
context
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
commits(last: 1) {
|
|
1301
|
-
nodes {
|
|
1302
|
-
commit {
|
|
1303
|
-
statusCheckRollup {
|
|
1304
|
-
contexts(first: 100) {
|
|
1305
|
-
nodes {
|
|
1306
|
-
__typename
|
|
1307
|
-
... on CheckRun {
|
|
1308
|
-
databaseId
|
|
1309
|
-
name
|
|
1310
|
-
conclusion
|
|
1311
|
-
}
|
|
1312
|
-
... on StatusContext {
|
|
1313
|
-
context
|
|
1314
|
-
state
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
reviewThreads(first: 100) {
|
|
1323
|
-
nodes {
|
|
1324
|
-
isResolved
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
1431
|
baseRef {
|
|
1328
1432
|
name
|
|
1329
1433
|
}
|
|
@@ -1378,16 +1482,9 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1378
1482
|
|
|
1379
1483
|
const pr = item.source;
|
|
1380
1484
|
const prUrl = pr.url || '';
|
|
1381
|
-
const baseRefName = pr.baseRefName ?? pr.baseRef?.name;
|
|
1382
|
-
const prStatus = this.computePrStatus(
|
|
1383
|
-
prUrl,
|
|
1384
|
-
pr.headRefName,
|
|
1385
|
-
baseRefName,
|
|
1386
|
-
pr,
|
|
1387
|
-
);
|
|
1388
1485
|
|
|
1389
|
-
let isConflicted =
|
|
1390
|
-
let mergeable =
|
|
1486
|
+
let isConflicted = pr.mergeable === 'CONFLICTING';
|
|
1487
|
+
let mergeable: string | null = pr.mergeable ?? null;
|
|
1391
1488
|
if (
|
|
1392
1489
|
pr.number !== undefined &&
|
|
1393
1490
|
(pr.mergeable === undefined ||
|
|
@@ -1426,6 +1523,38 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1426
1523
|
}
|
|
1427
1524
|
}
|
|
1428
1525
|
|
|
1526
|
+
if (pr.number === undefined) continue;
|
|
1527
|
+
|
|
1528
|
+
let prStatus: RelatedPullRequest;
|
|
1529
|
+
try {
|
|
1530
|
+
const slimPullRequest = await this.fetchSlimPullRequest(
|
|
1531
|
+
owner,
|
|
1532
|
+
repo,
|
|
1533
|
+
pr.number,
|
|
1534
|
+
);
|
|
1535
|
+
if (!slimPullRequest || slimPullRequest.state !== 'OPEN') {
|
|
1536
|
+
console.info(
|
|
1537
|
+
`ApiV3CheerioRestIssueRepository: pull request is no longer open, excluding it from related open PRs. prUrl: ${prUrl}`,
|
|
1538
|
+
);
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
const baseRefName =
|
|
1542
|
+
slimPullRequest.baseRefName ?? pr.baseRefName ?? pr.baseRef?.name;
|
|
1543
|
+
prStatus = await this.buildRelatedPullRequestFromSlim(owner, repo, {
|
|
1544
|
+
...slimPullRequest,
|
|
1545
|
+
url: slimPullRequest.url || prUrl,
|
|
1546
|
+
headRefName: slimPullRequest.headRefName ?? pr.headRefName,
|
|
1547
|
+
baseRefName,
|
|
1548
|
+
});
|
|
1549
|
+
} catch (error) {
|
|
1550
|
+
const errorMessage =
|
|
1551
|
+
error instanceof Error ? error.message : String(error);
|
|
1552
|
+
console.warn(
|
|
1553
|
+
`ApiV3CheerioRestIssueRepository: fetching pull request status failed, skipping PR for this cycle. prUrl: ${prUrl} error: ${errorMessage}`,
|
|
1554
|
+
);
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1429
1558
|
relatedPRsMap.set(prUrl, {
|
|
1430
1559
|
...prStatus,
|
|
1431
1560
|
isConflicted,
|
|
@@ -1476,112 +1605,16 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1476
1605
|
}
|
|
1477
1606
|
const { owner, repo, issueNumber: prNumber } = parsedUrl;
|
|
1478
1607
|
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
url
|
|
1484
|
-
state
|
|
1485
|
-
isDraft
|
|
1486
|
-
headRefName
|
|
1487
|
-
baseRefName
|
|
1488
|
-
mergeable
|
|
1489
|
-
baseRepository {
|
|
1490
|
-
branchProtectionRules(first: 100) {
|
|
1491
|
-
nodes {
|
|
1492
|
-
pattern
|
|
1493
|
-
requiredStatusCheckContexts
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
defaultBranchRef {
|
|
1497
|
-
name
|
|
1498
|
-
}
|
|
1499
|
-
rulesets(first: 100) {
|
|
1500
|
-
nodes {
|
|
1501
|
-
name
|
|
1502
|
-
enforcement
|
|
1503
|
-
conditions {
|
|
1504
|
-
refName {
|
|
1505
|
-
include
|
|
1506
|
-
exclude
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
rules(first: 100) {
|
|
1510
|
-
nodes {
|
|
1511
|
-
type
|
|
1512
|
-
parameters {
|
|
1513
|
-
... on RequiredStatusChecksParameters {
|
|
1514
|
-
requiredStatusChecks {
|
|
1515
|
-
context
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
commits(last: 1) {
|
|
1525
|
-
nodes {
|
|
1526
|
-
commit {
|
|
1527
|
-
statusCheckRollup {
|
|
1528
|
-
contexts(first: 100) {
|
|
1529
|
-
nodes {
|
|
1530
|
-
__typename
|
|
1531
|
-
... on CheckRun {
|
|
1532
|
-
databaseId
|
|
1533
|
-
name
|
|
1534
|
-
conclusion
|
|
1535
|
-
}
|
|
1536
|
-
... on StatusContext {
|
|
1537
|
-
context
|
|
1538
|
-
state
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
reviewThreads(first: 100) {
|
|
1547
|
-
nodes {
|
|
1548
|
-
isResolved
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
`;
|
|
1555
|
-
|
|
1556
|
-
const response = await this.fetchWithRateLimitRetry(() =>
|
|
1557
|
-
fetchGithubGraphql({
|
|
1558
|
-
ghToken: this.ghToken,
|
|
1559
|
-
query,
|
|
1560
|
-
variables: { owner, repo, prNumber },
|
|
1561
|
-
}),
|
|
1608
|
+
const slimPullRequest = await this.fetchSlimPullRequest(
|
|
1609
|
+
owner,
|
|
1610
|
+
repo,
|
|
1611
|
+
prNumber,
|
|
1562
1612
|
);
|
|
1563
|
-
|
|
1564
|
-
if (!response.ok) {
|
|
1565
|
-
throw new Error(
|
|
1566
|
-
`Failed to fetch pull request from GitHub GraphQL API: HTTP ${response.status}`,
|
|
1567
|
-
);
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
const responseData: unknown = await response.json();
|
|
1571
|
-
if (!isDirectPullRequestResponse(responseData)) {
|
|
1572
|
-
throw new Error('Unexpected response shape when fetching pull request');
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
if (responseData.errors && responseData.errors.length > 0) {
|
|
1576
|
-
throw new Error(`GraphQL errors: ${JSON.stringify(responseData.errors)}`);
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
const pr = responseData.data?.repository?.pullRequest;
|
|
1580
|
-
if (!pr || pr.state !== 'OPEN') {
|
|
1613
|
+
if (!slimPullRequest || slimPullRequest.state !== 'OPEN') {
|
|
1581
1614
|
return null;
|
|
1582
1615
|
}
|
|
1583
1616
|
|
|
1584
|
-
return this.
|
|
1617
|
+
return this.buildRelatedPullRequestFromSlim(owner, repo, slimPullRequest);
|
|
1585
1618
|
};
|
|
1586
1619
|
|
|
1587
1620
|
closePullRequest = async (prUrl: string): Promise<void> => {
|