github-issue-tower-defence-management 1.86.0 → 1.87.0
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/.eslintrc.cjs +5 -1
- package/.github/workflows/console-ui.yml +47 -0
- package/.prettierignore +3 -0
- package/CHANGELOG.md +8 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +1 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +49 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +13 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +306 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/package.json +22 -2
- package/scripts/copyConsoleUiDist.mjs +35 -0
- package/src/adapter/entry-points/console/ui/.storybook/main.ts +12 -0
- package/src/adapter/entry-points/console/ui/.storybook/preview.ts +15 -0
- package/src/adapter/entry-points/console/ui/biome.json +47 -0
- package/src/adapter/entry-points/console/ui/components.json +20 -0
- package/src/adapter/entry-points/console/ui/index.html +12 -0
- package/src/adapter/entry-points/console/ui/src/components/ui/badge.stories.tsx +35 -0
- package/src/adapter/entry-points/console/ui/src/components/ui/badge.tsx +28 -0
- package/src/adapter/entry-points/console/ui/src/components/ui/button.stories.tsx +34 -0
- package/src/adapter/entry-points/console/ui/src/components/ui/button.tsx +50 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +44 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +58 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +34 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +32 -0
- package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +47 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +65 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.ts +64 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +19 -0
- package/src/adapter/entry-points/console/ui/src/features/console/types.ts +69 -0
- package/src/adapter/entry-points/console/ui/src/index.css +31 -0
- package/src/adapter/entry-points/console/ui/src/lib/utils.ts +4 -0
- package/src/adapter/entry-points/console/ui/src/main.tsx +15 -0
- package/src/adapter/entry-points/console/ui/src/vite-env.d.ts +1 -0
- package/src/adapter/entry-points/console/ui/tsconfig.json +24 -0
- package/src/adapter/entry-points/console/ui/vite.config.ts +19 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +1 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +49 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +13 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +630 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +492 -0
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +51 -0
- package/tsconfig.build.json +7 -1
- package/tsconfig.json +6 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +18 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +47 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
IssueRepository,
|
|
3
3
|
RelatedPullRequest,
|
|
4
|
+
IssueComment,
|
|
5
|
+
PullRequestDetail,
|
|
6
|
+
PullRequestFile,
|
|
7
|
+
PullRequestCommit,
|
|
4
8
|
} from '../../../domain/usecases/adapter-interfaces/IssueRepository';
|
|
5
9
|
import { Project } from '../../../domain/entities/Project';
|
|
6
10
|
import { Issue } from '../../../domain/entities/Issue';
|
|
@@ -226,6 +230,163 @@ function isPullRequestFilesResponse(
|
|
|
226
230
|
);
|
|
227
231
|
}
|
|
228
232
|
|
|
233
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
234
|
+
return typeof value === 'object' && value !== null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isNullableString(value: unknown): value is string | null {
|
|
238
|
+
return value === null || typeof value === 'string';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isLoginContainer(value: unknown): value is { login: string } {
|
|
242
|
+
return isRecord(value) && typeof value.login === 'string';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isRefContainer(value: unknown): value is { ref: string } {
|
|
246
|
+
return isRecord(value) && typeof value.ref === 'string';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
type IssueOrPullRequestBodyResponse = {
|
|
250
|
+
body: string | null;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
function isIssueOrPullRequestBodyResponse(
|
|
254
|
+
value: unknown,
|
|
255
|
+
): value is IssueOrPullRequestBodyResponse {
|
|
256
|
+
return isRecord(value) && isNullableString(value.body);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
type IssueOrPullRequestStateResponse = {
|
|
260
|
+
state: string;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
function isIssueOrPullRequestStateResponse(
|
|
264
|
+
value: unknown,
|
|
265
|
+
): value is IssueOrPullRequestStateResponse {
|
|
266
|
+
return isRecord(value) && typeof value.state === 'string';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
type IssueCommentsResponseItem = {
|
|
270
|
+
user: { login: string } | null;
|
|
271
|
+
body: string | null;
|
|
272
|
+
created_at: string;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
function isIssueCommentsResponseItem(
|
|
276
|
+
value: unknown,
|
|
277
|
+
): value is IssueCommentsResponseItem {
|
|
278
|
+
if (!isRecord(value)) return false;
|
|
279
|
+
const userValid = value.user === null || isLoginContainer(value.user);
|
|
280
|
+
return (
|
|
281
|
+
userValid &&
|
|
282
|
+
isNullableString(value.body) &&
|
|
283
|
+
typeof value.created_at === 'string'
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isIssueCommentsResponse(
|
|
288
|
+
value: unknown,
|
|
289
|
+
): value is IssueCommentsResponseItem[] {
|
|
290
|
+
return Array.isArray(value) && value.every(isIssueCommentsResponseItem);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
type PullRequestDetailResponse = {
|
|
294
|
+
title: string;
|
|
295
|
+
state: string;
|
|
296
|
+
merged: boolean;
|
|
297
|
+
draft: boolean;
|
|
298
|
+
additions: number;
|
|
299
|
+
deletions: number;
|
|
300
|
+
changed_files: number;
|
|
301
|
+
head: { ref: string };
|
|
302
|
+
base: { ref: string };
|
|
303
|
+
user: { login: string } | null;
|
|
304
|
+
body: string | null;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
function isPullRequestDetailResponse(
|
|
308
|
+
value: unknown,
|
|
309
|
+
): value is PullRequestDetailResponse {
|
|
310
|
+
if (!isRecord(value)) return false;
|
|
311
|
+
const userValid = value.user === null || isLoginContainer(value.user);
|
|
312
|
+
return (
|
|
313
|
+
typeof value.title === 'string' &&
|
|
314
|
+
typeof value.state === 'string' &&
|
|
315
|
+
typeof value.merged === 'boolean' &&
|
|
316
|
+
typeof value.draft === 'boolean' &&
|
|
317
|
+
typeof value.additions === 'number' &&
|
|
318
|
+
typeof value.deletions === 'number' &&
|
|
319
|
+
typeof value.changed_files === 'number' &&
|
|
320
|
+
isRefContainer(value.head) &&
|
|
321
|
+
isRefContainer(value.base) &&
|
|
322
|
+
userValid &&
|
|
323
|
+
isNullableString(value.body)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
type PullRequestDetailFilesResponseItem = {
|
|
328
|
+
filename: string;
|
|
329
|
+
status: string;
|
|
330
|
+
additions: number;
|
|
331
|
+
deletions: number;
|
|
332
|
+
patch?: string;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
function isPullRequestDetailFilesResponseItem(
|
|
336
|
+
value: unknown,
|
|
337
|
+
): value is PullRequestDetailFilesResponseItem {
|
|
338
|
+
if (!isRecord(value)) return false;
|
|
339
|
+
return (
|
|
340
|
+
typeof value.filename === 'string' &&
|
|
341
|
+
typeof value.status === 'string' &&
|
|
342
|
+
typeof value.additions === 'number' &&
|
|
343
|
+
typeof value.deletions === 'number' &&
|
|
344
|
+
(value.patch === undefined || typeof value.patch === 'string')
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function isPullRequestDetailFilesResponse(
|
|
349
|
+
value: unknown,
|
|
350
|
+
): value is PullRequestDetailFilesResponseItem[] {
|
|
351
|
+
return (
|
|
352
|
+
Array.isArray(value) && value.every(isPullRequestDetailFilesResponseItem)
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
type PullRequestCommitsResponseItem = {
|
|
357
|
+
sha: string;
|
|
358
|
+
commit: {
|
|
359
|
+
message: string;
|
|
360
|
+
author: { name: string; date: string } | null;
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
function isCommitAuthor(
|
|
365
|
+
value: unknown,
|
|
366
|
+
): value is { name: string; date: string } {
|
|
367
|
+
return (
|
|
368
|
+
isRecord(value) &&
|
|
369
|
+
typeof value.name === 'string' &&
|
|
370
|
+
typeof value.date === 'string'
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function isPullRequestCommitsResponseItem(
|
|
375
|
+
value: unknown,
|
|
376
|
+
): value is PullRequestCommitsResponseItem {
|
|
377
|
+
if (!isRecord(value)) return false;
|
|
378
|
+
if (typeof value.sha !== 'string') return false;
|
|
379
|
+
if (!isRecord(value.commit)) return false;
|
|
380
|
+
if (typeof value.commit.message !== 'string') return false;
|
|
381
|
+
return value.commit.author === null || isCommitAuthor(value.commit.author);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function isPullRequestCommitsResponse(
|
|
385
|
+
value: unknown,
|
|
386
|
+
): value is PullRequestCommitsResponseItem[] {
|
|
387
|
+
return Array.isArray(value) && value.every(isPullRequestCommitsResponseItem);
|
|
388
|
+
}
|
|
389
|
+
|
|
229
390
|
const fnmatch = (pattern: string, str: string): boolean => {
|
|
230
391
|
let regexStr = '^';
|
|
231
392
|
let i = 0;
|
|
@@ -1243,4 +1404,335 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1243
1404
|
): Promise<void> => {
|
|
1244
1405
|
await this.restIssueRepository.createComment(issueOrPrUrl, commentBody);
|
|
1245
1406
|
};
|
|
1407
|
+
|
|
1408
|
+
getIssueOrPullRequestBody = async (url: string): Promise<string> => {
|
|
1409
|
+
const { owner, repo, issueNumber } = this.parseIssueUrl(url);
|
|
1410
|
+
const response = await fetch(
|
|
1411
|
+
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`,
|
|
1412
|
+
{
|
|
1413
|
+
method: 'GET',
|
|
1414
|
+
headers: {
|
|
1415
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1416
|
+
Accept: 'application/vnd.github+json',
|
|
1417
|
+
},
|
|
1418
|
+
},
|
|
1419
|
+
);
|
|
1420
|
+
if (!response.ok) {
|
|
1421
|
+
throw new Error(
|
|
1422
|
+
`Failed to fetch body for ${url}: HTTP ${response.status}`,
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
const body: unknown = await response.json();
|
|
1426
|
+
if (!isIssueOrPullRequestBodyResponse(body)) {
|
|
1427
|
+
throw new Error(
|
|
1428
|
+
`Unexpected response shape when fetching body for ${url}`,
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
return body.body ?? '';
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
getIssueOrPullRequestComments = async (
|
|
1435
|
+
url: string,
|
|
1436
|
+
): Promise<IssueComment[]> => {
|
|
1437
|
+
const { owner, repo, issueNumber } = this.parseIssueUrl(url);
|
|
1438
|
+
const perPage = 100;
|
|
1439
|
+
const collectedComments: IssueComment[] = [];
|
|
1440
|
+
let page = 1;
|
|
1441
|
+
let hasMore = true;
|
|
1442
|
+
while (hasMore) {
|
|
1443
|
+
const response = await fetch(
|
|
1444
|
+
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=${perPage}&page=${page}`,
|
|
1445
|
+
{
|
|
1446
|
+
method: 'GET',
|
|
1447
|
+
headers: {
|
|
1448
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1449
|
+
Accept: 'application/vnd.github+json',
|
|
1450
|
+
},
|
|
1451
|
+
},
|
|
1452
|
+
);
|
|
1453
|
+
if (!response.ok) {
|
|
1454
|
+
throw new Error(
|
|
1455
|
+
`Failed to fetch comments for ${url}: HTTP ${response.status}`,
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
const body: unknown = await response.json();
|
|
1459
|
+
if (!isIssueCommentsResponse(body)) {
|
|
1460
|
+
throw new Error(
|
|
1461
|
+
`Unexpected response shape when fetching comments for ${url}`,
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
for (const comment of body) {
|
|
1465
|
+
collectedComments.push({
|
|
1466
|
+
author: comment.user?.login ?? '',
|
|
1467
|
+
body: comment.body ?? '',
|
|
1468
|
+
createdAt: new Date(comment.created_at),
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
if (body.length < perPage) {
|
|
1472
|
+
hasMore = false;
|
|
1473
|
+
} else {
|
|
1474
|
+
page += 1;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return collectedComments;
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
getPullRequestDetail = async (
|
|
1481
|
+
prUrl: string,
|
|
1482
|
+
): Promise<PullRequestDetail | null> => {
|
|
1483
|
+
const {
|
|
1484
|
+
owner,
|
|
1485
|
+
repo,
|
|
1486
|
+
issueNumber: prNumber,
|
|
1487
|
+
isPr,
|
|
1488
|
+
} = this.parseIssueUrl(prUrl);
|
|
1489
|
+
if (!isPr) {
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
1492
|
+
const detailResponse = await fetch(
|
|
1493
|
+
`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`,
|
|
1494
|
+
{
|
|
1495
|
+
method: 'GET',
|
|
1496
|
+
headers: {
|
|
1497
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1498
|
+
Accept: 'application/vnd.github+json',
|
|
1499
|
+
},
|
|
1500
|
+
},
|
|
1501
|
+
);
|
|
1502
|
+
if (!detailResponse.ok) {
|
|
1503
|
+
throw new Error(
|
|
1504
|
+
`Failed to fetch detail for PR ${prUrl}: HTTP ${detailResponse.status}`,
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
const detailBody: unknown = await detailResponse.json();
|
|
1508
|
+
if (!isPullRequestDetailResponse(detailBody)) {
|
|
1509
|
+
throw new Error(
|
|
1510
|
+
`Unexpected response shape when fetching detail for PR ${prUrl}`,
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
const files = await this.fetchPullRequestFiles(
|
|
1514
|
+
owner,
|
|
1515
|
+
repo,
|
|
1516
|
+
prNumber,
|
|
1517
|
+
prUrl,
|
|
1518
|
+
);
|
|
1519
|
+
return {
|
|
1520
|
+
title: detailBody.title,
|
|
1521
|
+
state: detailBody.state,
|
|
1522
|
+
merged: detailBody.merged,
|
|
1523
|
+
isDraft: detailBody.draft,
|
|
1524
|
+
additions: detailBody.additions,
|
|
1525
|
+
deletions: detailBody.deletions,
|
|
1526
|
+
changedFiles: detailBody.changed_files,
|
|
1527
|
+
headRefName: detailBody.head.ref,
|
|
1528
|
+
baseRefName: detailBody.base.ref,
|
|
1529
|
+
author: detailBody.user?.login ?? '',
|
|
1530
|
+
files,
|
|
1531
|
+
};
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1534
|
+
private fetchPullRequestFiles = async (
|
|
1535
|
+
owner: string,
|
|
1536
|
+
repo: string,
|
|
1537
|
+
prNumber: number,
|
|
1538
|
+
prUrl: string,
|
|
1539
|
+
): Promise<PullRequestFile[]> => {
|
|
1540
|
+
const perPage = 100;
|
|
1541
|
+
const collectedFiles: PullRequestFile[] = [];
|
|
1542
|
+
let page = 1;
|
|
1543
|
+
let hasMore = true;
|
|
1544
|
+
while (hasMore) {
|
|
1545
|
+
const response = await fetch(
|
|
1546
|
+
`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=${perPage}&page=${page}`,
|
|
1547
|
+
{
|
|
1548
|
+
method: 'GET',
|
|
1549
|
+
headers: {
|
|
1550
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1551
|
+
Accept: 'application/vnd.github+json',
|
|
1552
|
+
},
|
|
1553
|
+
},
|
|
1554
|
+
);
|
|
1555
|
+
if (!response.ok) {
|
|
1556
|
+
throw new Error(
|
|
1557
|
+
`Failed to fetch files for PR ${prUrl}: HTTP ${response.status}`,
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
const body: unknown = await response.json();
|
|
1561
|
+
if (!isPullRequestDetailFilesResponse(body)) {
|
|
1562
|
+
throw new Error(
|
|
1563
|
+
`Unexpected response shape when fetching files for PR ${prUrl}`,
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
for (const file of body) {
|
|
1567
|
+
collectedFiles.push({
|
|
1568
|
+
filename: file.filename,
|
|
1569
|
+
status: file.status,
|
|
1570
|
+
additions: file.additions,
|
|
1571
|
+
deletions: file.deletions,
|
|
1572
|
+
patch: file.patch ?? null,
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
if (body.length < perPage) {
|
|
1576
|
+
hasMore = false;
|
|
1577
|
+
} else {
|
|
1578
|
+
page += 1;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return collectedFiles;
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1584
|
+
getPullRequestCommits = async (
|
|
1585
|
+
prUrl: string,
|
|
1586
|
+
): Promise<PullRequestCommit[]> => {
|
|
1587
|
+
const {
|
|
1588
|
+
owner,
|
|
1589
|
+
repo,
|
|
1590
|
+
issueNumber: prNumber,
|
|
1591
|
+
isPr,
|
|
1592
|
+
} = this.parseIssueUrl(prUrl);
|
|
1593
|
+
if (!isPr) {
|
|
1594
|
+
return [];
|
|
1595
|
+
}
|
|
1596
|
+
const perPage = 100;
|
|
1597
|
+
const collectedCommits: PullRequestCommit[] = [];
|
|
1598
|
+
let page = 1;
|
|
1599
|
+
let hasMore = true;
|
|
1600
|
+
while (hasMore) {
|
|
1601
|
+
const response = await fetch(
|
|
1602
|
+
`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/commits?per_page=${perPage}&page=${page}`,
|
|
1603
|
+
{
|
|
1604
|
+
method: 'GET',
|
|
1605
|
+
headers: {
|
|
1606
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1607
|
+
Accept: 'application/vnd.github+json',
|
|
1608
|
+
},
|
|
1609
|
+
},
|
|
1610
|
+
);
|
|
1611
|
+
if (!response.ok) {
|
|
1612
|
+
throw new Error(
|
|
1613
|
+
`Failed to fetch commits for PR ${prUrl}: HTTP ${response.status}`,
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
const body: unknown = await response.json();
|
|
1617
|
+
if (!isPullRequestCommitsResponse(body)) {
|
|
1618
|
+
throw new Error(
|
|
1619
|
+
`Unexpected response shape when fetching commits for PR ${prUrl}`,
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
for (const commit of body) {
|
|
1623
|
+
collectedCommits.push({
|
|
1624
|
+
sha: commit.sha,
|
|
1625
|
+
message: commit.commit.message,
|
|
1626
|
+
author: commit.commit.author?.name ?? '',
|
|
1627
|
+
authoredAt: new Date(commit.commit.author?.date ?? 0),
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
if (body.length < perPage) {
|
|
1631
|
+
hasMore = false;
|
|
1632
|
+
} else {
|
|
1633
|
+
page += 1;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return collectedCommits;
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
getIssueOrPullRequestState = async (
|
|
1640
|
+
url: string,
|
|
1641
|
+
): Promise<{ state: string; merged: boolean; isPullRequest: boolean }> => {
|
|
1642
|
+
const { owner, repo, issueNumber, isPr } = this.parseIssueUrl(url);
|
|
1643
|
+
if (isPr) {
|
|
1644
|
+
const response = await fetch(
|
|
1645
|
+
`https://api.github.com/repos/${owner}/${repo}/pulls/${issueNumber}`,
|
|
1646
|
+
{
|
|
1647
|
+
method: 'GET',
|
|
1648
|
+
headers: {
|
|
1649
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1650
|
+
Accept: 'application/vnd.github+json',
|
|
1651
|
+
},
|
|
1652
|
+
},
|
|
1653
|
+
);
|
|
1654
|
+
if (!response.ok) {
|
|
1655
|
+
throw new Error(
|
|
1656
|
+
`Failed to fetch state for ${url}: HTTP ${response.status}`,
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
const body: unknown = await response.json();
|
|
1660
|
+
if (!isPullRequestDetailResponse(body)) {
|
|
1661
|
+
throw new Error(
|
|
1662
|
+
`Unexpected response shape when fetching state for ${url}`,
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
return { state: body.state, merged: body.merged, isPullRequest: true };
|
|
1666
|
+
}
|
|
1667
|
+
const response = await fetch(
|
|
1668
|
+
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`,
|
|
1669
|
+
{
|
|
1670
|
+
method: 'GET',
|
|
1671
|
+
headers: {
|
|
1672
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1673
|
+
Accept: 'application/vnd.github+json',
|
|
1674
|
+
},
|
|
1675
|
+
},
|
|
1676
|
+
);
|
|
1677
|
+
if (!response.ok) {
|
|
1678
|
+
throw new Error(
|
|
1679
|
+
`Failed to fetch state for ${url}: HTTP ${response.status}`,
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
const body: unknown = await response.json();
|
|
1683
|
+
if (!isIssueOrPullRequestStateResponse(body)) {
|
|
1684
|
+
throw new Error(
|
|
1685
|
+
`Unexpected response shape when fetching state for ${url}`,
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
return { state: body.state, merged: false, isPullRequest: false };
|
|
1689
|
+
};
|
|
1690
|
+
|
|
1691
|
+
getPullRequestSummary = async (
|
|
1692
|
+
prUrl: string,
|
|
1693
|
+
): Promise<{
|
|
1694
|
+
title: string;
|
|
1695
|
+
body: string;
|
|
1696
|
+
additions: number;
|
|
1697
|
+
deletions: number;
|
|
1698
|
+
changedFiles: number;
|
|
1699
|
+
} | null> => {
|
|
1700
|
+
const {
|
|
1701
|
+
owner,
|
|
1702
|
+
repo,
|
|
1703
|
+
issueNumber: prNumber,
|
|
1704
|
+
isPr,
|
|
1705
|
+
} = this.parseIssueUrl(prUrl);
|
|
1706
|
+
if (!isPr) {
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1709
|
+
const response = await fetch(
|
|
1710
|
+
`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`,
|
|
1711
|
+
{
|
|
1712
|
+
method: 'GET',
|
|
1713
|
+
headers: {
|
|
1714
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1715
|
+
Accept: 'application/vnd.github+json',
|
|
1716
|
+
},
|
|
1717
|
+
},
|
|
1718
|
+
);
|
|
1719
|
+
if (!response.ok) {
|
|
1720
|
+
throw new Error(
|
|
1721
|
+
`Failed to fetch summary for PR ${prUrl}: HTTP ${response.status}`,
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
const body: unknown = await response.json();
|
|
1725
|
+
if (!isPullRequestDetailResponse(body)) {
|
|
1726
|
+
throw new Error(
|
|
1727
|
+
`Unexpected response shape when fetching summary for PR ${prUrl}`,
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
return {
|
|
1731
|
+
title: body.title,
|
|
1732
|
+
body: body.body ?? '',
|
|
1733
|
+
additions: body.additions,
|
|
1734
|
+
deletions: body.deletions,
|
|
1735
|
+
changedFiles: body.changed_files,
|
|
1736
|
+
};
|
|
1737
|
+
};
|
|
1246
1738
|
}
|
|
@@ -16,6 +16,41 @@ export type RelatedPullRequest = {
|
|
|
16
16
|
missingRequiredCheckNames: string[];
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
export type IssueComment = {
|
|
20
|
+
author: string;
|
|
21
|
+
body: string;
|
|
22
|
+
createdAt: Date;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type PullRequestFile = {
|
|
26
|
+
filename: string;
|
|
27
|
+
status: string;
|
|
28
|
+
additions: number;
|
|
29
|
+
deletions: number;
|
|
30
|
+
patch: string | null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type PullRequestDetail = {
|
|
34
|
+
title: string;
|
|
35
|
+
state: string;
|
|
36
|
+
merged: boolean;
|
|
37
|
+
isDraft: boolean;
|
|
38
|
+
additions: number;
|
|
39
|
+
deletions: number;
|
|
40
|
+
changedFiles: number;
|
|
41
|
+
headRefName: string;
|
|
42
|
+
baseRefName: string;
|
|
43
|
+
author: string;
|
|
44
|
+
files: PullRequestFile[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type PullRequestCommit = {
|
|
48
|
+
sha: string;
|
|
49
|
+
message: string;
|
|
50
|
+
author: string;
|
|
51
|
+
authoredAt: Date;
|
|
52
|
+
};
|
|
53
|
+
|
|
19
54
|
export interface IssueRepository {
|
|
20
55
|
getAllIssues: (
|
|
21
56
|
projectId: Project['id'],
|
|
@@ -117,4 +152,20 @@ export interface IssueRepository {
|
|
|
117
152
|
project: Project,
|
|
118
153
|
issueUrl: string,
|
|
119
154
|
) => Promise<void>;
|
|
155
|
+
getIssueOrPullRequestBody: (url: string) => Promise<string>;
|
|
156
|
+
getIssueOrPullRequestComments: (url: string) => Promise<IssueComment[]>;
|
|
157
|
+
getPullRequestDetail: (prUrl: string) => Promise<PullRequestDetail | null>;
|
|
158
|
+
getPullRequestCommits: (prUrl: string) => Promise<PullRequestCommit[]>;
|
|
159
|
+
getIssueOrPullRequestState: (url: string) => Promise<{
|
|
160
|
+
state: string;
|
|
161
|
+
merged: boolean;
|
|
162
|
+
isPullRequest: boolean;
|
|
163
|
+
}>;
|
|
164
|
+
getPullRequestSummary: (prUrl: string) => Promise<{
|
|
165
|
+
title: string;
|
|
166
|
+
body: string;
|
|
167
|
+
additions: number;
|
|
168
|
+
deletions: number;
|
|
169
|
+
changedFiles: number;
|
|
170
|
+
} | null>;
|
|
120
171
|
}
|
package/tsconfig.build.json
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": "./tsconfig.json",
|
|
3
3
|
"include": ["src"],
|
|
4
|
-
"exclude": [
|
|
4
|
+
"exclude": [
|
|
5
|
+
"node_modules",
|
|
6
|
+
"**/__tests__/*",
|
|
7
|
+
"**/*.test.ts",
|
|
8
|
+
"src/adapter/entry-points/console/ui/**",
|
|
9
|
+
"src/adapter/entry-points/console/ui-dist/**"
|
|
10
|
+
],
|
|
5
11
|
"compilerOptions": {
|
|
6
12
|
"rootDir": "./src",
|
|
7
13
|
"outDir": "bin",
|
package/tsconfig.json
CHANGED
|
@@ -20,5 +20,10 @@
|
|
|
20
20
|
"types": ["jest", "node"]
|
|
21
21
|
},
|
|
22
22
|
"include": ["src"],
|
|
23
|
-
"exclude": [
|
|
23
|
+
"exclude": [
|
|
24
|
+
"node_modules",
|
|
25
|
+
"**/__tests__/*",
|
|
26
|
+
"src/adapter/entry-points/console/ui/**",
|
|
27
|
+
"src/adapter/entry-points/console/ui-dist/**"
|
|
28
|
+
]
|
|
24
29
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IssueRepository, RelatedPullRequest } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
|
|
1
|
+
import { IssueRepository, RelatedPullRequest, IssueComment, PullRequestDetail, PullRequestCommit } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
|
|
2
2
|
import { Project } from '../../../domain/entities/Project';
|
|
3
3
|
import { Issue } from '../../../domain/entities/Issue';
|
|
4
4
|
import { StoryObjectMap } from '../../../domain/entities/StoryObjectMap';
|
|
@@ -70,5 +70,22 @@ export declare class ApiV3CheerioRestIssueRepository extends BaseGitHubRepositor
|
|
|
70
70
|
requestChangesWithInlineComment: (prUrl: string, changedFilePath: string | null, commentBody: string) => Promise<void>;
|
|
71
71
|
deletePullRequestBranch: (prUrl: string, branchName: string) => Promise<void>;
|
|
72
72
|
createCommentByUrl: (issueOrPrUrl: string, commentBody: string) => Promise<void>;
|
|
73
|
+
getIssueOrPullRequestBody: (url: string) => Promise<string>;
|
|
74
|
+
getIssueOrPullRequestComments: (url: string) => Promise<IssueComment[]>;
|
|
75
|
+
getPullRequestDetail: (prUrl: string) => Promise<PullRequestDetail | null>;
|
|
76
|
+
private fetchPullRequestFiles;
|
|
77
|
+
getPullRequestCommits: (prUrl: string) => Promise<PullRequestCommit[]>;
|
|
78
|
+
getIssueOrPullRequestState: (url: string) => Promise<{
|
|
79
|
+
state: string;
|
|
80
|
+
merged: boolean;
|
|
81
|
+
isPullRequest: boolean;
|
|
82
|
+
}>;
|
|
83
|
+
getPullRequestSummary: (prUrl: string) => Promise<{
|
|
84
|
+
title: string;
|
|
85
|
+
body: string;
|
|
86
|
+
additions: number;
|
|
87
|
+
deletions: number;
|
|
88
|
+
changedFiles: number;
|
|
89
|
+
} | null>;
|
|
73
90
|
}
|
|
74
91
|
//# sourceMappingURL=ApiV3CheerioRestIssueRepository.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiV3CheerioRestIssueRepository.d.ts","sourceRoot":"","sources":["../../../../src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,kBAAkB,
|
|
1
|
+
{"version":3,"file":"ApiV3CheerioRestIssueRepository.d.ts","sourceRoot":"","sources":["../../../../src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts"],"names":[],"mappings":"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"}
|