github-issue-tower-defence-management 1.39.0 → 1.41.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.
Files changed (58) hide show
  1. package/.github/workflows/create-pr.yml +12 -5
  2. package/.github/workflows/umino-project.yml +5 -4
  3. package/CHANGELOG.md +26 -0
  4. package/README.md +21 -9
  5. package/bin/adapter/entry-points/cli/index.js +45 -10
  6. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +32 -8
  8. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/repositories/NodeLocalCommandRunner.js +3 -3
  10. package/bin/adapter/repositories/NodeLocalCommandRunner.js.map +1 -1
  11. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +446 -11
  12. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  13. package/bin/domain/usecases/HandleScheduledEventUseCase.js +5 -2
  14. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  15. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +111 -16
  16. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  17. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +7 -2
  18. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  19. package/bin/domain/usecases/StartPreparationUseCase.js +107 -72
  20. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/adapter/entry-points/cli/index.test.ts +26 -13
  23. package/src/adapter/entry-points/cli/index.ts +74 -13
  24. package/src/adapter/repositories/NodeLocalCommandRunner.test.ts +12 -12
  25. package/src/adapter/repositories/NodeLocalCommandRunner.ts +7 -4
  26. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +3 -0
  27. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +689 -12
  28. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +3 -0
  29. package/src/domain/entities/Issue.ts +1 -0
  30. package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
  31. package/src/domain/usecases/HandleScheduledEventUseCase.ts +11 -3
  32. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +983 -167
  33. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +177 -26
  34. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +22 -9
  35. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +8 -3
  36. package/src/domain/usecases/StartPreparationUseCase.test.ts +1696 -290
  37. package/src/domain/usecases/StartPreparationUseCase.ts +171 -126
  38. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -4
  39. package/src/domain/usecases/adapter-interfaces/LocalCommandRunner.ts +4 -1
  40. package/types/adapter/entry-points/cli/index.d.ts +4 -1
  41. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  42. package/types/adapter/repositories/NodeLocalCommandRunner.d.ts +1 -1
  43. package/types/adapter/repositories/NodeLocalCommandRunner.d.ts.map +1 -1
  44. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +7 -6
  45. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  46. package/types/domain/entities/Issue.d.ts +1 -0
  47. package/types/domain/entities/Issue.d.ts.map +1 -1
  48. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +5 -1
  49. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  50. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +5 -1
  51. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  52. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
  53. package/types/domain/usecases/StartPreparationUseCase.d.ts +10 -18
  54. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  55. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +3 -3
  56. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  57. package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts +1 -1
  58. package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts.map +1 -1
@@ -18,6 +18,250 @@ import { normalizeFieldName } from '../utils';
18
18
  import { LocalStorageRepository } from '../LocalStorageRepository';
19
19
  import { Member } from '../../../domain/entities/Member';
20
20
 
