feat(apple): Add interactive Apple Snapshots wizard#1296
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
bd6e62e to
11c97d4
Compare
1ecaccd to
0e81310
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Unsafe managed scheme fallback
- Restricted managed scheme fallback to only occur when no explicit .xcscheme files exist, preventing incorrect scheme inference when explicit schemes are present but ambiguous.
Or push these changes by commenting:
@cursor push 98aa04b94b
Preview (98aa04b94b)
diff --git a/src/apple/snapshots/snapshot-verification-scheme.ts b/src/apple/snapshots/snapshot-verification-scheme.ts
--- a/src/apple/snapshots/snapshot-verification-scheme.ts
+++ b/src/apple/snapshots/snapshot-verification-scheme.ts
@@ -28,9 +28,11 @@
return matchingExplicitSchemeNames[0];
}
- const managedSchemeNames = getManagedSchemeNames(xcodeprojPath);
- if (managedSchemeNames.length === 1) {
- return managedSchemeNames[0];
+ if (explicitSchemes.length === 0) {
+ const managedSchemeNames = getManagedSchemeNames(xcodeprojPath);
+ if (managedSchemeNames.length === 1) {
+ return managedSchemeNames[0];
+ }
}
return undefined;You can send follow-ups to the cloud agent here.
0e81310 to
7d5069f
Compare
11c97d4 to
d063349
Compare
7d5069f to
1156eb3
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Wrong test class in guidance
- Modified ensureSnapshotTestFile to extract and return the actual class name from existing files, passed it through the wizard to verification guidance, and updated buildSnapshotExportCommand to use the actual class name instead of generating it.
- ✅ Fixed: Preferences link failure unreported
- Changed linked return value in configureSnapshotPreviewsXcodeProject to check that all package products (both SnapshottingTests and SnapshotPreferences) linked successfully using linkResults.every() instead of only checking SnapshottingTests.
Or push these changes by commenting:
@cursor push e5faf02297
Preview (e5faf02297)
diff --git a/src/apple/snapshots/apple-snapshots-wizard.ts b/src/apple/snapshots/apple-snapshots-wizard.ts
--- a/src/apple/snapshots/apple-snapshots-wizard.ts
+++ b/src/apple/snapshots/apple-snapshots-wizard.ts
@@ -15,7 +15,10 @@
import type { XcodeProject } from '../xcode-manager';
import { checkInstalledCLISnapshots } from './snapshots-cli-preflight';
import { configureSnapshotPreviewsXcodeProject } from './configure-snapshotpreviews-xcode-project';
-import { ensureSnapshotTestFile } from './snapshot-test-file';
+import {
+ ensureSnapshotTestFile,
+ snapshotTestClassName,
+} from './snapshot-test-file';
import { resolveSnapshotVerificationSchemeName } from './snapshot-verification-scheme';
import {
SNAPSHOTPREVIEWS_MINIMUM_VERSION,
@@ -164,6 +167,9 @@
verificationGuidance: {
appId: xcProject.getBundleIdentifierForTarget(appTargetName),
hostedTestTargetName,
+ snapshotTestClassName:
+ snapshotTestResult.className ??
+ snapshotTestClassName(hostedTestTargetName),
projectDir,
projectPath: xcProject.xcodeprojPath,
schemeName: resolveSnapshotVerificationSchemeName({
diff --git a/src/apple/snapshots/configure-snapshotpreviews-xcode-project.ts b/src/apple/snapshots/configure-snapshotpreviews-xcode-project.ts
--- a/src/apple/snapshots/configure-snapshotpreviews-xcode-project.ts
+++ b/src/apple/snapshots/configure-snapshotpreviews-xcode-project.ts
@@ -57,6 +57,6 @@
return {
changed: linkResults.some((result) => result.changed),
- linked: snapshottingTestsResult.linked,
+ linked: linkResults.every((result) => result.linked),
};
}
diff --git a/src/apple/snapshots/snapshot-test-file.ts b/src/apple/snapshots/snapshot-test-file.ts
--- a/src/apple/snapshots/snapshot-test-file.ts
+++ b/src/apple/snapshots/snapshot-test-file.ts
@@ -12,7 +12,12 @@
}: {
xcodeProject: XcodeProject;
hostedTestTargetName: string;
-}): { changed: boolean; included: boolean; filePath?: string } {
+}): {
+ changed: boolean;
+ included: boolean;
+ filePath?: string;
+ className?: string;
+} {
const existingSnapshotTestFile = findExistingSnapshotTestFile(
xcodeProject,
hostedTestTargetName,
@@ -21,7 +26,8 @@
return {
changed: false,
included: true,
- filePath: existingSnapshotTestFile,
+ filePath: existingSnapshotTestFile.filePath,
+ className: existingSnapshotTestFile.className,
};
}
@@ -54,6 +60,7 @@
changed: fileCreated || membership.changed,
included: true,
filePath,
+ className,
};
}
@@ -89,20 +96,28 @@
function findExistingSnapshotTestFile(
xcodeProject: XcodeProject,
hostedTestTargetName: string,
-): string | undefined {
+): { filePath: string; className: string } | undefined {
const sourceFiles =
xcodeProject.getSourceFilesForTarget(hostedTestTargetName);
- return sourceFiles?.find((filePath) => {
+ for (const filePath of sourceFiles ?? []) {
if (!filePath.endsWith('.swift') || !fs.existsSync(filePath)) {
- return false;
+ continue;
}
const contents = fs.readFileSync(filePath, 'utf8');
- return (
+ if (
contents.includes(SNAPSHOT_TEST_IMPORT) &&
contents.includes(SNAPSHOT_TEST_SUPERCLASS)
- );
- });
+ ) {
+ const classMatch = contents.match(
+ /\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*SnapshotTest\b/,
+ );
+ if (classMatch) {
+ return { filePath, className: classMatch[1] };
+ }
+ }
+ }
+ return undefined;
}
function getSnapshotTestDirectory(
diff --git a/src/apple/snapshots/snapshots-cli-preflight.ts b/src/apple/snapshots/snapshots-cli-preflight.ts
--- a/src/apple/snapshots/snapshots-cli-preflight.ts
+++ b/src/apple/snapshots/snapshots-cli-preflight.ts
@@ -7,11 +7,11 @@
import * as bash from '../../utils/bash';
import { checkInstalledCLI } from '../check-installed-cli';
import * as fastlane from '../fastlane';
-import { snapshotTestClassName } from './snapshot-test-file';
type SnapshotVerificationGuidance = {
appId?: string;
hostedTestTargetName: string;
+ snapshotTestClassName: string;
projectDir: string;
projectPath: string;
schemeName?: string;
@@ -130,9 +130,7 @@
} \\`,
" -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \\",
` -only-testing:${quoteShell(
- `${guidance.hostedTestTargetName}/${snapshotTestClassName(
- guidance.hostedTestTargetName,
- )}`,
+ `${guidance.hostedTestTargetName}/${guidance.snapshotTestClassName}`,
)} \\`,
' CODE_SIGNING_ALLOWED=NO \\',
' | xcpretty',
diff --git a/test/apple/snapshots/apple-snapshots-wizard.test.ts b/test/apple/snapshots/apple-snapshots-wizard.test.ts
--- a/test/apple/snapshots/apple-snapshots-wizard.test.ts
+++ b/test/apple/snapshots/apple-snapshots-wizard.test.ts
@@ -73,9 +73,16 @@
},
}));
-vi.mock('../../../src/apple/snapshots/snapshot-test-file', () => ({
- ensureSnapshotTestFile: mocks.ensureSnapshotTestFile,
-}));
+vi.mock('../../../src/apple/snapshots/snapshot-test-file', async (importOriginal) => {
+ const actual =
+ await importOriginal<
+ typeof import('../../../src/apple/snapshots/snapshot-test-file')
+ >();
+ return {
+ ...actual,
+ ensureSnapshotTestFile: mocks.ensureSnapshotTestFile,
+ };
+});
vi.mock(
'../../../src/apple/snapshots/configure-snapshotpreviews-xcode-project',
@@ -300,6 +307,7 @@
verificationGuidance: {
appId: 'com.getsentry.App',
hostedTestTargetName: 'AppTests',
+ snapshotTestClassName: 'AppTestsSnapshotTest',
projectDir: tempDir,
projectPath: path.join(tempDir, 'App.xcodeproj'),
schemeName: 'AppScheme',
diff --git a/test/apple/snapshots/snapshots-cli-preflight.test.ts b/test/apple/snapshots/snapshots-cli-preflight.test.ts
--- a/test/apple/snapshots/snapshots-cli-preflight.test.ts
+++ b/test/apple/snapshots/snapshots-cli-preflight.test.ts
@@ -52,6 +52,7 @@
return {
appId: 'com.getsentry.App',
hostedTestTargetName: 'AppTests',
+ snapshotTestClassName: 'AppTestsSnapshotTest',
projectDir,
projectPath: path.join(projectDir, 'App.xcodeproj'),
schemeName: 'App',You can send follow-ups to the cloud agent here.
fe68f49 to
a291a73
Compare
a291a73 to
a12c102
Compare
philprime
left a comment
There was a problem hiding this comment.
Left some feedback for you to consider
a12c102 to
842c852
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Existing snapshot path skips validation
- Removed conditional check so the template is always written when discovery doesn't find a valid snapshot test file, ensuring content consistency.
Or push these changes by commenting:
@cursor push b01bff67da
Preview (b01bff67da)
diff --git a/src/apple/snapshots/snapshot-test-file.ts b/src/apple/snapshots/snapshot-test-file.ts
--- a/src/apple/snapshots/snapshot-test-file.ts
+++ b/src/apple/snapshots/snapshot-test-file.ts
@@ -42,9 +42,7 @@
fs.mkdirSync(path.dirname(filePath), { recursive: true });
const fileCreated = !fs.existsSync(filePath);
- if (fileCreated) {
- fs.writeFileSync(filePath, snapshotTestTemplate(className), 'utf8');
- }
+ fs.writeFileSync(filePath, snapshotTestTemplate(className), 'utf8');
const membership = xcodeProject.addSwiftSourceFileToTarget({
targetName: hostedTestTargetName,You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Workspace projects get project flag
- Modified snapshot export command to use -workspace flag instead of -project when project is discovered via .xcworkspace, ensuring correct xcodebuild commands for CocoaPods and other workspace-based setups.
Or push these changes by commenting:
@cursor push 71433c66e5
Preview (71433c66e5)
diff --git a/src/apple/lookup-xcode-project.ts b/src/apple/lookup-xcode-project.ts
--- a/src/apple/lookup-xcode-project.ts
+++ b/src/apple/lookup-xcode-project.ts
@@ -17,8 +17,8 @@
projectDir: string;
}): Promise<XcodeProject> {
debug(`Looking for Xcode project in directory: ${chalk.cyan(projectDir)}`);
- const xcodeProjFiles = searchXcodeProjectAtPath(projectDir);
- if (xcodeProjFiles.length === 0) {
+ const searchResult = searchXcodeProjectAtPath(projectDir);
+ if (searchResult.projectPaths.length === 0) {
clack.log.error(
'No Xcode project found. Please run this command from the root of your project.',
);
@@ -27,24 +27,24 @@
}
debug(
`Found ${chalk.cyan(
- xcodeProjFiles.length.toString(),
+ searchResult.projectPaths.length.toString(),
)} candidates for Xcode project`,
);
// In case there is only one Xcode project, we can use that one.
// Otherwise, we need to ask the user which one they want to use.
let xcodeProjFile: string;
- if (xcodeProjFiles.length === 1) {
+ if (searchResult.projectPaths.length === 1) {
debug(`Found exactly one Xcode project, using it`);
Sentry.setTag('multiple-projects', false);
- xcodeProjFile = xcodeProjFiles[0];
+ xcodeProjFile = searchResult.projectPaths[0];
} else {
debug(`Found multiple Xcode projects, asking user to choose one`);
Sentry.setTag('multiple-projects', true);
xcodeProjFile = (
await traceStep('Choose Xcode project', () =>
askForItemSelection(
- xcodeProjFiles,
+ searchResult.projectPaths,
'Which project do you want to add Sentry to?',
),
)
@@ -59,7 +59,14 @@
return await abort();
}
- return new XcodeProject(pathToPbxproj);
+ const xcodeProject = new XcodeProject(pathToPbxproj);
+ if (searchResult.workspacePath) {
+ xcodeProject.xcworkspacePath = path.join(
+ projectDir,
+ searchResult.workspacePath,
+ );
+ }
+ return xcodeProject;
}
export async function selectXcodeTarget(
diff --git a/src/apple/search-xcode-project-at-path.ts b/src/apple/search-xcode-project-at-path.ts
--- a/src/apple/search-xcode-project-at-path.ts
+++ b/src/apple/search-xcode-project-at-path.ts
@@ -4,20 +4,27 @@
import { debug } from '../utils/debug';
import { findFilesWithExtension } from '../utils/find-files-with-extension';
-export function searchXcodeProjectAtPath(searchPath: string): string[] {
+export type XcodeProjectSearchResult = {
+ projectPaths: string[];
+ workspacePath?: string;
+};
+
+export function searchXcodeProjectAtPath(
+ searchPath: string,
+): XcodeProjectSearchResult {
debug('Searching for Xcode project at path: ' + searchPath);
const projs = findFilesWithExtension(searchPath, '.xcodeproj');
if (projs.length > 0) {
debug('Found Xcode project at paths:');
projs.forEach((proj) => debug(' ' + proj));
- return projs;
+ return { projectPaths: projs };
}
debug('Searching for Xcode workspace at path: ' + searchPath);
const workspace = findFilesWithExtension(searchPath, '.xcworkspace');
if (workspace.length == 0) {
debug('No Xcode workspace found at path: ' + searchPath);
- return [];
+ return { projectPaths: [] };
}
debug('Found Xcode workspace at path: ' + workspace[0]);
@@ -28,7 +35,7 @@
);
if (!fs.existsSync(xsworkspacedata)) {
debug('No Xcode workspace data found at path: ' + xsworkspacedata);
- return [];
+ return { projectPaths: [] };
}
debug('Parsing Xcode workspace data at path: ' + xsworkspacedata);
@@ -51,5 +58,5 @@
debug('Found Xcode project at paths:');
projs.forEach((proj) => debug(' ' + proj));
- return projs;
+ return { projectPaths: projs, workspacePath: workspace[0] };
}
diff --git a/src/apple/snapshots/apple-snapshots-wizard.ts b/src/apple/snapshots/apple-snapshots-wizard.ts
--- a/src/apple/snapshots/apple-snapshots-wizard.ts
+++ b/src/apple/snapshots/apple-snapshots-wizard.ts
@@ -151,6 +151,7 @@
hostedTestTargetName,
projectDir,
projectPath: xcProject.xcodeprojPath,
+ workspacePath: xcProject.xcworkspacePath,
schemeName: resolveSnapshotVerificationSchemeName({
hostedTestTargetName,
xcodeprojPath: xcProject.xcodeprojPath,
diff --git a/src/apple/snapshots/snapshots-cli-preflight.ts b/src/apple/snapshots/snapshots-cli-preflight.ts
--- a/src/apple/snapshots/snapshots-cli-preflight.ts
+++ b/src/apple/snapshots/snapshots-cli-preflight.ts
@@ -13,6 +13,7 @@
hostedTestTargetName: string;
projectDir: string;
projectPath: string;
+ workspacePath?: string;
schemeName?: string;
snapshotTestClassName: string;
};
@@ -117,14 +118,19 @@
function buildSnapshotExportCommand(
guidance: SnapshotVerificationGuidance,
): string {
+ const projectOrWorkspaceFlag = guidance.workspacePath
+ ? '-workspace'
+ : '-project';
+ const projectOrWorkspacePath = guidance.workspacePath ?? guidance.projectPath;
+ const relativePath =
+ path.relative(guidance.projectDir, projectOrWorkspacePath) ||
+ projectOrWorkspacePath;
+
return [
`From ${quoteShell(guidance.projectDir)}, export snapshots with:`,
'TEST_RUNNER_SNAPSHOTS_EXPORT_DIR="$PWD/snapshot-images" \\',
'xcodebuild test \\',
- ` -project ${quoteShell(
- path.relative(guidance.projectDir, guidance.projectPath) ||
- guidance.projectPath,
- )} \\`,
+ ` ${projectOrWorkspaceFlag} ${quoteShell(relativePath)} \\`,
` -scheme ${
guidance.schemeName ? quoteShell(guidance.schemeName) : '<scheme>'
} \\`,
diff --git a/src/apple/xcode-manager.ts b/src/apple/xcode-manager.ts
--- a/src/apple/xcode-manager.ts
+++ b/src/apple/xcode-manager.ts
@@ -211,6 +211,11 @@
xcodeprojPath: string;
/**
+ * The path to the `<PROJECT>.xcworkspace` directory, if the project was found via a workspace.
+ */
+ xcworkspacePath?: string;
+
+ /**
* The path to the `project.pbxproj` file.
*/
pbxprojPath: string;
diff --git a/test/apple/lookup-xcode-project.test.ts b/test/apple/lookup-xcode-project.test.ts
--- a/test/apple/lookup-xcode-project.test.ts
+++ b/test/apple/lookup-xcode-project.test.ts
@@ -111,7 +111,9 @@
describe('lookupXcodeProject', () => {
it('returns an Xcode project without selecting a target', async () => {
const pbxprojPath = createProject(projectDir, 'App.xcodeproj');
- mocks.searchXcodeProjectAtPath.mockReturnValue(['App.xcodeproj']);
+ mocks.searchXcodeProjectAtPath.mockReturnValue({
+ projectPaths: ['App.xcodeproj'],
+ });
mocks.targetsByProjectPath.set(pbxprojPath, ['App']);
const result = await lookupXcodeProject({ projectDir });
@@ -130,10 +132,9 @@
it('prompts when multiple Xcode projects are found', async () => {
createProject(projectDir, 'First.xcodeproj');
const selectedPbxprojPath = createProject(projectDir, 'Second.xcodeproj');
- mocks.searchXcodeProjectAtPath.mockReturnValue([
- 'First.xcodeproj',
- 'Second.xcodeproj',
- ]);
+ mocks.searchXcodeProjectAtPath.mockReturnValue({
+ projectPaths: ['First.xcodeproj', 'Second.xcodeproj'],
+ });
mocks.askForItemSelection.mockResolvedValue({
value: 'Second.xcodeproj',
});
diff --git a/test/apple/snapshots/snapshots-cli-preflight.test.ts b/test/apple/snapshots/snapshots-cli-preflight.test.ts
--- a/test/apple/snapshots/snapshots-cli-preflight.test.ts
+++ b/test/apple/snapshots/snapshots-cli-preflight.test.ts
@@ -228,4 +228,24 @@
hasSentryUploadAction: true,
});
});
+
+ it('uses -workspace flag when workspace path is provided', async () => {
+ const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'snapshot-cli-'));
+ mocks.hasSentryCLI.mockReturnValue(true);
+
+ await checkInstalledCLISnapshots({
+ projectDir,
+ verificationGuidance: {
+ ...getVerificationGuidance(projectDir),
+ workspacePath: path.join(projectDir, 'App.xcworkspace'),
+ },
+ });
+
+ expect(mocks.info).toHaveBeenCalledWith(
+ expect.stringContaining('-workspace App.xcworkspace'),
+ );
+ expect(mocks.info).toHaveBeenCalledWith(
+ expect.stringMatching(/xcodebuild test[^]*-workspace/),
+ );
+ });
});You can send follow-ups to the cloud agent here.
3fc21e5 to
2e5797b
Compare
1d82e9b to
5a74ab4
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Fastlane guidance always uses ios
- Modified getSnapshotFastlaneGuidance to detect whether the upload_sentry_snapshots lane is within the iOS platform block and adjusted the printed Fastlane command accordingly.
Or push these changes by commenting:
@cursor push e454c5de37
Preview (e454c5de37)
diff --git a/src/apple/snapshots/snapshots-cli-preflight.ts b/src/apple/snapshots/snapshots-cli-preflight.ts
--- a/src/apple/snapshots/snapshots-cli-preflight.ts
+++ b/src/apple/snapshots/snapshots-cli-preflight.ts
@@ -48,9 +48,24 @@
}
const contents = fs.readFileSync(fastfilePath, 'utf8');
+ const hasUploadLane = /lane\s+:upload_sentry_snapshots\b/.test(contents);
+
+ let isLaneInIOSPlatform = false;
+ if (hasUploadLane) {
+ const iosPlatformMatch = /platform\s+:ios\s+do([\s\S]*?)^end/m.exec(
+ contents,
+ );
+ if (iosPlatformMatch) {
+ isLaneInIOSPlatform = /lane\s+:upload_sentry_snapshots\b/.test(
+ iosPlatformMatch[1],
+ );
+ }
+ }
+
return {
fastfilePath,
- hasUploadLane: /lane\s+:upload_sentry_snapshots\b/.test(contents),
+ hasUploadLane,
+ isLaneInIOSPlatform,
hasSentryUploadAction: /\bsentry_upload_snapshots\s*\(/.test(contents),
};
}
@@ -76,12 +91,16 @@
verificationGuidance: SnapshotVerificationGuidance;
}): void {
if (fastlaneGuidance.fastfilePath && fastlaneGuidance.hasUploadLane) {
+ const fastlaneCommand = fastlaneGuidance.isLaneInIOSPlatform
+ ? 'bundle exec fastlane ios upload_sentry_snapshots path: "$PWD/snapshot-images"'
+ : 'bundle exec fastlane upload_sentry_snapshots path: "$PWD/snapshot-images"';
+
clack.log.info(
[
'Detected existing Fastlane snapshots upload support.',
buildSnapshotExportCommand(verificationGuidance),
'Then verify upload with:',
- 'bundle exec fastlane ios upload_sentry_snapshots path: "$PWD/snapshot-images"',
+ fastlaneCommand,
].join('\n'),
);
return;
diff --git a/test/apple/snapshots/snapshots-cli-preflight.test.ts b/test/apple/snapshots/snapshots-cli-preflight.test.ts
--- a/test/apple/snapshots/snapshots-cli-preflight.test.ts
+++ b/test/apple/snapshots/snapshots-cli-preflight.test.ts
@@ -225,6 +225,7 @@
expect(getSnapshotFastlaneGuidance(projectDir)).toEqual({
fastfilePath: path.join(fastlaneDir, 'Fastfile'),
hasUploadLane: true,
+ isLaneInIOSPlatform: false,
hasSentryUploadAction: true,
});
});You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 2e5797b. Configure here.
Merge activity
|
## Stack This is **1/3** in the Apple Snapshots stack replacing the original unified PR #1293. - 1/3: #1295 — Xcode project primitives (this PR) - 2/3: #1296 — Interactive Apple Snapshots wizard - 3/3: #1297 — Non-interactive Apple Snapshots mode The top of the stack is intended to be an exact, lossless re-expression of #1293. ## Summary This PR refactors the existing Apple/Xcode project infrastructure needed by the Apple Snapshots wizard, without exposing the new `appleSnapshots` wizard yet. The goal is to make the shared Xcode helpers reviewable independently before adding the user-facing SnapshotPreviews flow. Existing Sentry Apple setup remains routed through the current `ios` wizard; this layer should be shared infrastructure, not a new runtime setup behavior. ## What changed - Split Xcode project lookup/selection helpers so target discovery can be reused by the snapshots flow. - Move Sentry Swift package metadata into a reusable package spec. - Extend `XcodeProject` with reusable helpers for: - hosted XCTest target detection via `TEST_HOST` - app bundle ID / executable lookup - Swift package product linking - source file resolution - Swift source insertion - synchronized-folder source membership support - Preserve existing `configureXcodeProject` / `updateXcodeProject` behavior for the normal Apple SDK setup path. - Add focused tests for the refactored project lookup, Xcode mutation, and git helper behavior. ## What this PR intentionally does not do - Does not add the `appleSnapshots` integration option. - Does not add the Apple Snapshots wizard entrypoint. - Does not configure SnapshotPreviews yet. - Does not configure Sentry auth, DSNs, runtime SDK initialization, dSYM upload scripts, Fastlane setup, MCP config, or CI workflow files. Those pieces land in the upstack PRs. ## Suggested review focus - `.pbxproj` mutation correctness and idempotency. - Shared Swift package product linking behavior. - Hosted XCTest target detection via `TEST_HOST`. - Ensuring existing `ios` wizard behavior did not accidentally change. - Whether the helper boundaries are general enough for both normal Apple SDK setup and SnapshotPreviews setup. ## Risk Medium. This refactors shared Apple/Xcode project mutation code used by existing setup paths. The risk is primarily around unusual Xcode project graphs, package product linking, target discovery, and source membership handling. ## Validation Before submitting the stack: - `yarn test` passed: 947 passed, 4 skipped - `yarn build` passed - Final stack tree matched original PR #1293 exactly - `gt submit --stack --dry-run` passed Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Previously the snapshots preflight assumed the Fastfile was readable and called fs.readFileSync directly, which could throw and abort the wizard when the file existed but was inaccessible. Wrap the read in a try/catch, warn the user, and skip Fastlane snapshots upload detection so the wizard can continue gracefully. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
c833fc6 to
70d1f2b
Compare
| `${className}.swift`, | ||
| ); | ||
|
|
||
| fs.mkdirSync(path.dirname(filePath), { recursive: true }); |
There was a problem hiding this comment.
Bug: The fs.mkdirSync call in ensureSnapshotTestFile lacks error handling. Filesystem errors like permission issues will cause an unhandled exception, crashing the wizard.
Severity: MEDIUM
Suggested Fix
Wrap the fs.mkdirSync call in a try/catch block. In the catch block, handle the error gracefully. For example, you could clean up any partially created files or directories and inform the user about the filesystem issue instead of crashing the wizard.
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/snapshots/snapshot-test-file.ts#L43
Potential issue: The function `ensureSnapshotTestFile` calls `fs.mkdirSync` without a
`try/catch` block. Under certain conditions, such as the process lacking write
permissions, the disk being full, or other filesystem restrictions (EACCES, EPERM,
ENOSPC), this call will throw an exception. Because the exception is not caught locally,
it propagates up to the `withTelemetry` wrapper, which logs the error and then re-throws
it, causing the wizard process to terminate unexpectedly. This results in a crash rather
than a graceful exit with a user-friendly error message.
## Stack This is **3/3** in the Apple Snapshots stack replacing the original unified PR #1293. - 1/3: #1295 — Xcode project primitives - 2/3: #1296 — Interactive Apple Snapshots wizard - 3/3: #1297 — Non-interactive Apple Snapshots mode (this PR) This is the top of the stack. The final tree of this stack was verified to exactly match original PR #1293. ## Summary Adds unattended/agent-friendly execution for the Apple Snapshots wizard introduced in #1296. The main goal is that non-interactive mode never waits on stdin. It either proceeds from explicit arguments and safe single-target assumptions, or it fails with actionable manual setup instructions and the CLI flag needed to continue. ## What changed - Adds Apple Snapshots CLI options: - `--non-interactive` - `--app-target` - `--hosted-test-target` - Threads non-interactive behavior through: - CLI/run routing - project lookup - git safety checks - preview/missing-preview confirmation - sentry-cli preflight - hosted XCTest target selection - Adds app-target and hosted-test-target resolution behavior for unattended runs. - Adds prompt suppression paths so agentic setup cannot hang on stdin. - Updates help output for the new flags. - Adds tests for non-interactive success/failure paths, explicit target handling, and prompt-free behavior. ## Behavior contract in this layer - App target selection uses `--app-target` when provided. - Without `--app-target`, the wizard auto-selects only when there is a single app target. - If the app target cannot be resolved safely in non-interactive mode, the wizard fails with guidance to pass `--app-target`. - Hosted XCTest target selection uses `--hosted-test-target` when provided, as long as the target exists and has a non-empty `TEST_HOST`. - Without `--hosted-test-target`, the wizard first narrows hosted XCTest targets to those whose `TEST_HOST` matches the selected app target's bundle/executable names. - If app-specific hosted-test matching finds one target, the wizard uses it. - If app-specific hosted-test matching finds multiple targets, non-interactive mode fails with guidance to pass `--hosted-test-target`. - If app-specific hosted-test matching is inconclusive but the project has exactly one hosted XCTest target, the wizard assumes the common case that this target belongs to the selected app. - If there are multiple hosted XCTest targets and no safe match, non-interactive mode fails with guidance. - Missing Swift previews are not fatal. Non-interactive mode logs the limitation and continues because agents may still want the project wiring and manual next steps. - Non-interactive mode must never wait on stdin. ## What this PR intentionally does not do - Does not configure Sentry auth, DSNs, runtime SDK initialization, dSYM upload scripts, Fastlane dSYM setup, MCP config, or CI workflow files. - Does not change the existing `ios` wizard into a SnapshotPreviews setup path. - Does not make unsafe guesses when target resolution is ambiguous. ## Suggested review focus - The target-selection contract for explicit CLI choices, single-target assumptions, and non-interactive failures. - Hosted XCTest detection via `TEST_HOST` and app bundle/executable name matching. - Confirming all non-interactive paths avoid stdin prompts. - Error messages/manual setup guidance for ambiguous target/project structures. - Help output and CLI option behavior. ## Risk Medium. This layer controls unattended behavior and is the most important one for agentic setup safety. The main risk is choosing the wrong app/test target in unusual projects or leaving a prompt path reachable in non-interactive mode. ## Validation Before submitting the stack: - `yarn test` passed: 947 passed, 4 skipped - `yarn build` passed - Final stack tree matched original PR #1293 exactly - `gt submit --stack --dry-run` passed Co-Authored-By: GPT-5 Codex <noreply@openai.com>



Stack
This is 2/3 in the Apple Snapshots stack replacing the original unified PR #1293.
This PR builds on #1295. The top of the stack is intended to be an exact, lossless re-expression of #1293.
Summary
Adds a standalone Apple Snapshots wizard for configuring Sentry SnapshotPreviews in Apple/Xcode projects without running the normal iOS runtime SDK setup.
The new
appleSnapshotsintegration is available from the wizard picker and via--integration appleSnapshots. It reuses the Xcode project discovery path from #1295, asks for the app target that hosts Swift previews, then selects a hosted XCTest target for running SnapshotPreviews.This is intentionally separate from the existing
ioswizard. Snapshot setup is related to Apple projects, but it should not silently configure normal runtime SDK behavior. Keeping this as its own wizard makes the user-visible behavior narrower: project wiring plus explicit next-step guidance for snapshot export/upload.What changed
appleSnapshotsintegration and CLI routing.SnapshottingTeststo the hosted test targetSnapshotPreferencesto the selected app target only when Swift previews are detectedSnapshotTestsubclass.xcodebuild testguidance only.xcodebuildsentry-cli snapshots uploadBehavior contract in this layer
xcodebuild testguidance.What this PR intentionally does not do
--non-interactive,--app-target, or--hosted-test-targetflags for Apple Snapshots.Those pieces land in #1297.
Suggested review focus
.pbxprojmutation correctness and idempotency for package/product linking.xcodebuild testguidance.ioswizard did not pick up accidental runtime behavior changes.Risk
Medium. The new wizard mutates Xcode project graphs, links package products, and creates/reuses source files. The new
appleSnapshotsflow is isolated from auth/runtime/dSYM/CI configuration, but unusual Xcode project structures, ambiguous schemes, or non-standard hosted test target settings are the areas most likely to need reviewer scrutiny.Validation
Before submitting the stack:
yarn testpassed: 947 passed, 4 skippedyarn buildpassedgt submit --stack --dry-runpassedCo-Authored-By: GPT-5 Codex noreply@openai.com