From 1887093abd4a1331ead37d3cadae865fda2344d1 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 30 Jun 2026 13:26:37 +0200 Subject: [PATCH 1/4] feat(apple)!: remove CocoaPods as an installation option (COCOA-1241) CocoaPods trunk is going read-only on Dec 2, 2026. The wizard now only offers Swift Package Manager (SPM) for Apple SDK setup. - Delete src/apple/cocoapod.ts and its tests - Simplify configure-package-manager to always use SPM - Remove shouldUseSPM parameter from configure-xcode-project - Remove pod install prompt from React Native wizard - Update e2e tests to remove pod install expectations Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 4 + e2e-tests/tests/react-native.test.ts | 8 - src/apple/apple-wizard.ts | 3 +- src/apple/cocoapod.ts | 73 ----- src/apple/configure-package-manager.ts | 75 +---- src/apple/configure-xcode-project.ts | 16 +- src/react-native/react-native-wizard.ts | 27 -- test/apple/cocoapod.test.ts | 321 ------------------- test/apple/configure-package-manager.test.ts | 140 +------- 9 files changed, 22 insertions(+), 645 deletions(-) delete mode 100644 src/apple/cocoapod.ts delete mode 100644 test/apple/cocoapod.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index bed89bbf4..18634bc64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 6.13.0 +### Breaking Changes + +- feat(apple)!: Remove CocoaPods as an installation option (COCOA-1241). The wizard now only offers Swift Package Manager (SPM) for Apple SDK setup, since CocoaPods trunk is going read-only. + ### Fixes - fix(remix): Use `npx @sentry/remix --upload-sourcemaps` instead of `sentry-upload-sourcemaps` to avoid global bin collisions diff --git a/e2e-tests/tests/react-native.test.ts b/e2e-tests/tests/react-native.test.ts index d1297bb59..1f71c0cd3 100644 --- a/e2e-tests/tests/react-native.test.ts +++ b/e2e-tests/tests/react-native.test.ts @@ -40,14 +40,6 @@ describe('ReactNative', () => { .whenAsked('Do you want to enable Logs') .respondWith(KEYS.ENTER); - // Only prompt to run `pod install` if running on macOS. - if (process.platform === 'darwin') { - wizardInteraction - .whenAsked('Do you want to run `pod install` now?') - .respondWith(KEYS.ENTER) - .expectOutput('Pods installed.', { timeout: 240_000 }); - } - wizardExitCode = await wizardInteraction .expectOutput('Added Sentry.init to App.tsx') .whenAsked( diff --git a/src/apple/apple-wizard.ts b/src/apple/apple-wizard.ts index 27d7016e6..b61fc3d8d 100644 --- a/src/apple/apple-wizard.ts +++ b/src/apple/apple-wizard.ts @@ -76,7 +76,7 @@ async function runAppleWizardWithTelementry( }); // Step - Set up Package Manager - const { shouldUseSPM } = await configurePackageManager({ + configurePackageManager({ projectDir, }); @@ -85,7 +85,6 @@ async function runAppleWizardWithTelementry( xcProject, project: selectedProject, target, - shouldUseSPM, }); // Step - Feature Selection diff --git a/src/apple/cocoapod.ts b/src/apple/cocoapod.ts deleted file mode 100644 index ac5a76868..000000000 --- a/src/apple/cocoapod.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as bash from '../utils/bash'; -import * as Sentry from '@sentry/node'; -// @ts-expect-error - clack is ESM and TS complains about that. It works though -import * as clack from '@clack/prompts'; -import chalk from 'chalk'; - -export function usesCocoaPod(projPath: string): boolean { - return fs.existsSync(path.join(projPath, 'Podfile')); -} - -export async function addCocoaPods(projPath: string): Promise { - const podfile = path.join(projPath, 'Podfile'); - - const podContent = fs.readFileSync(podfile, 'utf8'); - - if ( - /^\s*pod\s+(['"]Sentry['"]|['"]SentrySwiftUI['"])\s*$/im.test(podContent) - ) { - // Already have Sentry pod - return true; - } - - let podMatch = /^( *)pod\s+['"](\w+)['"] *$/im.exec(podContent); - if (!podMatch) { - // No Podfile is empty, will try to add Sentry pod after "use_frameworks!" - const frameworkMatch = /^( *)use_frameworks![^\n]* *$/im.exec(podContent); - if (!frameworkMatch) { - return false; - } - podMatch = frameworkMatch; - } - - const insertIndex = podMatch.index + podMatch[0].length; - const newFileContent = - podContent.slice(0, insertIndex) + - '\n' + - podMatch[1] + - "pod 'Sentry'\n" + - podContent.slice(insertIndex); - fs.writeFileSync(podfile, newFileContent, 'utf8'); - - clack.log.step('Sentry pod added to the project podFile.'); - - await podInstall(); - - return true; -} - -export async function podInstall(dir = '.') { - const installSpinner = clack.spinner(); - installSpinner.start("Running 'pod install'. This may take a few minutes..."); - - try { - await bash.execute(`cd ${dir} && pod repo update`); - await bash.execute(`cd ${dir} && pod install --silent`); - installSpinner.stop('Pods installed.'); - Sentry.setTag('pods-installed', true); - } catch (e) { - installSpinner.stop('Failed to install pods.'); - Sentry.setTag('pods-installed', false); - clack.log.error( - `${chalk.red( - 'Encountered the following error during pods installation:', - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - )}\n\n${e}\n\n${chalk.dim( - 'If you think this issue is caused by the Sentry wizard, let us know here:\nhttps://github.com/getsentry/sentry-wizard/issues', - )}`, - ); - Sentry.captureException('Sentry pod install failed.'); - } -} diff --git a/src/apple/configure-package-manager.ts b/src/apple/configure-package-manager.ts index 23ee351eb..1594bd9ff 100644 --- a/src/apple/configure-package-manager.ts +++ b/src/apple/configure-package-manager.ts @@ -1,79 +1,14 @@ -// @ts-expect-error - clack is ESM and TS complains about that. It works though -import clack from '@clack/prompts'; import * as Sentry from '@sentry/node'; -import chalk from 'chalk'; -import { traceStep } from '../telemetry'; -import { abortIfCancelled } from '../utils/clack'; import { debug } from '../utils/debug'; -import * as cocoapod from './cocoapod'; -export async function configurePackageManager({ - projectDir, +export function configurePackageManager({ + projectDir: _projectDir, }: { projectDir: string; }) { - debug( - `Checking if CocoaPods is installed at path: ${chalk.cyan(projectDir)}`, - ); + debug('Using Swift Package Manager (SPM) as the package manager'); + Sentry.setTag('package-manager', 'SPM'); - // Xcode ships with the Swift Package Manager and potentially using CocoaPods. - // We need to check if the user has CocoaPods set up. - let shouldUseSPM = true; - - const isCocoaPodsAvailable = cocoapod.usesCocoaPod(projectDir); - Sentry.setTag('cocoapod-exists', isCocoaPodsAvailable); - debug(`CocoaPods is ${isCocoaPodsAvailable ? 'installed' : 'not installed'}`); - - if (isCocoaPodsAvailable) { - clack.log.warn( - 'CocoaPods is being deprecated. No new updates will be released after June 2026.\nWe recommend migrating to Swift Package Manager (SPM).', - ); - - debug('Asking user to choose a package manager'); - const pm: 'SPM' | 'CocoaPods' = await traceStep( - 'Choose a package manager', - () => - abortIfCancelled( - clack.select({ - message: - 'Which package manager would you like to use to add Sentry?', - options: [ - { - value: 'SPM', - label: 'Swift Package Manager', - hint: 'Recommended', - }, - { - value: 'CocoaPods', - label: 'CocoaPods', - hint: 'Deprecated - no updates after June 2026', - }, - ], - }), - ), - ); - debug(`User chose package manager: ${chalk.cyan(pm)}`); - - shouldUseSPM = pm === 'SPM'; - - if (!shouldUseSPM) { - debug('Adding CocoaPods reference'); - const podAdded = await traceStep('Add CocoaPods reference', () => - cocoapod.addCocoaPods(projectDir), - ); - Sentry.setTag('cocoapod-added', podAdded); - debug(`CocoaPods reference added: ${chalk.cyan(podAdded.toString())}`); - - if (!podAdded) { - clack.log.warn( - "Could not add Sentry pod to your Podfile. You'll have to add it manually.\nPlease follow the instructions at https://docs.sentry.io/platforms/apple/guides/ios/#install", - ); - } - } - } - debug(`Should use SPM: ${chalk.cyan(shouldUseSPM.toString())}`); - Sentry.setTag('package-manager', shouldUseSPM ? 'SPM' : 'CocoaPods'); - - return { shouldUseSPM }; + return { shouldUseSPM: true }; } diff --git a/src/apple/configure-xcode-project.ts b/src/apple/configure-xcode-project.ts index 4a826e35f..e8a31bdca 100644 --- a/src/apple/configure-xcode-project.ts +++ b/src/apple/configure-xcode-project.ts @@ -10,25 +10,21 @@ export function configureXcodeProject({ xcProject, project, target, - shouldUseSPM, }: { xcProject: XcodeProject; project: SentryProjectData; target: string; - shouldUseSPM: boolean; }) { traceStep('Update Xcode project', () => { xcProject.updateXcodeProject( project, target, - shouldUseSPM - ? { - product: sentrySwiftPackageProductSpec, - existingFrameworkComment: - SENTRY_SPM_ALREADY_LINKED_FRAMEWORK_COMMENT, - successMessage: 'Added Sentry SPM dependency to your project', - } - : undefined, + { + product: sentrySwiftPackageProductSpec, + existingFrameworkComment: + SENTRY_SPM_ALREADY_LINKED_FRAMEWORK_COMMENT, + successMessage: 'Added Sentry SPM dependency to your project', + }, true, ); }); diff --git a/src/react-native/react-native-wizard.ts b/src/react-native/react-native-wizard.ts index cf3851436..719cfe11e 100644 --- a/src/react-native/react-native-wizard.ts +++ b/src/react-native/react-native-wizard.ts @@ -5,8 +5,6 @@ import chalk from 'chalk'; import * as fs from 'fs'; import * as Sentry from '@sentry/node'; -import { platform } from 'os'; -import { podInstall } from '../apple/cocoapod'; import { traceStep, withTelemetry } from '../telemetry'; import { offerProjectScopedMcpConfig } from '../utils/clack/mcp-config'; import { @@ -349,10 +347,6 @@ async function patchXcodeFiles(config: RNCliSetupConfigContent) { gitignore: false, }); - if (platform() === 'darwin' && (await confirmPodInstall())) { - await traceStep('pod-install', () => podInstall('ios')); - } - const xcodeProjectPath = traceStep('find-xcode-project', () => getFirstMatchedPath(XCODE_PROJECT), ); @@ -483,24 +477,3 @@ async function patchAndroidFiles(config: RNCliSetupConfigContent) { chalk.green(`Android ${chalk.cyan('app/build.gradle')} saved.`), ); } - -async function confirmPodInstall(): Promise { - return traceStep('confirm-pod-install', async () => { - const continueWithPodInstall = await abortIfCancelled( - clack.select({ - message: 'Do you want to run `pod install` now?', - options: [ - { - value: true, - label: 'Yes', - hint: 'Recommended for smaller projects, this might take several minutes', - }, - { value: false, label: `No, I'll do it later` }, - ], - initialValue: true, - }), - ); - Sentry.setTag('continue-with-pod-install', continueWithPodInstall); - return continueWithPodInstall; - }); -} diff --git a/test/apple/cocoapod.test.ts b/test/apple/cocoapod.test.ts deleted file mode 100644 index 0164bfc6c..000000000 --- a/test/apple/cocoapod.test.ts +++ /dev/null @@ -1,321 +0,0 @@ -import * as Sentry from '@sentry/node'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import { - addCocoaPods, - podInstall, - usesCocoaPod, -} from '../../src/apple/cocoapod'; -import * as bash from '../../src/utils/bash'; -// @ts-expect-error - clack is ESM and TS complains about that. It works though -import * as clack from '@clack/prompts'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -vi.mock('@clack/prompts', async () => ({ - __esModule: true, - ...(await vi.importActual('@clack/prompts')), -})); - -vi.mock('../../src/utils/bash'); -vi.mock('@sentry/node', async () => { - const actual = await vi.importActual( - '@sentry/node', - ); - return { - ...actual, - setTag: vi.fn(), - captureException: vi.fn(() => 'id'), - }; -}); - -const clackSpinnerMock = { - start: vi.fn(), - stop: vi.fn(), - message: vi.fn(), -}; - -describe('cocoapod', () => { - beforeEach(() => { - vi.spyOn(clack, 'spinner').mockReturnValue(clackSpinnerMock); - vi.spyOn(clack.log, 'error').mockImplementation(() => { - /* empty */ - }); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - describe('usesCocoaPod', () => { - describe('Podfile exists', () => { - it('should return true', () => { - // -- Arrange -- - const projPath = path.join(os.tmpdir(), 'test-project-with-podfile'); - const tempDir = fs.mkdtempSync(projPath); - - const podfile = path.join(tempDir, 'Podfile'); - fs.writeFileSync(podfile, ''); - - // -- Act -- - const result = usesCocoaPod(tempDir); - - // -- Assert -- - expect(result).toBeTruthy(); - }); - }); - - describe('Podfile does not exist', () => { - it('should return false', () => { - // -- Arrange -- - const projPath = path.join(os.tmpdir(), 'test-project-without-podfile'); - const tempDir = fs.mkdtempSync(projPath); - - // -- Act -- - const result = usesCocoaPod(tempDir); - - // -- Assert -- - expect(result).toBeFalsy(); - }); - }); - }); - - describe('addCocoaPods', () => { - describe('Podfile does not exist', () => { - it('should throw an error', async () => { - // -- Arrange -- - const projPath = path.join(os.tmpdir(), 'test-project-without-podfile'); - const tempDir = fs.mkdtempSync(projPath); - - // -- Act & Assert -- - await expect(addCocoaPods(tempDir)).rejects.toThrow( - 'ENOENT: no such file or directory, open', - ); - }); - }); - - describe('Podfile exists', () => { - describe('Podfile includes Sentry pod', () => { - const variations = [ - { - case: 'simple', - content: 'pod "Sentry"', - }, - { - case: 'with-swiftui', - content: 'pod "SentrySwiftUI"', - }, - { - case: 'leading-space', - content: ' pod "Sentry"', - }, - { - case: 'leading-space-and-swiftui', - content: ' pod "SentrySwiftUI"', - }, - { - case: 'trailing-space', - content: 'pod "Sentry" ', - }, - { - case: 'trailing-space-and-swiftui', - content: 'pod "SentrySwiftUI" ', - }, - { - case: 'single-quotes', - content: "pod 'Sentry'", - }, - { - case: 'double-quotes', - content: 'pod "Sentry"', - }, - ]; - for (const variation of variations) { - it(`should not change the Podfile for ${variation.case}`, async () => { - // -- Arrange -- - const projPath = fs.mkdtempSync(path.join(os.tmpdir(), 'project')); - fs.mkdirSync(projPath, { - recursive: true, - }); - - const podfile = path.join(projPath, 'Podfile'); - fs.writeFileSync(podfile, variation.content, 'utf8'); - - // -- Act -- - const result = await addCocoaPods(projPath); - - // -- Assert -- - expect(result).toBeTruthy(); - expect(fs.readFileSync(podfile, 'utf8')).toBe(variation.content); - }); - } - }); - - describe('Podfile includes no other pods', () => { - describe('Podfile does not include use_frameworks!', () => { - it('should not change the Podfile', async () => { - // -- Arrange -- - const projPath = fs.mkdtempSync(path.join(os.tmpdir(), 'project')); - fs.mkdirSync(projPath, { - recursive: true, - }); - - const podfile = path.join(projPath, 'Podfile'); - fs.writeFileSync(podfile, '', 'utf8'); - - // -- Act -- - const result = await addCocoaPods(projPath); - - // -- Assert -- - expect(result).toBeFalsy(); - expect(fs.readFileSync(podfile, 'utf8')).toBe(''); - }); - }); - - describe('Podfile includes use_frameworks!', () => { - it('should change the Podfile', async () => { - // -- Arrange -- - const projPath = fs.mkdtempSync(path.join(os.tmpdir(), 'project')); - fs.mkdirSync(projPath, { - recursive: true, - }); - - const podfile = path.join(projPath, 'Podfile'); - fs.writeFileSync(podfile, `use_frameworks!`, 'utf8'); - - // -- Act -- - const result = await addCocoaPods(projPath); - - // -- Assert -- - expect(result).toBeTruthy(); - expect(fs.readFileSync(podfile, 'utf8')).toBe( - `use_frameworks!\npod 'Sentry'\n`, - ); - }); - }); - }); - - describe('Podfile includes other pods', () => { - it('should append Sentry pod after last pod', async () => { - // -- Arrange -- - const projPath = fs.mkdtempSync(path.join(os.tmpdir(), 'project')); - fs.mkdirSync(projPath, { - recursive: true, - }); - - const podfile = path.join(projPath, 'Podfile'); - fs.writeFileSync(podfile, 'pod "OtherPod"', 'utf8'); - - // -- Act -- - const result = await addCocoaPods(projPath); - - // -- Assert -- - expect(result).toBeTruthy(); - expect(fs.readFileSync(podfile, 'utf8')).toBe( - `pod "OtherPod"\npod 'Sentry'\n`, - ); - }); - }); - }); - }); - - describe('podInstall', () => { - let workDir: string; - - beforeEach(() => { - workDir = path.join(os.tmpdir(), 'test-project'); - }); - - describe('any bash scripts fail', () => { - beforeEach(() => { - vi.spyOn(bash, 'execute').mockRejectedValue(new Error('test error')); - }); - - it('should not throw an error', async () => { - // -- Act & Assert -- - await expect(podInstall(workDir)).resolves.not.toThrow(); - }); - - it('should set tag', async () => { - // -- Act -- - await podInstall(workDir); - - // -- Assert -- - expect(Sentry.setTag).toHaveBeenCalledWith('pods-installed', false); - }); - - it('should capture exception', async () => { - // -- Act -- - await podInstall(workDir); - - // -- Assert -- - expect(Sentry.captureException).toHaveBeenCalledWith( - 'Sentry pod install failed.', - ); - }); - - it('should start and stop spinner', async () => { - // -- Act -- - await podInstall(workDir); - - // -- Assert -- - expect(clackSpinnerMock.start).toHaveBeenCalledWith( - "Running 'pod install'. This may take a few minutes...", - ); - expect(clackSpinnerMock.stop).toHaveBeenCalledWith( - 'Failed to install pods.', - ); - }); - }); - - describe('all bash scripts work', () => { - beforeEach(() => { - vi.spyOn(bash, 'execute').mockResolvedValue(''); - }); - - it('should call pod update and install', async () => { - // -- Act -- - await podInstall(workDir); - - // -- Assert -- - expect(bash.execute).toHaveBeenCalledWith( - `cd ${workDir} && pod repo update`, - ); - expect(bash.execute).toHaveBeenCalledWith( - `cd ${workDir} && pod install --silent`, - ); - }); - - it('should set tag', async () => { - // -- Act -- - await podInstall(workDir); - - // -- Assert -- - expect(Sentry.setTag).toHaveBeenCalledWith('pods-installed', true); - }); - - it('should start and stop spinner', async () => { - // -- Act -- - await podInstall(workDir); - - // -- Assert -- - expect(clackSpinnerMock.start).toHaveBeenCalledWith( - "Running 'pod install'. This may take a few minutes...", - ); - expect(clackSpinnerMock.stop).toHaveBeenCalledWith('Pods installed.'); - }); - }); - - describe('dir not given', () => { - it('should use current directory', async () => { - // -- Act -- - await podInstall(); - - // -- Assert -- - expect(bash.execute).toHaveBeenCalledWith(`cd . && pod repo update`); - expect(bash.execute).toHaveBeenCalledWith( - `cd . && pod install --silent`, - ); - }); - }); - }); -}); diff --git a/test/apple/configure-package-manager.test.ts b/test/apple/configure-package-manager.test.ts index 1c71d692a..3b206c1fd 100644 --- a/test/apple/configure-package-manager.test.ts +++ b/test/apple/configure-package-manager.test.ts @@ -2,34 +2,9 @@ import * as Sentry from '@sentry/node'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import * as cocoapod from '../../src/apple/cocoapod'; import { configurePackageManager } from '../../src/apple/configure-package-manager'; -// @ts-expect-error - clack is ESM and TS complains about that. It works though -import clack from '@clack/prompts'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -vi.mock('@clack/prompts', () => ({ - __esModule: true, - default: { - log: { - warn: vi.fn(), - step: vi.fn(), - }, - select: vi.fn(), - }, -})); - -vi.mock('../../src/apple/cocoapod'); -vi.mock('../../src/utils/clack', () => ({ - abortIfCancelled: vi.fn((value: unknown) => Promise.resolve(value)), -})); - -vi.mock('../../src/telemetry', () => ({ - traceStep: vi.fn( - async (_name: string, fn: () => Promise) => await fn(), - ), -})); - vi.mock('../../src/utils/debug', () => ({ debug: vi.fn(), })); @@ -57,115 +32,12 @@ describe('configurePackageManager', () => { vi.clearAllMocks(); }); - describe('when CocoaPods is not available', () => { - it('should default to SPM and not prompt user', async () => { - // -- Arrange -- - vi.spyOn(cocoapod, 'usesCocoaPod').mockReturnValue(false); - - // -- Act -- - const result = await configurePackageManager({ projectDir }); - - // -- Assert -- - expect(result.shouldUseSPM).toBe(true); - expect(clack.select).not.toHaveBeenCalled(); - expect(clack.log.warn).not.toHaveBeenCalled(); - expect(Sentry.setTag).toHaveBeenCalledWith('cocoapod-exists', false); - expect(Sentry.setTag).toHaveBeenCalledWith('package-manager', 'SPM'); - }); - }); - - describe('when CocoaPods is available', () => { - beforeEach(() => { - vi.spyOn(cocoapod, 'usesCocoaPod').mockReturnValue(true); - }); - - it('should show deprecation warning', async () => { - // -- Arrange -- - vi.mocked(clack.select).mockResolvedValue('SPM' as never); - - // -- Act -- - await configurePackageManager({ projectDir }); - - // -- Assert -- - expect(clack.log.warn).toHaveBeenCalledWith( - 'CocoaPods is being deprecated. No new updates will be released after June 2026.\nWe recommend migrating to Swift Package Manager (SPM).', - ); - }); - - it('should prompt user to choose package manager', async () => { - // -- Arrange -- - vi.mocked(clack.select).mockResolvedValue('SPM' as never); - - // -- Act -- - await configurePackageManager({ projectDir }); - - // -- Assert -- - expect(clack.select).toHaveBeenCalledWith({ - message: 'Which package manager would you like to use to add Sentry?', - options: [ - { - value: 'SPM', - label: 'Swift Package Manager', - hint: 'Recommended', - }, - { - value: 'CocoaPods', - label: 'CocoaPods', - hint: 'Deprecated - no updates after June 2026', - }, - ], - }); - }); - - it('should use SPM when user selects SPM', async () => { - // -- Arrange -- - vi.mocked(clack.select).mockResolvedValue('SPM' as never); - - // -- Act -- - const result = await configurePackageManager({ projectDir }); - - // -- Assert -- - expect(result.shouldUseSPM).toBe(true); - expect(cocoapod.addCocoaPods).not.toHaveBeenCalled(); - expect(Sentry.setTag).toHaveBeenCalledWith('cocoapod-exists', true); - expect(Sentry.setTag).toHaveBeenCalledWith('package-manager', 'SPM'); - }); - - it('should use CocoaPods when user selects CocoaPods', async () => { - // -- Arrange -- - vi.mocked(clack.select).mockResolvedValue('CocoaPods' as never); - vi.spyOn(cocoapod, 'addCocoaPods').mockResolvedValue(true); - - // -- Act -- - const result = await configurePackageManager({ projectDir }); - - // -- Assert -- - expect(result.shouldUseSPM).toBe(false); - expect(cocoapod.addCocoaPods).toHaveBeenCalledWith(projectDir); - expect(Sentry.setTag).toHaveBeenCalledWith('cocoapod-exists', true); - expect(Sentry.setTag).toHaveBeenCalledWith('cocoapod-added', true); - expect(Sentry.setTag).toHaveBeenCalledWith( - 'package-manager', - 'CocoaPods', - ); - }); - - it('should handle CocoaPods addition failure', async () => { - // -- Arrange -- - vi.mocked(clack.select).mockResolvedValue('CocoaPods' as never); - vi.spyOn(cocoapod, 'addCocoaPods').mockResolvedValue(false); - - // -- Act -- - const result = await configurePackageManager({ projectDir }); + it('should always use SPM', () => { + // -- Act -- + const result = configurePackageManager({ projectDir }); - // -- Assert -- - expect(result.shouldUseSPM).toBe(false); - expect(cocoapod.addCocoaPods).toHaveBeenCalledWith(projectDir); - expect(Sentry.setTag).toHaveBeenCalledWith('cocoapod-added', false); - expect(Sentry.setTag).toHaveBeenCalledWith( - 'package-manager', - 'CocoaPods', - ); - }); + // -- Assert -- + expect(result.shouldUseSPM).toBe(true); + expect(Sentry.setTag).toHaveBeenCalledWith('package-manager', 'SPM'); }); }); From 925eda55bdba8eeb75bedafdde84d6878eeed1f3 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 3 Jul 2026 11:20:26 +0200 Subject: [PATCH 2/4] ref(apple): inline configurePackageManager into apple wizard Now that CocoaPods is removed, the package manager is always SPM and the function was just setting a telemetry tag. Inline it and delete the module. Co-Authored-By: Claude Opus 4.6 --- src/apple/apple-wizard.ts | 8 ++-- src/apple/configure-package-manager.ts | 14 ------- test/apple/configure-package-manager.test.ts | 43 -------------------- 3 files changed, 3 insertions(+), 62 deletions(-) delete mode 100644 src/apple/configure-package-manager.ts delete mode 100644 test/apple/configure-package-manager.test.ts diff --git a/src/apple/apple-wizard.ts b/src/apple/apple-wizard.ts index b61fc3d8d..3a3e26710 100644 --- a/src/apple/apple-wizard.ts +++ b/src/apple/apple-wizard.ts @@ -10,9 +10,10 @@ import { printWelcome, } from '../utils/clack'; import { offerProjectScopedMcpConfig } from '../utils/clack/mcp-config'; +import * as Sentry from '@sentry/node'; + import { checkInstalledCLI } from './check-installed-cli'; import { configureFastlane } from './configure-fastlane'; -import { configurePackageManager } from './configure-package-manager'; import { configureSentryCLI } from './configure-sentry-cli'; import { configureXcodeProject } from './configure-xcode-project'; import { injectCodeSnippet } from './inject-code-snippet'; @@ -75,10 +76,7 @@ async function runAppleWizardWithTelementry( authToken: authToken, }); - // Step - Set up Package Manager - configurePackageManager({ - projectDir, - }); + Sentry.setTag('package-manager', 'SPM'); // Step - Configure Xcode Project configureXcodeProject({ diff --git a/src/apple/configure-package-manager.ts b/src/apple/configure-package-manager.ts deleted file mode 100644 index 1594bd9ff..000000000 --- a/src/apple/configure-package-manager.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as Sentry from '@sentry/node'; - -import { debug } from '../utils/debug'; - -export function configurePackageManager({ - projectDir: _projectDir, -}: { - projectDir: string; -}) { - debug('Using Swift Package Manager (SPM) as the package manager'); - Sentry.setTag('package-manager', 'SPM'); - - return { shouldUseSPM: true }; -} diff --git a/test/apple/configure-package-manager.test.ts b/test/apple/configure-package-manager.test.ts deleted file mode 100644 index 3b206c1fd..000000000 --- a/test/apple/configure-package-manager.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as Sentry from '@sentry/node'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import { configurePackageManager } from '../../src/apple/configure-package-manager'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -vi.mock('../../src/utils/debug', () => ({ - debug: vi.fn(), -})); - -vi.mock('@sentry/node', async () => { - const actual = await vi.importActual( - '@sentry/node', - ); - return { - ...actual, - setTag: vi.fn(), - captureException: vi.fn(() => 'id'), - }; -}); - -describe('configurePackageManager', () => { - let projectDir: string; - - beforeEach(() => { - projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-project')); - vi.clearAllMocks(); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should always use SPM', () => { - // -- Act -- - const result = configurePackageManager({ projectDir }); - - // -- Assert -- - expect(result.shouldUseSPM).toBe(true); - expect(Sentry.setTag).toHaveBeenCalledWith('package-manager', 'SPM'); - }); -}); From f62fbadd0e7d160465adea0aca15423ba8fa6eaf Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 3 Jul 2026 11:24:03 +0200 Subject: [PATCH 3/4] chore: move changelog entry to Unreleased section Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18634bc64..641edb487 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,13 @@ # Changelog -## 6.13.0 +## Unreleased ### Breaking Changes - feat(apple)!: Remove CocoaPods as an installation option (COCOA-1241). The wizard now only offers Swift Package Manager (SPM) for Apple SDK setup, since CocoaPods trunk is going read-only. +## 6.13.0 + ### Fixes - fix(remix): Use `npx @sentry/remix --upload-sourcemaps` instead of `sentry-upload-sourcemaps` to avoid global bin collisions From 0bbd3a219a7ec42f63df035391f104ae80a2c4f3 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 3 Jul 2026 11:36:25 +0200 Subject: [PATCH 4/4] ref(apple): scope CocoaPods removal to apple wizard only Keep cocoapod.ts for react-native by moving it from src/apple/ to src/react-native/. Restore react-native wizard and e2e test to their original state. Fix Prettier formatting in configure-xcode-project. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 +- e2e-tests/tests/react-native.test.ts | 8 + src/apple/configure-xcode-project.ts | 3 +- src/react-native/cocoapod.ts | 73 ++++++ src/react-native/react-native-wizard.ts | 27 ++ test/react-native/cocoapod.test.ts | 321 ++++++++++++++++++++++++ 6 files changed, 431 insertions(+), 3 deletions(-) create mode 100644 src/react-native/cocoapod.ts create mode 100644 test/react-native/cocoapod.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 641edb487..d515f4e04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Breaking Changes -- feat(apple)!: Remove CocoaPods as an installation option (COCOA-1241). The wizard now only offers Swift Package Manager (SPM) for Apple SDK setup, since CocoaPods trunk is going read-only. +- feat(apple)!: Remove CocoaPods as an installation option ([#1299](https://github.com/getsentry/sentry-wizard/pull/1299)) ## 6.13.0 diff --git a/e2e-tests/tests/react-native.test.ts b/e2e-tests/tests/react-native.test.ts index 1f71c0cd3..d1297bb59 100644 --- a/e2e-tests/tests/react-native.test.ts +++ b/e2e-tests/tests/react-native.test.ts @@ -40,6 +40,14 @@ describe('ReactNative', () => { .whenAsked('Do you want to enable Logs') .respondWith(KEYS.ENTER); + // Only prompt to run `pod install` if running on macOS. + if (process.platform === 'darwin') { + wizardInteraction + .whenAsked('Do you want to run `pod install` now?') + .respondWith(KEYS.ENTER) + .expectOutput('Pods installed.', { timeout: 240_000 }); + } + wizardExitCode = await wizardInteraction .expectOutput('Added Sentry.init to App.tsx') .whenAsked( diff --git a/src/apple/configure-xcode-project.ts b/src/apple/configure-xcode-project.ts index e8a31bdca..d28d31442 100644 --- a/src/apple/configure-xcode-project.ts +++ b/src/apple/configure-xcode-project.ts @@ -21,8 +21,7 @@ export function configureXcodeProject({ target, { product: sentrySwiftPackageProductSpec, - existingFrameworkComment: - SENTRY_SPM_ALREADY_LINKED_FRAMEWORK_COMMENT, + existingFrameworkComment: SENTRY_SPM_ALREADY_LINKED_FRAMEWORK_COMMENT, successMessage: 'Added Sentry SPM dependency to your project', }, true, diff --git a/src/react-native/cocoapod.ts b/src/react-native/cocoapod.ts new file mode 100644 index 000000000..ac5a76868 --- /dev/null +++ b/src/react-native/cocoapod.ts @@ -0,0 +1,73 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as bash from '../utils/bash'; +import * as Sentry from '@sentry/node'; +// @ts-expect-error - clack is ESM and TS complains about that. It works though +import * as clack from '@clack/prompts'; +import chalk from 'chalk'; + +export function usesCocoaPod(projPath: string): boolean { + return fs.existsSync(path.join(projPath, 'Podfile')); +} + +export async function addCocoaPods(projPath: string): Promise { + const podfile = path.join(projPath, 'Podfile'); + + const podContent = fs.readFileSync(podfile, 'utf8'); + + if ( + /^\s*pod\s+(['"]Sentry['"]|['"]SentrySwiftUI['"])\s*$/im.test(podContent) + ) { + // Already have Sentry pod + return true; + } + + let podMatch = /^( *)pod\s+['"](\w+)['"] *$/im.exec(podContent); + if (!podMatch) { + // No Podfile is empty, will try to add Sentry pod after "use_frameworks!" + const frameworkMatch = /^( *)use_frameworks![^\n]* *$/im.exec(podContent); + if (!frameworkMatch) { + return false; + } + podMatch = frameworkMatch; + } + + const insertIndex = podMatch.index + podMatch[0].length; + const newFileContent = + podContent.slice(0, insertIndex) + + '\n' + + podMatch[1] + + "pod 'Sentry'\n" + + podContent.slice(insertIndex); + fs.writeFileSync(podfile, newFileContent, 'utf8'); + + clack.log.step('Sentry pod added to the project podFile.'); + + await podInstall(); + + return true; +} + +export async function podInstall(dir = '.') { + const installSpinner = clack.spinner(); + installSpinner.start("Running 'pod install'. This may take a few minutes..."); + + try { + await bash.execute(`cd ${dir} && pod repo update`); + await bash.execute(`cd ${dir} && pod install --silent`); + installSpinner.stop('Pods installed.'); + Sentry.setTag('pods-installed', true); + } catch (e) { + installSpinner.stop('Failed to install pods.'); + Sentry.setTag('pods-installed', false); + clack.log.error( + `${chalk.red( + 'Encountered the following error during pods installation:', + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + )}\n\n${e}\n\n${chalk.dim( + 'If you think this issue is caused by the Sentry wizard, let us know here:\nhttps://github.com/getsentry/sentry-wizard/issues', + )}`, + ); + Sentry.captureException('Sentry pod install failed.'); + } +} diff --git a/src/react-native/react-native-wizard.ts b/src/react-native/react-native-wizard.ts index 719cfe11e..c3efee308 100644 --- a/src/react-native/react-native-wizard.ts +++ b/src/react-native/react-native-wizard.ts @@ -5,6 +5,8 @@ import chalk from 'chalk'; import * as fs from 'fs'; import * as Sentry from '@sentry/node'; +import { platform } from 'os'; +import { podInstall } from './cocoapod'; import { traceStep, withTelemetry } from '../telemetry'; import { offerProjectScopedMcpConfig } from '../utils/clack/mcp-config'; import { @@ -347,6 +349,10 @@ async function patchXcodeFiles(config: RNCliSetupConfigContent) { gitignore: false, }); + if (platform() === 'darwin' && (await confirmPodInstall())) { + await traceStep('pod-install', () => podInstall('ios')); + } + const xcodeProjectPath = traceStep('find-xcode-project', () => getFirstMatchedPath(XCODE_PROJECT), ); @@ -477,3 +483,24 @@ async function patchAndroidFiles(config: RNCliSetupConfigContent) { chalk.green(`Android ${chalk.cyan('app/build.gradle')} saved.`), ); } + +async function confirmPodInstall(): Promise { + return traceStep('confirm-pod-install', async () => { + const continueWithPodInstall = await abortIfCancelled( + clack.select({ + message: 'Do you want to run `pod install` now?', + options: [ + { + value: true, + label: 'Yes', + hint: 'Recommended for smaller projects, this might take several minutes', + }, + { value: false, label: `No, I'll do it later` }, + ], + initialValue: true, + }), + ); + Sentry.setTag('continue-with-pod-install', continueWithPodInstall); + return continueWithPodInstall; + }); +} diff --git a/test/react-native/cocoapod.test.ts b/test/react-native/cocoapod.test.ts new file mode 100644 index 000000000..f79705663 --- /dev/null +++ b/test/react-native/cocoapod.test.ts @@ -0,0 +1,321 @@ +import * as Sentry from '@sentry/node'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { + addCocoaPods, + podInstall, + usesCocoaPod, +} from '../../src/react-native/cocoapod'; +import * as bash from '../../src/utils/bash'; +// @ts-expect-error - clack is ESM and TS complains about that. It works though +import * as clack from '@clack/prompts'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +vi.mock('@clack/prompts', async () => ({ + __esModule: true, + ...(await vi.importActual('@clack/prompts')), +})); + +vi.mock('../../src/utils/bash'); +vi.mock('@sentry/node', async () => { + const actual = await vi.importActual( + '@sentry/node', + ); + return { + ...actual, + setTag: vi.fn(), + captureException: vi.fn(() => 'id'), + }; +}); + +const clackSpinnerMock = { + start: vi.fn(), + stop: vi.fn(), + message: vi.fn(), +}; + +describe('cocoapod', () => { + beforeEach(() => { + vi.spyOn(clack, 'spinner').mockReturnValue(clackSpinnerMock); + vi.spyOn(clack.log, 'error').mockImplementation(() => { + /* empty */ + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('usesCocoaPod', () => { + describe('Podfile exists', () => { + it('should return true', () => { + // -- Arrange -- + const projPath = path.join(os.tmpdir(), 'test-project-with-podfile'); + const tempDir = fs.mkdtempSync(projPath); + + const podfile = path.join(tempDir, 'Podfile'); + fs.writeFileSync(podfile, ''); + + // -- Act -- + const result = usesCocoaPod(tempDir); + + // -- Assert -- + expect(result).toBeTruthy(); + }); + }); + + describe('Podfile does not exist', () => { + it('should return false', () => { + // -- Arrange -- + const projPath = path.join(os.tmpdir(), 'test-project-without-podfile'); + const tempDir = fs.mkdtempSync(projPath); + + // -- Act -- + const result = usesCocoaPod(tempDir); + + // -- Assert -- + expect(result).toBeFalsy(); + }); + }); + }); + + describe('addCocoaPods', () => { + describe('Podfile does not exist', () => { + it('should throw an error', async () => { + // -- Arrange -- + const projPath = path.join(os.tmpdir(), 'test-project-without-podfile'); + const tempDir = fs.mkdtempSync(projPath); + + // -- Act & Assert -- + await expect(addCocoaPods(tempDir)).rejects.toThrow( + 'ENOENT: no such file or directory, open', + ); + }); + }); + + describe('Podfile exists', () => { + describe('Podfile includes Sentry pod', () => { + const variations = [ + { + case: 'simple', + content: 'pod "Sentry"', + }, + { + case: 'with-swiftui', + content: 'pod "SentrySwiftUI"', + }, + { + case: 'leading-space', + content: ' pod "Sentry"', + }, + { + case: 'leading-space-and-swiftui', + content: ' pod "SentrySwiftUI"', + }, + { + case: 'trailing-space', + content: 'pod "Sentry" ', + }, + { + case: 'trailing-space-and-swiftui', + content: 'pod "SentrySwiftUI" ', + }, + { + case: 'single-quotes', + content: "pod 'Sentry'", + }, + { + case: 'double-quotes', + content: 'pod "Sentry"', + }, + ]; + for (const variation of variations) { + it(`should not change the Podfile for ${variation.case}`, async () => { + // -- Arrange -- + const projPath = fs.mkdtempSync(path.join(os.tmpdir(), 'project')); + fs.mkdirSync(projPath, { + recursive: true, + }); + + const podfile = path.join(projPath, 'Podfile'); + fs.writeFileSync(podfile, variation.content, 'utf8'); + + // -- Act -- + const result = await addCocoaPods(projPath); + + // -- Assert -- + expect(result).toBeTruthy(); + expect(fs.readFileSync(podfile, 'utf8')).toBe(variation.content); + }); + } + }); + + describe('Podfile includes no other pods', () => { + describe('Podfile does not include use_frameworks!', () => { + it('should not change the Podfile', async () => { + // -- Arrange -- + const projPath = fs.mkdtempSync(path.join(os.tmpdir(), 'project')); + fs.mkdirSync(projPath, { + recursive: true, + }); + + const podfile = path.join(projPath, 'Podfile'); + fs.writeFileSync(podfile, '', 'utf8'); + + // -- Act -- + const result = await addCocoaPods(projPath); + + // -- Assert -- + expect(result).toBeFalsy(); + expect(fs.readFileSync(podfile, 'utf8')).toBe(''); + }); + }); + + describe('Podfile includes use_frameworks!', () => { + it('should change the Podfile', async () => { + // -- Arrange -- + const projPath = fs.mkdtempSync(path.join(os.tmpdir(), 'project')); + fs.mkdirSync(projPath, { + recursive: true, + }); + + const podfile = path.join(projPath, 'Podfile'); + fs.writeFileSync(podfile, `use_frameworks!`, 'utf8'); + + // -- Act -- + const result = await addCocoaPods(projPath); + + // -- Assert -- + expect(result).toBeTruthy(); + expect(fs.readFileSync(podfile, 'utf8')).toBe( + `use_frameworks!\npod 'Sentry'\n`, + ); + }); + }); + }); + + describe('Podfile includes other pods', () => { + it('should append Sentry pod after last pod', async () => { + // -- Arrange -- + const projPath = fs.mkdtempSync(path.join(os.tmpdir(), 'project')); + fs.mkdirSync(projPath, { + recursive: true, + }); + + const podfile = path.join(projPath, 'Podfile'); + fs.writeFileSync(podfile, 'pod "OtherPod"', 'utf8'); + + // -- Act -- + const result = await addCocoaPods(projPath); + + // -- Assert -- + expect(result).toBeTruthy(); + expect(fs.readFileSync(podfile, 'utf8')).toBe( + `pod "OtherPod"\npod 'Sentry'\n`, + ); + }); + }); + }); + }); + + describe('podInstall', () => { + let workDir: string; + + beforeEach(() => { + workDir = path.join(os.tmpdir(), 'test-project'); + }); + + describe('any bash scripts fail', () => { + beforeEach(() => { + vi.spyOn(bash, 'execute').mockRejectedValue(new Error('test error')); + }); + + it('should not throw an error', async () => { + // -- Act & Assert -- + await expect(podInstall(workDir)).resolves.not.toThrow(); + }); + + it('should set tag', async () => { + // -- Act -- + await podInstall(workDir); + + // -- Assert -- + expect(Sentry.setTag).toHaveBeenCalledWith('pods-installed', false); + }); + + it('should capture exception', async () => { + // -- Act -- + await podInstall(workDir); + + // -- Assert -- + expect(Sentry.captureException).toHaveBeenCalledWith( + 'Sentry pod install failed.', + ); + }); + + it('should start and stop spinner', async () => { + // -- Act -- + await podInstall(workDir); + + // -- Assert -- + expect(clackSpinnerMock.start).toHaveBeenCalledWith( + "Running 'pod install'. This may take a few minutes...", + ); + expect(clackSpinnerMock.stop).toHaveBeenCalledWith( + 'Failed to install pods.', + ); + }); + }); + + describe('all bash scripts work', () => { + beforeEach(() => { + vi.spyOn(bash, 'execute').mockResolvedValue(''); + }); + + it('should call pod update and install', async () => { + // -- Act -- + await podInstall(workDir); + + // -- Assert -- + expect(bash.execute).toHaveBeenCalledWith( + `cd ${workDir} && pod repo update`, + ); + expect(bash.execute).toHaveBeenCalledWith( + `cd ${workDir} && pod install --silent`, + ); + }); + + it('should set tag', async () => { + // -- Act -- + await podInstall(workDir); + + // -- Assert -- + expect(Sentry.setTag).toHaveBeenCalledWith('pods-installed', true); + }); + + it('should start and stop spinner', async () => { + // -- Act -- + await podInstall(workDir); + + // -- Assert -- + expect(clackSpinnerMock.start).toHaveBeenCalledWith( + "Running 'pod install'. This may take a few minutes...", + ); + expect(clackSpinnerMock.stop).toHaveBeenCalledWith('Pods installed.'); + }); + }); + + describe('dir not given', () => { + it('should use current directory', async () => { + // -- Act -- + await podInstall(); + + // -- Assert -- + expect(bash.execute).toHaveBeenCalledWith(`cd . && pod repo update`); + expect(bash.execute).toHaveBeenCalledWith( + `cd . && pod install --silent`, + ); + }); + }); + }); +});