github-issue-tower-defence-management 1.2.0 → 1.4.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 (76) hide show
  1. package/.github/workflows/assign-all-cards-to-owner.yml +1 -1
  2. package/CHANGELOG.md +35 -0
  3. package/bin/adapter/entry-points/cli/index.js +2 -1
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +14 -2
  6. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  7. package/bin/adapter/repositories/BaseGitHubRepository.js +0 -7
  8. package/bin/adapter/repositories/BaseGitHubRepository.js.map +1 -1
  9. package/bin/adapter/repositories/GraphqlProjectRepository.js +14 -0
  10. package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
  11. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +9 -1
  12. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  13. package/bin/adapter/repositories/issue/ApiV3IssueRepository.js +17 -0
  14. package/bin/adapter/repositories/issue/ApiV3IssueRepository.js.map +1 -1
  15. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js +19 -5
  16. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js.map +1 -1
  17. package/bin/domain/usecases/AnalyzeStoriesUseCase.js +83 -17
  18. package/bin/domain/usecases/AnalyzeStoriesUseCase.js.map +1 -1
  19. package/bin/domain/usecases/ChangeStatusLongInReviewIssueUseCase.js +37 -0
  20. package/bin/domain/usecases/ChangeStatusLongInReviewIssueUseCase.js.map +1 -0
  21. package/bin/domain/usecases/ClearNextActionHourUseCase.js +8 -2
  22. package/bin/domain/usecases/ClearNextActionHourUseCase.js.map +1 -1
  23. package/bin/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.js +63 -0
  24. package/bin/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.js.map +1 -0
  25. package/bin/domain/usecases/CreateEstimationIssueUseCase.js +15 -0
  26. package/bin/domain/usecases/CreateEstimationIssueUseCase.js.map +1 -1
  27. package/bin/domain/usecases/HandleScheduledEventUseCase.js +14 -2
  28. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/adapter/entry-points/cli/index.test.ts +1 -0
  31. package/src/adapter/entry-points/cli/index.ts +3 -1
  32. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +19 -0
  33. package/src/adapter/repositories/BaseGitHubRepository.ts +0 -11
  34. package/src/adapter/repositories/GraphqlProjectRepository.test.ts +25 -0
  35. package/src/adapter/repositories/GraphqlProjectRepository.ts +16 -0
  36. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +21 -1
  37. package/src/adapter/repositories/issue/ApiV3IssueRepository.test.ts +14 -0
  38. package/src/adapter/repositories/issue/ApiV3IssueRepository.ts +31 -0
  39. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.ts +22 -5
  40. package/src/domain/entities/Project.ts +8 -2
  41. package/src/domain/usecases/AnalyzeStoriesUseCase.ts +155 -24
  42. package/src/domain/usecases/CONTRIBUTING_FOR_TEST.md +254 -0
  43. package/src/domain/usecases/ChangeStatusLongInReviewIssueUseCase.test.ts +204 -0
  44. package/src/domain/usecases/ChangeStatusLongInReviewIssueUseCase.ts +57 -0
  45. package/src/domain/usecases/ClearNextActionHourUseCase.ts +12 -2
  46. package/src/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.test.ts +490 -0
  47. package/src/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.ts +98 -0
  48. package/src/domain/usecases/CreateEstimationIssueUseCase.ts +39 -2
  49. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +1 -1
  50. package/src/domain/usecases/HandleScheduledEventUseCase.ts +14 -1
  51. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +5 -0
  52. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts +1 -1
  53. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  54. package/types/adapter/repositories/BaseGitHubRepository.d.ts.map +1 -1
  55. package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
  56. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
  57. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  58. package/types/adapter/repositories/issue/ApiV3IssueRepository.d.ts +5 -0
  59. package/types/adapter/repositories/issue/ApiV3IssueRepository.d.ts.map +1 -1
  60. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts.map +1 -1
  61. package/types/domain/entities/Project.d.ts +8 -2
  62. package/types/domain/entities/Project.d.ts.map +1 -1
  63. package/types/domain/usecases/AnalyzeStoriesUseCase.d.ts +19 -3
  64. package/types/domain/usecases/AnalyzeStoriesUseCase.d.ts.map +1 -1
  65. package/types/domain/usecases/ChangeStatusLongInReviewIssueUseCase.d.ts +17 -0
  66. package/types/domain/usecases/ChangeStatusLongInReviewIssueUseCase.d.ts.map +1 -0
  67. package/types/domain/usecases/ClearNextActionHourUseCase.d.ts.map +1 -1
  68. package/types/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.d.ts +20 -0
  69. package/types/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.d.ts.map +1 -0
  70. package/types/domain/usecases/CreateEstimationIssueUseCase.d.ts +3 -3
  71. package/types/domain/usecases/CreateEstimationIssueUseCase.d.ts.map +1 -1
  72. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -1
  73. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  74. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
  75. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  76. package/types/index.d.ts +1 -1
