Skip to content

Commit 8fce1c2

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

2 files changed

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

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

320+
const body = await buildFilterBody(opts);
253321
const limit = Math.min(Math.max(parseInt(opts.limit, 10) || 100, 1), 1000);
254322

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;
323+
if (opts.bulkIgnore) {
324+
if (this.getOptionValueSource("limit") === "cli") {
325+
this.error("--limit cannot be used with --bulk-ignore; the bulk-ignore path always processes all matching issues");
279326
}
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}.`);
327+
await executeBulkIgnore(provider, organization, repository, body, opts.ignoreComment);
297328
return;
298329
}
299330

0 commit comments

Comments
 (0)