From 31d523ceedce617acbc8463740932c93ba1f3f96 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 1 Sep 2026 14:42:24 +0200 Subject: [PATCH 1/4] perf(ci): Reuse screenshot test build Build the screenshot test bundle once and run retries without rebuilding. Pass the prewarmed simulator into Fastlane and use deterministic swipe actions for screenshot editing flows. --- .github/workflows/release-beta.yml | 3 +- .github/workflows/screenshots.yml | 3 +- .../Sources/ScreenshotUITests.swift | 25 +++++++----- fastlane/lanes/screenshots.rb | 3 +- fastlane/lanes/utilities.rb | 38 +++++++------------ 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index e16441e..9ec53f7 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -62,6 +62,7 @@ jobs: GITHUB_REPOSITORY: ${{ github.repository }} - name: Wait for iOS Simulator + id: simulator uses: kula-app/wait-for-services-action/ios-simulator@v1.3.0 with: device: iPhone 17 Pro @@ -71,7 +72,7 @@ jobs: scheme: ScreenshotUITests - name: Generate and Upload Screenshots to Sentry - run: bundle exec fastlane generate_and_upload_screenshots_ci + run: bundle exec fastlane generate_and_upload_screenshots_ci "simulator_udid:${{ steps.simulator.outputs.udid }}" env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} LICENSE_PLIST_GITHUB_TOKEN: ${{ steps.github_app_token.outputs.token }} diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index dd65676..5653aa3 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -32,6 +32,7 @@ jobs: bundler-cache: true - name: Wait for iOS Simulator + id: simulator uses: kula-app/wait-for-services-action/ios-simulator@v1.3.0 with: device: iPhone 17 Pro @@ -41,7 +42,7 @@ jobs: scheme: ScreenshotUITests - name: Capture and Upload Screenshots - run: bundle exec fastlane generate_and_upload_screenshots_ci + run: bundle exec fastlane generate_and_upload_screenshots_ci "simulator_udid:${{ steps.simulator.outputs.udid }}" env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} LICENSE_PLIST_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Targets/ScreenshotUITests/Sources/ScreenshotUITests.swift b/Targets/ScreenshotUITests/Sources/ScreenshotUITests.swift index 6c46424..d6d2eb2 100644 --- a/Targets/ScreenshotUITests/Sources/ScreenshotUITests.swift +++ b/Targets/ScreenshotUITests/Sources/ScreenshotUITests.swift @@ -5,6 +5,18 @@ final class ScreenshotUITests: XCTestCase { continueAfterFailure = false } + @MainActor + private func revealEditButton(for item: XCUIElement, named name: String, in app: XCUIApplication) -> XCUIElement { + let editButton = app.buttons["Edit \(name)"] + + item.swipeLeft() + if !editButton.waitForExistence(timeout: 5) { + item.swipeLeft() + } + + return editButton + } + @MainActor func testScreenshots() throws { // swiftlint:disable:this function_body_length let app = XCUIApplication() @@ -59,11 +71,8 @@ final class ScreenshotUITests: XCTestCase { let createdLinkButton = app.buttons.containing(NSPredicate(format: "label CONTAINS 'techprimate.com'")).firstMatch XCTAssert(createdLinkButton.waitForExistence(timeout: 10), "Newly created link not found after creation") - // Long press to bring up context menu for edit - createdLinkButton.press(forDuration: 1.0) - - let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS 'Edit'")).firstMatch - XCTAssert(editButton.waitForExistence(timeout: 10), "Edit button not found in context menu") + let editButton = revealEditButton(for: createdLinkButton, named: "techprimate.com", in: app) + XCTAssert(editButton.waitForExistence(timeout: 10), "Edit button not found in swipe actions") editButton.tap() // Wait for edit form to appear @@ -147,10 +156,8 @@ final class ScreenshotUITests: XCTestCase { // Open the edit list editor let createdListButton = app.buttons.containing(NSPredicate(format: "label CONTAINS '\(lists[idx].name)'")).firstMatch XCTAssert(createdListButton.waitForExistence(timeout: 10), "Created list button not found for list \(idx)") - createdListButton.press(forDuration: 1.0) - - let editButton = app.buttons.containing(NSPredicate(format: "label CONTAINS 'Edit \(lists[idx].name)'")).firstMatch - XCTAssert(editButton.waitForExistence(timeout: 10), "Edit button not found in context menu") + let editButton = revealEditButton(for: createdListButton, named: lists[idx].name, in: app) + XCTAssert(editButton.waitForExistence(timeout: 10), "Edit button not found in swipe actions") editButton.tap() // Wait for the edit list form to appear diff --git a/fastlane/lanes/screenshots.rb b/fastlane/lanes/screenshots.rb index a3ada65..f231c93 100644 --- a/fastlane/lanes/screenshots.rb +++ b/fastlane/lanes/screenshots.rb @@ -184,6 +184,7 @@ Options: language: language code (default: en-US) output_dir: output directory (default: fastlane/screenshots) + expected_count: expected screenshot count (default: all configured devices) DESC lane :collect_screenshots do |options| language = options[:language] || SCREENSHOT_LANGUAGE @@ -206,7 +207,7 @@ UI.message " ✅ #{File.basename(file)}" end - expected = SCREENSHOT_DEVICES.length * 4 # 4 screenshots per device + expected = options[:expected_count]&.to_i || SCREENSHOT_DEVICES.length * 4 if screenshots.length == expected UI.success "✅ All #{screenshots.length} screenshots collected!" else diff --git a/fastlane/lanes/utilities.rb b/fastlane/lanes/utilities.rb index 0af4ede..dd67ebc 100644 --- a/fastlane/lanes/utilities.rb +++ b/fastlane/lanes/utilities.rb @@ -134,32 +134,19 @@ Captures screenshots on a single iPhone device for faster CI builds Use generate_screenshots for full multi-device App Store screenshots DESC -lane :generate_screenshots_ci do +lane :generate_screenshots_ci do |options| UI.message "Generating screenshots for CI (single device)" - capture_screenshots( - scheme: "ScreenshotUITests", - devices: [ - "iPhone 17 Pro" # iPhone 6.3" display - ], - languages: ["en-US"], - configuration: "Debug", # Use Debug to reduce build time (~5-6 min faster than Release) - - clear_previous_screenshots: true, - concurrent_simulators: false, - skip_open_summary: true, - - reinstall_app: true, - override_status_bar: true, - localize_simulator: true, - disable_slide_to_type: true, + derived_data_path = options[:derived_data_path] || "/tmp/screenshot_derived_data" + simulator_udid = options[:simulator_udid] - skip_helper_version_check: true, - - # See generate_screenshots: retry flakes, then fail loudly. - number_of_retries: 3, - stop_after_first_error: true + build_screenshots(derived_data_path: derived_data_path) + run_screenshot_on_device( + device: "iPhone 17 Pro", + simulator_udid: simulator_udid, + derived_data_path: derived_data_path ) + collect_screenshots(expected_count: 4) UI.success "✅ CI screenshots generated successfully!" UI.message "Screenshots generated in: fastlane/screenshots/" @@ -206,8 +193,11 @@ Fast single-device screenshot generation for CI builds Combines generate_screenshots_ci and upload_screenshots_to_sentry lanes DESC -lane :generate_and_upload_screenshots_ci do - generate_screenshots_ci +lane :generate_and_upload_screenshots_ci do |options| + generate_screenshots_ci( + simulator_udid: options[:simulator_udid], + derived_data_path: options[:derived_data_path] + ) upload_screenshots_to_sentry end From ce9ad400709edee0ea2465d6bdac93eb36dc9d56 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 1 Sep 2026 15:09:56 +0200 Subject: [PATCH 2/4] perf(ci): Target screenshot build destination Build the single-device CI bundle only for the resolved simulator architecture. Reuse the explicit derived data path during test-without-building to avoid an extra build-settings lookup. --- fastlane/lanes/screenshots.rb | 5 ++++- fastlane/lanes/utilities.rb | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/fastlane/lanes/screenshots.rb b/fastlane/lanes/screenshots.rb index f231c93..5b8d619 100644 --- a/fastlane/lanes/screenshots.rb +++ b/fastlane/lanes/screenshots.rb @@ -38,9 +38,11 @@ The output can then be used by run_screenshot_on_device for each device. Options: derived_data_path: path for build products (default: /tmp/screenshot_derived_data) + destination: xcodebuild destination (default: generic iOS Simulator) DESC lane :build_screenshots do |options| derived_data_path = options[:derived_data_path] || "/tmp/screenshot_derived_data" + destination = options[:destination] || "generic/platform=iOS Simulator" UI.message "Building screenshot test bundle..." @@ -49,7 +51,7 @@ scheme: "ScreenshotUITests", configuration: "Debug", derived_data_path: derived_data_path, - destination: "generic/platform=iOS Simulator", + destination: destination, build_for_testing: true, xcargs: "SWIFT_TREAT_WARNINGS_AS_ERRORS=NO" ) @@ -134,6 +136,7 @@ result = run_tests( project: "Flinky.xcodeproj", scheme: "ScreenshotUITests", + derived_data_path: derived_data_path, xctestrun: xctestrun_path, test_without_building: true, destination: "platform=iOS Simulator,id=#{simulator_udid}", diff --git a/fastlane/lanes/utilities.rb b/fastlane/lanes/utilities.rb index dd67ebc..34af874 100644 --- a/fastlane/lanes/utilities.rb +++ b/fastlane/lanes/utilities.rb @@ -139,8 +139,9 @@ derived_data_path = options[:derived_data_path] || "/tmp/screenshot_derived_data" simulator_udid = options[:simulator_udid] + destination = simulator_udid ? "platform=iOS Simulator,id=#{simulator_udid}" : nil - build_screenshots(derived_data_path: derived_data_path) + build_screenshots(derived_data_path: derived_data_path, destination: destination) run_screenshot_on_device( device: "iPhone 17 Pro", simulator_udid: simulator_udid, From 20a4dfab23d2a10b86bed2a74570fc101f94b644 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 1 Sep 2026 16:48:12 +0200 Subject: [PATCH 3/4] perf(ci): Skip screenshot prewarming Let the real build-for-testing invocation initialize Xcode after the simulator boots. Keep prewarming enabled in release workflows until the PR experiment confirms stability. --- .github/workflows/screenshots.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 5653aa3..b24e7a2 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -37,7 +37,7 @@ jobs: with: device: iPhone 17 Pro boot: true - warm-xcodebuild-settings: true + warm-xcodebuild-settings: false project: Flinky.xcodeproj scheme: ScreenshotUITests From 8fdfa568565790f756ee5141e97e82c83a092c09 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 1 Sep 2026 17:01:15 +0200 Subject: [PATCH 4/4] fix(ci): Restore screenshot prewarming Keep the simulator action warm-up after the no-prewarm experiment showed Fastlane timing out before build-for-testing could begin. --- .github/workflows/screenshots.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index b24e7a2..5653aa3 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -37,7 +37,7 @@ jobs: with: device: iPhone 17 Pro boot: true - warm-xcodebuild-settings: false + warm-xcodebuild-settings: true project: Flinky.xcodeproj scheme: ScreenshotUITests