github-issue-tower-defence-management 1.103.1 → 1.104.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 (64) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +83 -58
  3. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  4. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +8 -1
  5. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  6. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +16 -0
  7. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
  8. package/bin/domain/usecases/HandleScheduledEventUseCase.js +7 -2
  9. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  10. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +4 -5
  11. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
  12. package/bin/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.js +30 -4
  13. package/bin/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.js.map +1 -1
  14. package/bin/domain/usecases/StartPreparationUseCase.js +2 -1
  15. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  16. package/bin/domain/usecases/resolveAllowedIssueAuthors.js +8 -0
  17. package/bin/domain/usecases/resolveAllowedIssueAuthors.js.map +1 -0
  18. package/package.json +1 -1
  19. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +1 -0
  20. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +36 -0
  21. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +23 -1
  22. package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.test.ts +1 -0
  23. package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +1 -0
  24. package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
  25. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +1 -0
  26. package/src/adapter/repositories/GitHubIssueCommentRepository.test.ts +1 -0
  27. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +9 -0
  28. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +10 -0
  29. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +24 -0
  30. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +1 -0
  31. package/src/domain/entities/Issue.ts +1 -0
  32. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +1 -0
  33. package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
  34. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +109 -0
  35. package/src/domain/usecases/HandleScheduledEventUseCase.ts +8 -2
  36. package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +1 -0
  37. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
  38. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +110 -12
  39. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +5 -7
  40. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +1 -0
  41. package/src/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.test.ts +148 -115
  42. package/src/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.ts +35 -10
  43. package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +1 -0
  44. package/src/domain/usecases/StartPreparationUseCase.test.ts +203 -88
  45. package/src/domain/usecases/StartPreparationUseCase.ts +2 -1
  46. package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +1 -0
  47. package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +1 -0
  48. package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
  49. package/src/domain/usecases/resolveAllowedIssueAuthors.test.ts +51 -0
  50. package/src/domain/usecases/resolveAllowedIssueAuthors.ts +6 -0
  51. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  52. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  53. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -0
  54. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
  55. package/types/domain/entities/Issue.d.ts +1 -0
  56. package/types/domain/entities/Issue.d.ts.map +1 -1
  57. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
  58. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  59. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
  60. package/types/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.d.ts +3 -2
  61. package/types/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.d.ts.map +1 -1
  62. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  63. package/types/domain/usecases/resolveAllowedIssueAuthors.d.ts +5 -0
  64. package/types/domain/usecases/resolveAllowedIssueAuthors.d.ts.map +1 -0
@@ -9,17 +9,15 @@ import {
9
9
  DEFAULT_STATUS_NAME,
10
10
  } from '../entities/WorkflowStatus';
11
11
 
12
- const DEPENDENCY_UPDATE_BOT_AUTHORS = ['dependabot[bot]', 'renovate[bot]'];
13
-
14
12
  const isAuthorAuthorizedForAutoStatusCheck = (
15
13
  author: string,
16
- allowedIssueAuthors: string[] | null,
14
+ allowedIssueAuthors: string[] | null | undefined,
17
15
  ): boolean => {
18
- if (DEPENDENCY_UPDATE_BOT_AUTHORS.includes(author)) {
19
- return true;
16
+ if (allowedIssueAuthors === null || allowedIssueAuthors === undefined) {
17
+ return false;
20
18
  }
21
- if (allowedIssueAuthors === null) {
22
- return true;
19
+ if (allowedIssueAuthors.length === 0) {
20
+ return false;
23
21
  }
24
22
  return allowedIssueAuthors.includes(author);
25
23
  };
@@ -32,6 +32,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
32
32
  isClosed: false,
33
33
  createdAt: new Date(),
34
34
  author: '',
35
+ closingIssueReferenceUrls: [],
35
36
  ...overrides,
36
37
  });
37
38
 
