github-issue-tower-defence-management 1.116.1 → 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 +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 +101 -60
  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 +237 -1
  22. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +274 -209
  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 +6 -2
  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,17 @@
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
+
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)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **console:** include GitHub response reason in PR-review operation errors ([#1080](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/1080)) ([00a2df7](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/00a2df7aa0b699a4081f391a9f8e3ad6f36d622f)), closes [#1079](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/1079)
14
+
1
15
  ## [1.116.1](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.116.0...v1.116.1) (2026-06-29)
2
16
 
3
17
 
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,32 +760,34 @@ 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
- throw new Error(`Failed to close PR ${prUrl}: HTTP ${response.status}`);
772
+ const reason = await this.formatGitHubErrorWithStatus(response);
773
+ throw new Error(`Failed to close PR ${prUrl}: ${reason}`);
770
774
  }
771
775
  };
772
776
  this.closeIssueByUrl = async (issueUrl, stateReason) => {
773
777
  const { owner, repo, issueNumber } = this.parseIssueUrl(issueUrl);
774
778
  const ownerSegment = encodeURIComponent(owner);
775
779
  const repoSegment = encodeURIComponent(repo);
776
- 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}`, {
777
781
  method: 'PATCH',
778
782
  headers: {
779
783
  Authorization: `Bearer ${this.ghToken}`,
780
784
  'Content-Type': 'application/json',
781
785
  },
782
786
  body: JSON.stringify({ state: 'closed', state_reason: stateReason }),
783
- });
787
+ }));
784
788
  if (!response.ok) {
785
- 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}`);
786
791
  }
787
792
  };
788
793
  this.getPullRequestChangedFilePaths = async (prUrl) => {
@@ -792,15 +797,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
792
797
  let page = 1;
793
798
  let hasMore = true;
794
799
  while (hasMore) {
795
- 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}`, {
796
801
  method: 'GET',
797
802
  headers: {
798
803
  Authorization: `Bearer ${this.ghToken}`,
799
804
  Accept: 'application/vnd.github+json',
800
805
  },
801
- });
806
+ }));
802
807
  if (!response.ok) {
803
- 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}`);
804
810
  }
805
811
  const body = await response.json();
806
812
  if (!isPullRequestFilesResponse(body)) {
@@ -820,7 +826,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
820
826
  };
821
827
  this.approvePullRequest = async (prUrl) => {
822
828
  const { owner, repo, issueNumber: prNumber } = this.parseIssueUrl(prUrl);
823
- 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`, {
824
830
  method: 'POST',
825
831
  headers: {
826
832
  Authorization: `Bearer ${this.ghToken}`,
@@ -828,9 +834,10 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
828
834
  Accept: 'application/vnd.github+json',
829
835
  },
830
836
  body: JSON.stringify({ event: 'APPROVE' }),
831
- });
837
+ }));
832
838
  if (!response.ok) {
833
- throw new Error(`Failed to approve PR ${prUrl}: HTTP ${response.status}`);
839
+ const reason = await this.formatGitHubErrorWithStatus(response);
840
+ throw new Error(`Failed to approve PR ${prUrl}: ${reason}`);
834
841
  }
835
842
  };
836
843
  this.requestChangesWithInlineComment = async (prUrl, changedFilePath, commentBody) => {
@@ -849,7 +856,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
849
856
  },
850
857
  ],
851
858
  };
852
- 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`, {
853
860
  method: 'POST',
854
861
  headers: {
855
862
  Authorization: `Bearer ${this.ghToken}`,
@@ -857,23 +864,25 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
857
864
  Accept: 'application/vnd.github+json',
858
865
  },
859
866
  body: JSON.stringify(reviewBody),
860
- });
867
+ }));
861
868
  if (!response.ok) {
862
- throw new Error(`Failed to request changes on PR ${prUrl}: HTTP ${response.status}`);
869
+ const reason = await this.formatGitHubErrorWithStatus(response);
870
+ throw new Error(`Failed to request changes on PR ${prUrl}: ${reason}`);
863
871
  }
864
872
  };
