Skip to content
Closed
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
14 changes: 12 additions & 2 deletions packages/cli/src/commands/cli/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,9 +516,14 @@ export const setupCommand = buildCommand({
brief: "Skip agent skill installation for AI coding assistants",
default: false,
},
// Legacy internal flag. `cli upgrade` now signals this intent through the
// SENTRY_ENSURE_AUTH_SCOPES env var (see upgrade.ts) so version-skewed
// spawns don't crash on an unknown flag. Retained (still honored below)
// only so that already-deployed pre-fix binaries, which still pass this
// flag, don't fail their argument parse when upgrading to this binary.
"ensure-auth-scopes": {
kind: "boolean",
brief: "Refresh an outdated stored OAuth authorization",
brief: "Refresh an outdated stored OAuth authorization (legacy)",
default: false,
hidden: true as const,
},
Expand Down Expand Up @@ -579,7 +584,12 @@ export const setupCommand = buildCommand({
warn,
});

if (flags["ensure-auth-scopes"]) {
// Honor either the env var (the mechanism `cli upgrade` uses now) or the
// legacy flag (still passed by pre-fix binaries upgrading to this one).
const ensureAuthScopes =
flags["ensure-auth-scopes"] ||
process.env.SENTRY_ENSURE_AUTH_SCOPES === "1";
if (ensureAuthScopes) {
await bestEffort(
"Authorization",
async () => {
Expand Down
22 changes: 16 additions & 6 deletions packages/cli/src/commands/cli/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,16 +536,26 @@ async function runSetupOnNewBinary(opts: SetupOptions): Promise<void> {
if (install) {
args.push("--install");
}
if (ensureAuthScopes) {
args.push("--ensure-auth-scopes");
}
if (noAgentSkills) {
args.push("--no-agent-skills");
}

const env = installDir
? { ...process.env, SENTRY_INSTALL_DIR: installDir }
: undefined;
// Signal "refresh OAuth scopes" through an env var, never a CLI flag. The
// spawned binary may be an ARBITRARY version — a downgrade, or a nightly
// upgrading to a stable release that predates this feature — and its strict
// argument parser aborts (non-zero exit) on any flag it doesn't recognize,
// failing the whole upgrade. An unknown env var is silently ignored, so only
// binaries that understand SENTRY_ENSURE_AUTH_SCOPES act on it. Any future
// setup signal that isn't guaranteed to exist in every upgradeable-from/-to
// version must travel this same way.
const childEnv: NodeJS.ProcessEnv = { ...process.env };
if (installDir) {
childEnv.SENTRY_INSTALL_DIR = installDir;
}
if (ensureAuthScopes) {
childEnv.SENTRY_ENSURE_AUTH_SCOPES = "1";
}
const env = installDir || ensureAuthScopes ? childEnv : undefined;

const exitCode = await spawnWithRetry(binaryPath, args, env);
if (exitCode !== 0) {
Expand Down
33 changes: 32 additions & 1 deletion packages/cli/test/commands/cli/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,38 @@ describe("sentry cli setup", () => {
expect(getOutput()).toBe("");
});

test("checks OAuth scopes when invoked by the upgrade command", async () => {
test("checks OAuth scopes when SENTRY_ENSURE_AUTH_SCOPES is set", async () => {
// This is the mechanism `cli upgrade` uses now: an env var, so a
// version-skewed target binary that predates the feature ignores it
// instead of aborting on an unknown flag.
const { context, restore } = createMockContext({
homeDir: testDir,
env: { SENTRY_ENSURE_AUTH_SCOPES: "1" },
});
restoreStderr = restore;

await run(
app,
[
"cli",
"setup",
"--quiet",
"--no-modify-path",
"--no-completions",
"--no-agent-skills",
],
context
);

expect(scopeRecovery.ensureCurrentOAuthScopes).toHaveBeenCalledOnce();
expect(scopeRecovery.ensureCurrentOAuthScopes).toHaveBeenCalledWith(
interactiveLogin.runInteractiveLogin
);
});

test("still honors the legacy --ensure-auth-scopes flag from pre-fix binaries", async () => {
// Retained for backward compat: already-deployed upgrade binaries pass this
// flag, and their target (this binary) must not fail its argument parse.
const { context, restore } = createMockContext({ homeDir: testDir });
restoreStderr = restore;

Expand Down
30 changes: 23 additions & 7 deletions packages/cli/test/commands/cli/upgrade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,11 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy
useTestConfigDir("test-upgrade-spawn-");

let testDir: string;
let spawnedArgs: Array<{ cmd: string; args: string[] }>;
let spawnedArgs: Array<{
cmd: string;
args: string[];
env?: NodeJS.ProcessEnv;
}>;
let spawnSpy: ReturnType<typeof spyOn>;
let restoreStderr: (() => void) | undefined;

Expand Down Expand Up @@ -709,10 +713,16 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy
// Spy on child_process.spawn — captures args and resolves with exit 0
spawnSpy = vi
.spyOn(child_process, "spawn")
.mockImplementation((cmd: string, args?: readonly string[]) => {
spawnedArgs.push({ cmd, args: [...(args ?? [])] });
return fakeChildProcess(0);
});
.mockImplementation(
(
cmd: string,
args?: readonly string[],
options?: { env?: NodeJS.ProcessEnv }
) => {
spawnedArgs.push({ cmd, args: [...(args ?? [])], env: options?.env });
return fakeChildProcess(0);
}
);
});

afterEach(async () => {
Expand Down Expand Up @@ -780,7 +790,10 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy
expect(setupCall?.args).toContain("--method");
expect(setupCall?.args).toContain("curl");
expect(setupCall?.args).toContain("--install");
expect(setupCall?.args).toContain("--ensure-auth-scopes");
// Scope-refresh intent travels via env var, not a flag a version-skewed
// target binary could reject.
expect(setupCall?.args).not.toContain("--ensure-auth-scopes");
expect(setupCall?.env?.SENTRY_ENSURE_AUTH_SCOPES).toBe("1");
});

test("does not launch interactive auth from JSON upgrades", async () => {
Expand All @@ -794,6 +807,7 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy
const setupCall = spawnedArgs.find((entry) => entry.args.includes("setup"));
expect(setupCall).toBeDefined();
expect(setupCall?.args).not.toContain("--ensure-auth-scopes");
expect(setupCall?.env?.SENTRY_ENSURE_AUTH_SCOPES).toBeUndefined();
});

test("does not pass --no-agent-skills to setup by default", async () => {
Expand Down Expand Up @@ -840,7 +854,8 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy

const setupCall = spawnedArgs.find((entry) => entry.args.includes("setup"));
expect(setupCall?.cmd).toBe(entryPath);
expect(setupCall?.args).toContain("--ensure-auth-scopes");
expect(setupCall?.args).not.toContain("--ensure-auth-scopes");
expect(setupCall?.env?.SENTRY_ENSURE_AUTH_SCOPES).toBe("1");
});

test("runs the new Homebrew binary and keeps JSON upgrades non-interactive", async () => {
Expand All @@ -860,6 +875,7 @@ describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy
const setupCall = spawnedArgs.find((entry) => entry.args.includes("setup"));
expect(setupCall?.cmd).toBe(binaryPath);
expect(setupCall?.args).not.toContain("--ensure-auth-scopes");
expect(setupCall?.env?.SENTRY_ENSURE_AUTH_SCOPES).toBeUndefined();
});

test("reports setup failure when spawn exits non-zero", async () => {
Expand Down
Loading