Skip to content

Commit e21f321

Browse files
alerizzoclaude
andauthored
feat: add --matches-stack filter to the patterns command (#45)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 1e0f160 commit e21f321

10 files changed

Lines changed: 247 additions & 12 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@codacy/codacy-cloud-cli": minor
3+
---
4+
5+
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.
6+
7+
It's a tri-state flag, the same shape as `issues --false-positives`:
8+
9+
```bash
10+
codacy patterns eslint9 --matches-stack # only patterns matching the repo stack
11+
codacy patterns eslint9 --matches-stack true # same
12+
codacy patterns eslint9 --matches-stack false # only patterns that don't match
13+
codacy patterns eslint9 # unfiltered
14+
```
15+
16+
The filter applies in bulk mode too, so `--enable-all` / `--disable-all` can be scoped to the stack:
17+
18+
```bash
19+
codacy patterns eslint9 --disable-all --matches-stack false
20+
```
21+
22+
The summary printed after a bulk update still reports counts for the whole tool, not just the updated subset.

‎SPECS/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ _No pending tasks._ All commands implemented.
2525
| `finding` | `fin` | ✅ Done (CVE enrichment included) | [finding.md](commands/finding.md) |
2626
| `tools` | `tls` | ✅ Done | [tools-and-patterns.md](commands/tools-and-patterns.md) |
2727
| `tool` | `tl` | ✅ Done | [tools-and-patterns.md](commands/tools-and-patterns.md) |
28-
| `patterns` | `pats` | ✅ Done | [tools-and-patterns.md](commands/tools-and-patterns.md) |
28+
| `patterns` | `pats` | ✅ Done (--matches-stack added) | [tools-and-patterns.md](commands/tools-and-patterns.md) |
2929
| `pattern` | `pat` | ✅ Done (info mode + guards added) | [tools-and-patterns.md](commands/tools-and-patterns.md) |
3030
| `analysis` | N/A | ✅ Done | [analysis.md](commands/analysis.md) |
3131
| `json-output` | N/A | ✅ Done | [json-output.md](commands/json-output.md) |
@@ -87,3 +87,4 @@ _No pending tasks._ All commands implemented.
8787
| 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) |
8888
| 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) |
8989
| 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) |
90+
| 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) |

‎SPECS/commands/tools-and-patterns.md‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ overwritten and the API can't list or change them:
110110
codacy patterns <provider> <organization> <repository> <toolName>
111111
codacy patterns gh my-org my-repo eslint --severities Critical,High --enabled
112112
codacy patterns gh my-org my-repo eslint --output json
113+
codacy patterns gh my-org my-repo eslint --matches-stack
114+
codacy patterns gh my-org my-repo eslint --matches-stack false
113115
codacy patterns gh my-org my-repo eslint --enable-all --categories Security
114116
codacy patterns gh my-org my-repo eslint --disable-all --severities Minor
115117
```
@@ -126,6 +128,7 @@ codacy patterns gh my-org my-repo eslint --disable-all --severities Minor
126128
| `--enabled` | `-e` | Show only enabled patterns (list mode only) |
127129
| `--disabled` | `-D` | Show only disabled patterns (list mode only) |
128130
| `--recommended` | `-r` | Show only recommended patterns |
131+
| `--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 |
129132
| `--enable-all` | `-E` | Bulk enable matching patterns |
130133
| `--disable-all` | `-X` | Bulk disable matching patterns |
131134

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

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

163-
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.
166+
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.
164167

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

170175
## Tests
171176

172-
File: `src/commands/patterns.test.ts` — 27 tests.
177+
File: `src/commands/patterns.test.ts` — 35 tests.
173178

174179
---
175180

