Skip to content

Commit 68739aa

Browse files
committed
feat: Address AI feedback, extract func CF-2412
1 parent 87edb94 commit 68739aa

2 files changed

Lines changed: 129 additions & 77 deletions

File tree

src/commands/issues.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,26 @@ describe("issues command", () => {
873873
});
874874

875875
describe("--bulk-ignore flag", () => {
876+
it("should error when --limit is explicitly combined with --bulk-ignore", async () => {
877+
const mockExit = vi.spyOn(process, "exit").mockImplementation(() => {
878+
throw new Error("process.exit called");
879+
});
880+
vi.spyOn(console, "error").mockImplementation(() => {});
881+
882+
const program = createProgram();
883+
await expect(
884+
program.parseAsync([
885+
"node", "test", "issues", "gh", "test-org", "test-repo",
886+
"--bulk-ignore", "--limit", "10",
887+
]),
888+
).rejects.toThrow("process.exit called");
889+
890+
expect(AnalysisService.bulkIgnoreIssues).not.toHaveBeenCalled();
891+
expect(AnalysisService.searchRepositoryIssues).not.toHaveBeenCalled();
892+
893+
mockExit.mockRestore();
894+
});
895+
876896
it("should fetch all FP issues with onlyPotentialFalsePositives: true and call bulkIgnoreIssues", async () => {
877897
vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({
878898
data: mockIssues,
@@ -969,7 +989,7 @@ describe("issues command", () => {
969989
await program.parseAsync([
970990
"node", "test", "issues", "gh", "test-org", "test-repo",
971991
"--bulk-ignore",
972-
"--comment", "Verified by security team",
992+
"--ignore-comment", "Verified by security team",
973993
]);
974994

975995
expect(AnalysisService.bulkIgnoreIssues).toHaveBeenCalledWith(

src/commands/issues.ts

Lines changed: 108 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,107 @@ function parseCommaList(value: string | undefined): string[] | undefined {
164164
.filter(Boolean);
165165
}
166166

167+
/** Paginate through all tools and return the full list. */
168+
async function fetchAllTools(): Promise<Tool[]> {
169+
const tools: Tool[] = [];
170+
let cursor: string | undefined;
171+
do {
172+
const resp = await ToolsService.listTools(cursor, 100);
173+
tools.push(...resp.data);
174+
cursor = resp.pagination?.cursor;
175+
} while (cursor);
176+
return tools;
177+
}
178+
179+
/**
180+
* Build the SearchRepositoryIssuesBody from parsed CLI options.
181+
* Resolves tool names/UUIDs via the Codacy API when --tools is provided.
182+
*/
183+
async function buildFilterBody(opts: Record<string, any>): Promise<SearchRepositoryIssuesBody> {
184+
const body: SearchRepositoryIssuesBody = {};
185+
186+
if (opts.branch) body.branchName = opts.branch;
187+
188+
const patterns = parseCommaList(opts.patterns);
189+
if (patterns) body.patternIds = patterns;
190+
191+
const severity = parseCommaList(opts.severities);
192+
if (severity) body.levels = severity.map(normalizeSeverity);
193+
194+
const category = parseCommaList(opts.categories);
195+
if (category) body.categories = category.map(normalizeCategory);
196+
197+
const language = parseCommaList(opts.languages);
198+
if (language) body.languages = language;
199+
200+
const tags = parseCommaList(opts.tags);
201+
if (tags) body.tags = tags;
202+
203+
const author = parseCommaList(opts.authors);
204+
if (author) body.authorEmails = author;
205+
206+
// --false-positives and --bulk-ignore both restrict the API query to FP issues only
207+
if (opts.falsePositives || opts.bulkIgnore) body.onlyPotentialFalsePositives = true;
208+
209+
const toolInputs = parseCommaList(opts.tools);
210+
if (toolInputs) body.toolUuids = await resolveToolUuids(toolInputs, fetchAllTools);
211+
212+
return body;
213+
}
214+
215+
/**
216+
* Fetch every false positive issue (all pages) then ignore them in batches of
217+
* BULK_BATCH_SIZE. Prints progress via spinners and exits when done.
218+
*/
219+
async function executeBulkIgnore(
220+
provider: string,
221+
organization: string,
222+
repository: string,
223+
body: SearchRepositoryIssuesBody,
224+
comment: string | undefined,
225+
): Promise<void> {
226+
const fetchSpinner = ora("Fetching false positive issues...").start();
227+
const allIssues: CommitIssue[] = [];
228+
let cursor: string | undefined;
229+
230+
do {
231+
const resp = await AnalysisService.searchRepositoryIssues(
232+
provider,
233+
organization,
234+
repository,
235+
cursor,
236+
100,
237+
body,
238+
);
239+
allIssues.push(...resp.data);
240+
cursor = resp.pagination?.cursor;
241+
} while (cursor);
242+
243+
fetchSpinner.stop();
244+
245+
if (allIssues.length === 0) {
246+
console.log(ansis.green("No false positive issues found."));
247+
return;
248+
}
249+
250+
const count = allIssues.length;
251+
const plural = count === 1 ? "" : "s";
252+
console.log(`Found ${ansis.bold(String(count))} false positive issue${plural}.`);
253+
254+
const ignoreSpinner = ora(`Ignoring ${count} issue${plural}...`).start();
255+
const issueIds = allIssues.map((i) => i.issueId);
256+
257+
for (let i = 0; i < issueIds.length; i += BULK_BATCH_SIZE) {
258+
await AnalysisService.bulkIgnoreIssues(provider, organization, repository, {
259+
issueIds: issueIds.slice(i, i + BULK_BATCH_SIZE),
260+
reason: "FalsePositive",
261+
comment: comment || undefined,
262+
});
263+
}
264+
265+
ignoreSpinner.succeed(`Ignored ${ansis.bold(String(count))} false positive issue${plural}.`);
266+
}
267+
167268
export function registerIssuesCommand(program: Command) {
168269
program
169270
.command("issues")
@@ -190,7 +291,7 @@ export function registerIssuesCommand(program: Command) {
190291
.option("-O, --overview", "show issue count totals instead of the issues list")
191292
.option("-F, --false-positives", "only show issues that are potential false positives")
192293
.option("-I, --bulk-ignore", "ignore all false positive issues matching the current filters")
193-
.option("-m, --comment <comment>", "optional comment when using --bulk-ignore")
294+
.option("-m, --ignore-comment <comment>", "optional comment when using --bulk-ignore")
194295
.addHelpText(
195296
"after",
196297
`
@@ -202,7 +303,7 @@ Examples:
202303
$ codacy issues gh my-org my-repo --limit 500
203304
$ codacy issues gh my-org my-repo --false-positives
204305
$ codacy issues gh my-org my-repo --bulk-ignore --branch main
205-
$ codacy issues gh my-org my-repo --bulk-ignore --patterns security-rule --comment "Confirmed FPs"
306+
$ codacy issues gh my-org my-repo --bulk-ignore --patterns security-rule --ignore-comment "Confirmed FPs"
206307
$ codacy issues gh my-org my-repo --output json`,
207308
)
208309
.action(async function (
@@ -216,84 +317,15 @@ Examples:
216317
const opts = this.opts();
217318
const format = getOutputFormat(this);
218319
const isOverview = !!opts.overview;
219-
const isBulkIgnore = !!opts.bulkIgnore;
220-
221-
// Build the shared filter body from CLI options
222-
const body: SearchRepositoryIssuesBody = {};
223-
if (opts.branch) body.branchName = opts.branch;
224-
const patterns = parseCommaList(opts.patterns);
225-
if (patterns) body.patternIds = patterns;
226-
const severity = parseCommaList(opts.severities);
227-
if (severity) body.levels = severity.map(normalizeSeverity);
228-
const category = parseCommaList(opts.categories);
229-
if (category) body.categories = category.map(normalizeCategory);
230-
const language = parseCommaList(opts.languages);
231-
if (language) body.languages = language;
232-
const tags = parseCommaList(opts.tags);
233-
if (tags) body.tags = tags;
234-
const author = parseCommaList(opts.authors);
235-
if (author) body.authorEmails = author;
236-
// --false-positives and --bulk-ignore both restrict the API query to FP issues only
237-
if (opts.falsePositives || isBulkIgnore) body.onlyPotentialFalsePositives = true;
238-
239-
const toolInputs = parseCommaList(opts.tools);
240-
if (toolInputs) {
241-
body.toolUuids = await resolveToolUuids(toolInputs, async () => {
242-
const tools: Tool[] = [];
243-
let cursor: string | undefined;
244-
do {
245-
const resp = await ToolsService.listTools(cursor, 100);
246-
tools.push(...resp.data);
247-
cursor = resp.pagination?.cursor;
248-
} while (cursor);
249-
return tools;
250-
});
251-
}
252320

321+
const body = await buildFilterBody(opts);
253322
const limit = Math.min(Math.max(parseInt(opts.limit, 10) || 100, 1), 1000);
254323

255-
// --bulk-ignore: fetch all FP issues (all pages) then call bulkIgnoreIssues in batches
256-
if (isBulkIgnore) {
257-
const fetchSpinner = ora("Fetching false positive issues...").start();
258-
const allIssues: CommitIssue[] = [];
259-
let cursor: string | undefined;
260-
261-
do {
262-
const resp = await AnalysisService.searchRepositoryIssues(
263-
provider,
264-
organization,
265-
repository,
266-
cursor,
267-
100,
268-
body,
269-
);
270-
allIssues.push(...resp.data);
271-
cursor = resp.pagination?.cursor;
272-
} while (cursor);
273-
274-
fetchSpinner.stop();
275-
276-
if (allIssues.length === 0) {
277-
console.log(ansis.green("No false positive issues found."));
278-
return;
324+
if (opts.bulkIgnore) {
325+
if (this.getOptionValueSource("limit") === "cli") {
326+
this.error("--limit cannot be used with --bulk-ignore; the bulk-ignore path always processes all matching issues");
279327
}
280-
281-
const count = allIssues.length;
282-
const plural = count === 1 ? "" : "s";
283-
console.log(`Found ${ansis.bold(String(count))} false positive issue${plural}.`);
284-
285-
const ignoreSpinner = ora(`Ignoring ${count} issue${plural}...`).start();
286-
const issueIds = allIssues.map((i) => i.issueId);
287-
288-
for (let i = 0; i < issueIds.length; i += BULK_BATCH_SIZE) {
289-
await AnalysisService.bulkIgnoreIssues(provider, organization, repository, {
290-
issueIds: issueIds.slice(i, i + BULK_BATCH_SIZE),
291-
reason: "FalsePositive",
292-
comment: opts.comment || undefined,
293-
});
294-
}
295-
296-
ignoreSpinner.succeed(`Ignored ${ansis.bold(String(count))} false positive issue${plural}.`);
328+
await executeBulkIgnore(provider, organization, repository, body, opts.ignoreComment);
297329
return;
298330
}
299331

0 commit comments

Comments
 (0)