Skip to content

Commit 3b880a9

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 3b880a9

File tree

14 files changed

+1657
-84
lines changed

14 files changed

+1657
-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: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212

1313
import { getEnv } from "./lib/env.js";
14+
import { applySentryCliRcEnvShim } from "./lib/sentryclirc.js";
1415

1516
/**
1617
* Fast-path: shell completion.
@@ -401,16 +402,26 @@ export async function runCli(cliArgs: string[]): Promise<void> {
401402
* Reads `process.argv`, dispatches to the completion fast-path or the full
402403
* CLI runner, and handles fatal errors. Called from `bin.ts`.
403404
*/
404-
export function startCli(): Promise<void> {
405+
export async function startCli(): Promise<void> {
405406
const args = process.argv.slice(2);
406407

408+
// Completions are a fast-path (~1ms) — skip .sentryclirc I/O.
407409
if (args[0] === "__complete") {
408410
return runCompletion(args.slice(1)).catch(() => {
409411
// Completions should never crash — silently return no results
410412
process.exitCode = 0;
411413
});
412414
}
413415

416+
// Load .sentryclirc config and apply env shims for auth token and URL.
417+
// Must run before any auth or API calls so the env vars are in place.
418+
// Errors are non-fatal — the CLI can still work via env vars and DSN detection.
419+
try {
420+
await applySentryCliRcEnvShim(process.cwd());
421+
} catch {
422+
// Gracefully degrade: .sentryclirc is optional, not required for CLI operation.
423+
}
424+
414425
return runCli(args).catch((err) => {
415426
process.stderr.write(`Fatal: ${err}\n`);
416427
process.exitCode = 1;

src/lib/dsn/project-root.ts

Lines changed: 47 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@
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+
applySentryCliRcDir,
22+
createSentryCliRcConfig,
23+
setSentryCliRcCache,
24+
} from "../sentryclirc.js";
2025
import { withFsSpan, withTracingSpan } from "../telemetry.js";
26+
import { walkUpFrom } from "../walk-up.js";
2127
import { ENV_FILES, extractDsnFromEnvContent } from "./env-file.js";
2228
import { handleFileError } from "./fs-utils.js";
2329
import { createDetectedDsn } from "./parser.js";
@@ -393,14 +399,6 @@ function selectProjectRoot(
393399
return { projectRoot: fallback, reason: "fallback" };
394400
}
395401

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-
404402
/**
405403
* Create result when DSN is found in .env file
406404
*/
@@ -450,87 +448,83 @@ function createRepoRootResult(
450448
/**
451449
* Walk up directories searching for project root.
452450
*
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)
451+
* Uses the shared {@link walkUpFrom} generator for directory traversal
452+
* (with symlink cycle detection). Also reads `.sentryclirc` files at each
453+
* level and populates the sentryclirc cache, so that a later
454+
* `loadSentryCliRc` call for the same `cwd` is a cache hit instead of a
455+
* second walk.
456+
*
457+
* Stops at the `stopBoundary` (home dir) after processing it, or when the
458+
* generator reaches the filesystem root.
457459
*/
458460
async function walkUpDirectories(
459461
resolvedStart: string,
460462
stopBoundary: string
461463
): Promise<ProjectRootResult> {
462-
const state: WalkState = {
463-
currentDir: resolvedStart,
464-
levelsTraversed: 0,
465-
languageMarkerAt: null,
466-
buildSystemAt: null,
467-
};
464+
let levelsTraversed = 0;
465+
let languageMarkerAt: string | null = null;
466+
let buildSystemAt: string | null = null;
467+
const rcConfig = createSentryCliRcConfig();
468468

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;
469+
for await (const currentDir of walkUpFrom(resolvedStart)) {
470+
levelsTraversed += 1;
473471

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

481479
// 1. Check for DSN in .env files - immediate return (unless at/above home directory)
482480
// Don't use a .env in the home directory as a project root indicator,
483481
// 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-
);
482+
if (dsnResult && currentDir !== stopBoundary) {
483+
setSentryCliRcCache(resolvedStart, rcConfig);
484+
return createDsnFoundResult(currentDir, dsnResult, levelsTraversed);
490485
}
491486

492487
// 2. Check for VCS/CI markers - definitive root, stop walking
493488
if (repoRootResult.found) {
489+
setSentryCliRcCache(resolvedStart, rcConfig);
494490
return createRepoRootResult(
495-
state.currentDir,
491+
currentDir,
496492
repoRootResult.type,
497-
state.levelsTraversed,
498-
state.languageMarkerAt
493+
levelsTraversed,
494+
languageMarkerAt
499495
);
500496
}
501497

502498
// 3. Remember language marker (closest to cwd wins)
503-
if (!state.languageMarkerAt && hasLang) {
504-
state.languageMarkerAt = state.currentDir;
499+
if (!languageMarkerAt && hasLang) {
500+
languageMarkerAt = currentDir;
505501
}
506502

507503
// 4. Remember build system marker (last resort)
508-
if (!state.buildSystemAt && hasBuild) {
509-
state.buildSystemAt = state.currentDir;
504+
if (!buildSystemAt && hasBuild) {
505+
buildSystemAt = currentDir;
510506
}
511507

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) {
508+
// Stop at boundary after processing it (e.g., home dir)
509+
if (currentDir === stopBoundary) {
517510
break;
518511
}
519-
state.currentDir = parentDir;
520-
// biome-ignore lint/correctness/noConstantCondition: loop exits via break
521-
} while (true);
512+
}
513+
514+
// Populate sentryclirc cache from accumulated data
515+
setSentryCliRcCache(resolvedStart, rcConfig);
522516

523517
// Determine project root from candidates (priority order)
524518
const selected = selectProjectRoot(
525-
state.languageMarkerAt,
526-
state.buildSystemAt,
519+
languageMarkerAt,
520+
buildSystemAt,
527521
resolvedStart
528522
);
529523

530524
return {
531525
projectRoot: selected.projectRoot,
532526
reason: selected.reason,
533-
levelsTraversed: state.levelsTraversed,
527+
levelsTraversed,
534528
};
535529
}
536530

0 commit comments

Comments
 (0)