Skip to content

Commit 30434f9

Browse files
authored
Upload Snapshots to Sentry using SnapshotPreview and SwiftSnapshotTesting libraries (#780)
Add dedicated iOS snapshot upload pipelines for Sentry. We want snapshot artifacts from both SnapshotPreviews and Swift Snapshot Testing in Sentry without piggybacking on the existing Emerge upload job. This PR adds dedicated Fastlane lanes and GitHub Actions workflows for each snapshot source, updates the preview target devices to match the current simulator lineup, and centralizes the runtime checks used to keep snapshot output deterministic during preview and snapshot runs. I kept the Sentry uploads as separate workflows instead of extending the Emerge pipeline further. That keeps each job single-purpose, lets the SnapshotPreviews flow build once and fan out across multiple simulators, and preserves the Swift Snapshot Testing flow as a record-and-upload path with its own wiring. Additional review context: - the SnapshotPreviews workflow now builds once, shares the build products across simulator jobs, and uploads the aggregated PNGs to Sentry in a final step - the Swift Snapshot Testing path now records snapshots in CI and uploads them through a dedicated fastlane lane - snapshot-specific runtime checks now go through a shared helper instead of repeating environment lookups in multiple places
1 parent 187abeb commit 30434f9

13 files changed

Lines changed: 334 additions & 38 deletions

File tree

.github/scripts/ios/setup.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
# Install Ruby Bundler
44
gem install bundler
55

6-
# Install Ruby Gems
6+
# Install Ruby Gems for Fastlane and XCPretty
77
bundle install

.github/workflows/ios_emerge_upload_snapshots.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,3 @@ jobs:
8888
--client-library swift-snapshot-testing \
8989
--project-root . \
9090
--debug
91-
- name: Upload snapshots to Sentry
92-
env:
93-
SENTRY_SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_SENTRY_AUTH_TOKEN }}
94-
run: bundle exec fastlane ios upload_sentry_snapshots
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
name: Sentry Snapshots Upload
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
paths: [ios/**, .github/workflows/ios*, .github/scripts/ios/**]
9+
10+
defaults:
11+
run:
12+
working-directory: ./ios
13+
14+
env:
15+
DERIVED_DATA_PATH: ${{ github.workspace }}/DerivedData-snapshot-upload
16+
SNAPSHOT_UPLOAD_BASE_DIR: ${{ github.workspace }}/ios/snapshot-images
17+
BUILD_PRODUCTS_ARCHIVE: ${{ github.workspace }}/snapshot-build-products.tar.gz
18+
XCTESTRUN_FILENAME: HackerNewsSnapshotTests.xctestrun
19+
20+
jobs:
21+
# Build the snapshot test bundle once on a single runner, then share the
22+
# products with downstream simulator jobs via an artifact.
23+
build_for_testing:
24+
runs-on: macos-26
25+
26+
env:
27+
TEST_RUNNER_EMERGE_IS_RUNNING_FOR_SNAPSHOTS: 1
28+
29+
steps:
30+
- name: Checkout
31+
uses: actions/checkout@v6
32+
with:
33+
fetch-depth: 0
34+
35+
- name: Select Xcode 26.4
36+
run: sudo xcode-select -s /Applications/Xcode_26.4.app
37+
38+
- name: Set up Ruby env
39+
uses: ruby/setup-ruby@v1
40+
with:
41+
ruby-version: 3.3.10
42+
bundler-cache: true
43+
44+
- name: Setup gems
45+
run: exec ../.github/scripts/ios/setup.sh
46+
47+
- name: Prepare DerivedData directory
48+
run: mkdir -p "${DERIVED_DATA_PATH}"
49+
50+
- name: Cache Swift Package Manager
51+
uses: actions/cache@v4
52+
with:
53+
path: |
54+
~/Library/Caches/org.swift.swiftpm
55+
${{ env.DERIVED_DATA_PATH }}/SourcePackages
56+
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
57+
restore-keys: ${{ runner.os }}-spm-
58+
59+
- name: Build snapshot tests for distribution
60+
run: |
61+
set -o pipefail && xcodebuild build-for-testing \
62+
-scheme HackerNews \
63+
-sdk iphonesimulator \
64+
-destination 'platform=iOS Simulator,name=iPhone 17 Pro Max,arch=arm64' \
65+
-only-testing:HackerNewsTests/HackerNewsSnapshotTest \
66+
-resultBundlePath ../SnapshotBuildForTesting.xcresult \
67+
-derivedDataPath "${DERIVED_DATA_PATH}" \
68+
-skipPackagePluginValidation \
69+
ONLY_ACTIVE_ARCH=YES \
70+
TARGETED_DEVICE_FAMILY="1,2" \
71+
SUPPORTS_MACCATALYST=NO \
72+
CODE_SIGNING_ALLOWED=NO \
73+
COMPILATION_CACHING=YES \
74+
EAGER_LINKING=YES \
75+
FUSE_BUILD_SCRIPT_PHASES=YES \
76+
| xcpretty
77+
78+
- name: Normalize xctestrun path
79+
run: |
80+
XCTESTRUN_SOURCE="$(find "${DERIVED_DATA_PATH}/Build/Products" -name '*.xctestrun' -print -quit)"
81+
if [ -z "${XCTESTRUN_SOURCE}" ]; then
82+
echo "No .xctestrun file found under ${DERIVED_DATA_PATH}/Build/Products" >&2
83+
exit 1
84+
fi
85+
86+
cp "${XCTESTRUN_SOURCE}" "${DERIVED_DATA_PATH}/Build/Products/${XCTESTRUN_FILENAME}"
87+
88+
- name: Archive test products
89+
run: tar -C "${DERIVED_DATA_PATH}/Build" -czf "${BUILD_PRODUCTS_ARCHIVE}" Products
90+
91+
- name: Upload build products artifact
92+
uses: actions/upload-artifact@v7
93+
with:
94+
name: snapshot-build-products
95+
path: ${{ env.BUILD_PRODUCTS_ARCHIVE }}
96+
if-no-files-found: error
97+
98+
# One simulator per runner. Each job extracts the shared build products,
99+
# runs the snapshot tests, and uploads its PNGs as a per-sim artifact so
100+
# the final job can aggregate them.
101+
generate_snapshots:
102+
runs-on: macos-26
103+
needs: build_for_testing
104+
105+
strategy:
106+
fail-fast: false
107+
matrix:
108+
include:
109+
- simulator_name: iPhone 17 Pro Max
110+
slug: iphone-17-pro-max
111+
- simulator_name: iPhone 17e
112+
slug: iphone-17e
113+
- simulator_name: iPad Air 11-inch (M4)
114+
slug: ipad-air-11-inch-m4
115+
116+
env:
117+
TEST_RUNNER_EMERGE_IS_RUNNING_FOR_SNAPSHOTS: 1
118+
# Keep this base path in sync with the workflow-level SNAPSHOT_UPLOAD_BASE_DIR.
119+
TEST_RUNNER_SNAPSHOTS_EXPORT_DIR: ${{ github.workspace }}/ios/snapshot-images/${{ matrix.slug }}
120+
121+
steps:
122+
- name: Checkout
123+
uses: actions/checkout@v6
124+
with:
125+
fetch-depth: 0
126+
127+
- name: Select Xcode 26.4
128+
run: sudo xcode-select -s /Applications/Xcode_26.4.app
129+
130+
- name: Set up Ruby env
131+
uses: ruby/setup-ruby@v1
132+
with:
133+
ruby-version: 3.3.10
134+
bundler-cache: true
135+
136+
- name: Setup gems
137+
run: exec ../.github/scripts/ios/setup.sh
138+
139+
- name: Download build products artifact
140+
uses: actions/download-artifact@v5
141+
with:
142+
name: snapshot-build-products
143+
path: ${{ github.workspace }}
144+
145+
- name: Extract test products
146+
run: |
147+
mkdir -p "${DERIVED_DATA_PATH}/Build"
148+
tar -C "${DERIVED_DATA_PATH}/Build" -xzf "${BUILD_PRODUCTS_ARCHIVE}"
149+
150+
- name: Boot simulator
151+
run: xcrun simctl boot "${{ matrix.simulator_name }}" || true
152+
153+
- name: Prepare snapshot export directory
154+
run: mkdir -p "${TEST_RUNNER_SNAPSHOTS_EXPORT_DIR}"
155+
156+
- name: Generate snapshot images
157+
run: |
158+
set -o pipefail && xcodebuild test-without-building \
159+
-xctestrun "${DERIVED_DATA_PATH}/Build/Products/${XCTESTRUN_FILENAME}" \
160+
-destination 'platform=iOS Simulator,name=${{ matrix.simulator_name }},arch=arm64' \
161+
-only-testing:HackerNewsTests/HackerNewsSnapshotTest \
162+
-resultBundlePath "../SnapshotResults-${{ matrix.slug }}.xcresult" \
163+
| xcpretty
164+
165+
- name: Upload snapshots artifacts
166+
uses: actions/upload-artifact@v7
167+
with:
168+
name: snapshots-${{ matrix.slug }}
169+
path: ${{ env.TEST_RUNNER_SNAPSHOTS_EXPORT_DIR }}
170+
if-no-files-found: error
171+
172+
# Aggregate every per-sim artifact and upload the combined set to Sentry
173+
# in a single fastlane invocation.
174+
upload_snapshots:
175+
runs-on: macos-26
176+
needs: generate_snapshots
177+
178+
steps:
179+
- name: Checkout
180+
uses: actions/checkout@v6
181+
with:
182+
fetch-depth: 0
183+
184+
- name: Set up Ruby env
185+
uses: ruby/setup-ruby@v1
186+
with:
187+
ruby-version: 3.3.10
188+
bundler-cache: true
189+
190+
- name: Setup gems
191+
run: exec ../.github/scripts/ios/setup.sh
192+
193+
- name: Download generated snapshots
194+
uses: actions/download-artifact@v5
195+
with:
196+
path: ${{ env.SNAPSHOT_UPLOAD_BASE_DIR }}
197+
pattern: "snapshots-*"
198+
199+
- name: List aggregated snapshot files
200+
run: |
201+
echo "Generated snapshot files:"
202+
find "${SNAPSHOT_UPLOAD_BASE_DIR}" -type f | sort
203+
echo
204+
echo "Total PNG images: $(find "${SNAPSHOT_UPLOAD_BASE_DIR}" -type f -name '*.png' | wc -l | tr -d ' ')"
205+
206+
- name: Upload snapshots to Sentry
207+
env:
208+
SENTRY_SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_SENTRY_AUTH_TOKEN }}
209+
run: bundle exec fastlane ios upload_sentry_snapshots path:"${SNAPSHOT_UPLOAD_BASE_DIR}"
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Sentry Snapshots Upload (Swift Snapshot Testing)
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
paths: [ios/**, .github/workflows/ios*, .github/scripts/ios/**]
9+
10+
jobs:
11+
upload_sentry_swift_snapshots:
12+
runs-on: macos-26
13+
14+
defaults:
15+
run:
16+
working-directory: ./ios
17+
18+
env:
19+
TEST_RUNNER_XCODE_RUNNING_FOR_PREVIEWS: 1
20+
# Enables recording mode when running Swift Snapshot Testing tests
21+
TEST_RUNNER_SNAPSHOT_TESTING_RECORD: all
22+
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v6
26+
with:
27+
fetch-depth: 0
28+
29+
- name: Set up Ruby env
30+
uses: ruby/setup-ruby@v1
31+
with:
32+
ruby-version: 3.3.10
33+
bundler-cache: true
34+
35+
- name: Setup gems
36+
run: exec ../.github/scripts/ios/setup.sh
37+
38+
- name: Boot iPhone simulator
39+
run: xcrun simctl boot "iPhone 17 Pro Max" || true
40+
41+
- name: Cache Swift Package Manager
42+
uses: actions/cache@v4
43+
with:
44+
path: |
45+
~/Library/Caches/org.swift.swiftpm
46+
~/Library/Developer/Xcode/DerivedData/HackerNews-*/SourcePackages
47+
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
48+
restore-keys: ${{ runner.os }}-spm-
49+
50+
- name: Generate snapshot images
51+
# continue-on-error is needed because Swift Snapshot Testing treats snapshot recording as test failures
52+
continue-on-error: true
53+
run: |
54+
set -o pipefail && xcodebuild test \
55+
-scheme HackerNews \
56+
-sdk iphonesimulator \
57+
-destination 'platform=iOS Simulator,name=iPhone 17 Pro Max,arch=arm64' \
58+
-only-testing:HackerNewsTests/SwiftSnapshotTest \
59+
-resultBundlePath ../SnapshotResults-swift-snapshots.xcresult \
60+
-skipPackagePluginValidation \
61+
ONLY_ACTIVE_ARCH=YES \
62+
TARGETED_DEVICE_FAMILY=1 \
63+
SUPPORTS_MACCATALYST=NO \
64+
CODE_SIGNING_ALLOWED=NO \
65+
COMPILATION_CACHING=YES \
66+
EAGER_LINKING=YES \
67+
FUSE_BUILD_SCRIPT_PHASES=YES \
68+
| xcpretty
69+
70+
- name: List generated images
71+
run: |
72+
echo "Generated snapshot images:"
73+
ls -1 HackerNewsTests/__Snapshots__/SwiftSnapshotTest/ || echo "No snapshots found"
74+
echo "Total: $(ls -1 HackerNewsTests/__Snapshots__/SwiftSnapshotTest/ 2>/dev/null | wc -l | tr -d ' ') images"
75+
76+
- name: Upload snapshots to Sentry
77+
env:
78+
SENTRY_SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_SENTRY_AUTH_TOKEN }}
79+
run: bundle exec fastlane ios upload_sentry_snapshots_swift_snapshot_testing

