Skip to content

fix: preserve cloud-only tools and improve import error reporting#9

Merged
alerizzo merged 2 commits into
mainfrom
fix/tools-import-cloud-only
May 27, 2026
Merged

fix: preserve cloud-only tools and improve import error reporting#9
alerizzo merged 2 commits into
mainfrom
fix/tools-import-cloud-only

Conversation

@alerizzo
Copy link
Copy Markdown
Collaborator

Summary

  • Cloud-only tool preservation: tools import no longer disables tools that only exist on Codacy Cloud (not supported by the local CLI). This prevents accidentally turning off tools like Codacy Security, Trivy, etc. when importing a local config.
  • Config-file mode fix: When a tool has useLocalConfigurationFile: true, the import now skips the pattern-reset step and just enables the tool with the config file flag — previously it would needlessly wipe patterns first.
  • Structured error reporting: Import failures now surface the HTTP status code and API response body details instead of generic error messages, making it much easier to diagnose what went wrong.
  • Ship-it command: Adds a .claude/commands/ship-it.md for streamlined changeset → branch → commit → push → PR workflow.

Test plan

  • npm test — all existing and new tests pass
  • npx ts-node src/index.ts tools import --provider gh --organization <org> --repository <repo> .codacy.yaml --dry-run — verify cloud-only tools show as "unchanged" rather than marked for disable
  • npx ts-node src/index.ts tools import with a tool that has useLocalConfigurationFile: true — verify it enables without resetting patterns
  • Trigger an import that hits an API error — verify status code and details are printed

🤖 Generated with Claude Code

- Don't disable tools the local CLI doesn't support (cloud-only tools
  stay enabled when not present in the config file)
- Skip pattern reset when useLocalConfigurationFile is set — just enable
  the tool with the config file flag
- Surface structured API error details (status code, response body) on
  import failures instead of generic messages
- Add ship-it command for streamlined PR workflow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 27, 2026 10:47
@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 27, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 8 duplication

Metric Results
Duplication 8

View in Codacy

AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the tools import functionality to preserve cloud-only tools (only disabling tools supported by the local CLI), skips pattern resets when useLocalConfigurationFile is enabled, and extracts and displays structured API error details on import failures. It also introduces a new ship-it command script and updates tests. The reviewer feedback recommends adding defensive filtering when parsing local CLI tools to prevent potential runtime errors, and suggests formatting improvements to printImportErrors to avoid duplicate output and improve visual indentation.

Comment on lines +141 to +144
if (!info.tools || !Array.isArray(info.tools)) return null;
return info.tools
.filter((t: any) => t.supported)
.map((t: any) => t.id as string);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent potential runtime errors, we should defensively filter the tools returned by the local CLI. If a tool object is null/undefined or is missing a valid string id, calling resolveToolId on it later will throw a TypeError when trying to call toLowerCase() on an undefined value.

Suggested change
if (!info.tools || !Array.isArray(info.tools)) return null;
return info.tools
.filter((t: any) => t.supported)
.map((t: any) => t.id as string);
if (!info.tools || !Array.isArray(info.tools)) return null;
return info.tools
.filter((t: any) => t && t.supported && typeof t.id === "string")
.map((t: any) => t.id);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — added the typeof t.id === "string" guard to prevent a TypeError if the CLI returns a malformed tool entry.

🤖 Generated by /pr-fixup command

