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
29 changes: 29 additions & 0 deletions .changeset/repository-token-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"@codacy/codacy-cloud-cli": minor
---

Add repository (project) token support

You can now authenticate with a **repository token** — scoped to a single repository — instead of a personal account API token that reaches every organization and repository you can see. This is the right credential for CI and for the auto-configuration agent: if it leaks, the blast radius is one repository.

```bash
codacy tools --repository-token <your-repository-token>
# or, for a whole CI job:
export CODACY_PROJECT_TOKEN=<your-repository-token>
```

Get one from **Codacy > Repository > Settings > Integrations > Project API token**. The new `--repository-token <token>` flag is accepted by every command, and `CODACY_PROJECT_TOKEN` is picked up automatically.

**Token precedence** (identical to the Codacy Analysis CLI): `--repository-token` > `CODACY_PROJECT_TOKEN` > `CODACY_API_TOKEN` > stored `codacy login`. An explicit `--repository-token` wins outright, so a deliberately scoped run is never silently widened. Note that `CODACY_PROJECT_TOKEN` outranks `CODACY_API_TOKEN` — unset it if you want your account token used.

**Not every command accepts a repository token**, because Codacy only honours them on a limited set of repository-scoped operations:

- **Fully supported:** `tools`, `tool`, `patterns`, `pattern`, `issues` (including `--overview`), `tools --import`, `repository --reanalyze` / `--reanalyze-and-wait`.
- **Partially supported:** `repository` works but omits the pull request and coverage sections. In `--output json`, `pullRequests` stays an empty array and a new `unavailable: ["pullRequests"]` field marks what couldn't be fetched. Output under an account token is unchanged.
- **Account token required:** `info`, `repositories`, `ls`, `directories`, `pull-request`, `pull-requests`, `issue`, `findings`, `finding`, `issues --ignore`/`--ignored`, `tools --import --force`, and `repository`'s `--add`/`--remove`/`--follow`/`--unfollow`/`--link-standard`/`--unlink-standard`.

Unsupported combinations now fail immediately with a message naming the operation, why a repository token can't perform it, and which token is in use — instead of sending a request that comes back as a bare `Unauthorized`.

`codacy login` continues to store account tokens only; repository tokens are passed per command or via the environment.

Also fixed: `codacy repository` no longer loses the entire dashboard when the pull request lookup fails, and `codacy login` no longer reports a repository token as "invalid" when it is rejected for being the wrong kind of token.
54 changes: 54 additions & 0 deletions .codacy/instructions/review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Codacy AI review instructions

Project-specific context for reviewing this repository. These notes exist to
prevent recurring false positives — they are not blanket exemptions, so still
flag a finding when it points at a concrete defect.

## Repository shape

- Single-package Node.js + TypeScript CLI (`@codacy/codacy-cloud-cli`) wrapping
the Codacy API v3. Commander for the CLI, Vitest for tests.
- `src/api/client/` is **auto-generated** from the OpenAPI spec by
`npm run update-api`. Never flag findings there and never suggest edits to it.
- Conventions live in `AGENTS.md` (root) and `src/commands/AGENTS.md`; specs and
the backlog live in `SPECS/`.

## Tests

- Test files are deliberately long and repetitive: fixtures are written out in
full rather than factored into builders, so each test reads standalone. **File-level
length and duplication findings on `*.test.ts` are expected** and should not be
reported.
- Each command test builds its own bare `new Command()` harness rather than
importing `src/index.ts`. That duplication is intentional — it keeps a command's
tests independent of global CLI wiring.

## Complexity metrics

- Lizard's TypeScript parser sometimes **merges adjacent function declarations**
into a single span, reporting their combined cyclomatic complexity against the
first function's name. Before reporting a complexity finding, check that the
named function really contains that many branches; if the reported span covers
more than one declaration, the number is a parser artifact.
- Command action handlers are inherently branchy — they dispatch across mutually
exclusive flag modes with early returns. Prefer suggesting extraction of a
cohesive block (validation, rendering) over generic "reduce complexity" advice.

## Authentication

- The CLI accepts two token kinds: an **account token** (`api-token` header) and a
**repository/project token** (`project-token` header). See
`SPECS/repository-tokens.md`.
- Codacy honours repository tokens on only a fixed set of operations. That
whitelist is **deliberately hardcoded** in the command guards — it mirrors a
server-side allowlist that the client cannot query, so don't suggest deriving it
dynamically. It carries a "re-verify after every `npm run update-api`" note.
- Guards intentionally refuse **before** issuing any request and before
`resolveRepoArgs()` runs, so an unsupported operation fails fast instead of
returning a bare `Unauthorized`.

## Documentation

