github-issue-tower-defence-management 1.71.0 → 1.72.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/.github/workflows/umino-project.yml +15 -7
- package/CHANGELOG.md +14 -0
- package/README.md +1 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +51 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +88 -13
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +37 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +139 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +69 -0
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +184 -0
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +118 -55
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +318 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +52 -1
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +2 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +2 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +4 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +2 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
|
@@ -267,6 +267,145 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
267
267
|
});
|
|
268
268
|
});
|
|
269
269
|
|
|
270
|
+
describe('getPullRequestChangedFilePaths', () => {
|
|
271
|
+
afterEach(() => {
|
|
272
|
+
jest.restoreAllMocks();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should fetch a single page of changed files and return their paths', async () => {
|
|
276
|
+
const fetchSpy = jest
|
|
277
|
+
.spyOn(global, 'fetch')
|
|
278
|
+
.mockResolvedValueOnce(
|
|
279
|
+
new Response(
|
|
280
|
+
JSON.stringify([
|
|
281
|
+
{ filename: 'src/domain/Foo.ts' },
|
|
282
|
+
{ filename: 'src/domain/Bar.ts' },
|
|
283
|
+
]),
|
|
284
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
285
|
+
),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
289
|
+
const result = await repository.getPullRequestChangedFilePaths(
|
|
290
|
+
'https://github.com/HiromiShikata/test-repository/pull/42',
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
expect(result).toEqual(['src/domain/Foo.ts', 'src/domain/Bar.ts']);
|
|
294
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
295
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
296
|
+
'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42/files?per_page=100&page=1',
|
|
297
|
+
expect.objectContaining({ method: 'GET' }),
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should paginate when a page returns exactly 100 entries and stop when fewer are returned', async () => {
|
|
302
|
+
const firstPage: { filename: string }[] = [];
|
|
303
|
+
for (let i = 0; i < 100; i += 1) {
|
|
304
|
+
firstPage.push({ filename: `src/domain/file${i}.ts` });
|
|
305
|
+
}
|
|
306
|
+
const secondPage = [
|
|
307
|
+
{ filename: 'src/domain/extra-a.ts' },
|
|
308
|
+
{ filename: 'src/domain/extra-b.ts' },
|
|
309
|
+
];
|
|
310
|
+
const fetchSpy = jest
|
|
311
|
+
.spyOn(global, 'fetch')
|
|
312
|
+
.mockResolvedValueOnce(
|
|
313
|
+
new Response(JSON.stringify(firstPage), {
|
|
314
|
+
status: 200,
|
|
315
|
+
headers: { 'Content-Type': 'application/json' },
|
|
316
|
+
}),
|
|
317
|
+
)
|
|
318
|
+
.mockResolvedValueOnce(
|
|
319
|
+
new Response(JSON.stringify(secondPage), {
|
|
320
|
+
status: 200,
|
|
321
|
+
headers: { 'Content-Type': 'application/json' },
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
326
|
+
const result = await repository.getPullRequestChangedFilePaths(
|
|
327
|
+
'https://github.com/HiromiShikata/test-repository/pull/42',
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
expect(result).toHaveLength(102);
|
|
331
|
+
expect(result[0]).toBe('src/domain/file0.ts');
|
|
332
|
+
expect(result[99]).toBe('src/domain/file99.ts');
|
|
333
|
+
expect(result[100]).toBe('src/domain/extra-a.ts');
|
|
334
|
+
expect(result[101]).toBe('src/domain/extra-b.ts');
|
|
335
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
336
|
+
expect(fetchSpy).toHaveBeenNthCalledWith(
|
|
337
|
+
1,
|
|
338
|
+
'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42/files?per_page=100&page=1',
|
|
339
|
+
expect.objectContaining({ method: 'GET' }),
|
|
340
|
+
);
|
|
341
|
+
expect(fetchSpy).toHaveBeenNthCalledWith(
|
|
342
|
+
2,
|
|
343
|
+
'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42/files?per_page=100&page=2',
|
|
344
|
+
expect.objectContaining({ method: 'GET' }),
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should throw when the API responds with a non-2xx status', async () => {
|
|
349
|
+
jest.spyOn(global, 'fetch').mockResolvedValueOnce(
|
|
350
|
+
new Response('Not Found', {
|
|
351
|
+
status: 404,
|
|
352
|
+
statusText: 'Not Found',
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
357
|
+
await expect(
|
|
358
|
+
repository.getPullRequestChangedFilePaths(
|
|
359
|
+
'https://github.com/HiromiShikata/test-repository/pull/42',
|
|
360
|
+
),
|
|
361
|
+
).rejects.toThrow('404');
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe('approvePullRequest', () => {
|
|
366
|
+
afterEach(() => {
|
|
367
|
+
jest.restoreAllMocks();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should POST an APPROVE review to the GitHub API for the PR', async () => {
|
|
371
|
+
const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValueOnce(
|
|
372
|
+
new Response(JSON.stringify({ id: 1, state: 'APPROVED' }), {
|
|
373
|
+
status: 200,
|
|
374
|
+
headers: { 'Content-Type': 'application/json' },
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
379
|
+
await repository.approvePullRequest(
|
|
380
|
+
'https://github.com/HiromiShikata/test-repository/pull/42',
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
384
|
+
'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42/reviews',
|
|
385
|
+
expect.objectContaining({
|
|
386
|
+
method: 'POST',
|
|
387
|
+
body: JSON.stringify({ event: 'APPROVE' }),
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should throw when the API responds with a non-2xx status', async () => {
|
|
393
|
+
jest.spyOn(global, 'fetch').mockResolvedValueOnce(
|
|
394
|
+
new Response('Unprocessable Entity', {
|
|
395
|
+
status: 422,
|
|
396
|
+
statusText: 'Unprocessable Entity',
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
401
|
+
await expect(
|
|
402
|
+
repository.approvePullRequest(
|
|
403
|
+
'https://github.com/HiromiShikata/test-repository/pull/42',
|
|
404
|
+
),
|
|
405
|
+
).rejects.toThrow('422');
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
270
409
|
const createApiV3CheerioRestIssueRepository = () => {
|
|
271
410
|
const apiV3IssueRepository = mock<ApiV3IssueRepository>();
|
|
272
411
|
const restIssueRepository = mock<RestIssueRepository>();
|
|
@@ -213,6 +213,16 @@ function isDirectPullRequestResponse(
|
|
|
213
213
|
return true;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
type PullRequestFilesResponseItem = {
|
|
217
|
+
filename: string;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
function isPullRequestFilesResponse(
|
|
221
|
+
value: unknown,
|
|
222
|
+
): value is PullRequestFilesResponseItem[] {
|
|
223
|
+
return typia.is<PullRequestFilesResponseItem[]>(value);
|
|
224
|
+
}
|
|
225
|
+
|
|
216
226
|
const fnmatch = (pattern: string, str: string): boolean => {
|
|
217
227
|
let regexStr = '^';
|
|
218
228
|
let i = 0;
|
|
@@ -1095,6 +1105,65 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1095
1105
|
}
|
|
1096
1106
|
};
|
|
1097
1107
|
|
|
1108
|
+
getPullRequestChangedFilePaths = async (prUrl: string): Promise<string[]> => {
|
|
1109
|
+
const { owner, repo, issueNumber: prNumber } = this.parseIssueUrl(prUrl);
|
|
1110
|
+
const perPage = 100;
|
|
1111
|
+
const collectedPaths: string[] = [];
|
|
1112
|
+
let page = 1;
|
|
1113
|
+
let hasMore = true;
|
|
1114
|
+
while (hasMore) {
|
|
1115
|
+
const response = await fetch(
|
|
1116
|
+
`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=${perPage}&page=${page}`,
|
|
1117
|
+
{
|
|
1118
|
+
method: 'GET',
|
|
1119
|
+
headers: {
|
|
1120
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1121
|
+
Accept: 'application/vnd.github+json',
|
|
1122
|
+
},
|
|
1123
|
+
},
|
|
1124
|
+
);
|
|
1125
|
+
if (!response.ok) {
|
|
1126
|
+
throw new Error(
|
|
1127
|
+
`Failed to fetch changed files for PR ${prUrl}: HTTP ${response.status}`,
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
const body: unknown = await response.json();
|
|
1131
|
+
if (!isPullRequestFilesResponse(body)) {
|
|
1132
|
+
throw new Error(
|
|
1133
|
+
`Unexpected response shape when fetching changed files for PR ${prUrl}`,
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
for (const file of body) {
|
|
1137
|
+
collectedPaths.push(file.filename);
|
|
1138
|
+
}
|
|
1139
|
+
if (body.length < perPage) {
|
|
1140
|
+
hasMore = false;
|
|
1141
|
+
} else {
|
|
1142
|
+
page += 1;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return collectedPaths;
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
approvePullRequest = async (prUrl: string): Promise<void> => {
|
|
1149
|
+
const { owner, repo, issueNumber: prNumber } = this.parseIssueUrl(prUrl);
|
|
1150
|
+
const response = await fetch(
|
|
1151
|
+
`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`,
|
|
1152
|
+
{
|
|
1153
|
+
method: 'POST',
|
|
1154
|
+
headers: {
|
|
1155
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
1156
|
+
'Content-Type': 'application/json',
|
|
1157
|
+
Accept: 'application/vnd.github+json',
|
|
1158
|
+
},
|
|
1159
|
+
body: JSON.stringify({ event: 'APPROVE' }),
|
|
1160
|
+
},
|
|
1161
|
+
);
|
|
1162
|
+
if (!response.ok) {
|
|
1163
|
+
throw new Error(`Failed to approve PR ${prUrl}: HTTP ${response.status}`);
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1098
1167
|
deletePullRequestBranch = async (
|
|
1099
1168
|
prUrl: string,
|
|
1100
1169
|
branchName: string,
|
|
@@ -49,6 +49,24 @@ const extractRequestedFirstFromMockCall = (
|
|
|
49
49
|
return typeof first === 'number' ? first : undefined;
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
const extractRequestedQueryFromMockCall = (
|
|
53
|
+
call: unknown,
|
|
54
|
+
): string | undefined => {
|
|
55
|
+
if (!Array.isArray(call)) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const second: unknown = call[1];
|
|
59
|
+
if (!isRecord(second)) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const json: unknown = second.json;
|
|
63
|
+
if (!isRecord(json)) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
const query: unknown = json.query;
|
|
67
|
+
return typeof query === 'string' ? query : undefined;
|
|
68
|
+
};
|
|
69
|
+
|
|
52
70
|
const extractErrorMessage = (value: unknown): string => {
|
|
53
71
|
if (value instanceof Error) {
|
|
54
72
|
return value.message;
|
|
@@ -477,4 +495,170 @@ describe('GraphqlProjectItemRepository', () => {
|
|
|
477
495
|
]);
|
|
478
496
|
});
|
|
479
497
|
});
|
|
498
|
+
|
|
499
|
+
describe('fetchProjectItemByUrl', () => {
|
|
500
|
+
afterEach(() => {
|
|
501
|
+
mockPost.mockClear();
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const makeContentNode = (url: string, number: number, title: string) => ({
|
|
505
|
+
number,
|
|
506
|
+
title,
|
|
507
|
+
state: 'OPEN',
|
|
508
|
+
url,
|
|
509
|
+
body: 'body text',
|
|
510
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
511
|
+
author: { login: 'octocat' },
|
|
512
|
+
labels: { nodes: [{ name: 'bug' }] },
|
|
513
|
+
assignees: { nodes: [{ login: 'octocat' }] },
|
|
514
|
+
repository: { nameWithOwner: 'owner/repo' },
|
|
515
|
+
projectItems: {
|
|
516
|
+
nodes: [
|
|
517
|
+
{
|
|
518
|
+
id: `item-${number}`,
|
|
519
|
+
fieldValues: {
|
|
520
|
+
nodes: [
|
|
521
|
+
{
|
|
522
|
+
__typename: 'ProjectV2ItemFieldSingleSelectValue',
|
|
523
|
+
name: 'Preparation',
|
|
524
|
+
field: { name: 'Status' },
|
|
525
|
+
},
|
|
526
|
+
],
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
],
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should return project item for an issue URL', async () => {
|
|
534
|
+
const localStorageRepository = new LocalStorageRepository();
|
|
535
|
+
const repository = new GraphqlProjectItemRepository(
|
|
536
|
+
localStorageRepository,
|
|
537
|
+
'dummy-token',
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
mockPost.mockReturnValueOnce(
|
|
541
|
+
mockJsonResponse({
|
|
542
|
+
data: {
|
|
543
|
+
repository: {
|
|
544
|
+
issue: makeContentNode(
|
|
545
|
+
'https://github.com/owner/repo/issues/7',
|
|
546
|
+
7,
|
|
547
|
+
'Issue Title',
|
|
548
|
+
),
|
|
549
|
+
pullRequest: null,
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
}),
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
const result = await repository.fetchProjectItemByUrl(
|
|
556
|
+
'https://github.com/owner/repo/issues/7',
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
expect(mockPost).toHaveBeenCalledTimes(1);
|
|
560
|
+
expect(result).not.toBeNull();
|
|
561
|
+
expect(result?.url).toBe('https://github.com/owner/repo/issues/7');
|
|
562
|
+
expect(result?.title).toBe('Issue Title');
|
|
563
|
+
expect(result?.id).toBe('item-7');
|
|
564
|
+
expect(result?.customFields).toEqual([
|
|
565
|
+
{ name: 'Status', value: 'Preparation' },
|
|
566
|
+
]);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('should return project item for a pull request URL when repository.issue is null', async () => {
|
|
570
|
+
const localStorageRepository = new LocalStorageRepository();
|
|
571
|
+
const repository = new GraphqlProjectItemRepository(
|
|
572
|
+
localStorageRepository,
|
|
573
|
+
'dummy-token',
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
mockPost.mockReturnValueOnce(
|
|
577
|
+
mockJsonResponse({
|
|
578
|
+
data: {
|
|
579
|
+
repository: {
|
|
580
|
+
issue: null,
|
|
581
|
+
pullRequest: makeContentNode(
|
|
582
|
+
'https://github.com/owner/repo/pull/9',
|
|
583
|
+
9,
|
|
584
|
+
'PR Title',
|
|
585
|
+
),
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
}),
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
const result = await repository.fetchProjectItemByUrl(
|
|
592
|
+
'https://github.com/owner/repo/pull/9',
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
expect(mockPost).toHaveBeenCalledTimes(1);
|
|
596
|
+
expect(result).not.toBeNull();
|
|
597
|
+
expect(result?.url).toBe('https://github.com/owner/repo/pull/9');
|
|
598
|
+
expect(result?.title).toBe('PR Title');
|
|
599
|
+
expect(result?.id).toBe('item-9');
|
|
600
|
+
expect(result?.customFields).toEqual([
|
|
601
|
+
{ name: 'Status', value: 'Preparation' },
|
|
602
|
+
]);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('should query both issue and pullRequest fields in a single request', async () => {
|
|
606
|
+
const localStorageRepository = new LocalStorageRepository();
|
|
607
|
+
const repository = new GraphqlProjectItemRepository(
|
|
608
|
+
localStorageRepository,
|
|
609
|
+
'dummy-token',
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
mockPost.mockReturnValueOnce(
|
|
613
|
+
mockJsonResponse({
|
|
614
|
+
data: {
|
|
615
|
+
repository: {
|
|
616
|
+
issue: null,
|
|
617
|
+
pullRequest: makeContentNode(
|
|
618
|
+
'https://github.com/owner/repo/pull/3',
|
|
619
|
+
3,
|
|
620
|
+
'PR Title',
|
|
621
|
+
),
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
}),
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
await repository.fetchProjectItemByUrl(
|
|
628
|
+
'https://github.com/owner/repo/pull/3',
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
const sentQuery = extractRequestedQueryFromMockCall(
|
|
632
|
+
mockPost.mock.calls[0],
|
|
633
|
+
);
|
|
634
|
+
expect(typeof sentQuery).toBe('string');
|
|
635
|
+
expect(sentQuery).toContain('issue(number: $number)');
|
|
636
|
+
expect(sentQuery).toContain('pullRequest(number: $number)');
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('should return null when neither issue nor pullRequest exists', async () => {
|
|
640
|
+
const localStorageRepository = new LocalStorageRepository();
|
|
641
|
+
const repository = new GraphqlProjectItemRepository(
|
|
642
|
+
localStorageRepository,
|
|
643
|
+
'dummy-token',
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
mockPost.mockReturnValueOnce(
|
|
647
|
+
mockJsonResponse({
|
|
648
|
+
data: {
|
|
649
|
+
repository: {
|
|
650
|
+
issue: null,
|
|
651
|
+
pullRequest: null,
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
}),
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
const result = await repository.fetchProjectItemByUrl(
|
|
658
|
+
'https://github.com/owner/repo/issues/123',
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
expect(result).toBeNull();
|
|
662
|
+
});
|
|
663
|
+
});
|
|
480
664
|
});
|
|
@@ -665,7 +665,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
665
665
|
issueUrl: string,
|
|
666
666
|
): Promise<ProjectItem | null> => {
|
|
667
667
|
const { owner, repo, issueNumber } = this.extractIssueFromUrl(issueUrl);
|
|
668
|
-
const graphql = `query
|
|
668
|
+
const graphql = `query GetIssueOrPullRequest($owner: String!, $repo: String!, $number: Int!) {
|
|
669
669
|
repository(owner: $owner, name: $repo) {
|
|
670
670
|
issue(number: $number) {
|
|
671
671
|
number
|
|
@@ -741,6 +741,80 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
741
741
|
}
|
|
742
742
|
}
|
|
743
743
|
}
|
|
744
|
+
pullRequest(number: $number) {
|
|
745
|
+
number
|
|
746
|
+
title
|
|
747
|
+
state
|
|
748
|
+
url
|
|
749
|
+
body
|
|
750
|
+
createdAt
|
|
751
|
+
author {
|
|
752
|
+
login
|
|
753
|
+
}
|
|
754
|
+
labels(first: 100) {
|
|
755
|
+
nodes {
|
|
756
|
+
name
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
assignees(first: 20) {
|
|
760
|
+
nodes {
|
|
761
|
+
login
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
repository {
|
|
765
|
+
nameWithOwner
|
|
766
|
+
}
|
|
767
|
+
projectItems(first: 10) {
|
|
768
|
+
nodes {
|
|
769
|
+
id
|
|
770
|
+
fieldValues(first: 10) {
|
|
771
|
+
nodes {
|
|
772
|
+
... on ProjectV2ItemFieldTextValue {
|
|
773
|
+
text
|
|
774
|
+
field {
|
|
775
|
+
... on ProjectV2Field {
|
|
776
|
+
name
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
... on ProjectV2ItemFieldNumberValue {
|
|
781
|
+
number
|
|
782
|
+
id
|
|
783
|
+
field {
|
|
784
|
+
... on ProjectV2Field {
|
|
785
|
+
name
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
... on ProjectV2ItemFieldDateValue {
|
|
790
|
+
date
|
|
791
|
+
field {
|
|
792
|
+
... on ProjectV2Field {
|
|
793
|
+
name
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
798
|
+
name
|
|
799
|
+
field {
|
|
800
|
+
... on ProjectV2SingleSelectField {
|
|
801
|
+
name
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
... on ProjectV2ItemFieldIterationValue {
|
|
806
|
+
title
|
|
807
|
+
field {
|
|
808
|
+
... on ProjectV2Field {
|
|
809
|
+
name
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
744
818
|
}
|
|
745
819
|
}`;
|
|
746
820
|
const graphqlQuery = {
|
|
@@ -751,6 +825,34 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
751
825
|
number: issueNumber,
|
|
752
826
|
},
|
|
753
827
|
};
|
|
828
|
+
type ContentNode = {
|
|
829
|
+
number: number;
|
|
830
|
+
title: string;
|
|
831
|
+
state: string;
|
|
832
|
+
url: string;
|
|
833
|
+
body: string;
|
|
834
|
+
createdAt: string;
|
|
835
|
+
author: { login: string } | null;
|
|
836
|
+
labels: { nodes: { name: string }[] };
|
|
837
|
+
assignees: { nodes: { login: string }[] };
|
|
838
|
+
repository: { nameWithOwner: string };
|
|
839
|
+
projectItems: {
|
|
840
|
+
nodes: {
|
|
841
|
+
id: string;
|
|
842
|
+
fieldValues: {
|
|
843
|
+
nodes: {
|
|
844
|
+
text: string;
|
|
845
|
+
number: number;
|
|
846
|
+
date: string;
|
|
847
|
+
name: string;
|
|
848
|
+
field: {
|
|
849
|
+
name: string;
|
|
850
|
+
};
|
|
851
|
+
}[];
|
|
852
|
+
};
|
|
853
|
+
}[];
|
|
854
|
+
};
|
|
855
|
+
};
|
|
754
856
|
const response = await ky
|
|
755
857
|
.post('https://api.github.com/graphql', {
|
|
756
858
|
json: graphqlQuery,
|
|
@@ -761,34 +863,8 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
761
863
|
.json<{
|
|
762
864
|
data?: {
|
|
763
865
|
repository: {
|
|
764
|
-
issue:
|
|
765
|
-
|
|
766
|
-
title: string;
|
|
767
|
-
state: string;
|
|
768
|
-
url: string;
|
|
769
|
-
body: string;
|
|
770
|
-
createdAt: string;
|
|
771
|
-
author: { login: string } | null;
|
|
772
|
-
labels: { nodes: { name: string }[] };
|
|
773
|
-
assignees: { nodes: { login: string }[] };
|
|
774
|
-
repository: { nameWithOwner: string };
|
|
775
|
-
projectItems: {
|
|
776
|
-
nodes: {
|
|
777
|
-
id: string;
|
|
778
|
-
fieldValues: {
|
|
779
|
-
nodes: {
|
|
780
|
-
text: string;
|
|
781
|
-
number: number;
|
|
782
|
-
date: string;
|
|
783
|
-
name: string;
|
|
784
|
-
field: {
|
|
785
|
-
name: string;
|
|
786
|
-
};
|
|
787
|
-
}[];
|
|
788
|
-
};
|
|
789
|
-
}[];
|
|
790
|
-
};
|
|
791
|
-
};
|
|
866
|
+
issue: ContentNode | null;
|
|
867
|
+
pullRequest: ContentNode | null;
|
|
792
868
|
};
|
|
793
869
|
};
|
|
794
870
|
errors?: { message: string }[];
|
|
@@ -802,40 +878,27 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
802
878
|
);
|
|
803
879
|
}
|
|
804
880
|
const data = response.data;
|
|
805
|
-
|
|
881
|
+
const content = data.repository.issue ?? data.repository.pullRequest;
|
|
882
|
+
if (!content) {
|
|
806
883
|
return null;
|
|
807
884
|
}
|
|
808
|
-
const projectItems
|
|
809
|
-
id: string;
|
|
810
|
-
fieldValues: {
|
|
811
|
-
nodes: {
|
|
812
|
-
text: string;
|
|
813
|
-
number: number;
|
|
814
|
-
date: string;
|
|
815
|
-
name: string;
|
|
816
|
-
field: {
|
|
817
|
-
name: string;
|
|
818
|
-
};
|
|
819
|
-
}[];
|
|
820
|
-
};
|
|
821
|
-
}[] = data.repository.issue.projectItems.nodes;
|
|
885
|
+
const projectItems = content.projectItems.nodes;
|
|
822
886
|
const item = projectItems[0];
|
|
823
887
|
if (!item) {
|
|
824
888
|
throw new Error(`No project item found for issue ${issueUrl}`);
|
|
825
889
|
}
|
|
826
890
|
return {
|
|
827
891
|
id: item.id,
|
|
828
|
-
nameWithOwner:
|
|
829
|
-
number:
|
|
830
|
-
title:
|
|
831
|
-
state: this.convertStrToState(
|
|
832
|
-
url:
|
|
833
|
-
body:
|
|
834
|
-
labels:
|
|
835
|
-
assignees:
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
author: data.repository.issue.author?.login || '',
|
|
892
|
+
nameWithOwner: content.repository.nameWithOwner,
|
|
893
|
+
number: content.number,
|
|
894
|
+
title: content.title,
|
|
895
|
+
state: this.convertStrToState(content.state),
|
|
896
|
+
url: content.url,
|
|
897
|
+
body: content.body,
|
|
898
|
+
labels: content.labels?.nodes?.map((l) => l.name) || [],
|
|
899
|
+
assignees: content.assignees?.nodes?.map((a) => a.login) || [],
|
|
900
|
+
createdAt: content.createdAt || new Date().toISOString(),
|
|
901
|
+
author: content.author?.login || '',
|
|
839
902
|
customFields: item.fieldValues.nodes
|
|
840
903
|
.filter((field) => !!field.field)
|
|
841
904
|
.map((field) => {
|