github-issue-tower-defence-management 1.116.2 → 1.116.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +74 -49
- 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 +11 -11
- 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 +111 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +243 -193
- 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 +4 -1
- 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.5](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.116.4...v1.116.5) (2026-06-29)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **test:** use sessionKey not sessionId on ClaudeLiveSession literals in LiveSessionOauthTokenSelectHandler test ([#1089](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/1089)) ([b649c7f](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/b649c7f9243e09b6c973c3d9d545014c0eb1b7f2))
|
|
7
|
+
|
|
8
|
+
## [1.116.4](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.116.3...v1.116.4) (2026-06-29)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **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))
|
|
14
|
+
|
|
1
15
|
## [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
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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)) {
|