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

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 +7 -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 +9 -9
  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
@@ -62,4 +62,4 @@ jobs:
62
62
  env:
63
63
  NODE_PATH: /tmp/commitlint-run/node_modules
64
64
  run: |
65
- /tmp/commitlint-run/node_modules/.bin/commitlint --from=origin/main --to=HEAD --config $GITHUB_WORKSPACE/.github/workflows/configs/commitlint.config.js
65
+ /tmp/commitlint-run/node_modules/.bin/commitlint --from=origin/main --to=HEAD --config $GITHUB_WORKSPACE/.github/workflows/configs/commitlint.config.cjs
@@ -0,0 +1,32 @@
1
+ // DO NOT EDIT DIRECTLY.
2
+ // This file is auto-synchronized from HiromiShikata/repositories-management.
3
+ // Direct edits in downstream repositories will be overwritten by the next sync.
4
+ // Update the source file in HiromiShikata/repositories-management instead.
5
+
6
+ module.exports = {
7
+ extends: ['@commitlint/config-conventional'],
8
+ rules: {
9
+ 'type-enum': [
10
+ 2,
11
+ 'always',
12
+ [
13
+ 'build',
14
+ 'chore',
15
+ 'ci',
16
+ 'docs',
17
+ 'feat',
18
+ 'fix',
19
+ 'perf',
20
+ 'refactor',
21
+ 'revert',
22
+ 'style',
23
+ 'test',
24
+ 'autogen',
25
+ 'prep',
26
+ 'adapt',
27
+ ],
28
+ ],
29
+ 'header-max-length': [0],
30
+ 'body-max-line-length': [0],
31
+ },
32
+ };
@@ -51,6 +51,8 @@ jobs:
51
51
  umino-job:
52
52
  if: >-
53
53
  github.actor != 'dependabot[bot]' &&
