github-issue-tower-defence-management 1.38.1 → 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.
Files changed (24) hide show
  1. package/.github/workflows/api-created_issue_pr.yml +82 -0
  2. package/.github/workflows/create-pr.yml +12 -5
  3. package/.github/workflows/umino-project.yml +45 -16
  4. package/CHANGELOG.md +20 -0
  5. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +206 -6
  6. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  7. package/bin/domain/usecases/HandleScheduledEventUseCase.js +7 -4
  8. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  9. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +111 -16
  10. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +324 -8
  13. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +146 -0
  14. package/src/domain/usecases/HandleScheduledEventUseCase.ts +17 -3
  15. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +982 -167
  16. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +177 -26
  17. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +3 -4
  18. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +3 -4
  19. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  20. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  21. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +5 -1
  22. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  23. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +2 -3
  24. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
@@ -12,6 +12,7 @@ import { DateRepository } from './adapter-interfaces/DateRepository';
12
12
  import { SpreadsheetRepository } from './adapter-interfaces/SpreadsheetRepository';
13
13
  import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
14
14
  import { IssueRepository } from './adapter-interfaces/IssueRepository';
15
+ import { Issue } from '../entities/Issue';
15
16
  import { Project } from '../entities/Project';
16
17
  import { ChangeStatusByStoryColorUseCase } from './ChangeStatusByStoryColorUseCase';
17
18
  import { SetNoStoryIssueToStoryUseCase } from './SetNoStoryIssueToStoryUseCase';
@@ -309,5 +310,150 @@ describe('HandleScheduledEventUseCase', () => {
309
310
  120,
310
311
  );
311
312
  });
313
+
314
+ describe('story issue creation progress logs', () => {
315
+ const storyInput = {
316
+ projectName: 'test-project',
317
+ org: 'test-org',
318
+ projectUrl: 'https://github.com/test-org/test-project',
319
+ manager: 'test-manager',
320
+ workingReport: {
321
+ repo: 'test-repo',
322
+ members: ['member1'],
323
+ spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
324
+ },
325
+ urlOfStoryView: 'https://github.com/test-org/test-project/issues',
326
+ disabledStatus: 'disabled',
327
+ defaultStatus: null,
328
+ disabled: false,
329
+ allowIssueCacheMinutes: 60,
330
+ };
331
+
332
+ const storyProject: Project = {
333
+ id: 'proj-1',
334
+ url: 'https://github.com/orgs/test-org/projects/1',
335
+ databaseId: 1,
336
+ name: 'test-project',
337
+ status: { name: 'Status', fieldId: 'f1', statuses: [] },
338
+ nextActionDate: null,
339
+ nextActionHour: null,
340
+ story: {
341
+ name: 'Story',
342
+ fieldId: 'f2',
343
+ databaseId: 2,
344
+ stories: [
345
+ {
346
+ id: 'story-1',
347
+ name: 'feature / StoryOne',
348
+ color: 'BLUE',
349
+ description: 'story desc',
350
+ },
351
+ ],
352
+ workflowManagementStory: { id: 'wm-1', name: 'workflow' },
353
+ },
354
+ remainingEstimationMinutes: null,
355
+ dependedIssueUrlSeparatedByComma: null,
356
+ completionDate50PercentConfidence: null,
357
+ };
358
+
359
+ const capturedLogs: string[] = [];
360
+ let consoleSpy: jest.SpyInstance;
361
+
362
+ beforeEach(() => {
363
+ capturedLogs.length = 0;
364
+ consoleSpy = jest
365
+ .spyOn(console, 'log')
366
+ .mockImplementation((...data: unknown[]) => {
367
+ const firstData = data[0];
368
+ if (
369
+ typeof firstData === 'string' &&
370
+ firstData.startsWith('[HandleScheduledEvent]')
371
+ ) {
372
+ capturedLogs.push(firstData);
373
+ }
374
+ });
375
+ jest.useFakeTimers();
376
+ mockProjectRepository.getProject.mockResolvedValue(storyProject);
377
+ mockIssueRepository.getAllIssues.mockResolvedValue({
378
+ issues: [],
379
+ cacheUsed: false,
380
+ });
381
+ mockIssueRepository.createNewIssue.mockResolvedValue(99);
382
+ const createdIssue = mock<Issue>();
383
+ createdIssue.itemId = 'item-99';
384
+ mockIssueRepository.getIssueByUrl.mockResolvedValue(createdIssue);
385
+ });
386
+
387
+ afterEach(() => {
388
+ consoleSpy.mockRestore();
389
+ jest.useRealTimers();
390
+ });
391
+
392
+ it('should emit Creating story issue log before createNewIssue', async () => {
393
+ const runPromise = useCase.run(storyInput);
394
+ await jest.runAllTimersAsync();
395
+ await runPromise;
396
+
397
+ expect(capturedLogs[0]).toContain('Creating story issue');
398
+ expect(capturedLogs[0]).toContain('feature / StoryOne');
399
+ });
400
+
401
+ it('should emit Polling for issue log before each 30s sleep', async () => {
402
+ const runPromise = useCase.run(storyInput);
403
+ await jest.runAllTimersAsync();
404
+ await runPromise;
405
+
406
+ expect(capturedLogs[1]).toContain('Polling for issue (attempt 1/3)');
407
+ expect(capturedLogs[1]).toContain(
408
+ 'https://github.com/test-org/test-repo/issues/99',
409
+ );
410
+ });
411
+
412
+ it('should emit Issue found log on successful issue lookup', async () => {
413
+ const runPromise = useCase.run(storyInput);
414
+ await jest.runAllTimersAsync();
415
+ await runPromise;
416
+
417
+ expect(capturedLogs[2]).toContain('Issue found');
418
+ expect(capturedLogs[2]).toContain(
419
+ 'https://github.com/test-org/test-repo/issues/99',
420
+ );
421
+ expect(capturedLogs[2]).toContain('itemId=item-99');
422
+ });
423
+
424
+ it('should emit Waiting for story update log before 10s sleep', async () => {
425
+ const runPromise = useCase.run(storyInput);
426
+ await jest.runAllTimersAsync();
427
+ await runPromise;
428
+
429
+ expect(capturedLogs[3]).toContain('Waiting for story update');
430
+ expect(capturedLogs[3]).toContain(
431
+ 'https://github.com/test-org/test-repo/issues/99',
432
+ );
433
+ });
434
+
435
+ it('should emit Story issue created log with elapsed time after iteration completes', async () => {
436
+ const runPromise = useCase.run(storyInput);
437
+ await jest.runAllTimersAsync();
438
+ await runPromise;
439
+
440
+ expect(capturedLogs[4]).toContain('Story issue created');
441
+ expect(capturedLogs[4]).toContain('feature / StoryOne');
442
+ expect(capturedLogs[4]).toMatch(/elapsed=\d+ms/);
443
+ });
444
+
445
+ it('should emit logs in expected order', async () => {
446
+ const runPromise = useCase.run(storyInput);
447
+ await jest.runAllTimersAsync();
448
+ await runPromise;
449
+
450
+ expect(capturedLogs).toHaveLength(5);
451
+ expect(capturedLogs[0]).toContain('Creating story issue');
452
+ expect(capturedLogs[1]).toContain('Polling for issue (attempt 1/3)');
453
+ expect(capturedLogs[2]).toContain('Issue found');
454
+ expect(capturedLogs[3]).toContain('Waiting for story update');
455
+ expect(capturedLogs[4]).toContain('Story issue created');
456
+ });
457
+ });
312
458
  });
