github-issue-tower-defence-management 1.148.6 → 1.148.8

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 (30) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js +5 -1
  3. package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js.map +1 -1
  4. package/bin/domain/usecases/DailySecurityScanUseCase.js +65 -41
  5. package/bin/domain/usecases/DailySecurityScanUseCase.js.map +1 -1
  6. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +8 -1
  7. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  8. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +6 -0
  9. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
  10. package/bin/domain/usecases/isPullRequestDeclaredUnnecessary.js +31 -0
  11. package/bin/domain/usecases/isPullRequestDeclaredUnnecessary.js.map +1 -0
  12. package/package.json +1 -1
  13. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +43 -0
  14. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.ts +11 -1
  15. package/src/domain/usecases/DailySecurityScanUseCase.test.ts +299 -237
  16. package/src/domain/usecases/DailySecurityScanUseCase.ts +106 -60
  17. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +79 -0
  18. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +13 -1
  19. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +81 -0
  20. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +13 -1
  21. package/src/domain/usecases/isPullRequestDeclaredUnnecessary.test.ts +114 -0
  22. package/src/domain/usecases/isPullRequestDeclaredUnnecessary.ts +34 -0
  23. package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts.map +1 -1
  24. package/types/domain/usecases/DailySecurityScanUseCase.d.ts +0 -1
  25. package/types/domain/usecases/DailySecurityScanUseCase.d.ts.map +1 -1
  26. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  27. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts +1 -1
  28. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
  29. package/types/domain/usecases/isPullRequestDeclaredUnnecessary.d.ts +5 -0
  30. package/types/domain/usecases/isPullRequestDeclaredUnnecessary.d.ts.map +1 -0
@@ -47,6 +47,78 @@ const isKevCatalog = (value: unknown): value is KevCatalog => {
47
47
  );
48
48
  };
49
49
 
50
+ type ScannedVulnerablePackage = {
51
+ repositoryName: string;
52
+ ecosystem: string;
53
+ packageName: string;
54
+ packageVersion: string;
55
+ vulnerabilityId: string;
56
+ vulnerabilityIdentifiers: string[];
57
+ summary: string;
58
+ };
59
+
60
+ const parseScannerVulnerabilities = (
61
+ repositoryName: string,
62
+ scannerOutput: string,
63
+ ): ScannedVulnerablePackage[] => {
64
+ let parsed: unknown;
65
+ try {
66
+ parsed = JSON.parse(scannerOutput);
67
+ } catch (error) {
68
+ console.error(
69
+ `Unparsable osv-scanner output for ${repositoryName}: ${String(error)}`,
70
+ );
71
+ return [];
72
+ }
73
+ const toRecord = (value: unknown): Record<string, unknown> =>
74
+ typeof value === 'object' && value !== null ? { ...value } : {};
75
+ const readArray = (value: unknown, key: string): unknown[] => {
76
+ const entry = toRecord(value)[key];
77
+ return Array.isArray(entry) ? entry : [];
78
+ };
79
+ const readString = (value: unknown, key: string): string => {
80
+ const entry = toRecord(value)[key];
81
+ return typeof entry === 'string' ? entry : '';
82
+ };
83
+ return readArray(parsed, 'results').flatMap((result) =>
84
+ readArray(result, 'packages').flatMap((scannedPackage) => {
85
+ const packageDetail = toRecord(scannedPackage).package;
86
+ return readArray(scannedPackage, 'vulnerabilities').map(
87
+ (vulnerability) => {
88
+ const vulnerabilityId = readString(vulnerability, 'id');
89
+ const aliases = readArray(vulnerability, 'aliases').filter(
90
+ (alias): alias is string => typeof alias === 'string',
91
+ );
92
+ return {
93
+ repositoryName,
94
+ ecosystem: readString(packageDetail, 'ecosystem'),
95
+ packageName: readString(packageDetail, 'name'),
96
+ packageVersion: readString(packageDetail, 'version'),
97
+ vulnerabilityId,
98
+ vulnerabilityIdentifiers: [vulnerabilityId, ...aliases],
99
+ summary: readString(vulnerability, 'summary'),
100
+ };
101
+ },
102
+ );
103
+ }),
104
+ );
105
+ };
106
+
107
+ const renderScannerFindings = (
108
+ today: string,
109
+ vulnerablePackages: ScannedVulnerablePackage[],
110
+ ): string =>
111
+ [
112
+ '## OSV-Scanner findings',
113
+ '',
114
+ `### ${today}`,
115
+ '',
116
+ ...vulnerablePackages.map(
117
+ (vulnerablePackage) =>
118
+ `- ${vulnerablePackage.ecosystem} ${vulnerablePackage.packageName} ${vulnerablePackage.packageVersion} ${vulnerablePackage.vulnerabilityIdentifiers.join(' ')} ${vulnerablePackage.summary}`,
119
+ ),
120
+ ].join('\n');
121
+
50
122
  const KEV_CATALOG_URL =
