From 23756d277c6d922d6d97a62bdce6a52ab42c6725 Mon Sep 17 00:00:00 2001 From: betegon Date: Fri, 19 Jun 2026 12:14:57 +0200 Subject: [PATCH] fix(init): materialize sentry auth token placeholders --- src/lib/init/tools/apply-patchset.ts | 44 +++++++++++-- test/lib/init/tools/filesystem-tools.test.ts | 65 ++++++++++++++++++++ 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/src/lib/init/tools/apply-patchset.ts b/src/lib/init/tools/apply-patchset.ts index c09c87078..4e79355af 100644 --- a/src/lib/init/tools/apply-patchset.ts +++ b/src/lib/init/tools/apply-patchset.ts @@ -13,6 +13,7 @@ import type { InitToolDefinition, ToolContext } from "./types.js"; /** Pattern matching empty or placeholder SENTRY_AUTH_TOKEN values in env files. */ const EMPTY_AUTH_TOKEN_RE = /^(SENTRY_AUTH_TOKEN[ \t]*=[ \t]*)(?:['"]?[ \t]*['"]?)?[ \t]*$/m; +const AUTH_TOKEN_PLACEHOLDER = "___ORG_AUTH_TOKEN___"; const PATH_SEGMENT_RE = /[/\\]/u; const WINDOWS_DRIVE_RE = /^[A-Za-z]:/; @@ -146,7 +147,11 @@ async function applySinglePatch( break; } case "modify": { - const content = await applyEdits(absPath, patch.path, patch.edits); + const content = materializeAuthToken( + await applyEdits(absPath, patch.path, patch.edits), + patch.path, + authToken + ); await fs.promises.writeFile(absPath, content, "utf-8"); break; } @@ -173,14 +178,36 @@ function resolvePatchContent( ? prettyPrintJson(patch.patch) : patch.patch; - if (authToken && isEnvFile(patch.path) && EMPTY_AUTH_TOKEN_RE.test(content)) { - content = content.replace( + content = materializeAuthToken(content, patch.path, authToken); + + return content; +} + +function materializeAuthToken( + content: string, + filePath: string, + authToken?: string +): string { + if (!authToken) { + return content; + } + + let nextContent = content; + if ( + shouldMaterializeAuthToken(filePath) && + nextContent.includes(AUTH_TOKEN_PLACEHOLDER) + ) { + nextContent = nextContent.replaceAll(AUTH_TOKEN_PLACEHOLDER, authToken); + } + + if (isEnvFile(filePath) && EMPTY_AUTH_TOKEN_RE.test(nextContent)) { + nextContent = nextContent.replace( EMPTY_AUTH_TOKEN_RE, (_, prefix) => `${prefix}${authToken}` ); } - return content; + return nextContent; } function prettyPrintJson(content: string): string { @@ -196,6 +223,15 @@ function isEnvFile(filePath: string): boolean { return name === ".env" || name.startsWith(".env."); } +function shouldMaterializeAuthToken(filePath: string): boolean { + const name = filePath.split(PATH_SEGMENT_RE).at(-1) ?? ""; + return ( + isEnvFile(filePath) || + name === ".sentryclirc" || + name === "sentry.properties" + ); +} + async function applyEdits( absPath: string, filePath: string, diff --git a/test/lib/init/tools/filesystem-tools.test.ts b/test/lib/init/tools/filesystem-tools.test.ts index ca08bfcc8..ac68019bb 100644 --- a/test/lib/init/tools/filesystem-tools.test.ts +++ b/test/lib/init/tools/filesystem-tools.test.ts @@ -128,6 +128,71 @@ describe("filesystem tools", () => { ).toContain("sntrys_test_token_123"); }); + test("materializes auth token placeholders in Sentry CLI config files", async () => { + const result = await executeTool( + { + type: "tool", + operation: "apply-patchset", + cwd: testDir, + params: { + patches: [ + { + path: ".sentryclirc", + action: "create", + patch: "[auth]\ntoken=___ORG_AUTH_TOKEN___\n", + }, + { + path: "ios/sentry.properties", + action: "create", + patch: "auth.token=___ORG_AUTH_TOKEN___\n", + }, + ], + }, + }, + makeContext(testDir) + ); + + expect(result.ok).toBe(true); + expect(fs.readFileSync(path.join(testDir, ".sentryclirc"), "utf-8")).toBe( + "[auth]\ntoken=sntrys_test_token_123\n" + ); + expect( + fs.readFileSync(path.join(testDir, "ios", "sentry.properties"), "utf-8") + ).toBe("auth.token=sntrys_test_token_123\n"); + }); + + test("preserves existing .gitignore entries when adding .sentryclirc", async () => { + fs.writeFileSync(path.join(testDir, ".gitignore"), "node_modules\n"); + + const result = await executeTool( + { + type: "tool", + operation: "apply-patchset", + cwd: testDir, + params: { + patches: [ + { + path: ".gitignore", + action: "modify", + edits: [ + { + oldString: "node_modules\n", + newString: "node_modules\n.sentryclirc\n", + }, + ], + }, + ], + }, + }, + makeContext(testDir) + ); + + expect(result.ok).toBe(true); + expect(fs.readFileSync(path.join(testDir, ".gitignore"), "utf-8")).toBe( + "node_modules\n.sentryclirc\n" + ); + }); + test("rejects unsafe apply-patchset paths before writing", async () => { const unsafePaths: unknown[] = [ null,