865
873
  this.fetchPullRequestHeadSha = async (owner, repo, prNumber, prUrl) => {
866
874
  const ownerSegment = encodeURIComponent(owner);
867
875
  const repoSegment = encodeURIComponent(repo);
868
- 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}`, {
869
877
  method: 'GET',
870
878
  headers: {
871
879
  Authorization: `Bearer ${this.ghToken}`,
872
880
  Accept: 'application/vnd.github+json',
873
881
  },
874
- });
882
+ }));
875
883
  if (!response.ok) {
876
- 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}`);
877
886
  }
878
887
  const body = await response.json();
879
888
  if (!isRecord(body) ||
@@ -888,7 +897,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
888
897
  const commitId = await this.fetchPullRequestHeadSha(owner, repo, prNumber, prUrl);
889
898
  const ownerSegment = encodeURIComponent(owner);
890
899
  const repoSegment = encodeURIComponent(repo);
891
- 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`, {
892
901
  method: 'POST',
893
902
  headers: {
894
903
  Authorization: `Bearer ${this.ghToken}`,
@@ -902,29 +911,34 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
902
911
  line,
903
912
  side,
904
913
  }),
905
- });
914
+ }));
906
915
  if (!response.ok) {
907
- const reason = await this.readGitHubErrorMessage(response);
916
+ const reason = await this.formatGitHubErrorWithStatus(response);
908
917
  throw new Error(`Failed to create review comment on PR ${prUrl}: ${reason}`);
909
918
  }
910
919
  };
911
- this.readGitHubErrorMessage = async (response) => {
912
- const fallback = `HTTP ${response.status}`;
920
+ this.readGitHubErrorReason = async (response) => {
913
921
  let parsed;
914
922
  try {
915
923
  parsed = await response.json();
916
924
  }
917
925
  catch {
918
- return fallback;
926
+ return null;
919
927
  }
920
928
  if (!isRecord(parsed) || typeof parsed.message !== 'string') {
921
- return fallback;
929
+ return null;
922
930
  }
923
931
  if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
924
932
  const details = parsed.errors
925
- .map((error) => isRecord(error) && typeof error.message === 'string'
926
- ? error.message
927
- : '')
933
+ .map((error) => {
934
+ if (typeof error === 'string') {
935
+ return error;
936
+ }
937
+ if (isRecord(error) && typeof error.message === 'string') {
938
+ return error.message;
939
+ }
940
+ return '';
941
+ })
928
942
  .filter((detail) => detail.length > 0)
929
943
  .join('; ');
930
944
  if (details.length > 0) {
@@ -933,16 +947,35 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
933
947
  }
934
948
  return parsed.message;
935
949
  };
950
+ this.formatGitHubErrorWithStatus = async (response) => {
951
+ const status = `HTTP ${response.status}`;
952
+ const bodyText = await response.clone().text();
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
+ }
963
+ if (reason === null) {
964
+ return status;
965
+ }
966
+ return `${status} ${reason}`;
967
+ };
936
968
  this.deletePullRequestBranch = async (prUrl, branchName) => {
937
969
  const { owner, repo } = this.parseIssueUrl(prUrl);
938
- 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)}`, {
939
971
  method: 'DELETE',
940
972
  headers: {
941
973
  Authorization: `Bearer ${this.ghToken}`,
942
974
  },
943
- });
975
+ }));
944
976
  if (!response.ok && response.status !== 422) {
945
- 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}`);
946
979
  }
947
980
  };
948
981
  this.createCommentByUrl = async (issueOrPrUrl, commentBody) => {
@@ -950,15 +983,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
950
983
  };
951
984
  this.getIssueOrPullRequestBody = async (url) => {
952
985
  const { owner, repo, issueNumber } = this.parseIssueUrl(url);
953
- 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}`, {
954
987
  method: 'GET',
955
988
  headers: {
956
989
  Authorization: `Bearer ${this.ghToken}`,
957
990
  Accept: 'application/vnd.github+json',
958
991
  },
959
- });
992
+ }));
960
993
  if (!response.ok) {
961
- 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}`);
962
996
  }
963
997
  const body = await response.json();
964
998
  if (!isIssueOrPullRequestBodyResponse(body)) {
@@ -973,15 +1007,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
973
1007
  let page = 1;
974
1008
  let hasMore = true;
975
1009
  while (hasMore) {
976
- 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}`, {
977
1011
  method: 'GET',
978
1012
  headers: {
979
1013
  Authorization: `Bearer ${this.ghToken}`,
980
1014
  Accept: 'application/vnd.github+json',
981
1015
  },
982
- });
1016
+ }));
983
1017
  if (!response.ok) {
984
- 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}`);
985
1020
  }
986
1021
  const body = await response.json();
987
1022
  if (!isIssueCommentsResponse(body)) {
@@ -1008,15 +1043,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
1008
1043
  if (!isPr) {
1009
1044
  return null;
1010
1045
  }
1011
- 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}`, {
1012
1047
  method: 'GET',
1013
1048
  headers: {
1014
1049
  Authorization: `Bearer ${this.ghToken}`,
1015
1050
  Accept: 'application/vnd.github+json',
1016
1051
  },
1017
- });
1052
+ }));
1018
1053
  if (!detailResponse.ok) {
1019
- 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}`);
1020
1056
  }
1021
1057
  const detailBody = await detailResponse.json();
1022
1058
  if (!isPullRequestDetailResponse(detailBody)) {
@@ -1043,15 +1079,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
1043
1079
  let page = 1;
1044
1080
  let hasMore = true;
1045
1081
  while (hasMore) {
1046
- 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}`, {
1047
1083
  method: 'GET',
1048
1084
  headers: {
1049
1085
  Authorization: `Bearer ${this.ghToken}`,
1050
1086
  Accept: 'application/vnd.github+json',
1051
1087
  },
1052
- });
1088
+ }));
1053
1089
  if (!response.ok) {
1054
- 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}`);
1055
1092
  }
1056
1093
  const body = await response.json();
1057
1094
  if (!isPullRequestDetailFilesResponse(body)) {
@@ -1085,15 +1122,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
1085
1122
  let page = 1;
1086
1123
  let hasMore = true;
1087
1124
  while (hasMore) {
1088
- 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}`, {
1089
1126
  method: 'GET',
1090
1127
  headers: {
1091
1128
  Authorization: `Bearer ${this.ghToken}`,
1092
1129
  Accept: 'application/vnd.github+json',
1093
1130
  },
1094
- });
1131
+ }));
1095
1132
  if (!response.ok) {
1096
- 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}`);
1097
1135
  }
1098
1136
  const body = await response.json();
1099
1137
  if (!isPullRequestCommitsResponse(body)) {
@@ -1119,15 +1157,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
1119
1157
  this.getIssueOrPullRequestState = async (url) => {
1120
1158
  const { owner, repo, issueNumber, isPr } = this.parseIssueUrl(url);
1121
1159
  if (isPr) {
1122
- 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}`, {
1123
1161
  method: 'GET',
1124
1162
  headers: {
1125
1163
  Authorization: `Bearer ${this.ghToken}`,
1126
1164
  Accept: 'application/vnd.github+json',
1127
1165
  },
1128
- });
1166
+ }));
1129
1167
  if (!response.ok) {
1130
- 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}`);
1131
1170
  }
1132
1171
  const body = await response.json();
1133
1172
  if (!isPullRequestDetailResponse(body)) {
@@ -1135,15 +1174,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
1135
1174
  }
1136
1175
  return { state: body.state, merged: body.merged, isPullRequest: true };
1137
1176
  }
1138
- 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}`, {
1139
1178
  method: 'GET',
1140
1179
  headers: {
1141
1180
  Authorization: `Bearer ${this.ghToken}`,
1142
1181
  Accept: 'application/vnd.github+json',
1143
1182
  },
1144
- });
1183
+ }));
1145
1184
  if (!response.ok) {
1146
- 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}`);
1147
1187
  }
1148
1188
  const body = await response.json();
1149
1189
  if (!isIssueOrPullRequestStateResponse(body)) {
@@ -1156,15 +1196,16 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
1156
1196
  if (!isPr) {
1157
1197
  return null;
1158
1198
  }
1159
- 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}`, {
1160
1200
  method: 'GET',
1161
1201
  headers: {
1162
1202
  Authorization: `Bearer ${this.ghToken}`,
1163
1203
  Accept: 'application/vnd.github+json',
1164
1204
  },
1165
- });
1205
+ }));
1166
1206
  if (!response.ok) {
1167
- 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}`);
1168
1209
  }
1169
1210
  const body = await response.json();
1170
1211
  if (!isPullRequestDetailResponse(body)) {