-
Notifications
You must be signed in to change notification settings - Fork 221
500 lines (463 loc) · 20.3 KB
/
Copy pathrc-smoke.yml
File metadata and controls
500 lines (463 loc) · 20.3 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
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# =============================================================================
# RC Smoke — post-publish validation against the npm RC artifact
# =============================================================================
#
# Stage: RC-SMOKE in the RC pipeline.
#
# Fires automatically after release.yml ("Release" RC path) completes
# with conclusion: success. Synthesizes example_rc_smoke/ from the demo app
# with react-native-appsflyer pinned to the RC version from npm, runs
# SMOKE-001/002/003 via af-scenario-runner.sh on both platforms, and posts a
# check_run named rc-smoke/npm on the release branch head SHA. That check_run
# is what promote-release.yml verifies before stripping -rcN.
#
# Manual dispatch is supported for reruns (rc_version input).
#
# When the parent RC-release run was dry_run=true, this workflow still fires
# but short-circuits and posts a skipped check-run so promotion can
# distinguish "not applicable" from "failing".
# =============================================================================
name: RC Smoke - npm artifact
on:
workflow_run:
workflows: ["Release"]
types: [completed]
workflow_dispatch:
inputs:
rc_version:
description: 'RC version to smoke, e.g. 6.18.0-rc1 (must exist on npm)'
required: true
type: string
release_branch:
description: 'Release branch the smoke result should be associated with'
required: true
type: string
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha || github.event.inputs.release_branch }}
cancel-in-progress: false
jobs:
# ===========================================================================
# Resolve RC version and branch context. Short-circuit on dry-run or failure
# of the parent RC-release run.
# ===========================================================================
resolve:
name: Resolve RC context
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.decide.outputs.should_run }}
skip_reason: ${{ steps.decide.outputs.skip_reason }}
rc_version: ${{ steps.decide.outputs.rc_version }}
release_branch: ${{ steps.decide.outputs.release_branch }}
head_sha: ${{ steps.decide.outputs.head_sha }}
steps:
- name: Log trigger context
run: |
echo "event_name=${{ github.event_name }}"
echo "workflow_run.conclusion=${{ github.event.workflow_run.conclusion }}"
echo "workflow_run.head_branch=${{ github.event.workflow_run.head_branch }}"
echo "workflow_run.head_sha=${{ github.event.workflow_run.head_sha }}"
- name: Checkout release branch (workflow_run path)
if: github.event_name == 'workflow_run'
uses: actions/checkout@v5
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 1
- name: Checkout release branch (manual dispatch path)
if: github.event_name == 'workflow_dispatch'
uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.release_branch }}
fetch-depth: 1
- name: Decide whether to run
id: decide
env:
EVENT_NAME: ${{ github.event_name }}
PARENT_CONCLUSION: ${{ github.event.workflow_run.conclusion }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
INPUT_VERSION: ${{ github.event.inputs.rc_version }}
INPUT_BRANCH: ${{ github.event.inputs.release_branch }}
run: |
set -euo pipefail
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
RC_VERSION="$INPUT_VERSION"
REL_BRANCH="$INPUT_BRANCH"
HEAD="$(git rev-parse HEAD)"
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "skip_reason=" >> "$GITHUB_OUTPUT"
echo "rc_version=$RC_VERSION" >> "$GITHUB_OUTPUT"
echo "release_branch=$REL_BRANCH" >> "$GITHUB_OUTPUT"
echo "head_sha=$HEAD" >> "$GITHUB_OUTPUT"
exit 0
fi
# workflow_run path
if [[ "$PARENT_CONCLUSION" != "success" ]]; then
echo "Parent RC-release run was $PARENT_CONCLUSION; skipping smoke."
echo "should_run=false" >> "$GITHUB_OUTPUT"
echo "skip_reason=parent_not_success" >> "$GITHUB_OUTPUT"
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
echo "release_branch=$HEAD_BRANCH" >> "$GITHUB_OUTPUT"
echo "rc_version=" >> "$GITHUB_OUTPUT"
exit 0
fi
# workflow_run listeners cannot read the parent's inputs directly,
# so we infer state from the release branch checkout: package.json
# carries the bumped RC version (committed by prepare-branch on both
# real and dry runs), and the npm API tells us if it was actually
# published. Dry runs naturally fail the visibility poll below and
# emit a skipped check-run.
VERSION=$(node -p "require('./package.json').version")
if [[ ! "$VERSION" =~ -rc[0-9]+$ ]]; then
echo "package.json version is not an RC ($VERSION); skipping."
echo "should_run=false" >> "$GITHUB_OUTPUT"
echo "skip_reason=not_rc_version" >> "$GITHUB_OUTPUT"
echo "rc_version=$VERSION" >> "$GITHUB_OUTPUT"
echo "release_branch=$HEAD_BRANCH" >> "$GITHUB_OUTPUT"
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
exit 0
fi
# npm registry indexing can lag a few minutes after a successful
# publish. Poll until the RC version appears, with a hard timeout.
# Only emit a skipped check if it never shows up (genuine dry run /
# publish failure that the success-conclusion check missed).
MAX_WAIT_SECONDS=900
POLL_INTERVAL_SECONDS=30
ELAPSED=0
FOUND=false
while (( ELAPSED < MAX_WAIT_SECONDS )); do
if npm view "react-native-appsflyer@$VERSION" version 2>/dev/null | grep -Fxq "$VERSION"; then
echo "RC $VERSION visible on npm after ${ELAPSED}s."
FOUND=true
break
fi
echo "RC $VERSION not yet on npm (waited ${ELAPSED}s, retrying in ${POLL_INTERVAL_SECONDS}s)"
sleep "$POLL_INTERVAL_SECONDS"
ELAPSED=$(( ELAPSED + POLL_INTERVAL_SECONDS ))
done
if [[ "$FOUND" != "true" ]]; then
echo "RC $VERSION never appeared on npm within ${MAX_WAIT_SECONDS}s; emitting skipped check."
echo "should_run=false" >> "$GITHUB_OUTPUT"
echo "skip_reason=not_on_npm" >> "$GITHUB_OUTPUT"
echo "rc_version=$VERSION" >> "$GITHUB_OUTPUT"
echo "release_branch=$HEAD_BRANCH" >> "$GITHUB_OUTPUT"
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "should_run=true" >> "$GITHUB_OUTPUT"
echo "skip_reason=" >> "$GITHUB_OUTPUT"
echo "rc_version=$VERSION" >> "$GITHUB_OUTPUT"
echo "release_branch=$HEAD_BRANCH" >> "$GITHUB_OUTPUT"
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
# ===========================================================================
# iOS smoke against the published RC on npm.
# ===========================================================================
smoke-ios:
name: rc-smoke iOS
needs: resolve
if: needs.resolve.outputs.should_run == 'true'
runs-on: macos-15
steps:
- name: Checkout release branch
uses: actions/checkout@v5
with:
ref: ${{ needs.resolve.outputs.head_sha }}
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Synthesize example_rc_smoke from demo app
env:
RC_VERSION: ${{ needs.resolve.outputs.rc_version }}
run: |
set -euo pipefail
rsync -a \
--exclude=node_modules \
--exclude=.env \
--exclude=build \
--exclude=ios/Pods \
--exclude=ios/build \
--exclude=android/.gradle \
--exclude=android/build \
--exclude='.metro-*' \
example/ example_rc_smoke/
cd example_rc_smoke
npm pkg set "dependencies.react-native-appsflyer=$RC_VERSION"
echo "Pinned react-native-appsflyer to $RC_VERSION"
grep "react-native-appsflyer" package.json
- name: Write .env
env:
ENV_FILE: ${{ secrets.ENV_FILE }}
run: echo "$ENV_FILE" | base64 -d > example_rc_smoke/.env
- name: Select Xcode version
run: |
XCODE=$(ls /Applications | grep -E "^Xcode_[0-9]" | sort -V | tail -1)
sudo xcode-select -s "/Applications/$XCODE"
xcodebuild -version
- name: Boot iOS simulator
run: |
DEVICES_JSON=$(xcrun simctl list -j devices available)
UDID=$(echo "$DEVICES_JSON" | jq -r '
.devices
| to_entries
| map(select(.key | test("iOS-1[78]")))
| sort_by(.key) | reverse
| map(
. as $rt
| $rt.value
| map(select(.name | test("^iPhone 1[5-7]$")))
| first
| (if . then {udid} else empty end)
)
| first // empty
| .udid // empty
')
if [[ -z "$UDID" ]]; then
echo "No plain iPhone 15/16/17 on iOS 17/18 found; falling back to first available iPhone."
UDID=$(echo "$DEVICES_JSON" | jq -r '.devices[][] | select(.name | test("^iPhone")) | .udid' | head -1)
fi
[[ -z "$UDID" ]] && { echo "::error::No iOS simulator available"; exit 1; }
xcrun simctl boot "$UDID"
xcrun simctl bootstatus "$UDID" -b
echo "IOS_SIMULATOR_UDID=$UDID" >> "$GITHUB_ENV"
- name: Install npm RC and CocoaPods
working-directory: example_rc_smoke
run: |
set -euo pipefail
# npm CDN propagation can lag; retry with backoff.
ATTEMPTS=5
SLEEP=30
for i in $(seq 1 "$ATTEMPTS"); do
echo "npm install attempt $i/$ATTEMPTS"
if npm install; then
echo "npm install succeeded on attempt $i"
break
fi
if [[ "$i" == "$ATTEMPTS" ]]; then
echo "npm install failed after $ATTEMPTS attempts" >&2
exit 1
fi
npm cache clean --force || true
sleep "$SLEEP"
done
cd ios && pod install
- name: Build iOS (debug)
working-directory: example_rc_smoke/ios
env:
FORCE_BUNDLING: "1"
run: |
xcodebuild build \
-workspace example.xcworkspace \
-scheme example \
-configuration Debug \
-destination "platform=iOS Simulator,id=$IOS_SIMULATOR_UDID" \
-derivedDataPath build \
| xcpretty
- name: Run smoke (SMOKE-001/002/003)
run: ./scripts/af-scenario-runner.sh --platform ios --plan .af-smoke/rc-test-plan.json
- name: Upload smoke reports
if: always()
uses: actions/upload-artifact@v5
with:
name: rc-smoke-ios-${{ github.run_number }}
path: .af-smoke/reports/
retention-days: 30
# ===========================================================================
# Android smoke against the published RC on npm.
# ===========================================================================
smoke-android:
name: rc-smoke Android
needs: resolve
if: needs.resolve.outputs.should_run == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout release branch
uses: actions/checkout@v5
with:
ref: ${{ needs.resolve.outputs.head_sha }}
fetch-depth: 1
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
ls -l /dev/kvm
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Synthesize example_rc_smoke from demo app
env:
RC_VERSION: ${{ needs.resolve.outputs.rc_version }}
run: |
set -euo pipefail
rsync -a \
--exclude=node_modules \
--exclude=.env \
--exclude=build \
--exclude=ios/Pods \
--exclude=ios/build \
--exclude=android/.gradle \
--exclude=android/build \
--exclude='.metro-*' \
example/ example_rc_smoke/
cd example_rc_smoke
npm pkg set "dependencies.react-native-appsflyer=$RC_VERSION"
echo "Pinned react-native-appsflyer to $RC_VERSION"
grep "react-native-appsflyer" package.json
- name: Write .env
env:
ENV_FILE: ${{ secrets.ENV_FILE }}
run: echo "$ENV_FILE" | base64 -d > example_rc_smoke/.env
- name: Install npm RC
working-directory: example_rc_smoke
run: |
set -euo pipefail
ATTEMPTS=5
SLEEP=30
for i in $(seq 1 "$ATTEMPTS"); do
echo "npm install attempt $i/$ATTEMPTS"
if npm install; then
echo "npm install succeeded on attempt $i"
break
fi
if [[ "$i" == "$ATTEMPTS" ]]; then
echo "npm install failed after $ATTEMPTS attempts" >&2
exit 1
fi
npm cache clean --force || true
sleep "$SLEEP"
done
- name: Build Android APK and run smoke on emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
arch: x86_64
profile: pixel_6
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -dns-server 8.8.8.8,1.1.1.1
disable-animations: true
script: |
set -e
adb shell 'getprop net.dns1; getprop net.dns2' || true
adb shell 'nslookup oyoxfj.conversions.appsflyersdk.com 2>&1 | head -5' || { sleep 10; adb shell 'nslookup oyoxfj.conversions.appsflyersdk.com 2>&1 | head -5'; } || { echo "::error::Emulator DNS cannot resolve AppsFlyer hosts"; exit 1; }
cd example_rc_smoke/android && cp ../../example/android/gradlew . && ./gradlew assembleDebug && cd ../..
bash ./scripts/af-scenario-runner.sh --platform android --plan .af-smoke/rc-test-plan.json
- name: Upload smoke reports
if: always()
uses: actions/upload-artifact@v5
with:
name: rc-smoke-android-${{ github.run_number }}
path: .af-smoke/reports/
retention-days: 30
# ===========================================================================
# Post the rc-smoke/npm check-run on the release branch head SHA. This is
# the gate promote-release.yml verifies before stripping -rcN.
# ===========================================================================
post-check-run:
name: Post rc-smoke/npm check-run
needs: [resolve, smoke-ios, smoke-android]
if: always() && needs.resolve.outputs.should_run == 'true' && needs.resolve.outputs.head_sha != ''
runs-on: ubuntu-latest
permissions:
checks: write
steps:
- name: Post check-run
uses: actions/github-script@v7
env:
HEAD_SHA: ${{ needs.resolve.outputs.head_sha }}
RC_VERSION: ${{ needs.resolve.outputs.rc_version }}
IOS_RESULT: ${{ needs.smoke-ios.result }}
ANDROID_RESULT: ${{ needs.smoke-android.result }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
with:
script: |
const { HEAD_SHA, RC_VERSION, IOS_RESULT, ANDROID_RESULT, RUN_URL } = process.env;
const bothGreen = IOS_RESULT === 'success' && ANDROID_RESULT === 'success';
const conclusion = bothGreen ? 'success' : 'failure';
const summary = `rc-smoke on npm RC \`${RC_VERSION}\`\n\n` +
`- iOS: **${IOS_RESULT}**\n` +
`- Android: **${ANDROID_RESULT}**\n\n` +
`Reports: ${RUN_URL}`;
await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'rc-smoke/npm',
head_sha: HEAD_SHA,
status: 'completed',
conclusion,
details_url: RUN_URL,
output: {
title: bothGreen ? 'rc-smoke PASS' : 'rc-smoke FAIL',
summary
}
});
# ===========================================================================
# Notify Slack on a real smoke failure.
# ===========================================================================
notify-failure:
name: Notify Smoke Failure
needs: [resolve, smoke-ios, smoke-android]
if: >-
always() &&
needs.resolve.outputs.should_run == 'true' &&
(needs.smoke-ios.result == 'failure' || needs.smoke-android.result == 'failure' ||
needs.smoke-ios.result == 'cancelled' || needs.smoke-android.result == 'cancelled')
runs-on: ubuntu-latest
steps:
- name: Send Slack failure notification
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "<!here>\n:warning: *React Native rc-smoke failed for `${{ needs.resolve.outputs.rc_version }}`*\n\nThe RC is published on npm but smoke checks did not pass on the artifact. Production promotion is blocked.\n\n*Smoke results:*\n- iOS: ${{ needs.smoke-ios.result }}\n- Android: ${{ needs.smoke-android.result }}\n\n*Branch:* ${{ needs.resolve.outputs.release_branch }}\n*Run:* https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n\n*Next step:* Bump to `rcN+1` and rerun the RC-release workflow with `dry_run=false`. Republishing the same RC version is not supported by npm."
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_HOOK }}
# ===========================================================================
# Post a skipped check-run when the parent run was dry, the version is not
# on npm, or the parent did not succeed.
# ===========================================================================
post-skipped-check:
name: Post rc-smoke/npm (skipped)
needs: [resolve]
if: needs.resolve.outputs.should_run == 'false' && needs.resolve.outputs.head_sha != ''
runs-on: ubuntu-latest
steps:
- name: Post skipped check-run
uses: actions/github-script@v7
env:
HEAD_SHA: ${{ needs.resolve.outputs.head_sha }}
SKIP_REASON: ${{ needs.resolve.outputs.skip_reason }}
RC_VERSION: ${{ needs.resolve.outputs.rc_version }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
with:
script: |
const { HEAD_SHA, SKIP_REASON, RC_VERSION, RUN_URL } = process.env;
const reasonLabels = {
parent_not_success: 'Parent RC-release run did not succeed',
not_rc_version: 'Head branch version is not an RC',
not_on_npm: 'RC not yet on npm (dry run or indexing delay)'
};
await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'rc-smoke/npm',
head_sha: HEAD_SHA,
status: 'completed',
conclusion: 'skipped',
details_url: RUN_URL,
output: {
title: 'rc-smoke skipped',
summary: `Skipped: ${reasonLabels[SKIP_REASON] || SKIP_REASON}\n\n` +
`Version: \`${RC_VERSION || '(unknown)'}\`\n\n` +
`Run: ${RUN_URL}\n\n` +
`promote-release.yml does not accept \`skipped\` as a green gate; if this was a dry run, start a new RC-release with \`dry_run=false\` to publish and re-smoke.`
}
});