Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for importing a Codacy tool configuration file into a remote repository, plus related CLI UX improvements (confirmation prompt, standards management) and pagination controls for listing issues/findings.
Changes:
- Add
tools --importflow to preview and apply a local config file to a repository (with confirmation and--forcestandard unlinking). - Add
repository --link-standard/--unlink-standardactions and show coding standard IDs in repository output. - Add
--limitpagination support forissuesandfindingscommands (up to 1000).
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/prompt.ts | Adds a reusable confirmation prompt for CLI actions. |
| src/utils/import-config.ts | Implements config parsing, preview generation, and apply/execute logic for tool imports. |
| src/utils/import-config.test.ts | Adds unit tests for the import-config helper functions. |
| src/types/codacy-config.ts | Re-exports config types from @codacy/tooling. |
| src/commands/tools.ts | Adds --import, --skip-approval, --force handling and integrates import execution into the tools command. |
| src/commands/tools.test.ts | Adds tests covering tools import mode behavior. |
| src/commands/repository.ts | Shows standard IDs and adds link/unlink standard actions. |
| src/commands/repository.test.ts | Adds tests for standard ID display and link/unlink actions. |
| src/commands/issues.ts | Adds --limit and paginated fetching for issues results. |
| src/commands/issues.test.ts | Adds tests for issues pagination/limit behavior and JSON output fields. |
| src/commands/findings.ts | Adds --limit and paginated fetching for findings results. |
| src/commands/findings.test.ts | Adds tests for findings pagination/limit behavior. |
| package.json | Bumps version and adds @codacy/tooling dependency. |
| package-lock.json | Updates lockfile for the new dependency. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "engines": { | ||
| "node": ">=18" | ||
| }, | ||
| "dependencies": { | ||
| "@codacy/tooling": "0.1.0", | ||
| "ansis": "4.0.0", |
There was a problem hiding this comment.
The new dependency @codacy/tooling@0.1.0 requires Node >=20.0.0 (per package-lock), but this package still declares engines.node: >=18. This will cause installs on Node 18/19 to fail or be rejected by tooling that enforces engines. Align the CLI's supported Node version with the dependency (bump engines to >=20) or use a @codacy/tooling version that supports Node 18.
| const spinner = ora("Reading configuration...").start(); | ||
|
|
||
| // Read config file | ||
| const config = readConfigFile(resolvedPath); | ||
|
|
||
| // Fetch current state in parallel | ||
| const [repoToolsResponse, allTools, repoResponse] = | ||
| await Promise.all([ | ||
| AnalysisService.listRepositoryTools( | ||
| provider, | ||
| organization, | ||
| repository, | ||
| ), | ||
| fetchAllTools(), | ||
| AnalysisService.getRepositoryWithAnalysis( | ||
| provider, | ||
| organization, | ||
| repository, | ||
| ), | ||
| ]); |
There was a problem hiding this comment.
In import mode, the spinner is started before reading/parsing the config and before the parallel API calls. If readConfigFile(...) throws or Promise.all(...) rejects, execution jumps to the outer catch and process.exit(1) runs without stopping the spinner, leaving the terminal in a bad state. Wrap the import block in a try/finally (or use spinner.fail(...)) so the spinner is always stopped/cleared on errors.
| ], | ||
| }; | ||
| const tmpPath = "/tmp/test-codacy-config.json"; | ||
| fs.writeFileSync(tmpPath, JSON.stringify(config)); | ||
| const result = readConfigFile(tmpPath); | ||
| expect(result.version).toBe(1); | ||
| expect(result.tools).toHaveLength(1); |
There was a problem hiding this comment.
These tests write to hard-coded /tmp/... paths, which is not portable (e.g., Windows runners) and can cause collisions between parallel test runs. Prefer os.tmpdir() + path.join(...) (and ideally include process.pid/random suffix) to create unique temp file paths.
| const tmpConfigPath = "/tmp/test-import-config.json"; | ||
|
|
||
| beforeEach(() => { | ||
| fs.writeFileSync(tmpConfigPath, configContent); | ||
| vi.mocked(AnalysisService.updateRepositoryToolPatterns).mockResolvedValue(undefined as any); |
There was a problem hiding this comment.
The import-mode tests use a hard-coded /tmp/test-import-config.json path. This is not portable and can be flaky under parallel test execution. Use os.tmpdir()/path.join(...) and a unique filename (pid/random) for the temp config file.
| const config = JSON.parse(raw) as CodacyConfig; | ||
| if (!config.version || !Array.isArray(config.tools)) { | ||
| throw new Error("Invalid configuration file: missing 'version' or 'tools' fields."); | ||
| } |
There was a problem hiding this comment.
readConfigFile only validates top-level version and tools, but later code assumes every tool has a patterns array (e.g., t.patterns.length, toolConfig.patterns.map(...)). A config missing patterns (or with non-array values) will crash with a less helpful runtime error. Consider validating each tools[] entry (e.g., toolId string and patterns as an array, defaulting to [] when absent) and throwing a clear error pointing to the offending entry.
| } | |
| } | |
| // Validate and normalize each tool configuration | |
| config.tools = config.tools.map((tool, index) => { | |
| if (!tool || typeof tool !== "object") { | |
| throw new Error( | |
| `Invalid configuration file: tools[${index}] must be an object.`, | |
| ); | |
| } | |
| const anyTool = tool as any; | |
| const toolId = anyTool.toolId; | |
| if (typeof toolId !== "string" || toolId.trim() === "") { | |
| throw new Error( | |
| `Invalid configuration file: tools[${index}] is missing a valid 'toolId' string.`, | |
| ); | |
| } | |
| const patterns = anyTool.patterns; | |
| if (patterns === undefined) { | |
| // Default to empty array when patterns are not specified | |
| return { | |
| ...anyTool, | |
| patterns: [], | |
| } as CodacyToolConfig; | |
| } | |
| if (!Array.isArray(patterns)) { | |
| throw new Error( | |
| `Invalid configuration file: 'patterns' for tool '${toolId}' must be an array.`, | |
| ); | |
| } | |
| return tool as CodacyToolConfig; | |
| }); |
| if (opts.linkStandard) { | ||
| const spinner = ora(`Linking coding standard #${opts.linkStandard} to ${repository}...`).start(); | ||
| await CodingStandardsService.applyCodingStandardToRepositories( | ||
| provider, | ||
| organization, | ||
| Number(opts.linkStandard), | ||
| { link: [repository], unlink: [] }, | ||
| ); |
There was a problem hiding this comment.
--link-standard/--unlink-standard convert the provided id with Number(...) without validating it. If the user passes a non-numeric value, Number() becomes NaN and the API call will be made with an invalid standard id. Parse with parseInt and fail fast with a clear error when the id is not a finite integer.
| await CodingStandardsService.applyCodingStandardToRepositories( | ||
| provider, | ||
| organization, | ||
| Number(opts.linkStandard), | ||
| { link: [repository], unlink: [] }, | ||
| ); | ||
| spinner.stop(); | ||
| console.log( | ||
| `${ansis.green("✓")} Coding standard #${opts.linkStandard} linked to ${ansis.bold(repository)}.`, | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| // ── Action: unlink-standard ─────────────────────────────────────── | ||
| if (opts.unlinkStandard) { | ||
| const spinner = ora(`Unlinking coding standard #${opts.unlinkStandard} from ${repository}...`).start(); | ||
| await CodingStandardsService.applyCodingStandardToRepositories( | ||
| provider, | ||
| organization, | ||
| Number(opts.unlinkStandard), | ||
| { link: [], unlink: [repository] }, | ||
| ); | ||
| spinner.stop(); | ||
| console.log( | ||
| `${ansis.green("✓")} Coding standard #${opts.unlinkStandard} unlinked from ${ansis.bold(repository)}.`, | ||
| ); | ||
| return; |
There was a problem hiding this comment.
The new link/unlink-standard actions start a spinner and await the API call, but don't stop/fail the spinner if the request throws. On error, the outer catch exits the process, which can leave the spinner running and the terminal output garbled. Use a local try/catch/finally around the API call to ensure the spinner is stopped (and optionally show spinner.fail(...)) before rethrowing/handling the error.
| await CodingStandardsService.applyCodingStandardToRepositories( | |
| provider, | |
| organization, | |
| Number(opts.linkStandard), | |
| { link: [repository], unlink: [] }, | |
| ); | |
| spinner.stop(); | |
| console.log( | |
| `${ansis.green("✓")} Coding standard #${opts.linkStandard} linked to ${ansis.bold(repository)}.`, | |
| ); | |
| return; | |
| } | |
| // ── Action: unlink-standard ─────────────────────────────────────── | |
| if (opts.unlinkStandard) { | |
| const spinner = ora(`Unlinking coding standard #${opts.unlinkStandard} from ${repository}...`).start(); | |
| await CodingStandardsService.applyCodingStandardToRepositories( | |
| provider, | |
| organization, | |
| Number(opts.unlinkStandard), | |
| { link: [], unlink: [repository] }, | |
| ); | |
| spinner.stop(); | |
| console.log( | |
| `${ansis.green("✓")} Coding standard #${opts.unlinkStandard} unlinked from ${ansis.bold(repository)}.`, | |
| ); | |
| return; | |
| try { | |
| await CodingStandardsService.applyCodingStandardToRepositories( | |
| provider, | |
| organization, | |
| Number(opts.linkStandard), | |
| { link: [repository], unlink: [] }, | |
| ); | |
| spinner.stop(); | |
| console.log( | |
| `${ansis.green("✓")} Coding standard #${opts.linkStandard} linked to ${ansis.bold(repository)}.`, | |
| ); | |
| return; | |
| } catch (linkStandardErr) { | |
| spinner.fail( | |
| `Failed to link coding standard #${opts.linkStandard} to ${repository}: ${ | |
| linkStandardErr instanceof Error ? linkStandardErr.message : linkStandardErr | |
| }`, | |
| ); | |
| throw linkStandardErr; | |
| } | |
| } | |
| // ── Action: unlink-standard ─────────────────────────────────────── | |
| if (opts.unlinkStandard) { | |
| const spinner = ora(`Unlinking coding standard #${opts.unlinkStandard} from ${repository}...`).start(); | |
| try { | |
| await CodingStandardsService.applyCodingStandardToRepositories( | |
| provider, | |
| organization, | |
| Number(opts.unlinkStandard), | |
| { link: [], unlink: [repository] }, | |
| ); | |
| spinner.stop(); | |
| console.log( | |
| `${ansis.green("✓")} Coding standard #${opts.unlinkStandard} unlinked from ${ansis.bold(repository)}.`, | |
| ); | |
| return; | |
| } catch (unlinkStandardErr) { | |
| spinner.fail( | |
| `Failed to unlink coding standard #${opts.unlinkStandard} from ${repository}: ${ | |
| unlinkStandardErr instanceof Error ? unlinkStandardErr.message : unlinkStandardErr | |
| }`, | |
| ); | |
| throw unlinkStandardErr; | |
| } |
Added import config from the file generated by the analysis-cli into a remote repository