@@ -28,6 +28,7 @@ describe('SetDependedIssueUrlForOpenTaskPRsUseCase', () => {
28
28
  isPr: false,
29
29
  isClosed: false,
30
30
  state: 'OPEN',
31
+ closingIssueReferenceUrls: [],
31
32
  };
32
33
  const closedTaskIssue: Issue = {
33
34
  ...mock<Issue>(),
@@ -35,50 +36,28 @@ describe('SetDependedIssueUrlForOpenTaskPRsUseCase', () => {
35
36
  isPr: false,
36
37
  isClosed: true,
37
38
  state: 'CLOSED',
39
+ closingIssueReferenceUrls: [],
38
40
  };
39
- const prItem: Issue = {
41
+
42
+ const openPrClosingIssue1: Issue = {
40
43
  ...mock<Issue>(),
41
- url: 'https://github.com/owner/repo/pull/10',
44
+ url: 'https://github.com/owner/repo/pull/100',
42
45
  isPr: true,
43
46
  isClosed: false,
44
47
  state: 'OPEN',
48
+ closingIssueReferenceUrls: ['https://github.com/owner/repo/issues/1'],
45
49
  };
46
50
 
47
51
  beforeEach(() => {
48
52
  jest.clearAllMocks();
49
53
  });
50
54
 
51
- it('should call setDependedIssueUrl for each related open PR linked to an open task issue', async () => {
52
- mockIssueRepository.findRelatedOpenPRs.mockImplementation(
53
- async (issueUrl) => {
54
- if (issueUrl === openTaskIssue.url) {
55
- return [
56
- {
57
- url: 'https://github.com/owner/repo/pull/100',
58
- branchName: null,
59
- createdAt: new Date(0),
60
- isDraft: false,
61
- isConflicted: false,
62
- isPassedAllCiJob: true,
63
- isCiStateSuccess: true,
64
- isResolvedAllReviewComments: true,
65
- isBranchOutOfDate: false,
66
- missingRequiredCheckNames: [],
67
- },
68
- ];
69
- }
70
- return [];
71
- },
72
- );
73
-
55
+ it('should call setDependedIssueUrl for each open PR whose closing keyword targets an open task issue', async () => {
74
56
  await useCase.run({
75
57
  project: projectWithField,
76
- issues: [openTaskIssue],
58
+ issues: [openTaskIssue, openPrClosingIssue1],
77
59
  });
78
60
 
79
- expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalledWith(
80
- openTaskIssue.url,
81
- );
82
61
  expect(mockIssueRepository.setDependedIssueUrl).toHaveBeenCalledTimes(1);
83
62
  expect(mockIssueRepository.setDependedIssueUrl).toHaveBeenCalledWith(
84
63
  'https://github.com/owner/repo/pull/100',
@@ -87,41 +66,108 @@ describe('SetDependedIssueUrlForOpenTaskPRsUseCase', () => {
87
66
  );
88
67
  });
89
68
 
69
+ it('should never call the per-issue timeline lookup', async () => {
70
+ await useCase.run({
71
+ project: projectWithField,
72
+ issues: [openTaskIssue, openPrClosingIssue1],
73
+ });
74
+
75
+ expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
76
+ });
77
+
90
78
  it('should skip closed task issues so that PRs linked only to a closed task are not touched', async () => {
91
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
79
+ const openPrClosingClosedIssue: Issue = {
80
+ ...mock<Issue>(),
81
+ url: 'https://github.com/owner/repo/pull/101',
82
+ isPr: true,
83
+ isClosed: false,
84
+ state: 'OPEN',
85
+ closingIssueReferenceUrls: ['https://github.com/owner/repo/issues/2'],
86
+ };
92
87
 
93
88
  await useCase.run({
94
89
  project: projectWithField,
95
- issues: [closedTaskIssue],
90
+ issues: [closedTaskIssue, openPrClosingClosedIssue],
96
91
  });
97
92
 
98
- expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
99
93
  expect(mockIssueRepository.setDependedIssueUrl).not.toHaveBeenCalled();
100
94
  });
101
95
 
102
- it('should skip PR project items themselves and only iterate task issues', async () => {
103
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
96
+ it('should ignore closed PRs even when they declare a closing keyword for an open task issue', async () => {
97
+ const closedPrClosingIssue1: Issue = {
98
+ ...mock<Issue>(),
99
+ url: 'https://github.com/owner/repo/pull/102',
100
+ isPr: true,
101
+ isClosed: true,
102
+ state: 'CLOSED',
103
+ closingIssueReferenceUrls: ['https://github.com/owner/repo/issues/1'],
104
+ };
104
105
 
105
106
  await useCase.run({
106
107
  project: projectWithField,
107
- issues: [prItem],
108
+ issues: [openTaskIssue, closedPrClosingIssue1],
108
109
  });
109
110
 
110
- expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
111
111
  expect(mockIssueRepository.setDependedIssueUrl).not.toHaveBeenCalled();
112
112
  });
113
113
 
114
- it('should not call setDependedIssueUrl when no related open PRs are found for an open task issue', async () => {
115
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
114
+ it('should ignore bare mentions because they are absent from the closing-keyword set', async () => {
115
+ const openPrMentioningButNotClosing: Issue = {
116
+ ...mock<Issue>(),
117
+ url: 'https://github.com/owner/repo/pull/103',
118
+ isPr: true,
119
+ isClosed: false,
120
+ state: 'OPEN',
121
+ closingIssueReferenceUrls: [],
122
+ };
116
123
 
117
124
  await useCase.run({
118
125
  project: projectWithField,
119
- issues: [openTaskIssue],
126
+ issues: [openTaskIssue, openPrMentioningButNotClosing],
120
127
  });
121
128
 
122
- expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalledWith(
123
- openTaskIssue.url,
129
+ expect(mockIssueRepository.setDependedIssueUrl).not.toHaveBeenCalled();
130
+ });
131
+
132
+ it('should map a cross-repo closing target by full issue URL', async () => {
133
+ const crossRepoOpenTaskIssue: Issue = {
134
+ ...mock<Issue>(),
135
+ url: 'https://github.com/owner/other-repo/issues/9',
136
+ isPr: false,
137
+ isClosed: false,
138
+ state: 'OPEN',
139
+ closingIssueReferenceUrls: [],
140
+ };
141
+ const openPrClosingCrossRepoIssue: Issue = {
142
+ ...mock<Issue>(),
143
+ url: 'https://github.com/owner/repo/pull/104',
144
+ isPr: true,
145
+ isClosed: false,
146
+ state: 'OPEN',
147
+ closingIssueReferenceUrls: [
148
+ 'https://github.com/owner/other-repo/issues/9',
149
+ ],
150
+ };
151
+
152
+ await useCase.run({
153
+ project: projectWithField,
154
+ issues: [crossRepoOpenTaskIssue, openPrClosingCrossRepoIssue],
155
+ });
156
+
157
+ expect(mockIssueRepository.setDependedIssueUrl).toHaveBeenCalledTimes(1);
158
+ expect(mockIssueRepository.setDependedIssueUrl).toHaveBeenCalledWith(
159
+ 'https://github.com/owner/repo/pull/104',
160
+ projectWithField,
161
+ crossRepoOpenTaskIssue.url,
124
162
  );
163
+ });
164
+
165
+ it('should not call setDependedIssueUrl when no open PR closes an open task issue', async () => {
166
+ await useCase.run({
167
+ project: projectWithField,
168
+ issues: [openTaskIssue],
169
+ });
170
+
125
171
  expect(mockIssueRepository.setDependedIssueUrl).not.toHaveBeenCalled();
126
172
  });
127
173
 
@@ -130,75 +176,72 @@ describe('SetDependedIssueUrlForOpenTaskPRsUseCase', () => {
130
176
 
131
177
  await useCase.run({
132
178
  project: projectWithoutField,
133
- issues: [openTaskIssue],
179
+ issues: [openTaskIssue, openPrClosingIssue1],
134
180
  });
135
181
 
136
- expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
137
182
  expect(mockIssueRepository.setDependedIssueUrl).not.toHaveBeenCalled();
138
183
  expect(warnSpy).toHaveBeenCalled();
139
184
 
140
185
  warnSpy.mockRestore();
141
186
  });
142
187
 
143
- it('should iterate over each open task issue and request its related open PRs independently', async () => {
188
+ it('should set dependencies for multiple open task issues from their respective open PRs', async () => {
144
189
  const secondOpenTaskIssue: Issue = {
145
190
  ...mock<Issue>(),
146
191
  url: 'https://github.com/owner/repo/issues/3',
147
192
  isPr: false,
148
193
  isClosed: false,
149
194
  state: 'OPEN',
195
+ closingIssueReferenceUrls: [],
196
+ };
197
+ const openPrClosingIssue3: Issue = {
198
+ ...mock<Issue>(),
199
+ url: 'https://github.com/owner/repo/pull/200',
200
+ isPr: true,
201
+ isClosed: false,
202
+ state: 'OPEN',
203
+ closingIssueReferenceUrls: ['https://github.com/owner/repo/issues/3'],
150
204
  };
151
-
152
- mockIssueRepository.findRelatedOpenPRs.mockImplementation(
153
- async (issueUrl) => {
154
- if (issueUrl === openTaskIssue.url) {
155
- return [
156
- {
157
- url: 'https://github.com/owner/repo/pull/100',
158
- branchName: null,
159
- createdAt: new Date(0),
160
- isDraft: false,
161
- isConflicted: false,
162
- isPassedAllCiJob: true,
163
- isCiStateSuccess: true,
164
- isResolvedAllReviewComments: true,
165
- isBranchOutOfDate: false,
166
- missingRequiredCheckNames: [],
167
- },
168
- ];
169
- }
170
- if (issueUrl === secondOpenTaskIssue.url) {
171
- return [
172
- {
173
- url: 'https://github.com/owner/repo/pull/200',
174
- branchName: null,
175
- createdAt: new Date(0),
176
- isDraft: false,
177
- isConflicted: false,
178
- isPassedAllCiJob: true,
179
- isCiStateSuccess: true,
180
- isResolvedAllReviewComments: true,
181
- isBranchOutOfDate: false,
182
- missingRequiredCheckNames: [],
183
- },
184
- ];
185
- }
186
- return [];
187
- },
188
- );
189
205
 
190
206
  await useCase.run({
191
207
  project: projectWithField,
192
- issues: [openTaskIssue, closedTaskIssue, prItem, secondOpenTaskIssue],
208
+ issues: [
209
+ openTaskIssue,
210
+ closedTaskIssue,
211
+ openPrClosingIssue1,
212
+ secondOpenTaskIssue,
213
+ openPrClosingIssue3,
214
+ ],
193
215
  });
194
216
 
195
- expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalledTimes(2);
196
- expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalledWith(
217
+ expect(mockIssueRepository.setDependedIssueUrl).toHaveBeenCalledTimes(2);
218
+ expect(mockIssueRepository.setDependedIssueUrl).toHaveBeenCalledWith(
219
+ 'https://github.com/owner/repo/pull/100',
220
+ projectWithField,
197
221
  openTaskIssue.url,
198
222
  );
199
- expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalledWith(
223
+ expect(mockIssueRepository.setDependedIssueUrl).toHaveBeenCalledWith(
224
+ 'https://github.com/owner/repo/pull/200',
225
+ projectWithField,
200
226
  secondOpenTaskIssue.url,
201
227
  );
228
+ });
229
+
230
+ it('should set dependencies for every open PR that closes the same open task issue', async () => {
231
+ const secondOpenPrClosingIssue1: Issue = {
232
+ ...mock<Issue>(),
233
+ url: 'https://github.com/owner/repo/pull/105',
234
+ isPr: true,
235
+ isClosed: false,
236
+ state: 'OPEN',
237
+ closingIssueReferenceUrls: ['https://github.com/owner/repo/issues/1'],
238
+ };
239
+
240
+ await useCase.run({
241
+ project: projectWithField,
242
+ issues: [openTaskIssue, openPrClosingIssue1, secondOpenPrClosingIssue1],
243
+ });
244
+
202
245
  expect(mockIssueRepository.setDependedIssueUrl).toHaveBeenCalledTimes(2);
203
246
  expect(mockIssueRepository.setDependedIssueUrl).toHaveBeenCalledWith(
204
247
  'https://github.com/owner/repo/pull/100',
@@ -206,9 +249,9 @@ describe('SetDependedIssueUrlForOpenTaskPRsUseCase', () => {
206
249
  openTaskIssue.url,
207
250
  );
208
251
  expect(mockIssueRepository.setDependedIssueUrl).toHaveBeenCalledWith(
209
- 'https://github.com/owner/repo/pull/200',
252
+ 'https://github.com/owner/repo/pull/105',
210
253
  projectWithField,
211
- secondOpenTaskIssue.url,
254
+ openTaskIssue.url,
212
255
  );
213
256
  });
214
257
 
@@ -216,33 +259,23 @@ describe('SetDependedIssueUrlForOpenTaskPRsUseCase', () => {
216
259
  const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
217
260
  const failingPrUrl = 'https://github.com/owner/repo/pull/100';
218
261
  const succeedingPrUrl = 'https://github.com/owner/repo/pull/200';
262
+ const failingPr: Issue = {
263
+ ...mock<Issue>(),
264
+ url: failingPrUrl,
265
+ isPr: true,
266
+ isClosed: false,
267
+ state: 'OPEN',
268
+ closingIssueReferenceUrls: ['https://github.com/owner/repo/issues/1'],
269
+ };
270
+ const succeedingPr: Issue = {
271
+ ...mock<Issue>(),
272
+ url: succeedingPrUrl,
273
+ isPr: true,
274
+ isClosed: false,
275
+ state: 'OPEN',
276
+ closingIssueReferenceUrls: ['https://github.com/owner/repo/issues/1'],
277
+ };
219
278
 
220
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
221
- {
222
- url: failingPrUrl,
223
- branchName: null,
224
- createdAt: new Date(0),
225
- isDraft: false,
226
- isConflicted: false,
227
- isPassedAllCiJob: true,
228
- isCiStateSuccess: true,
229
- isResolvedAllReviewComments: true,
230
- isBranchOutOfDate: false,
231
- missingRequiredCheckNames: [],
232
- },
233
- {
234
- url: succeedingPrUrl,
235
- branchName: null,
236
- createdAt: new Date(0),
237
- isDraft: false,
238
- isConflicted: false,
239
- isPassedAllCiJob: true,
240
- isCiStateSuccess: true,
241
- isResolvedAllReviewComments: true,
242
- isBranchOutOfDate: false,
243
- missingRequiredCheckNames: [],
244
- },
245
- ]);
246
279
  mockIssueRepository.setDependedIssueUrl.mockImplementation(
247
280
  async (prUrl) => {
248
281
  if (prUrl === failingPrUrl) {
@@ -254,7 +287,7 @@ describe('SetDependedIssueUrlForOpenTaskPRsUseCase', () => {
254
287
  await expect(
255
288
  useCase.run({
256
289
  project: projectWithField,
257
- issues: [openTaskIssue],
290
+ issues: [openTaskIssue, failingPr, succeedingPr],
258
291
  }),
259
292
  ).resolves.toBeUndefined();
260
293
 
@@ -4,10 +4,7 @@ import { IssueRepository } from './adapter-interfaces/IssueRepository';
4
4
 
5
5
  export class SetDependedIssueUrlForOpenTaskPRsUseCase {
6
6
  constructor(
7
- readonly issueRepository: Pick<
8
- IssueRepository,
9
- 'findRelatedOpenPRs' | 'setDependedIssueUrl'
10
- >,
7
+ readonly issueRepository: Pick<IssueRepository, 'setDependedIssueUrl'>,
11
8
  ) {}
12
9
 
13
10
  run = async (input: { project: Project; issues: Issue[] }): Promise<void> => {
@@ -17,26 +14,54 @@ export class SetDependedIssueUrlForOpenTaskPRsUseCase {
17
14
  );
18
15
  return;
19
16
  }
17
+ const openPrUrlsByClosedIssueUrl = this.buildOpenPrUrlsByClosedIssueUrl(
18
+ input.issues,
19
+ );
20
20
  for (const issue of input.issues) {
21
21
  if (issue.isPr || issue.isClosed) {
22
22
  continue;
23
23
  }
24
- const relatedOpenPRs = await this.issueRepository.findRelatedOpenPRs(
25
- issue.url,
26
- );
27
- for (const pr of relatedOpenPRs) {
24
+ const relatedOpenPrUrls = openPrUrlsByClosedIssueUrl.get(issue.url);
25
+ if (!relatedOpenPrUrls) {
26
+ continue;
27
+ }
28
+ for (const prUrl of relatedOpenPrUrls) {
28
29
  try {
29
30
  await this.issueRepository.setDependedIssueUrl(
30
- pr.url,
31
+ prUrl,
31
32
  input.project,
32
33
  issue.url,
33
34
  );
34
35
  } catch (error) {
35
36
  console.warn(
36
- `Failed to set depended issue URL for PR ${pr.url}, skipping and continuing with remaining PRs: ${error instanceof Error ? error.message : String(error)}`,
37
+ `Failed to set depended issue URL for PR ${prUrl}, skipping and continuing with remaining PRs: ${error instanceof Error ? error.message : String(error)}`,
37
38
  );
38
39
  }
39
40
  }
40
41
  }
41
42
  };
43
+
44
+ private buildOpenPrUrlsByClosedIssueUrl = (
45
+ issues: Issue[],
46
+ ): Map<string, string[]> => {
47
+ const openPrUrlsByClosedIssueUrl = new Map<string, Set<string>>();
48
+ for (const issue of issues) {
49
+ if (!issue.isPr || issue.isClosed) {
50
+ continue;
51
+ }
52
+ for (const closedIssueUrl of issue.closingIssueReferenceUrls) {
53
+ const existing = openPrUrlsByClosedIssueUrl.get(closedIssueUrl);
54
+ if (existing) {
55
+ existing.add(issue.url);
56
+ } else {
57
+ openPrUrlsByClosedIssueUrl.set(closedIssueUrl, new Set([issue.url]));
58
+ }
59
+ }
60
+ }
61
+ const result = new Map<string, string[]>();
62
+ for (const [closedIssueUrl, prUrls] of openPrUrlsByClosedIssueUrl) {
63
+ result.set(closedIssueUrl, Array.from(prUrls));
64
+ }
65
+ return result;
66
+ };
42
67
  }
@@ -72,6 +72,7 @@ const buildIssue = (overrides: Partial<Issue>): Issue => ({
72
72
  isClosed: false,
73
73
  createdAt: new Date('2024-01-01'),
74
74
  author: 'user',
75
+ closingIssueReferenceUrls: [],
75
76
  ...overrides,
76
77
  });
77
78