51
123
  'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json';
52
124
 
@@ -78,7 +150,7 @@ export class DailySecurityScanUseCase {
78
150
  const lastTargetDate = input.targetDates[input.targetDates.length - 1];
79
151
  const today = lastTargetDate.toISOString().slice(0, 10);
80
152
 
81
- await this.scanRepositories(
153
+ const scannedVulnerablePackages = await this.scanRepositories(
82
154
  input.org,
83
155
  input.manager,
84
156
  today,
@@ -90,6 +162,7 @@ export class DailySecurityScanUseCase {
90
162
  input.manager,
91
163
  lastTargetDate,
92
164
  input.dailySecurityScan,
165
+ scannedVulnerablePackages,
93
166
  );
94
167
  };
95
168
 
@@ -98,7 +171,7 @@ export class DailySecurityScanUseCase {
98
171
  manager: Member['name'],
99
172
  today: string,
100
173
  config: DailySecurityScanConfig,
101
- ): Promise<void> => {
174
+ ): Promise<ScannedVulnerablePackage[]> => {
102
175
  const { stdout: findOutput } = await this.localCommandRunner.runCommand(
103
176
  'find',
104
177
  [
@@ -121,9 +194,10 @@ export class DailySecurityScanUseCase {
121
194
  console.error(
122
195
  `No repositories found in scan base directory: ${config.scanBaseDirectory}`,
123
196
  );
124
- return;
197
+ return [];
125
198
  }
126
199
 
200
+ const scannedVulnerablePackages: ScannedVulnerablePackage[] = [];
127
201
  for (const repositoryDirectory of repositoryDirectories) {
128
202
  const { stdout: remoteUrl, exitCode: remoteExitCode } =
129
203
  await this.localCommandRunner.runCommand('git', [
@@ -155,6 +229,8 @@ export class DailySecurityScanUseCase {
155
229
  'source',
156
230
  '-r',
157
231
  repositoryDirectory,
232
+ '--format',
233
+ 'json',
158
234
  ]);
159
235
  if (scanExitCode === 0) {
160
236
  continue;
@@ -166,7 +242,13 @@ export class DailySecurityScanUseCase {
166
242
  continue;
167
243
  }
168
244
 
169
- const findingsBody = `## OSV-Scanner findings\n\n### ${today}\n\n\`\`\`\n${scanOutput}\n\`\`\``;
245
+ const vulnerablePackages = parseScannerVulnerabilities(
246
+ repositoryName,
247
+ scanOutput,
248
+ );
249
+ scannedVulnerablePackages.push(...vulnerablePackages);
250
+
251
+ const findingsBody = renderScannerFindings(today, vulnerablePackages);
170
252
  const existingIssues = await this.issueRepository.searchIssue({
171
253
  owner: repositoryOrg,
172
254
  repositoryName,
@@ -193,6 +275,7 @@ export class DailySecurityScanUseCase {
193
275
  );
194
276
  }
195
277
  }
278
+ return scannedVulnerablePackages;
196
279
  };
197
280
 
198
281
  private reportKevAdditions = async (
@@ -200,6 +283,7 @@ export class DailySecurityScanUseCase {
200
283
  manager: Member['name'],
201
284
  lastTargetDate: Date,
202
285
  config: DailySecurityScanConfig,
286
+ scannedVulnerablePackages: ScannedVulnerablePackage[],
203
287
  ): Promise<void> => {
204
288
  if (!config.enableKevNvdReport || !config.kevReportRepo) {
205
289
  return;
@@ -220,18 +304,15 @@ export class DailySecurityScanUseCase {
220
304
  const newKevEntries = parsedKev.vulnerabilities.filter(
221
305
  (vulnerability) => vulnerability.dateAdded >= yesterdayYmd,
222
306
  );
223
- const usedKevEntries: KevVulnerability[] = [];
224
- for (const vulnerability of newKevEntries) {
225
- if (
226
- await this.isProductPresentInScannedWorkspace(
227
- config.scanBaseDirectory,
228
- vulnerability.product,
229
- )
230
- ) {
231
- usedKevEntries.push(vulnerability);
232
- }
233
- }
234
- if (usedKevEntries.length === 0) {
307
+ const affectingKevEntries = newKevEntries
308
+ .map((vulnerability) => ({
309
+ vulnerability,
310
+ affectedPackages: scannedVulnerablePackages.filter((scannedPackage) =>
311
+ scannedPackage.vulnerabilityIdentifiers.includes(vulnerability.cveID),
312
+ ),
313
+ }))
314
+ .filter((entry) => entry.affectedPackages.length > 0);
315
+ if (affectingKevEntries.length === 0) {
235
316
  return;
236
317
  }
237
318
 
@@ -239,54 +320,19 @@ export class DailySecurityScanUseCase {
239
320
  org,
240
321
  config.kevReportRepo,
241
322
  `CISA KEV new additions since ${yesterdayYmd}`,
242
- usedKevEntries
243
- .map(
244
- (vulnerability) =>
245
- `- ${vulnerability.dateAdded} ${vulnerability.cveID} ${vulnerability.vulnerabilityName}`,
323
+ affectingKevEntries
324
+ .map((entry) =>
325
+ [
326
+ `- ${entry.vulnerability.dateAdded} ${entry.vulnerability.cveID} ${entry.vulnerability.vulnerabilityName}`,
327
+ ...entry.affectedPackages.map(
328
+ (affectedPackage) =>
329
+ ` - ${affectedPackage.repositoryName} ${affectedPackage.ecosystem} ${affectedPackage.packageName} ${affectedPackage.packageVersion}`,
330
+ ),
331
+ ].join('\n'),
246
332
  )
247
333
  .join('\n'),
248
334
  [manager],
249
335
  [],
250
336
  );
251
337
  };
252
-
253
- private isProductPresentInScannedWorkspace = async (
254
- scanBaseDirectory: string,
255
- product: string,
256
- ): Promise<boolean> => {
257
- const { stdout: findOutput } = await this.localCommandRunner.runCommand(
258
- 'find',
259
- [scanBaseDirectory, '-maxdepth', '3', '-name', '.git', '-type', 'd'],
260
- );
261
- const repositoryDirectories = findOutput
262
- .split('\n')
263
- .filter((line) => line.length > 0)
264
- .map((gitDirectory) => gitDirectory.replace(/\/\.git$/, ''));
265
-
266
- for (const repositoryDirectory of repositoryDirectories) {
267
- const { stderr, exitCode } = await this.localCommandRunner.runCommand(
268
- 'git',
269
- [
270
- '-C',
271
- repositoryDirectory,
272
- 'grep',
273
- '-I',
274
- '-i',
275
- '-q',
276
- '-F',
277
- '-e',
278
- product,
279
- ],
280
- );
281
- if (exitCode === 0) {
282
- return true;
283
- }
284
- if (exitCode !== 1) {
285
- console.error(
286
- `Failed to search ${repositoryDirectory} for ${product}: ${stderr}`,
287
- );
288
- }
289
- }
290
- return false;
291
- };
292
338
  }
@@ -1087,6 +1087,85 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1087
1087
  );
1088
1088
  });
1089
1089
 
1090
+ it('should not reject a missing PR when the last report declares pullRequestRequired as false', async () => {
1091
+ const issue = createMockIssue({
1092
+ url: 'https://github.com/user/repo/issues/1',
1093
+ status: 'Preparation',
1094
+ });
1095
+
1096
+ mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1097
+ mockIssueRepository.get.mockResolvedValue(issue);
1098
+ mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1099
+ createMockComment({
1100
+ content:
1101
+ 'From: :robot: Test report\n```json\n{"pullRequestRequired": false}\n```',
1102
+ }),
1103
+ ]);
1104
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
1105
+
1106
+ await useCase.run({
1107
+ projectUrl: 'https://github.com/users/user/projects/1',
1108
+ issueUrl: 'https://github.com/user/repo/issues/1',
1109
+ thresholdForAutoReject: 3,
1110
+ workflowBlockerResolvedWebhookUrl: null,
1111
+ allowedIssueAuthors: null,
1112
+ });
1113
+
1114
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalledWith(
1115
+ expect.anything(),
1116
+ expect.stringContaining('PULL_REQUEST_NOT_FOUND'),
1117
+ );
1118
+ expect(mockIssueRepository.update).toHaveBeenCalledWith(
1119
+ expect.objectContaining({
1120
+ status: 'Awaiting Quality Check',
1121
+ }),
1122
+ mockProject,
1123
+ );
1124
+ });
1125
+
1126
+ it('should still reject a draft PR when the last report declares pullRequestRequired as false', async () => {
1127
+ const issue = createMockIssue({
1128
+ url: 'https://github.com/user/repo/issues/1',
1129
+ status: 'Preparation',
1130
+ });
1131
+
1132
+ mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1133
+ mockIssueRepository.get.mockResolvedValue(issue);
1134
+ mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1135
+ createMockComment({
1136
+ content:
1137
+ 'From: :robot: Test report\n```json\n{"pullRequestRequired": false}\n```',
1138
+ }),
1139
+ ]);
1140
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1141
+ {
1142
+ url: 'https://github.com/user/repo/pull/1',
1143
+ isDraft: true,
1144
+ isConflicted: false,
1145
+ isPassedAllCiJob: true,
1146
+ isCiStateSuccess: true,
1147
+ isResolvedAllReviewComments: true,
1148
+ isBranchOutOfDate: false,
1149
+ missingRequiredCheckNames: [],
1150
+ },
1151
+ ]);
1152
+
1153
+ await useCase.run({
1154
+ projectUrl: 'https://github.com/users/user/projects/1',
1155
+ issueUrl: 'https://github.com/user/repo/issues/1',
1156
+ thresholdForAutoReject: 3,
1157
+ workflowBlockerResolvedWebhookUrl: null,
1158
+ allowedIssueAuthors: null,
1159
+ });
1160
+
1161
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
1162
+ expect.objectContaining({
1163
+ url: 'https://github.com/user/repo/issues/1',
1164
+ }),
1165
+ expect.stringContaining('PULL_REQUEST_IS_DRAFT'),
1166
+ );
1167
+ });
1168
+
1090
1169
  it('should not reject a missing PR when the issue label is only in labelsNotRequiringPullRequest', async () => {
1091
1170
  const issue = createMockIssue({
1092
1171
  url: 'https://github.com/user/repo/issues/1',
@@ -14,6 +14,7 @@ import {
14
14
  } from './IssueRejectionEvaluator';
15
15
  import { ChangeTargetPullRequestApprover } from './ChangeTargetPullRequestApprover';
16
16
  import { resolveLabelsNotRequiringPullRequest } from './resolveLabelsNotRequiringPullRequest';
17
+ import { isPullRequestDeclaredUnnecessary } from './isPullRequestDeclaredUnnecessary';
17
18
 
18
19
  export class IssueNotFoundError extends Error {
19
20
  constructor(issueUrl: string) {
@@ -318,7 +319,18 @@ export class NotifyFinishedIssuePreparationUseCase {
318
319
  issue,
319
320
  labelsNotRequiringPullRequest,
320
321
  );
321
- return { rejections: [...rejections, ...prRejections], approvedPrUrl };
322
+ const requiredPrRejections = isPullRequestDeclaredUnnecessary(
323
+ comments,
324
+ isTrustedAuthor,
325
+ )
326
+ ? prRejections.filter(
327
+ (rejection) => rejection.type !== 'PULL_REQUEST_NOT_FOUND',
328
+ )
329
+ : prRejections;
330
+ return {
331
+ rejections: [...rejections, ...requiredPrRejections],
332
+ approvedPrUrl,
333
+ };
322
334
  };
323
335
 
324
336
  private reportBodyHasNextStep = (body: string): boolean => {
@@ -166,6 +166,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
166
166
  };
167
167
  let mockIssueCommentRepository: {
168
168
  createComment: jest.Mock;
169
+ getCommentsFromIssue: jest.Mock;
169
170
  };
170
171
  let mockProject: Project;
171
172
  let useCase: RevertNotReadyReviewQueueIssueUseCase;
@@ -197,6 +198,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
197
198
 
198
199
  mockIssueCommentRepository = {
199
200
  createComment: jest.fn().mockResolvedValue(undefined),
201
+ getCommentsFromIssue: jest.fn().mockResolvedValue([]),
200
202
  };
201
203
 
202
204
  useCase = new RevertNotReadyReviewQueueIssueUseCase(
@@ -279,6 +281,85 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
279
281
  );
280
282
  });
281
283
 
284
+ it('should not revert an issue whose last agent report declares pullRequestRequired as false', async () => {
285
+ const issue = createMockIssue({
286
+ status: 'Awaiting Quality Check',
287
+ });
288
+ mockIssueRepository.getAllIssues.mockResolvedValue({
289
+ project: mockProject,
290
+ issues: [issue],
291
+ cacheUsed: false,
292
+ });
293
+ mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
294
+ {
295
+ author: 'owner',
296
+ content:
297
+ 'From: :robot: Agent report\n```json\n{"pullRequestRequired": false}\n```',
298
+ createdAt: new Date(),
299
+ },
300
+ ]);
301
+
302
+ await useCase.run({
303
+ manager: 'manager-user',
304
+ projectUrl: 'https://github.com/users/user/projects/1',
305
+ allowedIssueAuthors: ['owner'],
306
+ });
307
+
308
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
309
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
310
+ });
311
+
312
+ it('should still revert an issue whose linked PR is conflicted even when the last agent report declares pullRequestRequired as false', async () => {
313
+ const issue = createMockIssue({
314
+ status: 'Awaiting Quality Check',
315
+ });
316
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [
317
+ { ...createReadyPr(), isConflicted: true },
318
+ ]);
319
+ mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
320
+ {
321
+ author: 'owner',
322
+ content:
323
+ 'From: :robot: Agent report\n```json\n{"pullRequestRequired": false}\n```',
324
+ createdAt: new Date(),
325
+ },
326
+ ]);
327
+
328
+ await useCase.run({
329
+ manager: 'manager-user',
330
+ projectUrl: 'https://github.com/users/user/projects/1',
331
+ allowedIssueAuthors: ['owner'],
332
+ });
333
+
334
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
335
+ mockProject,
336
+ issue,
337
+ 'awaiting-workspace-id',
338
+ );
339
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
340
+ issue,
341
+ expect.stringContaining('PULL_REQUEST_CONFLICTED'),
342
+ );
343
+ });
344
+
345
+ it('should not read the comments of an issue whose linked PR is ready', async () => {
346
+ const issue = createMockIssue({
347
+ status: 'Awaiting Quality Check',
348
+ });
349
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [createReadyPr()]);
350
+
351
+ await useCase.run({
352
+ manager: 'manager-user',
353
+ projectUrl: 'https://github.com/users/user/projects/1',
354
+ allowedIssueAuthors: ['owner'],
355
+ });
356
+
357
+ expect(
358
+ mockIssueCommentRepository.getCommentsFromIssue,
359
+ ).not.toHaveBeenCalled();
360
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
361
+ });
362
+
282
363
  it('should not revert a story-labeled issue with no linked PR when story is in labelsAsLlmAgentName', async () => {
283
364
  const issue = createMockIssue({
284
365
  status: 'Awaiting Quality Check',
@@ -5,6 +5,7 @@ import { IssueCommentRepository } from './adapter-interfaces/IssueCommentReposit
5
5
  import { IssueRejectionEvaluator } from './IssueRejectionEvaluator';
6
6
  import { ChangeTargetPullRequestApprover } from './ChangeTargetPullRequestApprover';
7
7
  import { resolveLabelsNotRequiringPullRequest } from './resolveLabelsNotRequiringPullRequest';
8
+ import { isPullRequestDeclaredUnnecessary } from './isPullRequestDeclaredUnnecessary';
8
9
  import {
9
10
  AWAITING_QUALITY_CHECK_STATUS_NAME,
10
11
  AWAITING_WORKSPACE_STATUS_NAME,
@@ -59,7 +60,7 @@ export class RevertNotReadyReviewQueueIssueUseCase {
59
60
  >,
60
61
  private readonly issueCommentRepository: Pick<
61
62
  IssueCommentRepository,
62
- 'createComment'
63
+ 'createComment' | 'getCommentsFromIssue'
63
64
  >,
64
65
  ) {
65
66
  this.issueRejectionEvaluator = new IssueRejectionEvaluator(issueRepository);
@@ -130,6 +131,17 @@ export class RevertNotReadyReviewQueueIssueUseCase {
130
131
  relatedOpenPrUrlsByIssueUrl.get(issue.url) ?? null,
131
132
  },
132
133
  );
134
+ if (
135
+ rejections.length === 1 &&
136
+ rejections[0].type === 'PULL_REQUEST_NOT_FOUND' &&
137
+ isPullRequestDeclaredUnnecessary(
138
+ await this.issueCommentRepository.getCommentsFromIssue(issue),
139
+ (author) =>
140
+ isAuthorAuthorizedForAutoStatusCheck(author, allowedIssueAuthors),
141
+ )
142
+ ) {
143
+ continue;
144
+ }
133
145
  if (rejections.length > 0) {
134
146
  if (!issue.assignees.includes(params.manager)) {
135
147
  continue;
@@ -0,0 +1,114 @@
1
+ import { isPullRequestDeclaredUnnecessary } from './isPullRequestDeclaredUnnecessary';
2
+
3
+ const trustEveryAuthor = (): boolean => true;
4
+
5
+ const reportComment = (
6
+ reportJson: string,
7
+ author = 'agent-bot',
8
+ ): { author: string; content: string } => ({
9
+ author,
10
+ content: `From: :robot: agent report\n\`\`\`json\n${reportJson}\n\`\`\``,
11
+ });
12
+
13
+ describe('isPullRequestDeclaredUnnecessary', () => {
14
+ it('returns true when the last report declares pullRequestRequired as false', () => {
15
+ expect(
16
+ isPullRequestDeclaredUnnecessary(
17
+ [reportComment('{"pullRequestRequired": false}')],
18
+ trustEveryAuthor,
19
+ ),
20
+ ).toBe(true);
21
+ });
22
+
23
+ it('returns false when the last report declares pullRequestRequired as true', () => {
24
+ expect(
25
+ isPullRequestDeclaredUnnecessary(
26
+ [reportComment('{"pullRequestRequired": true}')],
27
+ trustEveryAuthor,
28
+ ),
29
+ ).toBe(false);
30
+ });
31
+
32
+ it('returns false when the report carries no pullRequestRequired field', () => {
33
+ expect(
34
+ isPullRequestDeclaredUnnecessary(
35
+ [reportComment('{"nextStep": null}')],
36
+ trustEveryAuthor,
37
+ ),
38
+ ).toBe(false);
39
+ });
40
+
41
+ it('returns false when pullRequestRequired is the string false rather than the boolean', () => {
42
+ expect(
43
+ isPullRequestDeclaredUnnecessary(
44
+ [reportComment('{"pullRequestRequired": "false"}')],
45
+ trustEveryAuthor,
46
+ ),
47
+ ).toBe(false);
48
+ });
49
+
50
+ it('returns false when the author of the last comment is not trusted', () => {
51
+ expect(
52
+ isPullRequestDeclaredUnnecessary(
53
+ [reportComment('{"pullRequestRequired": false}', 'stranger')],
54
+ (author) => author === 'agent-bot',
55
+ ),
56
+ ).toBe(false);
57
+ });
58
+
59
+ it('returns false when the last comment is not an agent report', () => {
60
+ expect(
61
+ isPullRequestDeclaredUnnecessary(
62
+ [
63
+ reportComment('{"pullRequestRequired": false}'),
64
+ {
65
+ author: 'agent-bot',
66
+ content:
67
+ 'A later human comment\n```json\n{"pullRequestRequired": false}\n```',
68
+ },
69
+ ],
70
+ trustEveryAuthor,
71
+ ),
72
+ ).toBe(false);
73
+ });
74
+
75
+ it('reads only the last comment and ignores an earlier declaration', () => {
76
+ expect(
77
+ isPullRequestDeclaredUnnecessary(
78
+ [
79
+ reportComment('{"pullRequestRequired": false}'),
80
+ reportComment('{"pullRequestRequired": true}'),
81
+ ],
82
+ trustEveryAuthor,
83
+ ),
84
+ ).toBe(false);
85
+ });
86
+
87
+ it('returns false when there is no comment at all', () => {
88
+ expect(isPullRequestDeclaredUnnecessary([], trustEveryAuthor)).toBe(false);
89
+ });
90
+
91
+ it('returns false when the JSON block cannot be parsed', () => {
92
+ const consoleWarn = jest
93
+ .spyOn(console, 'warn')
94
+ .mockImplementation(() => {});
95
+
96
+ expect(
97
+ isPullRequestDeclaredUnnecessary(
98
+ [reportComment('{"pullRequestRequired": false')],
99
+ trustEveryAuthor,
100
+ ),
101
+ ).toBe(false);
102
+
103
+ consoleWarn.mockRestore();
104
+ });
105
+
106
+ it('returns false when the report carries no JSON block', () => {
107
+ expect(
108
+ isPullRequestDeclaredUnnecessary(
109
+ [{ author: 'agent-bot', content: 'From: :robot: agent report' }],
110
+ trustEveryAuthor,
111
+ ),
112
+ ).toBe(false);
113
+ });
114
+ });
@@ -0,0 +1,34 @@
1
+ const AGENT_REPORT_PREFIX = 'From: :robot:';
2
+
3
+ export const isPullRequestDeclaredUnnecessary = (
4
+ comments: { author: string; content: string }[],
5
+ isTrustedAuthor: (author: string) => boolean,
6
+ ): boolean => {
7
+ const lastComment = comments[comments.length - 1];
8
+ if (
9
+ !lastComment ||
10
+ !isTrustedAuthor(lastComment.author) ||
11
+ !lastComment.content.startsWith(AGENT_REPORT_PREFIX)
12
+ ) {
13
+ return false;
14
+ }
15
+ const reportMatch = lastComment.content.match(/```json\n([\s\S]*?)\n```/);
16
+ if (!reportMatch || reportMatch.length < 2) {
17
+ return false;
18
+ }
19
+ let reportJson: unknown;
20
+ try {
21
+ reportJson = JSON.parse(reportMatch[1]);
22
+ } catch (error) {
23
+ console.warn(
24
+ 'Invalid JSON in report body while checking pullRequestRequired:',
25
+ error,
26
+ );
27
+ return false;
28
+ }
29
+ if (typeof reportJson !== 'object' || reportJson === null) {
30
+ return false;
31
+ }
32
+ const report: Record<string, unknown> = { ...reportJson };
33
+ return report.pullRequestRequired === false;
34
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"CheckIssueReviewReadinessUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/CheckIssueReviewReadinessUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EAEL,oBAAoB,EACrB,MAAM,2BAA2B,CAAC;AAGnC,KAAK,kBAAkB,GACnB,iBAAiB,GACjB,0BAA0B,GAC1B,sBAAsB,GACtB,oBAAoB,CAAC;AAEzB,MAAM,MAAM,0BAA0B,GAAG;IACvC,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE;QAAE,IAAI,EAAE,kBAAkB,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC5D,CAAC;AAEF,qBAAa,gCAAgC;IAIzC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAQhC,OAAO,CAAC,QAAQ,CAAC,sBAAsB;IAXzC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAA0B;gBAG/C,eAAe,EAAE,IAAI,CACpC,eAAe,EACb,eAAe,GACf,oBAAoB,GACpB,oBAAoB,GACpB,gCAAgC,GAChC,iCAAiC,CACpC,EACgB,sBAAsB,EAAE,IAAI,CAC3C,sBAAsB,EACtB,sBAAsB,CACvB;IAKH,GAAG,GAAU,QAAQ;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QACtC,oBAAoB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QACvC,6BAA6B,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;KACjD,KAAG,OAAO,CAAC,0BAA0B,CAAC,CAoDrC;IAEF,OAAO,CAAC,eAAe,CAIgD;IAEvE,OAAO,CAAC,qBAAqB,CAuB3B;CACH"}
1
+ {"version":3,"file":"CheckIssueReviewReadinessUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/CheckIssueReviewReadinessUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EAEL,oBAAoB,EACrB,MAAM,2BAA2B,CAAC;AAInC,KAAK,kBAAkB,GACnB,iBAAiB,GACjB,0BAA0B,GAC1B,sBAAsB,GACtB,oBAAoB,CAAC;AAEzB,MAAM,MAAM,0BAA0B,GAAG;IACvC,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE;QAAE,IAAI,EAAE,kBAAkB,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC5D,CAAC;AAEF,qBAAa,gCAAgC;IAIzC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAQhC,OAAO,CAAC,QAAQ,CAAC,sBAAsB;IAXzC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAA0B;gBAG/C,eAAe,EAAE,IAAI,CACpC,eAAe,EACb,eAAe,GACf,oBAAoB,GACpB,oBAAoB,GACpB,gCAAgC,GAChC,iCAAiC,CACpC,EACgB,sBAAsB,EAAE,IAAI,CAC3C,sBAAsB,EACtB,sBAAsB,CACvB;IAKH,GAAG,GAAU,QAAQ;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QACtC,oBAAoB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QACvC,6BAA6B,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;KACjD,KAAG,OAAO,CAAC,0BAA0B,CAAC,CA6DrC;IAEF,OAAO,CAAC,eAAe,CAIgD;IAEvE,OAAO,CAAC,qBAAqB,CAuB3B;CACH"}
@@ -21,6 +21,5 @@ export declare class DailySecurityScanUseCase {
21
21
  }) => Promise<void>;
22
22
  private scanRepositories;
23
23
  private reportKevAdditions;
24
- private isProductPresentInScannedWorkspace;
25
24
  }
26
25
  //# sourceMappingURL=DailySecurityScanUseCase.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DailySecurityScanUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/DailySecurityScanUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,MAAM,MAAM,uBAAuB,GAAG;IACpC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AA0CF,qBAAa,wBAAwB;IAEjC,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB;IAC/C,QAAQ,CAAC,eAAe,EAAE,IAAI,CAC5B,eAAe,EACf,gBAAgB,GAAG,aAAa,GAAG,oBAAoB,CACxD;IACD,QAAQ,CAAC,cAAc,EAAE,cAAc;gBAL9B,kBAAkB,EAAE,kBAAkB,EACtC,eAAe,EAAE,IAAI,CAC5B,eAAe,EACf,gBAAgB,GAAG,aAAa,GAAG,oBAAoB,CACxD,EACQ,cAAc,EAAE,cAAc;IAGzC,GAAG,GAAU,OAAO;QAClB,WAAW,EAAE,IAAI,EAAE,CAAC;QACpB,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACxB,iBAAiB,EAAE,uBAAuB,CAAC;KAC5C,KAAG,OAAO,CAAC,IAAI,CAAC,CA0Bf;IAEF,OAAO,CAAC,gBAAgB,CAoGtB;IAEF,OAAO,CAAC,kBAAkB,CAqDxB;IAEF,OAAO,CAAC,kCAAkC,CAsCxC;CACH"}
1
+ {"version":3,"file":"DailySecurityScanUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/DailySecurityScanUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,MAAM,MAAM,uBAAuB,GAAG;IACpC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAkHF,qBAAa,wBAAwB;IAEjC,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB;IAC/C,QAAQ,CAAC,eAAe,EAAE,IAAI,CAC5B,eAAe,EACf,gBAAgB,GAAG,aAAa,GAAG,oBAAoB,CACxD;IACD,QAAQ,CAAC,cAAc,EAAE,cAAc;gBAL9B,kBAAkB,EAAE,kBAAkB,EACtC,eAAe,EAAE,IAAI,CAC5B,eAAe,EACf,gBAAgB,GAAG,aAAa,GAAG,oBAAoB,CACxD,EACQ,cAAc,EAAE,cAAc;IAGzC,GAAG,GAAU,OAAO;QAClB,WAAW,EAAE,IAAI,EAAE,CAAC;QACpB,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACxB,iBAAiB,EAAE,uBAAuB,CAAC;KAC5C,KAAG,OAAO,CAAC,IAAI,CAAC,CA2Bf;IAEF,OAAO,CAAC,gBAAgB,CA8GtB;IAEF,OAAO,CAAC,kBAAkB,CAwDxB;CACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"NotifyFinishedIssuePreparationUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAc3E,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,QAAQ,EAAE,MAAM;CAI7B;AACD,qBAAa,uBAAwB,SAAQ,KAAK;gBAE9C,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,cAAc,EAAE,MAAM,GAAG,IAAI;CAOhC;AAID,qBAAa,qCAAqC;IAK9C,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAahC,OAAO,CAAC,QAAQ,CAAC,sBAAsB;IAIvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAtBpC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAA0B;IAClE,OAAO,CAAC,QAAQ,CAAC,+BAA+B,CAAkC;gBAG/D,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,CAAC,EACtD,eAAe,EAAE,IAAI,CACpC,eAAe,EACb,KAAK,GACL,QAAQ,GACR,cAAc,GACd,oBAAoB,GACpB,mBAAmB,GACnB,oBAAoB,GACpB,gCAAgC,GAChC,oBAAoB,GACpB,iCAAiC,GACjC,qBAAqB,CACxB,EACgB,sBAAsB,EAAE,IAAI,CAC3C,sBAAsB,EACtB,sBAAsB,GAAG,eAAe,CACzC,EACgB,iBAAiB,EAAE,IAAI,CACtC,iBAAiB,EACjB,gBAAgB,CACjB;IAQH,GAAG,GAAU,QAAQ;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,sBAAsB,EAAE,MAAM,CAAC;QAC/B,iCAAiC,EAAE,MAAM,GAAG,IAAI,CAAC;QACjD,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QACtC,oBAAoB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QACvC,6BAA6B,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAChD,uBAAuB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;KACzD,KAAG,OAAO,CAAC,IAAI,CAAC,CAsMf;IAEF,OAAO,CAAC,eAAe,CAIgD;IAEvE,OAAO,CAAC,iBAAiB,CAkCvB;IAEF,OAAO,CAAC,qBAAqB,CAuB3B;IAEF,OAAO,CAAC,gCAAgC,CAoBtC;IAEF,OAAO,CAAC,uBAAuB,CAQ7B;IAEF,OAAO,CAAC,+BAA+B,CAgCrC;CACH"}
1
+ {"version":3,"file":"NotifyFinishedIssuePreparationUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAe3E,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,QAAQ,EAAE,MAAM;CAI7B;AACD,qBAAa,uBAAwB,SAAQ,KAAK;gBAE9C,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,cAAc,EAAE,MAAM,GAAG,IAAI;CAOhC;AAID,qBAAa,qCAAqC;IAK9C,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAahC,OAAO,CAAC,QAAQ,CAAC,sBAAsB;IAIvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAtBpC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAA0B;IAClE,OAAO,CAAC,QAAQ,CAAC,+BAA+B,CAAkC;gBAG/D,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,CAAC,EACtD,eAAe,EAAE,IAAI,CACpC,eAAe,EACb,KAAK,GACL,QAAQ,GACR,cAAc,GACd,oBAAoB,GACpB,mBAAmB,GACnB,oBAAoB,GACpB,gCAAgC,GAChC,oBAAoB,GACpB,iCAAiC,GACjC,qBAAqB,CACxB,EACgB,sBAAsB,EAAE,IAAI,CAC3C,sBAAsB,EACtB,sBAAsB,GAAG,eAAe,CACzC,EACgB,iBAAiB,EAAE,IAAI,CACtC,iBAAiB,EACjB,gBAAgB,CACjB;IAQH,GAAG,GAAU,QAAQ;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,sBAAsB,EAAE,MAAM,CAAC;QAC/B,iCAAiC,EAAE,MAAM,GAAG,IAAI,CAAC;QACjD,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QACtC,oBAAoB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QACvC,6BAA6B,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAChD,uBAAuB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;KACzD,KAAG,OAAO,CAAC,IAAI,CAAC,CAsMf;IAEF,OAAO,CAAC,eAAe,CAIgD;IAEvE,OAAO,CAAC,iBAAiB,CA6CvB;IAEF,OAAO,CAAC,qBAAqB,CAuB3B;IAEF,OAAO,CAAC,gCAAgC,CAoBtC;IAEF,OAAO,CAAC,uBAAuB,CAQ7B;IAEF,OAAO,CAAC,+BAA+B,CAgCrC;CACH"}