From cfe7e81ed094814374f579a67b012d91717f264a Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Fri, 14 Nov 2025 16:26:53 -0300 Subject: [PATCH 1/4] feat: Add sentry plugin to Gemfile --- src/apple/configure-fastlane.ts | 21 ++++ src/apple/gemfile.ts | 73 ++++++++++++++ test/apple/gemfile.test.ts | 171 ++++++++++++++++++++++++++++++++ 3 files changed, 265 insertions(+) create mode 100644 src/apple/gemfile.ts create mode 100644 test/apple/gemfile.test.ts diff --git a/src/apple/configure-fastlane.ts b/src/apple/configure-fastlane.ts index 1c0065d0d..b63b323cc 100644 --- a/src/apple/configure-fastlane.ts +++ b/src/apple/configure-fastlane.ts @@ -5,6 +5,7 @@ import chalk from 'chalk'; import { traceStep } from '../telemetry'; import { debug } from '../utils/debug'; import * as fastlane from './fastlane'; +import * as gemfile from './gemfile'; export async function configureFastlane({ projectDir, @@ -41,6 +42,26 @@ export async function configureFastlane({ debug(`Fastlane added: ${chalk.cyan(added.toString())}`); if (added) { + debug(`Gemfile found, asking user if they want to configure Gemfile`); + const shouldAddGemfile = await clack.confirm({ + message: + 'Found a Gemfile in your project. Do you want to add the fastlane-plugin-sentry gem to your Gemfile?', + }); + debug(`User wants to add Gemfile: ${chalk.cyan(shouldAddGemfile.toString())}`); + Sentry.setTag('gemfile-desired', shouldAddGemfile); + + if (shouldAddGemfile) { + debug(`Adding sentry_cli action to Gemfile`); + const gemfileUpdated = gemfile.addSentryPluginToGemfile(projectDir); + debug(`Gemfile updated: ${chalk.cyan(gemfileUpdated.toString())}`); + + if (!gemfileUpdated) { + clack.log.warn( + 'Could not edit your Gemfile to add the fastlane-plugin-sentry gem. Please follow the instructions at https://docs.sentry.io/platforms/apple/guides/ios/dsym/#fastlane', + ); + } + } + clack.log.step( 'A new step was added to your fastlane file. Now and you build your project with fastlane, debug symbols and source context will be uploaded to Sentry.', ); diff --git a/src/apple/gemfile.ts b/src/apple/gemfile.ts new file mode 100644 index 000000000..5004e4b94 --- /dev/null +++ b/src/apple/gemfile.ts @@ -0,0 +1,73 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +export function gemFile(projectPath: string): string | null { + const gemfilePath = path.join(projectPath, 'Gemfile'); + return fs.existsSync(gemfilePath) ? gemfilePath : null; +} + +export function addSentryPluginToGemfile(projectDir: string): boolean { + const gemfilePath = gemFile(projectDir); + if (!gemfilePath) { + return false; + } + + const fileContent = fs.readFileSync(gemfilePath, 'utf8'); + + // Check if the sentry plugin is already in the Gemfile + const sentryPluginRegex = + /gem\s+['"]fastlane-plugin-sentry['"]/; + if (sentryPluginRegex.test(fileContent)) { + // Sentry plugin already exists, no need to add it + return true; + } + + // Find the best place to insert the gem + // Look for other fastlane plugins first, then fastlane gem, then add at the end + const fastlanePluginRegex = /gem\s+['"](fastlane-plugin-[^'"]+)['"]/; + const fastlaneGemRegex = /gem\s+['"]fastlane['"]/; + + let insertionPoint: number; + let insertionContent: string; + + const fastlanePluginMatch = fastlanePluginRegex.exec(fileContent); + const fastlaneGemMatch = fastlaneGemRegex.exec(fileContent); + + if (fastlanePluginMatch) { + // Insert after the last fastlane plugin + const lines = fileContent.split('\n'); + let lastPluginLine = -1; + for (let i = 0; i < lines.length; i++) { + if (fastlanePluginRegex.test(lines[i])) { + lastPluginLine = i; + } + } + const beforeInsert = lines.slice(0, lastPluginLine + 1); + const afterInsert = lines.slice(lastPluginLine + 1); + insertionContent = [ + ...beforeInsert, + "gem 'fastlane-plugin-sentry'", + ...afterInsert, + ].join('\n'); + } else if (fastlaneGemMatch) { + // Insert after the fastlane gem + const endOfMatch = fastlaneGemMatch.index + fastlaneGemMatch[0].length; + const nextLineIndex = fileContent.indexOf('\n', endOfMatch); + if (nextLineIndex !== -1) { + insertionPoint = nextLineIndex + 1; + insertionContent = + fileContent.slice(0, insertionPoint) + + "gem 'fastlane-plugin-sentry'\n" + + fileContent.slice(insertionPoint); + } else { + // Add at the end of the file + insertionContent = fileContent + "\ngem 'fastlane-plugin-sentry'\n"; + } + } else { + // Add at the end of the file + insertionContent = fileContent + "\ngem 'fastlane-plugin-sentry'\n"; + } + + fs.writeFileSync(gemfilePath, insertionContent, 'utf8'); + return true; +} diff --git a/test/apple/gemfile.test.ts b/test/apple/gemfile.test.ts new file mode 100644 index 000000000..88478256d --- /dev/null +++ b/test/apple/gemfile.test.ts @@ -0,0 +1,171 @@ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { addSentryPluginToGemfile, gemFile } from '../../src/apple/gemfile'; +import { describe, expect, it } from 'vitest'; + +describe('gemfile', () => { + describe('#gemFile', () => { + describe('file exists', () => { + it('should return path', () => { + // -- Arrange -- + const projectPath = createProjectDir(); + const gemfilePath = createGemfile(projectPath, 'gem "fastlane"'); + + // -- Act -- + const result = gemFile(projectPath); + + // -- Assert -- + expect(result).toBe(gemfilePath); + }); + }); + + describe('file does not exist', () => { + it('should return null', () => { + // -- Arrange -- + const projectPath = createProjectDir(); + // do not create Gemfile + + // -- Act -- + const result = gemFile(projectPath); + + // -- Assert -- + expect(result).toBeNull(); + }); + }); + }); + + describe('#addSentryPluginToGemfile', () => { + describe('Gemfile not found', () => { + it('should return false', () => { + // -- Arrange -- + const projectPath = createProjectDir(); + // do not create Gemfile + + // -- Act -- + const result = addSentryPluginToGemfile(projectPath); + + // -- Assert -- + expect(result).toBe(false); + }); + }); + + describe('sentry plugin already exists', () => { + it('should return true without modifying Gemfile', () => { + // -- Arrange -- + const projectPath = createProjectDir(); + const originalContent = `source 'https://rubygems.org' +gem 'fastlane-plugin-sentry' +gem 'fastlane'`; + const gemfilePath = createGemfile(projectPath, originalContent); + + // -- Act -- + const result = addSentryPluginToGemfile(projectPath); + + // -- Assert -- + expect(result).toBe(true); + expect(fs.readFileSync(gemfilePath, 'utf8')).toBe(originalContent); + }); + }); + + describe('adds sentry plugin to Gemfile', () => { + describe('after other fastlane plugins', () => { + it('should add after the last fastlane plugin', () => { + // -- Arrange -- + const projectPath = createProjectDir(); + const originalContent = `source 'https://rubygems.org' +gem 'fastlane-plugin-badge' +gem 'fastlane-plugin-firebase_app_distribution' +gem 'fastlane'`; + const gemfilePath = createGemfile(projectPath, originalContent); + + // -- Act -- + const result = addSentryPluginToGemfile(projectPath); + + // -- Assert -- + expect(result).toBe(true); + expect(fs.readFileSync(gemfilePath, 'utf8')) + .toBe(`source 'https://rubygems.org' +gem 'fastlane-plugin-badge' +gem 'fastlane-plugin-firebase_app_distribution' +gem 'fastlane-plugin-sentry' +gem 'fastlane'`); + }); + }); + + describe('after fastlane gem', () => { + it('should add after fastlane gem when no other plugins exist', () => { + // -- Arrange -- + const projectPath = createProjectDir(); + const originalContent = `source 'https://rubygems.org' +gem 'fastlane' +gem 'cocoapods'`; + const gemfilePath = createGemfile(projectPath, originalContent); + + // -- Act -- + const result = addSentryPluginToGemfile(projectPath); + + // -- Assert -- + expect(result).toBe(true); + expect(fs.readFileSync(gemfilePath, 'utf8')) + .toBe(`source 'https://rubygems.org' +gem 'fastlane' +gem 'fastlane-plugin-sentry' +gem 'cocoapods'`); + }); + }); + + describe('at the end of file', () => { + it('should add at the end when no fastlane gems exist', () => { + // -- Arrange -- + const projectPath = createProjectDir(); + const originalContent = `source 'https://rubygems.org' +gem 'cocoapods'`; + const gemfilePath = createGemfile(projectPath, originalContent); + + // -- Act -- + const result = addSentryPluginToGemfile(projectPath); + + // -- Assert -- + expect(result).toBe(true); + expect(fs.readFileSync(gemfilePath, 'utf8')) + .toBe(`source 'https://rubygems.org' +gem 'cocoapods' +gem 'fastlane-plugin-sentry' +`); + }); + + it('should add at the end when fastlane gem is at the end of file', () => { + // -- Arrange -- + const projectPath = createProjectDir(); + const originalContent = `source 'https://rubygems.org' +gem 'cocoapods' +gem 'fastlane'`; + const gemfilePath = createGemfile(projectPath, originalContent); + + // -- Act -- + const result = addSentryPluginToGemfile(projectPath); + + // -- Assert -- + expect(result).toBe(true); + expect(fs.readFileSync(gemfilePath, 'utf8')) + .toBe(`source 'https://rubygems.org' +gem 'cocoapods' +gem 'fastlane' +gem 'fastlane-plugin-sentry' +`); + }); + }); + }); + }); +}); + +function createProjectDir() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'test-project')); +} + +function createGemfile(projectPath: string, content: string) { + const gemfilePath = path.join(projectPath, 'Gemfile'); + fs.writeFileSync(gemfilePath, content); + return gemfilePath; +} From 54feb75e7c5c946272e31bf86244d8d51e8a321e Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Fri, 14 Nov 2025 17:17:01 -0300 Subject: [PATCH 2/4] Replace sentry_cli with sentry_debug_files_upload --- src/apple/configure-fastlane.ts | 2 +- src/apple/fastlane.ts | 2 +- src/apple/templates.ts | 2 +- test/apple/fastfile.test.ts | 16 ++++++++-------- test/apple/templates.test.ts | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/apple/configure-fastlane.ts b/src/apple/configure-fastlane.ts index b63b323cc..bbe33d25e 100644 --- a/src/apple/configure-fastlane.ts +++ b/src/apple/configure-fastlane.ts @@ -51,7 +51,7 @@ export async function configureFastlane({ Sentry.setTag('gemfile-desired', shouldAddGemfile); if (shouldAddGemfile) { - debug(`Adding sentry_cli action to Gemfile`); + debug(`Adding fastlane-plugin-sentry action to Gemfile`); const gemfileUpdated = gemfile.addSentryPluginToGemfile(projectDir); debug(`Gemfile updated: ${chalk.cyan(gemfileUpdated.toString())}`); diff --git a/src/apple/fastlane.ts b/src/apple/fastlane.ts index f523d3091..ad232d6fb 100644 --- a/src/apple/fastlane.ts +++ b/src/apple/fastlane.ts @@ -79,7 +79,7 @@ function addSentryToLane( project: string, ): string { const laneContent = content.slice(lane.index, lane.index + lane.length); - const sentryCLIMatch = /sentry_cli\s*\([^)]+\)/gim.exec(laneContent); + const sentryCLIMatch = /sentry_debug_files_upload\s*\([^)]+\)/gim.exec(laneContent); if (sentryCLIMatch) { // Sentry already added to lane. Update it. return ( diff --git a/src/apple/templates.ts b/src/apple/templates.ts index 21ae6c629..7c22e5b58 100644 --- a/src/apple/templates.ts +++ b/src/apple/templates.ts @@ -106,7 +106,7 @@ export function getObjcSnippet(dsn: string, enableLogs: boolean): string { } export function getFastlaneSnippet(org: string, project: string): string { - return ` sentry_cli( + return ` sentry_debug_files_upload( org_slug: '${org}', project_slug: '${project}', include_sources: true diff --git a/test/apple/fastfile.test.ts b/test/apple/fastfile.test.ts index 05d27939f..dd04b39ed 100644 --- a/test/apple/fastfile.test.ts +++ b/test/apple/fastfile.test.ts @@ -267,7 +267,7 @@ end }); describe('#addSentryToLane', () => { - describe('sentry_cli is not present', () => { + describe('sentry_debug_files_upload is not present', () => { it('should return original content', () => { // -- Arrange -- const content = ` @@ -293,7 +293,7 @@ platform :ios do lane :test do puts 'Hello, world!' - sentry_cli( + sentry_debug_files_upload( org_slug: 'test-org', project_slug: 'test-project', include_sources: true @@ -304,7 +304,7 @@ end }); }); - describe('sentry_cli is present', () => { + describe('sentry_debug_files_upload is present', () => { it('should return updated content', () => { // -- Arrange -- const content = ` @@ -312,11 +312,11 @@ platform :ios do lane :test do puts 'Hello, world!' - sentry_cli(org_slug: 'test-org', project_slug: 'test-project') + sentry_debug_files_upload(org_slug: 'test-org', project_slug: 'test-project') end end `; - const lane = { index: 34, length: 92, name: 'test' }; + const lane = { index: 34, length: 110, name: 'test' }; // -- Act -- const result = exportForTesting.addSentryToLane( @@ -333,7 +333,7 @@ platform :ios do lane :test do puts 'Hello, world!' - sentry_cli( + sentry_debug_files_upload( org_slug: 'test-org', project_slug: 'test-project', include_sources: true @@ -446,7 +446,7 @@ platform :ios do lane :test do puts 'Hello, world!' - sentry_cli( + sentry_debug_files_upload( org_slug: 'test-org', project_slug: 'test-project', include_sources: true @@ -518,7 +518,7 @@ end lane :beta do puts 'Beta lane' - sentry_cli( + sentry_debug_files_upload( org_slug: 'test-org', project_slug: 'test-project', include_sources: true diff --git a/test/apple/templates.test.ts b/test/apple/templates.test.ts index ab5bffdf5..d496d604a 100644 --- a/test/apple/templates.test.ts +++ b/test/apple/templates.test.ts @@ -267,7 +267,7 @@ fi // -- Assert -- expect(snippet).toBe( - ` sentry_cli( + ` sentry_debug_files_upload( org_slug: 'test-org', project_slug: 'test-project', include_sources: true From a1af87ab93f02711f9a8e817bbeb56114f5003e6 Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Fri, 14 Nov 2025 17:23:10 -0300 Subject: [PATCH 3/4] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d1afd121..c0a494b0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Feature + +- feat(apple): Add `fastlane-plugin-sentry` to Gemfile + use debug upload ([#1113](https://github.com/getsentry/sentry-wizard/pull/1113)) + ## 6.6.1 fix(telemetry): Handle promise rejections during wizard cancellation ([#1111](https://github.com/getsentry/sentry-wizard/pull/1111)) From a0561cf2381f7b66643a841f2e450475ff42d2ed Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Wed, 19 Nov 2025 09:59:55 +0100 Subject: [PATCH 4/4] Run linter --- src/apple/configure-fastlane.ts | 4 +++- src/apple/fastlane.ts | 4 +++- src/apple/gemfile.ts | 3 +-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/apple/configure-fastlane.ts b/src/apple/configure-fastlane.ts index bbe33d25e..dc2bf713a 100644 --- a/src/apple/configure-fastlane.ts +++ b/src/apple/configure-fastlane.ts @@ -47,7 +47,9 @@ export async function configureFastlane({ message: 'Found a Gemfile in your project. Do you want to add the fastlane-plugin-sentry gem to your Gemfile?', }); - debug(`User wants to add Gemfile: ${chalk.cyan(shouldAddGemfile.toString())}`); + debug( + `User wants to add Gemfile: ${chalk.cyan(shouldAddGemfile.toString())}`, + ); Sentry.setTag('gemfile-desired', shouldAddGemfile); if (shouldAddGemfile) { diff --git a/src/apple/fastlane.ts b/src/apple/fastlane.ts index ad232d6fb..af5c2e680 100644 --- a/src/apple/fastlane.ts +++ b/src/apple/fastlane.ts @@ -79,7 +79,9 @@ function addSentryToLane( project: string, ): string { const laneContent = content.slice(lane.index, lane.index + lane.length); - const sentryCLIMatch = /sentry_debug_files_upload\s*\([^)]+\)/gim.exec(laneContent); + const sentryCLIMatch = /sentry_debug_files_upload\s*\([^)]+\)/gim.exec( + laneContent, + ); if (sentryCLIMatch) { // Sentry already added to lane. Update it. return ( diff --git a/src/apple/gemfile.ts b/src/apple/gemfile.ts index 5004e4b94..cacbfe505 100644 --- a/src/apple/gemfile.ts +++ b/src/apple/gemfile.ts @@ -15,8 +15,7 @@ export function addSentryPluginToGemfile(projectDir: string): boolean { const fileContent = fs.readFileSync(gemfilePath, 'utf8'); // Check if the sentry plugin is already in the Gemfile - const sentryPluginRegex = - /gem\s+['"]fastlane-plugin-sentry['"]/; + const sentryPluginRegex = /gem\s+['"]fastlane-plugin-sentry['"]/; if (sentryPluginRegex.test(fileContent)) { // Sentry plugin already exists, no need to add it return true;