Skip to content

Commit b380be8

Browse files
committed
feat(config): support .sentryclirc config file for per-directory defaults
Add backward-compatible support for .sentryclirc INI config files. The CLI walks up from CWD to find config files, merging them (closest wins per-field) with ~/.sentryclirc as a global fallback. Supported fields: - [defaults] org, project, url - [auth] token Token and URL are applied via env shim (SENTRY_AUTH_TOKEN, SENTRY_URL). Org and project are inserted into the resolution chain between env vars and SQLite defaults with source tracking. This enables per-directory project defaults in monorepos and seamless migration from legacy sentry-cli.
1 parent dd4f76e commit b380be8

File tree

14 files changed

+1713
-84
lines changed

14 files changed

+1713
-84
lines changed

docs/src/content/docs/agent-guidance.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Best practices and operational guidance for AI coding agents using the Sentry CL
1212
- **Use `sentry schema` to explore the API** — if you need to discover API endpoints, run `sentry schema` to browse interactively or `sentry schema <resource>` to search. This is faster than fetching OpenAPI specs externally.
1313
- **Use `sentry issue view <id>` to investigate issues** — when asked about a specific issue (e.g., `CLI-G5`, `PROJECT-123`), use `sentry issue view` directly.
1414
- **Use `--json` for machine-readable output** — pipe through `jq` for filtering. Human-readable output includes formatting that is hard to parse.
15-
- **The CLI auto-detects org/project** — most commands work without explicit targets by scanning for DSNs in `.env` files, source code, config defaults, and directory names. Only specify `<org>/<project>` when the CLI reports it can't detect the target or detects the wrong one.
15+
- **The CLI auto-detects org/project** — most commands work without explicit targets by checking `.sentryclirc` config files, scanning for DSNs in `.env` files and source code, and matching directory names. Only specify `<org>/<project>` when the CLI reports it can't detect the target or detects the wrong one.
1616

1717
## Design Principles
1818

@@ -213,7 +213,7 @@ When querying the Events API (directly or via `sentry api`), valid dataset value
213213
- **Wrong issue ID format**: Use `PROJECT-123` (short ID), not the numeric ID `123456789`. The short ID includes the project prefix.
214214
- **Pre-authenticating unnecessarily**: Don't run `sentry auth login` before every command. The CLI detects missing/expired auth and prompts automatically. Only run `sentry auth login` if you need to switch accounts.
215215
- **Missing `--json` for piping**: Human-readable output includes formatting. Use `--json` when parsing output programmatically.
216-
- **Specifying org/project when not needed**: Auto-detection resolves org/project from DSNs, env vars, config defaults, and directory names. Let it work first — only add `<org>/<project>` if the CLI says it can't detect the target or detects the wrong one.
216+
- **Specifying org/project when not needed**: Auto-detection resolves org/project from `.sentryclirc` config files, DSNs, env vars, and directory names. Let it work first — only add `<org>/<project>` if the CLI says it can't detect the target or detects the wrong one.
217217
- **Confusing `--query` syntax**: The `--query` flag uses Sentry search syntax (e.g., `is:unresolved`, `assigned:me`), not free text search.
218218
- **Not using `--web`**: View commands support `-w`/`--web` to open the resource in the browser — useful for sharing links.
219219
- **Fetching API schemas instead of using the CLI**: Prefer `sentry schema` to browse the API and `sentry api` to make requests — the CLI handles authentication and endpoint resolution, so there's rarely a need to download OpenAPI specs separately.

docs/src/content/docs/configuration.md

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,67 @@
11
---
22
title: Configuration
3-
description: Environment variables and configuration options for the Sentry CLI
3+
description: Environment variables, config files, and configuration options for the Sentry CLI
44
---
55

6-
The Sentry CLI can be configured through environment variables and a local database. Most users don't need to set any of these — the CLI auto-detects your project from your codebase and stores credentials locally after `sentry auth login`.
6+
The Sentry CLI can be configured through config files, environment variables, and a local database. Most users don't need to set any of these — the CLI auto-detects your project from your codebase and stores credentials locally after `sentry auth login`.
7+
8+
## Configuration File (`.sentryclirc`)
9+
10+
The CLI supports a `.sentryclirc` config file using standard INI syntax. This is the same format used by the legacy `sentry-cli` tool, so existing config files are automatically picked up.
11+
12+
### How It Works
13+
14+
The CLI looks for `.sentryclirc` files by walking up from your current directory toward the filesystem root. If multiple files are found, values from the closest file take priority, with `~/.sentryclirc` serving as a global fallback.
15+
16+
```ini
17+
[defaults]
18+
org = my-org
19+
project = my-project
20+
21+
[auth]
22+
token = sntrys_...
23+
```
24+
25+
### Supported Fields
26+
27+
| Section | Key | Description |
28+
|---------|-----|-------------|
29+
| `[defaults]` | `org` | Default organization slug |
30+
| `[defaults]` | `project` | Default project slug |
31+
| `[defaults]` | `url` | Sentry base URL (for self-hosted) |
32+
| `[auth]` | `token` | Auth token (mapped to `SENTRY_AUTH_TOKEN`) |
33+
34+
### Monorepo Setup
35+
36+
In monorepos, place a `.sentryclirc` at the repo root with your org, then add per-package configs with just the project:
37+
38+
```
39+
my-monorepo/
40+
.sentryclirc # [defaults] org = my-company
41+
packages/
42+
frontend/
43+
.sentryclirc # [defaults] project = frontend-web
44+
backend/
45+
.sentryclirc # [defaults] project = backend-api
46+
```
47+
48+
When you run a command from `packages/frontend/`, the CLI resolves `org = my-company` from the root and `project = frontend-web` from the closest file.
49+
50+
### Resolution Priority
51+
52+
When the CLI needs to determine your org and project, it checks these sources in order:
53+
54+
1. **Explicit CLI arguments**`sentry issue list my-org/my-project`
55+
2. **Environment variables**`SENTRY_ORG` / `SENTRY_PROJECT`
56+
3. **`.sentryclirc` config file** — walked up from CWD, merged with `~/.sentryclirc`
57+
4. **DSN auto-detection** — scans source code and `.env` files
58+
5. **Directory name inference** — matches your directory name against project slugs
59+
60+
The first source that provides both org and project wins. For org-only commands, only the org is needed.
61+
62+
### Backward Compatibility
63+
64+
If you previously used the legacy `sentry-cli` and have a `~/.sentryclirc` file, the new CLI reads it automatically. The `[defaults]` and `[auth]` sections are fully compatible. The `[auth] token` value is mapped to the `SENTRY_AUTH_TOKEN` environment variable internally (only if the env var is not already set).
765

866
## Environment Variables
967

docs/src/content/docs/features.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ The Sentry CLI includes several features designed to streamline your workflow, e
77

88
## DSN Auto-Detection
99

