github-issue-tower-defence-management 1.172.2 → 1.173.1
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/CHANGELOG.md +14 -0
- package/README.md +2 -0
- package/bin/adapter/entry-points/console/consoleReadApi.js +126 -48
- package/bin/adapter/entry-points/console/consoleReadApi.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-ZYMkO0fm.js → index-BcnLfodS.js} +1 -1
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +19 -17
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/githubRateLimitRetry.js +8 -1
- package/bin/adapter/repositories/issue/githubRateLimitRetry.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +17 -3
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/QualityCheckAdvanceUseCase.js +50 -0
- package/bin/domain/usecases/QualityCheckAdvanceUseCase.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/console/consoleReadApi.test.ts +254 -0
- package/src/adapter/entry-points/console/consoleReadApi.ts +133 -55
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +10 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +14 -1
- package/src/adapter/entry-points/console/ui-dist/assets/{index-ZYMkO0fm.js → index-BcnLfodS.js} +1 -1
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/console/webServer.test.ts +35 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +6 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +47 -19
- package/src/adapter/repositories/issue/githubRateLimitRetry.ts +4 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +116 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +21 -0
- package/src/domain/usecases/QualityCheckAdvanceUseCase.test.ts +387 -0
- package/src/domain/usecases/QualityCheckAdvanceUseCase.ts +75 -0
- package/types/adapter/entry-points/console/consoleReadApi.d.ts +2 -0
- package/types/adapter/entry-points/console/consoleReadApi.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/githubRateLimitRetry.d.ts +3 -0
- package/types/adapter/repositories/issue/githubRateLimitRetry.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +5 -2
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/QualityCheckAdvanceUseCase.d.ts +14 -0
- package/types/domain/usecases/QualityCheckAdvanceUseCase.d.ts.map +1 -0
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
IssueComment,
|
|
4
4
|
PullRequestCommit,
|
|
5
5
|
} from '../../../domain/usecases/adapter-interfaces/IssueRepository';
|
|
6
|
+
import { GitHubRateLimitError } from '../../repositories/issue/githubRateLimitRetry';
|
|
6
7
|
|
|
7
8
|
export const ISSUE_TITLE_CACHE_TTL_MS = 300 * 1000;
|
|
8
9
|
|
|
@@ -72,6 +73,11 @@ export class IssueTitleStateCache {
|
|
|
72
73
|
return entry.state;
|
|
73
74
|
};
|
|
74
75
|
|
|
76
|
+
getStale = (url: string): IssueOrPullRequestState | null => {
|
|
77
|
+
const entry = this.entries.get(url);
|
|
78
|
+
return entry?.state ?? null;
|
|
79
|
+
};
|
|
80
|
+
|
|
75
81
|
set = (url: string, state: IssueOrPullRequestState): void => {
|
|
76
82
|
this.entries.set(url, { state, fetchedAtMs: this.nowMs() });
|
|
77
83
|
};
|
|
@@ -93,6 +99,11 @@ export class PullRequestStatusCache {
|
|
|
93
99
|
return entry.status;
|
|
94
100
|
};
|
|
95
101
|
|
|
102
|
+
getStale = (url: string): PullRequestStatusResponse | null => {
|
|
103
|
+
const entry = this.entries.get(url);
|
|
104
|
+
return entry?.status ?? null;
|
|
105
|
+
};
|
|
106
|
+
|
|
96
107
|
set = (url: string, status: PullRequestStatusResponse): void => {
|
|
97
108
|
this.entries.set(url, { status, fetchedAtMs: this.nowMs() });
|
|
98
109
|
};
|
|
@@ -103,6 +114,10 @@ export type ConsoleReadApiResponse = {
|
|
|
103
114
|
body: unknown;
|
|
104
115
|
};
|
|
105
116
|
|
|
117
|
+
const isGitHubRateLimitError = (
|
|
118
|
+
error: unknown,
|
|
119
|
+
): error is GitHubRateLimitError => error instanceof GitHubRateLimitError;
|
|
120
|
+
|
|
106
121
|
const badRequest = (message: string): ConsoleReadApiResponse => ({
|
|
107
122
|
statusCode: 400,
|
|
108
123
|
body: { error: message },
|
|
@@ -113,6 +128,11 @@ const ok = (body: unknown): ConsoleReadApiResponse => ({
|
|
|
113
128
|
body,
|
|
114
129
|
});
|
|
115
130
|
|
|
131
|
+
const rateLimited = (error: Error): ConsoleReadApiResponse => ({
|
|
132
|
+
statusCode: 429,
|
|
133
|
+
body: { error: error.message },
|
|
134
|
+
});
|
|
135
|
+
|
|
116
136
|
export type RelatedPullRequestWithSummary = {
|
|
117
137
|
url: string;
|
|
118
138
|
branchName: string | null;
|
|
@@ -160,8 +180,15 @@ export const handleItemBody = async (
|
|
|
160
180
|
if (!url) {
|
|
161
181
|
return badRequest('url query parameter is required');
|
|
162
182
|
}
|
|
163
|
-
|
|
164
|
-
|
|
183
|
+
try {
|
|
184
|
+
const body = await issueRepository.getIssueOrPullRequestBody(url);
|
|
185
|
+
return ok({ body });
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (isGitHubRateLimitError(error)) {
|
|
188
|
+
return rateLimited(error);
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
165
192
|
};
|
|
166
193
|
|
|
167
194
|
export const handleComments = async (
|
|
@@ -171,8 +198,15 @@ export const handleComments = async (
|
|
|
171
198
|
if (!url) {
|
|
172
199
|
return badRequest('url query parameter is required');
|
|
173
200
|
}
|
|
174
|
-
|
|
175
|
-
|
|
201
|
+
try {
|
|
202
|
+
const comments = await issueRepository.getIssueOrPullRequestComments(url);
|
|
203
|
+
return ok({ comments: serializeComments(comments) });
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (isGitHubRateLimitError(error)) {
|
|
206
|
+
return rateLimited(error);
|
|
207
|
+
}
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
176
210
|
};
|
|
177
211
|
|
|
178
212
|
export const handlePrFiles = async (
|
|
@@ -182,11 +216,18 @@ export const handlePrFiles = async (
|
|
|
182
216
|
if (!url) {
|
|
183
217
|
return badRequest('url query parameter is required');
|
|
184
218
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
219
|
+
try {
|
|
220
|
+
const detail = await issueRepository.getPullRequestDetail(url);
|
|
221
|
+
if (detail === null) {
|
|
222
|
+
return ok({ files: null });
|
|
223
|
+
}
|
|
224
|
+
return ok({ files: detail.files });
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (isGitHubRateLimitError(error)) {
|
|
227
|
+
return rateLimited(error);
|
|
228
|
+
}
|
|
229
|
+
throw error;
|
|
188
230
|
}
|
|
189
|
-
return ok({ files: detail.files });
|
|
190
231
|
};
|
|
191
232
|
|
|
192
233
|
export const handlePrCommits = async (
|
|
@@ -196,8 +237,15 @@ export const handlePrCommits = async (
|
|
|
196
237
|
if (!url) {
|
|
197
238
|
return badRequest('url query parameter is required');
|
|
198
239
|
}
|
|
199
|
-
|
|
200
|
-
|
|
240
|
+
try {
|
|
241
|
+
const commits = await issueRepository.getPullRequestCommits(url);
|
|
242
|
+
return ok({ commits: serializeCommits(commits) });
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (isGitHubRateLimitError(error)) {
|
|
245
|
+
return rateLimited(error);
|
|
246
|
+
}
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
201
249
|
};
|
|
202
250
|
|
|
203
251
|
export const handleRelatedPrs = async (
|
|
@@ -207,30 +255,38 @@ export const handleRelatedPrs = async (
|
|
|
207
255
|
if (!url) {
|
|
208
256
|
return badRequest('url query parameter is required');
|
|
209
257
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
258
|
+
try {
|
|
259
|
+
const relatedPullRequests = await issueRepository.findRelatedOpenPRs(url);
|
|
260
|
+
const withSummaries: RelatedPullRequestWithSummary[] = await Promise.all(
|
|
261
|
+
relatedPullRequests.map(async (relatedPullRequest) => {
|
|
262
|
+
const summary = await issueRepository.getPullRequestSummary(
|
|
263
|
+
relatedPullRequest.url,
|
|
264
|
+
);
|
|
265
|
+
return {
|
|
266
|
+
url: relatedPullRequest.url,
|
|
267
|
+
branchName: relatedPullRequest.branchName,
|
|
268
|
+
createdAt: relatedPullRequest.createdAt.toISOString(),
|
|
269
|
+
isDraft: relatedPullRequest.isDraft,
|
|
270
|
+
isConflicted: relatedPullRequest.isConflicted,
|
|
271
|
+
mergeableStatus: deriveMergeableStatus(relatedPullRequest.mergeable),
|
|
272
|
+
isPassedAllCiJob: relatedPullRequest.isPassedAllCiJob,
|
|
273
|
+
isCiStateSuccess: relatedPullRequest.isCiStateSuccess,
|
|
274
|
+
isResolvedAllReviewComments:
|
|
275
|
+
relatedPullRequest.isResolvedAllReviewComments,
|
|
276
|
+
isBranchOutOfDate: relatedPullRequest.isBranchOutOfDate,
|
|
277
|
+
missingRequiredCheckNames:
|
|
278
|
+
relatedPullRequest.missingRequiredCheckNames,
|
|
279
|
+
summary,
|
|
280
|
+
};
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
return ok({ relatedPullRequests: withSummaries });
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (isGitHubRateLimitError(error)) {
|
|
286
|
+
return rateLimited(error);
|
|
287
|
+
}
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
234
290
|
};
|
|
235
291
|
|
|
236
292
|
export const handleIssueTitle = async (
|
|
@@ -245,10 +301,21 @@ export const handleIssueTitle = async (
|
|
|
245
301
|
if (cached !== null) {
|
|
246
302
|
return ok(cached);
|
|
247
303
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
304
|
+
try {
|
|
305
|
+
const state: IssueOrPullRequestState =
|
|
306
|
+
await issueRepository.getIssueOrPullRequestState(url);
|
|
307
|
+
cache.set(url, state);
|
|
308
|
+
return ok(state);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (isGitHubRateLimitError(error)) {
|
|
311
|
+
const stale = cache.getStale(url);
|
|
312
|
+
if (stale !== null) {
|
|
313
|
+
return ok(stale);
|
|
314
|
+
}
|
|
315
|
+
return rateLimited(error);
|
|
316
|
+
}
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
252
319
|
};
|
|
253
320
|
|
|
254
321
|
export const handlePullRequestStatus = async (
|
|
@@ -263,21 +330,32 @@ export const handlePullRequestStatus = async (
|
|
|
263
330
|
if (cached !== null) {
|
|
264
331
|
return ok(cached);
|
|
265
332
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
333
|
+
try {
|
|
334
|
+
const pullRequest = await issueRepository.getOpenPullRequestCiStatus(url);
|
|
335
|
+
const response: PullRequestStatusResponse =
|
|
336
|
+
pullRequest === null
|
|
337
|
+
? { found: false, status: null }
|
|
338
|
+
: {
|
|
339
|
+
found: true,
|
|
340
|
+
status: {
|
|
341
|
+
isConflicted: pullRequest.isConflicted,
|
|
342
|
+
mergeableStatus: deriveMergeableStatus(pullRequest.mergeable),
|
|
343
|
+
isPassedAllCiJob: pullRequest.isPassedAllCiJob,
|
|
344
|
+
isCiStateSuccess: pullRequest.isCiStateSuccess,
|
|
345
|
+
isBranchOutOfDate: pullRequest.isBranchOutOfDate,
|
|
346
|
+
missingRequiredCheckNames: pullRequest.missingRequiredCheckNames,
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
cache.set(url, response);
|
|
350
|
+
return ok(response);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (isGitHubRateLimitError(error)) {
|
|
353
|
+
const stale = cache.getStale(url);
|
|
354
|
+
if (stale !== null) {
|
|
355
|
+
return ok(stale);
|
|
356
|
+
}
|
|
357
|
+
return rateLimited(error);
|
|
358
|
+
}
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
283
361
|
};
|
|
@@ -284,6 +284,16 @@ describe('createConsoleApiClient', () => {
|
|
|
284
284
|
).rejects.toThrow('HTTP 500');
|
|
285
285
|
});
|
|
286
286
|
|
|
287
|
+
it('throws the error message from the JSON body of a non-ok response instead of the HTTP status code', async () => {
|
|
288
|
+
const rateLimitMessage =
|
|
289
|
+
'Failed to fetch body for https://github.com/o/r/issues/1: HTTP 403 GitHub rate limit exceeded, please retry shortly (resets at 2026-01-01T01:00:00.000Z)';
|
|
290
|
+
mockFetchOnce({ error: rateLimitMessage }, false);
|
|
291
|
+
const client = createConsoleApiClient();
|
|
292
|
+
await expect(
|
|
293
|
+
client.fetchItemBody('https://github.com/o/r/issues/1'),
|
|
294
|
+
).rejects.toThrow(rateLimitMessage);
|
|
295
|
+
});
|
|
296
|
+
|
|
287
297
|
it('returns cached comments when the network fetch rejects', async () => {
|
|
288
298
|
const cachedBody = {
|
|
289
299
|
comments: [
|
|
@@ -80,7 +80,20 @@ const requestJson = async (
|
|
|
80
80
|
try {
|
|
81
81
|
const response = await fetch(url);
|
|
82
82
|
if (!response.ok) {
|
|
83
|
-
|
|
83
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
84
|
+
try {
|
|
85
|
+
const payload: unknown = await response.json();
|
|
86
|
+
if (
|
|
87
|
+
isRecord(payload) &&
|
|
88
|
+
typeof payload.error === 'string' &&
|
|
89
|
+
payload.error.length > 0
|
|
90
|
+
) {
|
|
91
|
+
errorMessage = payload.error;
|
|
92
|
+
}
|
|
93
|
+
} catch (e: unknown) {
|
|
94
|
+
console.warn('Failed to parse error body from non-ok response:', e);
|
|
95
|
+
}
|
|
96
|
+
throw new Error(errorMessage);
|
|
84
97
|
}
|
|
85
98
|
const payload: unknown = await response.json();
|
|
86
99
|
try {
|