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.
@@ -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 isDirectPullRequestResponse(value) {
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, baseRefName, data) => {
414
+ this.computePrStatus = (prUrl, headRefName, data) => {
453
415
  const isConflicted = data.mergeable === 'CONFLICTING';
454
- const lastCommit = data.commits?.nodes[0]?.commit;
455
- const statusCheckRollup = lastCommit?.statusCheckRollup;
456
- const contexts = statusCheckRollup?.contexts?.nodes || [];
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 (!statusCheckRollup)
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?.nodes || [];
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
- const baseRefName = pr.baseRefName ?? pr.baseRef?.name;
752
- const prStatus = this.computePrStatus(prUrl, pr.headRefName, baseRefName, pr);
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 query = `
822
- query PullRequestStatus($owner: String!, $repo: String!, $prNumber: Int!) {
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.computePrStatus(pr.url, pr.headRefName, pr.baseRefName, pr);
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);