Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))

## 7.0.0

### Breaking Changes
Expand Down
23 changes: 23 additions & 0 deletions src/apple/configure-fastlane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -41,6 +42,28 @@ 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?',
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing Gemfile existence check

Medium Severity

The Gemfile prompt always claims a Gemfile was found and asks to edit it, but never checks with gemFile first. Unlike the Fastfile flow above, projects without a Gemfile get a misleading prompt; accepting it then hits the failure path in addSentryPluginToGemfile.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3415c60. Configure here.

debug(
Comment on lines 44 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The Gemfile prompt is shown without checking if a Gemfile exists and does not correctly handle cancellation, leading to misleading messages and unexpected behavior.
Severity: MEDIUM

Suggested Fix

First, check for the existence of a Gemfile using gemFile(projectDir) before showing the prompt. Only prompt the user if a Gemfile is found. Second, wrap the clack.confirm call with the abortIfCancelled utility to ensure that user cancellation (e.g., via Ctrl+C) correctly terminates the wizard instead of proceeding with a faulty truthy value.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: src/apple/configure-fastlane.ts#L44-L50

Potential issue: The wizard unconditionally prompts the user with 'Found a Gemfile in
your project...' without first verifying that a `Gemfile` actually exists. If the user
agrees to add the gem but no `Gemfile` is present, the process fails and displays a
misleading warning: 'Could not edit your Gemfile...'. Additionally, if the user cancels
this prompt (e.g., by pressing Ctrl+C), the cancellation is not handled correctly. The
`clack.confirm` call is not wrapped in `abortIfCancelled`, causing its `Symbol` return
value to be treated as `true`, which leads to an unintended attempt to modify the
non-existent file.

Did we get this right? 👍 / 👎 to inform future reviews.

`User wants to add Gemfile: ${chalk.cyan(shouldAddGemfile.toString())}`,
);
Sentry.setTag('gemfile-desired', shouldAddGemfile);

if (shouldAddGemfile) {
debug(`Adding fastlane-plugin-sentry 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.',
);
Expand Down
72 changes: 72 additions & 0 deletions src/apple/gemfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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;
}
171 changes: 171 additions & 0 deletions test/apple/gemfile.test.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading