Skip to content

Commit 402edd8

Browse files
alerizzoclaude
andauthored
feat: Add repository (project) token support OD-489 (#37)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5a75cfe commit 402edd8

40 files changed

Lines changed: 2020 additions & 147 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@codacy/codacy-cloud-cli": minor
3+
---
4+
5+
Add repository (project) token support
6+
7+
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.
8+
9+
```bash
10+
codacy tools --repository-token <your-repository-token>
11+
# or, for a whole CI job:
12+
export CODACY_PROJECT_TOKEN=<your-repository-token>
13+
```
14+
15+
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.
16+
17+
**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.
18+
19+
**Not every command accepts a repository token**, because Codacy only honours them on a limited set of repository-scoped operations:
20+
21+
- **Fully supported:** `tools`, `tool`, `patterns`, `pattern`, `issues` (including `--overview`), `tools --import`, `repository --reanalyze` / `--reanalyze-and-wait`.
22+
- **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.
23+
- **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`.
24+
25+
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`.
26+
27+
`codacy login` continues to store account tokens only; repository tokens are passed per command or via the environment.
28+
29+
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.

.codacy/instructions/review.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Codacy AI review instructions
2+
3+
Project-specific context for reviewing this repository. These notes exist to
4+
prevent recurring false positives — they are not blanket exemptions, so still
5+
flag a finding when it points at a concrete defect.
6+
7+
## Repository shape
8+
9+
- Single-package Node.js + TypeScript CLI (`@codacy/codacy-cloud-cli`) wrapping
10+
the Codacy API v3. Commander for the CLI, Vitest for tests.
11+
- `src/api/client/` is **auto-generated** from the OpenAPI spec by
12+
`npm run update-api`. Never flag findings there and never suggest edits to it.
13+
- Conventions live in `AGENTS.md` (root) and `src/commands/AGENTS.md`; specs and
14+
the backlog live in `SPECS/`.
15+
16+
## Tests
17+
18+
- Test files are deliberately long and repetitive: fixtures are written out in
19+
full rather than factored into builders, so each test reads standalone. **File-level
20+
length and duplication findings on `*.test.ts` are expected** and should not be
21+
reported.
22+
- Each command test builds its own bare `new Command()` harness rather than
23+
importing `src/index.ts`. That duplication is intentional — it keeps a command's
24+
tests independent of global CLI wiring.
25+
26+
## Complexity metrics
27+
28+
- Lizard's TypeScript parser sometimes **merges adjacent function declarations**
29+
into a single span, reporting their combined cyclomatic complexity against the
30+
first function's name. Before reporting a complexity finding, check that the
31+
named function really contains that many branches; if the reported span covers
32+
more than one declaration, the number is a parser artifact.
33+
- Command action handlers are inherently branchy — they dispatch across mutually
34+
exclusive flag modes with early returns. Prefer suggesting extraction of a
35+
cohesive block (validation, rendering) over generic "reduce complexity" advice.
36+
37+
## Authentication
38+
39+
- The CLI accepts two token kinds: an **account token** (`api-token` header) and a
40+
**repository/project token** (`project-token` header). See
41+
`SPECS/repository-tokens.md`.
42+
- Codacy honours repository tokens on only a fixed set of operations. That
43+
whitelist is **deliberately hardcoded** in the command guards — it mirrors a
44+
server-side allowlist that the client cannot query, so don't suggest deriving it
45+
dynamically. It carries a "re-verify after every `npm run update-api`" note.
46+
- Guards intentionally refuse **before** issuing any request and before
47+
`resolveRepoArgs()` runs, so an unsupported operation fails fast instead of
48+
returning a bare `Unauthorized`.
49+
50+
## Documentation
51+
52+
- Cross-references between `SPECS/*.md`, `AGENTS.md`, and `README.md` are often
53+
added in the **same** pull request as the file they point at. Verify the target
54+
is absent from the PR's own diff before reporting a broken reference.

.gitignore

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ dist/
1616
# Ignore api-v3
1717
api-v3/
1818

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

2226
#Ignore vscode AI rules
2327
.github/instructions/codacy.instructions.md

AGENTS.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,26 @@ codacy-cloud-cli/
8686
- Prefer that over calling `setTimeout`/`sleep` directly in a command, unless you have a clear reason not to.
8787
- Default cadence is `POLL_INTERVAL_MS` (10s), capped at `MAX_WAIT_MS` (20min).
8888
- **Error handling:** Use `try/catch` with the shared `handleError()` from `src/utils/error.ts`
89-
- **Authentication:** All commands that call the API must call `checkApiToken()` from `src/utils/auth.ts` before making requests
9089
- **API base URL:** `https://app.codacy.com/api/v3` (configured in `src/index.ts` via `OpenAPI.BASE`)
91-
- **Auth mechanism:** `CODACY_API_TOKEN` environment variable, sent as `api-token` header
90+
- **Authentication — two token kinds.** Read `SPECS/repository-tokens.md` before touching auth or adding a command.
91+
- An **account token** (`api-token` header) reaches everything its owner can see.
92+
- 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.
93+
- 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.
94+
- **New commands must decide their token scope**, using the whitelist in `SPECS/repository-tokens.md`:
95+
- account-only end to end → `resolveAccountAuth(this, "<why a repository token can't do it>")`
96+
- fully whitelisted → `resolveAuth(this)`
97+
- mixed → `resolveAuth(this)` plus `requireAccountToken(auth, "<operation>", "<why>")` per unsupported flag, or `fetchIfAccountToken(...)` to skip an unsupported sub-call
98+
- **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.
99+
- Exception: a command whose endpoints are all whitelisted needs no guard at all — `resolveAuth(this)` alone is correct (see `tool`, `patterns`, `pattern`).
100+
- Exception: a data-dependent guard runs after the fetch it depends on.
101+
- Example: `guardForceUnlink` in `tools.ts` needs the coding-standard count.
102+
- Keep those reads whitelisted, so nothing doomed is sent.
103+
- Refuse before any prompt or mutation even so.
104+
- If an operation's scope is genuinely unclear, don't guess a guard.
105+
- Confirm the whitelist against the API owners instead.
106+
- Record the answer in `SPECS/repository-tokens.md`.
107+
- The whitelist is hardcoded in these guards.
108+
- **Re-verify the whitelist after every `npm run update-api`.**
92109

93110
### Command Pattern
94111

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

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

217239
| Variable | Required | Description |
218240
|---|---|---|
219-
| `CODACY_API_TOKEN` | Yes | API token for authenticating with Codacy. Get it from Codacy > Account > API Tokens |
241+
| `CODACY_API_TOKEN` | One of the two | Account API token. Get it from Codacy > Account > API Tokens |
242+
| `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` |
220243

221244
## Useful Context
222245

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ npm link
2222

2323
## Authentication
2424

25-
Log in interactively (recommended):
25+
The CLI accepts two kinds of token.
26+
27+
### Account API token
28+
29+
Reaches every organization and repository your account can see. Log in interactively (recommended):
2630

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

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

45+
### Repository (project) token
46+
47+
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**.
48+
49+
```bash
50+
codacy tools --repository-token your-repository-token
51+
# or, for a whole CI job:
52+
export CODACY_PROJECT_TOKEN=your-repository-token
53+
```
54+
55+
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:
56+
57+
| Works with a repository token | Requires an account token |
58+
|---|---|
59+
| `tools`, `tool`, `patterns`, `pattern` | `info`, `repositories` |
60+
| `issues` (including `--overview`) | `issues --ignore`, `issues --ignored`, `issue` |
61+
| `repository`, `repository --reanalyze` | `repository --add`/`--remove`/`--follow`/`--unfollow`/`--link-standard`/`--unlink-standard` |
62+
| | `pull-request`, `pull-requests`, `ls`, `directories`, `findings`, `finding` |
63+
64+
`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.
65+
66+
`codacy login` stores account tokens only; pass repository tokens per command or via `CODACY_PROJECT_TOKEN`.
67+
68+
### Token precedence
69+
70+
1. `--repository-token <token>`
71+
2. `CODACY_PROJECT_TOKEN`
72+
3. `CODACY_API_TOKEN`
73+
4. Stored credentials from `codacy login`
74+
75+
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.
76+
77+
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".
78+
4179
## Usage
4280

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

SPECS/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ _No pending tasks._ All commands implemented.
3737

3838
- [setup.md](setup.md) — test framework, build, CI/CD setup
3939
- [deployment.md](deployment.md) — npm publishing, brew formula
40+
- [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
41+
- [missing-endpoints.md](missing-endpoints.md) — API v3 operations that don't accept repository tokens yet, ranked; candidate Linear tasks
4042

4143
## Changelog
4244

@@ -83,3 +85,4 @@ _No pending tasks._ All commands implemented.
8385
| 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) |
8486
| 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) |
8587
| 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) |
88+
| 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) |

0 commit comments

Comments
 (0)