github-issue-tower-defence-management 1.103.1 → 1.104.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.
- package/CHANGELOG.md +7 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +8 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +16 -0
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/bin/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.js +30 -4
- package/bin/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +1 -0
- package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.test.ts +1 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
- package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +1 -0
- package/src/adapter/repositories/GitHubIssueCommentRepository.test.ts +1 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +9 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +10 -0
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +24 -0
- package/src/adapter/repositories/issue/RestIssueRepository.test.ts +1 -0
- package/src/domain/entities/Issue.ts +1 -0
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +1 -0
- package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
- package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +1 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +1 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.test.ts +148 -115
- package/src/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.ts +35 -10
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +1 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +1 -0
- package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +1 -0
- package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -0
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
- package/types/domain/entities/Issue.d.ts +1 -0
- package/types/domain/entities/Issue.d.ts.map +1 -1
- package/types/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.d.ts +3 -2
- package/types/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.d.ts.map +1 -1
|
@@ -14,6 +14,7 @@ export type ProjectItem = {
|
|
|
14
14
|
assignees: string[];
|
|
15
15
|
createdAt: string;
|
|
16
16
|
author: string;
|
|
17
|
+
closingIssueReferenceUrls: string[];
|
|
17
18
|
customFields: {
|
|
18
19
|
name: string;
|
|
19
20
|
value: string | null;
|
|
@@ -214,6 +215,11 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!) {
|
|
|
214
215
|
repository {
|
|
215
216
|
nameWithOwner
|
|
216
217
|
}
|
|
218
|
+
closingIssuesReferences(first: 50) {
|
|
219
|
+
nodes {
|
|
220
|
+
url
|
|
221
|
+
}
|
|
222
|
+
}
|
|
217
223
|
}
|
|
218
224
|
}
|
|
219
225
|
}
|
|
@@ -257,6 +263,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!) {
|
|
|
257
263
|
author: { login: string } | null;
|
|
258
264
|
labels: { nodes: { name: string }[] };
|
|
259
265
|
assignees: { nodes: { login: string }[] };
|
|
266
|
+
closingIssuesReferences?: { nodes: { url: string }[] };
|
|
260
267
|
};
|
|
261
268
|
}[];
|
|
262
269
|
};
|
|
@@ -309,6 +316,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!) {
|
|
|
309
316
|
author: { login: string } | null;
|
|
310
317
|
labels: { nodes: { name: string }[] };
|
|
311
318
|
assignees: { nodes: { login: string }[] };
|
|
319
|
+
closingIssuesReferences?: { nodes: { url: string }[] };
|
|
312
320
|
};
|
|
313
321
|
}[];
|
|
314
322
|
};
|
|
@@ -364,6 +372,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!) {
|
|
|
364
372
|
author: { login: string } | null;
|
|
365
373
|
labels: { nodes: { name: string }[] };
|
|
366
374
|
assignees: { nodes: { login: string }[] };
|
|
375
|
+
closingIssuesReferences?: { nodes: { url: string }[] };
|
|
367
376
|
};
|
|
368
377
|
}[];
|
|
369
378
|
};
|
|
@@ -435,6 +444,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!) {
|
|
|
435
444
|
author: { login: string } | null;
|
|
436
445
|
labels: { nodes: { name: string }[] };
|
|
437
446
|
assignees: { nodes: { login: string }[] };
|
|
447
|
+
closingIssuesReferences?: { nodes: { url: string }[] };
|
|
438
448
|
};
|
|
439
449
|
}[] = pageNodes;
|
|
440
450
|
projectItems.forEach((item) => {
|
|
@@ -453,6 +463,10 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!) {
|
|
|
453
463
|
assignees: item.content.assignees?.nodes?.map((a) => a.login) || [],
|
|
454
464
|
createdAt: item.content.createdAt || new Date().toISOString(),
|
|
455
465
|
author: item.content.author?.login || '',
|
|
466
|
+
closingIssueReferenceUrls:
|
|
467
|
+
item.content.closingIssuesReferences?.nodes
|
|
468
|
+
?.map((node) => node.url)
|
|
469
|
+
.filter((url) => url.length > 0) || [],
|
|
456
470
|
customFields: item.fieldValues.nodes
|
|
457
471
|
.filter((field) => !!field.field)
|
|
458
472
|
.map((field) => {
|
|
@@ -768,6 +782,11 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
768
782
|
repository {
|
|
769
783
|
nameWithOwner
|
|
770
784
|
}
|
|
785
|
+
closingIssuesReferences(first: 50) {
|
|
786
|
+
nodes {
|
|
787
|
+
url
|
|
788
|
+
}
|
|
789
|
+
}
|
|
771
790
|
projectItems(first: 10) {
|
|
772
791
|
nodes {
|
|
773
792
|
id
|
|
@@ -843,6 +862,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
843
862
|
labels: { nodes: { name: string }[] };
|
|
844
863
|
assignees: { nodes: { login: string }[] };
|
|
845
864
|
repository: { nameWithOwner: string };
|
|
865
|
+
closingIssuesReferences?: { nodes: { url: string }[] };
|
|
846
866
|
projectItems: {
|
|
847
867
|
nodes: {
|
|
848
868
|
id: string;
|
|
@@ -914,6 +934,10 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
914
934
|
assignees: content.assignees?.nodes?.map((a) => a.login) || [],
|
|
915
935
|
createdAt: content.createdAt || new Date().toISOString(),
|
|
916
936
|
author: content.author?.login || '',
|
|
937
|
+
closingIssueReferenceUrls:
|
|
938
|
+
content.closingIssuesReferences?.nodes
|
|
939
|
+
?.map((node) => node.url)
|
|
940
|
+
.filter((url) => url.length > 0) || [],
|
|
917
941
|
customFields: item.fieldValues.nodes
|
|
918
942
|
.filter((field) => !!field.field)
|
|
919
943
|
.map((field) => {
|
|
@@ -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
|
-
|
|
41
|
+
|
|
42
|
+
const openPrClosingIssue1: Issue = {
|
|
40
43
|
...mock<Issue>(),
|
|
41
|
-
url: 'https://github.com/owner/repo/pull/
|
|
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
|
|
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
|
-
|
|
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
|
|
103
|
-
|
|
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: [
|
|
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
|
|
115
|
-
|
|
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.
|
|
123
|
-
|
|
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
|
|
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: [
|
|
208
|
+
issues: [
|
|
209
|
+
openTaskIssue,
|
|
210
|
+
closedTaskIssue,
|
|
211
|
+
openPrClosingIssue1,
|
|
212
|
+
secondOpenTaskIssue,
|
|
213
|
+
openPrClosingIssue3,
|
|
214
|
+
],
|
|
193
215
|
});
|
|
194
216
|
|
|
195
|
-
expect(mockIssueRepository.
|
|
196
|
-
expect(mockIssueRepository.
|
|
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.
|
|
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/
|
|
252
|
+
'https://github.com/owner/repo/pull/105',
|
|
210
253
|
projectWithField,
|
|
211
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
}
|