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
1416import { basename } from "node:path" ;
@@ -461,6 +463,55 @@ 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 rawProject = process . env . SENTRY_PROJECT ?. trim ( ) ;
481+
482+ // SENTRY_PROJECT=org/project combo takes priority.
483+ // If the value contains a slash it is always treated as combo notation;
484+ // a malformed combo (empty org or project part) is discarded entirely
485+ // so it cannot leak a slash into a project slug.
486+ if ( rawProject ?. includes ( "/" ) ) {
487+ const slashIdx = rawProject . indexOf ( "/" ) ;
488+ const org = rawProject . slice ( 0 , slashIdx ) ;
489+ const project = rawProject . slice ( slashIdx + 1 ) ;
490+ if ( org && project ) {
491+ return { org, project, detectedFrom : "SENTRY_PROJECT env var" } ;
492+ }
493+ // Malformed combo — fall through without using rawProject as a slug
494+ const envOrg = process . env . SENTRY_ORG ?. trim ( ) ;
495+ return envOrg ? { org : envOrg , detectedFrom : "SENTRY_ORG env var" } : null ;
496+ }
497+
498+ const envOrg = process . env . SENTRY_ORG ?. trim ( ) ;
499+
500+ if ( envOrg && rawProject ) {
501+ return {
502+ org : envOrg ,
503+ project : rawProject ,
504+ detectedFrom : "SENTRY_ORG / SENTRY_PROJECT env vars" ,
505+ } ;
506+ }
507+
508+ if ( envOrg ) {
509+ return { org : envOrg , detectedFrom : "SENTRY_ORG env var" } ;
510+ }
511+
512+ return null ;
513+ }
514+
464515/**
465516 * Resolve all targets for monorepo-aware commands.
466517 *
@@ -469,9 +520,10 @@ async function inferFromDirectoryName(cwd: string): Promise<ResolvedTargets> {
469520 *
470521 * Resolution priority:
471522 * 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
523+ * 2. SENTRY_ORG / SENTRY_PROJECT env vars - returns single target
524+ * 3. Config defaults - returns single target
525+ * 4. DSN auto-detection - may return multiple targets
526+ * 5. Directory name inference - matches project slugs with word boundaries
475527 *
476528 * @param options - Resolution options with org, project, and cwd
477529 * @returns All resolved targets and optional footer message
@@ -504,7 +556,23 @@ export async function resolveAllTargets(
504556 ) ;
505557 }
506558
507- // 2. Config defaults
559+ // 2. SENTRY_ORG / SENTRY_PROJECT environment variables
560+ const envVars = resolveFromEnvVars ( ) ;
561+ if ( envVars ?. project ) {
562+ return {
563+ targets : [
564+ {
565+ org : envVars . org ,
566+ project : envVars . project ,
567+ orgDisplay : envVars . org ,
568+ projectDisplay : envVars . project ,
569+ detectedFrom : envVars . detectedFrom ,
570+ } ,
571+ ] ,
572+ } ;
573+ }
574+
575+ // 3. Config defaults
508576 const defaultOrg = await getDefaultOrganization ( ) ;
509577 const defaultProject = await getDefaultProject ( ) ;
510578 if ( defaultOrg && defaultProject ) {
@@ -520,11 +588,11 @@ export async function resolveAllTargets(
520588 } ;
521589 }
522590
523- // 3 . DSN auto-detection (may find multiple in monorepos)
591+ // 4 . DSN auto-detection (may find multiple in monorepos)
524592 const detection = await detectAllDsns ( cwd ) ;
525593
526594 if ( detection . all . length === 0 ) {
527- // 4 . Fallback: infer from directory name
595+ // 5 . Fallback: infer from directory name
528596 return inferFromDirectoryName ( cwd ) ;
529597 }
530598
@@ -576,9 +644,10 @@ export async function resolveAllTargets(
576644 *
577645 * Resolution priority:
578646 * 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
647+ * 2. SENTRY_ORG / SENTRY_PROJECT env vars
648+ * 3. Config defaults
649+ * 4. DSN auto-detection
650+ * 5. Directory name inference - matches project slugs with word boundaries
582651 *
583652 * @param options - Resolution options with org, project, and cwd
584653 * @returns Resolved target, or null if resolution failed
@@ -607,7 +676,19 @@ export async function resolveOrgAndProject(
607676 ) ;
608677 }
609678
610- // 2. Config defaults
679+ // 2. SENTRY_ORG / SENTRY_PROJECT environment variables
680+ const envVars = resolveFromEnvVars ( ) ;
681+ if ( envVars ?. project ) {
682+ return {
683+ org : envVars . org ,
684+ project : envVars . project ,
685+ orgDisplay : envVars . org ,
686+ projectDisplay : envVars . project ,
687+ detectedFrom : envVars . detectedFrom ,
688+ } ;
689+ }
690+
691+ // 3. Config defaults
611692 const defaultOrg = await getDefaultOrganization ( ) ;
612693 const defaultProject = await getDefaultProject ( ) ;
613694 if ( defaultOrg && defaultProject ) {
@@ -619,7 +700,7 @@ export async function resolveOrgAndProject(
619700 } ;
620701 }
621702
622- // 3 . DSN auto-detection
703+ // 4 . DSN auto-detection
623704 try {
624705 const dsnResult = await resolveFromDsn ( cwd ) ;
625706 if ( dsnResult ) {
@@ -629,32 +710,31 @@ export async function resolveOrgAndProject(
629710 // Fall through to directory inference
630711 }
631712
632- // 4 . Fallback: infer from directory name
713+ // 5 . Fallback: infer from directory name
633714 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- }
715+ const [ first ] = inferred . targets ;
716+ if ( ! first ) {
717+ return null ;
646718 }
647719
648- return null ;
720+ // If multiple matches, note it in detectedFrom
721+ return {
722+ ...first ,
723+ detectedFrom :
724+ inferred . targets . length > 1
725+ ? `${ first . detectedFrom } (1 of ${ inferred . targets . length } matches)`
726+ : first . detectedFrom ,
727+ } ;
649728}
650729
651730/**
652731 * Resolve organization only from multiple sources.
653732 *
654733 * Resolution priority:
655734 * 1. Positional argument
656- * 2. Config defaults
657- * 3. DSN auto-detection
735+ * 2. SENTRY_ORG / SENTRY_PROJECT env vars
736+ * 3. Config defaults
737+ * 4. DSN auto-detection
658738 *
659739 * @param options - Resolution options with flag and cwd
660740 * @returns Resolved org, or null if resolution failed
@@ -669,13 +749,19 @@ export async function resolveOrg(
669749 return { org } ;
670750 }
671751
672- // 2. Config defaults
752+ // 2. SENTRY_ORG / SENTRY_PROJECT environment variables
753+ const envVars = resolveFromEnvVars ( ) ;
754+ if ( envVars ) {
755+ return { org : envVars . org , detectedFrom : envVars . detectedFrom } ;
756+ }
757+
758+ // 3. Config defaults
673759 const defaultOrg = await getDefaultOrganization ( ) ;
674760 if ( defaultOrg ) {
675761 return { org : defaultOrg } ;
676762 }
677763
678- // 3 . DSN auto-detection
764+ // 4 . DSN auto-detection
679765 try {
680766 return await resolveOrgFromDsn ( cwd ) ;
681767 } catch {
@@ -756,6 +842,12 @@ export async function resolveOrgsForListing(
756842 return { orgs : [ orgFlag ] } ;
757843 }
758844
845+ // 2. SENTRY_ORG / SENTRY_PROJECT environment variables
846+ const envVars = resolveFromEnvVars ( ) ;
847+ if ( envVars ) {
848+ return { orgs : [ envVars . org ] } ;
849+ }
850+
759851 const defaultOrg = await getDefaultOrganization ( ) ;
760852 if ( defaultOrg ) {
761853 return { orgs : [ defaultOrg ] } ;
0 commit comments