54
+ github.actor != 'renovate[bot]' &&
55
+ github.actor != 'app/dependabot' &&
54
56
  ((github.event_name != 'issue_comment') ||
55
57
  (github.event.comment.user.login == 'HiromiShikata' &&
56
58
  (contains(github.event.comment.body, '/createissue') ||
@@ -277,7 +279,9 @@ jobs:
277
279
  name: Check linked issues in pull requests
278
280
  if: >-
279
281
  (github.event_name == 'pull_request') &&
280
- github.actor != 'dependabot[bot]'
282
+ github.actor != 'dependabot[bot]' &&
283
+ github.actor != 'renovate[bot]' &&
284
+ github.actor != 'app/dependabot'
281
285
  steps:
282
286
  - uses: step-security/harden-runner@v2
283
287
  with:
@@ -291,9 +295,11 @@ jobs:
291
295
  owner: HiromiShikata
292
296
  - uses: nearform-actions/github-action-check-linked-issues@7140e2e01aa0c18b8ac61bddf5e89abde1ca760e
293
297
  id: check-linked-issues
294
- if: github.event.pull_request.user.login != 'dependabot[bot]'
298
+ if: >-
299
+ github.event.pull_request.user.login != 'dependabot[bot]' &&
300
+ github.event.pull_request.user.login != 'app/dependabot'
295
301
  with:
296
- exclude-branches: 'release/**, dependabot/**, project-common/**, renovate/**'
302
+ exclude-branches: 'release/**, dependabot/**, dependabot-**, project-common/**, renovate/**'
297
303
  github-token: ${{ steps.app-token.outputs.token }}
298
304
  loose-matching: true
299
305
  skip-linked-issues-check-label: ''
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [1.116.4](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.116.3...v1.116.4) (2026-06-29)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **console:** retry transient GitHub rate-limit on item operations with bounded backoff and clear error classification ([#1082](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/1082)) ([61657ed](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/61657edb588f7ba381009fb15d7a52960dda8ad6))
7
+
1
8
  ## [1.116.2](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.116.1...v1.116.2) (2026-06-29)
2
9
 
3
10
 
package/README.md CHANGED
@@ -110,9 +110,9 @@ Each project's status and story options are loaded lazily the first time a `pjco
110
110
 
111
111
  The `checkIssueReviewReadiness` sub-command lets an agent self-check whether an issue is currently review-ready. It does NOT change the issue Status field and does NOT post any comment. It writes a single JSON line to stdout of the shape `{ "reviewReady": boolean, "rejections": [{ "type": string, "detail": string }] }` and exits 0 on a successful evaluation regardless of readiness; a non-zero exit indicates an operational error (auth failure, network error). The rejection types include: `ISSUE_NOT_FOUND`, `NO_REPORT_FROM_AGENT_BOT`, `REPORT_HAS_NEXT_STEP`, `PULL_REQUEST_NOT_FOUND`, `PULL_REQUEST_IS_DRAFT`, `PULL_REQUEST_CONFLICTED`, `ANY_CI_JOB_FAILED_OR_IN_PROGRESS`, `REQUIRED_CI_JOB_NEVER_STARTED`, `ANY_REVIEW_COMMENT_NOT_RESOLVED`, and `MULTIPLE_PULL_REQUESTS_FOUND`. The `--projectUrl` option is optional; when omitted the command still runs using only the issue URL.
112
112
 
113
- The `selectOauthToken` sub-command reads the same per-token rate-limit cache that the `startDaemon` proxy writes (see "Claude OAuth Token Rotation" below) and prints exactly one token string to stdout so a caller can choose an appropriate token before launching Claude Code. It is read-only: it never starts the proxy, never mutates any cache file, and never writes the token anywhere. Selection runs in two stages. First, a candidate filter keeps tokens whose 5-hour window is at least 60% free (5-hour utilization at most 0.40) AND whose 7-day window is at least 30% free (7-day utilization at most 0.70), where "% free" is `1 - utilization`. A token with no cache file, or whose window reset epoch has already passed, is treated as fully free (utilization 0) for these checks. Second, among the surviving candidates it selects the single token whose 7-day window reset epoch is nearest in the future (soonest reset), so weekly quota that would otherwise reset unused is consumed first; a candidate with no active 7-day window is treated as having the farthest reset (now + 7 days) and therefore sorts last. The selected token string is written to stdout (pipeable) and the per-candidate decision trace is written to stderr. When no token passes the filter, nothing is written to stdout and the command exits non-zero with an explanatory message on stderr. The token-list path comes from `--tokenListJsonPath` or the `CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH` environment variable; the cache directory comes from `--cacheDir` or the `TDPM_RATELIMIT_CACHE_DIR` environment variable, defaulting to `${XDG_CACHE_HOME:-~/.cache}/tdpm/ratelimit`.
113
+ The `selectOauthToken` sub-command reads the same per-token rate-limit cache that the `startDaemon` proxy writes (see "Claude OAuth Token Rotation" below) and prints exactly one token string to stdout so a caller can choose an appropriate token before launching Claude Code. It is read-only: it never starts the proxy, never mutates any cache file, and never writes the token anywhere. Selection runs in two stages. First, a candidate filter keeps tokens whose 5-hour window is at least 60% free (5-hour utilization at most 0.40) AND whose 7-day window is at least 7% free (7-day utilization at most 0.93), where "% free" is `1 - utilization`. A token with no cache file, or whose window reset epoch has already passed, is treated as fully free (utilization 0) for these checks. Second, among the surviving candidates it selects the single token whose 7-day window reset epoch is nearest in the future (soonest reset), so weekly quota that would otherwise reset unused is consumed first; a candidate with no active 7-day window is treated as having the farthest reset (now + 7 days) and therefore sorts last. The selected token string is written to stdout (pipeable) and the per-candidate decision trace is written to stderr. When no token passes the filter, nothing is written to stdout and the command exits non-zero with an explanatory message on stderr. The token-list path comes from `--tokenListJsonPath` or the `CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH` environment variable; the cache directory comes from `--cacheDir` or the `TDPM_RATELIMIT_CACHE_DIR` environment variable, defaulting to `${XDG_CACHE_HOME:-~/.cache}/tdpm/ratelimit`.
114
114
 
115
- The `selectLiveSessionOauthToken` sub-command picks a token for a new live interactive Claude Code session a human is about to start, so that concurrent interactive sessions spread across distinct tokens instead of stacking onto the same one. It loads the token list and reads the same per-token rate-limit cache as `selectOauthToken`, and it is read-only in exactly the same way: it never starts the proxy, never mutates any cache file, and never writes the token anywhere. It applies the identical rate-limit eligibility filter as `selectOauthToken` (5-hour window at least 60% free AND 7-day window at least 30% free, with a missing cache file or an expired window treated as fully free). It additionally measures current live occupancy per token by scanning running Claude Code processes on the local Linux host: for each process under `/proc` it reads the NUL-separated `/proc/<pid>/environ` and, when the process is a Claude Code process, takes its `CLAUDE_CODE_OAUTH_TOKEN` and `CLAUDE_CODE_SESSION_ID`. A token's occupancy is the number of distinct `CLAUDE_CODE_SESSION_ID` values bound to it, so child processes that inherit one session id count once. Processes without `CLAUDE_CODE_OAUTH_TOKEN` (for example API-key sessions) are ignored, and a process whose environ cannot be read is skipped. Among the eligible tokens it selects the one with the fewest live sessions (ideally zero), breaking ties by the soonest 7-day reset. The selected token string is written to stdout (pipeable) and the per-candidate decision trace is written to stderr; when no token passes the filter, nothing is written to stdout and the command exits non-zero with an explanatory message on stderr. The token-list path and cache directory are resolved exactly as for `selectOauthToken`. Because occupancy is read from `/proc/<pid>/environ`, this sub-command is Linux-specific.
115
+ The `selectLiveSessionOauthToken` sub-command picks a token for a new live interactive Claude Code session a human is about to start, so that concurrent interactive sessions spread across distinct tokens instead of stacking onto the same one. It loads the token list and reads the same per-token rate-limit cache as `selectOauthToken`, and it is read-only in exactly the same way: it never starts the proxy, never mutates any cache file, and never writes the token anywhere. It applies the identical rate-limit eligibility filter as `selectOauthToken` (5-hour window at least 60% free AND 7-day window at least 7% free, with a missing cache file or an expired window treated as fully free). It additionally measures current live occupancy per token by scanning running Claude Code processes on the local Linux host: for each process under `/proc` it reads the NUL-separated `/proc/<pid>/environ` and, when the process is a Claude Code process, takes its `CLAUDE_CODE_OAUTH_TOKEN` and `CLAUDE_CODE_SESSION_ID`. A token's occupancy is the number of distinct `CLAUDE_CODE_SESSION_ID` values bound to it, so child processes that inherit one session id count once. Processes without `CLAUDE_CODE_OAUTH_TOKEN` (for example API-key sessions) are ignored, and a process whose environ cannot be read is skipped. Among the eligible tokens it selects the one with the fewest live sessions (ideally zero), breaking ties by the soonest 7-day reset. The selected token string is written to stdout (pipeable) and the per-candidate decision trace is written to stderr; when no token passes the filter, nothing is written to stdout and the command exits non-zero with an explanatory message on stderr. The token-list path and cache directory are resolved exactly as for `selectOauthToken`. Because occupancy is read from `/proc/<pid>/environ`, this sub-command is Linux-specific.
116
116
 
117
117
  The `countInTmuxByHumanSessionsPerToken` sub-command reports, per Claude Code OAuth token, how many live interactive sessions running under that token belong to an issue that is currently in the GitHub Project Status `In Tmux by human`. It is read-only: it never starts the proxy, never mutates any cache file, and never writes any token anywhere. It enumerates live interactive sessions by scanning running processes on the local Linux host: for each process under `/proc` it reads the NUL-separated `/proc/<pid>/cmdline` and selects only processes launched for an interactive session, identified by a `--name <issue-url>` argument whose value is an HTTP or HTTPS URL; `Take ownership` background spawns (which have no `--name` argument) are excluded. For each selected process it reads `/proc/<pid>/environ` and takes its `CLAUDE_CODE_OAUTH_TOKEN` and `CLAUDE_CODE_SESSION_ID`; a process missing either is skipped, and a process whose files cannot be read is skipped. It then loads the project's issues with their Status (reusing the same on-disk issue cache the daemon uses, honoring `allowIssueCacheMinutes`), keeps only the sessions whose `--name` issue URL maps to an open issue in Status `In Tmux by human`, and counts the distinct `CLAUDE_CODE_SESSION_ID` values per token so child processes that inherit one session id count once. It writes one tab-separated line per token to stdout in the form `<tokenName>\t<count>` (the token name comes from the token list; the raw token value is never printed), and writes the per-run decision trace to stderr. The token-list path comes from `--tokenListJsonPath`, the `claudeCodeOauthTokenListJsonPath` config value, or the `CLAUDE_CODE_OAUTH_TOKEN_LIST_JSON_PATH` environment variable. Because occupancy is read from `/proc/<pid>/cmdline` and `/proc/<pid>/environ`, this sub-command is Linux-specific.
118
118
 
@@ -39,6 +39,7 @@ const path = __importStar(require("path"));
39
39
  const DEFAULT_PROC_DIRECTORY = '/proc';
40
40
  const OAUTH_TOKEN_ENVIRON_KEY = 'CLAUDE_CODE_OAUTH_TOKEN';
41
41
  const SESSION_ID_ENVIRON_KEY = 'CLAUDE_CODE_SESSION_ID';
42
+ const CONFIG_DIR_ENVIRON_KEY = 'CLAUDE_CONFIG_DIR';
42
43
  const isClaudeProcessCommand = (command) => {
43
44
  if (command.length === 0) {
44
45
  return false;
@@ -78,17 +79,28 @@ class ProcClaudeLiveSessionRepository {
78
79
  return null;
79
80
  }
80
81
  const token = environ.get(OAUTH_TOKEN_ENVIRON_KEY);
81
- const sessionId = environ.get(SESSION_ID_ENVIRON_KEY);
82
- if (token === undefined ||
83
- token.length === 0 ||
84
- sessionId === undefined ||
85
- sessionId.length === 0) {
82
+ if (token === undefined || token.length === 0) {
83
+ return null;
84
+ }
85
+ const sessionKey = this.deriveSessionKey(environ);
86
+ if (sessionKey === null) {
86
87
  return null;
87
88
  }
88
89
  if (!this.isClaudeProcess(processIdDirectory)) {
89
90
  return null;
90
91
  }
91
- return { token, sessionId };
92
+ return { token, sessionKey };
93
+ };
94
+ this.deriveSessionKey = (environ) => {
95
+ const configDir = environ.get(CONFIG_DIR_ENVIRON_KEY);
96
+ if (configDir !== undefined && configDir.length > 0) {
97
+ return configDir;
98
+ }
99
+ const sessionId = environ.get(SESSION_ID_ENVIRON_KEY);
100
+ if (sessionId !== undefined && sessionId.length > 0) {
101
+ return sessionId;
102
+ }
103
+ return null;
92
104
  };
93
105
  this.isClaudeProcess = (processIdDirectory) => {
94
106
  const basePath = path.join(this.procDirectory, processIdDirectory);
@@ -1 +1 @@
1
- {"version":3,"file":"ProcClaudeLiveSessionRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/ProcClaudeLiveSessionRepository.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ProcClaudeLiveSessionRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/ProcClaudeLiveSessionRepository.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,7 @@ exports.ApiV3CheerioRestIssueRepository = void 0;
7
7
  const typia_1 = __importDefault(require("typia"));
8
8
  const BaseGitHubRepository_1 = require("../BaseGitHubRepository");
9
9
  const utils_1 = require("../utils");
10
+ const githubRateLimitRetry_1 = require("./githubRateLimitRetry");
10
11
  function isIssueTimelineResponse(value) {
11
12
  if (typeof value !== 'object' || value === null)
12
13
  return false;
@@ -156,7 +157,7 @@ const fnmatch = (pattern, str) => {
156
157
  }
157
158
  };
158
159
  class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubRepository {
159
- constructor(apiV3IssueRepository, restIssueRepository, graphqlProjectItemRepository, localStorageCacheRepository, localStorageRepository, ghToken = process.env.GH_TOKEN || 'dummy') {
160
+ constructor(apiV3IssueRepository, restIssueRepository, graphqlProjectItemRepository, localStorageCacheRepository, localStorageRepository, ghToken = process.env.GH_TOKEN || 'dummy', sleep = githubRateLimitRetry_1.realSleep) {
160
161
  super(localStorageRepository, ghToken);
161
162
  this.apiV3IssueRepository = apiV3IssueRepository;
162
163
  this.restIssueRepository = restIssueRepository;
@@ -164,6 +165,8 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
164
165
  this.localStorageCacheRepository = localStorageCacheRepository;
165
166
  this.localStorageRepository = localStorageRepository;
166
167
  this.ghToken = ghToken;
168
+ this.sleep = sleep;
169
+ this.fetchWithRateLimitRetry = (request) => (0, githubRateLimitRetry_1.fetchWithGitHubRateLimitRetry)(request, this.sleep);
167
170
  this.updateStatus = async (project, issue, statusId) => {
168
171
  await this.graphqlProjectItemRepository.updateProjectField(project.id, project.status.fieldId, issue.itemId, { singleSelectOptionId: statusId });
169
172
  };
@@ -578,7 +581,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
578
581
  let after = null;
579
582
  let hasNextPage = true;
580
583
  while (hasNextPage) {
581
- const response = await fetch('https://api.github.com/graphql', {
584
+ const response = await this.fetchWithRateLimitRetry(() => fetch('https://api.github.com/graphql', {
582
585
  method: 'POST',
583
586
  headers: {
584
587
  Authorization: `Bearer ${this.ghToken}`,
@@ -588,7 +591,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
588
591
  query,
589
592
  variables: { owner, repo, issueNumber, after },
590
593
  }),
591
- });
594
+ }));
592
595
  if (!response.ok) {
593
596
  throw new Error(`Failed to fetch issue timeline from GitHub GraphQL API: HTTP ${response.status}`);
594
597
  }
@@ -728,7 +731,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
728
731
  }
729
732
  }
730
733
  `;
731
- const response = await fetch('https://api.github.com/graphql', {
734
+ const response = await this.fetchWithRateLimitRetry(() => fetch('https://api.github.com/graphql', {
732
735
  method: 'POST',
733
736
  headers: {
734
737
  Authorization: `Bearer ${this.ghToken}`,
@@ -738,7 +741,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
738
741
  query,
739
742
  variables: { owner, repo, prNumber },
740
743
  }),
741
- });
744
+ }));
742
745
  if (!response.ok) {
743
746
  throw new Error(`Failed to fetch pull request from GitHub GraphQL API: HTTP ${response.status}`);
744
747
  }
@@ -757,14 +760,14 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
757
760
  };
758
761
  this.closePullRequest = async (prUrl) => {
759
762
  const { owner, repo, issueNumber: prNumber } = this.parseIssueUrl(prUrl);
760
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`, {
763
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}`, {
761
764
  method: 'PATCH',
762
765
  headers: {
763
766
  Authorization: `Bearer ${this.ghToken}`,
764
767
  'Content-Type': 'application/json',
765
768
  },
766
769
  body: JSON.stringify({ state: 'closed' }),
767
- });
770
+ }));
768
771
  if (!response.ok) {
769
772
  const reason = await this.formatGitHubErrorWithStatus(response);
770
773
  throw new Error(`Failed to close PR ${prUrl}: ${reason}`);
@@ -774,16 +777,17 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
774
777
  const { owner, repo, issueNumber } = this.parseIssueUrl(issueUrl);
775
778
  const ownerSegment = encodeURIComponent(owner);
776
779
  const repoSegment = encodeURIComponent(repo);
777
- const response = await fetch(`https://api.github.com/repos/${ownerSegment}/${repoSegment}/issues/${issueNumber}`, {
780
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${ownerSegment}/${repoSegment}/issues/${issueNumber}`, {
778
781
  method: 'PATCH',
779
782
  headers: {
780
783
  Authorization: `Bearer ${this.ghToken}`,
781
784
  'Content-Type': 'application/json',
782
785
  },
783
786
  body: JSON.stringify({ state: 'closed', state_reason: stateReason }),
784
- });
787
+ }));
785
788
  if (!response.ok) {
786
- throw new Error(`Failed to close issue ${issueUrl}: HTTP ${response.status}`);
789
+ const reason = await this.formatGitHubErrorWithStatus(response);
790
+ throw new Error(`Failed to close issue ${issueUrl}: ${reason}`);
787
791
  }
788
792
  };
789
793
  this.getPullRequestChangedFilePaths = async (prUrl) => {
@@ -793,15 +797,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
793
797
  let page = 1;
794
798
  let hasMore = true;
795
799
  while (hasMore) {
796
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=${perPage}&page=${page}`, {
800
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/files?per_page=${perPage}&page=${page}`, {
797
801
  method: 'GET',
798
802
  headers: {
799
803
  Authorization: `Bearer ${this.ghToken}`,
800
804
  Accept: 'application/vnd.github+json',
801
805
  },
802
- });
806
+ }));
803
807
  if (!response.ok) {
804
- throw new Error(`Failed to fetch changed files for PR ${prUrl}: HTTP ${response.status}`);
808
+ const reason = await this.formatGitHubErrorWithStatus(response);
809
+ throw new Error(`Failed to fetch changed files for PR ${prUrl}: ${reason}`);
805
810
  }
806
811
  const body = await response.json();
807
812
  if (!isPullRequestFilesResponse(body)) {
@@ -821,7 +826,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
821
826
  };
822
827
  this.approvePullRequest = async (prUrl) => {
823
828
  const { owner, repo, issueNumber: prNumber } = this.parseIssueUrl(prUrl);
824
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`, {
829
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/reviews`, {
825
830
  method: 'POST',
826
831
  headers: {
827
832
  Authorization: `Bearer ${this.ghToken}`,
@@ -829,7 +834,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
829
834
  Accept: 'application/vnd.github+json',
830
835
  },
831
836
  body: JSON.stringify({ event: 'APPROVE' }),
832
- });
837
+ }));
833
838
  if (!response.ok) {
834
839
  const reason = await this.formatGitHubErrorWithStatus(response);
835
840
  throw new Error(`Failed to approve PR ${prUrl}: ${reason}`);
@@ -851,7 +856,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
851
856
  },
852
857
  ],
853
858
  };
854
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`, {
859
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/reviews`, {
855
860
  method: 'POST',
856
861
  headers: {
857
862
  Authorization: `Bearer ${this.ghToken}`,
@@ -859,7 +864,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
859
864
  Accept: 'application/vnd.github+json',
860
865
  },
861
866
  body: JSON.stringify(reviewBody),
862
- });
867
+ }));
863
868
  if (!response.ok) {
864
869
  const reason = await this.formatGitHubErrorWithStatus(response);
865
870
  throw new Error(`Failed to request changes on PR ${prUrl}: ${reason}`);
@@ -868,15 +873,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
868
873
  this.fetchPullRequestHeadSha = async (owner, repo, prNumber, prUrl) => {
869
874
  const ownerSegment = encodeURIComponent(owner);
870
875
  const repoSegment = encodeURIComponent(repo);
871
- const response = await fetch(`https://api.github.com/repos/${ownerSegment}/${repoSegment}/pulls/${prNumber}`, {
876
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${ownerSegment}/${repoSegment}/pulls/${prNumber}`, {
872
877
  method: 'GET',
873
878
  headers: {
874
879
  Authorization: `Bearer ${this.ghToken}`,
875
880
  Accept: 'application/vnd.github+json',
876
881
  },
877
- });
882
+ }));
878
883
  if (!response.ok) {
879
- throw new Error(`Failed to fetch head commit for PR ${prUrl}: HTTP ${response.status}`);
884
+ const reason = await this.formatGitHubErrorWithStatus(response);
885
+ throw new Error(`Failed to fetch head commit for PR ${prUrl}: ${reason}`);
880
886
  }
881
887
  const body = await response.json();
882
888
  if (!isRecord(body) ||
@@ -891,7 +897,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
891
897
  const commitId = await this.fetchPullRequestHeadSha(owner, repo, prNumber, prUrl);
892
898
  const ownerSegment = encodeURIComponent(owner);
893
899
  const repoSegment = encodeURIComponent(repo);
894
- const response = await fetch(`https://api.github.com/repos/${ownerSegment}/${repoSegment}/pulls/${prNumber}/comments`, {
900
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${ownerSegment}/${repoSegment}/pulls/${prNumber}/comments`, {
895
901
  method: 'POST',
896
902
  headers: {
897
903
  Authorization: `Bearer ${this.ghToken}`,
@@ -905,7 +911,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
905
911
  line,
906
912
  side,
907
913
  }),
908
- });
914
+ }));
909
915
  if (!response.ok) {
910
916
  const reason = await this.formatGitHubErrorWithStatus(response);
911
917
  throw new Error(`Failed to create review comment on PR ${prUrl}: ${reason}`);
@@ -943,7 +949,17 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
943
949
  };
944
950
  this.formatGitHubErrorWithStatus = async (response) => {
945
951
  const status = `HTTP ${response.status}`;
952
+ const bodyText = await response.clone().text();
946
953
  const reason = await this.readGitHubErrorReason(response);
954
+ if ((0, githubRateLimitRetry_1.hasRateLimitSignals)(response.status, response.headers, bodyText)) {
955
+ const resetIso = (0, githubRateLimitRetry_1.computeRateLimitResetIso)(response.headers);
956
+ const resetSuffix = resetIso === null ? '' : ` (resets at ${resetIso})`;
957
+ return `${status} GitHub rate limit exceeded, please retry shortly${resetSuffix}`;
958
+ }
959
+ if (response.status === 403) {
960
+ const permissionSuffix = reason === null ? '' : ` ${reason}`;
961
+ return `${status} permission denied, the token cannot perform this operation${permissionSuffix}`;
962
+ }
947
963
  if (reason === null) {
948
964
  return status;
949
965
  }
@@ -951,14 +967,15 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
951
967
  };
952
968
  this.deletePullRequestBranch = async (prUrl, branchName) => {
953
969
  const { owner, repo } = this.parseIssueUrl(prUrl);
954
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${encodeURIComponent(branchName)}`, {
970
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/refs/heads/${encodeURIComponent(branchName)}`, {
955
971
  method: 'DELETE',
956
972
  headers: {
957
973
  Authorization: `Bearer ${this.ghToken}`,
958
974
  },
959
- });
975
+ }));
960
976
  if (!response.ok && response.status !== 422) {
961
- throw new Error(`Failed to delete branch ${branchName} for PR ${prUrl}: HTTP ${response.status}`);
977
+ const reason = await this.formatGitHubErrorWithStatus(response);
978
+ throw new Error(`Failed to delete branch ${branchName} for PR ${prUrl}: ${reason}`);
962
979
  }
963
980
  };
964
981
  this.createCommentByUrl = async (issueOrPrUrl, commentBody) => {
@@ -966,15 +983,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
966
983
  };
967
984
  this.getIssueOrPullRequestBody = async (url) => {
968
985
  const { owner, repo, issueNumber } = this.parseIssueUrl(url);
969
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`, {
986
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}`, {
970
987
  method: 'GET',
971
988
  headers: {
972
989
  Authorization: `Bearer ${this.ghToken}`,
973
990
  Accept: 'application/vnd.github+json',
974
991
  },
975
- });
992
+ }));
976
993
  if (!response.ok) {
977
- throw new Error(`Failed to fetch body for ${url}: HTTP ${response.status}`);
994
+ const reason = await this.formatGitHubErrorWithStatus(response);
995
+ throw new Error(`Failed to fetch body for ${url}: ${reason}`);
978
996
  }
979
997
  const body = await response.json();
980
998
  if (!isIssueOrPullRequestBodyResponse(body)) {
@@ -989,15 +1007,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
989
1007
  let page = 1;
990
1008
  let hasMore = true;
991
1009
  while (hasMore) {
992
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=${perPage}&page=${page}`, {
1010
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}/comments?per_page=${perPage}&page=${page}`, {
993
1011
  method: 'GET',
994
1012
  headers: {
995
1013
  Authorization: `Bearer ${this.ghToken}`,
996
1014
  Accept: 'application/vnd.github+json',
997
1015
  },
998
- });
1016
+ }));
999
1017
  if (!response.ok) {
1000
- throw new Error(`Failed to fetch comments for ${url}: HTTP ${response.status}`);
1018
+ const reason = await this.formatGitHubErrorWithStatus(response);
1019
+ throw new Error(`Failed to fetch comments for ${url}: ${reason}`);
1001
1020
  }
1002
1021
  const body = await response.json();
1003
1022
  if (!isIssueCommentsResponse(body)) {
@@ -1024,15 +1043,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
1024
1043
  if (!isPr) {
1025
1044
  return null;
1026
1045
  }
1027
- const detailResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`, {
1046
+ const detailResponse = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}`, {
1028
1047
  method: 'GET',
1029
1048
  headers: {
1030
1049
  Authorization: `Bearer ${this.ghToken}`,
1031
1050
  Accept: 'application/vnd.github+json',
1032
1051
  },
1033
- });
1052
+ }));
1034
1053
  if (!detailResponse.ok) {
1035
- throw new Error(`Failed to fetch detail for PR ${prUrl}: HTTP ${detailResponse.status}`);
1054
+ const reason = await this.formatGitHubErrorWithStatus(detailResponse);
1055
+ throw new Error(`Failed to fetch detail for PR ${prUrl}: ${reason}`);
1036
1056
  }
1037
1057
  const detailBody = await detailResponse.json();
1038
1058
  if (!isPullRequestDetailResponse(detailBody)) {
@@ -1059,15 +1079,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
1059
1079
  let page = 1;
1060
1080
  let hasMore = true;
1061
1081
  while (hasMore) {
1062
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=${perPage}&page=${page}`, {
1082
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/files?per_page=${perPage}&page=${page}`, {
1063
1083
  method: 'GET',
1064
1084
  headers: {
1065
1085
  Authorization: `Bearer ${this.ghToken}`,
1066
1086
  Accept: 'application/vnd.github+json',
1067
1087
  },
1068
- });
1088
+ }));
1069
1089
  if (!response.ok) {
1070
- throw new Error(`Failed to fetch files for PR ${prUrl}: HTTP ${response.status}`);
1090
+ const reason = await this.formatGitHubErrorWithStatus(response);
1091
+ throw new Error(`Failed to fetch files for PR ${prUrl}: ${reason}`);
1071
1092
  }
1072
1093
  const body = await response.json();
1073
1094
  if (!isPullRequestDetailFilesResponse(body)) {
@@ -1101,15 +1122,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
1101
1122
  let page = 1;
1102
1123
  let hasMore = true;
1103
1124
  while (hasMore) {
1104
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/commits?per_page=${perPage}&page=${page}`, {
1125
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/commits?per_page=${perPage}&page=${page}`, {
1105
1126
  method: 'GET',
1106
1127
  headers: {
1107
1128
  Authorization: `Bearer ${this.ghToken}`,
1108
1129
  Accept: 'application/vnd.github+json',
1109
1130
  },
1110
- });
1131
+ }));
1111
1132
  if (!response.ok) {
1112
- throw new Error(`Failed to fetch commits for PR ${prUrl}: HTTP ${response.status}`);
1133
+ const reason = await this.formatGitHubErrorWithStatus(response);
1134
+ throw new Error(`Failed to fetch commits for PR ${prUrl}: ${reason}`);
1113
1135
  }
1114
1136
  const body = await response.json();
1115
1137
  if (!isPullRequestCommitsResponse(body)) {
@@ -1135,15 +1157,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
1135
1157
  this.getIssueOrPullRequestState = async (url) => {
1136
1158
  const { owner, repo, issueNumber, isPr } = this.parseIssueUrl(url);
1137
1159
  if (isPr) {
1138
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${issueNumber}`, {
1160
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${issueNumber}`, {
1139
1161
  method: 'GET',
1140
1162
  headers: {
1141
1163
  Authorization: `Bearer ${this.ghToken}`,
1142
1164
  Accept: 'application/vnd.github+json',
1143
1165
  },
1144
- });
1166
+ }));
1145
1167
  if (!response.ok) {
1146
- throw new Error(`Failed to fetch state for ${url}: HTTP ${response.status}`);
1168
+ const reason = await this.formatGitHubErrorWithStatus(response);
1169
+ throw new Error(`Failed to fetch state for ${url}: ${reason}`);
1147
1170
  }
1148
1171
  const body = await response.json();
1149
1172
  if (!isPullRequestDetailResponse(body)) {
@@ -1151,15 +1174,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
1151
1174
  }
1152
1175
  return { state: body.state, merged: body.merged, isPullRequest: true };
1153
1176
  }
1154
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`, {
1177
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${issueNumber}`, {
1155
1178
  method: 'GET',
1156
1179
  headers: {
1157
1180
  Authorization: `Bearer ${this.ghToken}`,
1158
1181
  Accept: 'application/vnd.github+json',
1159
1182
  },
1160
- });
1183
+ }));
1161
1184
  if (!response.ok) {
1162
- throw new Error(`Failed to fetch state for ${url}: HTTP ${response.status}`);
1185
+ const reason = await this.formatGitHubErrorWithStatus(response);
1186
+ throw new Error(`Failed to fetch state for ${url}: ${reason}`);
1163
1187
  }
1164
1188
  const body = await response.json();
1165
1189
  if (!isIssueOrPullRequestStateResponse(body)) {
@@ -1172,15 +1196,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
1172
1196
  if (!isPr) {
1173
1197
  return null;
1174
1198
  }
1175
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`, {
1199
+ const response = await this.fetchWithRateLimitRetry(() => fetch(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}`, {
1176
1200
  method: 'GET',
1177
1201
  headers: {
1178
1202
  Authorization: `Bearer ${this.ghToken}`,
1179
1203
  Accept: 'application/vnd.github+json',
1180
1204
  },
1181
- });
1205
+ }));
1182
1206
  if (!response.ok) {
1183
- throw new Error(`Failed to fetch summary for PR ${prUrl}: HTTP ${response.status}`);
1207
+ const reason = await this.formatGitHubErrorWithStatus(response);
1208
+ throw new Error(`Failed to fetch summary for PR ${prUrl}: ${reason}`);
1184
1209
  }
1185
1210
  const body = await response.json();
1186
1211
  if (!isPullRequestDetailResponse(body)) {