Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/skip-standard-locked-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codacy/codacy-cloud-cli": patch
---

`tools --import` no longer sends disables for tools and patterns enforced by a coding standard — it drops them from the import set up front (using `enabledBy` from the tools/patterns listings) instead of letting the server reject each one with a 409. These are now reported in a new `skipped[]` list, shown in the preview, the summary line, and `--output json`.
1 change: 1 addition & 0 deletions SPECS/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,5 @@ _No pending tasks._ All commands implemented.
| 2026-07-28 | (OD-296, findings side) `SrmItem` gained its own `advisoryInformation` field server-side (bumped pinned API `57.3.0` → `57.3.9`), closing the gap noted on 2026-07-24. `findings` (list) now shows the same compact "Vulnerable functions: fn1, fn2 (+N more)" line as `issues`, via the newly-exported `summarizeFunctions`. `finding` (detail) shows the full `printAdvisoryBlock` — but only when there's no linked Codacy issue, since `printIssueCodeContext` already renders the equivalent block from `issue.advisoryInformation` in that case; this is what makes vulnerable functions visible for SCA/dependency findings (and any other non-Codacy-source finding) that have no linked issue to borrow it from at all. Added to both commands' JSON `pickDeep` whitelists (6 new tests, 494 total) |
| 2026-07-28 | (OD-378) New `pull-requests` (`prs`) command — the plural counterpart to `pull-request`, listing PRs for a repository with the same analysis-gated table columns as `repository`'s "Open Pull Requests" section (reuses `buildGateStatus`/`formatStandards`/`formatPrIssues`/`formatPrCoverage`/`formatDelta`). `--search-text`/`-q` and `--branch`/`-b` map to the API's `textQuery`/`targetBranch` params added in OD-376; the classification param (`search`, Merged vs. last-updated) is deliberately not exposed — different axis, out of scope. `[provider] [org] [repo]` auto-detect via `resolveRepoArgs`, paginate-to-`--limit` loop matching `findings`. Registered in `src/index.ts` (10 new tests, 516 total) |
| 2026-07-30 | (OD-378, review follow-up) `pull-requests` table polish + a real data bug. **Bug:** Complexity rendered as "no data" on every PR because the API omits the flat top-level `deltaComplexity` and only returns `quality.deltaComplexity` (while still sending a top-level `deltaClonesCount`) — new shared `prQualityMetric(pr, key)` in `utils/formatting.ts` reads the nested `quality` value first and falls back to the flat field; also applied to `repository`'s Open PR table and `pull-request`'s Analysis section, which had the same bug. **Layout:** `✓` moved to the first column; metric order now matches `repositories` (issues → complexity → duplication → coverage); the Coverage column is dropped entirely when no listed PR has a coverage value (new `hasAnyPrCoverage()` — repos without coverage return `diffCoverage.cause` and no numbers on any PR); missing metric values now render as a dim `-` instead of `N/A` in `formatDelta`/`formatPrCoverage`/`formatPrIssues`, matching `formatStandards`/`formatCountCell`/`formatCoverageCell`; and a zero issue count renders as a bare `0` rather than `+0`/`-0` (`-0` read as a negative), matching what `pull-request`'s Files table and `formatDelta` already did. **JSON:** added `quality.resultReasons`/`coverage.resultReasons` (Codacy review suggestion — they drive the per-metric gate coloring, so consumers need them to see which gates passed/failed) plus the `quality.*` metric mirrors the table actually renders (23 new tests, 544 total) |
| 2026-09-03 | `tools --import` now skips standard-enforced tools/patterns instead of trying (and failing) to disable them: `buildImportPreview` drops locked tools from the disable set and fetches enabled patterns per reconfigured tool, reporting each as an `ImportSkip` in a new `skipped[]` (printed in the preview's coding-standards warning, and in `--output json`); `--force` bypasses all of this since standards are unlinked first. |
| 2026-08-11 | (OD-489) Repository (project) token support. New `--repository-token <token>` on every command (plus `CODACY_PROJECT_TOKEN`), sent as the `project-token` header; account tokens keep `api-token`. `src/utils/auth.ts` rewritten around a `RemoteAuth` discriminated union carrying both kind and source, replacing `checkApiToken()` with `resolveAuth(this)` / `resolveAccountAuth(this, why)` / `requireAccountToken(...)` / `fetchIfAccountToken(...)`. Precedence matches `codacy-analysis` exactly — flag > `CODACY_PROJECT_TOKEN` > `CODACY_API_TOKEN` > stored login — so `vitest.config.mts` now blanks `CODACY_PROJECT_TOKEN` (it outranks the account token and is exported job-wide by the coverage reporter, so tests would otherwise depend on the developer's shell). Codacy whitelists only 13 operations for repository tokens, so `tool`/`patterns`/`pattern` work unchanged, `issues` (incl. `--overview`) and `tools --import` work, and the 9 account-only commands plus `repository`'s 6 management flags, `issues --ignore`/`--ignored`, and `tools --import --force` (only when standards exist) **fail fast before any request** with a message naming the operation, the reason, and where the token came from. `repository`'s dashboard skips the two non-whitelisted calls: the table keeps the "Open Pull Requests" header with an explanatory line, and JSON keeps `pullRequests: []` (so `jq '.pullRequests[]'` still works) plus an additive `unavailable: ["pullRequests"]` — under an account token the payload is byte-identical. Also added the long-missing `.catch()` on the PR call so an account token lacking PR access degrades instead of losing the whole dashboard, and fixed `login`'s 401 message, which told repository-token users their token was "invalid" when it is rejected by `/user` by design. New `SPECS/repository-tokens.md` (whitelist + matrix, re-verify on every `npm run update-api`) and `SPECS/missing-endpoints.md` (ranked gaps for follow-up Linear tasks) (40 new tests, 606 total) |
2 changes: 1 addition & 1 deletion SPECS/repository-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ else it is rejected as if no token had been sent.
| operationId | Method | Used by this CLI |
|---|---|---|
| `listRepositoryTools` | GET | `tools`, `tool`, `patterns`, `pattern`, `issues -O` |
| `listRepositoryToolPatterns` | GET | `patterns`, `pattern`, `issues -O` |
| `listRepositoryToolPatterns` | GET | `patterns`, `pattern`, `issues -O`, `tools --import` |
| `getRepositoryWithAnalysis` | GET | `repository`, `tools --import` |
| `issuesOverview` | POST | `repository`, `issues -O` |
| `searchRepositoryIssues` | POST | `issues` |
Expand Down
10 changes: 10 additions & 0 deletions src/commands/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,16 @@ match. Modify-mode refusals (`pattern --enable/--disable/--parameter`, and
`patterns --enable-all/--disable-all`) print the reason and `process.exit(1)`;
info/list displays print a notice and exit 0.

`tools --import` applies the same coding-standard awareness instead of refusing:
the bulk pattern reset (`updateRepositoryToolPatterns({enabled: false})`) and
`configureTool` silently leave standard-enforced tools/patterns enabled
server-side, so `buildImportPreview` drops locked tools from the disable set
and fetches enabled patterns per reconfigured tool, reporting each as an
`ImportSkip` in `preview.skipped` (shown in the preview and in `--output json`)
rather than re-enabling them. `--force` bypasses all of it — it unlinks
standards first in `executeImport`, so nothing is locked by the time tools are
reconfigured.

## Reanalyze and wait (`utils/reanalyze-wait.ts`)

Shared by the `repository` and `pull-request` `--reanalyze-and-wait` (`-w`) modes.
Expand Down
88 changes: 88 additions & 0 deletions src/commands/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,10 @@ describe("tools command", () => {
fs.writeFileSync(tmpConfigPath, configContent);
vi.mocked(AnalysisService.updateRepositoryToolPatterns).mockResolvedValue(undefined as any);
vi.mocked(AnalysisService.configureTool).mockResolvedValue(undefined as any);
vi.mocked(AnalysisService.listRepositoryToolPatterns).mockResolvedValue({
data: [],
pagination: undefined,
} as any);
vi.spyOn(importConfig, "fetchAllTools").mockResolvedValue([
{
uuid: "uuid-eslint",
Expand Down Expand Up @@ -417,6 +421,86 @@ describe("tools command", () => {
const output = getAllOutput();
expect(output).toContain("error");
});

it("should print only the JSON object to stdout with --output json", async () => {
const program = createProgram();
await program.parseAsync([
"node", "test", "--output", "json", "tools", "gh", "test-org", "test-repo",
"--import", tmpConfigPath, "-y",
]);

const calls = (console.log as ReturnType<typeof vi.fn>).mock.calls;
expect(calls).toHaveLength(1);
const parsed = JSON.parse(calls[0][0] as string);
expect(parsed).toHaveProperty("succeeded");
expect(parsed).toHaveProperty("failed");
expect(parsed).toHaveProperty("skipped");
});

// ── Standard-enforced skips ─────────────────────────────────────────

/** A tool enabled by a coding standard, not present in the config file. */
function mockStandardLockedCheckov() {
vi.mocked(AnalysisService.listRepositoryTools).mockResolvedValue({
data: [
{
uuid: "uuid-checkov",
name: "Checkov",
isClientSide: false,
settings: {
isEnabled: true,
followsStandard: true,
isCustom: false,
hasConfigurationFile: false,
usesConfigurationFile: false,
enabledBy: [{ id: 1, name: "Security" }],
},
},
],
pagination: undefined,
} as any);
vi.spyOn(importConfig, "fetchAllTools").mockResolvedValue([
{ uuid: "uuid-eslint", name: "ESLint", shortName: "eslint", prefix: "ESLint_", languages: [], clientSide: false, standalone: false, configurable: true },
{ uuid: "uuid-checkov", name: "Checkov", shortName: "checkov", languages: [], clientSide: false, standalone: false, configurable: true },
] as any);
vi.spyOn(importConfig, "getLocalSupportedToolIds").mockResolvedValue(["ESLint", "checkov"]);
}

it("should print a skipped block in the preview and count it in the summary", async () => {
mockStandardLockedCheckov();

const program = createProgram();
await program.parseAsync([
"node", "test", "tools", "gh", "test-org", "test-repo",
"--import", tmpConfigPath, "-y",
]);

const output = getAllOutput();
expect(output).toContain("Skipped (enforced by coding standard):");
expect(output).toContain("Checkov (Security)");
expect(output).toContain("1 skipped.");
// Never sent a disable for the standard-locked tool.
expect(AnalysisService.configureTool).not.toHaveBeenCalledWith(
"gh", "test-org", "test-repo", "uuid-checkov", { enabled: false },
);
});

it("should include skipped in --output json", async () => {
mockStandardLockedCheckov();

const program = createProgram();
await program.parseAsync([
"node", "test", "--output", "json", "tools", "gh", "test-org", "test-repo",
"--import", tmpConfigPath, "-y",
]);

const calls = (console.log as ReturnType<typeof vi.fn>).mock.calls;
const jsonCall = calls.map((c) => c[0]).find((c) => typeof c === "string" && c.trim().startsWith("{"));
const parsed = JSON.parse(jsonCall as string);
expect(parsed.skipped).toEqual([
{ tool: "Checkov", standards: ["Security"], reason: "enforced by coding standard" },
]);
});
});

describe("auto-detect from git remote", () => {
Expand Down Expand Up @@ -453,6 +537,10 @@ describe("tools command", () => {
undefined as any,
);
vi.mocked(AnalysisService.configureTool).mockResolvedValue(undefined as any);
vi.mocked(AnalysisService.listRepositoryToolPatterns).mockResolvedValue({
data: [],
pagination: undefined,
} as any);
vi.spyOn(importConfig, "fetchAllTools").mockResolvedValue([
{
uuid: "uuid-eslint",
Expand Down
28 changes: 23 additions & 5 deletions src/commands/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,21 +188,31 @@ Examples:

spinner.stop();

const isJson = getOutputFormat(this) === "json";

// Build and display preview
const preview = buildImportPreview(
const preview = await buildImportPreview(
provider,
organization,
repository,
config,
repoToolsResponse.data,
allTools,
repoResponse.data.repository.standards,
resolvedPath,
localToolIds,
Boolean(opts.force),
);

guardForceUnlink(auth, preview.standards.length, Boolean(opts.force));

printImportPreview(preview, repository, Boolean(opts.force), {
canUnlinkStandards: auth.kind === "account-token",
});
printImportPreview(
preview,
repository,
Boolean(opts.force),
{ canUnlinkStandards: auth.kind === "account-token" },
isJson ? console.error : console.log,
);

// Confirm
if (!opts.skipApproval) {
Expand All @@ -215,7 +225,7 @@ Examples:
}
}

console.log();
if (!isJson) console.log();
const execSpinner = ora("Applying configuration...").start();
const result = await executeImport(
provider,
Expand All @@ -230,6 +240,11 @@ Examples:

execSpinner.stop();

if (isJson) {
printJson(result);
return;
}

if (result.failed.length === 0) {
console.log(
`${ansis.green("✓")} Configuration imported successfully.`,
Expand All @@ -249,6 +264,9 @@ Examples:
);
}
}
if (result.skipped.length > 0) {
console.log(ansis.dim(` ${result.skipped.length} skipped.`));
}
return;
}

Expand Down
Loading
Loading