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

Add HTTP/HTTPS proxy and TLS support, so the CLI works behind a corporate proxy (#40).

Every command now honors the standard environment variables:

- `HTTPS_PROXY` / `HTTP_PROXY` (and lowercase) — proxy URL per scheme; a bare `host:port` is accepted
- `NO_PROXY` / `no_proxy` — hosts that bypass the proxy (`*`, `.suffix`), matched per request
- `SSL_CERT_FILE` / `NODE_EXTRA_CA_CERTS` — PEM CA bundle for a TLS-intercepting proxy
- `CODACY_CLI_INSECURE` — disable TLS verification as a last resort (warns on stderr)

These are the same variable names the Codacy Analysis CLI and the Codacy VS Code extension use, so one environment configures all of them. The implementation is the shared `configureProxy()` from `@codacy/tooling` rather than a local reimplementation, which is what keeps the behavior identical across the tools. Misconfiguration fails immediately rather than silently doing something else: an unreadable or non-PEM CA bundle reports the path instead of quietly falling back to the default trust store, and a malformed proxy URL reports which variable was wrong and why (with any proxy password redacted) instead of a bare `Invalid URL`.

Nothing changes when no proxy variable is set — the proxy dependency is loaded lazily, so an unproxied run has no measurable overhead.

Thanks to @rattalur for reporting the gap and for the initial implementation in #39.
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,30 @@ jobs:
- name: Build
run: npm run build

- name: Smoke test the built CLI
run: |
# Nothing in the test suite executes the entry point -- every command test
# builds a bare `new Command()` -- so this step is the only coverage of
# src/index.ts's top-level block, on every Node version we support.
node dist/index.js --version

# With proxy env set, the undici dispatcher is actually constructed (the
# run above returns early, since no proxy variable is set). `--version`
# makes no request, so the unreachable proxy is never dialed. This guards
# against a proxy dependency that fails to load or construct on Node 20.
HTTPS_PROXY=http://127.0.0.1:9 node dist/index.js --version

# A misconfigured CA bundle must fail loudly rather than silently fall
# back to the system trust store.
if out=$(SSL_CERT_FILE=/nonexistent/ca.pem node dist/index.js --version 2>&1); then
echo "::error::Expected a non-zero exit for an unreadable SSL_CERT_FILE"
exit 1
fi
case "$out" in
*"Failed to read CA certificate"*) ;;
*) echo "::error::Unexpected failure output: $out"; exit 1 ;;
esac

- name: Test
run: npm test

Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
- 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`
- **API base URL:** `https://app.codacy.com/api/v3` (configured in `src/index.ts` via `OpenAPI.BASE`)
- **Proxy / TLS:** never hand-roll this. Outbound HTTP configuration is delegated to `configureProxy()` from `@codacy/tooling`, wrapped by `configureProxyFromEnv()` in `src/utils/proxy.ts` and called once at the top of `src/index.ts`. It installs a global `undici` dispatcher, so every `fetch` — the generated client and the CVE lookup alike — is covered without touching generated code. Keeping the implementation upstream is what keeps the environment contract identical to the Codacy Analysis CLI; a local reimplementation would drift. If proxy behavior needs to change, change it in `analysis-cli`'s `packages/tooling/src/proxy.ts` and bump the dependency here.

Check warning on line 90 in AGENTS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

AGENTS.md#L90

Absolute rule without escape hatch: "- **Proxy / TLS:** never hand-roll this. Outbound HTTP configuration i"

Check notice on line 90 in AGENTS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

AGENTS.md#L90

Overly complex sentence (91 words). Break into shorter instructions.

Check notice on line 90 in AGENTS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

AGENTS.md#L90

Undefined acronym "CVE" — define on first use or add to glossary.
Comment thread
alerizzo marked this conversation as resolved.
- **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.
Expand Down Expand Up @@ -240,6 +241,11 @@
|---|---|---|
| `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` |
| `HTTPS_PROXY` / `HTTP_PROXY` | No | Proxy URL per scheme (lowercase also honored). Resolved by `@codacy/tooling`'s `configureProxy()`, called once from `src/index.ts` via `configureProxyFromEnv()` |
| `NO_PROXY` / `no_proxy` | No | Comma-separated hosts that bypass the proxy (`*`, `.suffix`), matched **per request** — not once at startup |
| `SSL_CERT_FILE` / `NODE_EXTRA_CA_CERTS` | No | PEM CA bundle for a TLS-intercepting proxy. **Replaces** the default trust store; unreadable or non-PEM is fatal by design |

Check notice on line 246 in AGENTS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

AGENTS.md#L246

Undefined acronym "PEM" — define on first use or add to glossary.
Comment thread
alerizzo marked this conversation as resolved.
| `CODACY_CLI_INSECURE` | No | Disable TLS verification (also `NODE_TLS_REJECT_UNAUTHORIZED=0`). Last resort; warns on stderr |
| `CODACY_DISABLE_UPDATE_CHECK` | No | Disable the "update available" notice. Its `got` stack ignores the proxy variables above, so this is the escape hatch behind a strict proxy |

## Useful Context

Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,31 @@ An explicit `--repository-token` wins outright, so a deliberately scoped run is

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".

## Proxy and TLS

All outbound requests honor the standard proxy environment variables — set them once and every command routes accordingly.

| Variable | Purpose |
|---|---|
| `HTTPS_PROXY` / `HTTP_PROXY` (or lowercase) | Proxy URL for HTTPS / HTTP requests. A bare `host:port` is treated as `http://` |
| `NO_PROXY` / `no_proxy` | Comma-separated hosts that bypass the proxy (`*`, `.suffix`), matched per request |
| `SSL_CERT_FILE` / `NODE_EXTRA_CA_CERTS` | PEM CA bundle to trust, e.g. for a corporate SSL-inspection proxy |
| `CODACY_CLI_INSECURE` / `NODE_TLS_REJECT_UNAUTHORIZED=0` | Disable TLS verification (last resort; warns on stderr) |

```bash
export HTTPS_PROXY=http://proxy.corp:8080
export NO_PROXY=app.codacy.com,.internal
export SSL_CERT_FILE=/path/to/corporate-ca.pem # prefer trusting the CA over disabling TLS
```

If your proxy performs TLS interception (MITM), trust its CA rather than disabling verification. Node doesn't read the OS trust store, so requests can fail with `unable to get local issuer certificate` even when `curl -x "$HTTPS_PROXY" https://app.codacy.com/api/v3/user` against the same host succeeds — curl working while the CLI doesn't is the tell-tale sign. Ask your IT team for the bundle, or export it from your OS trust store in PEM format.

Note that `SSL_CERT_FILE` **replaces** the default trust store rather than adding to it, the same way curl's `--cacert` does, so the bundle must contain the full chain for every host you reach — including hosts that bypass the proxy via `NO_PROXY`. A misconfigured or unreadable bundle fails fast with a clear error instead of silently falling back.

These variable names match the Codacy Analysis CLI and the Codacy VS Code extension, so one environment drives all of them.

> The "update available" notice uses a separate network stack that does not honor these variables. Behind a strict proxy, disable it with `CODACY_DISABLE_UPDATE_CHECK=1`.

## Usage

```bash
Expand Down Expand Up @@ -144,7 +169,7 @@ npm run update-api # Update the auto-generated API client

### CI/CD

- **CI**: Runs on every push to `main` and on PRs. Builds and tests across Node.js 18, 20, and 22.
- **CI**: Runs on every push to `main` and on PRs. Builds, smoke-tests the built CLI, and runs the test suite across Node.js 20 and 22.
- **Release**: Uses [changesets](https://github.com/changesets/changesets) for automated versioning and npm publishing.

#### Publishing a new version
Expand Down
1 change: 1 addition & 0 deletions SPECS/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,4 @@ _No pending tasks._ All commands implemented.
| 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) |
| 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) |
5 changes: 3 additions & 2 deletions SPECS/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@

Triggers on: push and pull requests to `main`.

Matrix: Node.js 18, 20, 22.
Matrix: Node.js 20, 22.

Jobs:
- **build-and-test**: checkout → setup node → install → generate API client → type check → build → test
- **build-and-test**: checkout → setup node → install → generate API client → type check → build → smoke test the built CLI → test
- The smoke step runs `node dist/index.js --version` three ways (plain, with `HTTPS_PROXY` set, and with an unreadable `SSL_CERT_FILE` expected to fail). It is the only thing that executes the real entry point — every command test builds a bare `new Command()` — so it is what catches a dependency that loads or constructs fine on one Node version but not another.
- **changeset-check** (PRs only): verifies at least one `.changeset/*.md` file is present in the PR diff

### Release (`release.yml`)
Expand Down
24 changes: 18 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"node": ">=20"
},
"dependencies": {
"@codacy/tooling": "0.1.0",
"@codacy/tooling": "0.23.0",
"ansis": "4.0.0",
"cli-table3": "^0.6.3",
"commander": "14.0.0",
Expand Down
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { cliVersion } from "./version";
import { getOutputFormat } from "./utils/output";
import { BASE_HEADERS, repositoryTokenOption } from "./utils/auth";
import { maybeNotifyUpdate } from "./utils/update-check";
import { configureProxyFromEnv } from "./utils/proxy";
import { registerInfoCommand } from "./commands/info";
import { registerRepositoriesCommand } from "./commands/repositories";
import { registerRepositoryCommand } from "./commands/repository";
Expand All @@ -25,6 +26,14 @@ import { registerLogoutCommand } from "./commands/logout";

const program = new Command();

// Route all outbound fetch traffic through HTTP(S)_PROXY / NO_PROXY and any
// corporate CA before anything can make a request. Delegated to
// `@codacy/tooling` so the environment contract is identical to the Codacy
// Analysis CLI. No-op when no proxy or TLS variable is set. It only has to run
// before `program.parse` — every request happens inside a command action — but
// it goes first so the network stack is configured before we point it at the API.
configureProxyFromEnv();

OpenAPI.BASE = (process.env.CODACY_API_BASE_URL || "https://app.codacy.com").replace(/\/$/, "") + "/api/v3";
// No token here. Which header carries it depends on the token kind, which isn't
// known until a command resolves its auth — every API path installs headers
Expand Down
Loading
Loading