21
+ type TimelineItem = {
22
+ __typename: string;
23
+ willCloseTarget?: boolean;
24
+ source?: {
25
+ __typename: string;
26
+ url?: string;
27
+ number?: number;
28
+ state?: string;
29
+ mergeable?: string;
30
+ headRefName?: string;
31
+ baseRefName?: string;
32
+ baseRepository?: {
33
+ branchProtectionRules?: {
34
+ nodes: Array<{
35
+ pattern: string;
36
+ requiredStatusCheckContexts: string[];
37
+ }>;
38
+ };
39
+ defaultBranchRef?: {
40
+ name: string;
41
+ } | null;
42
+ rulesets?: {
43
+ nodes: Array<{
44
+ name: string;
45
+ enforcement: string;
46
+ conditions: {
47
+ refName: {
48
+ include: string[];
49
+ exclude: string[];
50
+ };
51
+ };
52
+ rules: {
53
+ nodes: Array<{
54
+ type: string;
55
+ parameters:
56
+ | {
57
+ requiredStatusChecks: Array<{
58
+ context: string;
59
+ }>;
60
+ }
61
+ | Record<string, never>;
62
+ }>;
63
+ };
64
+ }>;
65
+ };
66
+ };
67
+ commits?: {
68
+ nodes: Array<{
69
+ commit: {
70
+ statusCheckRollup?: {
71
+ state: string;
72
+ contexts?: {
73
+ nodes: Array<
74
+ | {
75
+ __typename: 'CheckRun';
76
+ name: string;
77
+ conclusion: string | null;
78
+ }
79
+ | {
80
+ __typename: 'StatusContext';
81
+ context: string;
82
+ state: string;
83
+ }
84
+ >;
85
+ };
86
+ } | null;
87
+ };
88
+ }>;
89
+ };
90
+ reviewThreads?: {
91
+ nodes: Array<{
92
+ isResolved: boolean;
93
+ }>;
94
+ };
95
+ baseRef?: {
96
+ name: string;
97
+ } | null;
98
+ };
99
+ };
100
+
101
+ type IssueTimelineResponse = {
102
+ data?: {
103
+ repository?: {
104
+ issue?: {
105
+ timelineItems: {
106
+ pageInfo: {
107
+ endCursor: string;
108
+ hasNextPage: boolean;
109
+ };
110
+ nodes: TimelineItem[];
111
+ };
112
+ };
113
+ };
114
+ };
115
+ errors?: Array<{ message: string }>;
116
+ };
117
+
118
+ type PrStatusComputationData = {
119
+ mergeable?: string;
120
+ baseRepository?: {
121
+ branchProtectionRules?: {
122
+ nodes: Array<{
123
+ pattern: string;
124
+ requiredStatusCheckContexts: string[];
125
+ }>;
126
+ };
127
+ defaultBranchRef?: {
128
+ name: string;
129
+ } | null;
130
+ rulesets?: {
131
+ nodes: Array<{
132
+ name: string;
133
+ enforcement: string;
134
+ conditions: {
135
+ refName: {
136
+ include: string[];
137
+ exclude: string[];
138
+ };
139
+ };
140
+ rules: {
141
+ nodes: Array<{
142
+ type: string;
143
+ parameters:
144
+ | {
145
+ requiredStatusChecks: Array<{
146
+ context: string;
147
+ }>;
148
+ }
149
+ | Record<string, never>;
150
+ }>;
151
+ };
152
+ }>;
153
+ };
154
+ };
155
+ commits?: {
156
+ nodes: Array<{
157
+ commit: {
158
+ statusCheckRollup?: {
159
+ state: string;
160
+ contexts?: {
161
+ nodes: Array<
162
+ | {
163
+ __typename: 'CheckRun';
164
+ name: string;
165
+ conclusion: string | null;
166
+ }
167
+ | {
168
+ __typename: 'StatusContext';
169
+ context: string;
170
+ state: string;
171
+ }
172
+ >;
173
+ };
174
+ } | null;
175
+ };
176
+ }>;
177
+ };
178
+ reviewThreads?: {
179
+ nodes: Array<{
180
+ isResolved: boolean;
181
+ }>;
182
+ };
183
+ };
184
+
185
+ type DirectPullRequestResponse = {
186
+ data?: {
187
+ repository?: {
188
+ pullRequest?: {
189
+ url: string;
190
+ state: string;
191
+ headRefName?: string;
192
+ baseRefName?: string;
193
+ } & PrStatusComputationData;
194
+ };
195
+ };
196
+ errors?: Array<{ message: string }>;
197
+ };
198
+
199
+ function isIssueTimelineResponse(
200
+ value: unknown,
201
+ ): value is IssueTimelineResponse {
202
+ if (typeof value !== 'object' || value === null) return false;
203
+ return true;
204
+ }
205
+
206
+ function isDirectPullRequestResponse(
207
+ value: unknown,
208
+ ): value is DirectPullRequestResponse {
209
+ if (typeof value !== 'object' || value === null) return false;
210
+ return true;
211
+ }
212
+
213
+ const fnmatch = (pattern: string, str: string): boolean => {
214
+ let regexStr = '^';
215
+ let i = 0;
216
+ while (i < pattern.length) {
217
+ const c = pattern[i];
218
+ if (c === '*') {
219
+ if (pattern[i + 1] === '*') {
220
+ regexStr += '.*';
221
+ i += 2;
222
+ if (pattern[i] === '/') {
223
+ i++;
224
+ }
225
+ } else {
226
+ regexStr += '[^/]*';
227
+ i++;
228
+ }
229
+ } else if (c === '?') {
230
+ regexStr += '[^/]';
231
+ i++;
232
+ } else if (c === '[') {
233
+ let j = i + 1;
234
+ while (j < pattern.length && pattern[j] !== ']') {
235
+ j++;
236
+ }
237
+ if (j >= pattern.length) {
238
+ regexStr += '\\[';
239
+ i++;
240
+ continue;
241
+ }
242
+ const content = pattern.slice(i + 1, j);
243
+ if (content.length > 0 && (content[0] === '!' || content[0] === '^')) {
244
+ const body = content.slice(1).replace(/\\/g, '\\\\');
245
+ regexStr += '[^' + body + ']';
246
+ } else {
247
+ const escapedContent = content.replace(/\\/g, '\\\\');
248
+ regexStr += '[' + escapedContent + ']';
249
+ }
250
+ i = j + 1;
251
+ } else {
252
+ regexStr += c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
253
+ i++;
254
+ }
255
+ }
256
+ regexStr += '$';
257
+ try {
258
+ const regex = new RegExp(regexStr);
259
+ return regex.test(str);
260
+ } catch {
261
+ return pattern === str;
262
+ }
263
+ };
264
+
21
265
  export class ApiV3CheerioRestIssueRepository
22
266
  extends BaseGitHubRepository
23
267
  implements IssueRepository
@@ -131,6 +375,7 @@ export class ApiV3CheerioRestIssueRepository
131
375
  isInProgress: normalizeFieldName(status || '').includes('progress'),
132
376
  isClosed: item.state !== 'OPEN',
133
377
  createdAt: new Date(item.createdAt || '2000-01-01'),
378
+ author: '',
134
379
  };
135
380
  };
136
381
  getAllIssuesFromCache = async (
@@ -239,18 +484,23 @@ export class ApiV3CheerioRestIssueRepository
239
484
  return this.convertProjectItemToIssue(projectItem);
240
485
  };
241
486
  updateNextActionDate = async (
242
- project: Project & { nextActionDate: Required<Project['nextActionDate']> },
243
- issue: Issue,
487
+ issueUrl: string,
488
+ project: Project,
244
489
  date: Date,
245
490
  ): Promise<void> => {
246
- if (project.nextActionDate === null) {
247
- throw new Error('nextActionDate is not defined');
491
+ if (!project.nextActionDate) {
492
+ return;
493
+ }
494
+ const projectItem =
495
+ await this.graphqlProjectItemRepository.fetchProjectItemByUrl(issueUrl);
496
+ if (!projectItem) {
497
+ return;
248
498
  }
249
499
  return this.graphqlProjectItemRepository.updateProjectField(
250
500
  project.id,
251
501
  project.nextActionDate.fieldId,
252
- issue.itemId,
253
- { date: date.toISOString() },
502
+ projectItem.id,
503
+ { date: date.toISOString().split('T')[0] },
254
504
  );
255
505
  };
256
506
  updateNextActionHour = async (
@@ -326,15 +576,442 @@ export class ApiV3CheerioRestIssueRepository
326
576
  update = async (issue: Issue, _project: Project): Promise<void> => {
327
577
  await this.updateIssue(issue);
328
578
  };
579
+ private parseIssueUrl = (
580
+ issueUrl: string,
581
+ ): {
582
+ owner: string;
583
+ repo: string;
584
+ issueNumber: number;
585
+ isPr: boolean;
586
+ } => {
587
+ const urlMatch = issueUrl.match(
588
+ /github\.com\/([^/]+)\/([^/]+)\/(issues|pull)\/(\d+)/,
589
+ );
590
+ if (!urlMatch) {
591
+ throw new Error(`Invalid GitHub issue URL: ${issueUrl}`);
592
+ }
593
+ return {
594
+ owner: urlMatch[1],
595
+ repo: urlMatch[2],
596
+ issueNumber: parseInt(urlMatch[4], 10),
597
+ isPr: urlMatch[3] === 'pull',
598
+ };
599
+ };
600
+
601
+ private computePrStatus = (
602
+ prUrl: string,
603
+ headRefName: string | undefined,
604
+ baseRefName: string | undefined,
605
+ data: PrStatusComputationData,
606
+ ): RelatedPullRequest => {
607
+ const isConflicted = data.mergeable === 'CONFLICTING';
608
+ const lastCommit = data.commits?.nodes[0]?.commit;
609
+ const ciState = lastCommit?.statusCheckRollup?.state;
610
+ const contexts = lastCommit?.statusCheckRollup?.contexts?.nodes || [];
611
+
612
+ const branchProtectionRules =
613
+ data.baseRepository?.branchProtectionRules?.nodes || [];
614
+ const matchingRules = baseRefName
615
+ ? branchProtectionRules.filter(
616
+ (rule) =>
617
+ rule.pattern === baseRefName || fnmatch(rule.pattern, baseRefName),
618
+ )
619
+ : [];
620
+ const requiredCheckNamesSet = new Set<string>();
621
+ for (const rule of matchingRules) {
622
+ for (const name of rule.requiredStatusCheckContexts) {
623
+ requiredCheckNamesSet.add(name);
624
+ }
625
+ }
626
+
627
+ const rulesets = data.baseRepository?.rulesets?.nodes || [];
628
+ const defaultBranchName = data.baseRepository?.defaultBranchRef?.name || '';
629
+ for (const ruleset of rulesets) {
630
+ if (ruleset.enforcement !== 'ACTIVE') continue;
631
+ const refIncludes = ruleset.conditions.refName.include;
632
+ const refExcludes = ruleset.conditions.refName.exclude;
633
+ const matchesInclude =
634
+ baseRefName !== undefined &&
635
+ refIncludes.some((pattern) => {
636
+ if (pattern === '~DEFAULT_BRANCH') {
637
+ return baseRefName === defaultBranchName;
638
+ }
639
+ if (pattern === '~ALL') {
640
+ return true;
641
+ }
642
+ const branchPattern = pattern.replace(/^refs\/heads\//, '');
643
+ return (
644
+ branchPattern === baseRefName || fnmatch(branchPattern, baseRefName)
645
+ );
646
+ });
647
+ if (!matchesInclude) continue;
648
+ const matchesExclude =
649
+ baseRefName !== undefined &&
650
+ refExcludes.some((pattern) => {
651
+ if (pattern === '~DEFAULT_BRANCH') {
652
+ return baseRefName === defaultBranchName;
653
+ }
654
+ const branchPattern = pattern.replace(/^refs\/heads\//, '');
655
+ return (
656
+ branchPattern === baseRefName || fnmatch(branchPattern, baseRefName)
657
+ );
658
+ });
659
+ if (matchesExclude) continue;
660
+ for (const rule of ruleset.rules.nodes) {
661
+ if (rule.type !== 'REQUIRED_STATUS_CHECKS') continue;
662
+ if ('requiredStatusChecks' in rule.parameters) {
663
+ for (const check of rule.parameters.requiredStatusChecks) {
664
+ requiredCheckNamesSet.add(check.context);
665
+ }
666
+ }
667
+ }
668
+ }
669
+
670
+ const requiredCheckNames = Array.from(requiredCheckNamesSet);
671
+ const seenContextNames = new Set<string>();
672
+ for (const ctx of contexts) {
673
+ if ('name' in ctx) {
674
+ seenContextNames.add(ctx.name);
675
+ }
676
+ if ('context' in ctx) {
677
+ seenContextNames.add(ctx.context);
678
+ }
679
+ }
680
+
681
+ const missingRequiredCheckNames = requiredCheckNames.filter(
682
+ (name) => !seenContextNames.has(name),
683
+ );
684
+ const allRequiredChecksPassed = missingRequiredCheckNames.length === 0;
685
+ const isCiStateSuccess = ciState === 'SUCCESS';
686
+ const isPassedAllCiJob = isCiStateSuccess && allRequiredChecksPassed;
687
+
688
+ const reviewThreads = data.reviewThreads?.nodes || [];
689
+ const isResolvedAllReviewComments =
690
+ reviewThreads.length === 0 ||
691
+ reviewThreads.every((thread) => thread.isResolved);
692
+
693
+ return {
694
+ url: prUrl,
695
+ branchName: headRefName ?? null,
696
+ isConflicted,
697
+ isPassedAllCiJob,
698
+ isCiStateSuccess,
699
+ isResolvedAllReviewComments,
700
+ isBranchOutOfDate: false,
701
+ missingRequiredCheckNames,
702
+ };
703
+ };
704
+
329
705
  findRelatedOpenPRs = async (
330
- _issueUrl: string,
706
+ issueUrl: string,
331
707
  ): Promise<RelatedPullRequest[]> => {
332
- throw new Error('findRelatedOpenPRs is not implemented');
708
+ const { owner, repo, issueNumber, isPr } = this.parseIssueUrl(issueUrl);
709
+ if (isPr) {
710
+ throw new Error(
711
+ 'findRelatedOpenPRs only supports issue URLs, not pull request URLs',
712
+ );
713
+ }
714
+
715
+ const query = `
716
+ query($owner: String!, $repo: String!, $issueNumber: Int!, $after: String) {
717
+ repository(owner: $owner, name: $repo) {
718
+ issue(number: $issueNumber) {
719
+ timelineItems(first: 100, after: $after, itemTypes: [CROSS_REFERENCED_EVENT]) {
720
+ pageInfo {
721
+ endCursor
722
+ hasNextPage
723
+ }
724
+ nodes {
725
+ __typename
726
+ ... on CrossReferencedEvent {
727
+ willCloseTarget
728
+ source {
729
+ __typename
730
+ ... on PullRequest {
731
+ url
732
+ number
733
+ state
734
+ mergeable
735
+ headRefName
736
+ baseRefName
737
+ baseRepository {
738
+ branchProtectionRules(first: 100) {
739
+ nodes {
740
+ pattern
741
+ requiredStatusCheckContexts
742
+ }
743
+ }
744
+ defaultBranchRef {
745
+ name
746
+ }
747
+ rulesets(first: 100) {
748
+ nodes {
749
+ name
750
+ enforcement
751
+ conditions {
752
+ refName {
753
+ include
754
+ exclude
755
+ }
756
+ }
757
+ rules(first: 100) {
758
+ nodes {
759
+ type
760
+ parameters {
761
+ ... on RequiredStatusChecksParameters {
762
+ requiredStatusChecks {
763
+ context
764
+ }
765
+ }
766
+ }
767
+ }
768
+ }
769
+ }
770
+ }
771
+ }
772
+ commits(last: 1) {
773
+ nodes {
774
+ commit {
775
+ statusCheckRollup {
776
+ state
777
+ contexts(first: 100) {
778
+ nodes {
779
+ __typename
780
+ ... on CheckRun {
781
+ name
782
+ conclusion
783
+ }
784
+ ... on StatusContext {
785
+ context
786
+ state
787
+ }
788
+ }
789
+ }
790
+ }
791
+ }
792
+ }
793
+ }
794
+ reviewThreads(first: 100) {
795
+ nodes {
796
+ isResolved
797
+ }
798
+ }
799
+ baseRef {
800
+ name
801
+ }
802
+ }
803
+ }
804
+ }
805
+ }
806
+ }
807
+ }
808
+ }
809
+ }
810
+ `;
811
+
812
+ const relatedPRsMap: Map<string, RelatedPullRequest> = new Map();
813
+ let after: string | null = null;
814
+ let hasNextPage = true;
815
+
816
+ while (hasNextPage) {
817
+ const response = await fetch('https://api.github.com/graphql', {
818
+ method: 'POST',
819
+ headers: {
820
+ Authorization: `Bearer ${this.ghToken}`,
821
+ 'Content-Type': 'application/json',
822
+ },
823
+ body: JSON.stringify({
824
+ query,
825
+ variables: { owner, repo, issueNumber, after },
826
+ }),
827
+ });
828
+
829
+ if (!response.ok) {
830
+ throw new Error(
831
+ `Failed to fetch issue timeline from GitHub GraphQL API: HTTP ${response.status}`,
832
+ );
833
+ }
834
+
835
+ const responseData: unknown = await response.json();
836
+ if (!isIssueTimelineResponse(responseData)) {
837
+ throw new Error(
838
+ 'Unexpected response shape when fetching issue timeline',
839
+ );
840
+ }
841
+
842
+ const issueData = responseData.data?.repository?.issue;
843
+ if (!issueData) {
844
+ throw new Error(
845
+ 'Issue not found when fetching timeline from GitHub GraphQL API',
846
+ );
847
+ }
848
+
849
+ for (const item of issueData.timelineItems.nodes) {
850
+ if (item.__typename !== 'CrossReferencedEvent') continue;
851
+ if (!item.source || item.source.__typename !== 'PullRequest') continue;
852
+ if (item.source.state !== 'OPEN') continue;
853
+ if (!item.willCloseTarget) continue;
854
+
855
+ const pr = item.source;
856
+ const prUrl = pr.url || '';
857
+ const baseRefName = pr.baseRefName ?? pr.baseRef?.name;
858
+
859
+ relatedPRsMap.set(
860
+ prUrl,
861
+ this.computePrStatus(prUrl, pr.headRefName, baseRefName, pr),
862
+ );
863
+ }
864
+
865
+ hasNextPage = issueData.timelineItems.pageInfo.hasNextPage;
866
+ after = issueData.timelineItems.pageInfo.endCursor;
867
+ }
868
+
869
+ return Array.from(relatedPRsMap.values());
333
870
  };
334
- getAllOpened = async (_project: Project): Promise<Issue[]> => {
335
- throw new Error('getAllOpened is not implemented');
871
+
872
+ getAllOpened = async (project: Project): Promise<Issue[]> => {
873
+ const { issues } = await this.getAllIssues(project.id, 0);
874
+ return issues.filter((issue) => !issue.isClosed);
336
875
  };
337
- getStoryObjectMap = async (_project: Project): Promise<StoryObjectMap> => {
338
- throw new Error('getStoryObjectMap is not implemented');
876
+
877
+ getStoryObjectMap = async (project: Project): Promise<StoryObjectMap> => {
878
+ const { issues } = await this.getAllIssues(project.id, 0);
879
+ const storyObjectMap: StoryObjectMap = new Map();
880
+ const targetStories = project.story?.stories || [];
881
+ for (const story of targetStories) {
882
+ const storyIssue = issues.find((issue) =>
883
+ story.name.startsWith(issue.title),
884
+ );
885
+ storyObjectMap.set(story.name, {
886
+ story,
887
+ storyIssue: storyIssue || null,
888
+ issues: [],
889
+ });
890
+ for (const issue of issues) {
891
+ if (issue.story !== story.name) continue;
892
+ storyObjectMap.get(story.name)?.issues.push(issue);
893
+ }
894
+ }
895
+ return storyObjectMap;
896
+ };
897
+
898
+ getOpenPullRequest = async (
899
+ prUrl: string,
900
+ ): Promise<RelatedPullRequest | null> => {
901
+ const parsedUrl = this.parseIssueUrl(prUrl);
902
+ if (!parsedUrl.isPr) {
903
+ return null;
904
+ }
905
+ const { owner, repo, issueNumber: prNumber } = parsedUrl;
906
+
907
+ const query = `
908
+ query($owner: String!, $repo: String!, $prNumber: Int!) {
909
+ repository(owner: $owner, name: $repo) {
910
+ pullRequest(number: $prNumber) {
911
+ url
912
+ state
913
+ headRefName
914
+ baseRefName
915
+ mergeable
916
+ baseRepository {
917
+ branchProtectionRules(first: 100) {
918
+ nodes {
919
+ pattern
920
+ requiredStatusCheckContexts
921
+ }
922
+ }
923
+ defaultBranchRef {
924
+ name
925
+ }
926
+ rulesets(first: 100) {
927
+ nodes {
928
+ name
929
+ enforcement
930
+ conditions {
931
+ refName {
932
+ include
933
+ exclude
934
+ }
935
+ }
936
+ rules(first: 100) {
937
+ nodes {
938
+ type
939
+ parameters {
940
+ ... on RequiredStatusChecksParameters {
941
+ requiredStatusChecks {
942
+ context
943
+ }
944
+ }
945
+ }
946
+ }
947
+ }
948
+ }
949
+ }
950
+ }
951
+ commits(last: 1) {
952
+ nodes {
953
+ commit {
954
+ statusCheckRollup {
955
+ state
956
+ contexts(first: 100) {
957
+ nodes {
958
+ __typename
959
+ ... on CheckRun {
960
+ name
961
+ conclusion
962
+ }
963
+ ... on StatusContext {
964
+ context
965
+ state
966
+ }
967
+ }
968
+ }
969
+ }
970
+ }
971
+ }
972
+ }
973
+ reviewThreads(first: 100) {
974
+ nodes {
975
+ isResolved
976
+ }
977
+ }
978
+ }
979
+ }
980
+ }
981
+ `;
982
+
983
+ const response = await fetch('https://api.github.com/graphql', {
984
+ method: 'POST',
985
+ headers: {
986
+ Authorization: `Bearer ${this.ghToken}`,
987
+ 'Content-Type': 'application/json',
988
+ },
989
+ body: JSON.stringify({
990
+ query,
991
+ variables: { owner, repo, prNumber },
992
+ }),
993
+ });
994
+
995
+ if (!response.ok) {
996
+ throw new Error(
997
+ `Failed to fetch pull request from GitHub GraphQL API: HTTP ${response.status}`,
998
+ );
999
+ }
1000
+
1001
+ const responseData: unknown = await response.json();
1002
+ if (!isDirectPullRequestResponse(responseData)) {
1003
+ throw new Error('Unexpected response shape when fetching pull request');
1004
+ }
1005
+
1006
+ if (responseData.errors && responseData.errors.length > 0) {
1007
+ throw new Error(`GraphQL errors: ${JSON.stringify(responseData.errors)}`);
1008
+ }
1009
+
1010
+ const pr = responseData.data?.repository?.pullRequest;
1011
+ if (!pr || pr.state !== 'OPEN') {
1012
+ return null;
1013
+ }
1014
+
1015
+ return this.computePrStatus(pr.url, pr.headRefName, pr.baseRefName, pr);
339
1016
  };
340
1017
  }