Skip to content

Commit 832abf9

Browse files
fix: inject auth token into generated .env.sentry-build-plugin files (#706)
## Summary - Threads the auth token from `wizard-runner.ts` through to `local-ops.ts` via a new `authToken` field on `WizardOptions` - During patchset application, when the server creates an env file with an empty `SENTRY_AUTH_TOKEN=`, the CLI injects the locally-available auth token before writing to disk - Token injection only applies to `.env*` files and only fills in empty values — non-empty values are never overwritten - The auth token never leaves the client (not sent to the server workflow) Closes getsentry/cli-init-api#72
1 parent fc2ac72 commit 832abf9

4 files changed

Lines changed: 190 additions & 10 deletions

File tree

src/lib/init/local-ops.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ export async function handleLocalOp(
295295
case "run-commands":
296296
return await runCommands(payload, options.dryRun);
297297
case "apply-patchset":
298-
return await applyPatchset(payload, options.dryRun);
298+
return await applyPatchset(payload, options.dryRun, options.authToken);
299299
case "create-sentry-project":
300300
return await createSentryProject(payload, options);
301301
case "detect-sentry":
@@ -582,15 +582,41 @@ function applyPatchsetDryRun(payload: ApplyPatchsetPayload): LocalOpResult {
582582
return { ok: true, data: { applied } };
583583
}
584584

585+
/** Pattern matching empty or placeholder SENTRY_AUTH_TOKEN values in env files.
586+
* Uses [ \t] (horizontal whitespace) instead of \s to avoid consuming newlines. */
587+
const EMPTY_AUTH_TOKEN_RE =
588+
/^(SENTRY_AUTH_TOKEN[ \t]*=[ \t]*)(?:['"]?[ \t]*['"]?)?[ \t]*$/m;
589+
585590
/**
586591
* Resolve the final file content for a full-content patch (create only),
587-
* pretty-printing JSON files to preserve readable formatting.
592+
* pretty-printing JSON files to preserve readable formatting, and injecting
593+
* the auth token into env files when the server left it empty.
588594
*/
589-
function resolvePatchContent(patch: { path: string; patch: string }): string {
590-
if (!patch.path.endsWith(".json")) {
591-
return patch.patch;
595+
function resolvePatchContent(
596+
patch: { path: string; patch: string },
597+
authToken?: string
598+
): string {
599+
let content = patch.path.endsWith(".json")
600+
? prettyPrintJson(patch.patch, DEFAULT_JSON_INDENT)
601+
: patch.patch;
602+
603+
// Inject the auth token into env files when the AI left the value empty.
604+
// The server never has access to the user's token, so it generates
605+
// SENTRY_AUTH_TOKEN= (empty). We fill it in client-side.
606+
if (authToken && isEnvFile(patch.path) && EMPTY_AUTH_TOKEN_RE.test(content)) {
607+
content = content.replace(
608+
EMPTY_AUTH_TOKEN_RE,
609+
(_, prefix) => `${prefix}${authToken}`
610+
);
592611
}
593-
return prettyPrintJson(patch.patch, DEFAULT_JSON_INDENT);
612+
613+
return content;
614+
}
615+
616+
/** Returns true if the file path looks like a .env file. */
617+
function isEnvFile(filePath: string): boolean {
618+
const name = filePath.split("/").pop() ?? "";
619+
return name === ".env" || name.startsWith(".env.");
594620
}
595621

596622
const VALID_PATCH_ACTIONS = new Set(["create", "modify", "delete"]);
@@ -623,13 +649,15 @@ async function applyEdits(
623649

624650
async function applySinglePatch(
625651
absPath: string,
626-
patch: ApplyPatchsetPatch
652+
patch: ApplyPatchsetPatch,
653+
authToken?: string
627654
): Promise<void> {
628655
switch (patch.action) {
629656
case "create": {
630657
await fs.promises.mkdir(path.dirname(absPath), { recursive: true });
631658
const content = resolvePatchContent(
632-
patch as ApplyPatchsetPatch & { patch: string }
659+
patch as ApplyPatchsetPatch & { patch: string },
660+
authToken
633661
);
634662
await fs.promises.writeFile(absPath, content, "utf-8");
635663
break;
@@ -656,7 +684,8 @@ async function applySinglePatch(
656684

657685
async function applyPatchset(
658686
payload: ApplyPatchsetPayload,
659-
dryRun?: boolean
687+
dryRun?: boolean,
688+
authToken?: string
660689
): Promise<LocalOpResult> {
661690
if (dryRun) {
662691
return applyPatchsetDryRun(payload);
@@ -693,7 +722,7 @@ async function applyPatchset(
693722
}
694723
}
695724

696-
await applySinglePatch(absPath, patch);
725+
await applySinglePatch(absPath, patch, authToken);
697726
applied.push({ path: patch.path, action: patch.action });
698727
}
699728

src/lib/init/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export type WizardOptions = {
1515
org?: string;
1616
/** Explicit project name from CLI arg (e.g., "my-app" from "acme/my-app"). Overrides wizard-detected name. */
1717
project?: string;
18+
/** Auth token for injecting into generated env files (e.g., .env.sentry-build-plugin). Never sent to the server. */
19+
authToken?: string;
1820
};
1921

2022
// Local-op suspend payloads

src/lib/init/wizard-runner.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,12 @@ export async function runWizard(initialOptions: WizardOptions): Promise<void> {
587587
};
588588

589589
const token = getAuthToken();
590+
591+
// Make the auth token available to local-ops for injecting into generated
592+
// env files (e.g. .env.sentry-build-plugin). The token is never sent to
593+
// the remote server — it stays client-side only.
594+
options.authToken = token;
595+
590596
const client = new MastraClient({
591597
baseUrl: MASTRA_API_URL,
592598
headers: token ? { Authorization: `Bearer ${token}` } : {},

test/lib/init/local-ops.test.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,149 @@ describe("handleLocalOp", () => {
10721072
const content = readFileSync(join(testDir, "existing.ts"), "utf-8");
10731073
expect(content).toContain('const updated = "new-value"');
10741074
});
1075+
1076+
test("injects auth token into .env.sentry-build-plugin with empty SENTRY_AUTH_TOKEN", async () => {
1077+
const payload: ApplyPatchsetPayload = {
1078+
type: "local-op",
1079+
operation: "apply-patchset",
1080+
cwd: testDir,
1081+
params: {
1082+
patches: [
1083+
{
1084+
path: ".env.sentry-build-plugin",
1085+
action: "create",
1086+
patch: "SENTRY_AUTH_TOKEN=\n",
1087+
},
1088+
],
1089+
},
1090+
};
1091+
1092+
const opts = makeOptions({
1093+
directory: testDir,
1094+
authToken: "sntrys_test_token_123",
1095+
});
1096+
const result = await handleLocalOp(payload, opts);
1097+
expect(result.ok).toBe(true);
1098+
1099+
const content = readFileSync(
1100+
join(testDir, ".env.sentry-build-plugin"),
1101+
"utf-8"
1102+
);
1103+
expect(content).toBe("SENTRY_AUTH_TOKEN=sntrys_test_token_123\n");
1104+
});
1105+
1106+
test("does not inject auth token into non-env files", async () => {
1107+
const payload: ApplyPatchsetPayload = {
1108+
type: "local-op",
1109+
operation: "apply-patchset",
1110+
cwd: testDir,
1111+
params: {
1112+
patches: [
1113+
{
1114+
path: "config.ts",
1115+
action: "create",
1116+
patch: "SENTRY_AUTH_TOKEN=\n",
1117+
},
1118+
],
1119+
},
1120+
};
1121+
1122+
const opts = makeOptions({
1123+
directory: testDir,
1124+
authToken: "sntrys_test_token_123",
1125+
});
1126+
const result = await handleLocalOp(payload, opts);
1127+
expect(result.ok).toBe(true);
1128+
1129+
const content = readFileSync(join(testDir, "config.ts"), "utf-8");
1130+
expect(content).toBe("SENTRY_AUTH_TOKEN=\n");
1131+
});
1132+
1133+
test("does not overwrite existing non-empty SENTRY_AUTH_TOKEN", async () => {
1134+
const payload: ApplyPatchsetPayload = {
1135+
type: "local-op",
1136+
operation: "apply-patchset",
1137+
cwd: testDir,
1138+
params: {
1139+
patches: [
1140+
{
1141+
path: ".env.sentry-build-plugin",
1142+
action: "create",
1143+
patch: "SENTRY_AUTH_TOKEN=existing_value\n",
1144+
},
1145+
],
1146+
},
1147+
};
1148+
1149+
const opts = makeOptions({
1150+
directory: testDir,
1151+
authToken: "sntrys_different_token",
1152+
});
1153+
const result = await handleLocalOp(payload, opts);
1154+
expect(result.ok).toBe(true);
1155+
1156+
const content = readFileSync(
1157+
join(testDir, ".env.sentry-build-plugin"),
1158+
"utf-8"
1159+
);
1160+
expect(content).toBe("SENTRY_AUTH_TOKEN=existing_value\n");
1161+
});
1162+
1163+
test("handles .env file with empty quoted SENTRY_AUTH_TOKEN", async () => {
1164+
const payload: ApplyPatchsetPayload = {
1165+
type: "local-op",
1166+
operation: "apply-patchset",
1167+
cwd: testDir,
1168+
params: {
1169+
patches: [
1170+
{
1171+
path: ".env.sentry-build-plugin",
1172+
action: "create",
1173+
patch: 'SENTRY_AUTH_TOKEN=""\n',
1174+
},
1175+
],
1176+
},
1177+
};
1178+
1179+
const opts = makeOptions({
1180+
directory: testDir,
1181+
authToken: "sntrys_test_token_456",
1182+
});
1183+
const result = await handleLocalOp(payload, opts);
1184+
expect(result.ok).toBe(true);
1185+
1186+
const content = readFileSync(
1187+
join(testDir, ".env.sentry-build-plugin"),
1188+
"utf-8"
1189+
);
1190+
expect(content).toBe("SENTRY_AUTH_TOKEN=sntrys_test_token_456\n");
1191+
});
1192+
1193+
test("does not inject when no auth token is available", async () => {
1194+
const payload: ApplyPatchsetPayload = {
1195+
type: "local-op",
1196+
operation: "apply-patchset",
1197+
cwd: testDir,
1198+
params: {
1199+
patches: [
1200+
{
1201+
path: ".env.sentry-build-plugin",
1202+
action: "create",
1203+
patch: "SENTRY_AUTH_TOKEN=\n",
1204+
},
1205+
],
1206+
},
1207+
};
1208+
1209+
const result = await handleLocalOp(payload, options);
1210+
expect(result.ok).toBe(true);
1211+
1212+
const content = readFileSync(
1213+
join(testDir, ".env.sentry-build-plugin"),
1214+
"utf-8"
1215+
);
1216+
expect(content).toBe("SENTRY_AUTH_TOKEN=\n");
1217+
});
10751218
});
10761219
});
10771220

0 commit comments

Comments
 (0)