From 87edb943eeffa5215c991bb5031ab3842ad55524 Mon Sep 17 00:00:00 2001 From: Milosz Jakubanis Date: Tue, 5 May 2026 09:21:05 +0100 Subject: [PATCH 1/4] 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/4] 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; } From 54bcedf3d52405a77d5d88f7f1797325961c90c7 Mon Sep 17 00:00:00 2001 From: Alejandro Rizzo Date: Thu, 7 May 2026 14:33:52 +0100 Subject: [PATCH 3/4] feat: Generalize --ignore for all issues, add --ignore-reason CF-2412 Rename --bulk-ignore to --ignore so it works for all issues (not just false positives). Make --false-positives a tri-state filter (true/false/ omit) and add --ignore-reason with the same choices as the issue command. Co-Authored-By: Claude Opus 4.6 (1M context) --- SPECS/commands/issues.md | 8 ++- src/commands/AGENTS.md | 13 ++++ src/commands/issues.test.ts | 119 ++++++++++++++++++++++++++++++------ src/commands/issues.ts | 44 ++++++++----- 4 files changed, 149 insertions(+), 35 deletions(-) diff --git a/SPECS/commands/issues.md b/SPECS/commands/issues.md index 62007c6..cd82977 100644 --- a/SPECS/commands/issues.md +++ b/SPECS/commands/issues.md @@ -33,7 +33,13 @@ Both accept the same `SearchRepositoryIssuesBody` for filtering. | `--languages ` | `-l` | Comma-separated language names | | `--tags ` | `-t` | Comma-separated tag names | | `--authors ` | `-a` | Comma-separated author emails | +| `--tools ` | `-T` | Comma-separated tool UUIDs or names | +| `--limit ` | `-n` | Maximum number of issues (default: 100, max: 1000) | | `--overview` | `-O` | Show overview counts instead of list | +| `--false-positives [value]` | `-F` | Filter by potential false positives (true, false, or omit) | +| `--ignore` | `-I` | Ignore all issues matching current filters | +| `--ignore-reason ` | `-R` | Reason for ignoring (AcceptedUse, FalsePositive, NotExploitable, TestCode, ExternalCode) | +| `--ignore-comment ` | `-m` | Optional comment when using --ignore | ## Output @@ -64,4 +70,4 @@ Six count tables sorted descending by count: Category, Severity, Language, Tag, ## Tests -File: `src/commands/issues.test.ts` — 11 tests. +File: `src/commands/issues.test.ts` — 39 tests. diff --git a/src/commands/AGENTS.md b/src/commands/AGENTS.md index 99293e9..5b13875 100644 --- a/src/commands/AGENTS.md +++ b/src/commands/AGENTS.md @@ -131,6 +131,19 @@ Several helpers are shared between `repository.ts` and `pull-request.ts` via `ut - **`--unignore` mode** (`-U`): calls `AnalysisService.updateIssueState` with `{ ignored: false }`; skips rendering issue details - The API uses the string UUID (`issue.issueId`), not the numeric `resultDataId`, for the `updateIssueState` call +## issues command (`issues.ts`) + +- Takes ``, ``, and `` as required arguments +- **List mode** (default): card-style format sorted by severity (Error > High > Warning > Info) +- **Overview mode** (`-O, --overview`): six count tables — Category, Severity, Language, Tag, Pattern, Author +- **Filters**: `--branch`, `--patterns`, `--tools`, `--severities`, `--categories`, `--languages`, `--tags`, `--authors`, `--limit` +- **`--false-positives [value]`** (`-F`): tri-state filter — `true` (default when flag present) sends `onlyPotentialFalsePositives: true`, `false` sends `onlyPotentialFalsePositives: false`, omitted sends nothing +- **`--ignore` mode** (`-I`): fetches all issues matching current filters (all pages), then calls `AnalysisService.bulkIgnoreIssues` in batches of 100 + - `-R, --ignore-reason`: `AcceptedUse` (default) | `FalsePositive` | `NotExploitable` | `TestCode` | `ExternalCode` + - `-m, --ignore-comment`: optional free-text comment + - Cannot be combined with `--overview` or `--limit` + - Works with any combination of filters; use `--false-positives --ignore` to ignore only FP issues + ## finding command (`finding.ts`) - Takes ``, ``, and `` (UUID shown on finding cards) as required arguments — **no `` argument** diff --git a/src/commands/issues.test.ts b/src/commands/issues.test.ts index 06b48b8..680ad1f 100644 --- a/src/commands/issues.test.ts +++ b/src/commands/issues.test.ts @@ -849,6 +849,40 @@ describe("issues command", () => { ); }); + it("should pass onlyPotentialFalsePositives: false when --false-positives false", 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", "false", + ]); + + expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledWith( + "gh", "test-org", "test-repo", undefined, 100, + { onlyPotentialFalsePositives: false }, + ); + }); + + it("should pass onlyPotentialFalsePositives: true when --false-positives true", 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", "true", + ]); + + expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledWith( + "gh", "test-org", "test-repo", undefined, 100, + { onlyPotentialFalsePositives: true }, + ); + }); + it("should display false positive issues in list format", async () => { const fpIssue = { ...mockIssues[0], @@ -872,8 +906,8 @@ describe("issues command", () => { }); }); - describe("--bulk-ignore flag", () => { - it("should error when --overview is combined with --bulk-ignore", async () => { + describe("--ignore flag", () => { + it("should error when --overview is combined with --ignore", async () => { const mockExit = vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("process.exit called"); }); @@ -883,7 +917,7 @@ describe("issues command", () => { await expect( program.parseAsync([ "node", "test", "issues", "gh", "test-org", "test-repo", - "--bulk-ignore", "--overview", + "--ignore", "--overview", ]), ).rejects.toThrow("process.exit called"); @@ -893,7 +927,7 @@ describe("issues command", () => { mockExit.mockRestore(); }); - it("should error when --limit is explicitly combined with --bulk-ignore", async () => { + it("should error when --limit is explicitly combined with --ignore", async () => { const mockExit = vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("process.exit called"); }); @@ -903,7 +937,7 @@ describe("issues command", () => { await expect( program.parseAsync([ "node", "test", "issues", "gh", "test-org", "test-repo", - "--bulk-ignore", "--limit", "10", + "--ignore", "--limit", "10", ]), ).rejects.toThrow("process.exit called"); @@ -913,7 +947,7 @@ describe("issues command", () => { mockExit.mockRestore(); }); - it("should fetch all FP issues with onlyPotentialFalsePositives: true and call bulkIgnoreIssues", async () => { + it("should fetch all issues and call bulkIgnoreIssues with default reason AcceptedUse", async () => { vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ data: mockIssues, } as any); @@ -922,24 +956,24 @@ describe("issues command", () => { const program = createProgram(); await program.parseAsync([ "node", "test", "issues", "gh", "test-org", "test-repo", - "--bulk-ignore", + "--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", + reason: "AcceptedUse", comment: undefined, }, ); }); - it("should show 'No false positive issues found' when API returns empty list", async () => { + it("should show 'No issues found' when API returns empty list", async () => { vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ data: [], } as any); @@ -947,12 +981,12 @@ describe("issues command", () => { const program = createProgram(); await program.parseAsync([ "node", "test", "issues", "gh", "test-org", "test-repo", - "--bulk-ignore", + "--ignore", ]); expect(AnalysisService.bulkIgnoreIssues).not.toHaveBeenCalled(); const output = getAllOutput(); - expect(output).toContain("No false positive issues found"); + expect(output).toContain("No issues found matching the current filters"); }); it("should batch bulkIgnoreIssues calls when there are more than 100 issues", async () => { @@ -982,7 +1016,7 @@ describe("issues command", () => { const program = createProgram(); await program.parseAsync([ "node", "test", "issues", "gh", "test-org", "test-repo", - "--bulk-ignore", + "--ignore", ]); // Should have made 2 search calls (paginated) @@ -1008,7 +1042,7 @@ describe("issues command", () => { const program = createProgram(); await program.parseAsync([ "node", "test", "issues", "gh", "test-org", "test-repo", - "--bulk-ignore", + "--ignore", "--ignore-comment", "Verified by security team", ]); @@ -1016,13 +1050,13 @@ describe("issues command", () => { "gh", "test-org", "test-repo", { issueIds: [mockIssues[0].issueId], - reason: "FalsePositive", + reason: "AcceptedUse", comment: "Verified by security team", }, ); }); - it("should combine --bulk-ignore with other filters (--branch, --patterns)", async () => { + it("should combine --ignore with other filters (--branch, --patterns)", async () => { vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ data: [], } as any); @@ -1030,7 +1064,7 @@ describe("issues command", () => { const program = createProgram(); await program.parseAsync([ "node", "test", "issues", "gh", "test-org", "test-repo", - "--bulk-ignore", + "--ignore", "--branch", "develop", "--patterns", "sql-injection", ]); @@ -1038,11 +1072,60 @@ describe("issues command", () => { expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledWith( "gh", "test-org", "test-repo", undefined, 100, { - onlyPotentialFalsePositives: true, branchName: "develop", patternIds: ["sql-injection"], }, ); }); + + it("should pass --ignore-reason 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", + "--ignore", + "--ignore-reason", "FalsePositive", + ]); + + expect(AnalysisService.bulkIgnoreIssues).toHaveBeenCalledWith( + "gh", "test-org", "test-repo", + { + issueIds: [mockIssues[0].issueId], + reason: "FalsePositive", + comment: undefined, + }, + ); + }); + + it("should combine --ignore with --false-positives to ignore only FP issues", 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", + "--ignore", + "--false-positives", + ]); + + 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], + reason: "AcceptedUse", + comment: undefined, + }, + ); + }); }); }); diff --git a/src/commands/issues.ts b/src/commands/issues.ts index e20c074..797d3b8 100644 --- a/src/commands/issues.ts +++ b/src/commands/issues.ts @@ -80,6 +80,10 @@ function normalizeCategory(input: string): string { return CATEGORY_NORMALIZE[key] ?? input; } +function parseBooleanOption(value: string): boolean { + return value.toLowerCase() !== "false"; +} + function printIssuesList(issues: CommitIssue[], total: number): void { printSection("Issues", total, "issue"); @@ -203,8 +207,8 @@ async function buildFilterBody(opts: Record): Promise { - const fetchSpinner = ora("Fetching false positive issues...").start(); + const fetchSpinner = ora("Fetching issues...").start(); const allIssues: CommitIssue[] = []; let cursor: string | undefined; @@ -243,13 +248,13 @@ async function executeBulkIgnore( fetchSpinner.stop(); if (allIssues.length === 0) { - console.log(ansis.green("No false positive issues found.")); + console.log(ansis.green("No issues found matching the current filters.")); return; } const count = allIssues.length; const plural = count === 1 ? "" : "s"; - console.log(`Found ${ansis.bold(String(count))} false positive issue${plural}.`); + console.log(`Found ${ansis.bold(String(count))} issue${plural}.`); const ignoreSpinner = ora(`Ignoring ${count} issue${plural}...`).start(); const issueIds = allIssues.map((i) => i.issueId); @@ -257,12 +262,12 @@ async function executeBulkIgnore( 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", + reason, comment: comment || undefined, }); } - ignoreSpinner.succeed(`Ignored ${ansis.bold(String(count))} false positive issue${plural}.`); + ignoreSpinner.succeed(`Ignored ${ansis.bold(String(count))} issue${plural}.`); } export function registerIssuesCommand(program: Command) { @@ -289,9 +294,14 @@ 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, --ignore-comment ", "optional comment when using --bulk-ignore") + .option("-F, --false-positives [value]", "filter by potential false positives (true, false, or omit)", parseBooleanOption) + .option("-I, --ignore", "ignore all issues matching the current filters") + .option( + "-R, --ignore-reason ", + "reason for ignoring (AcceptedUse|FalsePositive|NotExploitable|TestCode|ExternalCode)", + "AcceptedUse", + ) + .option("-m, --ignore-comment ", "optional comment when using --ignore") .addHelpText( "after", ` @@ -302,8 +312,10 @@ Examples: $ 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 --ignore-comment "Confirmed FPs" + $ codacy issues gh my-org my-repo --false-positives false + $ codacy issues gh my-org my-repo --ignore --branch main + $ codacy issues gh my-org my-repo --false-positives --ignore --ignore-reason FalsePositive + $ codacy issues gh my-org my-repo --ignore --ignore-reason NotExploitable --ignore-comment "Reviewed" $ codacy issues gh my-org my-repo --output json`, ) .action(async function ( @@ -321,14 +333,14 @@ Examples: const body = await buildFilterBody(opts); const limit = Math.min(Math.max(parseInt(opts.limit, 10) || 100, 1), 1000); - if (opts.bulkIgnore) { + if (opts.ignore) { if (isOverview) { - this.error("--overview cannot be used with --bulk-ignore; --overview is a read-only display mode"); + this.error("--overview cannot be used with --ignore; --overview is a read-only display mode"); } if (this.getOptionValueSource("limit") === "cli") { - this.error("--limit cannot be used with --bulk-ignore; the bulk-ignore path always processes all matching issues"); + this.error("--limit cannot be used with --ignore; the --ignore path always processes all matching issues"); } - await executeBulkIgnore(provider, organization, repository, body, opts.ignoreComment); + await executeBulkIgnore(provider, organization, repository, body, opts.ignoreReason, opts.ignoreComment); return; } From d44664151585a0a9adcd15283fdb148e2a92bed2 Mon Sep 17 00:00:00 2001 From: Alejandro Rizzo Date: Fri, 8 May 2026 14:21:03 +0100 Subject: [PATCH 4/4] feat: Add reanalyze hint after ignoring issues, fix field rename CF-2412 Show a "run reanalysis" hint with the exact command after ignoring issues in `issue`, `issues`, and `pull-request` commands. Also align tests with the API field rename (potentialFalsePositives) and restore the missing ignoreReason argument in the executeBulkIgnore call. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- src/commands/issue.ts | 3 + src/commands/issues.test.ts | 18 ++--- src/commands/issues.ts | 134 ++++++++++++++++++++++++----------- src/commands/pull-request.ts | 6 ++ 5 files changed, 112 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index 7e7a075..845e095 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/55.6.0/apiv3-bundled.yaml -o ./api-v3/api-swagger.yaml --create-dirs", + "fetch-api": "curl https://artifacts.codacy.com/api/codacy-api/55.6.4/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/issue.ts b/src/commands/issue.ts index 255f9e2..bb3d948 100644 --- a/src/commands/issue.ts +++ b/src/commands/issue.ts @@ -149,6 +149,9 @@ Examples: ignoreSpinner.succeed( `Issue #${issueId} ignored (reason: ${ignoreReason}).`, ); + console.log( + ansis.dim(`Run a new analysis to see changes reflected: codacy repository ${provider} ${organization} ${repository} --reanalyze`), + ); } if (shouldUnignore) { diff --git a/src/commands/issues.test.ts b/src/commands/issues.test.ts index 680ad1f..f166f2a 100644 --- a/src/commands/issues.test.ts +++ b/src/commands/issues.test.ts @@ -809,7 +809,7 @@ describe("issues command", () => { }); describe("--false-positives flag", () => { - it("should pass onlyPotentialFalsePositives: true in the body", async () => { + it("should pass potentialFalsePositives: true in the body", async () => { vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ data: [], } as any); @@ -822,11 +822,11 @@ describe("issues command", () => { expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledWith( "gh", "test-org", "test-repo", undefined, 100, - { onlyPotentialFalsePositives: true }, + { potentialFalsePositives: true }, ); }); - it("should combine onlyPotentialFalsePositives with other filters (--patterns)", async () => { + it("should combine potentialFalsePositives with other filters (--patterns)", async () => { vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ data: [], } as any); @@ -842,14 +842,14 @@ describe("issues command", () => { expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledWith( "gh", "test-org", "test-repo", undefined, 100, { - onlyPotentialFalsePositives: true, + potentialFalsePositives: true, patternIds: ["no-undef", "sql-injection"], branchName: "main", }, ); }); - it("should pass onlyPotentialFalsePositives: false when --false-positives false", async () => { + it("should pass potentialFalsePositives: false when --false-positives false", async () => { vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ data: [], } as any); @@ -862,11 +862,11 @@ describe("issues command", () => { expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledWith( "gh", "test-org", "test-repo", undefined, 100, - { onlyPotentialFalsePositives: false }, + { potentialFalsePositives: false }, ); }); - it("should pass onlyPotentialFalsePositives: true when --false-positives true", async () => { + it("should pass potentialFalsePositives: true when --false-positives true", async () => { vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({ data: [], } as any); @@ -879,7 +879,7 @@ describe("issues command", () => { expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledWith( "gh", "test-org", "test-repo", undefined, 100, - { onlyPotentialFalsePositives: true }, + { potentialFalsePositives: true }, ); }); @@ -1116,7 +1116,7 @@ describe("issues command", () => { expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledWith( "gh", "test-org", "test-repo", undefined, 100, - { onlyPotentialFalsePositives: true }, + { potentialFalsePositives: true }, ); expect(AnalysisService.bulkIgnoreIssues).toHaveBeenCalledWith( "gh", "test-org", "test-repo", diff --git a/src/commands/issues.ts b/src/commands/issues.ts index 797d3b8..1b765e1 100644 --- a/src/commands/issues.ts +++ b/src/commands/issues.ts @@ -10,7 +10,11 @@ import { printJson, printPaginationWarning, } from "../utils/output"; -import { printSection, printIssueCard, resolveToolUuids } from "../utils/formatting"; +import { + printSection, + printIssueCard, + resolveToolUuids, +} from "../utils/formatting"; import { AnalysisService } from "../api/client/services/AnalysisService"; import { ToolsService } from "../api/client/services/ToolsService"; import { Tool } from "../api/client/models/Tool"; @@ -84,7 +88,6 @@ function parseBooleanOption(value: string): boolean { return value.toLowerCase() !== "false"; } - function printIssuesList(issues: CommitIssue[], total: number): void { printSection("Issues", total, "issue"); if (issues.length === 0) { @@ -184,7 +187,9 @@ async function fetchAllTools(): Promise { * 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 { +async function buildFilterBody( + opts: Record, +): Promise { const body: SearchRepositoryIssuesBody = {}; if (opts.branch) body.branchName = opts.branch; @@ -207,11 +212,12 @@ async function buildFilterBody(opts: Record): Promise { const fetchSpinner = ora("Fetching issues...").start(); const allIssues: CommitIssue[] = []; @@ -263,11 +269,14 @@ async function executeBulkIgnore( await AnalysisService.bulkIgnoreIssues(provider, organization, repository, { issueIds: issueIds.slice(i, i + BULK_BATCH_SIZE), reason, - comment: comment || undefined, + comment, }); } ignoreSpinner.succeed(`Ignored ${ansis.bold(String(count))} issue${plural}.`); + console.log( + ansis.dim(`Run a new analysis to see changes reflected: codacy repository ${provider} ${organization} ${repository} --reanalyze`), + ); } export function registerIssuesCommand(program: Command) { @@ -278,9 +287,15 @@ export function registerIssuesCommand(program: Command) { .argument("", "git provider (gh, gl, or bb)") .argument("", "organization name") .argument("", "repository name") - .option("-b, --branch ", "branch name (defaults to the main branch)") + .option( + "-b, --branch ", + "branch name (defaults to the main branch)", + ) .option("-p, --patterns ", "comma-separated list of pattern IDs") - .option("-T, --tools ", "comma-separated tool UUIDs or names to filter by") + .option( + "-T, --tools ", + "comma-separated tool UUIDs or names to filter by", + ) .option( "-s, --severities ", "comma-separated severity levels: Critical, High, Medium, Minor (or Error, Warning, Info)", @@ -289,19 +304,36 @@ export function registerIssuesCommand(program: Command) { "-c, --categories ", "comma-separated category names (e.g. Security, CodeStyle, ErrorProne)", ) - .option("-l, --languages ", "comma-separated list of language names") + .option( + "-l, --languages ", + "comma-separated list of language names", + ) .option("-t, --tags ", "comma-separated list of tag names") .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 [value]", "filter by potential false positives (true, false, or omit)", parseBooleanOption) + .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 [value]", + "filter by potential false positives (true, false, or omit)", + parseBooleanOption, + ) .option("-I, --ignore", "ignore all issues matching the current filters") .option( "-R, --ignore-reason ", "reason for ignoring (AcceptedUse|FalsePositive|NotExploitable|TestCode|ExternalCode)", "AcceptedUse", ) - .option("-m, --ignore-comment ", "optional comment when using --ignore") + .option( + "-m, --ignore-comment ", + "optional comment when using --ignore", + ) .addHelpText( "after", ` @@ -331,16 +363,30 @@ Examples: const isOverview = !!opts.overview; const body = await buildFilterBody(opts); - const limit = Math.min(Math.max(parseInt(opts.limit, 10) || 100, 1), 1000); + const limit = Math.min( + Math.max(parseInt(opts.limit, 10) || 100, 1), + 1000, + ); if (opts.ignore) { if (isOverview) { - this.error("--overview cannot be used with --ignore; --overview is a read-only display mode"); + this.error( + "--overview cannot be used with --ignore; --overview is a read-only display mode", + ); } if (this.getOptionValueSource("limit") === "cli") { - this.error("--limit cannot be used with --ignore; the --ignore path always processes all matching issues"); + this.error( + "--limit cannot be used with --ignore; the ignore path always processes all matching issues", + ); } - await executeBulkIgnore(provider, organization, repository, body, opts.ignoreReason, opts.ignoreComment); + await executeBulkIgnore( + provider, + organization, + repository, + body, + opts.ignoreReason, + opts.ignoreComment, + ); return; } @@ -360,14 +406,16 @@ Examples: const counts = overviewResponse.data.counts; if (format === "json") { - printJson(pickDeep({ overview: counts }, [ - "overview.categories", - "overview.levels", - "overview.languages", - "overview.tags", - "overview.patterns", - "overview.authors", - ])); + printJson( + pickDeep({ overview: counts }, [ + "overview.categories", + "overview.levels", + "overview.languages", + "overview.tags", + "overview.patterns", + "overview.authors", + ]), + ); return; } @@ -405,20 +453,24 @@ Examples: spinner.stop(); if (format === "json") { - printJson({ issues: issues.map((issue: any) => pickDeep(issue, [ - "patternInfo.id", - "patternInfo.severityLevel", - "patternInfo.category", - "patternInfo.subCategory", - "message", - "filePath", - "lineNumber", - "lineText", - "resultDataId", - "falsePositiveProbability", - "falsePositiveThreshold", - "falsePositiveReason", - ])) }); + printJson({ + issues: issues.map((issue: any) => + pickDeep(issue, [ + "patternInfo.id", + "patternInfo.severityLevel", + "patternInfo.category", + "patternInfo.subCategory", + "message", + "filePath", + "lineNumber", + "lineText", + "resultDataId", + "falsePositiveProbability", + "falsePositiveThreshold", + "falsePositiveReason", + ]), + ), + }); return; } diff --git a/src/commands/pull-request.ts b/src/commands/pull-request.ts index 3ff5b6b..2751c3e 100644 --- a/src/commands/pull-request.ts +++ b/src/commands/pull-request.ts @@ -800,6 +800,9 @@ Examples: spinner.succeed( `Issue #${ignoreIssueId} ignored (reason: ${ignoreReason}).`, ); + console.log( + ansis.dim(`Run a new analysis to see changes reflected: codacy pull-request ${provider} ${organization} ${repository} ${prNumber} --reanalyze`), + ); return; } @@ -841,6 +844,9 @@ Examples: spinner.succeed( `Ignored ${toIgnore.length} potential false positive issue(s) (reason: FalsePositive).`, ); + console.log( + ansis.dim(`Run a new analysis to see changes reflected: codacy pull-request ${provider} ${organization} ${repository} ${prNumber} --reanalyze`), + ); return; }