From ad25e934866f17c0a1aec9b9fdcd59e65a7e1368 Mon Sep 17 00:00:00 2001 From: manufacturist <15235526+manufacturist@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:13:04 +0300 Subject: [PATCH 1/2] feat(repo): expose fileCount in --output json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `repository.fileCount` to the JSON output, derived from `AnalysisService.listCommitFiles(lastAnalysedCommit.sha, limit=1)` — one extra API call per JSON invocation, omitted when the repo has no analysed commit or the call fails. Unblocks repo-size visibility for the `configure-codacy-cloud` skill, which now reports both `languageCount` and `fileCount` in its summary without the harness needing repo-level access. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/repo-json-file-count.md | 5 +++ SPECS/README.md | 1 + package-lock.json | 4 +- src/commands/AGENTS.md | 2 +- src/commands/repository.test.ts | 72 ++++++++++++++++++++++++++++++ src/commands/repository.ts | 23 +++++++++- 6 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 .changeset/repo-json-file-count.md diff --git a/.changeset/repo-json-file-count.md b/.changeset/repo-json-file-count.md new file mode 100644 index 0000000..84a1b87 --- /dev/null +++ b/.changeset/repo-json-file-count.md @@ -0,0 +1,5 @@ +--- +"@codacy/codacy-cloud-cli": minor +--- + +`codacy repo --output json` now includes a `fileCount` field on the repository object, derived from the analysis of the last analysed commit. Lets consumers (e.g. the `configure-codacy-cloud` skill) read repo size without a separate API call. Returns `undefined` when the repo has no analysed commit. diff --git a/SPECS/README.md b/SPECS/README.md index efb0490..24890a0 100644 --- a/SPECS/README.md +++ b/SPECS/README.md @@ -68,3 +68,4 @@ _No pending tasks._ All commands implemented. | 2026-06-02 | `--reanalyze-and-wait` (`-w`) blocking variant for `repository` and `pull-request`: triggers reanalysis, polls to completion (10s interval, 20min cap), then prints issue deltas by pattern/severity/category. New `src/utils/reanalyze-wait.ts` + `formatDuration`/`isBeingAnalyzed` helpers (26 new tests, 356 total) | | 2026-06-02 | `issues --overview` improvements: relabel False Positives buckets (`belowThreshold`/`equalOrAboveThreshold` → "Not a False Positive"/"Potential False Positive"), and a "Suggested actions to reduce noise" section that flags noisy patterns (≥10% of issues or ≥3× the average) with a runnable `codacy pattern … --disable` command, resolving the tool via its `prefix` (3 new tests, 360 total) | | 2026-06-02 | Pattern config-file & coding-standard awareness: new `pattern ` **info mode** (same card as `patterns`); `pattern`/`patterns` skip listing and refuse updates when a tool uses a local config file; `pattern` refuses to modify coding-standard-enforced patterns; `issues --overview` noise suggestions now render a manual "update your config file / coding standard" step instead of a command when a pattern can't be disabled via CLI. `printPatternCard`/`PATTERN_JSON_FIELDS` moved to `utils/formatting.ts` (11 new tests, 371 total) | +| 2026-06-18 | `repo --output json` now includes `repository.fileCount`, derived from `listCommitFiles(limit=1)` on the last analysed commit; best-effort (omitted when no analysed commit or the call fails). Unlocks repo-size visibility for downstream consumers like the `configure-codacy-cloud` skill (2 new tests, 374 total) | diff --git a/package-lock.json b/package-lock.json index 8a80154..3e7d661 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@codacy/codacy-cloud-cli", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@codacy/codacy-cloud-cli", - "version": "1.2.0", + "version": "1.2.1", "license": "ISC", "dependencies": { "@codacy/tooling": "0.1.0", diff --git a/src/commands/AGENTS.md b/src/commands/AGENTS.md index bd876c9..297bbab 100644 --- a/src/commands/AGENTS.md +++ b/src/commands/AGENTS.md @@ -105,7 +105,7 @@ Instead of a dedicated "Visibility" column (wastes horizontal space), public rep - Green if gate passes, red if gate fails; no coloring if no matching gate exists - **Issues Overview**: three count tables — by category, severity level, and language — sorted descending by count within each group - Shows pagination warning for pull requests if more exist -- JSON output bundles all three API responses into a single object +- JSON output bundles all three API responses into a single object, plus a `repository.fileCount` field derived from `AnalysisService.listCommitFiles(lastAnalysedCommit.sha, limit=1).pagination.total`. Only fetched when `--output json` is requested and a last analysed commit exists; best-effort (omitted when the call fails) - **`--reanalyze` mode** (`-R`): fetches HEAD commit SHA, calls `RepositoryService.reanalyzeCommitById`; early return - **`--reanalyze-and-wait` mode** (`-w`): blocking variant — see "Reanalyze and wait" below. Baseline comes from `issuesOverview`; polling reads the repo's first commit via `listRepositoryCommits(limit=1)` analysis timestamps diff --git a/src/commands/repository.test.ts b/src/commands/repository.test.ts index 63d6b37..748c89a 100644 --- a/src/commands/repository.test.ts +++ b/src/commands/repository.test.ts @@ -38,6 +38,10 @@ function setupDefaultMocks() { vi.mocked(RepositoryService.listCoverageReports).mockResolvedValue({ data: { hasCoverageOverview: false }, } as any); + vi.mocked(AnalysisService.listCommitFiles).mockResolvedValue({ + pagination: { cursor: "1", limit: 1, total: 83 }, + data: [], + } as any); } function createProgram(): Command { @@ -250,6 +254,74 @@ describe("repository command", () => { expect(console.log).toHaveBeenCalledWith( expect.stringContaining('"name": "test-repo"'), ); + + const jsonCall = (console.log as ReturnType).mock.calls.find( + (c) => typeof c[0] === "string" && c[0].startsWith("{"), + ); + const parsed = JSON.parse(jsonCall![0]); + expect(parsed.repository.fileCount).toBe(83); + expect(AnalysisService.listCommitFiles).toHaveBeenCalledWith( + "gh", + "test-org", + "test-repo", + "abc1234567890", + undefined, + undefined, + undefined, + 1, + ); + }); + + it("omits fileCount from JSON when no analysed commit exists", async () => { + vi.mocked(AnalysisService.getRepositoryWithAnalysis).mockResolvedValue({ + data: { ...mockRepoData, lastAnalysedCommit: undefined } as any, + }); + vi.mocked(AnalysisService.listRepositoryPullRequests).mockResolvedValue({ + data: [], + }); + vi.mocked(AnalysisService.issuesOverview).mockResolvedValue({ + data: { counts: mockIssuesCounts }, + }); + + const program = createProgram(); + await program.parseAsync([ + "node", "test", "--output", "json", + "repository", "gh", "test-org", "test-repo", + ]); + + expect(AnalysisService.listCommitFiles).not.toHaveBeenCalled(); + const jsonCall = (console.log as ReturnType).mock.calls.find( + (c) => typeof c[0] === "string" && c[0].startsWith("{"), + ); + const parsed = JSON.parse(jsonCall![0]); + expect(parsed.repository.fileCount).toBeUndefined(); + }); + + it("leaves fileCount undefined when listCommitFiles fails", async () => { + vi.mocked(AnalysisService.getRepositoryWithAnalysis).mockResolvedValue({ + data: mockRepoData as any, + }); + vi.mocked(AnalysisService.listRepositoryPullRequests).mockResolvedValue({ + data: [], + }); + vi.mocked(AnalysisService.issuesOverview).mockResolvedValue({ + data: { counts: mockIssuesCounts }, + }); + vi.mocked(AnalysisService.listCommitFiles).mockRejectedValueOnce( + new Error("boom"), + ); + + const program = createProgram(); + await program.parseAsync([ + "node", "test", "--output", "json", + "repository", "gh", "test-org", "test-repo", + ]); + + const jsonCall = (console.log as ReturnType).mock.calls.find( + (c) => typeof c[0] === "string" && c[0].startsWith("{"), + ); + const parsed = JSON.parse(jsonCall![0]); + expect(parsed.repository.fileCount).toBeUndefined(); }); it("should handle repository with no PRs and no issues", async () => { diff --git a/src/commands/repository.ts b/src/commands/repository.ts index e388068..49c9951 100644 --- a/src/commands/repository.ts +++ b/src/commands/repository.ts @@ -526,8 +526,28 @@ Examples: const hasCoverageData = data.coverage?.coveragePercentage !== undefined; if (format === "json") { + let fileCount: number | undefined; + const analysedSha = data.lastAnalysedCommit?.sha; + if (analysedSha) { + try { + const filesResponse = await AnalysisService.listCommitFiles( + provider, + organization, + repository, + analysedSha, + undefined, + undefined, + undefined, + 1, + ); + fileCount = (filesResponse as any).pagination?.total; + } catch { + // file count is best-effort — leave undefined on failure + } + } + printJson(pickDeep({ - repository: data, + repository: { ...data, fileCount }, pullRequests, issuesOverview: issuesCounts, }, [ @@ -549,6 +569,7 @@ Examples: // Metrics "repository.issuesCount", "repository.loc", + "repository.fileCount", "repository.coverage.coveragePercentage", "repository.complexFilesPercentage", "repository.duplicationPercentage", From e92543e3c079c6446d38493d0333a9e4500602c1 Mon Sep 17 00:00:00 2001 From: manufacturist <15235526+manufacturist@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:41:14 +0300 Subject: [PATCH 2/2] refactor(repo): source fileCount from coverage.numberTotalFiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switches `repository.fileCount` from a dedicated `listCommitFiles(limit=1)` call to plucking `data.coverage.numberTotalFiles` off the existing `getRepositoryWithAnalysis` response. The field is populated even when the repo has no coverage data, so we get the file count for free — no extra API roundtrip, no commit-SHA dance, no best-effort try/catch. Drops 2 tests that covered the removed error/missing-commit paths and the default `listCommitFiles` mock. Test count: 374 → 373. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/repo-json-file-count.md | 2 +- SPECS/README.md | 2 +- src/commands/AGENTS.md | 2 +- src/commands/repository.test.ts | 48 ++---------------------------- src/commands/repository.ts | 25 +++------------- 5 files changed, 10 insertions(+), 69 deletions(-) diff --git a/.changeset/repo-json-file-count.md b/.changeset/repo-json-file-count.md index 84a1b87..06d88e5 100644 --- a/.changeset/repo-json-file-count.md +++ b/.changeset/repo-json-file-count.md @@ -2,4 +2,4 @@ "@codacy/codacy-cloud-cli": minor --- -`codacy repo --output json` now includes a `fileCount` field on the repository object, derived from the analysis of the last analysed commit. Lets consumers (e.g. the `configure-codacy-cloud` skill) read repo size without a separate API call. Returns `undefined` when the repo has no analysed commit. +`codacy repo --output json` now includes a `fileCount` field on the repository object, plucked from `coverage.numberTotalFiles` on the existing `getRepositoryWithAnalysis` response. The field is present even on repos without coverage data, so no extra API call is needed. Lets consumers (e.g. the `configure-codacy-cloud` skill) read repo size without a separate roundtrip. diff --git a/SPECS/README.md b/SPECS/README.md index 24890a0..8118722 100644 --- a/SPECS/README.md +++ b/SPECS/README.md @@ -68,4 +68,4 @@ _No pending tasks._ All commands implemented. | 2026-06-02 | `--reanalyze-and-wait` (`-w`) blocking variant for `repository` and `pull-request`: triggers reanalysis, polls to completion (10s interval, 20min cap), then prints issue deltas by pattern/severity/category. New `src/utils/reanalyze-wait.ts` + `formatDuration`/`isBeingAnalyzed` helpers (26 new tests, 356 total) | | 2026-06-02 | `issues --overview` improvements: relabel False Positives buckets (`belowThreshold`/`equalOrAboveThreshold` → "Not a False Positive"/"Potential False Positive"), and a "Suggested actions to reduce noise" section that flags noisy patterns (≥10% of issues or ≥3× the average) with a runnable `codacy pattern … --disable` command, resolving the tool via its `prefix` (3 new tests, 360 total) | | 2026-06-02 | Pattern config-file & coding-standard awareness: new `pattern ` **info mode** (same card as `patterns`); `pattern`/`patterns` skip listing and refuse updates when a tool uses a local config file; `pattern` refuses to modify coding-standard-enforced patterns; `issues --overview` noise suggestions now render a manual "update your config file / coding standard" step instead of a command when a pattern can't be disabled via CLI. `printPatternCard`/`PATTERN_JSON_FIELDS` moved to `utils/formatting.ts` (11 new tests, 371 total) | -| 2026-06-18 | `repo --output json` now includes `repository.fileCount`, derived from `listCommitFiles(limit=1)` on the last analysed commit; best-effort (omitted when no analysed commit or the call fails). Unlocks repo-size visibility for downstream consumers like the `configure-codacy-cloud` skill (2 new tests, 374 total) | +| 2026-06-18 | `repo --output json` now includes `repository.fileCount`, plucked from `coverage.numberTotalFiles` on the existing `getRepositoryWithAnalysis` response (present even without coverage data — no extra API call). Unlocks repo-size visibility for downstream consumers like the `configure-codacy-cloud` skill (1 new test, 373 total) | diff --git a/src/commands/AGENTS.md b/src/commands/AGENTS.md index 297bbab..5bddea7 100644 --- a/src/commands/AGENTS.md +++ b/src/commands/AGENTS.md @@ -105,7 +105,7 @@ Instead of a dedicated "Visibility" column (wastes horizontal space), public rep - Green if gate passes, red if gate fails; no coloring if no matching gate exists - **Issues Overview**: three count tables — by category, severity level, and language — sorted descending by count within each group - Shows pagination warning for pull requests if more exist -- JSON output bundles all three API responses into a single object, plus a `repository.fileCount` field derived from `AnalysisService.listCommitFiles(lastAnalysedCommit.sha, limit=1).pagination.total`. Only fetched when `--output json` is requested and a last analysed commit exists; best-effort (omitted when the call fails) +- JSON output bundles all three API responses into a single object, plus a `repository.fileCount` field plucked from `data.coverage.numberTotalFiles` (present on the existing `getRepositoryWithAnalysis` response even when the repo has no coverage data — no extra API call). Omitted when that field is absent - **`--reanalyze` mode** (`-R`): fetches HEAD commit SHA, calls `RepositoryService.reanalyzeCommitById`; early return - **`--reanalyze-and-wait` mode** (`-w`): blocking variant — see "Reanalyze and wait" below. Baseline comes from `issuesOverview`; polling reads the repo's first commit via `listRepositoryCommits(limit=1)` analysis timestamps diff --git a/src/commands/repository.test.ts b/src/commands/repository.test.ts index 748c89a..9204148 100644 --- a/src/commands/repository.test.ts +++ b/src/commands/repository.test.ts @@ -38,10 +38,6 @@ function setupDefaultMocks() { vi.mocked(RepositoryService.listCoverageReports).mockResolvedValue({ data: { hasCoverageOverview: false }, } as any); - vi.mocked(AnalysisService.listCommitFiles).mockResolvedValue({ - pagination: { cursor: "1", limit: 1, total: 83 }, - data: [], - } as any); } function createProgram(): Command { @@ -90,7 +86,7 @@ const mockRepoData = { addedState: "Added", gatePolicyName: "Codacy recommended", }, - coverage: { coveragePercentage: 78 }, + coverage: { coveragePercentage: 78, numberTotalFiles: 83 }, goals: { maxComplexFilesPercentage: 25, maxDuplicatedFilesPercentage: 10, @@ -260,21 +256,11 @@ describe("repository command", () => { ); const parsed = JSON.parse(jsonCall![0]); expect(parsed.repository.fileCount).toBe(83); - expect(AnalysisService.listCommitFiles).toHaveBeenCalledWith( - "gh", - "test-org", - "test-repo", - "abc1234567890", - undefined, - undefined, - undefined, - 1, - ); }); - it("omits fileCount from JSON when no analysed commit exists", async () => { + it("omits fileCount from JSON when coverage.numberTotalFiles is absent", async () => { vi.mocked(AnalysisService.getRepositoryWithAnalysis).mockResolvedValue({ - data: { ...mockRepoData, lastAnalysedCommit: undefined } as any, + data: { ...mockRepoData, coverage: {} } as any, }); vi.mocked(AnalysisService.listRepositoryPullRequests).mockResolvedValue({ data: [], @@ -289,34 +275,6 @@ describe("repository command", () => { "repository", "gh", "test-org", "test-repo", ]); - expect(AnalysisService.listCommitFiles).not.toHaveBeenCalled(); - const jsonCall = (console.log as ReturnType).mock.calls.find( - (c) => typeof c[0] === "string" && c[0].startsWith("{"), - ); - const parsed = JSON.parse(jsonCall![0]); - expect(parsed.repository.fileCount).toBeUndefined(); - }); - - it("leaves fileCount undefined when listCommitFiles fails", async () => { - vi.mocked(AnalysisService.getRepositoryWithAnalysis).mockResolvedValue({ - data: mockRepoData as any, - }); - vi.mocked(AnalysisService.listRepositoryPullRequests).mockResolvedValue({ - data: [], - }); - vi.mocked(AnalysisService.issuesOverview).mockResolvedValue({ - data: { counts: mockIssuesCounts }, - }); - vi.mocked(AnalysisService.listCommitFiles).mockRejectedValueOnce( - new Error("boom"), - ); - - const program = createProgram(); - await program.parseAsync([ - "node", "test", "--output", "json", - "repository", "gh", "test-org", "test-repo", - ]); - const jsonCall = (console.log as ReturnType).mock.calls.find( (c) => typeof c[0] === "string" && c[0].startsWith("{"), ); diff --git a/src/commands/repository.ts b/src/commands/repository.ts index 49c9951..08f6228 100644 --- a/src/commands/repository.ts +++ b/src/commands/repository.ts @@ -526,28 +526,11 @@ Examples: const hasCoverageData = data.coverage?.coveragePercentage !== undefined; if (format === "json") { - let fileCount: number | undefined; - const analysedSha = data.lastAnalysedCommit?.sha; - if (analysedSha) { - try { - const filesResponse = await AnalysisService.listCommitFiles( - provider, - organization, - repository, - analysedSha, - undefined, - undefined, - undefined, - 1, - ); - fileCount = (filesResponse as any).pagination?.total; - } catch { - // file count is best-effort — leave undefined on failure - } - } - printJson(pickDeep({ - repository: { ...data, fileCount }, + repository: { + ...data, + fileCount: data.coverage?.numberTotalFiles, + }, pullRequests, issuesOverview: issuesCounts, }, [