diff --git a/apps/cli-docs/src/content/docs/migrating-from-v3.md b/apps/cli-docs/src/content/docs/migrating-from-v3.md index f4a72b532..549834123 100644 --- a/apps/cli-docs/src/content/docs/migrating-from-v3.md +++ b/apps/cli-docs/src/content/docs/migrating-from-v3.md @@ -601,7 +601,8 @@ Configuration precedence is unchanged in spirit and fully backward compatible: 1. CLI flags 2. `SENTRY_ORG` / `SENTRY_PROJECT` env vars (`SENTRY_PROJECT` accepts - `org/project`) + `org/project`). Without `SENTRY_ORG`, the org embedded in an org auth + token (`sntrys_…`) is used, as in v3 3. Stored defaults (`sentry auth login` / `sentry cli defaults`) 4. DSN auto-detection from your source and `.env` files 5. Directory-name inference diff --git a/packages/cli/src/lib/resolve-target.ts b/packages/cli/src/lib/resolve-target.ts index 61927327f..050020181 100644 --- a/packages/cli/src/lib/resolve-target.ts +++ b/packages/cli/src/lib/resolve-target.ts @@ -38,7 +38,7 @@ import { type ParsedOrgProject, parseOrgProjectArg, } from "./arg-parsing.js"; -import { isAuthenticated } from "./db/auth.js"; +import { getAuthToken, isAuthenticated } from "./db/auth.js"; import { getDefaultOrganization, getDefaultProject, @@ -78,6 +78,7 @@ import { logger } from "./logger.js"; import { resolveEffectiveOrg } from "./region.js"; import { CONFIG_FILENAME, loadSentryCliRc } from "./sentryclirc.js"; import { setOrgProjectContext, withTracingSpan } from "./telemetry.js"; +import { parseSntrysClaim } from "./token-claims.js"; import { isAllDigits, shellQuote } from "./utils.js"; const log = logger.withTag("resolve-target"); @@ -686,7 +687,12 @@ async function inferFromDirectoryName(cwd: string): Promise { * `/` distinguishes it from a plain project slug). When the combo form is * used, SENTRY_ORG is ignored. * - * @returns Resolved org+project, org-only, or null if no env vars are set + * When no org is set, the org embedded in an org auth token (`sntrys_`) is + * used, matching legacy sentry-cli. Such a token only works for that org, so + * it is the only org that can succeed. This is what lets bundler-plugin and CI + * setups pass just a token and a project (CLI-3B). + * + * @returns Resolved org+project, org-only, or null if no org source is set */ function resolveFromEnvVars(): { org: string; @@ -725,6 +731,17 @@ function resolveFromEnvVars(): { return { org: envOrg, detectedFrom: "SENTRY_ORG env var" }; } + const tokenOrg = parseSntrysClaim(getAuthToken())?.org; + if (tokenOrg) { + return rawProject + ? { + org: tokenOrg, + project: rawProject, + detectedFrom: "org auth token / SENTRY_PROJECT env var", + } + : { org: tokenOrg, detectedFrom: "org auth token" }; + } + return null; } diff --git a/packages/cli/test/lib/resolve-target.test.ts b/packages/cli/test/lib/resolve-target.test.ts index 4f5e67337..3856547cb 100644 --- a/packages/cli/test/lib/resolve-target.test.ts +++ b/packages/cli/test/lib/resolve-target.test.ts @@ -44,7 +44,7 @@ import { resetCacheState, } from "../../src/lib/response-cache.js"; import { resetAuthenticatedFetch } from "../../src/lib/sentry-client.js"; -import { mockFetch, useTestConfigDir } from "../helpers.js"; +import { mintSntrysToken, mockFetch, useTestConfigDir } from "../helpers.js"; // ============================================================================ // Arbitraries for Property-Based Testing @@ -357,6 +357,54 @@ describe("Environment variable resolution (SENTRY_ORG / SENTRY_PROJECT)", () => const result = await resolveOrgsForListing(undefined, "/tmp"); expect(result.orgs).toContain("env-org"); }); + + // --- org auth token (sntrys_) org claim --- + + function useOrgAuthToken(org: string): void { + setAuthToken( + mintSntrysToken({ iat: 1_700_000_000, url: DEFAULT_SENTRY_URL, org }), + ); + } + + test("resolveOrg: falls back to the org embedded in an org auth token", async () => { + useOrgAuthToken("token-org"); + const result = await resolveOrg({ cwd: "/tmp" }); + expect(result).toEqual({ + org: "token-org", + detectedFrom: "org auth token", + }); + }); + + test("resolveOrgAndProject: org auth token org + SENTRY_PROJECT", async () => { + useOrgAuthToken("token-org"); + process.env.SENTRY_PROJECT = "web"; + const result = await resolveOrgAndProject({ cwd: "/tmp" }); + expect(result?.org).toBe("token-org"); + expect(result?.project).toBe("web"); + expect(result?.detectedFrom).toContain("org auth token"); + }); + + test("resolveAllTargets: org auth token org + SENTRY_PROJECT", async () => { + useOrgAuthToken("token-org"); + process.env.SENTRY_PROJECT = "web"; + const result = await resolveAllTargets({ cwd: "/tmp" }); + expect(result.targets).toHaveLength(1); + expect(result.targets[0]?.org).toBe("token-org"); + expect(result.targets[0]?.project).toBe("web"); + }); + + test("resolveOrg: SENTRY_ORG wins over the org auth token org", async () => { + useOrgAuthToken("token-org"); + process.env.SENTRY_ORG = "env-org"; + const result = await resolveOrg({ cwd: "/tmp" }); + expect(result?.org).toBe("env-org"); + }); + + test("resolveOrg: a user token contributes no org", async () => { + setAuthToken("sntryu_user-token"); + const result = await resolveOrg({ cwd: "/tmp" }); + expect(result?.detectedFrom ?? "").not.toContain("org auth token"); + }); }); // ============================================================================