Skip to content

perf(ci): Parallelize screenshot generation across CI jobs#113

Merged
philprime merged 13 commits intomainfrom
perf/parallel-screenshots
Apr 27, 2026
Merged

perf(ci): Parallelize screenshot generation across CI jobs#113
philprime merged 13 commits intomainfrom
perf/parallel-screenshots

Conversation

@philprime
Copy link
Copy Markdown
Member

Parallelize App Store screenshot generation to reduce release workflow time.

Previously, fastlane snapshot ran screenshots in 2 batches (iPhones, then iPads), rebuilding the test target for each batch. With 4 devices this took ~48 minutes — most of it building, not testing (actual UI tests take <1 min per device).

This PR introduces a build-once + run-per-device approach:

  1. Build phase: build_screenshots lane runs xcodebuild build-for-testing once
  2. Run phase: run_screenshot_on_device lane runs test-without-building on a single device with status bar override
  3. Collect phase: collect_screenshots gathers PNGs into fastlane/screenshots/en-US/

The release workflow is restructured into parallel jobs:

  • release-build — IPA build, validation, Sentry setup (runs in parallel with screenshots)
  • build-screenshots — single test bundle build
  • screenshot (matrix: 4 devices) — parallel per-device test execution
  • release-upload — collects everything, uploads to App Store

Benefits:

  • Eliminates duplicate builds (was 2, now 1)
  • IPA build and screenshot build run in parallel
  • Per-device jobs can be retried individually on failure
  • No resource contention — each device gets its own runner

New lanes release_ci_build and release_ci_upload split the monolithic release_ci for the multi-job workflow. The original release_ci lane is preserved for single-job use.

Split screenshot generation into build-once + run-per-device jobs
to eliminate fastlane snapshot's per-device-family rebuild and enable
targeted retries on failure.

New lanes:
- build_screenshots: xcodebuild build-for-testing (single build)
- run_screenshot_on_device: test-without-building on one device
- collect_screenshots: gather results into fastlane/screenshots/
- generate_screenshots_parallel: local convenience lane

Split release_ci into release_ci_build + release_ci_upload so the
release workflow can run IPA build and screenshot generation in
parallel, then combine for App Store upload.
@sentry
Copy link
Copy Markdown

sentry Bot commented Mar 20, 2026

📲 Install Builds

iOS

🔗 App Name App ID Version Configuration
Flinky com.techprimate.Flinky 1.1.3 (53) --

⚙️ flinky Build Distribution Settings

@philprime philprime marked this pull request as ready for review April 16, 2026 14:38
Adds a resolve-ref job that captures main's HEAD once, then feeds that
SHA into every downstream job's checkout. Without this, each parallel
job resolves `ref: main` independently and could diverge if main
advances mid-release.
Adds a `ref` workflow_dispatch input (default: main) so the release
can be tested from a feature branch before merging. The resolve-ref
job resolves whichever ref was dispatched to an explicit SHA.

Concurrency group is now ref-agnostic: only one release runs at a
time regardless of ref, to avoid App Store Connect contention.
Comment thread fastlane/lanes/release.rb Outdated
scan auto-discovers Flinky.xcodeproj from cwd and bails with "Multiple
schemes found". Adds explicit scheme: "ScreenshotUITests" and
test_without_building: true to reuse the pre-built xctestrun bundle
instead of triggering a rebuild.
_commit_and_tag_version_signed overlays VERSION_BUMP_FILES onto main's
tree and fast-forwards main, regardless of which ref was dispatched.
When dispatched from a feature branch, the resulting tag points to a
hybrid tree that does not match the IPA actually uploaded — a data
integrity issue.

release_ci_upload now accepts a `ref` option and skips the commit+tag
step unless ref == main. The release workflow passes inputs.ref
through. Normal main releases are unchanged; feature-branch test
dispatches leave git history clean.
Comment thread fastlane/lanes/screenshots.rb Outdated
Comment thread fastlane/lanes/screenshots.rb Outdated
_boot_simulator ran `xcrun simctl boot` and immediately returned, but
that command only kicks off the boot process — it doesn't wait for the
simulator to reach the home screen. On slow CI runners,
_override_status_bar would then race the boot and silently no-op,
leaking the real clock into screenshots (same class of bug that PR
#120 fixed in the snapshot action's path).

Add `xcrun simctl bootstatus <udid> -b` after `boot` to block until the
simulator is fully booted. This matches what fastlane/snapshot does in
simulator_launcher_base.rb:124.
Comment thread .github/workflows/release.yml
The matrix screenshot jobs use scan (run_tests) rather than snapshot
(capture_screenshots), so the retries added in #119 don't apply here.
iPad matrix jobs are consistently flaking at the long-press /
context-menu interaction (ScreenshotUITests:66) and failing the whole
release because release-upload needs the matrix.

Add number_of_retries: 3 to the run_tests call so scan retries the
test before giving up, matching the snapshot lane's behaviour.
Comment thread fastlane/lanes/screenshots.rb Outdated
upload_to_app_store rejects build_number when ipa: is set; the value is
read from the IPA. Drop the duplicate arg so publish_ci_upload stops
failing with "You can't use 'build_number' and 'ipa' options in one run."

Rename release_beta_ci -> beta_ci, release_ci_build -> publish_ci_build,
release_ci_upload -> publish_ci_upload so the local/CI lane pairs share
prefixes. Drop the dead release_ci lane (superseded by the parallel
build/upload split).
run_screenshot_on_device already passes number_of_retries: 3 to scan, but
GHA simulator runs still failed the step when the first iteration flaked
and a later iteration passed: scan's exit logic counted the retried
attempt as a real failure.

Switch the run_tests call to fail_build: false and decide pass/fail from
the returned :number_of_failures_excluding_retries. Add
output_remove_retry_attempts: true so reports stay clean. Wrap everything
in a 2-attempt outer loop that hard-reboots the simulator
(shutdown + boot + bootstatus -b) between attempts to recover from a
wedged sim that even -retry-tests-on-failure can't unstick.

Drop log: false on the simctl boot/shutdown/bootstatus/clear commands so
their output is visible when CI fails — only the bulky
`simctl list devices -j` JSON dump stays silenced.
Comment thread fastlane/lanes/screenshots.rb
@philprime philprime enabled auto-merge (squash) April 27, 2026 09:59
@philprime philprime disabled auto-merge April 27, 2026 10:02
@philprime philprime merged commit 085be18 into main Apr 27, 2026
17 of 22 checks passed
@philprime philprime deleted the perf/parallel-screenshots branch April 27, 2026 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant