-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
362 lines (321 loc) · 15.6 KB
/
run-e2e-workflow.yml
File metadata and controls
362 lines (321 loc) · 15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# This workflow runs mobile E2E tests for a specific test category.
# It passes matrix sharding info to the test framework via environment variables.
name: Run E2E
on:
workflow_call:
inputs:
test-suite-name:
description: 'Name of the test suite'
required: true
type: string
platform:
description: 'Platform to test (ios or android)'
required: true
type: string
test_suite_tag:
description: 'The tests tag expression to use for filtering tests'
required: true
type: string
split_number:
description: 'Which split number to run (1-based index)'
required: false
type: number
default: 1
total_splits:
description: 'Total number of splits to divide tests into'
required: false
type: number
default: 1
test-timeout-minutes:
description: 'The timeout in minutes for the test command'
required: false
type: number
default: 30
build_type:
description: 'The type of build to perform'
required: false
type: string
default: 'main'
metamask_environment:
description: 'The environment to build for'
required: false
type: string
default: 'e2e'
changed_files:
description: 'Changed files'
required: false
type: string
default: ''
artifact_name_prefix:
description: 'Optional prefix added to uploaded test artifacts'
required: false
type: string
default: 'main-'
use_bitrise_runner:
description: 'Route iOS E2E jobs to Bitrise self-hosted runner group.'
required: false
type: boolean
default: false
jobs:
test-e2e-mobile:
name: ${{ inputs.test-suite-name }}
runs-on: ${{ inputs.platform == 'ios' && (inputs.use_bitrise_runner && fromJSON('{"group":"temp-bitrise-runners","labels":["bitrise_pool_name:DemoFAL"]}') || 'ghcr.io/cirruslabs/macos-runner:tahoe') || 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-lg' }}
outputs:
apk-target-path: ${{ steps.determine-target-paths.outputs.apk-target-path }}
test-apk-target-path: ${{ steps.determine-target-paths.outputs.test-apk-target-path }}
env:
PREBUILT_IOS_APP_PATH: artifacts/${{ inputs.build_type }}-${{ inputs.metamask_environment }}-MetaMask.app
METAMASK_ENVIRONMENT: ${{ inputs.metamask_environment }}
METAMASK_BUILD_TYPE: ${{ inputs.build_type }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PLATFORM: ${{ inputs.platform }}
TEST_SUITE_TAG: ${{ inputs.test_suite_tag }}
CHANGED_FILES: ${{ inputs.changed_files }}
GITHUB_CI: 'true'
SPLIT_NUMBER: ${{ inputs.split_number }}
TOTAL_SPLITS: ${{ inputs.total_splits }}
RAMP_INTERNAL_BUILD: 'true'
BRIDGE_USE_DEV_APIS: 'true'
MM_SECURITY_ALERTS_API_ENABLED: 'true'
MM_NOTIFICATIONS_UI_ENABLED: 'true'
FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN: ${{ secrets.FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN }}
FEATURES_ANNOUNCEMENTS_SPACE_ID: ${{ secrets.FEATURES_ANNOUNCEMENTS_SPACE_ID }}
MM_TEST_WALLET_SRP: ${{ secrets.MM_TEST_WALLET_SRP }}
SEEDLESS_ONBOARDING_ENABLED: 'true'
SEGMENT_WRITE_KEY_QA: ${{ secrets.SEGMENT_WRITE_KEY_QA }}
SEGMENT_PROXY_URL_QA: ${{ secrets.SEGMENT_PROXY_URL_QA }}
SEGMENT_DELETE_API_SOURCE_ID_QA: ${{ secrets.SEGMENT_DELETE_API_SOURCE_ID_QA }}
MAIN_IOS_GOOGLE_CLIENT_ID_UAT: ${{ secrets.MAIN_IOS_GOOGLE_CLIENT_ID_UAT }}
MAIN_IOS_GOOGLE_REDIRECT_URI_UAT: ${{ secrets.MAIN_IOS_GOOGLE_REDIRECT_URI_UAT }}
MAIN_ANDROID_APPLE_CLIENT_ID_UAT: ${{ secrets.MAIN_ANDROID_APPLE_CLIENT_ID_UAT }}
MAIN_ANDROID_GOOGLE_CLIENT_ID_UAT: ${{ secrets.MAIN_ANDROID_GOOGLE_CLIENT_ID_UAT }}
MAIN_ANDROID_GOOGLE_SERVER_CLIENT_ID_UAT: ${{ secrets.MAIN_ANDROID_GOOGLE_SERVER_CLIENT_ID_UAT }}
SEGMENT_REGULATIONS_ENDPOINT_QA: ${{ secrets.SEGMENT_REGULATIONS_ENDPOINT_QA }}
MM_SENTRY_DSN_TEST: ${{ secrets.MM_SENTRY_DSN_TEST }}
MM_SENTRY_AUTH_TOKEN: ${{ secrets.MM_SENTRY_AUTH_TOKEN }}
GOOGLE_SERVICES_B64_IOS: ${{ secrets.GOOGLE_SERVICES_B64_IOS }}
GOOGLE_SERVICES_B64_ANDROID: ${{ secrets.GOOGLE_SERVICES_B64_ANDROID }}
MM_SOLANA_E2E_TEST_SRP: ${{ secrets.MM_SOLANA_E2E_TEST_SRP }}
MM_INFURA_PROJECT_ID: ${{ secrets.MM_INFURA_PROJECT_ID }}
YARN_ENABLE_GLOBAL_CACHE: 'true' # Enable Yarn global cache for faster installs
steps:
- name: Checkout
uses: actions/checkout@v6
# TEMPORARY: Bitrise runners use Vagrant (user=vagrant, HOME=/Users/vagrant).
# GitHub Actions tools hardcode /Users/runner paths, so create the symlink.
- name: Fix Vagrant environment paths (Bitrise runners)
if: ${{ inputs.use_bitrise_runner && inputs.platform == 'ios' }}
run: |
if [ -L /Users/runner ]; then
current_target="$(readlink /Users/runner)"
if [ "$current_target" = "/Users/vagrant" ]; then
echo "Symlink already correct: /Users/runner -> /Users/vagrant"
else
echo "Replacing incorrect symlink /Users/runner -> $current_target"
sudo rm /Users/runner
sudo ln -s /Users/vagrant /Users/runner
echo "Recreated symlink: /Users/runner -> /Users/vagrant"
fi
elif [ -e /Users/runner ]; then
echo "Error: /Users/runner exists but is not a symlink"
ls -ld /Users/runner
exit 1
else
sudo ln -s /Users/vagrant /Users/runner
echo "Created symlink /Users/runner -> /Users/vagrant"
fi
mkdir -p "$HOME/hostedtoolcache" "$HOME/tmp"
echo "RUNNER_TOOL_CACHE=$HOME/hostedtoolcache" >> "$GITHUB_ENV"
echo "RUNNER_TEMP=$HOME/tmp" >> "$GITHUB_ENV"
shell: bash
- name: Restore .metamask folder
uses: actions/cache@v4
with:
path: .metamask
key: .metamask-${{ hashFiles('package.json', 'yarn.lock') }}
- name: Set up E2E environment
timeout-minutes: 25
uses: ./.github/actions/setup-e2e-env
with:
platform: ${{ inputs.platform }}
setup-simulator: true
android-avd-name: emulator
android-api-level: 36
configure-keystores: false
- name: Build Detox framework cache (iOS)
if: ${{ inputs.platform == 'ios' }}
run: |
echo "Building Detox framework cache for iOS..."
yarn detox clean-framework-cache
yarn detox build-framework-cache
- name: Determine android target paths and Artifact Names
id: determine-target-paths
if: ${{ inputs.platform == 'android' }}
run: |
if [[ "${{ inputs.build_type }}" == "flask" ]]; then
{
echo "apk-target-path=android/app/build/outputs/apk/flask/release"
echo "test-apk-target-path=android/app/build/outputs/apk/androidTest/flask/release"
echo "artifact_name=app-flask-release"
} >> "$GITHUB_OUTPUT"
elif [[ "${{ inputs.build_type }}" == "main" ]]; then
{
echo "apk-target-path=android/app/build/outputs/apk/prod/release"
echo "test-apk-target-path=android/app/build/outputs/apk/androidTest/prod/release"
echo "artifact_name=app-prod-release"
} >> "$GITHUB_OUTPUT"
else
echo "❌ Error: build_type ${{ inputs.build_type }} is not valid"
exit 1
fi
- name: Setup Android artifacts from build job
if: ${{ inputs.platform == 'android' }}
run: |
echo "🏗 Setting up Android artifacts from build job..."
mkdir -p ${{ steps.determine-target-paths.outputs.apk-target-path }}
mkdir -p ${{ steps.determine-target-paths.outputs.test-apk-target-path }}
- name: Download Android build artifacts
if: ${{ inputs.platform == 'android' }}
uses: actions/download-artifact@v4
with:
path: artifacts/
- name: Move Android artifacts to expected locations
if: ${{ inputs.platform == 'android' }}
run: |
# Copy from artifacts to expected locations
# Each artifact is uploaded as a folder so we need to cp from inside the folder
cp artifacts/${{ inputs.build_type }}-${{ inputs.metamask_environment }}-release.apk/${{ steps.determine-target-paths.outputs.artifact_name }}.apk ${{ steps.determine-target-paths.outputs.apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.apk
cp artifacts/${{ inputs.build_type }}-${{ inputs.metamask_environment }}-release-androidTest.apk/${{ steps.determine-target-paths.outputs.artifact_name }}-androidTest.apk ${{ steps.determine-target-paths.outputs.test-apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}-androidTest.apk
echo "✅ Android artifacts ready for E2E tests"
- name: Setup iOS artifacts from build job
if: ${{ inputs.platform == 'ios' }}
run: |
echo "🏗 Setting up iOS artifacts from build job..."
# Create required directories
mkdir -p ios/build/Build/Products/Release-iphonesimulator/
- name: Download iOS build artifacts
if: ${{ inputs.platform == 'ios' }}
uses: actions/download-artifact@v4
with:
path: artifacts/
# actions/upload-artifact strips execute permissions from the ZIP.
# Also fixes any residual case mismatch as a safety net.
- name: Restore iOS bundle executable permissions
if: ${{ inputs.platform == 'ios' }}
env:
BUILD_TYPE: ${{ inputs.build_type }}
METAMASK_ENV: ${{ inputs.metamask_environment }}
run: |
APP_PATH="artifacts/${BUILD_TYPE}-${METAMASK_ENV}-MetaMask.app"
BUNDLE_EXEC=$(/usr/libexec/PlistBuddy -c "Print CFBundleExecutable" "$APP_PATH/Info.plist" 2>/dev/null)
if [ -z "$BUNDLE_EXEC" ]; then
echo "Could not read CFBundleExecutable from Info.plist"
exit 1
fi
ACTUAL_PATH=$(find "$APP_PATH" -maxdepth 1 -iname "$BUNDLE_EXEC" -type f | head -1)
if [ -z "$ACTUAL_PATH" ]; then
echo "Bundle executable not found: $BUNDLE_EXEC"
exit 1
fi
# Two-step rename to fix case on case-insensitive APFS (direct rename is a no-op)
if [ "$(basename "$ACTUAL_PATH")" != "$BUNDLE_EXEC" ]; then
mv "$ACTUAL_PATH" "$APP_PATH/${BUNDLE_EXEC}_fix"
mv "$APP_PATH/${BUNDLE_EXEC}_fix" "$APP_PATH/$BUNDLE_EXEC"
fi
chmod +x "$APP_PATH/$BUNDLE_EXEC"
shell: bash
# On re-run (run_attempt > 1), download previous test results to identify failed tests
- name: Download previous test results (on re-run)
if: ${{ github.run_attempt > 1 }}
id: download-previous-results
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: test-e2e-${{ inputs.artifact_name_prefix }}${{ inputs.test-suite-name }}-junit-results
path: ./previous-test-results/
- name: Debug - List downloaded test results (on re-run)
if: ${{ github.run_attempt > 1 && steps.download-previous-results.outcome == 'success' }}
run: |
echo "📂 Contents of ./previous-test-results/:"
find ./previous-test-results -type f -name "*.xml" | head -20
echo ""
echo "📄 First XML file preview (first 50 lines):"
find ./previous-test-results -type f -name "*.xml" | head -1 | xargs head -50 || echo "No XML files found"
- name: 🧪 Run E2E tests
timeout-minutes: ${{ inputs.test-timeout-minutes }}
run: |
echo "Running ${{ inputs.test-suite-name }} tests on ${{ inputs.platform }}"
echo "Running split ${{ inputs.split_number }} of ${{ inputs.total_splits }}"
echo "Using TEST_SUITE_TAG: ${{ inputs.test_suite_tag }}"
echo "Run attempt: ${{ github.run_attempt }}"
# Always use the splitting script (handles both split and non-split cases)
node .github/scripts/e2e-split-tags-shards.mjs
env:
JOB_NAME: ${{ inputs.test-suite-name }}
RUN_ID: ${{ github.run_id }}
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
RUN_ATTEMPT: ${{ github.run_attempt }}
# On re-run, pass the path to previous results to run only failed tests
PREVIOUS_RESULTS_PATH: ${{ steps.download-previous-results.outcome == 'success' && './previous-test-results' || '' }}
- name: Merge Detox JUnit reports
if: always()
continue-on-error: true
run: |
echo "📊 Merging Detox JUnit XML reports..."
# Install xml2js for XML parsing (lightweight, no full yarn install needed)
mkdir -p temp-deps && cd temp-deps
npm init -y > /dev/null 2>&1
npm install xml2js@0.6.2 --no-audit --no-fund --silent
# Copy node_modules to workspace for script usage
cp -r node_modules ${{ github.workspace }}/
cd ${{ github.workspace }}
# Run merge script
node .github/scripts/e2e-merge-detox-junit-reports.mjs
# Clean up temporary node_modules
rm -rf node_modules temp-deps
# Merge previous test results with current results to preserve pass/fail history across re-runs.
# Without this, tests that passed in attempt N but were skipped in attempt N+1 would have no
# results in the artifact, causing them to be re-run in attempt N+2.
- name: Merge previous test results (on re-run)
id: merge-previous-results
if: ${{ !cancelled() && github.run_attempt > 1 && steps.download-previous-results.outcome == 'success' }}
continue-on-error: true
run: |
echo "📊 Merging previous test results to preserve history..."
# Install xml2js for XML parsing (lightweight, no full yarn install needed)
mkdir -p temp-deps && cd temp-deps
npm init -y > /dev/null 2>&1
npm install xml2js@0.6.2 --no-audit --no-fund --silent
# Copy node_modules to workspace for script usage
cp -r node_modules ${{ github.workspace }}/
cd ${{ github.workspace }}
# Run merge script to combine previous results with current
node .github/scripts/e2e-merge-test-results.mjs ./previous-test-results ./tests/reports
# Clean up temporary node_modules
rm -rf node_modules temp-deps
- name: Rebuild merged JUnit after previous-results merge (on re-run)
if: ${{ !cancelled() && steps.merge-previous-results.outcome == 'success' }}
continue-on-error: true
run: |
echo "📊 Rebuilding merged junit.xml after previous-results merge..."
mkdir -p temp-deps && cd temp-deps
npm init -y > /dev/null 2>&1
npm install xml2js@0.6.2 --no-audit --no-fund --silent
cp -r node_modules ${{ github.workspace }}/
cd ${{ github.workspace }}
node .github/scripts/e2e-merge-detox-junit-reports.mjs
rm -rf node_modules temp-deps
- name: Upload JUnit XML results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-e2e-${{ inputs.artifact_name_prefix }}${{ inputs.test-suite-name }}-junit-results
path: tests/reports/
retention-days: 7
- name: Upload Screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-e2e-${{ inputs.artifact_name_prefix }}${{ inputs.test-suite-name }}-screenshots
path: tests/artifacts/
retention-days: 7