Skip to content
Merged
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
22 changes: 22 additions & 0 deletions .changeset/patterns-matches-stack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@codacy/codacy-cloud-cli": minor
---

New `-k, --matches-stack [value]` filter on `codacy patterns`, which narrows a tool's code patterns to those that do (or don't) match the repository's detected stack.

It's a tri-state flag, the same shape as `issues --false-positives`:

```bash
codacy patterns eslint9 --matches-stack # only patterns matching the repo stack
codacy patterns eslint9 --matches-stack true # same
codacy patterns eslint9 --matches-stack false # only patterns that don't match
codacy patterns eslint9 # unfiltered
```

The filter applies in bulk mode too, so `--enable-all` / `--disable-all` can be scoped to the stack:

```bash
codacy patterns eslint9 --disable-all --matches-stack false
```

The summary printed after a bulk update still reports counts for the whole tool, not just the updated subset.
3 changes: 2 additions & 1 deletion SPECS/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ _No pending tasks._ All commands implemented.
| `finding` | `fin` | ✅ Done (CVE enrichment included) | [finding.md](commands/finding.md) |
| `tools` | `tls` | ✅ Done | [tools-and-patterns.md](commands/tools-and-patterns.md) |
| `tool` | `tl` | ✅ Done | [tools-and-patterns.md](commands/tools-and-patterns.md) |
| `patterns` | `pats` | ✅ Done | [tools-and-patterns.md](commands/tools-and-patterns.md) |
| `patterns` | `pats` | ✅ Done (--matches-stack added) | [tools-and-patterns.md](commands/tools-and-patterns.md) |
| `pattern` | `pat` | ✅ Done (info mode + guards added) | [tools-and-patterns.md](commands/tools-and-patterns.md) |
| `analysis` | N/A | ✅ Done | [analysis.md](commands/analysis.md) |
| `json-output` | N/A | ✅ Done | [json-output.md](commands/json-output.md) |
Expand Down Expand Up @@ -87,3 +87,4 @@ _No pending tasks._ All commands implemented.
| 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-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) |
| 2026-09-07 | HTTP/HTTPS proxy + TLS support (issue #40). Node's global `fetch` — used by the generated client and the MITRE CVE lookup in `commands/finding.ts` — ignores `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`, so the CLI was unusable behind a corporate proxy. Rather than reimplement it, this delegates to `configureProxy()` from `@codacy/tooling` (pinned `0.1.0` → `0.23.0`, the same function `analysis-cli` calls), which installs a global `undici` dispatcher doing per-request protocol + `NO_PROXY` routing, bare `host:port` normalization, and `SSL_CERT_FILE`/`NODE_EXTRA_CA_CERTS` CA loading. New `src/utils/proxy.ts` is a ~4-line seam — `configureProxyFromEnv()` calls it and routes its deliberate fail-loud throw (unreadable/non-PEM CA bundle) into `handleError()`, giving red `Error: <message>` and exit 1 like every other failure here; `analysis-cli` exits 2 because it has a documented exit-code scheme, which this CLI does not. Called at the top of `src/index.ts`, above `OpenAPI.BASE` (ordering is only constrained to precede `program.parse`, since the dispatcher is resolved per request). Kept top-level rather than in the `preAction` hook so a typo'd `SSL_CERT_FILE` fails even on `--version`. Deliberately zero-argument: env is the sole input, which is what keeps parity exact. Superseded external PR #39, which hand-rolled the same feature with `undici@8.10.1` — that requires Node ≥ 22.19.0 against this package's `engines: ">=20"`, so `require("undici")` threw at module load and the CLI would not start at all on any Node 20.x; tooling's `undici@^6.21.0` supports Node ≥ 18.17. That regression passed CI, so `ci.yml` gained a smoke step running the built entry point (plain, with `HTTPS_PROXY`, and with a bad `SSL_CERT_FILE` expected to fail) — previously nothing executed `src/index.ts`, since every command test builds a bare `new Command()`. Upstream owns the proxy semantics and their 24 tests, so only the seam is tested here. Pinned exactly rather than with a caret: for a pre-1.0 package `^0.22.0` spans patches only (`>=0.22.0 <0.23.0-0`), so a caret would have bought silent patch drift against a dependency this repo has no proxy coverage for, without ever picking up a minor. Two findings from this work were fixed upstream and taken here via 0.23.0 — `undici` now loads lazily behind `configureProxy`'s early-out (an unproxied `--version` went from +27 ms to +0 ms against a `main` build), and a malformed proxy URL now fails with `Invalid HTTPS_PROXY value "...": <reason>`, naming the setting and redacting any credentials instead of surfacing a bare `Invalid URL` (4 new tests, 614 total) |
| 2026-09-09 | New `-k, --matches-stack [value]` filter on `patterns`, surfacing the API's `matchesStack` query param (filter a tool's code patterns by whether they match the repository's detected stack). Tri-state, matching the existing `issues --false-positives`: the bare flag or `true` sends `matchesStack=true`, `false` sends `matchesStack=false`, omitting it sends nothing — read explicitly rather than by truthiness so an explicit `false` stays distinct from "not requested". Applies in **both** list mode (`listRepositoryToolPatterns`) and bulk mode (`updateRepositoryToolPatterns`), like every other filter; the post-update `toolPatternsOverview` call deliberately stays unfiltered, since its counts describe the whole tool rather than the updated subset. The shared tri-state coercion `parseBooleanOption` moved out of `issues.ts` into a new `utils/options.ts` (+ tests) and is now imported by both commands. **Required an API bump: pinned `57.3.9` → `57.4.17`** (`matchesStack` first ships in `57.4.14`; `57.4.17` is the latest published build). The spec delta is purely additive — 2 unused new operations, 4 new schemas, `stackTagsFilterParam` on `listOrganizationRepositories` (unused; the CLI calls `...WithAnalysis`) — but `matchesStack` is inserted *mid-signature* on `listRepositoryToolPatterns` (arg 12, before `sort`), so every full-positional-arg assertion in `patterns.test.ts` gained a trailing `undefined`; `pattern.ts`/`issues.ts` stop at `search` (arg 9) and were unaffected. `SPECS/repository-tokens.md` re-verified: `57.4.x` now declares the `ProjectTokenAuth` scheme in the spec (it was absent in `57.3.9`), making the whitelist machine-checkable, and it is **14** operations, not 13 — the addition is `searchAiInventoryCategories`, unused here. `patterns` stays fully whitelisted, so no new token guard (11 new tests, 625 total) |
11 changes: 8 additions & 3 deletions SPECS/commands/tools-and-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ overwritten and the API can't list or change them:
codacy patterns <provider> <organization> <repository> <toolName>
codacy patterns gh my-org my-repo eslint --severities Critical,High --enabled
codacy patterns gh my-org my-repo eslint --output json
codacy patterns gh my-org my-repo eslint --matches-stack
codacy patterns gh my-org my-repo eslint --matches-stack false
codacy patterns gh my-org my-repo eslint --enable-all --categories Security
codacy patterns gh my-org my-repo eslint --disable-all --severities Minor
```
Expand All @@ -126,6 +128,7 @@ codacy patterns gh my-org my-repo eslint --disable-all --severities Minor
| `--enabled` | `-e` | Show only enabled patterns (list mode only) |
| `--disabled` | `-D` | Show only disabled patterns (list mode only) |
| `--recommended` | `-r` | Show only recommended patterns |
| `--matches-stack [value]` | `-k` | Filter by whether patterns match the repository stack. Tri-state: the bare flag or `true` sends `matchesStack=true`, `false` sends `matchesStack=false`, omitting it sends nothing |
| `--enable-all` | `-E` | Bulk enable matching patterns |
| `--disable-all` | `-X` | Bulk disable matching patterns |

Expand Down Expand Up @@ -160,16 +163,18 @@ Shows pagination warning if more than 100 results exist.

### Bulk update mode (`--enable-all` / `--disable-all`)

Enables or disables all patterns matching the applied filters (languages, categories, severities, tags, search, recommended). The `--enabled`/`--disabled` filter is not used in bulk update mode since it would be redundant. `--enable-all` and `--disable-all` are mutually exclusive.
Enables or disables all patterns matching the applied filters (languages, categories, severities, tags, search, recommended, matches-stack). The `--enabled`/`--disabled` filter is not used in bulk update mode since it would be redundant. `--enable-all` and `--disable-all` are mutually exclusive.

After the update, fetches the tool patterns overview and shows a summary:
After the update, fetches the tool patterns overview and shows a summary. The
overview call deliberately carries **no** filters — including `--matches-stack`
— because the counts describe the whole tool, not the updated subset:
```
✔ Enabled matching ESLint patterns. 120/200 patterns now enabled.
```

## Tests

File: `src/commands/patterns.test.ts` — 27 tests.
File: `src/commands/patterns.test.ts` — 35 tests.

---

Expand Down
23 changes: 21 additions & 2 deletions SPECS/repository-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,26 @@ depends on it). Both flag and env values are trimmed.
> hardcode it; if the backend adds an operation, a guard here will still refuse
> it. Source: Linear project *"Project token works in selected API v3
> (+expiration)"*.

Codacy accepts a repository token on **exactly** these 13 operations. Everywhere
>
> **Last verified against API `57.4.17`** (2026-09-09). Since `57.4.x` the spec
> declares the `ProjectTokenAuth` security scheme (`project-token` header) on
> each supported operation, so the whitelist is now machine-checkable:
>
> ```bash
> python3 -c "
> import re
> cur=None; out=[]
> for l in open('api-v3/api-swagger.yaml'):
> m=re.match(r'\s*operationId:\s*(\S+)', l)
> if m: cur=m.group(1)
> if 'ProjectTokenAuth' in l and cur and cur not in out: out.append(cur)
> print(len(out)); print('\n'.join(sorted(out)))"
> ```
>
> On `57.3.9` (the previously pinned build) the scheme was not declared
> anywhere, which is why this table was maintained by hand.

Codacy accepts a repository token on **exactly** these 14 operations. Everywhere
else it is rejected as if no token had been sent.

| operationId | Method | Used by this CLI |
Expand All @@ -64,6 +82,7 @@ else it is rejected as if no token had been sent.
| `getRepositoryLanguages` | GET | — |
| `getRepository` | GET | — |
| `listIgnoredFiles` | GET | — |
| `searchAiInventoryCategories` | POST | — |

`ToolsService.listTools` / `listPatterns` / `getPattern` are declared
`security: []` in the spec — unauthenticated, so they work with any token or
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,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/57.3.9/apiv3-bundled.yaml -o ./api-v3/api-swagger.yaml --create-dirs",
"fetch-api": "curl https://artifacts.codacy.com/api/codacy-api/57.4.17/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"
Expand Down
5 changes: 1 addition & 4 deletions src/commands/issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
formatCount,
} from "../utils/formatting";
import { sanitizeText } from "../utils/sanitize";
import { parseBooleanOption } from "../utils/options";
import { AnalysisService } from "../api/client/services/AnalysisService";
import { ToolsService } from "../api/client/services/ToolsService";
import { Tool } from "../api/client/models/Tool";
Expand Down Expand Up @@ -140,10 +141,6 @@ 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");
if (issues.length === 0) {
Expand Down
128 changes: 127 additions & 1 deletion src/commands/patterns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ describe("patterns command", () => {
undefined,
undefined,
undefined,
undefined,
);

const output = getAllOutput();
Expand Down Expand Up @@ -295,6 +296,7 @@ describe("patterns command", () => {
"sql injection",
true,
undefined,
undefined,
);
});

Expand Down Expand Up @@ -324,6 +326,7 @@ describe("patterns command", () => {
undefined,
undefined,
undefined,
undefined,
);
});

Expand Down Expand Up @@ -352,9 +355,69 @@ describe("patterns command", () => {
undefined,
undefined,
true,
undefined,
);
});

// `--matches-stack [value]` is a tri-state: the bare flag and an explicit
// `true` both opt in, `false` opts out, and omitting it sends nothing.
describe("--matches-stack", () => {
const expectMatchesStack = (value: boolean | undefined) =>
expect(AnalysisService.listRepositoryToolPatterns).toHaveBeenCalledWith(
"gh",
"test-org",
"test-repo",
"uuid-eslint",
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
value,
);

const run = async (...extraArgs: string[]) => {
const program = createProgram();
await program.parseAsync([
"node",
"test",
"patterns",
"gh",
"test-org",
"test-repo",
"eslint",
...extraArgs,
]);
};

it("sends matchesStack=true for the bare flag", async () => {
await run("--matches-stack");
expectMatchesStack(true);
});

it("sends matchesStack=true for --matches-stack true", async () => {
await run("--matches-stack", "true");
expectMatchesStack(true);
});

it("sends matchesStack=false for --matches-stack false", async () => {
await run("--matches-stack", "false");
expectMatchesStack(false);
});

it("sends nothing when the flag is omitted", async () => {
await run();
expectMatchesStack(undefined);
});

it("accepts the -k short flag", async () => {
await run("-k", "false");
expectMatchesStack(false);
});
});

it("should show ☑️ icon for patterns enforced by a coding standard", async () => {
const program = createProgram();
await program.parseAsync([
Expand Down Expand Up @@ -593,6 +656,7 @@ describe("patterns command", () => {
undefined,
undefined,
undefined,
undefined,
);
expect(AnalysisService.toolPatternsOverview).toHaveBeenCalledWith(
"gh",
Expand Down Expand Up @@ -630,6 +694,7 @@ describe("patterns command", () => {
undefined,
undefined,
undefined,
undefined,
);
});

Expand Down Expand Up @@ -671,6 +736,66 @@ describe("patterns command", () => {
"security",
"injection",
true,
undefined,
);
});

// `matchesStack` is the 12th positional argument of
// updateRepositoryToolPatterns, so the bulk path filters on the stack too.
it("should pass --matches-stack to updateRepositoryToolPatterns", async () => {
const program = createProgram();
await program.parseAsync([
"node",
"test",
"patterns",
"gh",
"test-org",
"test-repo",
"eslint",
"--enable-all",
"--matches-stack",
"false",
]);

expect(
AnalysisService.updateRepositoryToolPatterns,
).toHaveBeenCalledWith(
"gh",
"test-org",
"test-repo",
"uuid-eslint",
{ enabled: true },
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
false,
);
});

it("should not scope the post-update overview to --matches-stack", async () => {
const program = createProgram();
await program.parseAsync([
"node",
"test",
"patterns",
"gh",
"test-org",
"test-repo",
"eslint",
"--disable-all",
"--matches-stack",
]);

// The summary counts ("N/M patterns now enabled") describe the whole
// tool, so the overview call deliberately carries no filters.
expect(AnalysisService.toolPatternsOverview).toHaveBeenCalledWith(
"gh",
"test-org",
"test-repo",
"uuid-eslint",
);
});

Expand Down Expand Up @@ -740,7 +865,7 @@ describe("patterns command", () => {
expect(call[4]).toEqual({ enabled: true });
// The enabled filter should not be passed to bulk update
// updateRepositoryToolPatterns has no enabled query param
expect(call).toHaveLength(11);
expect(call).toHaveLength(12);
});
});

Expand Down Expand Up @@ -841,6 +966,7 @@ describe("patterns command", () => {
undefined,
undefined,
undefined,
undefined,
);
});
});
Expand Down
Loading
Loading