Skip to content

Capture & Upload Screenshots #13

Capture & Upload Screenshots

Capture & Upload Screenshots #13

Workflow file for this run

# 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