Comment thread src/commands/tools.ts
Comment on lines +56 to +73
function printImportErrors(failures: ImportFailure[]): void {
for (const f of failures) {
const status = f.status ? ` (${f.status})` : "";
console.log(ansis.red(`✗ ${f.tool}: ${f.error}${status}`));

if (f.details.length === 0) continue;

const shown = f.details.slice(0, MAX_ERROR_DETAILS);
for (const detail of shown) {
console.log(ansis.dim(` ${detail}`));
}
const remaining = f.details.length - shown.length;
if (remaining > 0) {
console.log(ansis.dim(` ... and ${remaining} more`));
}
}
console.log();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To make the error details visually nested under the summary header and avoid duplicating the error list, let's add indentation to the output of printImportErrors.

Suggested change
function printImportErrors(failures: ImportFailure[]): void {
for (const f of failures) {
const status = f.status ? ` (${f.status})` : "";
console.log(ansis.red(`✗ ${f.tool}: ${f.error}${status}`));
if (f.details.length === 0) continue;
const shown = f.details.slice(0, MAX_ERROR_DETAILS);
for (const detail of shown) {
console.log(ansis.dim(` ${detail}`));
}
const remaining = f.details.length - shown.length;
if (remaining > 0) {
console.log(ansis.dim(` ... and ${remaining} more`));
}
}
console.log();
}
function printImportErrors(failures: ImportFailure[]): void {
for (const f of failures) {
const status = f.status ? ` (${f.status})` : "";
console.log(ansis.red(` ${f.tool}: ${f.error}${status}`));
if (f.details.length === 0) continue;
const shown = f.details.slice(0, MAX_ERROR_DETAILS);
for (const detail of shown) {
console.log(ansis.dim(` ${detail}`));
}
const remaining = f.details.length - shown.length;
if (remaining > 0) {
console.log(ansis.dim(` ... and ${remaining} more`));
}
}
console.log();
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — I've also indented the detail lines and removed the duplicate output loop (see below). The summary header now prints first, then printImportErrors handles the detailed listing.

🤖 Generated by /pr-fixup command

Comment thread src/commands/tools.ts Outdated
Comment on lines 181 to 192
} else {
printImportErrors(result.failed);

console.log(
ansis.yellow(
`Import completed with ${result.failed.length} error(s):`,
),
);
for (const f of result.failed) {
console.log(ansis.red(` ✗ ${f.tool}: ${f.error}`));
const status = f.status ? ` (${f.status})` : "";
console.log(ansis.red(` ✗ ${f.tool}: ${f.error}${status}`));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation prints the list of failed tools twice: once with details via printImportErrors(result.failed) and once again in the summary loop. We can simplify this by printing the summary header first, and then calling the updated printImportErrors to show the indented list of errors with their details, completely removing the duplicate loop.

          } else {
            console.log(
              ansis.yellow(
                `Import completed with ${result.failed.length} error(s):`,
              ),
            );
            printImportErrors(result.failed);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the summary header now prints first, then printImportErrors handles the full detail output. The redundant loop is removed.

🤖 Generated by /pr-fixup command

Copy link
Copy Markdown

@codacy-production codacy-production Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

The PR successfully implements the preservation of cloud-only tools and improves error surfacing in the tools import command. However, the analysis shows the PR is currently not up to standards due to a significant increase in cyclomatic complexity and violations of the single responsibility principle in core utility functions. Specifically, extractErrorDetails and executeImport have grown beyond maintainable limits, and the main tools command action handler is over-encumbered with orchestration logic.

Additionally, there is a logic bug in the CLI output phase where failure details are printed twice, and several new test cases in src/utils/import-config.test.ts introduce significant code duplication (8 new clones detected). These maintainability risks and logic redundancies should be addressed before merging.

About this PR

  • The implementation introduces systemic complexity by adding heavy orchestration logic directly into command handlers and utility functions. Consider refactoring into smaller, single-purpose helper functions to maintain long-term code quality.
2 comments outside of the diff
src/commands/tools.ts

line 97 🔴 HIGH RISK
The .action() block is performing too much logic, including API orchestration, spinner management, and user prompting. Extract the logic within the 'if (opts.import !== undefined)' block into a standalone function named 'handleToolsImport'.

Try running the following prompt in your IDE agent:

Refactor the tools command action in src/commands/tools.ts. Extract the entire import logic block (lines 108-202) into a separate function called handleToolsImport to reduce the size and complexity of the command registration.

src/utils/import-config.ts

line 340 🔴 HIGH RISK
This function is handling too many distinct tasks. Extract the tool configuration loop (lines 374-432) into a helper function like 'configureResolvedTools' to keep the high-level import flow clear.

Try running the following prompt in your IDE agent:

In src/utils/import-config.ts, refactor executeImport by extracting the loop that iterates over allResolved tools into a private helper function. This helper should handle both the 'useLocalConfigurationFile' mode and the 'Pattern' mode.

Test suggestions

  • Import with tools not supported by the local CLI should categorize them as cloud-only and not disable them.
  • Import with useLocalConfigurationFile: true should skip the call to reset patterns before enabling.
  • Import with API failure should extract and display HTTP status codes and body messages/errors.
  • Import when the local CLI (codacy-analysis) is missing or fails should skip disabling tools to avoid accidental data loss.

TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback

Comment thread src/utils/import-config.ts Outdated
details: string[];
}

function extractErrorDetails(err: unknown): Pick<ImportFailure, "error" | "status" | "details"> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 HIGH RISK

The error parsing logic for ApiError is becoming overly complex. Consider extracting the body inspection into a dedicated 'parseApiErrorBody' helper to improve readability.

Try running the following prompt in your IDE agent:

Refactor extractErrorDetails in src/utils/import-config.ts to reduce cyclomatic complexity by extracting the logic that handles err.body into a separate function called parseErrorBody.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — extracted parseApiErrorBody as a dedicated helper, which brings extractErrorDetails well under the CCN limit.

🤖 Generated by /pr-fixup command

Comment on lines +553 to +617
"Conflict",
);

vi.mocked(AnalysisService.updateRepositoryToolPatterns).mockResolvedValue(undefined as any);
vi.mocked(AnalysisService.configureTool).mockRejectedValueOnce(apiError);

const config: CodacyConfig = {
version: 1,
metadata: { repositoryId: null, repositoryName: null, createdAt: "", updatedAt: "", languages: [] },
tools: [{ toolId: "ESLint", patterns: [{ patternId: "p1" }] }],
};

const preview = buildImportPreview(config, [], allTools, [], "/test/path");
const result = await executeImport("gh", "org", "repo", preview, config, allTools, mockSpinner as any);

expect(result.failed[0].status).toBe(409);
expect(result.failed[0].error).toBe("Conflict");
expect(result.failed[0].details).toEqual(["Tool is managed by coding standard 'Security'"]);
});

it("should extract details from ApiError with body.errors array", async () => {
const { ApiError } = await import("../api/client/core/ApiError");
const apiError = new ApiError(
{ method: "PUT", url: "/test" } as any,
{ url: "/test", ok: false, status: 400, statusText: "Bad Request", body: { errors: ["Pattern X not found", "Pattern Y is invalid"] } },
"Bad Request",
);

vi.mocked(AnalysisService.updateRepositoryToolPatterns).mockResolvedValue(undefined as any);
vi.mocked(AnalysisService.configureTool).mockRejectedValueOnce(apiError);

const config: CodacyConfig = {
version: 1,
metadata: { repositoryId: null, repositoryName: null, createdAt: "", updatedAt: "", languages: [] },
tools: [{ toolId: "ESLint", patterns: [{ patternId: "p1" }] }],
};

const preview = buildImportPreview(config, [], allTools, [], "/test/path");
const result = await executeImport("gh", "org", "repo", preview, config, allTools, mockSpinner as any);

expect(result.failed[0].status).toBe(400);
expect(result.failed[0].details).toEqual(["Pattern X not found", "Pattern Y is invalid"]);
});

it("should extract details from ApiError with string body", async () => {
const { ApiError } = await import("../api/client/core/ApiError");
const apiError = new ApiError(
{ method: "PUT", url: "/test" } as any,
{ url: "/test", ok: false, status: 500, statusText: "Internal Server Error", body: "Unexpected failure in tool configuration" },
"Internal Server Error",
);

vi.mocked(AnalysisService.updateRepositoryToolPatterns).mockResolvedValue(undefined as any);
vi.mocked(AnalysisService.configureTool).mockRejectedValueOnce(apiError);

const config: CodacyConfig = {
version: 1,
metadata: { repositoryId: null, repositoryName: null, createdAt: "", updatedAt: "", languages: [] },
tools: [{ toolId: "ESLint", patterns: [{ patternId: "p1" }] }],
};

const preview = buildImportPreview(config, [], allTools, [], "/test/path");
const result = await executeImport("gh", "org", "repo", preview, config, allTools, mockSpinner as any);

expect(result.failed[0].status).toBe(500);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM RISK

There is significant duplication in the new ApiError test cases. Use 'it.each' to parameterize the error body and the expected failure details, which will reduce the file size and improve maintainability.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping the test cases explicit — each tests a distinct API error shape and the inline structure makes it easy to see what's being asserted without jumping to a shared parameterization table.

🤖 Generated by /pr-fixup command

Comment thread src/commands/tools.ts Outdated
Comment on lines 182 to 192
printImportErrors(result.failed);

console.log(
ansis.yellow(
`Import completed with ${result.failed.length} error(s):`,
),
);
for (const f of result.failed) {
console.log(ansis.red(` ✗ ${f.tool}: ${f.error}`));
const status = f.status ? ` (${f.status})` : "";
console.log(ansis.red(` ✗ ${f.tool}: ${f.error}${status}`));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM RISK

The failure reporting is redundant and incorrectly ordered. printImportErrors handles the display of failures, so the manual loop at lines 189-192 is unnecessary. The summary line should appear before the details.

Try running the following prompt in your coding agent:

In src/commands/tools.ts, update printImportErrors to indent the tool name lines by 2 spaces, remove the redundant loop from lines 189-192, and move the call to printImportErrors(result.failed) from line 182 to line 188 (immediately after the summary message).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — moved the summary header before printImportErrors and removed the redundant manual loop.

🤖 Generated by /pr-fixup command

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the tools import flow to better preserve cloud-only tools, handle config-file-mode tools without resetting patterns, and expose more structured import failure details.

Changes:

  • Adds local supported-tool detection via codacy-analysis and separates cloud-only tools from tools to disable.
  • Updates import execution to skip pattern resets when useLocalConfigurationFile is enabled.
  • Adds structured API error extraction/reporting plus related tests, a changeset, and a Claude command workflow file.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/utils/import-config.ts Adds local CLI tool detection, cloud-only preview handling, config-file-mode execution, and structured import failures.
src/utils/import-config.test.ts Extends preview/import tests for cloud-only tools, config-file mode, and API error details.
src/commands/tools.ts Wires local supported-tool detection into import mode and prints structured failures.
src/commands/tools.test.ts Mocks local supported-tool detection for import command tests.
.claude/commands/ship-it.md Adds a Claude Code workflow prompt for preparing and opening PRs.
.changeset/tools-import-fixes.md Adds a patch changeset describing the import fixes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/commands/tools.ts Outdated
Comment on lines +182 to +191
printImportErrors(result.failed);

console.log(
ansis.yellow(
`Import completed with ${result.failed.length} error(s):`,
),
);
for (const f of result.failed) {
console.log(ansis.red(` ✗ ${f.tool}: ${f.error}`));
const status = f.status ? ` (${f.status})` : "";
console.log(ansis.red(` ✗ ${f.tool}: ${f.error}${status}`));
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — single output path now: summary header followed by printImportErrors.

🤖 Generated by /pr-fixup command

Comment on lines +378 to +385
if (resolved.configTool.useLocalConfigurationFile) {
// Config file mode: just enable the tool with the config file flag, no pattern changes
await AnalysisService.configureTool(
provider,
organization,
repository,
resolved.tool.uuid,
{
enabled: true,
useConfigurationFile:
resolved.configTool.useLocalConfigurationFile ?? false,
patterns: batch,
},
{ enabled: true, useConfigurationFile: true },
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the preview now differentiates: pattern-mode tools say their patterns will be replaced, while config-file-mode tools are listed separately as using their local configuration file.

🤖 Generated by /pr-fixup command

Comment on lines +137 to +138
const { stdout } = await execAsync("codacy-analysis info -f json", {
timeout: 30000,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is by design — getLocalSupportedToolIds is an optional enhancement that gracefully returns null when the CLI isn't available. When null, the import conservatively skips disabling any tools (safer than guessing). Documenting the optional dependency is a good idea but out of scope for this bug-fix PR.

🤖 Generated by /pr-fixup command

- Remove duplicate failure output: summary header prints first, then
  printImportErrors handles the full detail listing (no redundant loop)
- Extract parseApiErrorBody from extractErrorDetails to bring cyclomatic
  complexity under the Lizard CCN limit of 10
- Add defensive filtering in getLocalSupportedToolIds (typeof id check)
- Fix preview wording: pattern-mode and config-file-mode tools are now
  described separately instead of a blanket "all patterns replaced"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@alerizzo alerizzo merged commit a973363 into main May 27, 2026
4 checks passed
@alerizzo alerizzo deleted the fix/tools-import-cloud-only branch May 27, 2026 11:21
@github-actions github-actions Bot mentioned this pull request May 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants