Skip to content

Commit 9289271

Browse files
committed
feat: support SENTRY_ORG and SENTRY_PROJECT environment variables
Add SENTRY_ORG and SENTRY_PROJECT env var support to context resolution, sitting between CLI flags (highest priority) and config defaults. This addresses the most common user error pattern (CLI-17: 110 events, 50 users) where users run commands without context in CI environments or outside project directories. SENTRY_PROJECT supports the `<org>/<project>` combo notation — presence of `/` auto-splits into org and project, ignoring SENTRY_ORG. This matches the CLI's native positional arg format. Resolution priority is now: 1. Explicit CLI flags 2. SENTRY_ORG / SENTRY_PROJECT env vars (new) 3. Config defaults (sentry login) 4. DSN auto-detection 5. Directory name inference Also updates the ContextError message to mention SENTRY_ORG/SENTRY_PROJECT alongside SENTRY_DSN as alternatives. Fixes CLI-17
1 parent 22d5f39 commit 9289271

File tree

5 files changed

+313
-36
lines changed

5 files changed

+313
-36
lines changed

src/lib/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export class ConfigError extends CliError {
132132

133133
const DEFAULT_CONTEXT_ALTERNATIVES = [
134134
"Run from a directory with a Sentry-configured project",
135-
"Set SENTRY_DSN environment variable",
135+
"Set SENTRY_ORG and SENTRY_PROJECT (or SENTRY_DSN) environment variables",
136136
] as const;
137137

138138
/**

src/lib/resolve-target.ts

Lines changed: 119 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
* Target Resolution
33
*
44
* Shared utilities for resolving organization and project context from
5-
* various sources: CLI flags, config defaults, and DSN detection.
5+
* various sources: CLI flags, environment variables, config defaults,
6+
* and DSN detection.
67
*
78
* Resolution priority (highest to lowest):
89
* 1. Explicit CLI flags
9-
* 2. Config defaults
10-
* 3. DSN auto-detection (source code, .env files, environment variables)
11-
* 4. Directory name inference (matches project slugs with word boundaries)
10+
* 2. SENTRY_ORG / SENTRY_PROJECT environment variables
11+
* 3. Config defaults
12+
* 4. DSN auto-detection (source code, .env files, environment variables)
13+
* 5. Directory name inference (matches project slugs with word boundaries)
1214
*/
1315

1416
import { basename } from "node:path";
@@ -461,6 +463,49 @@ async function inferFromDirectoryName(cwd: string): Promise<ResolvedTargets> {
461463
};
462464
}
463465

466+
/**
467+
* Read org/project from SENTRY_ORG and SENTRY_PROJECT environment variables.
468+
*
469+
* SENTRY_PROJECT supports the `<org>/<project>` combo notation (presence of
470+
* `/` distinguishes it from a plain project slug). When the combo form is
471+
* used, SENTRY_ORG is ignored.
472+
*
473+
* @returns Resolved org+project, org-only, or null if no env vars are set
474+
*/
475+
function resolveFromEnvVars(): {
476+
org: string;
477+
project?: string;
478+
detectedFrom: string;
479+
} | null {
480+
const envProject = process.env.SENTRY_PROJECT?.trim();
481+
482+
// SENTRY_PROJECT=org/project combo takes priority
483+
if (envProject?.includes("/")) {
484+
const slashIdx = envProject.indexOf("/");
485+
const org = envProject.slice(0, slashIdx);
486+
const project = envProject.slice(slashIdx + 1);
487+
if (org && project) {
488+
return { org, project, detectedFrom: "SENTRY_PROJECT env var" };
489+
}
490+
}
491+
492+
const envOrg = process.env.SENTRY_ORG?.trim();
493+
494+
if (envOrg && envProject) {
495+
return {
496+
org: envOrg,
497+
project: envProject,
498+
detectedFrom: "SENTRY_ORG / SENTRY_PROJECT env vars",
499+
};
500+
}
501+
502+
if (envOrg) {
503+
return { org: envOrg, detectedFrom: "SENTRY_ORG env var" };
504+
}
505+
506+
return null;
507+
}
508+
464509
/**
465510
* Resolve all targets for monorepo-aware commands.
466511
*
@@ -469,9 +514,10 @@ async function inferFromDirectoryName(cwd: string): Promise<ResolvedTargets> {
469514
*
470515
* Resolution priority:
471516
* 1. Explicit org and project - returns single target
472-
* 2. Config defaults - returns single target
473-
* 3. DSN auto-detection - may return multiple targets
474-
* 4. Directory name inference - matches project slugs with word boundaries
517+
* 2. SENTRY_ORG / SENTRY_PROJECT env vars - returns single target
518+
* 3. Config defaults - returns single target
519+
* 4. DSN auto-detection - may return multiple targets
520+
* 5. Directory name inference - matches project slugs with word boundaries
475521
*
476522
* @param options - Resolution options with org, project, and cwd
477523
* @returns All resolved targets and optional footer message
@@ -504,7 +550,23 @@ export async function resolveAllTargets(
504550
);
505551
}
506552

507-
// 2. Config defaults
553+
// 2. SENTRY_ORG / SENTRY_PROJECT environment variables
554+
const envVars = resolveFromEnvVars();
555+
if (envVars?.project) {
556+
return {
557+
targets: [
558+
{
559+
org: envVars.org,
560+
project: envVars.project,
561+
orgDisplay: envVars.org,
562+
projectDisplay: envVars.project,
563+
detectedFrom: envVars.detectedFrom,
564+
},
565+
],
566+
};
567+
}
568+
569+
// 3. Config defaults
508570
const defaultOrg = await getDefaultOrganization();
509571
const defaultProject = await getDefaultProject();
510572
if (defaultOrg && defaultProject) {
@@ -520,11 +582,11 @@ export async function resolveAllTargets(
520582
};
521583
}
522584

523-
// 3. DSN auto-detection (may find multiple in monorepos)
585+
// 4. DSN auto-detection (may find multiple in monorepos)
524586
const detection = await detectAllDsns(cwd);
525587

526588
if (detection.all.length === 0) {
527-
// 4. Fallback: infer from directory name
589+
// 5. Fallback: infer from directory name
528590
return inferFromDirectoryName(cwd);
529591
}
530592

@@ -576,9 +638,10 @@ export async function resolveAllTargets(
576638
*
577639
* Resolution priority:
578640
* 1. Explicit org and project - both must be provided together
579-
* 2. Config defaults
580-
* 3. DSN auto-detection
581-
* 4. Directory name inference - matches project slugs with word boundaries
641+
* 2. SENTRY_ORG / SENTRY_PROJECT env vars
642+
* 3. Config defaults
643+
* 4. DSN auto-detection
644+
* 5. Directory name inference - matches project slugs with word boundaries
582645
*
583646
* @param options - Resolution options with org, project, and cwd
584647
* @returns Resolved target, or null if resolution failed
@@ -607,7 +670,19 @@ export async function resolveOrgAndProject(
607670
);
608671
}
609672

610-
// 2. Config defaults
673+
// 2. SENTRY_ORG / SENTRY_PROJECT environment variables
674+
const envVars = resolveFromEnvVars();
675+
if (envVars?.project) {
676+
return {
677+
org: envVars.org,
678+
project: envVars.project,
679+
orgDisplay: envVars.org,
680+
projectDisplay: envVars.project,
681+
detectedFrom: envVars.detectedFrom,
682+
};
683+
}
684+
685+
// 3. Config defaults
611686
const defaultOrg = await getDefaultOrganization();
612687
const defaultProject = await getDefaultProject();
613688
if (defaultOrg && defaultProject) {
@@ -619,7 +694,7 @@ export async function resolveOrgAndProject(
619694
};
620695
}
621696

622-
// 3. DSN auto-detection
697+
// 4. DSN auto-detection
623698
try {
624699
const dsnResult = await resolveFromDsn(cwd);
625700
if (dsnResult) {
@@ -629,32 +704,31 @@ export async function resolveOrgAndProject(
629704
// Fall through to directory inference
630705
}
631706

632-
// 4. Fallback: infer from directory name
707+
// 5. Fallback: infer from directory name
633708
const inferred = await inferFromDirectoryName(cwd);
634-
if (inferred.targets.length > 0) {
635-
const [first] = inferred.targets;
636-
if (first) {
637-
// If multiple matches, note it in detectedFrom
638-
return {
639-
...first,
640-
detectedFrom:
641-
inferred.targets.length > 1
642-
? `${first.detectedFrom} (1 of ${inferred.targets.length} matches)`
643-
: first.detectedFrom,
644-
};
645-
}
709+
const [first] = inferred.targets;
710+
if (!first) {
711+
return null;
646712
}
647713

648-
return null;
714+
// If multiple matches, note it in detectedFrom
715+
return {
716+
...first,
717+
detectedFrom:
718+
inferred.targets.length > 1
719+
? `${first.detectedFrom} (1 of ${inferred.targets.length} matches)`
720+
: first.detectedFrom,
721+
};
649722
}
650723

651724
/**
652725
* Resolve organization only from multiple sources.
653726
*
654727
* Resolution priority:
655728
* 1. Positional argument
656-
* 2. Config defaults
657-
* 3. DSN auto-detection
729+
* 2. SENTRY_ORG / SENTRY_PROJECT env vars
730+
* 3. Config defaults
731+
* 4. DSN auto-detection
658732
*
659733
* @param options - Resolution options with flag and cwd
660734
* @returns Resolved org, or null if resolution failed
@@ -669,13 +743,19 @@ export async function resolveOrg(
669743
return { org };
670744
}
671745

672-
// 2. Config defaults
746+
// 2. SENTRY_ORG / SENTRY_PROJECT environment variables
747+
const envVars = resolveFromEnvVars();
748+
if (envVars) {
749+
return { org: envVars.org, detectedFrom: envVars.detectedFrom };
750+
}
751+
752+
// 3. Config defaults
673753
const defaultOrg = await getDefaultOrganization();
674754
if (defaultOrg) {
675755
return { org: defaultOrg };
676756
}
677757

678-
// 3. DSN auto-detection
758+
// 4. DSN auto-detection
679759
try {
680760
return await resolveOrgFromDsn(cwd);
681761
} catch {
@@ -756,6 +836,12 @@ export async function resolveOrgsForListing(
756836
return { orgs: [orgFlag] };
757837
}
758838

839+
// 2. SENTRY_ORG / SENTRY_PROJECT environment variables
840+
const envVars = resolveFromEnvVars();
841+
if (envVars) {
842+
return { orgs: [envVars.org] };
843+
}
844+
759845
const defaultOrg = await getDefaultOrganization();
760846
if (defaultOrg) {
761847
return { orgs: [defaultOrg] };

0 commit comments

Comments
 (0)