github-issue-tower-defence-management 1.60.1 → 1.63.1

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 (123) hide show
  1. package/.github/workflows/publish.yml +13 -0
  2. package/.github/workflows/test.yml +0 -4
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +53 -10
  5. package/bin/adapter/entry-points/cli/index.js +11 -11
  6. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js +3 -22
  8. package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +8 -22
  10. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  11. package/bin/adapter/entry-points/handlers/rotationOrderFileWriter.js +56 -0
  12. package/bin/adapter/entry-points/handlers/rotationOrderFileWriter.js.map +1 -0
  13. package/bin/adapter/entry-points/handlers/situationFileWriter.js +5 -0
  14. package/bin/adapter/entry-points/handlers/situationFileWriter.js.map +1 -1
  15. package/bin/adapter/proxy/TokenListLoader.js +21 -6
  16. package/bin/adapter/proxy/TokenListLoader.js.map +1 -1
  17. package/bin/adapter/proxy/proxyEntry.js +1 -0
  18. package/bin/adapter/proxy/proxyEntry.js.map +1 -1
  19. package/bin/adapter/repositories/BaseGitHubRepository.js +1 -113
  20. package/bin/adapter/repositories/BaseGitHubRepository.js.map +1 -1
  21. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +5 -3
  22. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -1
  23. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +8 -7
  24. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  25. package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js +19 -9
  26. package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js.map +1 -1
  27. package/bin/domain/usecases/HandleScheduledEventUseCase.js +15 -3
  28. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  29. package/bin/domain/usecases/IssueRejectionEvaluator.js +8 -1
  30. package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
  31. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +5 -1
  32. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  33. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +1 -1
  34. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  35. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js +32 -1
  36. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js.map +1 -1
  37. package/bin/domain/usecases/StartPreparationUseCase.js +91 -12
  38. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  39. package/package.json +1 -4
  40. package/src/adapter/entry-points/cli/index.test.ts +16 -16
  41. package/src/adapter/entry-points/cli/index.ts +8 -11
  42. package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.test.ts +2 -55
  43. package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.ts +1 -11
  44. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +6 -56
  45. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +7 -11
  46. package/src/adapter/entry-points/handlers/rotationOrderFileWriter.test.ts +177 -0
  47. package/src/adapter/entry-points/handlers/rotationOrderFileWriter.ts +20 -0
  48. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +36 -0
  49. package/src/adapter/entry-points/handlers/situationFileWriter.ts +8 -0
  50. package/src/adapter/proxy/TokenListLoader.test.ts +50 -1
  51. package/src/adapter/proxy/TokenListLoader.ts +25 -5
  52. package/src/adapter/proxy/proxyEntry.test.ts +270 -1
  53. package/src/adapter/proxy/proxyEntry.ts +2 -1
  54. package/src/adapter/repositories/BaseGitHubRepository.test.ts +1 -186
  55. package/src/adapter/repositories/BaseGitHubRepository.ts +1 -139
  56. package/src/adapter/repositories/GraphqlProjectRepository.errorHandling.test.ts +0 -1
  57. package/src/adapter/repositories/GraphqlProjectRepository.fetchProjectId.test.ts +4 -1
  58. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +60 -19
  59. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +6 -4
  60. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +23 -13
  61. package/src/adapter/repositories/issue/ApiV3IssueRepository.test.ts +0 -1
  62. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +0 -8
  63. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +0 -1
  64. package/src/domain/entities/ClaudeTokenUsage.ts +1 -0
  65. package/src/domain/usecases/CreateNewStoryByLabelUseCase.test.ts +196 -11
  66. package/src/domain/usecases/CreateNewStoryByLabelUseCase.ts +32 -15
  67. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +4 -0
  68. package/src/domain/usecases/HandleScheduledEventUseCase.ts +21 -5
  69. package/src/domain/usecases/IssueRejectionEvaluator.test.ts +153 -0
  70. package/src/domain/usecases/IssueRejectionEvaluator.ts +8 -0
  71. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +175 -31
  72. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +7 -1
  73. package/src/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.test.ts +32 -0
  74. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +39 -5
  75. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +1 -1
  76. package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.test.ts +139 -20
  77. package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.ts +62 -2
  78. package/src/domain/usecases/StartPreparationUseCase.test.ts +404 -21
  79. package/src/domain/usecases/StartPreparationUseCase.ts +152 -16
  80. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +16 -0
  81. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  82. package/types/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.d.ts.map +1 -1
  83. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  84. package/types/adapter/entry-points/handlers/rotationOrderFileWriter.d.ts +3 -0
  85. package/types/adapter/entry-points/handlers/rotationOrderFileWriter.d.ts.map +1 -0
  86. package/types/adapter/entry-points/handlers/situationFileWriter.d.ts +1 -0
  87. package/types/adapter/entry-points/handlers/situationFileWriter.d.ts.map +1 -1
  88. package/types/adapter/proxy/TokenListLoader.d.ts +5 -0
  89. package/types/adapter/proxy/TokenListLoader.d.ts.map +1 -1
  90. package/types/adapter/proxy/proxyEntry.d.ts +2 -1
  91. package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
  92. package/types/adapter/repositories/BaseGitHubRepository.d.ts +1 -23
  93. package/types/adapter/repositories/BaseGitHubRepository.d.ts.map +1 -1
  94. package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts.map +1 -1
  95. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +14 -5
  96. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  97. package/types/domain/entities/ClaudeTokenUsage.d.ts +1 -0
  98. package/types/domain/entities/ClaudeTokenUsage.d.ts.map +1 -1
  99. package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts +4 -2
  100. package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts.map +1 -1
  101. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +5 -2
  102. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  103. package/types/domain/usecases/IssueRejectionEvaluator.d.ts +1 -1
  104. package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
  105. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  106. package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts +5 -2
  107. package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts.map +1 -1
  108. package/types/domain/usecases/StartPreparationUseCase.d.ts +15 -1
  109. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  110. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +14 -0
  111. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  112. package/bin/adapter/repositories/issue/CheerioIssueRepository.js +0 -136
  113. package/bin/adapter/repositories/issue/CheerioIssueRepository.js.map +0 -1
  114. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js +0 -1606
  115. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js.map +0 -1
  116. package/src/adapter/repositories/issue/CheerioIssueRepository.test.ts +0 -6552
  117. package/src/adapter/repositories/issue/CheerioIssueRepository.ts +0 -142
  118. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.test.ts +0 -118
  119. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.ts +0 -584
  120. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts +0 -40
  121. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts.map +0 -1
  122. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts +0 -220
  123. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts.map +0 -1