‎SPECS/repository-tokens.md‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,26 @@ depends on it). Both flag and env values are trimmed.
4545
> hardcode it; if the backend adds an operation, a guard here will still refuse
4646
> it. Source: Linear project *"Project token works in selected API v3
4747
> (+expiration)"*.
48-
49-
Codacy accepts a repository token on **exactly** these 13 operations. Everywhere
48+
>
49+
> **Last verified against API `57.4.17`** (2026-09-09). Since `57.4.x` the spec
50+
> declares the `ProjectTokenAuth` security scheme (`project-token` header) on
51+
> each supported operation, so the whitelist is now machine-checkable:
52+
>
53+
> ```bash
54+
> python3 -c "
55+
> import re
56+
> cur=None; out=[]
57+
> for l in open('api-v3/api-swagger.yaml'):
58+
> m=re.match(r'\s*operationId:\s*(\S+)', l)
59+
> if m: cur=m.group(1)
60+
> if 'ProjectTokenAuth' in l and cur and cur not in out: out.append(cur)
61+
> print(len(out)); print('\n'.join(sorted(out)))"
62+
> ```
63+
>
64+
> On `57.3.9` (the previously pinned build) the scheme was not declared
65+
> anywhere, which is why this table was maintained by hand.
66+
67+
Codacy accepts a repository token on **exactly** these 14 operations. Everywhere
5068
else it is rejected as if no token had been sent.
5169
5270
| operationId | Method | Used by this CLI |
@@ -64,6 +82,7 @@ else it is rejected as if no token had been sent.
6482
| `getRepositoryLanguages` | GET | — |
6583
| `getRepository` | GET | — |
6684
| `listIgnoredFiles` | GET | — |
85+
| `searchAiInventoryCategories` | POST | — |
6786
6887
`ToolsService.listTools` / `listPatterns` / `getPattern` are declared
6988
`security: []` in the spec — unauthenticated, so they work with any token or

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"prepublishOnly": "npm run update-api && npm run build",
2727
"start": "npx ts-node src/index.ts",
2828
"start:dist": "node dist/index.js",
29-
"fetch-api": "curl https://artifacts.codacy.com/api/codacy-api/57.3.9/apiv3-bundled.yaml -o ./api-v3/api-swagger.yaml --create-dirs",
29+
"fetch-api": "curl https://artifacts.codacy.com/api/codacy-api/57.4.17/apiv3-bundled.yaml -o ./api-v3/api-swagger.yaml --create-dirs",
3030
"generate-api": "rm -rf ./src/api/client && openapi --input ./api-v3/api-swagger.yaml --output ./src/api/client --useUnionTypes --indent 2 --client fetch",
3131
"update-api": "npm run fetch-api && npm run generate-api",
3232
"check-types": "tsc --noEmit"

‎src/commands/issues.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
formatCount,
2525
} from "../utils/formatting";
2626
import { sanitizeText } from "../utils/sanitize";
27+
import { parseBooleanOption } from "../utils/options";
2728
import { AnalysisService } from "../api/client/services/AnalysisService";
2829
import { ToolsService } from "../api/client/services/ToolsService";
2930
import { Tool } from "../api/client/models/Tool";
@@ -140,10 +141,6 @@ function normalizeCategory(input: string): string {
140141
return CATEGORY_NORMALIZE[key] ?? input;
141142
}
142143

143-
function parseBooleanOption(value: string): boolean {
144-
return value.toLowerCase() !== "false";
145-
}
146-
147144
function printIssuesList(issues: CommitIssue[], total: number): void {
148145
printSection("Issues", total, "issue");
149146
if (issues.length === 0) {

‎src/commands/patterns.test.ts‎

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ describe("patterns command", () => {
186186
undefined,
187187
undefined,
188188
undefined,
189+
undefined,
189190
);
190191

191192
const output = getAllOutput();
@@ -295,6 +296,7 @@ describe("patterns command", () => {
295296
"sql injection",
296297
true,
297298
undefined,
299+
undefined,
298300
);
299301
});
300302

@@ -324,6 +326,7 @@ describe("patterns command", () => {
324326
undefined,
325327
undefined,
326328
undefined,
329+
undefined,
327330
);
328331
});
329332

@@ -352,9 +355,69 @@ describe("patterns command", () => {
352355
undefined,
353356
undefined,
354357
true,
358+
undefined,
355359
);
356360
});
357361

