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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.125.0](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.124.0...v1.125.0) (2026-07-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **pr-status:** split findRelatedOpenPRs into two stages and resolve required checks and CI state via REST with a TTL cache ([#1198](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/1198)) ([5361641](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/5361641c91e2217927296e124e2c55328823d951))
|
|
7
|
+
|
|
1
8
|
# [1.124.0](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.123.3...v1.124.0) (2026-07-17)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ApiV3CheerioRestIssueRepository = exports.INCREMENTAL_FETCH_SKEW_BUFFER_MS = exports.FULL_ISSUE_FETCH_INTERVAL_MS = void 0;
|
|
6
|
+
exports.ApiV3CheerioRestIssueRepository = exports.REQUIRED_CHECKS_CACHE_TTL_MS = exports.INCREMENTAL_FETCH_SKEW_BUFFER_MS = exports.FULL_ISSUE_FETCH_INTERVAL_MS = void 0;
|
|
7
7
|
const typia_1 = __importDefault(require("typia"));
|
|
8
8
|
const BaseGitHubRepository_1 = require("../BaseGitHubRepository");
|
|
9
9
|
const githubGraphqlClient_1 = require("../githubGraphqlClient");
|
|
@@ -11,16 +11,35 @@ const utils_1 = require("../utils");
|
|
|
11
11
|
const githubRateLimitRetry_1 = require("./githubRateLimitRetry");
|
|
12
12
|
exports.FULL_ISSUE_FETCH_INTERVAL_MS = 60 * 60 * 1000;
|
|
13
13
|
exports.INCREMENTAL_FETCH_SKEW_BUFFER_MS = 5 * 60 * 1000;
|
|
14
|
+
exports.REQUIRED_CHECKS_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
14
15
|
function isIssueTimelineResponse(value) {
|
|
15
16
|
if (typeof value !== 'object' || value === null)
|
|
16
17
|
return false;
|
|
17
18
|
return true;
|
|
18
19
|
}
|
|
19
|
-
function
|
|
20
|
+
function isSlimPullRequestResponse(value) {
|
|
20
21
|
if (typeof value !== 'object' || value === null)
|
|
21
22
|
return false;
|
|
22
23
|
return true;
|
|
23
24
|
}
|
|
25
|
+
function isBranchRulesResponse(value) {
|
|
26
|
+
return Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
function isBranchDetailResponse(value) {
|
|
29
|
+
if (typeof value !== 'object' || value === null)
|
|
30
|
+
return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
function isCheckRunsResponse(value) {
|
|
34
|
+
if (typeof value !== 'object' || value === null)
|
|
35
|
+
return false;
|
|
36
|
+
return 'check_runs' in value && Array.isArray(value.check_runs);
|
|
37
|
+
}
|
|
38
|
+
function isCombinedStatusResponse(value) {
|
|
39
|
+
if (typeof value !== 'object' || value === null)
|
|
40
|
+
return false;
|
|
41
|
+
return 'statuses' in value && Array.isArray(value.statuses);
|
|
42
|
+
}
|
|
24
43
|
function isPullRequestMergeabilityResponse(value) {
|
|
25
44
|
if (typeof value !== 'object' || value === null)
|
|
26
45
|
return false;
|
|
@@ -107,63 +126,6 @@ function isPullRequestCommitsResponseItem(value) {
|
|
|
107
126
|
function isPullRequestCommitsResponse(value) {
|
|
108
127
|
return Array.isArray(value) && value.every(isPullRequestCommitsResponseItem);
|
|
109
128
|
}
|
|
110
|
-
const fnmatch = (pattern, str) => {
|
|
111
|
-
let regexStr = '^';
|
|
112
|
-
let i = 0;
|
|
113
|
-
while (i < pattern.length) {
|
|
114
|
-
const c = pattern[i];
|
|
115
|
-
if (c === '*') {
|
|
116
|
-
if (pattern[i + 1] === '*') {
|
|
117
|
-
regexStr += '.*';
|
|
118
|
-
i += 2;
|
|
119
|
-
if (pattern[i] === '/') {
|
|
120
|
-
i++;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
regexStr += '[^/]*';
|
|
125
|
-
i++;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
else if (c === '?') {
|
|
129
|
-
regexStr += '[^/]';
|
|
130
|
-
i++;
|
|
131
|
-
}
|
|
132
|
-
else if (c === '[') {
|
|
133
|
-
let j = i + 1;
|
|
134
|
-
while (j < pattern.length && pattern[j] !== ']') {
|
|
135
|
-
j++;
|
|
136
|
-
}
|
|
137
|
-
if (j >= pattern.length) {
|
|
138
|
-
regexStr += '\\[';
|
|
139
|
-
i++;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
const content = pattern.slice(i + 1, j);
|
|
143
|
-
if (content.length > 0 && (content[0] === '!' || content[0] === '^')) {
|
|
144
|
-
const body = content.slice(1).replace(/\\/g, '\\\\');
|
|
145
|
-
regexStr += '[^' + body + ']';
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
const escapedContent = content.replace(/\\/g, '\\\\');
|
|
149
|
-
regexStr += '[' + escapedContent + ']';
|
|
150
|
-
}
|
|
151
|
-
i = j + 1;
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
regexStr += c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
155
|
-
i++;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
regexStr += '$';
|
|
159
|
-
try {
|
|
160
|
-
const regex = new RegExp(regexStr);
|
|
161
|
-
return regex.test(str);
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
return pattern === str;
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
129
|
class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubRepository {
|
|
168
130
|
constructor(apiV3IssueRepository, restIssueRepository, graphqlProjectItemRepository, localStorageCacheRepository, projectRepository, dateRepository, localStorageRepository, ghToken = process.env.GH_TOKEN || 'dummy', sleep = githubRateLimitRetry_1.realSleep) {
|
|
169
131
|
super(localStorageRepository, ghToken);
|
|
@@ -449,62 +411,11 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
449
411
|
isPr: urlMatch[3] === 'pull',
|
|
450
412
|
};
|
|
451
413
|
};
|
|
452
|
-
this.computePrStatus = (prUrl, headRefName,
|
|
414
|
+
this.computePrStatus = (prUrl, headRefName, data) => {
|
|
453
415
|
const isConflicted = data.mergeable === 'CONFLICTING';
|
|
454
|
-
const
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
const branchProtectionRules = data.baseRepository?.branchProtectionRules?.nodes || [];
|
|
458
|
-
const matchingRules = baseRefName
|
|
459
|
-
? branchProtectionRules.filter((rule) => rule.pattern === baseRefName || fnmatch(rule.pattern, baseRefName))
|
|
460
|
-
: [];
|
|
461
|
-
const requiredCheckNamesSet = new Set();
|
|
462
|
-
for (const rule of matchingRules) {
|
|
463
|
-
for (const name of rule.requiredStatusCheckContexts) {
|
|
464
|
-
requiredCheckNamesSet.add(name);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
const rulesets = data.baseRepository?.rulesets?.nodes || [];
|
|
468
|
-
const defaultBranchName = data.baseRepository?.defaultBranchRef?.name || '';
|
|
469
|
-
for (const ruleset of rulesets) {
|
|
470
|
-
if (ruleset.enforcement !== 'ACTIVE')
|
|
471
|
-
continue;
|
|
472
|
-
const refIncludes = ruleset.conditions.refName.include;
|
|
473
|
-
const refExcludes = ruleset.conditions.refName.exclude;
|
|
474
|
-
const matchesInclude = baseRefName !== undefined &&
|
|
475
|
-
refIncludes.some((pattern) => {
|
|
476
|
-
if (pattern === '~DEFAULT_BRANCH') {
|
|
477
|
-
return baseRefName === defaultBranchName;
|
|
478
|
-
}
|
|
479
|
-
if (pattern === '~ALL') {
|
|
480
|
-
return true;
|
|
481
|
-
}
|
|
482
|
-
const branchPattern = pattern.replace(/^refs\/heads\//, '');
|
|
483
|
-
return (branchPattern === baseRefName || fnmatch(branchPattern, baseRefName));
|
|
484
|
-
});
|
|
485
|
-
if (!matchesInclude)
|
|
486
|
-
continue;
|
|
487
|
-
const matchesExclude = baseRefName !== undefined &&
|
|
488
|
-
refExcludes.some((pattern) => {
|
|
489
|
-
if (pattern === '~DEFAULT_BRANCH') {
|
|
490
|
-
return baseRefName === defaultBranchName;
|
|
491
|
-
}
|
|
492
|
-
const branchPattern = pattern.replace(/^refs\/heads\//, '');
|
|
493
|
-
return (branchPattern === baseRefName || fnmatch(branchPattern, baseRefName));
|
|
494
|
-
});
|
|
495
|
-
if (matchesExclude)
|
|
496
|
-
continue;
|
|
497
|
-
for (const rule of ruleset.rules.nodes) {
|
|
498
|
-
if (rule.type !== 'REQUIRED_STATUS_CHECKS')
|
|
499
|
-
continue;
|
|
500
|
-
if ('requiredStatusChecks' in rule.parameters) {
|
|
501
|
-
for (const check of rule.parameters.requiredStatusChecks) {
|
|
502
|
-
requiredCheckNamesSet.add(check.context);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
const requiredCheckNames = Array.from(requiredCheckNamesSet);
|
|
416
|
+
const hasStatusCheckRollup = data.ciContexts.length > 0;
|
|
417
|
+
const contexts = data.ciContexts;
|
|
418
|
+
const requiredCheckNames = data.requiredCheckNames;
|
|
508
419
|
const seenContextNames = new Set();
|
|
509
420
|
for (const ctx of contexts) {
|
|
510
421
|
if ('name' in ctx) {
|
|
@@ -537,7 +448,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
537
448
|
'STALE',
|
|
538
449
|
]);
|
|
539
450
|
const isCiStateSuccess = (() => {
|
|
540
|
-
if (!
|
|
451
|
+
if (!hasStatusCheckRollup)
|
|
541
452
|
return false;
|
|
542
453
|
const latestRuns = [...latestCheckRunByName.values()];
|
|
543
454
|
const statusContexts = contexts.filter((ctx) => ctx.__typename === 'StatusContext');
|
|
@@ -550,7 +461,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
550
461
|
return !hasPending;
|
|
551
462
|
})();
|
|
552
463
|
const isPassedAllCiJob = isCiStateSuccess && allRequiredChecksPassed;
|
|
553
|
-
const reviewThreads = data.reviewThreads
|
|
464
|
+
const reviewThreads = data.reviewThreads;
|
|
554
465
|
const isResolvedAllReviewComments = reviewThreads.length === 0 ||
|
|
555
466
|
reviewThreads.every((thread) => thread.isResolved);
|
|
556
467
|
return {
|
|
@@ -567,6 +478,219 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
567
478
|
missingRequiredCheckNames,
|
|
568
479
|
};
|
|
569
480
|
};
|
|
481
|
+
this.requiredCheckNamesCache = new Map();
|
|
482
|
+
this.getRequiredCheckNames = async (owner, repo, branch) => {
|
|
483
|
+
const cacheKey = `${owner}/${repo}/${branch}`;
|
|
484
|
+
const nowMs = (await this.dateRepository.now()).getTime();
|
|
485
|
+
const cached = this.requiredCheckNamesCache.get(cacheKey);
|
|
486
|
+
if (cached && nowMs - cached.fetchedAtMs < exports.REQUIRED_CHECKS_CACHE_TTL_MS) {
|
|
487
|
+
return cached.names;
|
|
488
|
+
}
|
|
489
|
+
const ownerSegment = encodeURIComponent(owner);
|
|
490
|
+
const repoSegment = encodeURIComponent(repo);
|
|
491
|
+
const branchSegment = encodeURIComponent(branch);
|
|
492
|
+
const requiredCheckNamesSet = new Set();
|
|
493
|
+
const rulesResponse = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${ownerSegment}/${repoSegment}/rules/branches/${branchSegment}?per_page=100`, {
|
|
494
|
+
method: 'GET',
|
|
495
|
+
headers: {
|
|
496
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
497
|
+
Accept: 'application/vnd.github+json',
|
|
498
|
+
},
|
|
499
|
+
}));
|
|
500
|
+
if (rulesResponse.ok) {
|
|
501
|
+
const rulesBody = await rulesResponse.json();
|
|
502
|
+
if (!isBranchRulesResponse(rulesBody)) {
|
|
503
|
+
throw new Error(`Unexpected response shape when fetching branch rules: ${owner}/${repo}/${branch}`);
|
|
504
|
+
}
|
|
505
|
+
for (const rule of rulesBody) {
|
|
506
|
+
if (rule.type !== 'required_status_checks')
|
|
507
|
+
continue;
|
|
508
|
+
for (const check of rule.parameters?.required_status_checks || []) {
|
|
509
|
+
requiredCheckNamesSet.add(check.context);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else if (rulesResponse.status !== 404) {
|
|
514
|
+
const reason = await this.formatGitHubErrorWithStatus(rulesResponse);
|
|
515
|
+
throw new Error(`Failed to fetch branch rules for ${owner}/${repo}/${branch}: ${reason}`);
|
|
516
|
+
}
|
|
517
|
+
const branchResponse = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${ownerSegment}/${repoSegment}/branches/${branchSegment}`, {
|
|
518
|
+
method: 'GET',
|
|
519
|
+
headers: {
|
|
520
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
521
|
+
Accept: 'application/vnd.github+json',
|
|
522
|
+
},
|
|
523
|
+
}));
|
|
524
|
+
if (branchResponse.ok) {
|
|
525
|
+
const branchBody = await branchResponse.json();
|
|
526
|
+
if (!isBranchDetailResponse(branchBody)) {
|
|
527
|
+
throw new Error(`Unexpected response shape when fetching branch detail: ${owner}/${repo}/${branch}`);
|
|
528
|
+
}
|
|
529
|
+
for (const context of branchBody.protection?.required_status_checks
|
|
530
|
+
?.contexts || []) {
|
|
531
|
+
requiredCheckNamesSet.add(context);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
else if (branchResponse.status !== 404) {
|
|
535
|
+
const reason = await this.formatGitHubErrorWithStatus(branchResponse);
|
|
536
|
+
throw new Error(`Failed to fetch branch detail for ${owner}/${repo}/${branch}: ${reason}`);
|
|
537
|
+
}
|
|
538
|
+
const names = Array.from(requiredCheckNamesSet);
|
|
539
|
+
this.requiredCheckNamesCache.set(cacheKey, {
|
|
540
|
+
fetchedAtMs: nowMs,
|
|
541
|
+
names,
|
|
542
|
+
});
|
|
543
|
+
return names;
|
|
544
|
+
};
|
|
545
|
+
this.getCommitCiContexts = async (owner, repo, commitSha) => {
|
|
546
|
+
const ownerSegment = encodeURIComponent(owner);
|
|
547
|
+
const repoSegment = encodeURIComponent(repo);
|
|
548
|
+
const shaSegment = encodeURIComponent(commitSha);
|
|
549
|
+
const contexts = [];
|
|
550
|
+
const perPage = 100;
|
|
551
|
+
let page = 1;
|
|
552
|
+
let hasMore = true;
|
|
553
|
+
while (hasMore) {
|
|
554
|
+
const checkRunsResponse = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${ownerSegment}/${repoSegment}/commits/${shaSegment}/check-runs?per_page=${perPage}&page=${page}`, {
|
|
555
|
+
method: 'GET',
|
|
556
|
+
headers: {
|
|
557
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
558
|
+
Accept: 'application/vnd.github+json',
|
|
559
|
+
},
|
|
560
|
+
}));
|
|
561
|
+
if (!checkRunsResponse.ok) {
|
|
562
|
+
const reason = await this.formatGitHubErrorWithStatus(checkRunsResponse);
|
|
563
|
+
throw new Error(`Failed to fetch check runs for ${owner}/${repo}@${commitSha}: ${reason}`);
|
|
564
|
+
}
|
|
565
|
+
const checkRunsBody = await checkRunsResponse.json();
|
|
566
|
+
if (!isCheckRunsResponse(checkRunsBody)) {
|
|
567
|
+
throw new Error(`Unexpected response shape when fetching check runs: ${owner}/${repo}@${commitSha}`);
|
|
568
|
+
}
|
|
569
|
+
for (const checkRun of checkRunsBody.check_runs) {
|
|
570
|
+
contexts.push({
|
|
571
|
+
__typename: 'CheckRun',
|
|
572
|
+
name: checkRun.name,
|
|
573
|
+
conclusion: checkRun.conclusion
|
|
574
|
+
? checkRun.conclusion.toUpperCase()
|
|
575
|
+
: null,
|
|
576
|
+
databaseId: checkRun.id,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
if (checkRunsBody.check_runs.length < perPage ||
|
|
580
|
+
page * perPage >= checkRunsBody.total_count) {
|
|
581
|
+
hasMore = false;
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
page += 1;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const combinedStatusResponse = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${ownerSegment}/${repoSegment}/commits/${shaSegment}/status?per_page=100`, {
|
|
588
|
+
method: 'GET',
|
|
589
|
+
headers: {
|
|
590
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
591
|
+
Accept: 'application/vnd.github+json',
|
|
592
|
+
},
|
|
593
|
+
}));
|
|
594
|
+
if (!combinedStatusResponse.ok) {
|
|
595
|
+
const reason = await this.formatGitHubErrorWithStatus(combinedStatusResponse);
|
|
596
|
+
throw new Error(`Failed to fetch combined status for ${owner}/${repo}@${commitSha}: ${reason}`);
|
|
597
|
+
}
|
|
598
|
+
const combinedStatusBody = await combinedStatusResponse.json();
|
|
599
|
+
if (!isCombinedStatusResponse(combinedStatusBody)) {
|
|
600
|
+
throw new Error(`Unexpected response shape when fetching combined status: ${owner}/${repo}@${commitSha}`);
|
|
601
|
+
}
|
|
602
|
+
for (const status of combinedStatusBody.statuses) {
|
|
603
|
+
contexts.push({
|
|
604
|
+
__typename: 'StatusContext',
|
|
605
|
+
context: status.context,
|
|
606
|
+
state: status.state.toUpperCase(),
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
return contexts;
|
|
610
|
+
};
|
|
611
|
+
this.fetchSlimPullRequest = async (owner, repo, prNumber) => {
|
|
612
|
+
const query = `
|
|
613
|
+
query PullRequestSlimStatus($owner: String!, $repo: String!, $prNumber: Int!, $reviewThreadsAfter: String) {
|
|
614
|
+
repository(owner: $owner, name: $repo) {
|
|
615
|
+
pullRequest(number: $prNumber) {
|
|
616
|
+
url
|
|
617
|
+
state
|
|
618
|
+
isDraft
|
|
619
|
+
headRefName
|
|
620
|
+
baseRefName
|
|
621
|
+
mergeable
|
|
622
|
+
headRefOid
|
|
623
|
+
reviewThreads(first: 100, after: $reviewThreadsAfter) {
|
|
624
|
+
pageInfo {
|
|
625
|
+
endCursor
|
|
626
|
+
hasNextPage
|
|
627
|
+
}
|
|
628
|
+
nodes {
|
|
629
|
+
isResolved
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
`;
|
|
636
|
+
let slimPullRequest = null;
|
|
637
|
+
let reviewThreadsAfter = null;
|
|
638
|
+
let hasNextPage = true;
|
|
639
|
+
while (hasNextPage) {
|
|
640
|
+
const response = await this.fetchWithRateLimitRetry(() => (0, githubGraphqlClient_1.fetchGithubGraphql)({
|
|
641
|
+
ghToken: this.ghToken,
|
|
642
|
+
query,
|
|
643
|
+
variables: { owner, repo, prNumber, reviewThreadsAfter },
|
|
644
|
+
}));
|
|
645
|
+
if (!response.ok) {
|
|
646
|
+
throw new Error(`Failed to fetch pull request from GitHub GraphQL API: HTTP ${response.status}`);
|
|
647
|
+
}
|
|
648
|
+
const responseData = await response.json();
|
|
649
|
+
if (!isSlimPullRequestResponse(responseData)) {
|
|
650
|
+
throw new Error('Unexpected response shape when fetching pull request');
|
|
651
|
+
}
|
|
652
|
+
if (responseData.errors && responseData.errors.length > 0) {
|
|
653
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(responseData.errors)}`);
|
|
654
|
+
}
|
|
655
|
+
const pr = responseData.data?.repository?.pullRequest;
|
|
656
|
+
if (!pr) {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
if (!slimPullRequest) {
|
|
660
|
+
slimPullRequest = {
|
|
661
|
+
url: pr.url,
|
|
662
|
+
state: pr.state,
|
|
663
|
+
isDraft: pr.isDraft,
|
|
664
|
+
headRefName: pr.headRefName,
|
|
665
|
+
baseRefName: pr.baseRefName,
|
|
666
|
+
mergeable: pr.mergeable,
|
|
667
|
+
headRefOid: pr.headRefOid,
|
|
668
|
+
reviewThreads: [],
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
for (const thread of pr.reviewThreads?.nodes || []) {
|
|
672
|
+
slimPullRequest.reviewThreads.push({ isResolved: thread.isResolved });
|
|
673
|
+
}
|
|
674
|
+
hasNextPage = pr.reviewThreads?.pageInfo.hasNextPage === true;
|
|
675
|
+
reviewThreadsAfter = pr.reviewThreads?.pageInfo.endCursor ?? null;
|
|
676
|
+
}
|
|
677
|
+
return slimPullRequest;
|
|
678
|
+
};
|
|
679
|
+
this.buildRelatedPullRequestFromSlim = async (owner, repo, slimPullRequest) => {
|
|
680
|
+
const requiredCheckNames = slimPullRequest.baseRefName
|
|
681
|
+
? await this.getRequiredCheckNames(owner, repo, slimPullRequest.baseRefName)
|
|
682
|
+
: [];
|
|
683
|
+
const ciContexts = slimPullRequest.headRefOid
|
|
684
|
+
? await this.getCommitCiContexts(owner, repo, slimPullRequest.headRefOid)
|
|
685
|
+
: [];
|
|
686
|
+
return this.computePrStatus(slimPullRequest.url, slimPullRequest.headRefName, {
|
|
687
|
+
isDraft: slimPullRequest.isDraft,
|
|
688
|
+
mergeable: slimPullRequest.mergeable,
|
|
689
|
+
requiredCheckNames,
|
|
690
|
+
ciContexts,
|
|
691
|
+
reviewThreads: slimPullRequest.reviewThreads,
|
|
692
|
+
});
|
|
693
|
+
};
|
|
570
694
|
this.resolveMergeabilityWithRetry = async (owner, repo, prNumber) => {
|
|
571
695
|
const query = `
|
|
572
696
|
query PullRequestMergeability($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
@@ -643,68 +767,6 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
643
767
|
mergeable
|
|
644
768
|
headRefName
|
|
645
769
|
baseRefName
|
|
646
|
-
baseRepository {
|
|
647
|
-
branchProtectionRules(first: 100) {
|
|
648
|
-
nodes {
|
|
649
|
-
pattern
|
|
650
|
-
requiredStatusCheckContexts
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
defaultBranchRef {
|
|
654
|
-
name
|
|
655
|
-
}
|
|
656
|
-
rulesets(first: 100) {
|
|
657
|
-
nodes {
|
|
658
|
-
name
|
|
659
|
-
enforcement
|
|
660
|
-
conditions {
|
|
661
|
-
refName {
|
|
662
|
-
include
|
|
663
|
-
exclude
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
rules(first: 100) {
|
|
667
|
-
nodes {
|
|
668
|
-
type
|
|
669
|
-
parameters {
|
|
670
|
-
... on RequiredStatusChecksParameters {
|
|
671
|
-
requiredStatusChecks {
|
|
672
|
-
context
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
commits(last: 1) {
|
|
682
|
-
nodes {
|
|
683
|
-
commit {
|
|
684
|
-
statusCheckRollup {
|
|
685
|
-
contexts(first: 100) {
|
|
686
|
-
nodes {
|
|
687
|
-
__typename
|
|
688
|
-
... on CheckRun {
|
|
689
|
-
databaseId
|
|
690
|
-
name
|
|
691
|
-
conclusion
|
|
692
|
-
}
|
|
693
|
-
... on StatusContext {
|
|
694
|
-
context
|
|
695
|
-
state
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
reviewThreads(first: 100) {
|
|
704
|
-
nodes {
|
|
705
|
-
isResolved
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
770
|
baseRef {
|
|
709
771
|
name
|
|
710
772
|
}
|
|
@@ -748,10 +810,8 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
748
810
|
continue;
|
|
749
811
|
const pr = item.source;
|
|
750
812
|
const prUrl = pr.url || '';
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
let isConflicted = prStatus.isConflicted;
|
|
754
|
-
let mergeable = prStatus.mergeable;
|
|
813
|
+
let isConflicted = pr.mergeable === 'CONFLICTING';
|
|
814
|
+
let mergeable = pr.mergeable ?? null;
|
|
755
815
|
if (pr.number !== undefined &&
|
|
756
816
|
(pr.mergeable === undefined ||
|
|
757
817
|
pr.mergeable === null ||
|
|
@@ -777,6 +837,28 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
777
837
|
resolved.mergeStateStatus === 'DIRTY';
|
|
778
838
|
}
|
|
779
839
|
}
|
|
840
|
+
if (pr.number === undefined)
|
|
841
|
+
continue;
|
|
842
|
+
let prStatus;
|
|
843
|
+
try {
|
|
844
|
+
const slimPullRequest = await this.fetchSlimPullRequest(owner, repo, pr.number);
|
|
845
|
+
if (!slimPullRequest || slimPullRequest.state !== 'OPEN') {
|
|
846
|
+
console.info(`ApiV3CheerioRestIssueRepository: pull request is no longer open, excluding it from related open PRs. prUrl: ${prUrl}`);
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const baseRefName = slimPullRequest.baseRefName ?? pr.baseRefName ?? pr.baseRef?.name;
|
|
850
|
+
prStatus = await this.buildRelatedPullRequestFromSlim(owner, repo, {
|
|
851
|
+
...slimPullRequest,
|
|
852
|
+
url: slimPullRequest.url || prUrl,
|
|
853
|
+
headRefName: slimPullRequest.headRefName ?? pr.headRefName,
|
|
854
|
+
baseRefName,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
859
|
+
console.warn(`ApiV3CheerioRestIssueRepository: fetching pull request status failed, skipping PR for this cycle. prUrl: ${prUrl} error: ${errorMessage}`);
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
780
862
|
relatedPRsMap.set(prUrl, {
|
|
781
863
|
...prStatus,
|
|
782
864
|
isConflicted,
|
|
@@ -818,102 +900,11 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
818
900
|
return null;
|
|
819
901
|
}
|
|
820
902
|
const { owner, repo, issueNumber: prNumber } = parsedUrl;
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
repository(owner: $owner, name: $repo) {
|
|
824
|
-
pullRequest(number: $prNumber) {
|
|
825
|
-
url
|
|
826
|
-
state
|
|
827
|
-
isDraft
|
|
828
|
-
headRefName
|
|
829
|
-
baseRefName
|
|
830
|
-
mergeable
|
|
831
|
-
baseRepository {
|
|
832
|
-
branchProtectionRules(first: 100) {
|
|
833
|
-
nodes {
|
|
834
|
-
pattern
|
|
835
|
-
requiredStatusCheckContexts
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
defaultBranchRef {
|
|
839
|
-
name
|
|
840
|
-
}
|
|
841
|
-
rulesets(first: 100) {
|
|
842
|
-
nodes {
|
|
843
|
-
name
|
|
844
|
-
enforcement
|
|
845
|
-
conditions {
|
|
846
|
-
refName {
|
|
847
|
-
include
|
|
848
|
-
exclude
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
rules(first: 100) {
|
|
852
|
-
nodes {
|
|
853
|
-
type
|
|
854
|
-
parameters {
|
|
855
|
-
... on RequiredStatusChecksParameters {
|
|
856
|
-
requiredStatusChecks {
|
|
857
|
-
context
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
commits(last: 1) {
|
|
867
|
-
nodes {
|
|
868
|
-
commit {
|
|
869
|
-
statusCheckRollup {
|
|
870
|
-
contexts(first: 100) {
|
|
871
|
-
nodes {
|
|
872
|
-
__typename
|
|
873
|
-
... on CheckRun {
|
|
874
|
-
databaseId
|
|
875
|
-
name
|
|
876
|
-
conclusion
|
|
877
|
-
}
|
|
878
|
-
... on StatusContext {
|
|
879
|
-
context
|
|
880
|
-
state
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
reviewThreads(first: 100) {
|
|
889
|
-
nodes {
|
|
890
|
-
isResolved
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
`;
|
|
897
|
-
const response = await this.fetchWithRateLimitRetry(() => (0, githubGraphqlClient_1.fetchGithubGraphql)({
|
|
898
|
-
ghToken: this.ghToken,
|
|
899
|
-
query,
|
|
900
|
-
variables: { owner, repo, prNumber },
|
|
901
|
-
}));
|
|
902
|
-
if (!response.ok) {
|
|
903
|
-
throw new Error(`Failed to fetch pull request from GitHub GraphQL API: HTTP ${response.status}`);
|
|
904
|
-
}
|
|
905
|
-
const responseData = await response.json();
|
|
906
|
-
if (!isDirectPullRequestResponse(responseData)) {
|
|
907
|
-
throw new Error('Unexpected response shape when fetching pull request');
|
|
908
|
-
}
|
|
909
|
-
if (responseData.errors && responseData.errors.length > 0) {
|
|
910
|
-
throw new Error(`GraphQL errors: ${JSON.stringify(responseData.errors)}`);
|
|
911
|
-
}
|
|
912
|
-
const pr = responseData.data?.repository?.pullRequest;
|
|
913
|
-
if (!pr || pr.state !== 'OPEN') {
|
|
903
|
+
const slimPullRequest = await this.fetchSlimPullRequest(owner, repo, prNumber);
|
|
904
|
+
if (!slimPullRequest || slimPullRequest.state !== 'OPEN') {
|
|
914
905
|
return null;
|
|
915
906
|
}
|
|
916
|
-
return this.
|
|
907
|
+
return this.buildRelatedPullRequestFromSlim(owner, repo, slimPullRequest);
|
|
917
908
|
};
|
|
918
909
|
this.closePullRequest = async (prUrl) => {
|
|
919
910
|
const { owner, repo, issueNumber: prNumber } = this.parseIssueUrl(prUrl);
|