@@ -83,6 +83,7 @@ const createPassingPr = () => ({
83
83
  url: 'https://github.com/user/repo/pull/5',
84
84
  branchName: 'i1',
85
85
  createdAt: new Date('2024-01-01T00:00:00Z'),
86
+ isDraft: false,
86
87
  isConflicted: false,
87
88
  isPassedAllCiJob: true,
88
89
  isCiStateSuccess: true,
@@ -193,7 +194,7 @@ describe('RevertOrphanedPreparationUseCase', () => {
193
194
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
194
195
  {
195
196
  author: 'bot',
196
- content: 'From: agent report',
197
+ content: 'From: :robot: agent report',
197
198
  createdAt: new Date(),
198
199
  },
199
200
  ]);
@@ -228,7 +229,7 @@ describe('RevertOrphanedPreparationUseCase', () => {
228
229
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
229
230
  {
230
231
  author: 'bot',
231
- content: 'From: agent report',
232
+ content: 'From: :robot: agent report',
232
233
  createdAt: new Date(),
233
234
  },
234
235
  ]);
@@ -266,7 +267,7 @@ describe('RevertOrphanedPreparationUseCase', () => {
266
267
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
267
268
  {
268
269
  author: 'bot',
269
- content: 'From: agent report',
270
+ content: 'From: :robot: agent report',
270
271
  createdAt: new Date(),
271
272
  },
272
273
  ]);
@@ -300,7 +301,7 @@ describe('RevertOrphanedPreparationUseCase', () => {
300
301
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
301
302
  {
302
303
  author: 'bot',
303
- content: 'From: agent report',
304
+ content: 'From: :robot: agent report',
304
305
  createdAt: new Date(),
305
306
  },
306
307
  ]);
@@ -334,7 +335,40 @@ describe('RevertOrphanedPreparationUseCase', () => {
334
335
  {
335
336
  author: 'bot',
336
337
  content:
337
- 'From: agent report\n```json\n{"nextStep": "do something"}\n```',
338
+ 'From: :robot: agent report\n```json\n{"nextStep": "do something"}\n```',
339
+ createdAt: new Date(),
340
+ },
341
+ ]);
342
+
343
+ await useCase.run({
344
+ projectUrl: 'https://github.com/user/repo',
345
+ allowIssueCacheMinutes: 60,
346
+ preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
347
+ });
348
+
349
+ expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
350
+ expect(mockIssueRepository.updateStatus.mock.calls[0][2]).toBe('1');
351
+ });
352
+
353
+ it('should revert orphaned issue to Awaiting Workspace when last comment is a cross-issue notification starting with From: :warning:', async () => {
354
+ const stuckIssue = createMockIssue({
355
+ url: 'https://github.com/user/repo/issues/10',
356
+ status: 'Preparation',
357
+ });
358
+ mockIssueRepository.getAllIssues.mockResolvedValue({
359
+ issues: [stuckIssue],
360
+ cacheUsed: false,
361
+ });
362
+ mockLocalCommandRunner.runCommand.mockResolvedValue({
363
+ stdout: '',
364
+ stderr: '',
365
+ exitCode: 1,
366
+ });
367
+ mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
368
+ {
369
+ author: 'bot',
370
+ content:
371
+ 'From: :warning: This message is from https://github.com/user/repo/tree/i999 AI HS Implement AI Agent (claude-sonnet-4-6)',
338
372
  createdAt: new Date(),
339
373
  },
340
374
  ]);
