github-issue-tower-defence-management 1.116.2 → 1.116.5

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.
Files changed (39) hide show
  1. package/.github/workflows/commit-lint.yml +1 -1
  2. package/.github/workflows/configs/commitlint.config.cjs +32 -0
  3. package/.github/workflows/umino-project.yml +9 -3
  4. package/CHANGELOG.md +14 -0
  5. package/README.md +2 -2
  6. package/bin/adapter/repositories/ProcClaudeLiveSessionRepository.js +18 -6
  7. package/bin/adapter/repositories/ProcClaudeLiveSessionRepository.js.map +1 -1
  8. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +74 -49
  9. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  10. package/bin/adapter/repositories/issue/githubRateLimitRetry.js +77 -0
  11. package/bin/adapter/repositories/issue/githubRateLimitRetry.js.map +1 -0
  12. package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js +6 -6
  13. package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js.map +1 -1
  14. package/bin/domain/usecases/OauthTokenSelectUseCase.js +1 -1
  15. package/bin/domain/usecases/OauthTokenSelectUseCase.js.map +1 -1
  16. package/package.json +1 -1
  17. package/renovate.json +1 -0
  18. package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.test.ts +11 -11
  19. package/src/adapter/repositories/ProcClaudeLiveSessionRepository.test.ts +69 -7
  20. package/src/adapter/repositories/ProcClaudeLiveSessionRepository.ts +19 -8
  21. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +111 -0
  22. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +243 -193
  23. package/src/adapter/repositories/issue/githubRateLimitRetry.test.ts +167 -0
  24. package/src/adapter/repositories/issue/githubRateLimitRetry.ts +105 -0
  25. package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.test.ts +25 -3
  26. package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.ts +7 -7
  27. package/src/domain/usecases/OauthTokenSelectUseCase.test.ts +13 -4
  28. package/src/domain/usecases/OauthTokenSelectUseCase.ts +1 -1
  29. package/src/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.ts +1 -1
  30. package/types/adapter/repositories/ProcClaudeLiveSessionRepository.d.ts +1 -0
  31. package/types/adapter/repositories/ProcClaudeLiveSessionRepository.d.ts.map +1 -1
  32. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +4 -1
  33. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  34. package/types/adapter/repositories/issue/githubRateLimitRetry.d.ts +10 -0
  35. package/types/adapter/repositories/issue/githubRateLimitRetry.d.ts.map +1 -0
  36. package/types/domain/usecases/OauthTokenSelectUseCase.d.ts +1 -1
  37. package/types/domain/usecases/OauthTokenSelectUseCase.d.ts.map +1 -1
  38. package/types/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.d.ts +1 -1
  39. package/types/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.d.ts.map +1 -1
@@ -22,6 +22,13 @@ import { BaseGitHubRepository } from '../BaseGitHubRepository';
22
22
  import { normalizeFieldName } from '../utils';
23
23
  import { LocalStorageRepository } from '../LocalStorageRepository';
24
24
  import { Member } from '../../../domain/entities/Member';
25
+ import {
26
+ Sleep,
27
+ realSleep,
28
+ fetchWithGitHubRateLimitRetry,
29
+ computeRateLimitResetIso,
30
+ hasRateLimitSignals,
31
+ } from './githubRateLimitRetry';
25
32
 
26
33
  type TimelineItem = {
27
34
  __typename: string;
@@ -471,10 +478,15 @@ export class ApiV3CheerioRestIssueRepository
471
478
  >,
472
479
  readonly localStorageRepository: LocalStorageRepository,
473
480
  readonly ghToken: string = process.env.GH_TOKEN || 'dummy',
481
+ readonly sleep: Sleep = realSleep,
474
482
  ) {
475
483
  super(localStorageRepository, ghToken);
476
484
  }
477
485
 