362+
// `--matches-stack [value]` is a tri-state: the bare flag and an explicit
363+
// `true` both opt in, `false` opts out, and omitting it sends nothing.
364+
describe("--matches-stack", () => {
365+
const expectMatchesStack = (value: boolean | undefined) =>
366+
expect(AnalysisService.listRepositoryToolPatterns).toHaveBeenCalledWith(
367+
"gh",
368+
"test-org",
369+
"test-repo",
370+
"uuid-eslint",
371+
undefined,
372+
undefined,
373+
undefined,
374+
undefined,
375+
undefined,
376+
undefined,
377+
undefined,
378+
value,
379+
);
380+
381+
const run = async (...extraArgs: string[]) => {
382+
const program = createProgram();
383+
await program.parseAsync([
384+
"node",
385+
"test",
386+
"patterns",
387+
"gh",
388+
"test-org",
389+
"test-repo",
390+
"eslint",
391+
...extraArgs,
392+
]);
393+
};
394+
395+
it("sends matchesStack=true for the bare flag", async () => {
396+
await run("--matches-stack");
397+
expectMatchesStack(true);
398+
});
399+
400+
it("sends matchesStack=true for --matches-stack true", async () => {
401+
await run("--matches-stack", "true");
402+
expectMatchesStack(true);
403+
});
404+
405+
it("sends matchesStack=false for --matches-stack false", async () => {
406+
await run("--matches-stack", "false");
407+
expectMatchesStack(false);
408+
});
409+
410+
it("sends nothing when the flag is omitted", async () => {
411+
await run();
412+
expectMatchesStack(undefined);
413+
});
414+
415+
it("accepts the -k short flag", async () => {
416+
await run("-k", "false");
417+
expectMatchesStack(false);
418+
});
419+
});
420+
358421
it("should show ☑️ icon for patterns enforced by a coding standard", async () => {
359422
const program = createProgram();
360423
await program.parseAsync([
@@ -593,6 +656,7 @@ describe("patterns command", () => {
593656
undefined,
594657
undefined,
595658
undefined,
659+
undefined,
596660
);
597661
expect(AnalysisService.toolPatternsOverview).toHaveBeenCalledWith(
598662
"gh",
@@ -630,6 +694,7 @@ describe("patterns command", () => {
630694
undefined,
631695
undefined,
632696
undefined,
697+
undefined,
633698
);
634699
});
635700

@@ -671,6 +736,66 @@ describe("patterns command", () => {
671736
"security",
672737
"injection",
673738
true,
739+
undefined,
740+
);
741+
});
742+
743+
// `matchesStack` is the 12th positional argument of
744+
// updateRepositoryToolPatterns, so the bulk path filters on the stack too.
745+
it("should pass --matches-stack to updateRepositoryToolPatterns", async () => {
746+
const program = createProgram();
747+
await program.parseAsync([
748+
"node",
749+
"test",
750+
"patterns",
751+
"gh",
752+
"test-org",
753+
"test-repo",
754+
"eslint",
755+
"--enable-all",
756+
"--matches-stack",
757+
"false",
758+
]);
759+
760+
expect(
761+
AnalysisService.updateRepositoryToolPatterns,
762+
).toHaveBeenCalledWith(
763+
"gh",
764+
"test-org",
765+
"test-repo",
766+
"uuid-eslint",
767+
{ enabled: true },
768+
undefined,
769+
undefined,
770+
undefined,
771+
undefined,
772+
undefined,
773+
undefined,
774+
false,
775+
);
776+
});
777+
778+
it("should not scope the post-update overview to --matches-stack", async () => {
779+
const program = createProgram();
780+
await program.parseAsync([
781+
"node",
782+
"test",
783+
"patterns",
784+
"gh",
785+
"test-org",
786+
"test-repo",
787+
"eslint",
788+
"--disable-all",
789+
"--matches-stack",
790+
]);
791+
792+
// The summary counts ("N/M patterns now enabled") describe the whole
793+
// tool, so the overview call deliberately carries no filters.
794+
expect(AnalysisService.toolPatternsOverview).toHaveBeenCalledWith(
795+
"gh",
796+
"test-org",
797+
"test-repo",
798+
"uuid-eslint",
674799
);
675800
});
676801

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

@@ -841,6 +966,7 @@ describe("patterns command", () => {
841966
undefined,
842967
undefined,
843968
undefined,
969+
undefined,
844970
);
845971
});
846972
});

0 commit comments

Comments
 (0)