Capture & Upload Screenshots #13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Capture & Upload App Store Screenshots | |
| # | |
| # Manually triggered workflow that captures screenshots on iOS simulators | |
| # and macOS desktop, then uploads them to App Store Connect for both platforms. | |
| # | |
| # Visual layout in GitHub Actions: | |
| # | |
| # capture-ios ──┐ | |
| # ├── upload | |
| # capture-macos ┘ | |
| # | |
| # iOS and macOS capture run in parallel, then upload depends on both. | |
| # | |
| # Required Secrets: | |
| # APP_STORE_CONNECT_API_KEY_KEY - Base64-encoded .p8 API key | |
| # APP_STORE_CONNECT_API_KEY_KEY_ID - API key ID | |
| # APP_STORE_CONNECT_API_KEY_ISSUER_ID - Issuer ID | |
| name: Capture & Upload Screenshots | |
| on: | |
| workflow_dispatch: | |
| concurrency: | |
| group: screenshots | |
| cancel-in-progress: true | |
| env: | |
| FLUTTER_VERSION_FILE: '.github/flutter-version.txt' | |
| jobs: | |
| # ============================================================================ | |
| # Capture iOS Screenshots (iPhone + iPad, in parallel with macOS) | |
| # ============================================================================ | |
| capture-ios: | |
| name: Capture iOS | |
| runs-on: macos-14 | |
| timeout-minutes: 45 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| submodules: recursive | |
| - name: Read Flutter version | |
| id: flutter-ver | |
| run: echo "version=$(cat ${{ env.FLUTTER_VERSION_FILE }})" >> "$GITHUB_OUTPUT" | |
| - name: Setup Xcode | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: 'latest-stable' | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ steps.flutter-ver.outputs.version }} | |
| channel: stable | |
| cache: true | |
| - name: Setup Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.x' | |
| - name: Install Flutter dependencies | |
| run: | | |
| flutter pub get | |
| dart run build_runner build --delete-conflicting-outputs | |
| - name: Generate UDDF test data | |
| run: | | |
| mkdir -p integration_test/fixtures | |
| python3 scripts/generate_uddf_test_data.py -n 50 --sample-interval 30 -o integration_test/fixtures/screenshot_test_data.uddf | |
| - name: Capture iPhone 6.7" screenshots | |
| run: | | |
| UDID=$(xcrun simctl list devices available | grep "iPhone 16 Pro Max" | head -1 | grep -oE "[0-9A-F-]{36}") | |
| if [ -z "$UDID" ]; then | |
| UDID=$(xcrun simctl list devices available | grep "iPhone 15 Pro Max" | head -1 | grep -oE "[0-9A-F-]{36}") | |
| fi | |
| if [ -z "$UDID" ]; then echo "No 6.7-inch iPhone simulator found"; exit 1; fi | |
| xcrun simctl boot "$UDID" 2>/dev/null || true | |
| sleep 3 | |
| xcrun simctl status_bar "$UDID" override \ | |
| --time "9:41" --batteryState charged --batteryLevel 100 \ | |
| --wiFiMode active --wiFiBars 3 --cellularMode active --cellularBars 4 \ | |
| --operatorName "" 2>/dev/null || true | |
| # iOS simulator resolves relative paths against the app bundle (read-only). | |
| # Use absolute paths so the Dart VM can read/write the host filesystem. | |
| # --ignore-timeouts: CI simulators are slow; job timeout is the real guard. | |
| flutter test integration_test/screenshots_test.dart \ | |
| -d "$UDID" \ | |
| --ignore-timeouts \ | |
| --dart-define=SCREENSHOT_MODE=true \ | |
| --dart-define=SCREENSHOT_DEVICE_NAME=iPhone_6_7_inch \ | |
| --dart-define=SCREENSHOT_OUTPUT_DIR="$GITHUB_WORKSPACE/screenshots" \ | |
| --dart-define=SCREENSHOT_ORIENTATION=portrait \ | |
| --dart-define=UDDF_TEST_DATA_PATH="$GITHUB_WORKSPACE/integration_test/fixtures/screenshot_test_data.uddf" | |
| xcrun simctl shutdown "$UDID" 2>/dev/null || true | |
| - name: Capture iPad 13" screenshots (landscape) | |
| run: | | |
| UDID=$(xcrun simctl list devices available | grep "iPad Pro" | grep "13" | head -1 | grep -oE "[0-9A-F-]{36}") | |
| if [ -z "$UDID" ]; then echo "No iPad Pro 13-inch simulator found"; exit 1; fi | |
| xcrun simctl boot "$UDID" 2>/dev/null || true | |
| # Force landscape orientation via Info.plist | |
| PLIST=ios/Runner/Info.plist | |
| cp "$PLIST" "$PLIST.backup" | |
| /usr/libexec/PlistBuddy -c "Delete :UISupportedInterfaceOrientations~ipad" "$PLIST" 2>/dev/null || true | |
| /usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations~ipad array" "$PLIST" | |
| /usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations~ipad:0 string UIInterfaceOrientationLandscapeLeft" "$PLIST" | |
| /usr/libexec/PlistBuddy -c "Add :UISupportedInterfaceOrientations~ipad:1 string UIInterfaceOrientationLandscapeRight" "$PLIST" | |
| sleep 3 | |
| xcrun simctl status_bar "$UDID" override \ | |
| --time "9:41" --batteryState charged --batteryLevel 100 \ | |
| --wiFiMode active --wiFiBars 3 --cellularMode active --cellularBars 4 \ | |
| --operatorName "" 2>/dev/null || true | |
| flutter test integration_test/screenshots_test.dart \ | |
| -d "$UDID" \ | |
| --ignore-timeouts \ | |
| --dart-define=SCREENSHOT_MODE=true \ | |
| --dart-define=SCREENSHOT_DEVICE_NAME=iPad_13_inch \ | |
| --dart-define=SCREENSHOT_OUTPUT_DIR="$GITHUB_WORKSPACE/screenshots" \ | |
| --dart-define=SCREENSHOT_ORIENTATION=landscape \ | |
| --dart-define=UDDF_TEST_DATA_PATH="$GITHUB_WORKSPACE/integration_test/fixtures/screenshot_test_data.uddf" | |
| mv "$PLIST.backup" "$PLIST" | |
| xcrun simctl shutdown "$UDID" 2>/dev/null || true | |
| - name: Upload iOS screenshot artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: screenshots-ios | |
| path: | | |
| screenshots/iPhone_6_7_inch/ | |
| screenshots/iPad_13_inch/ | |
| retention-days: 30 | |
| # ============================================================================ | |
| # Capture macOS Screenshots (in parallel with iOS) | |
| # ============================================================================ | |
| capture-macos: | |
| name: Capture macOS | |
| runs-on: macos-14 | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| submodules: recursive | |
| - name: Read Flutter version | |
| id: flutter-ver | |
| run: echo "version=$(cat ${{ env.FLUTTER_VERSION_FILE }})" >> "$GITHUB_OUTPUT" | |
| - name: Setup Xcode | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: 'latest-stable' | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| flutter-version: ${{ steps.flutter-ver.outputs.version }} | |
| channel: stable | |
| cache: true | |
| - name: Setup Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.x' | |
| - name: Install Flutter dependencies | |
| run: | | |
| flutter pub get | |
| dart run build_runner build --delete-conflicting-outputs | |
| - name: Disable code signing for CI | |
| run: | | |
| echo 'CODE_SIGNING_ALLOWED = NO' >> macos/Runner/Configs/Debug.xcconfig | |
| echo 'CODE_SIGNING_REQUIRED = NO' >> macos/Runner/Configs/Debug.xcconfig | |
| echo 'CODE_SIGN_IDENTITY = ' >> macos/Runner/Configs/Debug.xcconfig | |
| - name: Generate UDDF test data | |
| run: | | |
| mkdir -p integration_test/fixtures | |
| python3 scripts/generate_uddf_test_data.py -n 50 --sample-interval 30 -o integration_test/fixtures/screenshot_test_data.uddf | |
| - name: Capture macOS screenshots | |
| run: | | |
| flutter test integration_test/screenshots_test.dart \ | |
| -d macos \ | |
| --dart-define=SCREENSHOT_MODE=true \ | |
| --dart-define=SCREENSHOT_DEVICE_NAME=macOS \ | |
| --dart-define=SCREENSHOT_OUTPUT_DIR=screenshots \ | |
| --dart-define=UDDF_TEST_DATA_PATH=integration_test/fixtures/screenshot_test_data.uddf | |
| - name: Collect screenshots | |
| run: | | |
| # Without code signing, sandbox is not enforced — screenshots may be | |
| # in the project directory or in the sandbox container. Check both. | |
| if [ -d "screenshots/macOS" ] && ls screenshots/macOS/*.png 1>/dev/null 2>&1; then | |
| echo "Found $(ls screenshots/macOS/*.png | wc -l | tr -d ' ') screenshots in project directory" | |
| else | |
| SANDBOX_SCREENSHOTS="$HOME/Library/Containers/app.submersion/Data/screenshots/macOS" | |
| if [ -d "$SANDBOX_SCREENSHOTS" ] && ls "$SANDBOX_SCREENSHOTS"/*.png 1>/dev/null 2>&1; then | |
| mkdir -p screenshots/macOS | |
| cp "$SANDBOX_SCREENSHOTS"/*.png screenshots/macOS/ | |
| echo "Retrieved $(ls screenshots/macOS/*.png | wc -l | tr -d ' ') screenshots from sandbox container" | |
| else | |
| echo "No screenshots found in project directory or sandbox container" | |
| exit 1 | |
| fi | |
| fi | |
| - name: Upload macOS screenshot artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: screenshots-macos | |
| path: screenshots/macOS/ | |
| retention-days: 30 | |
| # ============================================================================ | |
| # Upload to App Store Connect (after both captures complete) | |
| # ============================================================================ | |
| upload: | |
| name: Upload to App Store Connect | |
| runs-on: macos-14 | |
| needs: [capture-ios, capture-macos] | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup Xcode | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: 'latest-stable' | |
| - name: Download iOS screenshots | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: screenshots-ios | |
| path: screenshots/ | |
| - name: Download macOS screenshots | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: screenshots-macos | |
| path: screenshots/macOS/ | |
| - name: List downloaded screenshots | |
| run: find screenshots -name "*.png" | sort | |
| - name: Setup Ruby (iOS) | |
| uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: '3.2' | |
| bundler-cache: true | |
| working-directory: ios | |
| - name: Install macOS Fastlane gems | |
| run: cd macos && bundle install | |
| - name: Install ImageMagick | |
| run: brew install imagemagick | |
| - name: Decode API Key | |
| env: | |
| APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} | |
| run: | | |
| echo "$APP_STORE_CONNECT_API_KEY_KEY" | base64 -d > ios/fastlane/AuthKey.p8 | |
| cp ios/fastlane/AuthKey.p8 macos/fastlane/AuthKey.p8 | |
| # Each platform's screenshots go into their own directory so Fastlane | |
| # only sees the correct display types for that platform's App Store listing. | |
| - name: Prepare and upload iOS screenshots | |
| env: | |
| APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} | |
| APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} | |
| APP_STORE_CONNECT_API_KEY_KEY_FILEPATH: fastlane/AuthKey.p8 | |
| run: | | |
| mkdir -p screenshots/ios/en-US | |
| for dir in screenshots/iPhone_6_7_inch screenshots/iPad_13_inch; do | |
| if [ -d "$dir" ] && ls "$dir"/*.png 1>/dev/null 2>&1; then | |
| echo "Processing $(basename "$dir")..." | |
| for f in "$dir"/*.png; do | |
| magick "$f" -background white -alpha remove -alpha off "screenshots/ios/en-US/$(basename "$f")" | |
| done | |
| fi | |
| done | |
| echo "iOS screenshots prepared:" | |
| ls -1 screenshots/ios/en-US/ | |
| cd ios && bundle exec fastlane upload_screenshots | |
| - name: Prepare and upload macOS screenshots | |
| env: | |
| APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} | |
| APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} | |
| APP_STORE_CONNECT_API_KEY_KEY_FILEPATH: fastlane/AuthKey.p8 | |
| run: | | |
| mkdir -p screenshots/macos/en-US | |
| if [ -d "screenshots/macOS" ] && ls screenshots/macOS/*.png 1>/dev/null 2>&1; then | |
| echo "Processing macOS..." | |
| for f in screenshots/macOS/*.png; do | |
| magick "$f" -resize 2560x1600 -background white -alpha remove -alpha off "screenshots/macos/en-US/$(basename "$f")" | |
| done | |
| fi | |
| echo "macOS screenshots prepared:" | |
| ls -1 screenshots/macos/en-US/ | |
| cd macos && bundle exec fastlane upload_screenshots | |
| - name: Upload all screenshots as artifact | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: screenshots-organized | |
| path: screenshots/ | |
| retention-days: 30 | |
| - name: Cleanup sensitive files | |
| if: always() | |
| run: | | |
| rm -f ios/fastlane/AuthKey.p8 | |
| rm -f macos/fastlane/AuthKey.p8 |