Skip to content

Commit 3b215b3

Browse files
alerizzorattalurclaude
authored
feat: add HTTP/HTTPS proxy and TLS support (#40) (#43)
Co-authored-by: rattalur <145406381+rattalur@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6584be5 commit 3b215b3

11 files changed

Lines changed: 253 additions & 10 deletions

File tree

‎.changeset/proxy-tls-support.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@codacy/codacy-cloud-cli": minor
3+
---
4+
5+
Add HTTP/HTTPS proxy and TLS support, so the CLI works behind a corporate proxy (#40).
6+
7+
Every command now honors the standard environment variables:
8+
9+
- `HTTPS_PROXY` / `HTTP_PROXY` (and lowercase) — proxy URL per scheme; a bare `host:port` is accepted
10+
- `NO_PROXY` / `no_proxy` — hosts that bypass the proxy (`*`, `.suffix`), matched per request
11+
- `SSL_CERT_FILE` / `NODE_EXTRA_CA_CERTS` — PEM CA bundle for a TLS-intercepting proxy
12+
- `CODACY_CLI_INSECURE` — disable TLS verification as a last resort (warns on stderr)
13+
14+
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`.
15+
16+
Nothing changes when no proxy variable is set — the proxy dependency is loaded lazily, so an unproxied run has no measurable overhead.
17+
18+
Thanks to @rattalur for reporting the gap and for the initial implementation in #39.

‎.github/workflows/ci.yml‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,30 @@ jobs:
3535
- name: Build
3636
run: npm run build
3737

38+
- name: Smoke test the built CLI
39+
run: |
40+
# Nothing in the test suite executes the entry point -- every command test
41+
# builds a bare `new Command()` -- so this step is the only coverage of
42+
# src/index.ts's top-level block, on every Node version we support.
43+
node dist/index.js --version
44+
45+
# With proxy env set, the undici dispatcher is actually constructed (the
46+
# run above returns early, since no proxy variable is set). `--version`
47+
# makes no request, so the unreachable proxy is never dialed. This guards
48+
# against a proxy dependency that fails to load or construct on Node 20.
49+
HTTPS_PROXY=http://127.0.0.1:9 node dist/index.js --version
50+
51+
# A misconfigured CA bundle must fail loudly rather than silently fall
52+
# back to the system trust store.
53+
if out=$(SSL_CERT_FILE=/nonexistent/ca.pem node dist/index.js --version 2>&1); then
54+
echo "::error::Expected a non-zero exit for an unreadable SSL_CERT_FILE"
55+
exit 1
56+
fi
57+
case "$out" in
58+
*"Failed to read CA certificate"*) ;;
59+
*) echo "::error::Unexpected failure output: $out"; exit 1 ;;
60+
esac
61+
3862
- name: Test
3963
run: npm test
4064

‎AGENTS.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ codacy-cloud-cli/
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`
8989
- **API base URL:** `https://app.codacy.com/api/v3` (configured in `src/index.ts` via `OpenAPI.BASE`)
90+
- **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.
9091
- **Authentication — two token kinds.** Read `SPECS/repository-tokens.md` before touching auth or adding a command.
9192
- An **account token** (`api-token` header) reaches everything its owner can see.
9293
- 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.
@@ -240,6 +241,11 @@ When completing work, agents **must** update relevant documentation:
240241
|---|---|---|
241242
| `CODACY_API_TOKEN` | One of the two | Account API token. Get it from Codacy > Account > API Tokens |
242243
| `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` |
244+
| `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()` |
245+
| `NO_PROXY` / `no_proxy` | No | Comma-separated hosts that bypass the proxy (`*`, `.suffix`), matched **per request** — not once at startup |
246+
| `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 |
247+
| `CODACY_CLI_INSECURE` | No | Disable TLS verification (also `NODE_TLS_REJECT_UNAUTHORIZED=0`). Last resort; warns on stderr |
248+
| `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 |
243249

244250
## Useful Context
245251

‎README.md‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,31 @@ An explicit `--repository-token` wins outright, so a deliberately scoped run is
7676

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

79+
## Proxy and TLS
80+
81+
All outbound requests honor the standard proxy environment variables — set them once and every command routes accordingly.
82+
83+
| Variable | Purpose |
84+
|---|---|
85+
| `HTTPS_PROXY` / `HTTP_PROXY` (or lowercase) | Proxy URL for HTTPS / HTTP requests. A bare `host:port` is treated as `http://` |
86+
| `NO_PROXY` / `no_proxy` | Comma-separated hosts that bypass the proxy (`*`, `.suffix`), matched per request |
87+
| `SSL_CERT_FILE` / `NODE_EXTRA_CA_CERTS` | PEM CA bundle to trust, e.g. for a corporate SSL-inspection proxy |
88+
| `CODACY_CLI_INSECURE` / `NODE_TLS_REJECT_UNAUTHORIZED=0` | Disable TLS verification (last resort; warns on stderr) |
89+
90+
```bash
91+
export HTTPS_PROXY=http://proxy.corp:8080
92+
export NO_PROXY=app.codacy.com,.internal
93+
export SSL_CERT_FILE=/path/to/corporate-ca.pem # prefer trusting the CA over disabling TLS
94+
```
95+
96+
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.
97+
98+
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.
99+
100+
These variable names match the Codacy Analysis CLI and the Codacy VS Code extension, so one environment drives all of them.
101+
102+
> 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`.
103+
79104
## Usage
80105

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

145170
### CI/CD
146171

147-
- **CI**: Runs on every push to `main` and on PRs. Builds and tests across Node.js 18, 20, and 22.
172+
- **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.
148173
- **Release**: Uses [changesets](https://github.com/changesets/changesets) for automated versioning and npm publishing.
149174

150175
#### Publishing a new version

‎SPECS/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,4 @@ _No pending tasks._ All commands implemented.
8686
| 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) |
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) |
89+
| 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) |

‎SPECS/deployment.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616

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

19-
Matrix: Node.js 18, 20, 22.
19+
Matrix: Node.js 20, 22.
2020

2121
Jobs:
22-
- **build-and-test**: checkout → setup node → install → generate API client → type check → build → test
22+
- **build-and-test**: checkout → setup node → install → generate API client → type check → build → smoke test the built CLI → test
23+
- 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.
2324
- **changeset-check** (PRs only): verifies at least one `.changeset/*.md` file is present in the PR diff
2425

2526
### Release (`release.yml`)

‎package-lock.json‎

Lines changed: 18 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"node": ">=20"
4747
},
4848
"dependencies": {
49-
"@codacy/tooling": "0.1.0",
49+
"@codacy/tooling": "0.23.0",
5050
"ansis": "4.0.0",
5151
"cli-table3": "^0.6.3",
5252
"commander": "14.0.0",

‎src/index.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { cliVersion } from "./version";
55
import { getOutputFormat } from "./utils/output";
66
import { BASE_HEADERS, repositoryTokenOption } from "./utils/auth";
77
import { maybeNotifyUpdate } from "./utils/update-check";
8+
import { configureProxyFromEnv } from "./utils/proxy";
89
import { registerInfoCommand } from "./commands/info";
910
import { registerRepositoriesCommand } from "./commands/repositories";
1011
import { registerRepositoryCommand } from "./commands/repository";
@@ -25,6 +26,14 @@ import { registerLogoutCommand } from "./commands/logout";
2526

2627
const program = new Command();
2728

29+
// Route all outbound fetch traffic through HTTP(S)_PROXY / NO_PROXY and any
30+
// corporate CA before anything can make a request. Delegated to
31+
// `@codacy/tooling` so the environment contract is identical to the Codacy
32+
// Analysis CLI. No-op when no proxy or TLS variable is set. It only has to run
33+
// before `program.parse` — every request happens inside a command action — but
34+
// it goes first so the network stack is configured before we point it at the API.
35+
configureProxyFromEnv();
36+
2837
OpenAPI.BASE = (process.env.CODACY_API_BASE_URL || "https://app.codacy.com").replace(/\/$/, "") + "/api/v3";
2938
// No token here. Which header carries it depends on the token kind, which isn't
3039
// known until a command resolves its auth — every API path installs headers

0 commit comments

Comments
 (0)