github-issue-tower-defence-management 1.39.0 → 1.40.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/create-pr.yml +12 -5
- package/CHANGELOG.md +13 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +206 -6
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +111 -16
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +324 -8
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +982 -167
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +177 -26
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +3 -4
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +3 -4
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +5 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +2 -3
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
|
@@ -18,6 +18,90 @@ import { normalizeFieldName } from '../utils';
|
|
|
18
18
|
import { LocalStorageRepository } from '../LocalStorageRepository';
|
|
19
19
|
import { Member } from '../../../domain/entities/Member';
|
|
20
20
|
|
|
21
|
+
type GetPullRequestResponse = {
|
|
22
|
+
data?: {
|
|
23
|
+
repository?: {
|
|
24
|
+
pullRequest?: {
|
|
25
|
+
state: string;
|
|
26
|
+
mergeable: string;
|
|
27
|
+
commits: {
|
|
28
|
+
nodes: {
|
|
29
|
+
commit: {
|
|
30
|
+
statusCheckRollup: {
|
|
31
|
+
state: string;
|
|
32
|
+
contexts: {
|
|
33
|
+
nodes: (
|
|
34
|
+
| {
|
|
35
|
+
name?: string;
|
|
36
|
+
status?: string;
|
|
37
|
+
conclusion?: string | null;
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
context?: string;
|
|
41
|
+
state?: string;
|
|
42
|
+
}
|
|
43
|
+
)[];
|
|
44
|
+
};
|
|
45
|
+
} | null;
|
|
46
|
+
};
|
|
47
|
+
}[];
|
|
48
|
+
};
|
|
49
|
+
reviewThreads: {
|
|
50
|
+
nodes: { isResolved: boolean }[];
|
|
51
|
+
};
|
|
52
|
+
baseRepository: {
|
|
53
|
+
branchProtectionRules: {
|
|
54
|
+
nodes: { requiredStatusCheckContexts: string[] }[];
|
|
55
|
+
};
|
|
56
|
+
rulesets: {
|
|
57
|
+
nodes: {
|
|
58
|
+
rules: {
|
|
59
|
+
nodes: {
|
|
60
|
+
type: string;
|
|
61
|
+
parameters?: {
|
|
62
|
+
requiredStatusChecks?: { context: string }[];
|
|
63
|
+
};
|
|
64
|
+
}[];
|
|
65
|
+
};
|
|
66
|
+
}[];
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
errors?: { message: string }[];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type FindRelatedPRsResponse = {
|
|
76
|
+
data?: {
|
|
77
|
+
repository?: {
|
|
78
|
+
issue?: {
|
|
79
|
+
timelineItems: {
|
|
80
|
+
nodes: {
|
|
81
|
+
source?: {
|
|
82
|
+
url?: string;
|
|
83
|
+
state?: string;
|
|
84
|
+
};
|
|
85
|
+
}[];
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
errors?: { message: string }[];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function isGetPullRequestResponse(
|
|
94
|
+
value: unknown,
|
|
95
|
+
): value is GetPullRequestResponse {
|
|
96
|
+
return typia.is<GetPullRequestResponse>(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isFindRelatedPRsResponse(
|
|
100
|
+
value: unknown,
|
|
101
|
+
): value is FindRelatedPRsResponse {
|
|
102
|
+
return typia.is<FindRelatedPRsResponse>(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
21
105
|
export class ApiV3CheerioRestIssueRepository
|
|
22
106
|
extends BaseGitHubRepository
|
|
23
107
|
implements IssueRepository
|
|
@@ -239,19 +323,181 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
239
323
|
return this.convertProjectItemToIssue(projectItem);
|
|
240
324
|
};
|
|
241
325
|
updateNextActionDate = async (
|
|
242
|
-
|
|
243
|
-
|
|
326
|
+
issueUrl: string,
|
|
327
|
+
project: Project,
|
|
244
328
|
date: Date,
|
|
245
329
|
): Promise<void> => {
|
|
246
|
-
if (project.nextActionDate
|
|
247
|
-
|
|
330
|
+
if (!project.nextActionDate) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const projectItem =
|
|
334
|
+
await this.graphqlProjectItemRepository.fetchProjectItemByUrl(issueUrl);
|
|
335
|
+
if (!projectItem) {
|
|
336
|
+
return;
|
|
248
337
|
}
|
|
249
338
|
return this.graphqlProjectItemRepository.updateProjectField(
|
|
250
339
|
project.id,
|
|
251
340
|
project.nextActionDate.fieldId,
|
|
252
|
-
|
|
253
|
-
{ date: date.toISOString() },
|
|
341
|
+
projectItem.id,
|
|
342
|
+
{ date: date.toISOString().split('T')[0] },
|
|
343
|
+
);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
getOpenPullRequest = async (
|
|
347
|
+
prUrl: string,
|
|
348
|
+
): Promise<RelatedPullRequest | null> => {
|
|
349
|
+
const match = prUrl.match(
|
|
350
|
+
/https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/,
|
|
351
|
+
);
|
|
352
|
+
if (!match) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
const [, owner, repo, prNumberStr] = match;
|
|
356
|
+
const prNumber = parseInt(prNumberStr, 10);
|
|
357
|
+
|
|
358
|
+
const query = `query GetPullRequest($owner: String!, $repo: String!, $number: Int!) {
|
|
359
|
+
repository(owner: $owner, name: $repo) {
|
|
360
|
+
pullRequest(number: $number) {
|
|
361
|
+
state
|
|
362
|
+
mergeable
|
|
363
|
+
commits(last: 1) {
|
|
364
|
+
nodes {
|
|
365
|
+
commit {
|
|
366
|
+
statusCheckRollup {
|
|
367
|
+
state
|
|
368
|
+
contexts(first: 100) {
|
|
369
|
+
nodes {
|
|
370
|
+
... on CheckRun {
|
|
371
|
+
name
|
|
372
|
+
status
|
|
373
|
+
conclusion
|
|
374
|
+
}
|
|
375
|
+
... on StatusContext {
|
|
376
|
+
context
|
|
377
|
+
state
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
reviewThreads(first: 100) {
|
|
386
|
+
nodes {
|
|
387
|
+
isResolved
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
baseRepository {
|
|
391
|
+
branchProtectionRules(first: 10) {
|
|
392
|
+
nodes {
|
|
393
|
+
requiredStatusCheckContexts
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
rulesets(first: 10) {
|
|
397
|
+
nodes {
|
|
398
|
+
rules(first: 50) {
|
|
399
|
+
nodes {
|
|
400
|
+
type
|
|
401
|
+
parameters {
|
|
402
|
+
... on RequiredStatusChecksParameters {
|
|
403
|
+
requiredStatusChecks {
|
|
404
|
+
context
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}`;
|
|
416
|
+
|
|
417
|
+
const response = await fetch('https://api.github.com/graphql', {
|
|
418
|
+
method: 'POST',
|
|
419
|
+
headers: {
|
|
420
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
421
|
+
'Content-Type': 'application/json',
|
|
422
|
+
},
|
|
423
|
+
body: JSON.stringify({
|
|
424
|
+
query,
|
|
425
|
+
variables: { owner, repo, number: prNumber },
|
|
426
|
+
}),
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const responseData: unknown = await response.json();
|
|
430
|
+
if (!isGetPullRequestResponse(responseData)) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
'Unexpected response shape when fetching pull request from GitHub GraphQL API',
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (responseData.errors && responseData.errors.length > 0) {
|
|
437
|
+
throw new Error(responseData.errors.map((e) => e.message).join('\n'));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const pr = responseData.data?.repository?.pullRequest;
|
|
441
|
+
if (!pr || pr.state !== 'OPEN') {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const isConflicted = pr.mergeable === 'CONFLICTING';
|
|
446
|
+
|
|
447
|
+
const lastCommit = pr.commits.nodes[pr.commits.nodes.length - 1];
|
|
448
|
+
const rollup = lastCommit?.commit?.statusCheckRollup;
|
|
449
|
+
const isCiStateSuccess = rollup?.state === 'SUCCESS';
|
|
450
|
+
|
|
451
|
+
const requiredCheckNames: string[] = [];
|
|
452
|
+
for (const rule of pr.baseRepository.branchProtectionRules.nodes) {
|
|
453
|
+
requiredCheckNames.push(...rule.requiredStatusCheckContexts);
|
|
454
|
+
}
|
|
455
|
+
for (const ruleset of pr.baseRepository.rulesets.nodes) {
|
|
456
|
+
for (const rule of ruleset.rules.nodes) {
|
|
457
|
+
if (
|
|
458
|
+
rule.type === 'REQUIRED_STATUS_CHECKS' &&
|
|
459
|
+
rule.parameters?.requiredStatusChecks
|
|
460
|
+
) {
|
|
461
|
+
requiredCheckNames.push(
|
|
462
|
+
...rule.parameters.requiredStatusChecks.map((c) => c.context),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const contextNodes = rollup?.contexts?.nodes ?? [];
|
|
469
|
+
const completedCheckNames = contextNodes
|
|
470
|
+
.map((node) => {
|
|
471
|
+
if ('name' in node && node.name) {
|
|
472
|
+
return node.name;
|
|
473
|
+
}
|
|
474
|
+
if ('context' in node && node.context) {
|
|
475
|
+
return node.context;
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
})
|
|
479
|
+
.filter((name): name is string => name !== null);
|
|
480
|
+
|
|
481
|
+
const missingRequiredCheckNames = requiredCheckNames.filter(
|
|
482
|
+
(required) => !completedCheckNames.includes(required),
|
|
254
483
|
);
|
|
484
|
+
|
|
485
|
+
const isPassedAllCiJob =
|
|
486
|
+
isCiStateSuccess && missingRequiredCheckNames.length === 0;
|
|
487
|
+
|
|
488
|
+
const isResolvedAllReviewComments = pr.reviewThreads.nodes.every(
|
|
489
|
+
(thread) => thread.isResolved,
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
url: prUrl,
|
|
494
|
+
isConflicted,
|
|
495
|
+
isPassedAllCiJob,
|
|
496
|
+
isCiStateSuccess,
|
|
497
|
+
isResolvedAllReviewComments,
|
|
498
|
+
isBranchOutOfDate: false,
|
|
499
|
+
missingRequiredCheckNames,
|
|
500
|
+
};
|
|
255
501
|
};
|
|
256
502
|
updateNextActionHour = async (
|
|
257
503
|
project: Project & {
|
|
@@ -327,9 +573,79 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
327
573
|
await this.updateIssue(issue);
|
|
328
574
|
};
|
|
329
575
|
findRelatedOpenPRs = async (
|
|
330
|
-
|
|
576
|
+
issueUrl: string,
|
|
331
577
|
): Promise<RelatedPullRequest[]> => {
|
|
332
|
-
|
|
578
|
+
const match = issueUrl.match(
|
|
579
|
+
/https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/,
|
|
580
|
+
);
|
|
581
|
+
if (!match) {
|
|
582
|
+
return [];
|
|
583
|
+
}
|
|
584
|
+
const [, owner, repo, issueNumberStr] = match;
|
|
585
|
+
const issueNumber = parseInt(issueNumberStr, 10);
|
|
586
|
+
|
|
587
|
+
const query = `query FindRelatedPRs($owner: String!, $repo: String!, $number: Int!) {
|
|
588
|
+
repository(owner: $owner, name: $repo) {
|
|
589
|
+
issue(number: $number) {
|
|
590
|
+
timelineItems(itemTypes: [CROSS_REFERENCED_EVENT], first: 50) {
|
|
591
|
+
nodes {
|
|
592
|
+
... on CrossReferencedEvent {
|
|
593
|
+
source {
|
|
594
|
+
... on PullRequest {
|
|
595
|
+
url
|
|
596
|
+
state
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}`;
|
|
605
|
+
|
|
606
|
+
const response = await fetch('https://api.github.com/graphql', {
|
|
607
|
+
method: 'POST',
|
|
608
|
+
headers: {
|
|
609
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
610
|
+
'Content-Type': 'application/json',
|
|
611
|
+
},
|
|
612
|
+
body: JSON.stringify({
|
|
613
|
+
query,
|
|
614
|
+
variables: { owner, repo, number: issueNumber },
|
|
615
|
+
}),
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const responseData: unknown = await response.json();
|
|
619
|
+
if (!isFindRelatedPRsResponse(responseData)) {
|
|
620
|
+
throw new Error(
|
|
621
|
+
'Unexpected response shape when fetching related PRs from GitHub GraphQL API',
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (responseData.errors && responseData.errors.length > 0) {
|
|
626
|
+
throw new Error(responseData.errors.map((e) => e.message).join('\n'));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const nodes =
|
|
630
|
+
responseData.data?.repository?.issue?.timelineItems?.nodes ?? [];
|
|
631
|
+
const openPrUrls = nodes
|
|
632
|
+
.filter(
|
|
633
|
+
(node) =>
|
|
634
|
+
node.source?.url &&
|
|
635
|
+
node.source?.state === 'OPEN' &&
|
|
636
|
+
node.source.url.includes('/pull/'),
|
|
637
|
+
)
|
|
638
|
+
.map((node) => node.source?.url)
|
|
639
|
+
.filter((url): url is string => url !== undefined);
|
|
640
|
+
|
|
641
|
+
const results: RelatedPullRequest[] = [];
|
|
642
|
+
for (const prUrl of openPrUrls) {
|
|
643
|
+
const pr = await this.getOpenPullRequest(prUrl);
|
|
644
|
+
if (pr) {
|
|
645
|
+
results.push(pr);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return results;
|
|
333
649
|
};
|
|
334
650
|
getAllOpened = async (_project: Project): Promise<Issue[]> => {
|
|
335
651
|
throw new Error('getAllOpened is not implemented');
|