1+ name : Sentry Snapshots Upload
2+
3+ on :
4+ push :
5+ branches : [main, cameroncooke/snapshots-ci]
6+ paths : [ios/**, .github/workflows/ios*]
7+ pull_request :
8+ branches : [main, cameroncooke/snapshots-ci]
9+ paths : [ios/**, .github/workflows/ios*]
10+
11+ defaults :
12+ run :
13+ working-directory : ./ios
14+
15+ env :
16+ DERIVED_DATA_PATH : ${{ github.workspace }}/DerivedData-snapshot-upload
17+ SNAPSHOT_UPLOAD_BASE_DIR : ${{ github.workspace }}/ios/snapshot-images
18+ BUILD_PRODUCTS_ARCHIVE : ${{ github.workspace }}/snapshot-build-products.tar.gz
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+ XCODE_RUNNING_FOR_PREVIEWS : 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 : Archive test products
79+ run : tar -C "${DERIVED_DATA_PATH}/Build" -czf "${BUILD_PRODUCTS_ARCHIVE}" Products
80+
81+ - name : Upload build products artifact
82+ uses : actions/upload-artifact@v7
83+ with :
84+ name : snapshot-build-products
85+ path : ${{ env.BUILD_PRODUCTS_ARCHIVE }}
86+ if-no-files-found : error
87+
88+ # One simulator per runner. Each job extracts the shared build products,
89+ # runs the snapshot tests, and uploads its PNGs as a per-sim artifact so
90+ # the final job can aggregate them.
91+ generate_snapshots :
92+ runs-on : macos-26
93+ needs : build_for_testing
94+
95+ strategy :
96+ fail-fast : false
97+ matrix :
98+ include :
99+ - simulator_name : iPhone 17 Pro Max
100+ slug : iphone-17-pro-max
101+ - simulator_name : iPhone 17e
102+ slug : iphone-17e
103+ - simulator_name : iPad Air 11-inch (M4)
104+ slug : ipad-air-11-inch-m4
105+
106+ env :
107+ XCODE_RUNNING_FOR_PREVIEWS : 1
108+
109+ steps :
110+ - name : Checkout
111+ uses : actions/checkout@v6
112+ with :
113+ fetch-depth : 0
114+
115+ - name : Select Xcode 26.4
116+ run : sudo xcode-select -s /Applications/Xcode_26.4.app
117+
118+ - name : Set up Ruby env
119+ uses : ruby/setup-ruby@v1
120+ with :
121+ ruby-version : 3.3.10
122+ bundler-cache : true
123+
124+ - name : Setup gems
125+ run : exec ../.github/scripts/ios/setup.sh
126+
127+ - name : Download build products artifact
128+ uses : actions/download-artifact@v5
129+ with :
130+ name : snapshot-build-products
131+ path : ${{ github.workspace }}
132+
133+ - name : Extract test products
134+ # Restore the build products back under the same DerivedData path used
135+ # during build-for-testing. The generated .xctestrun file references
136+ # those product locations directly, so test-without-building does not
137+ # need an extra -derivedDataPath flag as long as we recreate the same
138+ # layout on each runner.
139+ run : |
140+ mkdir -p "${DERIVED_DATA_PATH}/Build"
141+ tar -C "${DERIVED_DATA_PATH}/Build" -xzf "${BUILD_PRODUCTS_ARCHIVE}"
142+
143+ - name : Set snapshot export directory
144+ # Xcode strips the TEST_RUNNER_ prefix and forwards the rest into the
145+ # test runner process, which is how the snapshot tests discover where
146+ # to write PNGs.
147+ run : echo "TEST_RUNNER_SNAPSHOTS_EXPORT_DIR=${SNAPSHOT_UPLOAD_BASE_DIR}/${{ matrix.slug }}" >> "$GITHUB_ENV"
148+
149+ - name : Resolve xctestrun file
150+ run : |
151+ XCTESTRUN_PATH="$(find "${DERIVED_DATA_PATH}/Build/Products" -name '*.xctestrun' -print -quit)"
152+ if [ -z "${XCTESTRUN_PATH}" ]; then
153+ echo "No .xctestrun file found under ${DERIVED_DATA_PATH}/Build/Products" >&2
154+ exit 1
155+ fi
156+ echo "XCTESTRUN_PATH=${XCTESTRUN_PATH}" >> "$GITHUB_ENV"
157+
158+ - name : Boot simulator
159+ run : xcrun simctl boot "${{ matrix.simulator_name }}" || true
160+
161+ - name : Prepare snapshot export directory
162+ run : mkdir -p "${TEST_RUNNER_SNAPSHOTS_EXPORT_DIR}"
163+
164+ - name : Generate snapshot images
165+ run : |
166+ set -o pipefail && xcodebuild test-without-building \
167+ -xctestrun "${XCTESTRUN_PATH}" \
168+ -destination 'platform=iOS Simulator,name=${{ matrix.simulator_name }},arch=arm64' \
169+ -only-testing:HackerNewsTests/HackerNewsSnapshotTest \
170+ -resultBundlePath "../SnapshotResults-${{ matrix.slug }}.xcresult" \
171+ | xcpretty
172+
173+ - name : List generated snapshot files
174+ run : |
175+ echo "Generated snapshot files for ${{ matrix.simulator_name }}:"
176+ find "${TEST_RUNNER_SNAPSHOTS_EXPORT_DIR}" -type f | sort
177+ echo
178+ echo "Total PNG images: $(find "${TEST_RUNNER_SNAPSHOTS_EXPORT_DIR}" -type f -name '*.png' | wc -l | tr -d ' ')"
179+
180+ - name : Upload generated snapshots
181+ uses : actions/upload-artifact@v7
182+ with :
183+ name : ${{ matrix.slug }}
184+ path : ${{ env.TEST_RUNNER_SNAPSHOTS_EXPORT_DIR }}
185+ if-no-files-found : error
186+
187+ # Aggregate every per-sim artifact and upload the combined set to Sentry
188+ # in a single fastlane invocation.
189+ upload_snapshots :
190+ runs-on : macos-26
191+ needs : generate_snapshots
192+
193+ steps :
194+ - name : Checkout
195+ uses : actions/checkout@v6
196+ with :
197+ fetch-depth : 0
198+
199+ - name : Set up Ruby env
200+ uses : ruby/setup-ruby@v1
201+ with :
202+ ruby-version : 3.3.10
203+ bundler-cache : true
204+
205+ - name : Setup gems
206+ run : exec ../.github/scripts/ios/setup.sh
207+
208+ - name : Download generated snapshots
209+ uses : actions/download-artifact@v5
210+ with :
211+ path : ${{ env.SNAPSHOT_UPLOAD_BASE_DIR }}
212+ pattern : " *"
213+
214+ - name : List aggregated snapshot files
215+ run : |
216+ echo "Generated snapshot files:"
217+ find "${SNAPSHOT_UPLOAD_BASE_DIR}" -type f | sort
218+ echo
219+ echo "Total PNG images: $(find "${SNAPSHOT_UPLOAD_BASE_DIR}" -type f -name '*.png' | wc -l | tr -d ' ')"
220+
221+ - name : Upload snapshots to Sentry
222+ env :
223+ SENTRY_SENTRY_AUTH_TOKEN : ${{ secrets.SENTRY_SENTRY_AUTH_TOKEN }}
224+ run : bundle exec fastlane ios upload_sentry_snapshots path:"${SNAPSHOT_UPLOAD_BASE_DIR}"
0 commit comments