313
459
  });
@@ -131,6 +131,10 @@ export class HandleScheduledEventUseCase {
131
131
  ) {
132
132
  continue;
133
133
  }
134
+ const storyStartTime = Date.now();
135
+ console.log(
136
+ `[HandleScheduledEvent] Creating story issue: story="${storyObject.story.name}"`,
137
+ );
134
138
  const issueNumber = await this.issueRepository.createNewIssue(
135
139
  input.org,
136
140
  input.workingReport.repo,
@@ -142,13 +146,17 @@ export class HandleScheduledEventUseCase {
142
146
  const issueUrl = `https://github.com/${input.org}/${input.workingReport.repo}/issues/${issueNumber}`;
143
147
  let issue: Issue | null = null;
144
148
  for (let i = 0; i < 3; i++) {
149
+ console.log(
150
+ `[HandleScheduledEvent] Polling for issue (attempt ${i + 1}/3): url=${issueUrl}`,
151
+ );
145
152
  await new Promise((resolve) => setTimeout(resolve, 30 * 1000));
146
153
  issue = await this.issueRepository.getIssueByUrl(issueUrl);
147
- if (!issue) {
148
- continue;
149
- } else if (!issue.itemId) {
154
+ if (!issue || !issue.itemId) {
150
155
  continue;
151
156
  }
157
+ console.log(
158
+ `[HandleScheduledEvent] Issue found: url=${issueUrl} itemId=${issue.itemId}`,
159
+ );
152
160
  break;
153
161
  }
154
162
  if (!issue) {
@@ -161,6 +169,9 @@ export class HandleScheduledEventUseCase {
161
169
  issue,
162
170
  storyObject.story.id,
163
171
  );
172
+ console.log(
173
+ `[HandleScheduledEvent] Waiting for story update: url=${issueUrl}`,
174
+ );
164
175
  await new Promise((resolve) => setTimeout(resolve, 10 * 1000));
165
176
  const newIssue = await this.issueRepository.getIssueByUrl(issueUrl);
166
177
  if (!newIssue) {
@@ -169,6 +180,9 @@ export class HandleScheduledEventUseCase {
169
180
  storyObject.storyIssue = newIssue;
170
181
  issues.push(newIssue);
171
182
  storyObject.issues.push(newIssue);
183
+ console.log(
184
+ `[HandleScheduledEvent] Story issue created: story="${storyObject.story.name}" elapsed=${Date.now() - storyStartTime}ms`,
185
+ );
172
186
  }
173
187
 
174
188
  const targetDateTimes: Date[] =