github-issue-tower-defence-management 1.124.0 → 1.125.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/umino-project.yml +3 -0
- package/CHANGELOG.md +14 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +275 -276
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +794 -325
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +464 -421
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +6 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
|
@@ -279,6 +279,9 @@ jobs:
|
|
|
279
279
|
name: Check linked issues in pull requests
|
|
280
280
|
if: >-
|
|
281
281
|
(github.event_name == 'pull_request') &&
|
|
282
|
+
(github.event.action == 'edited' ||
|
|
283
|
+
github.event.action == 'synchronize' ||
|
|
284
|
+
github.event.action == 'reopened') &&
|
|
282
285
|
github.actor != 'dependabot[bot]' &&
|
|
283
286
|
github.actor != 'renovate[bot]' &&
|
|
284
287
|
github.actor != 'app/dependabot'
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [1.125.1](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.125.0...v1.125.1) (2026-07-18)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **pr-status:** treat HTTP 403 from branch rules and classic protection as no required checks ([#1212](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/1212)) ([b264917](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/b26491761589ce63636d721fb2b3166be9832a85))
|
|
7
|
+
|
|
8
|
+
# [1.125.0](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.124.0...v1.125.0) (2026-07-17)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **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))
|
|
14
|
+
|
|
1
15
|
# [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
16
|
|
|
3
17
|
|
|
@@ -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,227 @@ 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 === 403) {
|
|
514
|
+
const reason = await this.formatGitHubErrorWithStatus(rulesResponse);
|
|
515
|
+
console.warn(`ApiV3CheerioRestIssueRepository: branch rules are not accessible for ${owner}/${repo}/${branch}, treating as no required checks. reason: ${reason}`);
|
|
516
|
+
}
|
|
517
|
+
else if (rulesResponse.status !== 404) {
|
|
518
|
+
const reason = await this.formatGitHubErrorWithStatus(rulesResponse);
|
|
519
|
+
throw new Error(`Failed to fetch branch rules for ${owner}/${repo}/${branch}: ${reason}`);
|
|
520
|
+
}
|
|
521
|
+
const branchResponse = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${ownerSegment}/${repoSegment}/branches/${branchSegment}`, {
|
|
522
|
+
method: 'GET',
|
|
523
|
+
headers: {
|
|
524
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
525
|
+
Accept: 'application/vnd.github+json',
|
|
526
|
+
},
|
|
527
|
+
}));
|
|
528
|
+
if (branchResponse.ok) {
|
|
529
|
+
const branchBody = await branchResponse.json();
|
|
530
|
+
if (!isBranchDetailResponse(branchBody)) {
|
|
531
|
+
throw new Error(`Unexpected response shape when fetching branch detail: ${owner}/${repo}/${branch}`);
|
|
532
|
+
}
|
|
533
|
+
for (const context of branchBody.protection?.required_status_checks
|
|
534
|
+
?.contexts || []) {
|
|
535
|
+
requiredCheckNamesSet.add(context);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
else if (branchResponse.status === 403) {
|
|
539
|
+
const reason = await this.formatGitHubErrorWithStatus(branchResponse);
|
|
540
|
+
console.warn(`ApiV3CheerioRestIssueRepository: branch detail (classic protection) is not accessible for ${owner}/${repo}/${branch}, treating as no required checks. reason: ${reason}`);
|
|
541
|
+
}
|
|
542
|
+
else if (branchResponse.status !== 404) {
|
|
543
|
+
const reason = await this.formatGitHubErrorWithStatus(branchResponse);
|
|
544
|
+
throw new Error(`Failed to fetch branch detail for ${owner}/${repo}/${branch}: ${reason}`);
|
|
545
|
+
}
|
|
546
|
+
const names = Array.from(requiredCheckNamesSet);
|
|
547
|
+
this.requiredCheckNamesCache.set(cacheKey, {
|
|
548
|
+
fetchedAtMs: nowMs,
|
|
549
|
+
names,
|
|
550
|
+
});
|
|
551
|
+
return names;
|
|
552
|
+
};
|
|
553
|
+
this.getCommitCiContexts = async (owner, repo, commitSha) => {
|
|
554
|
+
const ownerSegment = encodeURIComponent(owner);
|
|
555
|
+
const repoSegment = encodeURIComponent(repo);
|
|
556
|
+
const shaSegment = encodeURIComponent(commitSha);
|
|
557
|
+
const contexts = [];
|
|
558
|
+
const perPage = 100;
|
|
559
|
+
let page = 1;
|
|
560
|
+
let hasMore = true;
|
|
561
|
+
while (hasMore) {
|
|
562
|
+
const checkRunsResponse = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${ownerSegment}/${repoSegment}/commits/${shaSegment}/check-runs?per_page=${perPage}&page=${page}`, {
|
|
563
|
+
method: 'GET',
|
|
564
|
+
headers: {
|
|
565
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
566
|
+
Accept: 'application/vnd.github+json',
|
|
567
|
+
},
|
|
568
|
+
}));
|
|
569
|
+
if (!checkRunsResponse.ok) {
|
|
570
|
+
const reason = await this.formatGitHubErrorWithStatus(checkRunsResponse);
|
|
571
|
+
throw new Error(`Failed to fetch check runs for ${owner}/${repo}@${commitSha}: ${reason}`);
|
|
572
|
+
}
|
|
573
|
+
const checkRunsBody = await checkRunsResponse.json();
|
|
574
|
+
if (!isCheckRunsResponse(checkRunsBody)) {
|
|
575
|
+
throw new Error(`Unexpected response shape when fetching check runs: ${owner}/${repo}@${commitSha}`);
|
|
576
|
+
}
|
|
577
|
+
for (const checkRun of checkRunsBody.check_runs) {
|
|
578
|
+
contexts.push({
|
|
579
|
+
__typename: 'CheckRun',
|
|
580
|
+
name: checkRun.name,
|
|
581
|
+
conclusion: checkRun.conclusion
|
|
582
|
+
? checkRun.conclusion.toUpperCase()
|
|
583
|
+
: null,
|
|
584
|
+
databaseId: checkRun.id,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
if (checkRunsBody.check_runs.length < perPage ||
|
|
588
|
+
page * perPage >= checkRunsBody.total_count) {
|
|
589
|
+
hasMore = false;
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
page += 1;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const combinedStatusResponse = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${ownerSegment}/${repoSegment}/commits/${shaSegment}/status?per_page=100`, {
|
|
596
|
+
method: 'GET',
|
|
597
|
+
headers: {
|
|
598
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
599
|
+
Accept: 'application/vnd.github+json',
|
|
600
|
+
},
|
|
601
|
+
}));
|
|
602
|
+
if (!combinedStatusResponse.ok) {
|
|
603
|
+
const reason = await this.formatGitHubErrorWithStatus(combinedStatusResponse);
|
|
604
|
+
throw new Error(`Failed to fetch combined status for ${owner}/${repo}@${commitSha}: ${reason}`);
|
|
605
|
+
}
|
|
606
|
+
const combinedStatusBody = await combinedStatusResponse.json();
|
|
607
|
+
if (!isCombinedStatusResponse(combinedStatusBody)) {
|
|
608
|
+
throw new Error(`Unexpected response shape when fetching combined status: ${owner}/${repo}@${commitSha}`);
|
|
609
|
+
}
|
|
610
|
+
for (const status of combinedStatusBody.statuses) {
|
|
611
|
+
contexts.push({
|
|
612
|
+
__typename: 'StatusContext',
|
|
613
|
+
context: status.context,
|
|
614
|
+
state: status.state.toUpperCase(),
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
return contexts;
|
|
618
|
+
};
|
|
619
|
+
this.fetchSlimPullRequest = async (owner, repo, prNumber) => {
|
|
620
|
+
const query = `
|
|
621
|
+
query PullRequestSlimStatus($owner: String!, $repo: String!, $prNumber: Int!, $reviewThreadsAfter: String) {
|
|
622
|
+
repository(owner: $owner, name: $repo) {
|
|
623
|
+
pullRequest(number: $prNumber) {
|
|
624
|
+
url
|
|
625
|
+
state
|
|
626
|
+
isDraft
|
|
627
|
+
headRefName
|
|
628
|
+
baseRefName
|
|
629
|
+
mergeable
|
|
630
|
+
headRefOid
|
|
631
|
+
reviewThreads(first: 100, after: $reviewThreadsAfter) {
|
|
632
|
+
pageInfo {
|
|
633
|
+
endCursor
|
|
634
|
+
hasNextPage
|
|
635
|
+
}
|
|
636
|
+
nodes {
|
|
637
|
+
isResolved
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
`;
|
|
644
|
+
let slimPullRequest = null;
|
|
645
|
+
let reviewThreadsAfter = null;
|
|
646
|
+
let hasNextPage = true;
|
|
647
|
+
while (hasNextPage) {
|
|
648
|
+
const response = await this.fetchWithRateLimitRetry(() => (0, githubGraphqlClient_1.fetchGithubGraphql)({
|
|
649
|
+
ghToken: this.ghToken,
|
|
650
|
+
query,
|
|
651
|
+
variables: { owner, repo, prNumber, reviewThreadsAfter },
|
|
652
|
+
}));
|
|
653
|
+
if (!response.ok) {
|
|
654
|
+
throw new Error(`Failed to fetch pull request from GitHub GraphQL API: HTTP ${response.status}`);
|
|
655
|
+
}
|
|
656
|
+
const responseData = await response.json();
|
|
657
|
+
if (!isSlimPullRequestResponse(responseData)) {
|
|
658
|
+
throw new Error('Unexpected response shape when fetching pull request');
|
|
659
|
+
}
|
|
660
|
+
if (responseData.errors && responseData.errors.length > 0) {
|
|
661
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(responseData.errors)}`);
|
|
662
|
+
}
|
|
663
|
+
const pr = responseData.data?.repository?.pullRequest;
|
|
664
|
+
if (!pr) {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
if (!slimPullRequest) {
|
|
668
|
+
slimPullRequest = {
|
|
669
|
+
url: pr.url,
|
|
670
|
+
state: pr.state,
|
|
671
|
+
isDraft: pr.isDraft,
|
|
672
|
+
headRefName: pr.headRefName,
|
|
673
|
+
baseRefName: pr.baseRefName,
|
|
674
|
+
mergeable: pr.mergeable,
|
|
675
|
+
headRefOid: pr.headRefOid,
|
|
676
|
+
reviewThreads: [],
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
for (const thread of pr.reviewThreads?.nodes || []) {
|
|
680
|
+
slimPullRequest.reviewThreads.push({ isResolved: thread.isResolved });
|
|
681
|
+
}
|
|
682
|
+
hasNextPage = pr.reviewThreads?.pageInfo.hasNextPage === true;
|
|
683
|
+
reviewThreadsAfter = pr.reviewThreads?.pageInfo.endCursor ?? null;
|
|
684
|
+
}
|
|
685
|
+
return slimPullRequest;
|
|
686
|
+
};
|
|
687
|
+
this.buildRelatedPullRequestFromSlim = async (owner, repo, slimPullRequest) => {
|
|
688
|
+
const requiredCheckNames = slimPullRequest.baseRefName
|
|
689
|
+
? await this.getRequiredCheckNames(owner, repo, slimPullRequest.baseRefName)
|
|
690
|
+
: [];
|
|
691
|
+
const ciContexts = slimPullRequest.headRefOid
|
|
692
|
+
? await this.getCommitCiContexts(owner, repo, slimPullRequest.headRefOid)
|
|
693
|
+
: [];
|
|
694
|
+
return this.computePrStatus(slimPullRequest.url, slimPullRequest.headRefName, {
|
|
695
|
+
isDraft: slimPullRequest.isDraft,
|
|
696
|
+
mergeable: slimPullRequest.mergeable,
|
|
697
|
+
requiredCheckNames,
|
|
698
|
+
ciContexts,
|
|
699
|
+
reviewThreads: slimPullRequest.reviewThreads,
|
|
700
|
+
});
|
|
701
|
+
};
|
|
570
702
|
this.resolveMergeabilityWithRetry = async (owner, repo, prNumber) => {
|
|
571
703
|
const query = `
|
|
572
704
|
query PullRequestMergeability($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
@@ -643,68 +775,6 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
643
775
|
mergeable
|
|
644
776
|
headRefName
|
|
645
777
|
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
778
|
baseRef {
|
|
709
779
|
name
|
|
710
780
|
}
|
|
@@ -748,10 +818,8 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
748
818
|
continue;
|
|
749
819
|
const pr = item.source;
|
|
750
820
|
const prUrl = pr.url || '';
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
let isConflicted = prStatus.isConflicted;
|
|
754
|
-
let mergeable = prStatus.mergeable;
|
|
821
|
+
let isConflicted = pr.mergeable === 'CONFLICTING';
|
|
822
|
+
let mergeable = pr.mergeable ?? null;
|
|
755
823
|
if (pr.number !== undefined &&
|
|
756
824
|
(pr.mergeable === undefined ||
|
|
757
825
|
pr.mergeable === null ||
|
|
@@ -777,6 +845,28 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
777
845
|
resolved.mergeStateStatus === 'DIRTY';
|
|
778
846
|
}
|
|
779
847
|
}
|
|
848
|
+
if (pr.number === undefined)
|
|
849
|
+
continue;
|
|
850
|
+
let prStatus;
|
|
851
|
+
try {
|
|
852
|
+
const slimPullRequest = await this.fetchSlimPullRequest(owner, repo, pr.number);
|
|
853
|
+
if (!slimPullRequest || slimPullRequest.state !== 'OPEN') {
|
|
854
|
+
console.info(`ApiV3CheerioRestIssueRepository: pull request is no longer open, excluding it from related open PRs. prUrl: ${prUrl}`);
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
const baseRefName = slimPullRequest.baseRefName ?? pr.baseRefName ?? pr.baseRef?.name;
|
|
858
|
+
prStatus = await this.buildRelatedPullRequestFromSlim(owner, repo, {
|
|
859
|
+
...slimPullRequest,
|
|
860
|
+
url: slimPullRequest.url || prUrl,
|
|
861
|
+
headRefName: slimPullRequest.headRefName ?? pr.headRefName,
|
|
862
|
+
baseRefName,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
catch (error) {
|
|
866
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
867
|
+
console.warn(`ApiV3CheerioRestIssueRepository: fetching pull request status failed, skipping PR for this cycle. prUrl: ${prUrl} error: ${errorMessage}`);
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
780
870
|
relatedPRsMap.set(prUrl, {
|
|
781
871
|
...prStatus,
|
|
782
872
|
isConflicted,
|
|
@@ -818,102 +908,11 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
818
908
|
return null;
|
|
819
909
|
}
|
|
820
910
|
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') {
|
|
911
|
+
const slimPullRequest = await this.fetchSlimPullRequest(owner, repo, prNumber);
|
|
912
|
+
if (!slimPullRequest || slimPullRequest.state !== 'OPEN') {
|
|
914
913
|
return null;
|
|
915
914
|
}
|
|
916
|
-
return this.
|
|
915
|
+
return this.buildRelatedPullRequestFromSlim(owner, repo, slimPullRequest);
|
|
917
916
|
};
|
|
918
917
|
this.closePullRequest = async (prUrl) => {
|
|
919
918
|
const { owner, repo, issueNumber: prNumber } = this.parseIssueUrl(prUrl);
|