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.
- package/.github/workflows/commit-lint.yml +1 -1
- package/.github/workflows/configs/commitlint.config.cjs +32 -0
- package/.github/workflows/umino-project.yml +9 -3
- package/CHANGELOG.md +14 -0
- package/README.md +2 -2
- package/bin/adapter/repositories/ProcClaudeLiveSessionRepository.js +18 -6
- package/bin/adapter/repositories/ProcClaudeLiveSessionRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +101 -60
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/githubRateLimitRetry.js +77 -0
- package/bin/adapter/repositories/issue/githubRateLimitRetry.js.map +1 -0
- package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js +6 -6
- package/bin/domain/usecases/LiveSessionOauthTokenSelectUseCase.js.map +1 -1
- package/bin/domain/usecases/OauthTokenSelectUseCase.js +1 -1
- package/bin/domain/usecases/OauthTokenSelectUseCase.js.map +1 -1
- package/package.json +1 -1
- package/renovate.json +1 -0
- package/src/adapter/entry-points/handlers/LiveSessionOauthTokenSelectHandler.test.ts +9 -9
- package/src/adapter/repositories/ProcClaudeLiveSessionRepository.test.ts +69 -7
- package/src/adapter/repositories/ProcClaudeLiveSessionRepository.ts +19 -8
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +237 -1
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +274 -209
- package/src/adapter/repositories/issue/githubRateLimitRetry.test.ts +167 -0
- package/src/adapter/repositories/issue/githubRateLimitRetry.ts +105 -0
- package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.test.ts +25 -3
- package/src/domain/usecases/LiveSessionOauthTokenSelectUseCase.ts +7 -7
- package/src/domain/usecases/OauthTokenSelectUseCase.test.ts +13 -4
- package/src/domain/usecases/OauthTokenSelectUseCase.ts +1 -1
- package/src/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.ts +1 -1
- package/types/adapter/repositories/ProcClaudeLiveSessionRepository.d.ts +1 -0
- package/types/adapter/repositories/ProcClaudeLiveSessionRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +6 -2
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/githubRateLimitRetry.d.ts +10 -0
- package/types/adapter/repositories/issue/githubRateLimitRetry.d.ts.map +1 -0
- package/types/domain/usecases/OauthTokenSelectUseCase.d.ts +1 -1
- package/types/domain/usecases/OauthTokenSelectUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/ClaudeLiveSessionRepository.d.ts +1 -1
- 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.
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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,
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAyB;AACzB,2CAA6B;AAM7B,MAAM,sBAAsB,GAAG,OAAO,CAAC;AACvC,MAAM,uBAAuB,GAAG,yBAAyB,CAAC;AAC1D,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
926
|
+
return null;
|
|
919
927
|
}
|
|
920
928
|
if (!isRecord(parsed) || typeof parsed.message !== 'string') {
|
|
921
|
-
return
|
|
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) =>
|
|
926
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)) {
|