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.
@@ -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
- project: Project & { nextActionDate: Required<Project['nextActionDate']> },
243
- issue: Issue,
326
+ issueUrl: string,
327
+ project: Project,
244
328
  date: Date,
245
329
  ): Promise<void> => {
246
- if (project.nextActionDate === null) {
247
- throw new Error('nextActionDate is not defined');
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
- issue.itemId,
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
- _issueUrl: string,
576
+ issueUrl: string,
331
577
  ): Promise<RelatedPullRequest[]> => {
332
- throw new Error('findRelatedOpenPRs is not implemented');
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');