486
+ private fetchWithRateLimitRetry = (
487
+ request: () => Promise<Response>,
488
+ ): Promise<Response> => fetchWithGitHubRateLimitRetry(request, this.sleep);
489
+
478
490
  updateStatus: (
479
491
  project: Project,
480
492
  issue: Issue,
@@ -1106,17 +1118,19 @@ export class ApiV3CheerioRestIssueRepository
1106
1118
  let hasNextPage = true;
1107
1119
 
1108
1120
  while (hasNextPage) {
1109
- const response = await fetch('https://api.github.com/graphql', {
1110
- method: 'POST',
1111
- headers: {
1112
- Authorization: `Bearer ${this.ghToken}`,
1113
- 'Content-Type': 'application/json',
1114
- },
1115
- body: JSON.stringify({
1116
- query,
1117
- variables: { owner, repo, issueNumber, after },
1121
+ const response = await this.fetchWithRateLimitRetry(() =>
1122
+ fetch('https://api.github.com/graphql', {
1123
+ method: 'POST',
1124
+ headers: {
1125
+ Authorization: `Bearer ${this.ghToken}`,
1126
+ 'Content-Type': 'application/json',
1127
+ },
1128
+ body: JSON.stringify({
1129
+ query,
1130
+ variables: { owner, repo, issueNumber, after },
1131
+ }),
1118
1132
  }),
1119
- });
1133
+ );
1120
1134
 
1121
1135
  if (!response.ok) {
1122
1136
  throw new Error(
@@ -1285,17 +1299,19 @@ export class ApiV3CheerioRestIssueRepository
1285
1299
  }
1286
1300
  `;
1287
1301
 
1288
- const response = await fetch('https://api.github.com/graphql', {
1289
- method: 'POST',
1290
- headers: {
1291
- Authorization: `Bearer ${this.ghToken}`,
1292
- 'Content-Type': 'application/json',
1293
- },
1294
- body: JSON.stringify({
1295
- query,
1296
- variables: { owner, repo, prNumber },
1302
+ const response = await this.fetchWithRateLimitRetry(() =>
1303
+ fetch('https://api.github.com/graphql', {
1304
+ method: 'POST',
1305
+ headers: {
1306
+ Authorization: `Bearer ${this.ghToken}`,
1307
+ 'Content-Type': 'application/json',
1308
+ },
1309
+ body: JSON.stringify({
1310
+ query,
1311
+ variables: { owner, repo, prNumber },
1312
+ }),
1297
1313
  }),
1298
- });
1314
+ );
1299
1315
 
1300
1316
  if (!response.ok) {
1301
1317
  throw new Error(
@@ -1322,16 +1338,18 @@ export class ApiV3CheerioRestIssueRepository
1322
1338
 
1323
1339
  closePullRequest = async (prUrl: string): Promise<void> => {
1324
1340
  const { owner, repo, issueNumber: prNumber } = this.parseIssueUrl(prUrl);
1325
- const response = await fetch(
1326
- `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`,
1327
- {
1328
- method: 'PATCH',
1329
- headers: {
1330
- Authorization: `Bearer ${this.ghToken}`,
1331
- 'Content-Type': 'application/json',
1341
+ const response = await this.fetchWithRateLimitRetry(() =>
1342
+ fetch(
1343
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}`,
1344
+ {
1345
+ method: 'PATCH',
1346
+ headers: {
1347
+ Authorization: `Bearer ${this.ghToken}`,
1348
+ 'Content-Type': 'application/json',
1349
+ },
1350
+ body: JSON.stringify({ state: 'closed' }),
1332
1351
  },
1333
- body: JSON.stringify({ state: 'closed' }),
1334
- },
1352
+ ),
1335
1353
  );
1336
1354
  if (!response.ok) {
1337
1355
  const reason = await this.formatGitHubErrorWithStatus(response);
@@ -1346,21 +1364,22 @@ export class ApiV3CheerioRestIssueRepository
1346
1364
  const { owner, repo, issueNumber } = this.parseIssueUrl(issueUrl);
1347
1365
  const ownerSegment = encodeURIComponent(owner);
1348
1366
  const repoSegment = encodeURIComponent(repo);
1349
- const response = await fetch(
1350
- `https://api.github.com/repos/${ownerSegment}/${repoSegment}/issues/${issueNumber}`,
1351
- {
1352
- method: 'PATCH',
1353
- headers: {
1354
- Authorization: `Bearer ${this.ghToken}`,
1355
- 'Content-Type': 'application/json',
1367
+ const response = await this.fetchWithRateLimitRetry(() =>
1368
+ fetch(
1369
+ `https://api.github.com/repos/${ownerSegment}/${repoSegment}/issues/${issueNumber}`,
1370
+ {
1371
+ method: 'PATCH',
1372
+ headers: {
1373
+ Authorization: `Bearer ${this.ghToken}`,
1374
+ 'Content-Type': 'application/json',
1375
+ },
1376
+ body: JSON.stringify({ state: 'closed', state_reason: stateReason }),
1356
1377
  },
1357
- body: JSON.stringify({ state: 'closed', state_reason: stateReason }),
1358
- },
1378
+ ),
1359
1379
  );
1360
1380
  if (!response.ok) {
1361
- throw new Error(
1362
- `Failed to close issue ${issueUrl}: HTTP ${response.status}`,
1363
- );
1381
+ const reason = await this.formatGitHubErrorWithStatus(response);
1382
+ throw new Error(`Failed to close issue ${issueUrl}: ${reason}`);
1364
1383
  }
1365
1384
  };
1366
1385
 
@@ -1371,19 +1390,22 @@ export class ApiV3CheerioRestIssueRepository
1371
1390
  let page = 1;
1372
1391
  let hasMore = true;
1373
1392
  while (hasMore) {
1374
- const response = await fetch(
1375
- `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=${perPage}&page=${page}`,
1376
- {
1377
- method: 'GET',
1378
- headers: {
1379
- Authorization: `Bearer ${this.ghToken}`,
1380
- Accept: 'application/vnd.github+json',
1393
+ const response = await this.fetchWithRateLimitRetry(() =>
1394
+ fetch(
1395
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/files?per_page=${perPage}&page=${page}`,
1396
+ {
1397
+ method: 'GET',
1398
+ headers: {
1399
+ Authorization: `Bearer ${this.ghToken}`,
1400
+ Accept: 'application/vnd.github+json',
1401
+ },
1381
1402
  },
1382
- },
1403
+ ),
1383
1404
  );
1384
1405
  if (!response.ok) {
1406
+ const reason = await this.formatGitHubErrorWithStatus(response);
1385
1407
  throw new Error(
1386
- `Failed to fetch changed files for PR ${prUrl}: HTTP ${response.status}`,
1408
+ `Failed to fetch changed files for PR ${prUrl}: ${reason}`,
1387
1409
  );
1388
1410
  }
1389
1411
  const body: unknown = await response.json();
@@ -1406,17 +1428,19 @@ export class ApiV3CheerioRestIssueRepository
1406
1428
 
1407
1429
  approvePullRequest = async (prUrl: string): Promise<void> => {
1408
1430
  const { owner, repo, issueNumber: prNumber } = this.parseIssueUrl(prUrl);
1409
- const response = await fetch(
1410
- `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`,
1411
- {
1412
- method: 'POST',
1413
- headers: {
1414
- Authorization: `Bearer ${this.ghToken}`,
1415
- 'Content-Type': 'application/json',
1416
- Accept: 'application/vnd.github+json',
1431
+ const response = await this.fetchWithRateLimitRetry(() =>
1432
+ fetch(
1433
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/reviews`,
1434
+ {
1435
+ method: 'POST',
1436
+ headers: {
1437
+ Authorization: `Bearer ${this.ghToken}`,
1438
+ 'Content-Type': 'application/json',
1439
+ Accept: 'application/vnd.github+json',
1440
+ },
1441
+ body: JSON.stringify({ event: 'APPROVE' }),
1417
1442
  },
1418
- body: JSON.stringify({ event: 'APPROVE' }),
1419
- },
1443
+ ),
1420
1444
  );
1421
1445
  if (!response.ok) {
1422
1446
  const reason = await this.formatGitHubErrorWithStatus(response);
@@ -1444,17 +1468,19 @@ export class ApiV3CheerioRestIssueRepository
1444
1468
  },
1445
1469
  ],
1446
1470
  };
1447
- const response = await fetch(
1448
- `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`,
1449
- {
1450
- method: 'POST',
1451
- headers: {
1452
- Authorization: `Bearer ${this.ghToken}`,
1453
- 'Content-Type': 'application/json',
1454
- Accept: 'application/vnd.github+json',
1471
+ const response = await this.fetchWithRateLimitRetry(() =>
1472
+ fetch(
1473
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/reviews`,
1474
+ {
1475
+ method: 'POST',
1476
+ headers: {
1477
+ Authorization: `Bearer ${this.ghToken}`,
1478
+ 'Content-Type': 'application/json',
1479
+ Accept: 'application/vnd.github+json',
1480
+ },
1481
+ body: JSON.stringify(reviewBody),
1455
1482
  },
1456
- body: JSON.stringify(reviewBody),
1457
- },
1483
+ ),
1458
1484
  );
1459
1485
  if (!response.ok) {
1460
1486
  const reason = await this.formatGitHubErrorWithStatus(response);
@@ -1470,20 +1496,21 @@ export class ApiV3CheerioRestIssueRepository
1470
1496
  ): Promise<string> => {
1471
1497
  const ownerSegment = encodeURIComponent(owner);
1472
1498
  const repoSegment = encodeURIComponent(repo);
1473
- const response = await fetch(
1474
- `https://api.github.com/repos/${ownerSegment}/${repoSegment}/pulls/${prNumber}`,
1475
- {
1476
- method: 'GET',
1477
- headers: {
1478
- Authorization: `Bearer ${this.ghToken}`,
1479
- Accept: 'application/vnd.github+json',
1499
+ const response = await this.fetchWithRateLimitRetry(() =>
1500
+ fetch(
1501
+ `https://api.github.com/repos/${ownerSegment}/${repoSegment}/pulls/${prNumber}`,
1502
+ {
1503
+ method: 'GET',
1504
+ headers: {
1505
+ Authorization: `Bearer ${this.ghToken}`,
1506
+ Accept: 'application/vnd.github+json',
1507
+ },
1480
1508
  },
1481
- },
1509
+ ),
1482
1510
  );
1483
1511
  if (!response.ok) {
1484
- throw new Error(
1485
- `Failed to fetch head commit for PR ${prUrl}: HTTP ${response.status}`,
1486
- );
1512
+ const reason = await this.formatGitHubErrorWithStatus(response);
1513
+ throw new Error(`Failed to fetch head commit for PR ${prUrl}: ${reason}`);
1487
1514
  }
1488
1515
  const body: unknown = await response.json();
1489
1516
  if (
@@ -1514,23 +1541,25 @@ export class ApiV3CheerioRestIssueRepository
1514
1541
  );
1515
1542
  const ownerSegment = encodeURIComponent(owner);
1516
1543
  const repoSegment = encodeURIComponent(repo);
1517
- const response = await fetch(
1518
- `https://api.github.com/repos/${ownerSegment}/${repoSegment}/pulls/${prNumber}/comments`,
1519
- {
1520
- method: 'POST',
1521
- headers: {
1522
- Authorization: `Bearer ${this.ghToken}`,
1523
- 'Content-Type': 'application/json',
1524
- Accept: 'application/vnd.github+json',
1544
+ const response = await this.fetchWithRateLimitRetry(() =>
1545
+ fetch(
1546
+ `https://api.github.com/repos/${ownerSegment}/${repoSegment}/pulls/${prNumber}/comments`,
1547
+ {
1548
+ method: 'POST',
1549
+ headers: {
1550
+ Authorization: `Bearer ${this.ghToken}`,
1551
+ 'Content-Type': 'application/json',
1552
+ Accept: 'application/vnd.github+json',
1553
+ },
1554
+ body: JSON.stringify({
1555
+ body: commentBody,
1556
+ commit_id: commitId,
1557
+ path,
1558
+ line,
1559
+ side,
1560
+ }),
1525
1561
  },
1526
- body: JSON.stringify({
1527
- body: commentBody,
1528
- commit_id: commitId,
1529
- path,
1530
- line,
1531
- side,
1532
- }),
1533
- },
1562
+ ),
1534
1563
  );
1535
1564
  if (!response.ok) {
1536
1565
  const reason = await this.formatGitHubErrorWithStatus(response);
@@ -1576,7 +1605,17 @@ export class ApiV3CheerioRestIssueRepository
1576
1605
  response: Response,
1577
1606
  ): Promise<string> => {
1578
1607
  const status = `HTTP ${response.status}`;
1608
+ const bodyText = await response.clone().text();
1579
1609
  const reason = await this.readGitHubErrorReason(response);
1610
+ if (hasRateLimitSignals(response.status, response.headers, bodyText)) {
1611
+ const resetIso = computeRateLimitResetIso(response.headers);
1612
+ const resetSuffix = resetIso === null ? '' : ` (resets at ${resetIso})`;
1613
+ return `${status} GitHub rate limit exceeded, please retry shortly${resetSuffix}`;
1614
+ }
1615
+ if (response.status === 403) {
1616
+ const permissionSuffix = reason === null ? '' : ` ${reason}`;
1617
+ return `${status} permission denied, the token cannot perform this operation${permissionSuffix}`;
1618
+ }
1580
1619
  if (reason === null) {
1581
1620
  return status;
1582
1621
  }
@@ -1588,18 +1627,21 @@ export class ApiV3CheerioRestIssueRepository
1588
1627
  branchName: string,
1589
1628
  ): Promise<void> => {
1590
1629
  const { owner, repo } = this.parseIssueUrl(prUrl);
1591
- const response = await fetch(
1592
- `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${encodeURIComponent(branchName)}`,
1593
- {
1594
- method: 'DELETE',
1595
- headers: {
1596
- Authorization: `Bearer ${this.ghToken}`,
1630
+ const response = await this.fetchWithRateLimitRetry(() =>
1631
+ fetch(
1632
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/refs/heads/${encodeURIComponent(branchName)}`,
1633
+ {
1634
+ method: 'DELETE',
1635
+ headers: {
1636
+ Authorization: `Bearer ${this.ghToken}`,
1637
+ },
1597
1638
  },
1598
- },
1639
+ ),
1599
1640
  );
1600
1641
  if (!response.ok && response.status !== 422) {
1642
+ const reason = await this.formatGitHubErrorWithStatus(response);
1601
1643
  throw new Error(
1602
- `Failed to delete branch ${branchName} for PR ${prUrl}: HTTP ${response.status}`,
1644
+ `Failed to delete branch ${branchName} for PR ${prUrl}: ${reason}`,
1603
1645
  );
1604
1646
  }
1605
1647
  };
@@ -1613,20 +1655,21 @@ export class ApiV3CheerioRestIssueRepository
1613
1655
 
1614
1656
  getIssueOrPullRequestBody = async (url: string): Promise<string> => {
1615
1657
  const { owner, repo, issueNumber } = this.parseIssueUrl(url);
1616
- const response = await fetch(
1617
- `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`,
1618
- {
1619
- method: 'GET',
1620
- headers: {
1621
- Authorization: `Bearer ${this.ghToken}`,
1622
- Accept: 'application/vnd.github+json',
1658
+ const response = await this.fetchWithRateLimitRetry(() =>
1659
+ fetch(
1660
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}`,
1661
+ {
1662
+ method: 'GET',
1663
+ headers: {
1664
+ Authorization: `Bearer ${this.ghToken}`,
1665
+ Accept: 'application/vnd.github+json',
1666
+ },
1623
1667
  },
1624
- },
1668
+ ),
1625
1669
  );
1626
1670
  if (!response.ok) {
1627
- throw new Error(
1628
- `Failed to fetch body for ${url}: HTTP ${response.status}`,
1629
- );
1671
+ const reason = await this.formatGitHubErrorWithStatus(response);
1672
+ throw new Error(`Failed to fetch body for ${url}: ${reason}`);
1630
1673
  }
1631
1674
  const body: unknown = await response.json();
1632
1675
  if (!isIssueOrPullRequestBodyResponse(body)) {
@@ -1646,20 +1689,21 @@ export class ApiV3CheerioRestIssueRepository
1646
1689
  let page = 1;
1647
1690
  let hasMore = true;
1648
1691
  while (hasMore) {
1649
- const response = await fetch(
1650
- `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=${perPage}&page=${page}`,
1651
- {
1652
- method: 'GET',
1653
- headers: {
1654
- Authorization: `Bearer ${this.ghToken}`,
1655
- Accept: 'application/vnd.github+json',
1692
+ const response = await this.fetchWithRateLimitRetry(() =>
1693
+ fetch(
1694
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}/comments?per_page=${perPage}&page=${page}`,
1695
+ {
1696
+ method: 'GET',
1697
+ headers: {
1698
+ Authorization: `Bearer ${this.ghToken}`,
1699
+ Accept: 'application/vnd.github+json',
1700
+ },
1656
1701
  },
1657
- },
1702
+ ),
1658
1703
  );
1659
1704
  if (!response.ok) {
1660
- throw new Error(
1661
- `Failed to fetch comments for ${url}: HTTP ${response.status}`,
1662
- );
1705
+ const reason = await this.formatGitHubErrorWithStatus(response);
1706
+ throw new Error(`Failed to fetch comments for ${url}: ${reason}`);
1663
1707
  }
1664
1708
  const body: unknown = await response.json();
1665
1709
  if (!isIssueCommentsResponse(body)) {
@@ -1695,20 +1739,21 @@ export class ApiV3CheerioRestIssueRepository
1695
1739
  if (!isPr) {
1696
1740
  return null;
1697
1741
  }
1698
- const detailResponse = await fetch(
1699
- `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`,
1700
- {
1701
- method: 'GET',
1702
- headers: {
1703
- Authorization: `Bearer ${this.ghToken}`,
1704
- Accept: 'application/vnd.github+json',
1742
+ const detailResponse = await this.fetchWithRateLimitRetry(() =>
1743
+ fetch(
1744
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}`,
1745
+ {
1746
+ method: 'GET',
1747
+ headers: {
1748
+ Authorization: `Bearer ${this.ghToken}`,
1749
+ Accept: 'application/vnd.github+json',
1750
+ },
1705
1751
  },
1706
- },
1752
+ ),
1707
1753
  );
1708
1754
  if (!detailResponse.ok) {
1709
- throw new Error(
1710
- `Failed to fetch detail for PR ${prUrl}: HTTP ${detailResponse.status}`,
1711
- );
1755
+ const reason = await this.formatGitHubErrorWithStatus(detailResponse);
1756
+ throw new Error(`Failed to fetch detail for PR ${prUrl}: ${reason}`);
1712
1757
  }
1713
1758
  const detailBody: unknown = await detailResponse.json();
1714
1759
  if (!isPullRequestDetailResponse(detailBody)) {
@@ -1748,20 +1793,21 @@ export class ApiV3CheerioRestIssueRepository
1748
1793
  let page = 1;
1749
1794
  let hasMore = true;
1750
1795
  while (hasMore) {
1751
- const response = await fetch(
1752
- `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=${perPage}&page=${page}`,
1753
- {
1754
- method: 'GET',
1755
- headers: {
1756
- Authorization: `Bearer ${this.ghToken}`,
1757
- Accept: 'application/vnd.github+json',
1796
+ const response = await this.fetchWithRateLimitRetry(() =>
1797
+ fetch(
1798
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/files?per_page=${perPage}&page=${page}`,
1799
+ {
1800
+ method: 'GET',
1801
+ headers: {
1802
+ Authorization: `Bearer ${this.ghToken}`,
1803
+ Accept: 'application/vnd.github+json',
1804
+ },
1758
1805
  },
1759
- },
1806
+ ),
1760
1807
  );
1761
1808
  if (!response.ok) {
1762
- throw new Error(
1763
- `Failed to fetch files for PR ${prUrl}: HTTP ${response.status}`,
1764
- );
1809
+ const reason = await this.formatGitHubErrorWithStatus(response);
1810
+ throw new Error(`Failed to fetch files for PR ${prUrl}: ${reason}`);
1765
1811
  }
1766
1812
  const body: unknown = await response.json();
1767
1813
  if (!isPullRequestDetailFilesResponse(body)) {
@@ -1804,20 +1850,21 @@ export class ApiV3CheerioRestIssueRepository
1804
1850
  let page = 1;
1805
1851
  let hasMore = true;
1806
1852
  while (hasMore) {
1807
- const response = await fetch(
1808
- `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/commits?per_page=${perPage}&page=${page}`,
1809
- {
1810
- method: 'GET',
1811
- headers: {
1812
- Authorization: `Bearer ${this.ghToken}`,
1813
- Accept: 'application/vnd.github+json',
1853
+ const response = await this.fetchWithRateLimitRetry(() =>
1854
+ fetch(
1855
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/commits?per_page=${perPage}&page=${page}`,
1856
+ {
1857
+ method: 'GET',
1858
+ headers: {
1859
+ Authorization: `Bearer ${this.ghToken}`,
1860
+ Accept: 'application/vnd.github+json',
1861
+ },
1814
1862
  },
1815
- },
1863
+ ),
1816
1864
  );
1817
1865
  if (!response.ok) {
1818
- throw new Error(
1819
- `Failed to fetch commits for PR ${prUrl}: HTTP ${response.status}`,
1820
- );
1866
+ const reason = await this.formatGitHubErrorWithStatus(response);
1867
+ throw new Error(`Failed to fetch commits for PR ${prUrl}: ${reason}`);
1821
1868
  }
1822
1869
  const body: unknown = await response.json();
1823
1870
  if (!isPullRequestCommitsResponse(body)) {
@@ -1847,20 +1894,21 @@ export class ApiV3CheerioRestIssueRepository
1847
1894
  ): Promise<{ state: string; merged: boolean; isPullRequest: boolean }> => {
1848
1895
  const { owner, repo, issueNumber, isPr } = this.parseIssueUrl(url);
1849
1896
  if (isPr) {
1850
- const response = await fetch(
1851
- `https://api.github.com/repos/${owner}/${repo}/pulls/${issueNumber}`,
1852
- {
1853
- method: 'GET',
1854
- headers: {
1855
- Authorization: `Bearer ${this.ghToken}`,
1856
- Accept: 'application/vnd.github+json',
1897
+ const response = await this.fetchWithRateLimitRetry(() =>
1898
+ fetch(
1899
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${issueNumber}`,
1900
+ {
1901
+ method: 'GET',
1902
+ headers: {
1903
+ Authorization: `Bearer ${this.ghToken}`,
1904
+ Accept: 'application/vnd.github+json',
1905
+ },
1857
1906
  },
1858
- },
1907
+ ),
1859
1908
  );
1860
1909
  if (!response.ok) {
1861
- throw new Error(
1862
- `Failed to fetch state for ${url}: HTTP ${response.status}`,
1863
- );
1910
+ const reason = await this.formatGitHubErrorWithStatus(response);
1911
+ throw new Error(`Failed to fetch state for ${url}: ${reason}`);
1864
1912
  }
1865
1913
  const body: unknown = await response.json();
1866
1914
  if (!isPullRequestDetailResponse(body)) {
@@ -1870,20 +1918,21 @@ export class ApiV3CheerioRestIssueRepository
1870
1918
  }
1871
1919
  return { state: body.state, merged: body.merged, isPullRequest: true };
1872
1920
  }
1873
- const response = await fetch(
1874
- `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`,
1875
- {
1876
- method: 'GET',
1877
- headers: {
1878
- Authorization: `Bearer ${this.ghToken}`,
1879
- Accept: 'application/vnd.github+json',
1921
+ const response = await this.fetchWithRateLimitRetry(() =>
1922
+ fetch(
1923
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}`,
1924
+ {
1925
+ method: 'GET',
1926
+ headers: {
1927
+ Authorization: `Bearer ${this.ghToken}`,
1928
+ Accept: 'application/vnd.github+json',
1929
+ },
1880
1930
  },
1881
- },
1931
+ ),
1882
1932
  );
1883
1933
  if (!response.ok) {
1884
- throw new Error(
1885
- `Failed to fetch state for ${url}: HTTP ${response.status}`,
1886
- );
1934
+ const reason = await this.formatGitHubErrorWithStatus(response);
1935
+ throw new Error(`Failed to fetch state for ${url}: ${reason}`);
1887
1936
  }
1888
1937
  const body: unknown = await response.json();
1889
1938
  if (!isIssueOrPullRequestStateResponse(body)) {
@@ -1912,20 +1961,21 @@ export class ApiV3CheerioRestIssueRepository
1912
1961
  if (!isPr) {
1913
1962
  return null;
1914
1963
  }
1915
- const response = await fetch(
1916
- `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`,
1917
- {
1918
- method: 'GET',
1919
- headers: {
1920
- Authorization: `Bearer ${this.ghToken}`,
1921
- Accept: 'application/vnd.github+json',
1964
+ const response = await this.fetchWithRateLimitRetry(() =>
1965
+ fetch(
1966
+ `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}`,
1967
+ {
1968
+ method: 'GET',
1969
+ headers: {
1970
+ Authorization: `Bearer ${this.ghToken}`,
1971
+ Accept: 'application/vnd.github+json',
1972
+ },
1922
1973
  },
1923
- },
1974
+ ),
1924
1975
  );
1925
1976
  if (!response.ok) {
1926
- throw new Error(
1927
- `Failed to fetch summary for PR ${prUrl}: HTTP ${response.status}`,
1928
- );
1977
+ const reason = await this.formatGitHubErrorWithStatus(response);
1978
+ throw new Error(`Failed to fetch summary for PR ${prUrl}: ${reason}`);
1929
1979
  }
1930
1980
  const body: unknown = await response.json();
1931
1981
  if (!isPullRequestDetailResponse(body)) {