10-
The CLI automatically detects your Sentry project from your codebase, eliminating the need to specify the target for every command.
10+
The CLI automatically detects your Sentry project from your codebase, eliminating the need to specify the target for every command. DSN detection is one part of the [resolution priority chain](./configuration/#resolution-priority) — it runs after checking for explicit arguments, environment variables, and `.sentryclirc` config files.
1111

1212
### How It Works
1313

@@ -19,6 +19,10 @@ DSN detection follows this priority order (highest first):
1919

2020
When a DSN is found, the CLI resolves it to your organization and project, then caches the result for fast subsequent lookups.
2121

22+
:::tip
23+
For monorepos or when DSN detection picks up the wrong project, use a [`.sentryclirc` config file](./configuration/#configuration-file-sentryclirc) to pin your org/project explicitly.
24+
:::
25+
2226
### Supported Languages
2327

2428
The CLI can detect DSNs from source code in these languages:

plugins/sentry-cli/skills/sentry-cli/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Best practices and operational guidance for AI coding agents using the Sentry CL
2222
- **Use `sentry schema` to explore the API** — if you need to discover API endpoints, run `sentry schema` to browse interactively or `sentry schema <resource>` to search. This is faster than fetching OpenAPI specs externally.
2323
- **Use `sentry issue view <id>` to investigate issues** — when asked about a specific issue (e.g., `CLI-G5`, `PROJECT-123`), use `sentry issue view` directly.
2424
- **Use `--json` for machine-readable output** — pipe through `jq` for filtering. Human-readable output includes formatting that is hard to parse.
25-
- **The CLI auto-detects org/project** — most commands work without explicit targets by scanning for DSNs in `.env` files, source code, config defaults, and directory names. Only specify `<org>/<project>` when the CLI reports it can't detect the target or detects the wrong one.
25+
- **The CLI auto-detects org/project** — most commands work without explicit targets by checking `.sentryclirc` config files, scanning for DSNs in `.env` files and source code, and matching directory names. Only specify `<org>/<project>` when the CLI reports it can't detect the target or detects the wrong one.
2626

2727
### Design Principles
2828

@@ -223,7 +223,7 @@ When querying the Events API (directly or via `sentry api`), valid dataset value
223223
- **Wrong issue ID format**: Use `PROJECT-123` (short ID), not the numeric ID `123456789`. The short ID includes the project prefix.
224224
- **Pre-authenticating unnecessarily**: Don't run `sentry auth login` before every command. The CLI detects missing/expired auth and prompts automatically. Only run `sentry auth login` if you need to switch accounts.
225225
- **Missing `--json` for piping**: Human-readable output includes formatting. Use `--json` when parsing output programmatically.
226-
- **Specifying org/project when not needed**: Auto-detection resolves org/project from DSNs, env vars, config defaults, and directory names. Let it work first — only add `<org>/<project>` if the CLI says it can't detect the target or detects the wrong one.
226+
- **Specifying org/project when not needed**: Auto-detection resolves org/project from `.sentryclirc` config files, DSNs, env vars, and directory names. Let it work first — only add `<org>/<project>` if the CLI says it can't detect the target or detects the wrong one.
227227
- **Confusing `--query` syntax**: The `--query` flag uses Sentry search syntax (e.g., `is:unresolved`, `assigned:me`), not free text search.
228228
- **Not using `--web`**: View commands support `-w`/`--web` to open the resource in the browser — useful for sharing links.
229229
- **Fetching API schemas instead of using the CLI**: Prefer `sentry schema` to browse the API and `sentry api` to make requests — the CLI handles authentication and endpoint resolution, so there's rarely a need to download OpenAPI specs separately.

src/cli.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,31 @@
1111
*/
1212

1313
import { getEnv } from "./lib/env.js";
14+
import { applySentryCliRcEnvShim } from "./lib/sentryclirc.js";
15+
16+
/**
17+
* Preload project context: walk up from `cwd` once, finding both the
18+
* project root (for DSN detection) and `.sentryclirc` config (for
19+
* org/project defaults and env shim). Caches both results so later calls
20+
* to `findProjectRoot` and `loadSentryCliRc` are cache hits.
21+
*/
22+
async function preloadProjectContext(cwd: string): Promise<void> {
23+
// Dynamic import keeps the heavy DSN/DB modules out of the completion fast-path
24+
const [{ findProjectRoot }, { setCachedProjectRoot }] = await Promise.all([
25+
import("./lib/dsn/project-root.js"),
26+
import("./lib/db/project-root-cache.js"),
27+
]);
28+
29+
const result = await findProjectRoot(cwd);
30+
await setCachedProjectRoot(cwd, {
31+
projectRoot: result.projectRoot,
32+
reason: result.reason,
33+
});
34+
35+
// Apply .sentryclirc env shim (token, URL) — sentryclirc cache was
36+
// populated as a side effect of findProjectRoot's walk
37+
await applySentryCliRcEnvShim(cwd);
38+
}
1439

1540
/**
1641
* Fast-path: shell completion.
@@ -401,16 +426,26 @@ export async function runCli(cliArgs: string[]): Promise<void> {
401426
* Reads `process.argv`, dispatches to the completion fast-path or the full
402427
* CLI runner, and handles fatal errors. Called from `bin.ts`.
403428
*/
404-
export function startCli(): Promise<void> {
429+
export async function startCli(): Promise<void> {
405430
const args = process.argv.slice(2);
406431

432+
// Completions are a fast-path (~1ms) — skip .sentryclirc I/O.
407433
if (args[0] === "__complete") {
408434
return runCompletion(args.slice(1)).catch(() => {
409435
// Completions should never crash — silently return no results
410436
process.exitCode = 0;
411437
});
412438
}
413439

440+
// Walk up from CWD once to find project root AND .sentryclirc config.
441+
// Caches both so later findProjectRoot / loadSentryCliRc calls are hits.
442+
// Non-fatal — the CLI can still work via env vars and DSN detection.
443+
try {
444+
await preloadProjectContext(process.cwd());
445+
} catch {
446+
// Gracefully degrade: project context is optional for CLI operation.
447+
}
448+
414449
return runCli(args).catch((err) => {
415450
process.stderr.write(`Fatal: ${err}\n`);
416451
process.exitCode = 1;

src/lib/dsn/project-root.ts

Lines changed: 64 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,18 @@
1515

1616
import { opendir, stat } from "node:fs/promises";
1717
import { homedir } from "node:os";
18-
import { dirname, join, resolve } from "node:path";
18+
import { join, resolve } from "node:path";
1919
import { anyTrue } from "../promises.js";
20+
import {
21+
applyGlobalFallbacks,
22+
applySentryCliRcDir,
23+
createSentryCliRcConfig,
24+
CONFIG_FILENAME as SENTRYCLIRC_FILENAME,
25+
type SentryCliRcConfig,
26+
setSentryCliRcCache,
27+
} from "../sentryclirc.js";
2028
import { withFsSpan, withTracingSpan } from "../telemetry.js";
29+
import { walkUpFrom } from "../walk-up.js";
2130
import { ENV_FILES, extractDsnFromEnvContent } from "./env-file.js";
2231
import { handleFileError } from "./fs-utils.js";
2332
import { createDetectedDsn } from "./parser.js";
@@ -74,6 +83,9 @@ const CI_MARKERS = [
7483

7584
/** Language/package markers - strong project boundary */
7685
const LANGUAGE_MARKERS = [
86+
// Sentry CLI config — treated as a project boundary (not definitive root,
87+
// so the walk continues past it to find VCS markers in monorepos)
88+
SENTRYCLIRC_FILENAME,
7789
// JavaScript/Node ecosystem
7890
"package.json",
7991
"deno.json",
@@ -393,14 +405,6 @@ function selectProjectRoot(
393405
return { projectRoot: fallback, reason: "fallback" };
394406
}
395407

396-
/** State tracked during directory walk-up */
397-
type WalkState = {
398-
currentDir: string;
399-
levelsTraversed: number;
400-
languageMarkerAt: string | null;
401-
buildSystemAt: string | null;
402-
};
403-
404408
/**
405409
* Create result when DSN is found in .env file
406410
*/
@@ -447,90 +451,97 @@ function createRepoRootResult(
447451
};
448452
}
449453

454+
/**
455+
* Finalize accumulated sentryclirc config: apply global fallbacks and cache.
456+
*/
457+
async function finalizeSentryCliRc(
458+
cwd: string,
459+
config: SentryCliRcConfig
460+
): Promise<void> {
461+
await applyGlobalFallbacks(config);
462+
setSentryCliRcCache(cwd, config);
463+
}
464+
450465
/**
451466
* Walk up directories searching for project root.
452467
*
453-
* Loop logic:
454-
* 1. Always process starting directory (do-while ensures this)
455-
* 2. Stop at stopBoundary AFTER processing it (break before moving to parent)
456-
* 3. Stop at filesystem root (parentDir === currentDir)
468+
* Uses the shared {@link walkUpFrom} generator for directory traversal
469+
* (with symlink cycle detection). Also reads `.sentryclirc` files at each
470+
* level and populates the sentryclirc cache (with global fallbacks applied),
471+
* so that a later `loadSentryCliRc` call for the same `cwd` is a cache hit
472+
* instead of a second walk.
473+
*
474+
* Stops at the `stopBoundary` (home dir) after processing it, or when the
475+
* generator reaches the filesystem root.
457476
*/
458477
async function walkUpDirectories(
459478
resolvedStart: string,
460479
stopBoundary: string
461480
): Promise<ProjectRootResult> {
462-
const state: WalkState = {
463-
currentDir: resolvedStart,
464-
levelsTraversed: 0,
465-
languageMarkerAt: null,
466-
buildSystemAt: null,
467-
};
481+
let levelsTraversed = 0;
482+
let languageMarkerAt: string | null = null;
483+
let buildSystemAt: string | null = null;
484+
const rcConfig = createSentryCliRcConfig();
468485

469-
// do-while ensures starting directory is always checked,
470-
// even when it equals the stop boundary (e.g., user runs from home dir)
471-
do {
472-
state.levelsTraversed += 1;
486+
for await (const currentDir of walkUpFrom(resolvedStart)) {
487+
levelsTraversed += 1;
473488

474-
const { dsnResult, repoRootResult, hasLang, hasBuild } =
475-
await processDirectoryLevel(
476-
state.currentDir,
477-
state.languageMarkerAt,
478-
state.buildSystemAt
479-
);
489+
// Check project-root markers AND .sentryclirc in parallel
490+
const [{ dsnResult, repoRootResult, hasLang, hasBuild }] =
491+
await Promise.all([
492+
processDirectoryLevel(currentDir, languageMarkerAt, buildSystemAt),
493+
applySentryCliRcDir(rcConfig, currentDir),
494+
]);
480495

481496
// 1. Check for DSN in .env files - immediate return (unless at/above home directory)
482497
// Don't use a .env in the home directory as a project root indicator,
483498
// as users may have global configs that shouldn't define project boundaries
484-
if (dsnResult && state.currentDir !== stopBoundary) {
485-
return createDsnFoundResult(
486-
state.currentDir,
487-
dsnResult,
488-
state.levelsTraversed
489-
);
499+
if (dsnResult && currentDir !== stopBoundary) {
500+
await finalizeSentryCliRc(resolvedStart, rcConfig);
501+
return createDsnFoundResult(currentDir, dsnResult, levelsTraversed);
490502
}
491503

492504
// 2. Check for VCS/CI markers - definitive root, stop walking
493505
if (repoRootResult.found) {
506+
await finalizeSentryCliRc(resolvedStart, rcConfig);
494507
return createRepoRootResult(
495-
state.currentDir,
508+
currentDir,
496509
repoRootResult.type,
497-
state.levelsTraversed,
498-
state.languageMarkerAt
510+
levelsTraversed,
511+
languageMarkerAt
499512
);
500513
}
501514

502515
// 3. Remember language marker (closest to cwd wins)
503-
if (!state.languageMarkerAt && hasLang) {
504-
state.languageMarkerAt = state.currentDir;
516+
if (!languageMarkerAt && hasLang) {
517+
languageMarkerAt = currentDir;
505518
}
506519

507520
// 4. Remember build system marker (last resort)
508-
if (!state.buildSystemAt && hasBuild) {
509-
state.buildSystemAt = state.currentDir;
521+
if (!buildSystemAt && hasBuild) {
522+
buildSystemAt = currentDir;
510523
}
511524

512-
// Move to parent directory (or stop if at boundary/root)
513-
const parentDir = dirname(state.currentDir);
514-
const shouldStop =
515-
state.currentDir === stopBoundary || parentDir === state.currentDir;
516-
if (shouldStop) {
525+
// Stop at boundary after processing it (e.g., home dir)
526+
if (currentDir === stopBoundary) {
517527
break;
518528
}
519-
state.currentDir = parentDir;
520-
// biome-ignore lint/correctness/noConstantCondition: loop exits via break
521-
} while (true);
529+
}
530+
531+
// Populate sentryclirc cache from accumulated data
532+
setSentryCliRcCache(resolvedStart, rcConfig);
522533

523534
// Determine project root from candidates (priority order)
524535
const selected = selectProjectRoot(
525-
state.languageMarkerAt,
526-
state.buildSystemAt,
536+
languageMarkerAt,
537+
buildSystemAt,
527538
resolvedStart
528539
);
529540

530541
return {
531542
projectRoot: selected.projectRoot,
532543
reason: selected.reason,
533-
levelsTraversed: state.levelsTraversed,
544+
levelsTraversed,
534545
};
535546
}
536547

0 commit comments

Comments
 (0)