From 87edb943eeffa5215c991bb5031ab3842ad55524 Mon Sep 17 00:00:00 2001 From: Milosz Jakubanis Date: Tue, 5 May 2026 09:21:05 +0100 Subject: [PATCH 1/2] feat: Add filter and bulk-ignore for FP CF-2412 --- package-lock.json | 9 +- package.json | 2 +- src/commands/issues.test.ts | 198 ++++++++++++++++++++++++++++++++++++ src/commands/issues.ts | 57 +++++++++++ 4 files changed, 259 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index e5d4a0a..1b8b7d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@codacy/codacy-cloud-cli", - "version": "1.0.4", + "version": "1.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@codacy/codacy-cloud-cli", - "version": "1.0.4", + "version": "1.0.5", "license": "ISC", "dependencies": { "@codacy/tooling": "0.1.0", @@ -33,7 +33,7 @@ "vitest": "4.0.18" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -980,7 +980,6 @@ "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1757,7 +1756,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2096,7 +2094,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/package.json b/package.json index ecaf861..7e7a075 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "prepublishOnly": "npm run update-api && npm run build", "start": "npx ts-node src/index.ts", "start:dist": "node dist/index.js", - "fetch-api": "curl https://artifacts.codacy.com/api/codacy-api/52.1.31/apiv3-bundled.yaml -o ./api-v3/api-swagger.yaml --create-dirs", + "fetch-api": "curl https://artifacts.codacy.com/api/codacy-api/55.6.0/apiv3-bundled.yaml -o ./api-v3/api-swagger.yaml --create-dirs", "generate-api": "rm -rf ./src/api/client && openapi --input ./api-v3/api-swagger.yaml --output ./src/api/client --useUnionTypes --indent 2 --client fetch", "update-api": "npm run fetch-api && npm run generate-api", "check-types": "tsc --noEmit" diff --git a/src/commands/issues.test.ts b/src/commands/issues.test.ts index 8d4ca4f..d48483d 100644 --- a/src/commands/issues.test.ts +++ b/src/commands/issues.test.ts @@ -807,4 +807,202 @@ describe("issues command", () => { mockExit.mockRestore(); }); + + describe("--false-positives flag", () => { + it("should pass onlyPotentialFalsePositives: true in the body", async () => { + vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ + data: [], + } as any); + + const program = createProgram(); + await program.parseAsync([ + "node", "test", "issues", "gh", "test-org", "test-repo", + "--false-positives", + ]); + + expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledWith( + "gh", "test-org", "test-repo", undefined, 100, + { onlyPotentialFalsePositives: true }, + ); + }); + + it("should combine onlyPotentialFalsePositives with other filters (--patterns)", async () => { + vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ + data: [], + } as any); + + const program = createProgram(); + await program.parseAsync([ + "node", "test", "issues", "gh", "test-org", "test-repo", + "--false-positives", + "--patterns", "no-undef,sql-injection", + "--branch", "main", + ]); + + expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledWith( + "gh", "test-org", "test-repo", undefined, 100, + { + onlyPotentialFalsePositives: true, + patternIds: ["no-undef", "sql-injection"], + branchName: "main", + }, + ); + }); + + it("should display false positive issues in list format", async () => { + const fpIssue = { + ...mockIssues[0], + falsePositiveProbability: 0.9, + falsePositiveThreshold: 0.5, + falsePositiveReason: "Common safe pattern", + }; + vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ + data: [fpIssue], + } as any); + + const program = createProgram(); + await program.parseAsync([ + "node", "test", "issues", "gh", "test-org", "test-repo", + "--false-positives", + ]); + + const output = getAllOutput(); + expect(output).toContain("Potential SQL injection vulnerability"); + expect(output).toContain("Potential false positive"); + }); + }); + + describe("--bulk-ignore flag", () => { + it("should fetch all FP issues with onlyPotentialFalsePositives: true and call bulkIgnoreIssues", async () => { + vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ + data: mockIssues, + } as any); + vi.mocked(AnalysisService.bulkIgnoreIssues).mockResolvedValue(undefined as any); + + const program = createProgram(); + await program.parseAsync([ + "node", "test", "issues", "gh", "test-org", "test-repo", + "--bulk-ignore", + ]); + + expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledWith( + "gh", "test-org", "test-repo", undefined, 100, + { onlyPotentialFalsePositives: true }, + ); + expect(AnalysisService.bulkIgnoreIssues).toHaveBeenCalledWith( + "gh", "test-org", "test-repo", + { + issueIds: [mockIssues[0].issueId, mockIssues[1].issueId], + reason: "FalsePositive", + comment: undefined, + }, + ); + }); + + it("should show 'No false positive issues found' when API returns empty list", async () => { + vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ + data: [], + } as any); + + const program = createProgram(); + await program.parseAsync([ + "node", "test", "issues", "gh", "test-org", "test-repo", + "--bulk-ignore", + ]); + + expect(AnalysisService.bulkIgnoreIssues).not.toHaveBeenCalled(); + const output = getAllOutput(); + expect(output).toContain("No false positive issues found"); + }); + + it("should batch bulkIgnoreIssues calls when there are more than 100 issues", async () => { + // 150 issues across two pages + const page1 = Array.from({ length: 100 }, (_, i) => ({ + ...mockIssues[0], + issueId: `fp-${i}`, + resultDataId: i, + })); + const page2 = Array.from({ length: 50 }, (_, i) => ({ + ...mockIssues[0], + issueId: `fp-${100 + i}`, + resultDataId: 100 + i, + })); + + vi.mocked(AnalysisService.searchRepositoryIssues) + .mockResolvedValueOnce({ + data: page1, + pagination: { cursor: "cursor-2", limit: 100, total: 150 }, + } as any) + .mockResolvedValueOnce({ + data: page2, + pagination: { cursor: undefined, limit: 100, total: 150 }, + } as any); + vi.mocked(AnalysisService.bulkIgnoreIssues).mockResolvedValue(undefined as any); + + const program = createProgram(); + await program.parseAsync([ + "node", "test", "issues", "gh", "test-org", "test-repo", + "--bulk-ignore", + ]); + + // Should have made 2 search calls (paginated) + expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledTimes(2); + // Should have made 2 bulk-ignore calls: one with 100 IDs, one with 50 IDs + expect(AnalysisService.bulkIgnoreIssues).toHaveBeenCalledTimes(2); + expect(AnalysisService.bulkIgnoreIssues).toHaveBeenNthCalledWith( + 1, "gh", "test-org", "test-repo", + expect.objectContaining({ issueIds: expect.arrayContaining([expect.stringMatching(/^fp-/)]) }), + ); + const firstCallIds: string[] = (AnalysisService.bulkIgnoreIssues as ReturnType).mock.calls[0][3].issueIds; + expect(firstCallIds).toHaveLength(100); + const secondCallIds: string[] = (AnalysisService.bulkIgnoreIssues as ReturnType).mock.calls[1][3].issueIds; + expect(secondCallIds).toHaveLength(50); + }); + + it("should forward --comment to bulkIgnoreIssues", async () => { + vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ + data: [mockIssues[0]], + } as any); + vi.mocked(AnalysisService.bulkIgnoreIssues).mockResolvedValue(undefined as any); + + const program = createProgram(); + await program.parseAsync([ + "node", "test", "issues", "gh", "test-org", "test-repo", + "--bulk-ignore", + "--comment", "Verified by security team", + ]); + + expect(AnalysisService.bulkIgnoreIssues).toHaveBeenCalledWith( + "gh", "test-org", "test-repo", + { + issueIds: [mockIssues[0].issueId], + reason: "FalsePositive", + comment: "Verified by security team", + }, + ); + }); + + it("should combine --bulk-ignore with other filters (--branch, --patterns)", async () => { + vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ + data: [], + } as any); + + const program = createProgram(); + await program.parseAsync([ + "node", "test", "issues", "gh", "test-org", "test-repo", + "--bulk-ignore", + "--branch", "develop", + "--patterns", "sql-injection", + ]); + + expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledWith( + "gh", "test-org", "test-repo", undefined, 100, + { + onlyPotentialFalsePositives: true, + branchName: "develop", + patternIds: ["sql-injection"], + }, + ); + }); + }); }); diff --git a/src/commands/issues.ts b/src/commands/issues.ts index a1fd29d..a2a9aed 100644 --- a/src/commands/issues.ts +++ b/src/commands/issues.ts @@ -20,6 +20,9 @@ import { SearchRepositoryIssuesBody } from "../api/client/models/SearchRepositor import { Count } from "../api/client/models/Count"; import { PatternsCount } from "../api/client/models/PatternsCount"; +// API allows a maximum of 100 issue IDs per bulk-ignore call +const BULK_BATCH_SIZE = 100; + const SEVERITY_ORDER: Record = { Error: 0, High: 1, @@ -185,6 +188,9 @@ export function registerIssuesCommand(program: Command) { .option("-a, --authors ", "comma-separated list of author emails") .option("-n, --limit ", "maximum number of issues to return (default: 100, max: 1000)", "100") .option("-O, --overview", "show issue count totals instead of the issues list") + .option("-F, --false-positives", "only show issues that are potential false positives") + .option("-I, --bulk-ignore", "ignore all false positive issues matching the current filters") + .option("-m, --comment ", "optional comment when using --bulk-ignore") .addHelpText( "after", ` @@ -194,6 +200,9 @@ Examples: $ codacy issues gh my-org my-repo --categories Security --overview $ codacy issues gh my-org my-repo --tools eslint,semgrep $ codacy issues gh my-org my-repo --limit 500 + $ codacy issues gh my-org my-repo --false-positives + $ codacy issues gh my-org my-repo --bulk-ignore --branch main + $ codacy issues gh my-org my-repo --bulk-ignore --patterns security-rule --comment "Confirmed FPs" $ codacy issues gh my-org my-repo --output json`, ) .action(async function ( @@ -207,6 +216,7 @@ Examples: const opts = this.opts(); const format = getOutputFormat(this); const isOverview = !!opts.overview; + const isBulkIgnore = !!opts.bulkIgnore; // Build the shared filter body from CLI options const body: SearchRepositoryIssuesBody = {}; @@ -223,6 +233,8 @@ Examples: if (tags) body.tags = tags; const author = parseCommaList(opts.authors); if (author) body.authorEmails = author; + // --false-positives and --bulk-ignore both restrict the API query to FP issues only + if (opts.falsePositives || isBulkIgnore) body.onlyPotentialFalsePositives = true; const toolInputs = parseCommaList(opts.tools); if (toolInputs) { @@ -240,6 +252,51 @@ Examples: const limit = Math.min(Math.max(parseInt(opts.limit, 10) || 100, 1), 1000); + // --bulk-ignore: fetch all FP issues (all pages) then call bulkIgnoreIssues in batches + if (isBulkIgnore) { + const fetchSpinner = ora("Fetching false positive issues...").start(); + const allIssues: CommitIssue[] = []; + let cursor: string | undefined; + + do { + const resp = await AnalysisService.searchRepositoryIssues( + provider, + organization, + repository, + cursor, + 100, + body, + ); + allIssues.push(...resp.data); + cursor = resp.pagination?.cursor; + } while (cursor); + + fetchSpinner.stop(); + + if (allIssues.length === 0) { + console.log(ansis.green("No false positive issues found.")); + return; + } + + const count = allIssues.length; + const plural = count === 1 ? "" : "s"; + console.log(`Found ${ansis.bold(String(count))} false positive issue${plural}.`); + + const ignoreSpinner = ora(`Ignoring ${count} issue${plural}...`).start(); + const issueIds = allIssues.map((i) => i.issueId); + + for (let i = 0; i < issueIds.length; i += BULK_BATCH_SIZE) { + await AnalysisService.bulkIgnoreIssues(provider, organization, repository, { + issueIds: issueIds.slice(i, i + BULK_BATCH_SIZE), + reason: "FalsePositive", + comment: opts.comment || undefined, + }); + } + + ignoreSpinner.succeed(`Ignored ${ansis.bold(String(count))} false positive issue${plural}.`); + return; + } + const spinner = ora( isOverview ? "Fetching issues overview..." : "Fetching issues...", ).start(); From 095445e66edc2495ecdb23182a32109b0401e364 Mon Sep 17 00:00:00 2001 From: Milosz Jakubanis Date: Tue, 5 May 2026 12:01:50 +0100 Subject: [PATCH 2/2] feat: Address AI feedback, extract func CF-2412 --- src/commands/issues.test.ts | 44 ++++++++- src/commands/issues.ts | 185 +++++++++++++++++++++--------------- 2 files changed, 152 insertions(+), 77 deletions(-) diff --git a/src/commands/issues.test.ts b/src/commands/issues.test.ts index d48483d..06b48b8 100644 --- a/src/commands/issues.test.ts +++ b/src/commands/issues.test.ts @@ -873,6 +873,46 @@ describe("issues command", () => { }); describe("--bulk-ignore flag", () => { + it("should error when --overview is combined with --bulk-ignore", async () => { + const mockExit = vi.spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit called"); + }); + vi.spyOn(console, "error").mockImplementation(() => {}); + + const program = createProgram(); + await expect( + program.parseAsync([ + "node", "test", "issues", "gh", "test-org", "test-repo", + "--bulk-ignore", "--overview", + ]), + ).rejects.toThrow("process.exit called"); + + expect(AnalysisService.bulkIgnoreIssues).not.toHaveBeenCalled(); + expect(AnalysisService.searchRepositoryIssues).not.toHaveBeenCalled(); + + mockExit.mockRestore(); + }); + + it("should error when --limit is explicitly combined with --bulk-ignore", async () => { + const mockExit = vi.spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit called"); + }); + vi.spyOn(console, "error").mockImplementation(() => {}); + + const program = createProgram(); + await expect( + program.parseAsync([ + "node", "test", "issues", "gh", "test-org", "test-repo", + "--bulk-ignore", "--limit", "10", + ]), + ).rejects.toThrow("process.exit called"); + + expect(AnalysisService.bulkIgnoreIssues).not.toHaveBeenCalled(); + expect(AnalysisService.searchRepositoryIssues).not.toHaveBeenCalled(); + + mockExit.mockRestore(); + }); + it("should fetch all FP issues with onlyPotentialFalsePositives: true and call bulkIgnoreIssues", async () => { vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ data: mockIssues, @@ -959,7 +999,7 @@ describe("issues command", () => { expect(secondCallIds).toHaveLength(50); }); - it("should forward --comment to bulkIgnoreIssues", async () => { + it("should forward --ignore-comment to bulkIgnoreIssues", async () => { vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ data: [mockIssues[0]], } as any); @@ -969,7 +1009,7 @@ describe("issues command", () => { await program.parseAsync([ "node", "test", "issues", "gh", "test-org", "test-repo", "--bulk-ignore", - "--comment", "Verified by security team", + "--ignore-comment", "Verified by security team", ]); expect(AnalysisService.bulkIgnoreIssues).toHaveBeenCalledWith( diff --git a/src/commands/issues.ts b/src/commands/issues.ts index a2a9aed..e20c074 100644 --- a/src/commands/issues.ts +++ b/src/commands/issues.ts @@ -164,6 +164,107 @@ function parseCommaList(value: string | undefined): string[] | undefined { .filter(Boolean); } +/** Paginate through all tools and return the full list. */ +async function fetchAllTools(): Promise { + const tools: Tool[] = []; + let cursor: string | undefined; + do { + const resp = await ToolsService.listTools(cursor, 100); + tools.push(...resp.data); + cursor = resp.pagination?.cursor; + } while (cursor); + return tools; +} + +/** + * Build the SearchRepositoryIssuesBody from parsed CLI options. + * Resolves tool names/UUIDs via the Codacy API when --tools is provided. + */ +async function buildFilterBody(opts: Record): Promise { + const body: SearchRepositoryIssuesBody = {}; + + if (opts.branch) body.branchName = opts.branch; + + const patterns = parseCommaList(opts.patterns); + if (patterns) body.patternIds = patterns; + + const severity = parseCommaList(opts.severities); + if (severity) body.levels = severity.map(normalizeSeverity); + + const category = parseCommaList(opts.categories); + if (category) body.categories = category.map(normalizeCategory); + + const language = parseCommaList(opts.languages); + if (language) body.languages = language; + + const tags = parseCommaList(opts.tags); + if (tags) body.tags = tags; + + const author = parseCommaList(opts.authors); + if (author) body.authorEmails = author; + + // --false-positives and --bulk-ignore both restrict the API query to FP issues only + if (opts.falsePositives || opts.bulkIgnore) body.onlyPotentialFalsePositives = true; + + const toolInputs = parseCommaList(opts.tools); + if (toolInputs) body.toolUuids = await resolveToolUuids(toolInputs, fetchAllTools); + + return body; +} + +/** + * Fetch every false positive issue (all pages) then ignore them in batches of + * BULK_BATCH_SIZE. Prints progress via spinners and exits when done. + */ +async function executeBulkIgnore( + provider: string, + organization: string, + repository: string, + body: SearchRepositoryIssuesBody, + comment: string | undefined, +): Promise { + const fetchSpinner = ora("Fetching false positive issues...").start(); + const allIssues: CommitIssue[] = []; + let cursor: string | undefined; + + do { + const resp = await AnalysisService.searchRepositoryIssues( + provider, + organization, + repository, + cursor, + 100, + body, + ); + allIssues.push(...resp.data); + cursor = resp.pagination?.cursor; + } while (cursor); + + fetchSpinner.stop(); + + if (allIssues.length === 0) { + console.log(ansis.green("No false positive issues found.")); + return; + } + + const count = allIssues.length; + const plural = count === 1 ? "" : "s"; + console.log(`Found ${ansis.bold(String(count))} false positive issue${plural}.`); + + const ignoreSpinner = ora(`Ignoring ${count} issue${plural}...`).start(); + const issueIds = allIssues.map((i) => i.issueId); + + for (let i = 0; i < issueIds.length; i += BULK_BATCH_SIZE) { + await AnalysisService.bulkIgnoreIssues(provider, organization, repository, { + issueIds: issueIds.slice(i, i + BULK_BATCH_SIZE), + reason: "FalsePositive", + comment: comment || undefined, + }); + } + + ignoreSpinner.succeed(`Ignored ${ansis.bold(String(count))} false positive issue${plural}.`); +} + export function registerIssuesCommand(program: Command) { program .command("issues") @@ -190,7 +291,7 @@ export function registerIssuesCommand(program: Command) { .option("-O, --overview", "show issue count totals instead of the issues list") .option("-F, --false-positives", "only show issues that are potential false positives") .option("-I, --bulk-ignore", "ignore all false positive issues matching the current filters") - .option("-m, --comment ", "optional comment when using --bulk-ignore") + .option("-m, --ignore-comment ", "optional comment when using --bulk-ignore") .addHelpText( "after", ` @@ -202,7 +303,7 @@ Examples: $ codacy issues gh my-org my-repo --limit 500 $ codacy issues gh my-org my-repo --false-positives $ codacy issues gh my-org my-repo --bulk-ignore --branch main - $ codacy issues gh my-org my-repo --bulk-ignore --patterns security-rule --comment "Confirmed FPs" + $ codacy issues gh my-org my-repo --bulk-ignore --patterns security-rule --ignore-comment "Confirmed FPs" $ codacy issues gh my-org my-repo --output json`, ) .action(async function ( @@ -216,84 +317,18 @@ Examples: const opts = this.opts(); const format = getOutputFormat(this); const isOverview = !!opts.overview; - const isBulkIgnore = !!opts.bulkIgnore; - - // Build the shared filter body from CLI options - const body: SearchRepositoryIssuesBody = {}; - if (opts.branch) body.branchName = opts.branch; - const patterns = parseCommaList(opts.patterns); - if (patterns) body.patternIds = patterns; - const severity = parseCommaList(opts.severities); - if (severity) body.levels = severity.map(normalizeSeverity); - const category = parseCommaList(opts.categories); - if (category) body.categories = category.map(normalizeCategory); - const language = parseCommaList(opts.languages); - if (language) body.languages = language; - const tags = parseCommaList(opts.tags); - if (tags) body.tags = tags; - const author = parseCommaList(opts.authors); - if (author) body.authorEmails = author; - // --false-positives and --bulk-ignore both restrict the API query to FP issues only - if (opts.falsePositives || isBulkIgnore) body.onlyPotentialFalsePositives = true; - - const toolInputs = parseCommaList(opts.tools); - if (toolInputs) { - body.toolUuids = await resolveToolUuids(toolInputs, async () => { - const tools: Tool[] = []; - let cursor: string | undefined; - do { - const resp = await ToolsService.listTools(cursor, 100); - tools.push(...resp.data); - cursor = resp.pagination?.cursor; - } while (cursor); - return tools; - }); - } + const body = await buildFilterBody(opts); const limit = Math.min(Math.max(parseInt(opts.limit, 10) || 100, 1), 1000); - // --bulk-ignore: fetch all FP issues (all pages) then call bulkIgnoreIssues in batches - if (isBulkIgnore) { - const fetchSpinner = ora("Fetching false positive issues...").start(); - const allIssues: CommitIssue[] = []; - let cursor: string | undefined; - - do { - const resp = await AnalysisService.searchRepositoryIssues( - provider, - organization, - repository, - cursor, - 100, - body, - ); - allIssues.push(...resp.data); - cursor = resp.pagination?.cursor; - } while (cursor); - - fetchSpinner.stop(); - - if (allIssues.length === 0) { - console.log(ansis.green("No false positive issues found.")); - return; + if (opts.bulkIgnore) { + if (isOverview) { + this.error("--overview cannot be used with --bulk-ignore; --overview is a read-only display mode"); } - - const count = allIssues.length; - const plural = count === 1 ? "" : "s"; - console.log(`Found ${ansis.bold(String(count))} false positive issue${plural}.`); - - const ignoreSpinner = ora(`Ignoring ${count} issue${plural}...`).start(); - const issueIds = allIssues.map((i) => i.issueId); - - for (let i = 0; i < issueIds.length; i += BULK_BATCH_SIZE) { - await AnalysisService.bulkIgnoreIssues(provider, organization, repository, { - issueIds: issueIds.slice(i, i + BULK_BATCH_SIZE), - reason: "FalsePositive", - comment: opts.comment || undefined, - }); + if (this.getOptionValueSource("limit") === "cli") { + this.error("--limit cannot be used with --bulk-ignore; the bulk-ignore path always processes all matching issues"); } - - ignoreSpinner.succeed(`Ignored ${ansis.bold(String(count))} false positive issue${plural}.`); + await executeBulkIgnore(provider, organization, repository, body, opts.ignoreComment); return; }