@@ -102,7 +102,7 @@ export class RevertOrphanedPreparationUseCase {
102
102
  const comments =
103
103
  await this.issueCommentRepository.getCommentsFromIssue(issue);
104
104
  const lastComment = comments[comments.length - 1];
105
- if (!lastComment || !lastComment.content.startsWith('From:')) {
105
+ if (!lastComment || !lastComment.content.startsWith('From: :robot:')) {
106
106
  return true;
107
107
  }
108
108
  if (this.reportBodyHasNextStep(lastComment.content)) {
@@ -353,7 +353,7 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
353
353
  ]);
354
354
  });
355
355
 
356
- it('should throw error when story:* label has no matching regular / ... option', async () => {
356
+ it('should skip issue and create one notification issue when story:* label has no matching regular / ... option', async () => {
357
357
  const issue: Issue = {
358
358
  ...mock<Issue>(),
359
359
  labels: ['story:routine-management'],
@@ -362,24 +362,88 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
362
362
  nextActionDate: null,
363
363
  nextActionHour: null,
364
364
  isPr: false,
365
+ org: 'xcare-medical',
366
+ repo: 'xcare-platform',
367
+ url: 'https://github.com/xcare-medical/xcare-platform/issues/1445',
365
368
  };
369
+ mockIssueRepository.searchIssue.mockResolvedValue([]);
366
370
 
367
- await expect(
368
- useCase.run({
369
- targetDates: [targetDate],
370
- project: basicProject,
371
- issues: [issue],
372
- cacheUsed: false,
373
- }),
374
- ).rejects.toThrow(
375
- 'No matching story found for label: story:routine-management',
371
+ const promise = useCase.run({
372
+ targetDates: [targetDate],
373
+ project: basicProject,
374
+ issues: [issue],
375
+ cacheUsed: false,
376
+ });
377
+ await jest.runAllTimersAsync();
378
+ await promise;
379
+
380
+ expect(mockIssueRepository.updateStory).not.toHaveBeenCalled();
381
+ expect(mockIssueRepository.removeLabel).not.toHaveBeenCalled();
382
+ expect(mockIssueRepository.searchIssue.mock.calls).toEqual([
383
+ [
384
+ {
385
+ owner: 'xcare-medical',
386
+ repositoryName: 'xcare-platform',
387
+ type: 'issue',
388
+ state: 'open',
389
+ title:
390
+ 'TDPM: story label "story:routine-management" has no matching "regular / routine-management" Story option',
391
+ },
392
+ ],
393
+ ]);
394
+ expect(mockIssueRepository.createNewIssue).toHaveBeenCalledTimes(1);
395
+ const createCall = mockIssueRepository.createNewIssue.mock.calls[0];
396
+ expect(createCall[0]).toEqual('xcare-medical');
397
+ expect(createCall[1]).toEqual('xcare-platform');
398
+ expect(createCall[2]).toEqual(
399
+ 'TDPM: story label "story:routine-management" has no matching "regular / routine-management" Story option',
400
+ );
401
+ expect(createCall[3]).toContain(
402
+ 'https://github.com/xcare-medical/xcare-platform/issues/1445',
376
403
  );
404
+ expect(createCall[3]).toContain('story:routine-management');
405
+ expect(createCall[4]).toEqual(['xcare-medical']);
406
+ expect(createCall[5]).toEqual([]);
407
+ });
408
+
409
+ it('should not create a duplicate notification issue when an open one already exists', async () => {
410
+ const issue: Issue = {
411
+ ...mock<Issue>(),
412
+ labels: ['story:routine-management'],
413
+ story: null,
414
+ state: 'OPEN',
415
+ nextActionDate: null,
416
+ nextActionHour: null,
417
+ isPr: false,
418
+ org: 'xcare-medical',
419
+ repo: 'xcare-platform',
420
+ url: 'https://github.com/xcare-medical/xcare-platform/issues/1445',
421
+ };
422
+ mockIssueRepository.searchIssue.mockResolvedValue([
423
+ {
424
+ url: 'https://github.com/xcare-medical/xcare-platform/issues/9999',
425
+ title:
426
+ 'TDPM: story label "story:routine-management" has no matching "regular / routine-management" Story option',
427
+ number: '9999',
428
+ },
429
+ ]);
430
+
431
+ const promise = useCase.run({
432
+ targetDates: [targetDate],
433
+ project: basicProject,
434
+ issues: [issue],
435
+ cacheUsed: false,
436
+ });
437
+ await jest.runAllTimersAsync();
438
+ await promise;
377
439
 
378
440
  expect(mockIssueRepository.updateStory).not.toHaveBeenCalled();
379
441
  expect(mockIssueRepository.removeLabel).not.toHaveBeenCalled();
442
+ expect(mockIssueRepository.searchIssue).toHaveBeenCalledTimes(1);
443
+ expect(mockIssueRepository.createNewIssue).not.toHaveBeenCalled();
380
444
  });
381
445
 
382
- it('should not match story option that does not start with regular / ', async () => {
446
+ it('should skip issue and notify when story option does not start with regular / ', async () => {
383
447
  const issue: Issue = {
384
448
  ...mock<Issue>(),
385
449
  labels: ['story:workflow-board'],
@@ -388,20 +452,75 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
388
452
  nextActionDate: null,
389
453
  nextActionHour: null,
390
454
  isPr: false,
455
+ org: 'xcare-medical',
456
+ repo: 'xcare-platform',
457
+ url: 'https://github.com/xcare-medical/xcare-platform/issues/1500',
391
458
  };
459
+ mockIssueRepository.searchIssue.mockResolvedValue([]);
460
+
461
+ const promise = useCase.run({
462
+ targetDates: [targetDate],
463
+ project: basicProject,
464
+ issues: [issue],
465
+ cacheUsed: false,
466
+ });
467
+ await jest.runAllTimersAsync();
468
+ await promise;
392
469
 
393
- await expect(
394
- useCase.run({
395
- targetDates: [targetDate],
396
- project: basicProject,
397
- issues: [issue],
398
- cacheUsed: false,
399
- }),
400
- ).rejects.toThrow(
401
- 'No matching story found for label: story:workflow-board',
470
+ expect(mockIssueRepository.updateStory).not.toHaveBeenCalled();
471
+ expect(mockIssueRepository.removeLabel).not.toHaveBeenCalled();
472
+ expect(mockIssueRepository.createNewIssue).toHaveBeenCalledTimes(1);
473
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][2]).toEqual(
474
+ 'TDPM: story label "story:workflow-board" has no matching "regular / workflow-board" Story option',
402
475
  );
403
476
  });
404
477
 
478
+ it('should continue processing remaining issues after an unmatched label', async () => {
479
+ const unmatchedIssue: Issue = {
480
+ ...mock<Issue>(),
481
+ labels: ['story:routine-management'],
482
+ story: null,
483
+ state: 'OPEN',
484
+ nextActionDate: null,
485
+ nextActionHour: null,
486
+ isPr: false,
487
+ org: 'xcare-medical',
488
+ repo: 'xcare-platform',
489
+ url: 'https://github.com/xcare-medical/xcare-platform/issues/1445',
490
+ };
491
+ const matchedIssue: Issue = {
492
+ ...mock<Issue>(),
493
+ labels: ['story:high-priority'],
494
+ story: null,
495
+ state: 'OPEN',
496
+ nextActionDate: null,
497
+ nextActionHour: null,
498
+ isPr: false,
499
+ };
500
+ mockIssueRepository.searchIssue.mockResolvedValue([]);
501
+
502
+ const promise = useCase.run({
503
+ targetDates: [targetDate],
504
+ project: basicProject,
505
+ issues: [unmatchedIssue, matchedIssue],
506
+ cacheUsed: false,
507
+ });
508
+ await jest.runAllTimersAsync();
509
+ await promise;
510
+
511
+ expect(mockIssueRepository.createNewIssue).toHaveBeenCalledTimes(1);
512
+ expect(mockIssueRepository.updateStory.mock.calls).toEqual([
513
+ [
514
+ { ...basicProject, story: basicProject.story },
515
+ matchedIssue,
516
+ 'highPriorityId',
517
+ ],
518
+ ]);
519
+ expect(mockIssueRepository.removeLabel.mock.calls).toEqual([
520
+ [matchedIssue, 'story:high-priority'],
521
+ ]);
522
+ });
523
+
405
524
  it('should skip issue that already has a story assigned', async () => {
406
525
  const issue: Issue = {
407
526
  ...mock<Issue>(),
@@ -6,7 +6,7 @@ export class SetWorkflowManagementIssueToStoryUseCase {
6
6
  constructor(
7
7
  readonly issueRepository: Pick<
8
8
  IssueRepository,
9
- 'updateStory' | 'removeLabel'
9
+ 'updateStory' | 'removeLabel' | 'searchIssue' | 'createNewIssue'
10
10
  >,
11
11
  ) {}
12
12
 
@@ -103,7 +103,8 @@ export class SetWorkflowManagementIssueToStoryUseCase {
103
103
  });
104
104
 
105
105
  if (!matchingStory) {
106
- throw new Error(`No matching story found for label: ${storyLabel}`);
106
+ await this.notifyUnmatchedStoryLabel(issue, storyLabel, labelSuffix);
107
+ continue;
107
108
  }
108
109
 
109
110
  await this.issueRepository.updateStory(
@@ -116,6 +117,65 @@ export class SetWorkflowManagementIssueToStoryUseCase {
116
117
  }
117
118
  };
118
119
 
120
+ static buildUnmatchedStoryLabelTitle = (
121
+ storyLabel: string,
122
+ labelSuffix: string,
123
+ ): string =>
124
+ `TDPM: story label "${storyLabel}" has no matching "${SetWorkflowManagementIssueToStoryUseCase.REGULAR_STORY_PREFIX}${labelSuffix}" Story option`;
125
+
126
+ private notifyUnmatchedStoryLabel = async (
127
+ issue: Issue,
128
+ storyLabel: string,
129
+ labelSuffix: string,
130
+ ): Promise<void> => {
131
+ const title =
132
+ SetWorkflowManagementIssueToStoryUseCase.buildUnmatchedStoryLabelTitle(
133
+ storyLabel,
134
+ labelSuffix,
135
+ );
136
+ const existingOpenIssues = await this.issueRepository.searchIssue({
137
+ owner: issue.org,
138
+ repositoryName: issue.repo,
139
+ type: 'issue',
140
+ state: 'open',
141
+ title,
142
+ });
143
+ const alreadyNotified = existingOpenIssues.some(
144
+ (existing) => existing.title === title,
145
+ );
146
+ if (alreadyNotified) {
147
+ return;
148
+ }
149
+ const body = this.buildUnmatchedStoryLabelBody(issue, storyLabel);
150
+ await this.issueRepository.createNewIssue(
151
+ issue.org,
152
+ issue.repo,
153
+ title,
154
+ body,
155
+ [issue.org],
156
+ [],
157
+ );
158
+ };
159
+
160
+ private buildUnmatchedStoryLabelBody = (
161
+ issue: Issue,
162
+ storyLabel: string,
163
+ ): string => {
164
+ const labelSuffix = storyLabel.slice(
165
+ SetWorkflowManagementIssueToStoryUseCase.STORY_LABEL_PREFIX.length,
166
+ );
167
+ return [
168
+ 'From: :robot: SetWorkflowManagementIssueToStoryUseCase',
169
+ '',
170
+ `The issue below carries the label \`${storyLabel}\`, but the project has no matching \`${SetWorkflowManagementIssueToStoryUseCase.REGULAR_STORY_PREFIX}${labelSuffix}\` Story option.`,
171
+ '',
172
+ issue.url,
173
+ '',
174
+ `Because no matching \`${SetWorkflowManagementIssueToStoryUseCase.REGULAR_STORY_PREFIX}${labelSuffix}\` Story option exists, the label cannot be auto-converted to a Story.`,
175
+ 'Add the missing Story option to the project, or correct the label on the issue above, to resolve this.',
176
+ ].join('\n');
177
+ };
178
+
119
179
  private isEligibleIssue = (issue: Issue, targetDates: Date[]): boolean => {
120
180
  const hasStoryOrWorkflowTrigger =
121
181
  issue.labels.some((label) =>