From 594b128a6836f4d29e853d086ca28274257fade1 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Tue, 3 Mar 2026 10:55:15 -0800 Subject: [PATCH 1/9] Try out snapshots upload flow --- .../workflows/ios_sentry_upload_snapshots.yml | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 .github/workflows/ios_sentry_upload_snapshots.yml diff --git a/.github/workflows/ios_sentry_upload_snapshots.yml b/.github/workflows/ios_sentry_upload_snapshots.yml new file mode 100644 index 00000000..34ecfac7 --- /dev/null +++ b/.github/workflows/ios_sentry_upload_snapshots.yml @@ -0,0 +1,83 @@ +name: Sentry iOS Upload (Snapshots) + +on: + push: + branches: [main] + pull_request: + branches: [main] + paths: [ios/**, .github/workflows/ios*] + +jobs: + upload_sentry_snapshots: + runs-on: macos-26 + + defaults: + run: + working-directory: ./ios + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Generate snapshot images (iPhone) + env: + EMERGE_DEFAULT_DEVICE_NAME: iPhone 11 Pro Max + run: | + set -o pipefail && xcodebuild test \ + -scheme HackerNews \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 11 Pro Max' \ + -only-testing:HackerNewsTests/HackerNewsSnapshotTest \ + -resultBundlePath ../SnapshotResults-iphone.xcresult \ + ONLY_ACTIVE_ARCH=YES \ + TARGETED_DEVICE_FAMILY=1 \ + SUPPORTS_MACCATALYST=NO + continue-on-error: true + + - name: Generate snapshot images (iPad) + env: + EMERGE_DEFAULT_DEVICE_NAME: iPhone 11 Pro Max + run: | + set -o pipefail && xcodebuild test \ + -scheme HackerNews \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPad Air (5th generation)' \ + -only-testing:HackerNewsTests/HackerNewsSnapshotTest \ + -resultBundlePath ../SnapshotResults-ipad.xcresult \ + ONLY_ACTIVE_ARCH=YES \ + TARGETED_DEVICE_FAMILY="1,2" \ + SUPPORTS_MACCATALYST=NO + continue-on-error: true + + - name: Extract images (iPhone) + run: | + xcrun xcresulttool export attachments \ + --path ../SnapshotResults-iphone.xcresult \ + --output-path ../snapshots-iphone + + - name: Extract images (iPad) + run: | + xcrun xcresulttool export attachments \ + --path ../SnapshotResults-ipad.xcresult \ + --output-path ../snapshots-ipad + + - name: Merge images + run: | + mkdir -p ../snapshot-images + cp ../snapshots-iphone/*.png ../snapshot-images/ 2>/dev/null || true + cp ../snapshots-ipad/*.png ../snapshot-images/ 2>/dev/null || true + + - name: Install sentry-cli + run: npm install -g @sentry/cli + + - name: Upload snapshots to Sentry + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + run: | + sentry-cli build snapshots \ + --org emerge-tools \ + --project hackernews-ios \ + --app-id com.emergetools.hackernews \ + ../snapshot-images From c140e230c56d75e2bb1f311e81e83f18b2461de1 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Tue, 3 Mar 2026 10:56:06 -0800 Subject: [PATCH 2/9] add a ls step --- .github/workflows/ios_sentry_upload_snapshots.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ios_sentry_upload_snapshots.yml b/.github/workflows/ios_sentry_upload_snapshots.yml index 34ecfac7..bc19997f 100644 --- a/.github/workflows/ios_sentry_upload_snapshots.yml +++ b/.github/workflows/ios_sentry_upload_snapshots.yml @@ -69,6 +69,12 @@ jobs: cp ../snapshots-iphone/*.png ../snapshot-images/ 2>/dev/null || true cp ../snapshots-ipad/*.png ../snapshot-images/ 2>/dev/null || true + - name: List generated images + run: | + echo "Generated snapshot images:" + ls -1 ../snapshot-images/ + echo "Total: $(ls -1 ../snapshot-images/ | wc -l | tr -d ' ') images" + - name: Install sentry-cli run: npm install -g @sentry/cli From 811838a3e9ed9abf1ebd9508182a4262d761ad03 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Tue, 3 Mar 2026 11:02:16 -0800 Subject: [PATCH 3/9] dont continue on error --- .github/workflows/ios_sentry_upload_snapshots.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ios_sentry_upload_snapshots.yml b/.github/workflows/ios_sentry_upload_snapshots.yml index bc19997f..da0e69ea 100644 --- a/.github/workflows/ios_sentry_upload_snapshots.yml +++ b/.github/workflows/ios_sentry_upload_snapshots.yml @@ -34,7 +34,6 @@ jobs: ONLY_ACTIVE_ARCH=YES \ TARGETED_DEVICE_FAMILY=1 \ SUPPORTS_MACCATALYST=NO - continue-on-error: true - name: Generate snapshot images (iPad) env: @@ -49,7 +48,6 @@ jobs: ONLY_ACTIVE_ARCH=YES \ TARGETED_DEVICE_FAMILY="1,2" \ SUPPORTS_MACCATALYST=NO - continue-on-error: true - name: Extract images (iPhone) run: | From dc36282b21d857eac5e7bd22b61df23aa91d3854 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Thu, 19 Mar 2026 10:36:38 -0700 Subject: [PATCH 4/9] update --- .github/workflows/ios_emerge_upload_snapshots.yml | 3 --- .github/workflows/ios_sentry_upload_snapshots.yml | 8 ++++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ios_emerge_upload_snapshots.yml b/.github/workflows/ios_emerge_upload_snapshots.yml index 9c00112c..0895e85c 100644 --- a/.github/workflows/ios_emerge_upload_snapshots.yml +++ b/.github/workflows/ios_emerge_upload_snapshots.yml @@ -3,9 +3,6 @@ name: Emerge PR iOS Upload (Snapshots) on: push: branches: [main] - pull_request: - branches: [main] - paths: [ios/**, .github/workflows/ios*] jobs: upload_emerge_snapshots: diff --git a/.github/workflows/ios_sentry_upload_snapshots.yml b/.github/workflows/ios_sentry_upload_snapshots.yml index da0e69ea..e3dc34e2 100644 --- a/.github/workflows/ios_sentry_upload_snapshots.yml +++ b/.github/workflows/ios_sentry_upload_snapshots.yml @@ -23,12 +23,12 @@ jobs: - name: Generate snapshot images (iPhone) env: - EMERGE_DEFAULT_DEVICE_NAME: iPhone 11 Pro Max + EMERGE_DEFAULT_DEVICE_NAME: iPhone 17 Pro Max run: | set -o pipefail && xcodebuild test \ -scheme HackerNews \ -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone 11 Pro Max' \ + -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' \ -only-testing:HackerNewsTests/HackerNewsSnapshotTest \ -resultBundlePath ../SnapshotResults-iphone.xcresult \ ONLY_ACTIVE_ARCH=YES \ @@ -37,12 +37,12 @@ jobs: - name: Generate snapshot images (iPad) env: - EMERGE_DEFAULT_DEVICE_NAME: iPhone 11 Pro Max + EMERGE_DEFAULT_DEVICE_NAME: iPhone 17 Pro Max run: | set -o pipefail && xcodebuild test \ -scheme HackerNews \ -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPad Air (5th generation)' \ + -destination 'platform=iOS Simulator,name=iPad Air 11-inch (M3)' \ -only-testing:HackerNewsTests/HackerNewsSnapshotTest \ -resultBundlePath ../SnapshotResults-ipad.xcresult \ ONLY_ACTIVE_ARCH=YES \ From 0fea4c1d0948cc4d48f736fa3c18ba20d61378c2 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Thu, 19 Mar 2026 11:04:34 -0700 Subject: [PATCH 5/9] try uploading via fastlane + clean up log output --- .../workflows/ios_sentry_upload_snapshots.yml | 78 +++++++++++-------- ios/fastlane/Fastfile | 11 +++ 2 files changed, 55 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ios_sentry_upload_snapshots.yml b/.github/workflows/ios_sentry_upload_snapshots.yml index e3dc34e2..0b9cc4e4 100644 --- a/.github/workflows/ios_sentry_upload_snapshots.yml +++ b/.github/workflows/ios_sentry_upload_snapshots.yml @@ -21,6 +21,22 @@ jobs: with: fetch-depth: 0 + - name: Set up Ruby env + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3.10 + bundler-cache: true + + - name: Setup gems + run: exec ../.github/scripts/ios/setup.sh + + - name: Cache Swift Package Manager + uses: actions/cache@v4 + with: + path: ~/Library/Caches/org.swift.swiftpm + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: ${{ runner.os }}-spm- + - name: Generate snapshot images (iPhone) env: EMERGE_DEFAULT_DEVICE_NAME: iPhone 17 Pro Max @@ -35,53 +51,47 @@ jobs: TARGETED_DEVICE_FAMILY=1 \ SUPPORTS_MACCATALYST=NO - - name: Generate snapshot images (iPad) - env: - EMERGE_DEFAULT_DEVICE_NAME: iPhone 17 Pro Max - run: | - set -o pipefail && xcodebuild test \ - -scheme HackerNews \ - -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPad Air 11-inch (M3)' \ - -only-testing:HackerNewsTests/HackerNewsSnapshotTest \ - -resultBundlePath ../SnapshotResults-ipad.xcresult \ - ONLY_ACTIVE_ARCH=YES \ - TARGETED_DEVICE_FAMILY="1,2" \ - SUPPORTS_MACCATALYST=NO + # - name: Generate snapshot images (iPad) + # env: + # EMERGE_DEFAULT_DEVICE_NAME: iPhone 17 Pro Max + # run: | + # set -o pipefail && xcodebuild test \ + # -scheme HackerNews \ + # -sdk iphonesimulator \ + # -destination 'platform=iOS Simulator,name=iPad Air 11-inch (M3)' \ + # -only-testing:HackerNewsTests/HackerNewsSnapshotTest \ + # -resultBundlePath ../SnapshotResults-ipad.xcresult \ + # ONLY_ACTIVE_ARCH=YES \ + # TARGETED_DEVICE_FAMILY="1,2" \ + # SUPPORTS_MACCATALYST=NO - name: Extract images (iPhone) run: | xcrun xcresulttool export attachments \ --path ../SnapshotResults-iphone.xcresult \ - --output-path ../snapshots-iphone + --output-path ../snapshots-iphone \ + | grep "Generated manifest" - - name: Extract images (iPad) - run: | - xcrun xcresulttool export attachments \ - --path ../SnapshotResults-ipad.xcresult \ - --output-path ../snapshots-ipad + # - name: Extract images (iPad) + # run: | + # xcrun xcresulttool export attachments \ + # --path ../SnapshotResults-ipad.xcresult \ + # --output-path ../snapshots-ipad \ + # | grep "Generated manifest" - name: Merge images run: | - mkdir -p ../snapshot-images - cp ../snapshots-iphone/*.png ../snapshot-images/ 2>/dev/null || true - cp ../snapshots-ipad/*.png ../snapshot-images/ 2>/dev/null || true + mkdir -p snapshot-images + cp ../snapshots-iphone/*.png snapshot-images/ 2>/dev/null || true + # cp ../snapshots-ipad/*.png snapshot-images/ 2>/dev/null || true - name: List generated images run: | echo "Generated snapshot images:" - ls -1 ../snapshot-images/ - echo "Total: $(ls -1 ../snapshot-images/ | wc -l | tr -d ' ') images" - - - name: Install sentry-cli - run: npm install -g @sentry/cli + ls -1 snapshot-images/ + echo "Total: $(ls -1 snapshot-images/ | wc -l | tr -d ' ') images" - name: Upload snapshots to Sentry env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - run: | - sentry-cli build snapshots \ - --org emerge-tools \ - --project hackernews-ios \ - --app-id com.emergetools.hackernews \ - ../snapshot-images + SENTRY_SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_SENTRY_AUTH_TOKEN }} + run: bundle exec fastlane ios upload_sentry_preview_snapshots diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index cfcccf66..41a56dc2 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -291,6 +291,17 @@ platform :ios do UI.success("Successfully replaced app in XCArchive with thinned build for #{device_type}") end + desc 'Upload SnapshotPreviews snapshots to Sentry' + lane :upload_sentry_preview_snapshots do + sentry_upload_snapshots( + path: 'snapshot-images', + app_id: 'com.emergetools.hackernews', + auth_token: ENV['SENTRY_SENTRY_AUTH_TOKEN'], + org_slug: 'sentry', + project_slug: 'hackernews-ios' + ) + end + desc 'Upload swift-snapshot-testing snapshots to Sentry' lane :upload_sentry_snapshots do sentry_upload_snapshots( From 709f559776bd2de37d1b35f0b5661b47aef3bf99 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Thu, 19 Mar 2026 11:15:07 -0700 Subject: [PATCH 6/9] add xcpretty and pre-boot the simulator --- .github/workflows/ios_sentry_upload_snapshots.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ios_sentry_upload_snapshots.yml b/.github/workflows/ios_sentry_upload_snapshots.yml index 0b9cc4e4..fed83747 100644 --- a/.github/workflows/ios_sentry_upload_snapshots.yml +++ b/.github/workflows/ios_sentry_upload_snapshots.yml @@ -33,10 +33,15 @@ jobs: - name: Cache Swift Package Manager uses: actions/cache@v4 with: - path: ~/Library/Caches/org.swift.swiftpm + path: | + ~/Library/Caches/org.swift.swiftpm + ~/Library/Developer/Xcode/DerivedData/HackerNews-*/SourcePackages key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} restore-keys: ${{ runner.os }}-spm- + - name: Boot simulator + run: xcrun simctl boot "iPhone 17 Pro Max" || true + - name: Generate snapshot images (iPhone) env: EMERGE_DEFAULT_DEVICE_NAME: iPhone 17 Pro Max @@ -49,7 +54,8 @@ jobs: -resultBundlePath ../SnapshotResults-iphone.xcresult \ ONLY_ACTIVE_ARCH=YES \ TARGETED_DEVICE_FAMILY=1 \ - SUPPORTS_MACCATALYST=NO + SUPPORTS_MACCATALYST=NO \ + | xcpretty # - name: Generate snapshot images (iPad) # env: @@ -63,7 +69,8 @@ jobs: # -resultBundlePath ../SnapshotResults-ipad.xcresult \ # ONLY_ACTIVE_ARCH=YES \ # TARGETED_DEVICE_FAMILY="1,2" \ - # SUPPORTS_MACCATALYST=NO + # SUPPORTS_MACCATALYST=NO \ + # | xcpretty - name: Extract images (iPhone) run: | From 2abfe8289e503f04f31910643e3128daab18220b Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Thu, 19 Mar 2026 12:39:44 -0700 Subject: [PATCH 7/9] try new script approach --- .github/emerge-snapshots.yml | 14 + .github/scripts/ios/emerge-snapshots | 263 ++++++++++++++++++ .../ios_sentry_upload_snapshots_new.yml | 48 ++++ 3 files changed, 325 insertions(+) create mode 100644 .github/emerge-snapshots.yml create mode 100755 .github/scripts/ios/emerge-snapshots create mode 100644 .github/workflows/ios_sentry_upload_snapshots_new.yml diff --git a/.github/emerge-snapshots.yml b/.github/emerge-snapshots.yml new file mode 100644 index 00000000..1624cb98 --- /dev/null +++ b/.github/emerge-snapshots.yml @@ -0,0 +1,14 @@ +scheme: HackerNews +test_target: HackerNewsTests/HackerNewsSnapshotTest +bundle_id: com.emergetools.hackernews +max_retries: 1 + +destinations: + - name: iPhone 17 Pro Max + sdk: iphonesimulator + xcode_flags: TARGETED_DEVICE_FAMILY=1 + enabled: true + - name: iPad Air 11-inch (M3) + sdk: iphonesimulator + xcode_flags: TARGETED_DEVICE_FAMILY=1,2 + enabled: false diff --git a/.github/scripts/ios/emerge-snapshots b/.github/scripts/ios/emerge-snapshots new file mode 100755 index 00000000..60dac0cb --- /dev/null +++ b/.github/scripts/ios/emerge-snapshots @@ -0,0 +1,263 @@ +#!/usr/bin/env bash +# Benefits pulled from Emerge's internal iOS snapshot infrastructure: +# +# - Crash report collection: after any xcodebuild failure, captures .ips files from +# ~/Library/Logs/DiagnosticReports scoped to the test run window +# - Retry on crash: distinguishes crash failures from legitimate test failures and +# retries automatically (configurable via max_retries) +# - Zero-size image detection: scans extracted PNGs with sips after xcresulttool +# export to catch renders that "succeed" but produce empty/corrupt images +# - Multi-device orchestration: runs against multiple simulators (iPhone, iPad, OS +# versions) and collects results per-device before aggregating +# - Permission reset before run: calls xcrun simctl privacy reset all before each +# run to prevent permission dialogs from blocking renders +# - Config file support: YAML config for scheme, test target, bundle ID, and +# destinations — no bash knowledge required for customers +# - Simulator lifecycle management: boots simulators, waits for ready state, and +# tears down cleanly between runs +# - Structured summary manifest: emits a JSON manifest with per-device image counts, +# attempt numbers, and any captured crash report filenames +# +# Potential follow-ups (not yet implemented): +# - Regex-based exclusion filtering: filter extracted PNGs by filename pattern before +# upload, driven by exclusion rules in the config file +# - Per-test timing: extract per-test duration from xcresult and include in the +# manifest so Sentry can surface slow previews +# - Image metadata enrichment: emit a sidecar JSON per image with device name, OS +# version, orientation, and color scheme for richer Sentry UI context +# - Sharding: split the test suite across multiple CI machines by enumerating test +# method names (via xcodebuild build-for-testing + xctest list), accepting +# shard/num_shards config params, and merging per-shard image directories +# afterward; requires the customer's test target to expose one method per preview +set -euo pipefail + +CONFIG_FILE=".github/emerge-snapshots.yml" +OUTPUT_DIR="snapshot-images" +XCRESULT_DIR=".." + +while [[ $# -gt 0 ]]; do + case "$1" in + --config) CONFIG_FILE="$2"; shift 2 ;; + --output-dir) OUTPUT_DIR="$2"; shift 2 ;; + --xcresult-dir) XCRESULT_DIR="$2"; shift 2 ;; + --help|-h) echo "Usage: emerge-snapshots [--config PATH] [--output-dir PATH] [--xcresult-dir PATH]"; exit 0 ;; + *) echo "Unknown argument: $1"; exit 1 ;; + esac +done +[[ -f "$CONFIG_FILE" ]] || { echo "Config not found: $CONFIG_FILE"; exit 1; } + +parse_config() { + ruby -ryaml -e " + c = YAML.load_file(ARGV[0]) + %w[scheme test_target bundle_id].each { |k| abort(\"#{k} required\") unless c[k] } + puts \"SCHEME=#{c['scheme']}\" + puts \"TEST_TARGET=#{c['test_target']}\" + puts \"BUNDLE_ID=#{c['bundle_id']}\" + puts \"MAX_RETRIES=#{c.fetch('max_retries', 1)}\" + puts '---' + (c['destinations'] || []).select { |d| d['enabled'] }.each do |d| + puts [d['name'], d['sdk'], d['xcode_flags']].join('|') + end + " "$1" +} + +config_output=$(parse_config "$CONFIG_FILE") +eval "$(echo "$config_output" | awk '/^---$/{exit} {print}')" +mapfile -t DESTINATIONS < <(echo "$config_output" | awk '/^---$/{found=1; next} found{print}') + +[[ ${#DESTINATIONS[@]} -gt 0 ]] || { echo "No enabled destinations in config"; exit 1; } + +shopt -s nullglob + +TOTAL_IMAGE_COUNT=0 +declare -a DEVICE_SUMMARIES=() +declare -a CAPTURED_CRASH_REPORTS=() + +boot_simulator() { + local device_name="$1" + xcrun simctl boot "$device_name" || true + xcrun simctl bootstatus "$device_name" -b + CURRENT_UDID=$(xcrun simctl list devices booted --json \ + | jq -r '.devices | to_entries[] | .value[] | select(.state == "Booted") | .udid' \ + | head -1) +} + +reset_simulator_permissions() { + local udid="$1" + local bundle_id="$2" + if [ -z "$udid" ]; then + echo "Warning: UDID empty, skipping permission reset" + return 0 + fi + xcrun simctl privacy "$udid" reset all "$bundle_id" +} + +run_tests() { + local device_name="$1" + local sdk="$2" + local xcode_flags="$3" + local result_path="$4" + + rm -rf "$result_path" + touch /tmp/snapshot_test_start_marker + TEST_START_EPOCH=$(date +%s) + + set +e + set -o pipefail + xcodebuild test \ + -scheme "$SCHEME" \ + -sdk "$sdk" \ + -destination "platform=iOS Simulator,name=${device_name}" \ + -only-testing:"$TEST_TARGET" \ + -resultBundlePath "$result_path" \ + $xcode_flags \ + ONLY_ACTIVE_ARCH=YES \ + SUPPORTS_MACCATALYST=NO \ + | xcpretty + local exit_code=$? + set -e + return $exit_code +} + +collect_crash_reports() { + local dest_dir="$1" + [ -d ~/Library/Logs/DiagnosticReports ] || return 0 + + local count=0 + mkdir -p "$dest_dir" + + while IFS= read -r -d '' ips_file; do + cp "$ips_file" "$dest_dir/" + CAPTURED_CRASH_REPORTS+=("$(basename "$ips_file")") + count=$((count + 1)) + done < <(find ~/Library/Logs/DiagnosticReports -name "*.ips" -newer /tmp/snapshot_test_start_marker -print0 2>/dev/null) + + echo "Crash reports collected: $count" + return $count +} + +extract_images() { + local result_path="$1" + local dest_dir="$2" + xcrun xcresulttool export attachments \ + --path "$result_path" \ + --output-path "$dest_dir" +} + +check_zero_size_images() { + local dir="$1" + command -v sips &>/dev/null || return 0 + + for f in "$dir"/*.png; do + local width + width=$(sips -g pixelWidth "$f" | awk '/pixelWidth/{print $2}') + if [ -z "$width" ] || [ "$width" -eq 0 ]; then + echo "Error: Zero-size image detected: $f" + exit 1 + fi + done +} + +run_device() { + local device_name="$1" + local sdk="$2" + local xcode_flags="$3" + local label="$4" + + boot_simulator "$device_name" + reset_simulator_permissions "$CURRENT_UDID" "$BUNDLE_ID" + + local attempt=1 + local exit_code=0 + local xcresult="" + + while true; do + xcresult="$XCRESULT_DIR/SnapshotResults-${label}-attempt${attempt}.xcresult" + run_tests "$device_name" "$sdk" "$xcode_flags" "$xcresult" || exit_code=$? + + if [ "$exit_code" -eq 0 ]; then + break + fi + + local crash_count=0 + collect_crash_reports "$OUTPUT_DIR/crash-reports" || crash_count=$? + + if [ "$crash_count" -gt 0 ] && [ "$attempt" -le "$MAX_RETRIES" ]; then + echo "Crash detected (attempt $attempt), retrying..." + attempt=$((attempt + 1)) + exit_code=0 + continue + else + exit 1 + fi + done + + extract_images "$xcresult" "$XCRESULT_DIR/snapshots-$label" + check_zero_size_images "$XCRESULT_DIR/snapshots-$label" + + local copied=0 + for png in "$XCRESULT_DIR/snapshots-$label/"*.png; do + cp "$png" "$OUTPUT_DIR/" + copied=$((copied + 1)) + done + + TOTAL_IMAGE_COUNT=$((TOTAL_IMAGE_COUNT + copied)) + DEVICE_SUMMARIES+=("${label}|${device_name}|${copied}|${attempt}") +} + +emit_summary() { + echo "" + echo "=== Snapshot Capture Summary ===" + printf "%-35s %-8s %-9s\n" "Device" "Images" "Attempts" + printf "%-35s %-8s %-9s\n" "------" "------" "--------" + + local devices_json="[]" + for entry in "${DEVICE_SUMMARIES[@]}"; do + IFS='|' read -r label name count attempts <<< "$entry" + printf "%-35s %-8s %-9s\n" "$name" "$count" "$attempts" + if command -v jq &>/dev/null; then + devices_json=$(echo "$devices_json" | jq \ + --arg label "$label" \ + --arg name "$name" \ + --argjson count "$count" \ + --argjson attempts "$attempts" \ + '. + [{"label": $label, "name": $name, "image_count": $count, "attempts": $attempts}]') + fi + done + + echo "--------------------------------" + echo "Total images: $TOTAL_IMAGE_COUNT" + + if ! command -v jq &>/dev/null; then + echo "Warning: jq not available, skipping manifest write" + return 0 + fi + + local crash_json="[]" + for cr in "${CAPTURED_CRASH_REPORTS[@]}"; do + crash_json=$(echo "$crash_json" | jq --arg cr "$cr" '. + [$cr]') + done + + jq --null-input \ + --arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ + --argjson devices "$devices_json" \ + --argjson total "$TOTAL_IMAGE_COUNT" \ + --argjson crashes "$crash_json" \ + '{ + generated_at: $ts, + devices: $devices, + total_image_count: $total, + crash_reports_captured: $crashes + }' > "$OUTPUT_DIR/snapshot-manifest.json" +} + +mkdir -p "$OUTPUT_DIR" + +for dest in "${DESTINATIONS[@]}"; do + IFS='|' read -r device_name sdk xcode_flags <<< "$dest" + label=$(echo "$device_name" | tr ' (),' '-' | tr '[:upper:]' '[:lower:]' | tr -s '-') + run_device "$device_name" "$sdk" "$xcode_flags" "$label" +done + +emit_summary +echo "Done. $TOTAL_IMAGE_COUNT images in $OUTPUT_DIR/" diff --git a/.github/workflows/ios_sentry_upload_snapshots_new.yml b/.github/workflows/ios_sentry_upload_snapshots_new.yml new file mode 100644 index 00000000..1e702b8c --- /dev/null +++ b/.github/workflows/ios_sentry_upload_snapshots_new.yml @@ -0,0 +1,48 @@ +name: Sentry iOS Upload (Snapshots) + +on: + push: + branches: [main] + pull_request: + branches: [main] + paths: [ios/**, .github/workflows/ios*] + +jobs: + upload_sentry_snapshots: + runs-on: macos-26 + + defaults: + run: + working-directory: ./ios + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Ruby env + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3.10 + bundler-cache: true + + - name: Setup gems + run: exec ../.github/scripts/ios/setup.sh + + - name: Cache Swift Package Manager + uses: actions/cache@v4 + with: + path: | + ~/Library/Caches/org.swift.swiftpm + ~/Library/Developer/Xcode/DerivedData/HackerNews-*/SourcePackages + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: ${{ runner.os }}-spm- + + - name: Capture snapshots + run: ../.github/scripts/ios/emerge-snapshots --config ../.github/emerge-snapshots.yml + + - name: Upload snapshots to Sentry + env: + SENTRY_SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_SENTRY_AUTH_TOKEN }} + run: bundle exec fastlane ios upload_sentry_preview_snapshots From 573947889c14a921e1e26d7176fa75b222ad2225 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Thu, 19 Mar 2026 12:44:39 -0700 Subject: [PATCH 8/9] tweaks --- .github/scripts/ios/emerge-snapshots | 5 ++++- .github/workflows/ios_sentry_upload_snapshots.yml | 6 +++--- ..._new.yml => ios_sentry_upload_snapshots_with_script.yml} | 0 3 files changed, 7 insertions(+), 4 deletions(-) rename .github/workflows/{ios_sentry_upload_snapshots_new.yml => ios_sentry_upload_snapshots_with_script.yml} (100%) diff --git a/.github/scripts/ios/emerge-snapshots b/.github/scripts/ios/emerge-snapshots index 60dac0cb..48c01e5a 100755 --- a/.github/scripts/ios/emerge-snapshots +++ b/.github/scripts/ios/emerge-snapshots @@ -63,7 +63,10 @@ parse_config() { config_output=$(parse_config "$CONFIG_FILE") eval "$(echo "$config_output" | awk '/^---$/{exit} {print}')" -mapfile -t DESTINATIONS < <(echo "$config_output" | awk '/^---$/{found=1; next} found{print}') +DESTINATIONS=() +while IFS= read -r line; do + DESTINATIONS+=("$line") +done < <(echo "$config_output" | awk '/^---$/{found=1; next} found{print}') [[ ${#DESTINATIONS[@]} -gt 0 ]] || { echo "No enabled destinations in config"; exit 1; } diff --git a/.github/workflows/ios_sentry_upload_snapshots.yml b/.github/workflows/ios_sentry_upload_snapshots.yml index fed83747..435b9666 100644 --- a/.github/workflows/ios_sentry_upload_snapshots.yml +++ b/.github/workflows/ios_sentry_upload_snapshots.yml @@ -3,9 +3,9 @@ name: Sentry iOS Upload (Snapshots) on: push: branches: [main] - pull_request: - branches: [main] - paths: [ios/**, .github/workflows/ios*] + # pull_request: + # branches: [main] + # paths: [ios/**, .github/workflows/ios*] jobs: upload_sentry_snapshots: diff --git a/.github/workflows/ios_sentry_upload_snapshots_new.yml b/.github/workflows/ios_sentry_upload_snapshots_with_script.yml similarity index 100% rename from .github/workflows/ios_sentry_upload_snapshots_new.yml rename to .github/workflows/ios_sentry_upload_snapshots_with_script.yml From dffa25301a315c9baea2ea2148159114b48cbff4 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Thu, 19 Mar 2026 12:53:08 -0700 Subject: [PATCH 9/9] tweak --- .github/scripts/ios/emerge-snapshots | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/scripts/ios/emerge-snapshots b/.github/scripts/ios/emerge-snapshots index 48c01e5a..4fdd0eb8 100755 --- a/.github/scripts/ios/emerge-snapshots +++ b/.github/scripts/ios/emerge-snapshots @@ -1,4 +1,8 @@ #!/usr/bin/env bash +# +# TODO: Potentially put this behind a CLI +# +# # Benefits pulled from Emerge's internal iOS snapshot infrastructure: # # - Crash report collection: after any xcodebuild failure, captures .ips files from @@ -215,7 +219,7 @@ emit_summary() { printf "%-35s %-8s %-9s\n" "------" "------" "--------" local devices_json="[]" - for entry in "${DEVICE_SUMMARIES[@]}"; do + for entry in ${DEVICE_SUMMARIES[@]+"${DEVICE_SUMMARIES[@]}"}; do IFS='|' read -r label name count attempts <<< "$entry" printf "%-35s %-8s %-9s\n" "$name" "$count" "$attempts" if command -v jq &>/dev/null; then @@ -237,7 +241,7 @@ emit_summary() { fi local crash_json="[]" - for cr in "${CAPTURED_CRASH_REPORTS[@]}"; do + for cr in ${CAPTURED_CRASH_REPORTS[@]+"${CAPTURED_CRASH_REPORTS[@]}"}; do crash_json=$(echo "$crash_json" | jq --arg cr "$cr" '. + [$cr]') done