- Cross-references between `SPECS/*.md`, `AGENTS.md`, and `README.md` are often
added in the **same** pull request as the file they point at. Verify the target
is absent from the PR's own diff before reporting a broken reference.
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ dist/
# Ignore api-v3
api-v3/

# Ignore .codacy
.codacy/
# Ignore .codacy local state (config, logs, tool configs, generated files).
# Uses `.codacy/*` rather than `.codacy/` so authored, shareable files below can
# be re-included — git cannot un-ignore anything inside an excluded directory.
.codacy/*
# Instructions for Codacy's AI reviewer are authored and belong in the repo.
!.codacy/instructions/

#Ignore vscode AI rules
.github/instructions/codacy.instructions.md
35 changes: 29 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,26 @@ codacy-cloud-cli/
- Prefer that over calling `setTimeout`/`sleep` directly in a command, unless you have a clear reason not to.
- Default cadence is `POLL_INTERVAL_MS` (10s), capped at `MAX_WAIT_MS` (20min).
- **Error handling:** Use `try/catch` with the shared `handleError()` from `src/utils/error.ts`
- **Authentication:** All commands that call the API must call `checkApiToken()` from `src/utils/auth.ts` before making requests
- **API base URL:** `https://app.codacy.com/api/v3` (configured in `src/index.ts` via `OpenAPI.BASE`)
- **Auth mechanism:** `CODACY_API_TOKEN` environment variable, sent as `api-token` header
- **Authentication — two token kinds.** Read `SPECS/repository-tokens.md` before touching auth or adding a command.
- An **account token** (`api-token` header) reaches everything its owner can see.
- A **repository token** (`project-token` header) is scoped to one repository. It is accepted only on a fixed whitelist of 13 operations; everywhere else Codacy rejects it as if no token had been sent.
- Every command that calls the API resolves auth first, via `resolveAuth(this)` from `src/utils/auth.ts` (returns a `RemoteAuth` discriminated union), and declares `.addOption(repositoryTokenOption())` so `--repository-token` parses.
- **New commands must decide their token scope**, using the whitelist in `SPECS/repository-tokens.md`:
- account-only end to end → `resolveAccountAuth(this, "<why a repository token can't do it>")`
- fully whitelisted → `resolveAuth(this)`
- mixed → `resolveAuth(this)` plus `requireAccountToken(auth, "<operation>", "<why>")` per unsupported flag, or `fetchIfAccountToken(...)` to skip an unsupported sub-call
- **Guards must run before any request**, and before `resolveRepoArgs()` — that shells out to git and prints an auto-detection line, which is misleading ahead of a refusal.

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

Document a clear exception or escalation process alongside this absolute rule. Include criteria for when an exception might apply and who can authorize it.

See Issue in Codacy

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.

Fair — the rule is absolute but the exception was left implicit. Added two sub-bullets: commands whose endpoints are all whitelisted need no guard at all (tool, patterns, pattern are the live examples), and when an operation's scope is genuinely unclear the escalation is to confirm against the API owners and record it in SPECS/repository-tokens.md rather than guess a guard.

🤖 Generated by /pr-fixup command

- Exception: a command whose endpoints are all whitelisted needs no guard at all — `resolveAuth(this)` alone is correct (see `tool`, `patterns`, `pattern`).
- Exception: a data-dependent guard runs after the fetch it depends on.
- Example: `guardForceUnlink` in `tools.ts` needs the coding-standard count.
- Keep those reads whitelisted, so nothing doomed is sent.
- Refuse before any prompt or mutation even so.
- If an operation's scope is genuinely unclear, don't guess a guard.
- Confirm the whitelist against the API owners instead.
- Record the answer in `SPECS/repository-tokens.md`.
- The whitelist is hardcoded in these guards.
- **Re-verify the whitelist after every `npm run update-api`.**

### Command Pattern

Expand All @@ -98,7 +115,7 @@ Every command file follows this structure:
// src/commands/<command-name>.ts
import { Command } from "commander";
import ora from "ora";
import { checkApiToken } from "../utils/auth";
import { repositoryTokenOption, resolveAuth } from "../utils/auth";
import { handleError } from "../utils/error";
// Import relevant API service(s)

Expand All @@ -108,9 +125,14 @@ export function register<Name>Command(program: Command) {
.description("Clear description of what this command does")
.argument("[args]", "Description of arguments")
.option("--flag <value>", "Description of options")
.action(async (args, options) => {
// Declared per command (not only in index.ts) so `--repository-token` parses
// in the test harnesses, which each build a bare `new Command()`.
.addOption(repositoryTokenOption())
.action(async function (this: Command, args, options) {
try {
checkApiToken();
// Or resolveAccountAuth(this, "<why>") for an account-only command —
// see the Authentication bullet above.
const auth = resolveAuth(this);
const spinner = ora("Loading...").start();
// Call API service
// Format and display output
Expand Down Expand Up @@ -216,7 +238,8 @@ When completing work, agents **must** update relevant documentation:

| Variable | Required | Description |
|---|---|---|
| `CODACY_API_TOKEN` | Yes | API token for authenticating with Codacy. Get it from Codacy > Account > API Tokens |
| `CODACY_API_TOKEN` | One of the two | Account API token. Get it from Codacy > Account > API Tokens |
| `CODACY_PROJECT_TOKEN` | One of the two | Repository (project) token, scoped to one repository. Get it from Codacy > Repository > Settings > Integrations > Project API token. **Outranks `CODACY_API_TOKEN`** — see `SPECS/repository-tokens.md` |

## Useful Context

Expand Down
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ npm link

## Authentication

Log in interactively (recommended):
The CLI accepts two kinds of token.

### Account API token

Reaches every organization and repository your account can see. Log in interactively (recommended):

```bash
codacy login
Expand All @@ -38,6 +42,40 @@ You can get a token from **Codacy > My Account > Access Management > API Tokens*

The `login` command stores the token encrypted at `~/.codacy/credentials`. The environment variable takes precedence over stored credentials when both are present.

### Repository (project) token

Scoped to a single repository — the right choice for CI, since a leaked token can't reach anything else. Get one from **Codacy > Repository > Settings > Integrations > Project API token**.

```bash
codacy tools --repository-token your-repository-token
# or, for a whole CI job:
export CODACY_PROJECT_TOKEN=your-repository-token
```

Codacy accepts repository tokens on a **limited set of repository-scoped operations**, so some commands require an account token and say so explicitly rather than failing with a generic authorization error:

| Works with a repository token | Requires an account token |
|---|---|
| `tools`, `tool`, `patterns`, `pattern` | `info`, `repositories` |
| `issues` (including `--overview`) | `issues --ignore`, `issues --ignored`, `issue` |
| `repository`, `repository --reanalyze` | `repository --add`/`--remove`/`--follow`/`--unfollow`/`--link-standard`/`--unlink-standard` |
| | `pull-request`, `pull-requests`, `ls`, `directories`, `findings`, `finding` |

`codacy repository` works, but omits the pull request and coverage-report sections — those endpoints don't accept repository tokens. In `--output json` it marks them as `"unavailable": ["pullRequests", "coverageReports"]`, so a consumer can tell "none" apart from "couldn't look". Note that skipping coverage reports also suppresses the "waiting for / missing coverage reports" hint on the Analysis row.

`codacy login` stores account tokens only; pass repository tokens per command or via `CODACY_PROJECT_TOKEN`.

### Token precedence

1. `--repository-token <token>`
2. `CODACY_PROJECT_TOKEN`
3. `CODACY_API_TOKEN`
4. Stored credentials from `codacy login`

An explicit `--repository-token` wins outright, so a deliberately scoped run is never silently widened by an environment variable or a stale login. Note that `CODACY_PROJECT_TOKEN` outranks `CODACY_API_TOKEN` (matching the [Codacy Analysis CLI](https://github.com/codacy/analysis-cli)) — unset it if you want your account token used.

Passing `--repository-token` with an **empty** value is an error rather than a fallback. `--repository-token "$CODACY_PROJECT_TOKEN"` with the secret unset is a common CI mistake, and quietly falling back to an account token would run with much wider access than you asked for. An empty *environment variable*, by contrast, simply means "unset".

## Usage

```bash
Expand All @@ -50,6 +88,7 @@ codacy <command> --help # Detailed usage for any command
| Option | Description |
|---|---|
| `-o, --output <format>` | Output format: `table` (default) or `json` |
| `--repository-token <token>` | Repository (project) token, scoped to one repository (env: `CODACY_PROJECT_TOKEN`) |
| `-V, --version` | Show version |
| `-h, --help` | Show help |

Expand Down
3 changes: 3 additions & 0 deletions SPECS/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ _No pending tasks._ All commands implemented.

- [setup.md](setup.md) — test framework, build, CI/CD setup
- [deployment.md](deployment.md) — npm publishing, brew formula
- [repository-tokens.md](repository-tokens.md) — **read before touching auth or adding a command**: the two token kinds, precedence, the 13-operation backend whitelist, and the per-command support matrix
- [missing-endpoints.md](missing-endpoints.md) — API v3 operations that don't accept repository tokens yet, ranked; candidate Linear tasks

## Changelog

Expand Down Expand Up @@ -83,3 +85,4 @@ _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-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) |
Loading
Loading