github-issue-tower-defence-management 1.175.1 → 1.177.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/.github/workflows/umino-project.yml +4 -107
- package/CHANGELOG.md +20 -0
- package/README.md +2 -0
- package/bin/adapter/entry-points/cli/index.js +2 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +6 -0
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-BhDothvr.js → index-D6aAQrf7.js} +2 -2
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -0
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +1 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +6 -0
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/bin/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.js +3 -0
- package/bin/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.js.map +1 -1
- package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js +1 -1
- package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/IssueRejectionEvaluator.js +4 -3
- package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +3 -3
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +2 -1
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
- package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js +23 -10
- package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +4 -0
- package/src/adapter/entry-points/cli/index.ts +2 -0
- package/src/adapter/entry-points/cli/projectConfig.test.ts +58 -0
- package/src/adapter/entry-points/cli/projectConfig.ts +8 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.stories.tsx +36 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/grouping.test.ts +88 -0
- package/src/adapter/entry-points/console/ui/src/features/console/logic/grouping.ts +16 -4
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +13 -3
- package/src/adapter/entry-points/console/ui-dist/assets/{index-BhDothvr.js → index-D6aAQrf7.js} +2 -2
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +2 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +5 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +1 -0
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +12 -5
- package/src/domain/entities/Issue.ts +1 -0
- package/src/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.test.ts +49 -0
- package/src/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.ts +4 -0
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.ts +2 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +2 -0
- package/src/domain/usecases/IssueRejectionEvaluator.test.ts +55 -0
- package/src/domain/usecases/IssueRejectionEvaluator.ts +11 -2
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +31 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +4 -0
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +3 -0
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +138 -0
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.ts +41 -17
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- 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/AssignNoAssigneeIssueToManagerUseCase.d.ts.map +1 -1
- package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts +1 -0
- package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts +2 -1
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +1 -0
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts +1 -0
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
- package/types/domain/usecases/SetupTowerDefenceProjectUseCase.d.ts.map +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>TDPM Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-D6aAQrf7.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-Bh2slMtk.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -297,6 +297,8 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
297
297
|
input.claudeCodeOauthTokenListJsonPath,
|
|
298
298
|
thresholdForAutoReject:
|
|
299
299
|
readmeConfig.thresholdForAutoReject ?? input.thresholdForAutoReject,
|
|
300
|
+
developerAgentName:
|
|
301
|
+
readmeConfig.developerAgentName ?? input.developerAgentName ?? null,
|
|
300
302
|
startPreparation: input.startPreparation
|
|
301
303
|
? {
|
|
302
304
|
...input.startPreparation,
|
|
@@ -72,6 +72,7 @@ const buildProjectItem = (url: string, title: string): ProjectItem => ({
|
|
|
72
72
|
updatedAt: '2026-07-06T00:00:00.000Z',
|
|
73
73
|
author: '',
|
|
74
74
|
closingIssueReferenceUrls: [],
|
|
75
|
+
isRepoArchived: false,
|
|
75
76
|
customFields: [],
|
|
76
77
|
});
|
|
77
78
|
|
|
@@ -116,6 +117,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
116
117
|
closingIssueReferenceUrls: [
|
|
117
118
|
'https://github.com/HiromiShikata/test-repository/issues/7',
|
|
118
119
|
],
|
|
120
|
+
isRepoArchived: false,
|
|
119
121
|
customFields: [
|
|
120
122
|
{ name: 'nextActionDate', value: '2000-01-01' },
|
|
121
123
|
{ name: 'nextActionHour', value: '1' },
|
|
@@ -153,6 +155,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
153
155
|
'https://github.com/HiromiShikata/test-repository/issues/7',
|
|
154
156
|
],
|
|
155
157
|
agent: null,
|
|
158
|
+
isRepoArchived: false,
|
|
156
159
|
},
|
|
157
160
|
},
|
|
158
161
|
{
|
|
@@ -172,6 +175,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
172
175
|
updatedAt: '2024-01-02T00:00:00Z',
|
|
173
176
|
author: '',
|
|
174
177
|
closingIssueReferenceUrls: [],
|
|
178
|
+
isRepoArchived: false,
|
|
175
179
|
customFields: [
|
|
176
180
|
{
|
|
177
181
|
name: 'DependedIssueUrls',
|
|
@@ -210,6 +214,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
210
214
|
author: '',
|
|
211
215
|
closingIssueReferenceUrls: [],
|
|
212
216
|
agent: null,
|
|
217
|
+
isRepoArchived: false,
|
|
213
218
|
},
|
|
214
219
|
},
|
|
215
220
|
];
|
|
@@ -686,6 +686,7 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
686
686
|
author: item.author,
|
|
687
687
|
closingIssueReferenceUrls: item.closingIssueReferenceUrls,
|
|
688
688
|
agent,
|
|
689
|
+
isRepoArchived: item.isRepoArchived,
|
|
689
690
|
};
|
|
690
691
|
};
|
|
691
692
|
private restoreIssuesFromCache = (rawIssues: unknown): Issue[] | null => {
|
|
@@ -17,6 +17,7 @@ export type ProjectItem = {
|
|
|
17
17
|
updatedAt: string;
|
|
18
18
|
author: string;
|
|
19
19
|
closingIssueReferenceUrls: string[];
|
|
20
|
+
isRepoArchived: boolean;
|
|
20
21
|
customFields: {
|
|
21
22
|
name: string;
|
|
22
23
|
value: string | null;
|
|
@@ -38,7 +39,7 @@ type ProjectV2ItemFieldValueNode = {
|
|
|
38
39
|
};
|
|
39
40
|
};
|
|
40
41
|
type ProjectV2ItemContentNode = {
|
|
41
|
-
repository: { nameWithOwner: string };
|
|
42
|
+
repository: { nameWithOwner: string; isArchived: boolean };
|
|
42
43
|
number: number;
|
|
43
44
|
title: string;
|
|
44
45
|
state: string;
|
|
@@ -139,6 +140,7 @@ const PROJECT_V2_ITEM_FIELD_VALUES_AND_CONTENT_SELECTION = `
|
|
|
139
140
|
}
|
|
140
141
|
repository {
|
|
141
142
|
nameWithOwner
|
|
143
|
+
isArchived
|
|
142
144
|
}
|
|
143
145
|
}
|
|
144
146
|
... on PullRequest {
|
|
@@ -163,6 +165,7 @@ const PROJECT_V2_ITEM_FIELD_VALUES_AND_CONTENT_SELECTION = `
|
|
|
163
165
|
}
|
|
164
166
|
repository {
|
|
165
167
|
nameWithOwner
|
|
168
|
+
isArchived
|
|
166
169
|
}
|
|
167
170
|
closingIssuesReferences(first: 50) {
|
|
168
171
|
nodes {
|
|
@@ -393,7 +396,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!, $query: Str
|
|
|
393
396
|
}[];
|
|
394
397
|
};
|
|
395
398
|
content: {
|
|
396
|
-
repository: { nameWithOwner: string };
|
|
399
|
+
repository: { nameWithOwner: string; isArchived: boolean };
|
|
397
400
|
number: number;
|
|
398
401
|
title: string;
|
|
399
402
|
state: string;
|
|
@@ -443,7 +446,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!, $query: Str
|
|
|
443
446
|
}[];
|
|
444
447
|
};
|
|
445
448
|
content: {
|
|
446
|
-
repository: { nameWithOwner: string };
|
|
449
|
+
repository: { nameWithOwner: string; isArchived: boolean };
|
|
447
450
|
number: number;
|
|
448
451
|
title: string;
|
|
449
452
|
state: string;
|
|
@@ -506,7 +509,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!, $query: Str
|
|
|
506
509
|
}[];
|
|
507
510
|
};
|
|
508
511
|
content: {
|
|
509
|
-
repository: { nameWithOwner: string };
|
|
512
|
+
repository: { nameWithOwner: string; isArchived: boolean };
|
|
510
513
|
number: number;
|
|
511
514
|
title: string;
|
|
512
515
|
state: string;
|
|
@@ -630,6 +633,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!, $query: Str
|
|
|
630
633
|
item.content.closingIssuesReferences?.nodes
|
|
631
634
|
?.map((node) => node.url)
|
|
632
635
|
.filter((url) => url.length > 0) || [],
|
|
636
|
+
isRepoArchived: item.content.repository.isArchived ?? false,
|
|
633
637
|
customFields: item.fieldValues.nodes
|
|
634
638
|
.filter((field) => !!field.field)
|
|
635
639
|
.map((field) => {
|
|
@@ -1064,6 +1068,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
1064
1068
|
}
|
|
1065
1069
|
repository {
|
|
1066
1070
|
nameWithOwner
|
|
1071
|
+
isArchived
|
|
1067
1072
|
}
|
|
1068
1073
|
projectItems(first: 10) {
|
|
1069
1074
|
nodes {
|
|
@@ -1142,6 +1147,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
1142
1147
|
}
|
|
1143
1148
|
repository {
|
|
1144
1149
|
nameWithOwner
|
|
1150
|
+
isArchived
|
|
1145
1151
|
}
|
|
1146
1152
|
closingIssuesReferences(first: 50) {
|
|
1147
1153
|
nodes {
|
|
@@ -1223,7 +1229,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
1223
1229
|
author: { login: string } | null;
|
|
1224
1230
|
labels: { nodes: { name: string }[] };
|
|
1225
1231
|
assignees: { nodes: { login: string }[] };
|
|
1226
|
-
repository: { nameWithOwner: string };
|
|
1232
|
+
repository: { nameWithOwner: string; isArchived: boolean };
|
|
1227
1233
|
closingIssuesReferences?: { nodes: { url: string }[] };
|
|
1228
1234
|
projectItems: {
|
|
1229
1235
|
nodes: {
|
|
@@ -1302,6 +1308,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
1302
1308
|
content.closingIssuesReferences?.nodes
|
|
1303
1309
|
?.map((node) => node.url)
|
|
1304
1310
|
.filter((url) => url.length > 0) || [],
|
|
1311
|
+
isRepoArchived: content.repository.isArchived ?? false,
|
|
1305
1312
|
customFields: item.fieldValues.nodes
|
|
1306
1313
|
.filter((field) => !!field.field)
|
|
1307
1314
|
.map((field) => {
|
|
@@ -507,6 +507,55 @@ describe('AssignNoAssigneeIssueToManagerUseCase', () => {
|
|
|
507
507
|
});
|
|
508
508
|
});
|
|
509
509
|
|
|
510
|
+
it('should skip issues in archived repositories', async () => {
|
|
511
|
+
const archivedIssue = {
|
|
512
|
+
...basicIssue,
|
|
513
|
+
isRepoArchived: true,
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
await useCase.run({
|
|
517
|
+
issues: [archivedIssue],
|
|
518
|
+
manager: 'manager1',
|
|
519
|
+
cacheUsed: false,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
expect(mockIssueRepository.updateAssigneeList.mock.calls).toEqual([]);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should assign manager to issues in non-archived repositories', async () => {
|
|
526
|
+
const nonArchivedIssue = {
|
|
527
|
+
...basicIssue,
|
|
528
|
+
isRepoArchived: false,
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
await useCase.run({
|
|
532
|
+
issues: [nonArchivedIssue],
|
|
533
|
+
manager: 'manager1',
|
|
534
|
+
cacheUsed: false,
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
expect(mockIssueRepository.updateAssigneeList.mock.calls).toEqual([
|
|
538
|
+
[nonArchivedIssue, ['manager1']],
|
|
539
|
+
]);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should assign manager to issues where isRepoArchived is undefined', async () => {
|
|
543
|
+
const issueWithoutArchivedFlag = {
|
|
544
|
+
...basicIssue,
|
|
545
|
+
isRepoArchived: undefined,
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
await useCase.run({
|
|
549
|
+
issues: [issueWithoutArchivedFlag],
|
|
550
|
+
manager: 'manager1',
|
|
551
|
+
cacheUsed: false,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
expect(mockIssueRepository.updateAssigneeList.mock.calls).toEqual([
|
|
555
|
+
[issueWithoutArchivedFlag, ['manager1']],
|
|
556
|
+
]);
|
|
557
|
+
});
|
|
558
|
+
|
|
510
559
|
it('should rethrow non-Error thrown values without logging', async () => {
|
|
511
560
|
const failingIssue = {
|
|
512
561
|
...basicIssue,
|
|
@@ -30,7 +30,11 @@ export class AssignNoAssigneeIssueToManagerUseCase {
|
|
|
30
30
|
assignees: Member['name'][];
|
|
31
31
|
state: Issue['state'];
|
|
32
32
|
author: string;
|
|
33
|
+
isRepoArchived?: boolean;
|
|
33
34
|
}): boolean => {
|
|
35
|
+
if (target.isRepoArchived) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
34
38
|
if (target.assignees.length > 0 || target.state !== 'OPEN') {
|
|
35
39
|
return false;
|
|
36
40
|
}
|
|
@@ -44,6 +44,7 @@ export class CheckIssueReviewReadinessUseCase {
|
|
|
44
44
|
allowedIssueAuthors?: string[] | null;
|
|
45
45
|
labelsAsLlmAgentName?: string[] | null;
|
|
46
46
|
labelsNotRequiringPullRequest?: string[] | null;
|
|
47
|
+
developerAgentName?: string | null;
|
|
47
48
|
}): Promise<IssueReviewReadinessResult> => {
|
|
48
49
|
const issue = await this.issueRepository.getIssueByUrl(params.issueUrl);
|
|
49
50
|
|
|
@@ -88,6 +89,7 @@ export class CheckIssueReviewReadinessUseCase {
|
|
|
88
89
|
await this.issueRejectionEvaluator.evaluate(
|
|
89
90
|
issue,
|
|
90
91
|
resolveLabelsNotRequiringPullRequest(params),
|
|
92
|
+
{ developerAgentName: params.developerAgentName },
|
|
91
93
|
);
|
|
92
94
|
|
|
93
95
|
const requiredPrRejections = isPullRequestDeclaredUnnecessary(
|
|
@@ -188,6 +188,7 @@ export class HandleScheduledEventUseCase {
|
|
|
188
188
|
queryToAddProject?: string | null;
|
|
189
189
|
storyProgressCommentEnabled?: boolean;
|
|
190
190
|
dailySecurityScan?: DailySecurityScanConfig | null;
|
|
191
|
+
developerAgentName?: string | null;
|
|
191
192
|
}): Promise<{
|
|
192
193
|
project: Project;
|
|
193
194
|
issues: Issue[];
|
|
@@ -436,6 +437,7 @@ ${JSON.stringify(e)}
|
|
|
436
437
|
labelsNotRequiringPullRequest: input.labelsNotRequiringPullRequest,
|
|
437
438
|
changeTargetPathAliases: input.changeTargetPathAliases,
|
|
438
439
|
allowedIssueAuthors,
|
|
440
|
+
developerAgentName: input.developerAgentName,
|
|
439
441
|
});
|
|
440
442
|
await this.triagerApprovalDispatchUseCase.run({
|
|
441
443
|
projectUrl: input.projectUrl,
|
|
@@ -203,6 +203,61 @@ describe('IssueRejectionEvaluator', () => {
|
|
|
203
203
|
expect(result.approvedPrUrl).toBeNull();
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
+
it('should require PR evaluation for custom developer agent name when it matches issue agent', async () => {
|
|
207
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
|
|
208
|
+
|
|
209
|
+
const result = await evaluator.evaluate(
|
|
210
|
+
{
|
|
211
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
212
|
+
labels: [],
|
|
213
|
+
isPr: false,
|
|
214
|
+
agent: 'my-developer',
|
|
215
|
+
},
|
|
216
|
+
[],
|
|
217
|
+
{ developerAgentName: 'my-developer' },
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalled();
|
|
221
|
+
expect(result.rejections).toHaveLength(1);
|
|
222
|
+
expect(result.rejections[0].type).toBe('PULL_REQUEST_NOT_FOUND');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should not require PR evaluation when issue agent does not match custom developer agent name', async () => {
|
|
226
|
+
const result = await evaluator.evaluate(
|
|
227
|
+
{
|
|
228
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
229
|
+
labels: [],
|
|
230
|
+
isPr: false,
|
|
231
|
+
agent: 'other-agent',
|
|
232
|
+
},
|
|
233
|
+
[],
|
|
234
|
+
{ developerAgentName: 'my-developer' },
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
238
|
+
expect(result.rejections).toHaveLength(0);
|
|
239
|
+
expect(result.approvedPrUrl).toBeNull();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should default to developer when developerAgentName is null', async () => {
|
|
243
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
|
|
244
|
+
|
|
245
|
+
const result = await evaluator.evaluate(
|
|
246
|
+
{
|
|
247
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
248
|
+
labels: [],
|
|
249
|
+
isPr: false,
|
|
250
|
+
agent: 'developer',
|
|
251
|
+
},
|
|
252
|
+
[],
|
|
253
|
+
{ developerAgentName: null },
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalled();
|
|
257
|
+
expect(result.rejections).toHaveLength(1);
|
|
258
|
+
expect(result.rejections[0].type).toBe('PULL_REQUEST_NOT_FOUND');
|
|
259
|
+
});
|
|
260
|
+
|
|
206
261
|
it('should not reject for draft state when issue has non-e2e category label', async () => {
|
|
207
262
|
const result = await evaluator.evaluate({
|
|
208
263
|
url: 'https://github.com/user/repo/issues/1',
|
|
@@ -34,6 +34,9 @@ export type EvaluateOptions = {
|
|
|
34
34
|
// getOpenPullRequest call, which keeps the transient-failure handling that
|
|
35
35
|
// distinguishes an unknown state from an absent pull request.
|
|
36
36
|
resolvedOpenPrByUrl?: ReadonlyMap<string, RelatedPullRequest | null> | null;
|
|
37
|
+
// The agent name treated as the developer agent for PR/CI/conflict checks.
|
|
38
|
+
// When null or undefined, defaults to 'developer'.
|
|
39
|
+
developerAgentName?: string | null;
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
export class IssueRejectionEvaluator {
|
|
@@ -62,7 +65,11 @@ export class IssueRejectionEvaluator {
|
|
|
62
65
|
let approvedPrUrl: string | null = null;
|
|
63
66
|
|
|
64
67
|
if (
|
|
65
|
-
this.requiresPullRequestEvaluation(
|
|
68
|
+
this.requiresPullRequestEvaluation(
|
|
69
|
+
issue,
|
|
70
|
+
labelsNotRequiringPullRequest,
|
|
71
|
+
options.developerAgentName,
|
|
72
|
+
)
|
|
66
73
|
) {
|
|
67
74
|
let prsToCheck: RelatedPullRequest[];
|
|
68
75
|
let anyPrResolutionFailed = false;
|
|
@@ -190,12 +197,14 @@ export class IssueRejectionEvaluator {
|
|
|
190
197
|
requiresPullRequestEvaluation = (
|
|
191
198
|
issue: { labels: string[]; body?: string | null; agent?: string | null },
|
|
192
199
|
labelsNotRequiringPullRequest: string[] = [],
|
|
200
|
+
developerAgentName?: string | null,
|
|
193
201
|
): boolean => {
|
|
194
202
|
const categoryLabels = issue.labels.filter((label) =>
|
|
195
203
|
label.startsWith('category:'),
|
|
196
204
|
);
|
|
205
|
+
const effectiveDeveloperAgentName = developerAgentName ?? 'developer';
|
|
197
206
|
const isNonDeveloperAgent =
|
|
198
|
-
issue.agent != null && issue.agent !==
|
|
207
|
+
issue.agent != null && issue.agent !== effectiveDeveloperAgentName;
|
|
199
208
|
const hasLabelNotRequiringPullRequest = issue.labels.some((label) =>
|
|
200
209
|
labelsNotRequiringPullRequest.includes(label),
|
|
201
210
|
);
|
|
@@ -1703,6 +1703,37 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
1703
1703
|
);
|
|
1704
1704
|
});
|
|
1705
1705
|
|
|
1706
|
+
it('should reject a missing PR when the issue agent matches developerAgentName', async () => {
|
|
1707
|
+
const issue = createMockIssue({
|
|
1708
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1709
|
+
status: 'Preparation',
|
|
1710
|
+
agent: 'my-agent',
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1714
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1715
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1716
|
+
createMockComment({ content: 'From: :robot: Test report' }),
|
|
1717
|
+
]);
|
|
1718
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
|
|
1719
|
+
|
|
1720
|
+
await useCase.run({
|
|
1721
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1722
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1723
|
+
thresholdForAutoReject: 3,
|
|
1724
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1725
|
+
allowedIssueAuthors: null,
|
|
1726
|
+
developerAgentName: 'my-agent',
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
1730
|
+
expect.objectContaining({
|
|
1731
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1732
|
+
}),
|
|
1733
|
+
expect.stringContaining('PULL_REQUEST_NOT_FOUND'),
|
|
1734
|
+
);
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1706
1737
|
it('should reject when multiple PRs are found', async () => {
|
|
1707
1738
|
const issue = createMockIssue({
|
|
1708
1739
|
url: 'https://github.com/user/repo/issues/1',
|
|
@@ -109,6 +109,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
109
109
|
missingAgentName?: string | null;
|
|
110
110
|
sessionErrorLine?: string | null;
|
|
111
111
|
manager?: string | null;
|
|
112
|
+
developerAgentName?: string | null;
|
|
112
113
|
}): Promise<void> => {
|
|
113
114
|
const project = await this.projectRepository.getByUrl(params.projectUrl);
|
|
114
115
|
|
|
@@ -256,6 +257,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
256
257
|
comments,
|
|
257
258
|
isTrustedAuthor,
|
|
258
259
|
resolveLabelsNotRequiringPullRequest(params),
|
|
260
|
+
params.developerAgentName,
|
|
259
261
|
);
|
|
260
262
|
|
|
261
263
|
const rejectionStatusMessage =
|
|
@@ -468,6 +470,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
468
470
|
comments: { author: string; content: string }[],
|
|
469
471
|
isTrustedAuthor: (author: string) => boolean,
|
|
470
472
|
labelsNotRequiringPullRequest: string[],
|
|
473
|
+
developerAgentName?: string | null,
|
|
471
474
|
): Promise<{
|
|
472
475
|
rejections: { type: RejectedReasonType; detail: string }[];
|
|
473
476
|
approvedPrUrl: string | null;
|
|
@@ -495,6 +498,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
495
498
|
await this.issueRejectionEvaluator.evaluate(
|
|
496
499
|
issue,
|
|
497
500
|
labelsNotRequiringPullRequest,
|
|
501
|
+
{ developerAgentName },
|
|
498
502
|
);
|
|
499
503
|
const requiredPrRejections = isPullRequestDeclaredUnnecessary(
|
|
500
504
|
comments,
|
|
@@ -64,6 +64,7 @@ export class RevertNotReadyReviewQueueIssueUseCase {
|
|
|
64
64
|
labelsNotRequiringPullRequest?: string[] | null;
|
|
65
65
|
changeTargetPathAliases?: Record<string, string> | null;
|
|
66
66
|
allowedIssueAuthors?: string[] | null;
|
|
67
|
+
developerAgentName?: string | null;
|
|
67
68
|
}): Promise<void> => {
|
|
68
69
|
const allowedIssueAuthors = params.allowedIssueAuthors ?? null;
|
|
69
70
|
const projectId = await this.projectRepository.findProjectIdByUrl(
|
|
@@ -102,6 +103,7 @@ export class RevertNotReadyReviewQueueIssueUseCase {
|
|
|
102
103
|
this.issueRejectionEvaluator.requiresPullRequestEvaluation(
|
|
103
104
|
item,
|
|
104
105
|
labelsNotRequiringPullRequest,
|
|
106
|
+
params.developerAgentName,
|
|
105
107
|
);
|
|
106
108
|
|
|
107
109
|
const resolvedOpenPrByUrl = await this.issueRepository.getOpenPullRequests(
|
|
@@ -132,6 +134,7 @@ export class RevertNotReadyReviewQueueIssueUseCase {
|
|
|
132
134
|
relatedOpenPrUrls:
|
|
133
135
|
relatedOpenPrUrlsByIssueUrl.get(issue.url) ?? null,
|
|
134
136
|
resolvedOpenPrByUrl,
|
|
137
|
+
developerAgentName: params.developerAgentName,
|
|
135
138
|
},
|
|
136
139
|
);
|
|
137
140
|
if (
|
|
@@ -1013,6 +1013,144 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
1013
1013
|
).toBe(false);
|
|
1014
1014
|
});
|
|
1015
1015
|
|
|
1016
|
+
it('should move open issue with Done status to Awaiting Workspace even when project has canonical statuses', async () => {
|
|
1017
|
+
const mockProjectRepository =
|
|
1018
|
+
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|
|
1019
|
+
const mockIssueRepository =
|
|
1020
|
+
mock<Pick<IssueRepository, 'getAllIssues' | 'updateStatus'>>();
|
|
1021
|
+
const canonicalStatuses = buildCanonicalStatuses();
|
|
1022
|
+
const awaitingWorkspaceId = canonicalStatuses[0].id;
|
|
1023
|
+
const project = buildProject(canonicalStatuses);
|
|
1024
|
+
mockProjectRepository.getByUrl.mockResolvedValue(project);
|
|
1025
|
+
const openDoneIssue = buildIssue({
|
|
1026
|
+
number: 10,
|
|
1027
|
+
url: 'https://github.com/test-org/test-repo/issues/10',
|
|
1028
|
+
itemId: 'item-10',
|
|
1029
|
+
state: 'OPEN',
|
|
1030
|
+
status: DONE_STATUS_NAME,
|
|
1031
|
+
});
|
|
1032
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
1033
|
+
project: mock<Project>(),
|
|
1034
|
+
issues: [openDoneIssue],
|
|
1035
|
+
cacheUsed: false,
|
|
1036
|
+
});
|
|
1037
|
+
mockIssueRepository.updateStatus.mockResolvedValue(undefined);
|
|
1038
|
+
|
|
1039
|
+
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
1040
|
+
mockProjectRepository,
|
|
1041
|
+
mockIssueRepository,
|
|
1042
|
+
);
|
|
1043
|
+
await useCase.run({ projectUrl: project.url });
|
|
1044
|
+
|
|
1045
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledTimes(1);
|
|
1046
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
1047
|
+
project,
|
|
1048
|
+
openDoneIssue,
|
|
1049
|
+
awaitingWorkspaceId,
|
|
1050
|
+
);
|
|
1051
|
+
expect(mockProjectRepository.updateStatusList).not.toHaveBeenCalled();
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it('should move open issue with null status to Awaiting Workspace even when project has canonical statuses', async () => {
|
|
1055
|
+
const mockProjectRepository =
|
|
1056
|
+
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|
|
1057
|
+
const mockIssueRepository =
|
|
1058
|
+
mock<Pick<IssueRepository, 'getAllIssues' | 'updateStatus'>>();
|
|
1059
|
+
const canonicalStatuses = buildCanonicalStatuses();
|
|
1060
|
+
const awaitingWorkspaceId = canonicalStatuses[0].id;
|
|
1061
|
+
const project = buildProject(canonicalStatuses);
|
|
1062
|
+
mockProjectRepository.getByUrl.mockResolvedValue(project);
|
|
1063
|
+
const openNullStatusIssue = buildIssue({
|
|
1064
|
+
number: 11,
|
|
1065
|
+
url: 'https://github.com/test-org/test-repo/issues/11',
|
|
1066
|
+
itemId: 'item-11',
|
|
1067
|
+
state: 'OPEN',
|
|
1068
|
+
status: null,
|
|
1069
|
+
});
|
|
1070
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
1071
|
+
project: mock<Project>(),
|
|
1072
|
+
issues: [openNullStatusIssue],
|
|
1073
|
+
cacheUsed: false,
|
|
1074
|
+
});
|
|
1075
|
+
mockIssueRepository.updateStatus.mockResolvedValue(undefined);
|
|
1076
|
+
|
|
1077
|
+
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
1078
|
+
mockProjectRepository,
|
|
1079
|
+
mockIssueRepository,
|
|
1080
|
+
);
|
|
1081
|
+
await useCase.run({ projectUrl: project.url });
|
|
1082
|
+
|
|
1083
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledTimes(1);
|
|
1084
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
1085
|
+
project,
|
|
1086
|
+
openNullStatusIssue,
|
|
1087
|
+
awaitingWorkspaceId,
|
|
1088
|
+
);
|
|
1089
|
+
expect(mockProjectRepository.updateStatusList).not.toHaveBeenCalled();
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
it('should not move closed issue with Done status to Awaiting Workspace', async () => {
|
|
1093
|
+
const mockProjectRepository =
|
|
1094
|
+
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|
|
1095
|
+
const mockIssueRepository =
|
|
1096
|
+
mock<Pick<IssueRepository, 'getAllIssues' | 'updateStatus'>>();
|
|
1097
|
+
const project = buildProject(buildCanonicalStatuses());
|
|
1098
|
+
mockProjectRepository.getByUrl.mockResolvedValue(project);
|
|
1099
|
+
const closedDoneIssue = buildIssue({
|
|
1100
|
+
number: 12,
|
|
1101
|
+
url: 'https://github.com/test-org/test-repo/issues/12',
|
|
1102
|
+
itemId: 'item-12',
|
|
1103
|
+
state: 'CLOSED',
|
|
1104
|
+
isClosed: true,
|
|
1105
|
+
status: DONE_STATUS_NAME,
|
|
1106
|
+
});
|
|
1107
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
1108
|
+
project: mock<Project>(),
|
|
1109
|
+
issues: [closedDoneIssue],
|
|
1110
|
+
cacheUsed: false,
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
1114
|
+
mockProjectRepository,
|
|
1115
|
+
mockIssueRepository,
|
|
1116
|
+
);
|
|
1117
|
+
await useCase.run({ projectUrl: project.url });
|
|
1118
|
+
|
|
1119
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
1120
|
+
expect(mockProjectRepository.updateStatusList).not.toHaveBeenCalled();
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it('should not move closed issue with null status to Awaiting Workspace', async () => {
|
|
1124
|
+
const mockProjectRepository =
|
|
1125
|
+
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|
|
1126
|
+
const mockIssueRepository =
|
|
1127
|
+
mock<Pick<IssueRepository, 'getAllIssues' | 'updateStatus'>>();
|
|
1128
|
+
const project = buildProject(buildCanonicalStatuses());
|
|
1129
|
+
mockProjectRepository.getByUrl.mockResolvedValue(project);
|
|
1130
|
+
const closedNullStatusIssue = buildIssue({
|
|
1131
|
+
number: 13,
|
|
1132
|
+
url: 'https://github.com/test-org/test-repo/issues/13',
|
|
1133
|
+
itemId: 'item-13',
|
|
1134
|
+
state: 'CLOSED',
|
|
1135
|
+
isClosed: true,
|
|
1136
|
+
status: null,
|
|
1137
|
+
});
|
|
1138
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
1139
|
+
project: mock<Project>(),
|
|
1140
|
+
issues: [closedNullStatusIssue],
|
|
1141
|
+
cacheUsed: false,
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
const useCase = new SetupTowerDefenceProjectUseCase(
|
|
1145
|
+
mockProjectRepository,
|
|
1146
|
+
mockIssueRepository,
|
|
1147
|
+
);
|
|
1148
|
+
await useCase.run({ projectUrl: project.url });
|
|
1149
|
+
|
|
1150
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
1151
|
+
expect(mockProjectRepository.updateStatusList).not.toHaveBeenCalled();
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1016
1154
|
it('should migrate issues from Unread to Awaiting Workspace when both statuses exist in the project', async () => {
|
|
1017
1155
|
const mockProjectRepository =
|
|
1018
1156
|
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|