Skip to content

RC Smoke - npm artifact #18

RC Smoke - npm artifact

RC Smoke - npm artifact #18

Workflow file for this run

# =============================================================================
# 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.`
}
});