ios/.ruby-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.2.6

ios/Gemfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ GEM
124124
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
125125
fastlane-plugin-emerge (0.10.8)
126126
faraday (~> 1.1)
127-
fastlane-plugin-sentry (2.5.0)
127+
fastlane-plugin-sentry (2.5.2)
128128
os (~> 1.1, >= 1.1.4)
129129
fastlane-sirp (1.0.0)
130130
sysrandom (~> 1.0)
@@ -243,7 +243,7 @@ PLATFORMS
243243
DEPENDENCIES
244244
fastlane
245245
fastlane-plugin-emerge (= 0.10.8)
246-
fastlane-plugin-sentry (= 2.5.0)
246+
fastlane-plugin-sentry (>= 2.5.1, < 3.0.0)
247247
xcpretty
248248

249249
BUNDLED WITH

ios/HackerNews.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ios/HackerNews/HNApp.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ struct HackerNewsApp: App {
2424
EMGReaper.sharedInstance().start(
2525
withAPIKey: "f77fb081-cfc2-4d15-acb5-18bad59c9376")
2626

27-
if ProcessInfo.processInfo.environment["EMERGE_IS_RUNNING_FOR_SNAPSHOTS"] != "1" {
27+
if !AppRuntime.isRunningForSnapshots {
2828
SentrySDK.start { options in
2929
options.dsn =
3030
"https://118cff4b239bd3e0ede8fd74aad9bf8f@o497846.ingest.sentry.io/4506027753668608"

ios/HackerNewsTests/SwiftSnapshotTest.swift

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,6 @@ import Common
1414

1515
final class SwiftSnapshotTest: XCTestCase {
1616

17-
override func invokeTest() {
18-
// record: .all always records new snapshots and fails every test.
19-
// Change to .missing to only record when no reference image exists,
20-
// or remove this override entirely to compare against existing snapshots.
21-
withSnapshotTesting(record: .all) {
22-
super.invokeTest()
23-
}
24-
}
25-
2617
@MainActor func testPostListScreen() {
2718
@State var appViewModel = AppViewModel(
2819
bookmarkStore: FakeBookmarkDataStore(),
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Foundation
2+
3+
public enum AppRuntime {
4+
public static var isRunningForSnapshots: Bool {
5+
let environment = ProcessInfo.processInfo.environment
6+
return environment["EMERGE_IS_RUNNING_FOR_SNAPSHOTS"] == "1"
7+
|| environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
8+
}
9+
}

0 commit comments

Comments
 (0)