@@ -0,0 +1,57 @@
1
+ import { Issue } from '../entities/Issue';
2
+ import { IssueRepository } from './adapter-interfaces/IssueRepository';
3
+ import { Project } from '../entities/Project';
4
+ import { DateRepository } from './adapter-interfaces/DateRepository';
5
+
6
+ export class ChangeStatusLongInReviewIssueUseCase {
7
+ constructor(
8
+ readonly dateRepository: Pick<DateRepository, 'now'>,
9
+ readonly issueRepository: Pick<
10
+ IssueRepository,
11
+ 'updateStatus' | 'createComment'
12
+ >,
13
+ ) {}
14
+
15
+ run = async (input: {
16
+ project: Project;
17
+ issues: Issue[];
18
+ cacheUsed: boolean;
19
+ org: string;
20
+ repo: string;
21
+ }): Promise<void> => {
22
+ const firstStatus = input.project.status.statuses[0];
23
+ if (!firstStatus) {
24
+ throw new Error('First status is not found');
25
+ }
26
+ for (const issue of input.issues) {
27
+ if (
28
+ issue.isPr ||
29
+ issue.isClosed ||
30
+ !issue.status?.toLowerCase().includes('review')
31
+ ) {
32
+ continue;
33
+ }
34
+ if (issue.workingTimeline.length > 0) {
35
+ const latestWorkingTime =
36
+ issue.workingTimeline[issue.workingTimeline.length - 1];
37
+ const now = await this.dateRepository.now();
38
+ const diff = now.getTime() - latestWorkingTime.endedAt.getTime();
39
+ const diffHours = diff / (1000 * 60 * 60);
40
+ if (diffHours < 48) {
41
+ continue;
42
+ }
43
+ }
44
+ await this.issueRepository.createComment(
45
+ issue,
46
+ `${issue.assignees.map((assignee) => `@${assignee}`).join(' ')}
47
+ This issue has been in review for more than 48 hours.
48
+ Please check the situation of PR again :pray:`,
49
+ );
50
+ await this.issueRepository.updateStatus(
51
+ input.project,
52
+ issue,
53
+ firstStatus.id,
54
+ );
55
+ }
56
+ };
57
+ }
@@ -14,9 +14,10 @@ export class ClearNextActionHourUseCase {
14
14
  cacheUsed: boolean;
15
15
  }): Promise<void> => {
16
16
  const nextActionHour = input.project.nextActionHour;
17
- if (!nextActionHour || input.cacheUsed) {
17
+ if (!nextActionHour) {
18
18
  return;
19
19
  }
20
+ const nextActionDate = input.project.nextActionDate;
20
21
  const targetDates = input.targetDates
21
22
  .filter((targetDate) => targetDate.getMinutes() === 45)
22
23
  .reverse();
@@ -26,7 +27,7 @@ export class ClearNextActionHourUseCase {
26
27
  const targetDate = new Date(
27
28
  targetDates[targetDates.length - 1].getTime() + 5 * 60 * 1000,
28
29
  );
29
- const targetHour = targetDate.getHours();
30
+ const targetHour = targetDate.getHours() + 1;
30
31
  const isTargetIssue = (issue: Issue): boolean => {
31
32
  return (
32
33
  issue.nextActionHour !== null &&
@@ -46,6 +47,15 @@ export class ClearNextActionHourUseCase {
46
47
  issue,
47
48
  );
48
49
  await new Promise((resolve) => setTimeout(resolve, 5000));
50
+ if (!nextActionDate) {
51
+ continue;
52
+ }
53
+ await this.issueRepository.clearProjectField(
54
+ input.project,
55
+ nextActionDate.fieldId,
56
+ issue,
57
+ );
58
+ await new Promise((resolve) => setTimeout(resolve, 5000));
49
59
  }
50
60
  };
51
61
  }
@@ -0,0 +1,490 @@
1
+ import { mock } from 'jest-mock-extended';
2
+ import { IssueRepository } from './adapter-interfaces/IssueRepository';
3
+ import { ConvertCheckboxToIssueInStoryIssueUseCase } from './ConvertCheckboxToIssueInStoryIssueUseCase';
4
+ import { Project, StoryOption } from '../entities/Project';
5
+ import { Issue } from '../entities/Issue';
6
+ import { StoryObject, StoryObjectMap } from './HandleScheduledEventUseCase';
7
+
8
+ describe('ConvertCheckboxToIssueInStoryIssueUseCase', () => {
9
+ jest.setTimeout(5 * 60 * 1000);
10
+ const mockIssueRepository = mock<IssueRepository>();
11
+
12
+ describe('run', () => {
13
+ const basicProject = {
14
+ ...mock<Project>(),
15
+ story: {
16
+ name: 'Story Field',
17
+ fieldId: 'storyFieldId',
18
+ stories: [
19
+ { ...mock<StoryOption>(), id: 'story1', name: 'Story 1' },
20
+ { ...mock<StoryOption>(), id: 'story2', name: 'Story 2' },
21
+ { ...mock<StoryOption>(), id: 'regular3', name: 'regular / Story 3' },
22
+ ],
23
+ workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
24
+ },
25
+ };
26
+
27
+ const basicStoryIssue1 = {
28
+ ...mock<Issue>(),
29
+ title: 'Story 1',
30
+ number: 123,
31
+ body: `- [ ] Task 1
32
+ - [ ] Task 2`,
33
+ url: 'https://github.com/org/repo/issues/123',
34
+ };
35
+
36
+ const basicStoryIssue2 = {
37
+ ...mock<Issue>(),
38
+ title: 'Story 2',
39
+ number: 456,
40
+ body: `- [ ] Task 3
41
+ - [ ] Task 4`,
42
+ url: 'https://github.com/org/repo/issues/456',
43
+ };
44
+
45
+ const basicStoryObject1: StoryObject = {
46
+ story: { ...mock<StoryOption>(), id: 'story1', name: 'Story 1' },
47
+ storyIssue: basicStoryIssue1,
48
+ issues: [],
49
+ };
50
+ const basicStoryObject2: StoryObject = {
51
+ story: { ...mock<StoryOption>(), id: 'story2', name: 'Story 2' },
52
+ storyIssue: basicStoryIssue2,
53
+ issues: [],
54
+ };
55
+
56
+ const basicStoryObjectMap: StoryObjectMap = new Map([
57
+ ['Story 1', basicStoryObject1],
58
+ ['Story 2', basicStoryObject2],
59
+ ]);
60
+
61
+ const regularStoryProject = {
62
+ ...basicProject,
63
+ story: {
64
+ name: 'Story Field',
65
+ fieldId: 'storyFieldId',
66
+ stories: [
67
+ { ...mock<StoryOption>(), id: 'regular1', name: 'regular / Story 1' },
68
+ ],
69
+ workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
70
+ },
71
+ };
72
+
73
+ const regularStoryObjectMap = new Map([
74
+ [
75
+ 'regular / Story 1',
76
+ {
77
+ story: {
78
+ ...mock<StoryOption>(),
79
+ id: 'regular1',
80
+ name: 'regular / Story 1',
81
+ },
82
+ storyIssue: basicStoryIssue1,
83
+ issues: [],
84
+ },
85
+ ],
86
+ ]);
87
+
88
+ const testCases: {
89
+ name: string;
90
+ input: {
91
+ project: Project;
92
+ issues: Issue[];
93
+ cacheUsed: boolean;
94
+ org: string;
95
+ repo: string;
96
+ urlOfStoryView: string;
97
+ disabledStatus: string;
98
+ storyObjectMap: StoryObjectMap;
99
+ };
100
+ expectedThrowError?: Error;
101
+ expectedCreateNewIssueCalls: [
102
+ string,
103
+ string,
104
+ string,
105
+ string,
106
+ string[],
107
+ string[],
108
+ ][];
109
+ expectedUpdateIssueCalls: [Issue][];
110
+ expectedUpdateStoryCalls: [Project, Issue, string][];
111
+ expectedGetIssueByUrlCalls: [string][];
112
+ }[] = [
113
+ {
114
+ name: 'should not process when story is not set',
115
+ input: {
116
+ project: { ...basicProject, story: null },
117
+ issues: [basicStoryIssue1],
118
+ cacheUsed: false,
119
+ org: 'testOrg',
120
+ repo: 'testRepo',
121
+ urlOfStoryView: 'https://example.com',
122
+ disabledStatus: 'Closed',
123
+ storyObjectMap: basicStoryObjectMap,
124
+ },
125
+ expectedCreateNewIssueCalls: [],
126
+ expectedUpdateIssueCalls: [],
127
+ expectedUpdateStoryCalls: [],
128
+ expectedGetIssueByUrlCalls: [],
129
+ },
130
+ {
131
+ name: 'should not process when cache is used',
132
+ input: {
133
+ project: basicProject,
134
+ issues: [basicStoryIssue1],
135
+ cacheUsed: true,
136
+ org: 'testOrg',
137
+ repo: 'testRepo',
138
+ urlOfStoryView: 'https://example.com',
139
+ disabledStatus: 'Closed',
140
+ storyObjectMap: basicStoryObjectMap,
141
+ },
142
+ expectedCreateNewIssueCalls: [],
143
+ expectedUpdateIssueCalls: [],
144
+ expectedUpdateStoryCalls: [],
145
+ expectedGetIssueByUrlCalls: [],
146
+ },
147
+ {
148
+ name: 'should skip regular stories',
149
+ input: {
150
+ project: regularStoryProject,
151
+ issues: [basicStoryIssue1],
152
+ cacheUsed: false,
153
+ org: 'testOrg',
154
+ repo: 'testRepo',
155
+ urlOfStoryView: 'https://example.com',
156
+ disabledStatus: 'Closed',
157
+ storyObjectMap: regularStoryObjectMap,
158
+ },
159
+ expectedCreateNewIssueCalls: [],
160
+ expectedUpdateIssueCalls: [],
161
+ expectedUpdateStoryCalls: [],
162
+ expectedGetIssueByUrlCalls: [],
163
+ },
164
+ {
165
+ name: 'should throw error when story issue not found',
166
+ input: {
167
+ project: basicProject,
168
+ issues: [],
169
+ cacheUsed: false,
170
+ org: 'testOrg',
171
+ repo: 'testRepo',
172
+ urlOfStoryView: 'https://example.com',
173
+ disabledStatus: 'Closed',
174
+ storyObjectMap: basicStoryObjectMap,
175
+ },
176
+ expectedThrowError: new Error('Story issue not found: Story 1'),
177
+ expectedCreateNewIssueCalls: [],
178
+ expectedUpdateIssueCalls: [],
179
+ expectedUpdateStoryCalls: [],
180
+ expectedGetIssueByUrlCalls: [],
181
+ },
182
+
183
+ {
184
+ name: 'should skip closed story issues or disabled status',
185
+ input: {
186
+ project: basicProject,
187
+ issues: [
188
+ {
189
+ ...basicStoryIssue1,
190
+ state: 'CLOSED',
191
+ isClosed: true,
192
+ },
193
+ {
194
+ ...basicStoryIssue2,
195
+ status: 'Disabled',
196
+ },
197
+ ],
198
+ cacheUsed: false,
199
+ org: 'testOrg',
200
+ repo: 'testRepo',
201
+ urlOfStoryView: 'https://example.com',
202
+ disabledStatus: 'Disabled',
203
+ storyObjectMap: basicStoryObjectMap,
204
+ },
205
+ expectedCreateNewIssueCalls: [],
206
+ expectedUpdateIssueCalls: [],
207
+ expectedUpdateStoryCalls: [],
208
+ expectedGetIssueByUrlCalls: [],
209
+ },
210
+ {
211
+ name: 'should create new issues for checkboxes and update story issue',
212
+ input: {
213
+ project: basicProject,
214
+ issues: [basicStoryIssue1, basicStoryIssue2],
215
+ cacheUsed: false,
216
+ org: 'testOrg',
217
+ repo: 'testRepo',
218
+ urlOfStoryView: 'https://example.com',
219
+ disabledStatus: 'Closed',
220
+ storyObjectMap: basicStoryObjectMap,
221
+ },
222
+ expectedCreateNewIssueCalls: [
223
+ ['testOrg', 'testRepo', 'Task 1', '', [], []],
224
+ ['testOrg', 'testRepo', 'Task 2', '', [], []],
225
+ ['testOrg', 'testRepo', 'Task 3', '', [], []],
226
+ ['testOrg', 'testRepo', 'Task 4', '', [], []],
227
+ ],
228
+ expectedUpdateIssueCalls: [
229
+ [
230
+ {
231
+ ...basicStoryIssue1,
232
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/1
233
+ - [ ] Task 2`,
234
+ },
235
+ ],
236
+ [
237
+ {
238
+ ...basicStoryIssue1,
239
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/1
240
+ - [ ] https://github.com/testOrg/testRepo/issues/2`,
241
+ },
242
+ ],
243
+ [
244
+ {
245
+ ...basicStoryIssue2,
246
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/3
247
+ - [ ] Task 4`,
248
+ },
249
+ ],
250
+ [
251
+ {
252
+ ...basicStoryIssue2,
253
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/3
254
+ - [ ] https://github.com/testOrg/testRepo/issues/4`,
255
+ },
256
+ ],
257
+ ],
258
+ expectedUpdateStoryCalls: [
259
+ [
260
+ basicProject,
261
+ {
262
+ ...mock<Issue>(),
263
+ url: 'https://github.com/testOrg/testRepo/issues/1',
264
+ },
265
+ 'story1',
266
+ ],
267
+ [
268
+ basicProject,
269
+ {
270
+ ...mock<Issue>(),
271
+ url: 'https://github.com/testOrg/testRepo/issues/2',
272
+ },
273
+ 'story1',
274
+ ],
275
+ [
276
+ basicProject,
277
+ {
278
+ ...mock<Issue>(),
279
+ url: 'https://github.com/testOrg/testRepo/issues/3',
280
+ },
281
+ 'story2',
282
+ ],
283
+ [
284
+ basicProject,
285
+ {
286
+ ...mock<Issue>(),
287
+ url: 'https://github.com/testOrg/testRepo/issues/4',
288
+ },
289
+ 'story2',
290
+ ],
291
+ ],
292
+ expectedGetIssueByUrlCalls: [
293
+ ['https://github.com/testOrg/testRepo/issues/1'],
294
+ ['https://github.com/testOrg/testRepo/issues/2'],
295
+ ['https://github.com/testOrg/testRepo/issues/3'],
296
+ ['https://github.com/testOrg/testRepo/issues/4'],
297
+ ],
298
+ },
299
+ {
300
+ name: 'should create new issues with replaced STORYNAME for checkboxes and update story issue',
301
+ input: {
302
+ project: basicProject,
303
+ issues: [
304
+ {
305
+ ...basicStoryIssue1,
306
+ body: `- [ ] Task 1
307
+ - [ ] Task 2 for \`STORYNAME\``,
308
+ },
309
+ basicStoryIssue2,
310
+ ],
311
+ cacheUsed: false,
312
+ org: 'testOrg',
313
+ repo: 'testRepo',
314
+ urlOfStoryView: 'https://example.com',
315
+ disabledStatus: 'Closed',
316
+ storyObjectMap: basicStoryObjectMap,
317
+ },
318
+ expectedCreateNewIssueCalls: [
319
+ ['testOrg', 'testRepo', 'Task 1', '', [], []],
320
+ ['testOrg', 'testRepo', 'Task 2 for `Story 1 #123`', '', [], []],
321
+ ['testOrg', 'testRepo', 'Task 3', '', [], []],
322
+ ['testOrg', 'testRepo', 'Task 4', '', [], []],
323
+ ],
324
+ expectedUpdateIssueCalls: [
325
+ [
326
+ {
327
+ ...basicStoryIssue1,
328
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/1
329
+ - [ ] Task 2 for \`STORYNAME\``,
330
+ },
331
+ ],
332
+ [
333
+ {
334
+ ...basicStoryIssue1,
335
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/1
336
+ - [ ] https://github.com/testOrg/testRepo/issues/2`,
337
+ },
338
+ ],
339
+ [
340
+ {
341
+ ...basicStoryIssue2,
342
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/3
343
+ - [ ] Task 4`,
344
+ },
345
+ ],
346
+ [
347
+ {
348
+ ...basicStoryIssue2,
349
+ body: `- [ ] https://github.com/testOrg/testRepo/issues/3
350
+ - [ ] https://github.com/testOrg/testRepo/issues/4`,
351
+ },
352
+ ],
353
+ ],
354
+ expectedUpdateStoryCalls: [
355
+ [
356
+ basicProject,
357
+ {
358
+ ...mock<Issue>(),
359
+ url: 'https://github.com/testOrg/testRepo/issues/1',
360
+ },
361
+ 'story1',
362
+ ],
363
+ [
364
+ basicProject,
365
+ {
366
+ ...mock<Issue>(),
367
+ url: 'https://github.com/testOrg/testRepo/issues/2',
368
+ },
369
+ 'story1',
370
+ ],
371
+ [
372
+ basicProject,
373
+ {
374
+ ...mock<Issue>(),
375
+ url: 'https://github.com/testOrg/testRepo/issues/3',
376
+ },
377
+ 'story2',
378
+ ],
379
+ [
380
+ basicProject,
381
+ {
382
+ ...mock<Issue>(),
383
+ url: 'https://github.com/testOrg/testRepo/issues/4',
384
+ },
385
+ 'story2',
386
+ ],
387
+ ],
388
+ expectedGetIssueByUrlCalls: [
389
+ ['https://github.com/testOrg/testRepo/issues/1'],
390
+ ['https://github.com/testOrg/testRepo/issues/2'],
391
+ ['https://github.com/testOrg/testRepo/issues/3'],
392
+ ['https://github.com/testOrg/testRepo/issues/4'],
393
+ ],
394
+ },
395
+ ];
396
+
397
+ testCases.forEach(
398
+ ({
399
+ name,
400
+ input,
401
+ expectedThrowError,
402
+ expectedCreateNewIssueCalls,
403
+ expectedUpdateIssueCalls,
404
+ expectedUpdateStoryCalls,
405
+ expectedGetIssueByUrlCalls,
406
+ }) => {
407
+ it(name, async () => {
408
+ jest.clearAllMocks();
409
+
410
+ let issueCounter = 1;
411
+ mockIssueRepository.createNewIssue.mockImplementation(
412
+ async () => issueCounter++,
413
+ );
414
+ mockIssueRepository.getIssueByUrl.mockImplementation(async (url) => ({
415
+ ...mock<Issue>(),
416
+ url,
417
+ }));
418
+
419
+ const useCase = new ConvertCheckboxToIssueInStoryIssueUseCase(
420
+ mockIssueRepository,
421
+ );
422
+ try {
423
+ await useCase.run(input);
424
+ } catch (e) {
425
+ if (expectedThrowError === undefined) {
426
+ throw e;
427
+ }
428
+ if (e === null || typeof e !== 'object' || !('message' in e)) {
429
+ expect(e).toEqual(expectedThrowError);
430
+ }
431
+ }
432
+
433
+ expect(mockIssueRepository.createNewIssue.mock.calls).toEqual(
434
+ expectedCreateNewIssueCalls,
435
+ );
436
+ expect(mockIssueRepository.updateIssue.mock.calls).toEqual(
437
+ expectedUpdateIssueCalls,
438
+ );
439
+ expect(mockIssueRepository.updateStory.mock.calls).toEqual(
440
+ expectedUpdateStoryCalls,
441
+ );
442
+ expect(mockIssueRepository.getIssueByUrl.mock.calls).toEqual(
443
+ expectedGetIssueByUrlCalls,
444
+ );
445
+ });
446
+ },
447
+ );
448
+ });
449
+
450
+ describe('findCheckboxTextsNotCreatedIssue', () => {
451
+ const testCases: {
452
+ name: string;
453
+ input: string;
454
+ expected: string[];
455
+ }[] = [
456
+ {
457
+ name: 'should find checkbox texts not created as issues',
458
+ input: `- [ ] Task 1
459
+ - [ ] Task 2
460
+ - [x] Task 3
461
+ - [ ] #5
462
+ - [ ] #6
463
+ - [ ] https://github.com/org/repo/issues/1
464
+ - [ ] https://github.com/org/repo/issues/7 `,
465
+ expected: ['Task 1', 'Task 2'],
466
+ },
467
+ {
468
+ name: 'should return empty array when no checkboxes found',
469
+ input: 'No checkboxes here',
470
+ expected: [],
471
+ },
472
+ {
473
+ name: 'should return empty array when all checkboxes are issues',
474
+ input: `- [ ] https://github.com/org/repo/issues/1
475
+ - [ ] https://github.com/org/repo/issues/2`,
476
+ expected: [],
477
+ },
478
+ ];
479
+
480
+ testCases.forEach(({ name, input, expected }) => {
481
+ it(name, () => {
482
+ const useCase = new ConvertCheckboxToIssueInStoryIssueUseCase(
483
+ mockIssueRepository,
484
+ );
485
+ const result = useCase.findCheckboxTextsNotCreatedIssue(input);
486
+ expect(result).toEqual(expected);
487
+ });
488
+ });
489
+ });
490
+ });
@@ -0,0 +1,98 @@
1
+ import { Issue } from '../entities/Issue';
2
+ import { IssueRepository } from './adapter-interfaces/IssueRepository';
3
+ import { Project } from '../entities/Project';
4
+ import { StoryObjectMap } from './HandleScheduledEventUseCase';
5
+
6
+ export class ConvertCheckboxToIssueInStoryIssueUseCase {
7
+ constructor(
8
+ readonly issueRepository: Pick<
9
+ IssueRepository,
10
+ 'createNewIssue' | 'updateIssue' | 'updateStory' | 'getIssueByUrl'
11
+ >,
12
+ ) {}
13
+
14
+ run = async (input: {
15
+ project: Project;
16
+ issues: Issue[];
17
+ cacheUsed: boolean;
18
+ org: string;
19
+ repo: string;
20
+ urlOfStoryView: string;
21
+ disabledStatus: string;
22
+ storyObjectMap: StoryObjectMap;
23
+ }): Promise<void> => {
24
+ const story = input.project.story;
25
+ if (!story || input.cacheUsed) {
26
+ return;
27
+ }
28
+
29
+ for (const storyOption of input.project.story?.stories || []) {
30
+ const storyIssue = input.issues.find((issue) =>
31
+ storyOption.name.startsWith(issue.title),
32
+ );
33
+ const storyObject = input.storyObjectMap.get(storyOption.name);
34
+ if (storyOption.name.startsWith('regular / ')) {
35
+ continue;
36
+ } else if (!storyIssue || !storyObject) {
37
+ throw new Error(`Story issue not found: ${storyOption.name}`);
38
+ } else if (
39
+ storyIssue.isClosed ||
40
+ storyIssue.status === input.disabledStatus
41
+ ) {
42
+ continue;
43
+ } else if (!storyIssue.body.includes('- [ ] ')) {
44
+ continue;
45
+ }
46
+ const checkboxTextsNotCreatedIssue =
47
+ this.findCheckboxTextsNotCreatedIssue(storyIssue.body);
48
+ let newBody = storyIssue.body;
49
+ for (const checkboxText of checkboxTextsNotCreatedIssue) {
50
+ const issueTitle = checkboxText.replace(
51
+ 'STORYNAME',
52
+ `${storyOption.name} #${storyIssue.number}`,
53
+ );
54
+ const newIssueNumber = await this.issueRepository.createNewIssue(
55
+ input.org,
56
+ input.repo,
57
+ issueTitle,
58
+ '',
59
+ [],
60
+ [],
61
+ );
62
+ const newIssueUrl = `https://github.com/${input.org}/${input.repo}/issues/${newIssueNumber}`;
63
+ newBody = newBody.replace(
64
+ `- [ ] ${checkboxText}`,
65
+ `- [ ] ${newIssueUrl}`,
66
+ );
67
+ await this.issueRepository.updateIssue({
68
+ ...storyIssue,
69
+ body: newBody,
70
+ });
71
+ await new Promise((resolve) => setTimeout(resolve, 30 * 1000));
72
+ const newIssue = await this.issueRepository.getIssueByUrl(newIssueUrl);
73
+ if (!newIssue) {
74
+ throw new Error(`Issue not found: ${newIssueUrl}`);
75
+ }
76
+ await this.issueRepository.updateStory(
77
+ { ...input.project, story: story },
78
+ newIssue,
79
+ storyOption.id,
80
+ );
81
+ }
82
+ }
83
+ };
84
+ findCheckboxTextsNotCreatedIssue = (storyIssueBody: string): string[] => {
85
+ const regexToFindCheckboxes = /^- \[ ] (.*)$/gm;
86
+ const match = storyIssueBody.match(regexToFindCheckboxes);
87
+ if (!match) return [];
88
+ const checkboxes: string[] = [];
89
+ for (let i = 0; i < match.length; i++) {
90
+ checkboxes.push(match[i].replace('- [ ] ', '').trim());
91
+ }
92
+ return checkboxes.filter(
93
+ (checkbox) =>
94
+ !checkbox.match(/^https:\/\/github.com\/.*\/issues\/\d+$/) &&
95
+ !checkbox.match(/^#\d+$/),
96
+ );
97
+ };
98
+ }