Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/cli-docs/src/content/docs/migrating-from-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 19 additions & 2 deletions packages/cli/src/lib/resolve-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -686,7 +687,12 @@ async function inferFromDirectoryName(cwd: string): Promise<ResolvedTargets> {
* `/` 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;
Expand Down Expand Up @@ -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;
}

Expand Down
50 changes: 49 additions & 1 deletion packages/cli/test/lib/resolve-target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
});
});

// ============================================================================
Expand Down
Loading