Files
openobserve/.github/workflows/mobile-sdk-e2e.yml
T

861 lines
45 KiB
YAML

# Mobile RUM SDK — End-to-End Tests (self-contained / no secrets)
# =============================================================================================
# Runs tests/mobile-testing (Maestro drives 4 fixture apps → API asserts _rumdata → Playwright
# asserts the RUM dashboard) across RN-Android, Android-native, RN-iOS and iOS-native.
#
# MODEL B — mirrors playwright.yml: each device job stands up its OWN OpenObserve on the runner and
# points the app at it. No shared cluster, no secrets, and every run is fully isolated (so the
# Android and iOS jobs run in PARALLEL — each has its own instance).
# · RUM ingestion is OSS, so a plain OpenObserve accepts the data.
# · The apps read their RUM target from a build-time-overridable config (committed default =
# placeholder; CI regenerates it): inline @gen consts in App.tsx (RN, via gen-config.js),
# BuildConfig (android-native), GeneratedConfig.swift (ios-native). See docs/CI-NOTES.md.
# · Emulator networking: Android reaches the host at 10.0.2.2; the iOS simulator shares the host,
# so it uses localhost. The suite's API/UI checks (on the host) always use localhost.
#
# WHY SEPARATE (not in playwright.yml): it needs Android emulators AND macOS iOS simulators and
# builds 4 phone apps — too heavy to put on every PR.
#
# TRIGGERS (the `decide` job turns these into run_android / run_ios — see it for the exact matrix)
# - repository_dispatch (mobile-sdk-release): the real "SDK changed" signal (SDK lives in other repos;
# each dispatches here on release with client_payload.sdk → runs that SDK's platform). See docs/CI-NOTES.md.
# - pull_request touching mobile-RUM paths: Android device jobs auto-run when Android-specific or SHARED
# files change; iOS device jobs run only with the `mobile-e2e` label (macOS runners are scarce).
# - `mobile-e2e` label on a PR → both platforms (full pre-merge gate).
# - workflow_dispatch → manual; run_full_suite=true + platforms input to pick android/ios/both.
#
# STATUS: green end-to-end on GitHub runners (Android + iOS device jobs + merge). See docs/CI-NOTES.md.
# =============================================================================================
name: Mobile RUM SDK E2E
on:
repository_dispatch:
types: [mobile-sdk-release]
workflow_dispatch:
inputs:
run_full_suite:
description: "Build apps + drive emulator/simulator (OFF = preflight only)"
type: boolean
default: false
platforms:
description: "Which device jobs to run when full suite is on"
type: choice
options: [both, android, ios]
default: both
sdk:
description: "Override one SDK to test its latest version (else the committed pin is used)"
type: choice
options: [none, react-native, android, ios]
default: none
sdk_version:
description: "SDK version to test (used with 'sdk' above)"
type: string
default: ""
pull_request:
# `labeled` so adding the 'mobile-e2e' label re-fires the run and turns the device jobs on
# (lets you validate the full Android + iOS suite on a PR before merging).
types: [opened, reopened, synchronize, labeled, ready_for_review, converted_to_draft]
paths:
- "tests/mobile-testing/**"
- ".github/workflows/mobile-sdk-e2e.yml"
# Mobile-RUM product code: re-run the suite whenever the RUM pipeline, dashboard,
# or the in-app SDK install docs (version pins) change.
- "web/src/views/RUM/**"
- "web/src/composables/rum/**"
- "web/src/components/ingestion/setupCard/content/rumReactNative.ts"
- "web/src/components/ingestion/setupCard/content/rumAndroid.ts"
- "web/src/components/ingestion/setupCard/content/rumIOS.ts"
- "src/**/rum/**"
# schedule:
# - cron: "0 2 * * 1" # Mondays 02:00 UTC — weekly safety net
concurrency:
# PR pushes supersede each other (share the PR-number group); every non-PR run (each SDK-release
# dispatch / manual run) gets its OWN group via run_id, so a second SDK release can't cancel the
# first's in-flight device gate.
group: mobile-sdk-e2e-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
actions: read
env:
# Non-secret defaults for a fresh local OpenObserve (identical to the other test workflows).
ZO_ROOT_USER_EMAIL: root@example.com
ZO_ROOT_USER_PASSWORD: Complexpass#123
# RUM-ingestion parity with the proven playwright.yml suite. Without these a fresh
# OpenObserve can silently reject/misfile RUM ingest (loopback SSRF guard, missing org,
# dynamic schema, past-timestamp window). The iOS binary inherits these from the job env;
# the Android Docker container gets them via explicit -e flags below.
ZO_SSRF_ALLOW_LOOPBACK: "true"
ZO_CREATE_ORG_THROUGH_INGESTION: "true"
ZO_ALLOW_USER_DEFINED_SCHEMAS: "true"
ZO_INGEST_ALLOWED_UPTO: "5"
OO_USER: root@example.com
OO_PASS: "Complexpass#123"
OO_ORG: default
# Query-side env expectation must match the app's baked O2_RUM_ENV (device jobs set "testing").
RN_ENV: testing
# Placeholder so config.js (which requires OO_URL) loads during preflight `--list`; the device
# jobs override this via $GITHUB_ENV with their own on-runner instance.
OO_URL: http://localhost:5080
jobs:
# -------------------------------------------------------------------------------------------
# PREFLIGHT — always runs, no devices. Validates the suite compiles and prints a readiness note.
# Self-contained model → no secrets to check.
# -------------------------------------------------------------------------------------------
preflight:
# Paid CI runs only on PRs carrying the 'ready-for-ci' label; the merge queue runs unit tests on the merged code.
if: ${{ github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'ready-for-ci') && github.event.pull_request.draft == false) }}
name: Preflight & readiness
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
suite_ok: ${{ steps.validate.outputs.suite_ok }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20" }
- name: Install suite dependencies
working-directory: tests/mobile-testing
run: npm ci || npm install
- name: Validate the suite parses (playwright --list)
id: validate
working-directory: tests/mobile-testing
run: |
set +e
LIST=$(npx playwright test --list 2>&1); CODE=$?
echo "$LIST" | tail -3
TOTAL=$(echo "$LIST" | grep -Eo 'Total: [0-9]+ tests' | grep -Eo '[0-9]+' | head -1)
[ $CODE -eq 0 ] && echo "suite_ok=true" >> "$GITHUB_OUTPUT" || echo "suite_ok=false" >> "$GITHUB_OUTPUT"
echo "TOTAL_TESTS=${TOTAL:-0}" >> "$GITHUB_ENV"
- name: Readiness summary
run: |
{
echo "## Mobile RUM SDK E2E — readiness"
echo ""
echo "Suite parsed: **${{ steps.validate.outputs.suite_ok }}** (${TOTAL_TESTS:-?} tests, 4 platforms)"
echo ""
echo "| Item | Status | Notes |"
echo "|---|---|---|"
echo "| Suite compiles | ${{ steps.validate.outputs.suite_ok == 'true' && '✅ ok' || '❌ fail' }} | gates device jobs |"
echo "| Secrets required | ✅ none | self-contained: each run builds its own OpenObserve |"
echo "| Android runner | ✅ Linux + KVM | own instance via 10.0.2.2 |"
echo "| iOS runner | ⚠️ macOS | free on this public repo; own instance via localhost |"
echo ""
echo "> Android device jobs auto-run when Android/shared mobile-RUM code changes in a PR."
echo "> iOS device jobs run on the \`mobile-e2e\` label, an SDK-release dispatch, or a manual run"
echo "> (macOS runners are scarce). See the \`decide\` job for the exact run matrix."
} >> "$GITHUB_STEP_SUMMARY"
- name: Fail if the suite does not compile
if: steps.validate.outputs.suite_ok != 'true'
run: echo "::error::mobile-testing suite failed to parse" && exit 1
# -------------------------------------------------------------------------------------------
# DECIDE — which platforms to run this time. Keeps macOS (iOS) spend in check while letting the
# cheap Linux (Android) side auto-run on relevant PRs. Emits run_android / run_ios; every device
# + build job gates on these. Policy:
# · pull_request (no label): Android runs if Android-specific OR shared paths changed; iOS does
# NOT auto-run (label/dispatch/manual only).
# · mobile-e2e label: both (full pre-merge validation gate).
# · repository_dispatch (SDK release): run the platform of the SDK that changed
# (client_payload.sdk: ios→iOS, android→Android, react-native/unset→both).
# · workflow_dispatch: honor the platforms input (both/android/ios) when run_full_suite=true.
# -------------------------------------------------------------------------------------------
decide:
name: Decide platforms
needs: preflight
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
run_android: ${{ steps.d.outputs.run_android }}
run_ios: ${{ steps.d.outputs.run_ios }}
steps:
- uses: actions/checkout@v4
# Which platform's files changed in this PR? (only evaluated for pull_request events)
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: filter
if: github.event_name == 'pull_request'
with:
filters: |
android:
- 'tests/mobile-testing/apps/o2-native-android/**'
- 'tests/mobile-testing/maestro/android-native/**'
- 'tests/mobile-testing/specs/android-native.*'
- 'tests/mobile-testing/specs/rn-android.*'
ios:
- 'tests/mobile-testing/apps/o2-native-ios/**'
- 'tests/mobile-testing/maestro/ios-native/**'
- 'tests/mobile-testing/maestro/ios-react-native/**'
- 'tests/mobile-testing/specs/ios-native.*'
- 'tests/mobile-testing/specs/rn-ios.*'
common:
- 'tests/mobile-testing/apps/o2-rum-tester/**'
- 'tests/mobile-testing/maestro/react-native/**'
- 'tests/mobile-testing/utils/**'
- 'tests/mobile-testing/playwright.config.js'
- 'tests/mobile-testing/merge.config.js'
- 'tests/mobile-testing/package.json'
- 'tests/mobile-testing/package-lock.json'
- '.github/workflows/mobile-sdk-e2e.yml'
- 'src/**/rum/**'
- 'web/src/views/RUM/**'
- 'web/src/composables/rum/**'
- 'web/src/components/ingestion/setupCard/content/rum*.ts'
- name: Compute run_android / run_ios
id: d
env:
EV: ${{ github.event_name }}
FULL: ${{ inputs.run_full_suite }}
PLATFORMS: ${{ inputs.platforms }}
SDK: ${{ github.event.client_payload.sdk }}
LABELED: ${{ contains(github.event.pull_request.labels.*.name, 'mobile-e2e') }}
F_ANDROID: ${{ steps.filter.outputs.android }}
F_COMMON: ${{ steps.filter.outputs.common }}
run: |
A=false; I=false
case "$EV" in
workflow_dispatch)
if [ "$FULL" = "true" ]; then
case "$PLATFORMS" in
both) A=true; I=true ;; android) A=true ;; ios) I=true ;;
esac
fi ;;
repository_dispatch)
case "$SDK" in
ios) I=true ;;
android) A=true ;;
*) A=true; I=true ;; # react-native / rn / unset → both
esac ;;
pull_request)
if [ "$LABELED" = "true" ]; then
A=true; I=true # label = full gate (both platforms)
else
# Android auto-runs on Android-specific or shared changes; iOS stays label/dispatch-gated.
if [ "$F_ANDROID" = "true" ] || [ "$F_COMMON" = "true" ]; then A=true; fi
fi ;;
*) A=true; I=true ;; # schedule / other → both
esac
echo "run_android=$A" >> "$GITHUB_OUTPUT"
echo "run_ios=$I" >> "$GITHUB_OUTPUT"
echo "Decision (event=$EV): run_android=$A run_ios=$I"
# Reuse the exact Playwright release-ci Linux binary and frontend when that
# workflow has already built this same merge SHA. Mobile-only/manual/SDK-release
# runs may not have a matching Playwright artifact, so keep a local build fallback.
resolve_oo_linux:
name: Find reusable OpenObserve Linux artifact
needs: [preflight, decide]
if: needs.decide.outputs.run_android == 'true'
runs-on: ubuntu-latest
# Allow 15 minutes of artifact polling plus setup/API overhead. A recent
# cold Playwright build published its pair 55 seconds after the old
# 40-attempt window expired, which forced a redundant 36-minute fallback.
timeout-minutes: 18
permissions:
actions: read
contents: read
outputs:
reuse: ${{ steps.find.outputs.reuse }}
run_id: ${{ steps.find.outputs.run_id }}
steps:
- name: Find exact-SHA Playwright artifacts
id: find
env:
GH_TOKEN: ${{ github.token }}
BINARY_ARTIFACT: release-ci-binary-${{ github.sha }}
FRONTEND_ARTIFACT: frontend-dist-${{ github.sha }}
SOURCE_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
artifacts_ready() {
local run_id="$1"
gh api --method GET \
"repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/artifacts?per_page=100" \
--jq "[.artifacts[] | select(.expired == false and (.name == \"${BINARY_ARTIFACT}\" or .name == \"${FRONTEND_ARTIFACT}\"))] | length" \
2>/dev/null || echo 0
}
RUN_ID=$(gh api --method GET \
"repos/${GITHUB_REPOSITORY}/actions/artifacts?name=${BINARY_ARTIFACT}&per_page=100" \
--jq '[.artifacts[] | select(.expired == false)] | sort_by(.created_at) | last | .workflow_run.id // empty' \
2>/dev/null || true)
if [ -n "$RUN_ID" ]; then
READY=$(artifacts_ready "$RUN_ID")
[ "$READY" -ge 2 ] || RUN_ID=''
fi
# When both workflows started for the same PR update, Mobile reaches this
# resolver before Playwright publishes its artifact pair. Wait up to
# 15 minutes for that active run instead of starting a duplicate
# ~35 minute cold build; completed producers still exit early.
if [ -z "$RUN_ID" ]; then
PLAYWRIGHT_RUN_ID=$(gh api --method GET \
"repos/${GITHUB_REPOSITORY}/actions/workflows/playwright.yml/runs?head_sha=${SOURCE_HEAD_SHA}&per_page=20" \
--jq '[.workflow_runs[] | select(.status != "completed")] | first | .id // empty' \
2>/dev/null || true)
if [ -n "$PLAYWRIGHT_RUN_ID" ]; then
echo "Waiting for Playwright run $PLAYWRIGHT_RUN_ID to publish exact-SHA artifacts"
for _ in $(seq 1 60); do
READY=$(artifacts_ready "$PLAYWRIGHT_RUN_ID")
if [ "$READY" -ge 2 ]; then
RUN_ID="$PLAYWRIGHT_RUN_ID"
break
fi
STATUS=$(gh api --method GET \
"repos/${GITHUB_REPOSITORY}/actions/runs/${PLAYWRIGHT_RUN_ID}" \
--jq '.status' 2>/dev/null || true)
[ "$STATUS" = "completed" ] && break
sleep 15
done
fi
fi
if [ -n "$RUN_ID" ]; then
echo "reuse=true" >> "$GITHUB_OUTPUT"
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
echo "Reusing Playwright artifacts from run $RUN_ID for $GITHUB_SHA"
else
echo "reuse=false" >> "$GITHUB_OUTPUT"
echo "run_id=" >> "$GITHUB_OUTPUT"
echo "No exact-SHA Playwright artifact found; Mobile will build locally"
fi
# -------------------------------------------------------------------------------------------
# BUILD OO (linux fallback) — only compile when no exact-SHA Playwright artifact exists.
# This keeps Mobile standalone for SDK-release/manual runs without duplicating a build when the
# normal Playwright workflow has already produced the compatible release-ci binary + frontend.
# The Android job runs this binary NATIVELY (not Docker): the CI emulator can't reach a Docker-
# published port via 10.0.2.2 (proven — EMUPROBE UNREACHABLE), but reaches a native host process
# through the adb-reverse/localhost tunnel.
# -------------------------------------------------------------------------------------------
build_oo_linux:
name: Build OpenObserve (linux fallback)
needs: [preflight, decide, resolve_oo_linux]
if: >-
needs.decide.outputs.run_android == 'true' &&
needs.resolve_oo_linux.outputs.reuse != 'true'
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/github-auth
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master
with:
toolchain: stable
- name: Install Protoc
run: bash ./.github/protoc.sh
- uses: actions/setup-node@v4
with: { node-version: "20" }
- name: Build binary + web UI (embedded)
env:
NODE_OPTIONS: "--max-old-space-size=8192"
run: |
# Mirror playwright.yml: build the web UI to /tmp/dist and point rust-embed there, so the
# dashboard is actually embedded (the default ../../web/dist path doesn't resolve during the
# crate build → empty UI → the login page never renders → UI-login tests fail).
mkdir -p /tmp/dist
(cd web && npm ci && npm run build && cp -r ./dist/. /tmp/dist/)
test -f /tmp/dist/index.html
sed -i 's|folder = "../../web/dist/"|folder = "/tmp/dist/"|' src/web/src/lib.rs
grep -q 'folder = "/tmp/dist/"' src/web/src/lib.rs
cargo build --profile release-ci
test -f target/release-ci/openobserve
- uses: actions/upload-artifact@v4
with:
name: oo-linux-binary
path: target/release-ci/openobserve
if-no-files-found: error
# The release-ci profile sets debug-assertions=true, which puts rust-embed-for-web in DISK-READ
# mode — the binary serves the dashboard from /tmp/dist at RUNTIME rather than embedding it. So
# ship the built UI too (same as playwright.yml's frontend-dist); the device job extracts it to
# /tmp/dist so /web serves and the dashboard-UI tests can run.
- uses: actions/upload-artifact@v4
with:
name: oo-linux-frontend
path: /tmp/dist
if-no-files-found: error
# -------------------------------------------------------------------------------------------
# BUILD OO (macOS) — same as build_oo_linux but a DARWIN binary for the iOS job (iOS simulators
# only run on macOS; a Linux binary can't execute there and there's no published darwin binary).
# Runs in PARALLEL with build_oo_linux; the iOS job just downloads this artifact (no in-job build).
# -------------------------------------------------------------------------------------------
build_oo_macos:
name: Build OpenObserve (macOS, from source)
needs: [preflight, decide]
if: needs.decide.outputs.run_ios == 'true'
runs-on: macos-15
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/github-auth
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master
with:
toolchain: stable
- name: Install Protoc (macOS)
run: brew install protobuf || true
- uses: actions/setup-node@v4
with: { node-version: "20" }
- name: Build binary + web UI (embedded)
env:
NODE_OPTIONS: "--max-old-space-size=8192"
run: |
mkdir -p /tmp/dist
(cd web && npm ci && npm run build && cp -r ./dist/. /tmp/dist/)
test -f /tmp/dist/index.html
sed -i '' 's|folder = "../../web/dist/"|folder = "/tmp/dist/"|' src/web/src/lib.rs
grep -q 'folder = "/tmp/dist/"' src/web/src/lib.rs
cargo build --profile release-ci
test -f target/release-ci/openobserve
- uses: actions/upload-artifact@v4
with:
name: oo-macos-binary
path: target/release-ci/openobserve
if-no-files-found: error
# Ship the built UI too (release-ci is disk-read; see build_oo_linux note) so the iOS job's OO
# serves /web and the dashboard-UI tests can run.
- uses: actions/upload-artifact@v4
with:
name: oo-macos-frontend
path: /tmp/dist
if-no-files-found: error
# -------------------------------------------------------------------------------------------
# ANDROID — runs the from-source OO binary NATIVELY; the app posts to it via the emulator's host
# alias 10.0.2.2:5080 (the suite's own API/UI checks, on the host, use localhost:5080).
# -------------------------------------------------------------------------------------------
android:
# Both Android platforms run in ONE job on a single emulator (proven-green config). A per-platform
# matrix split was tried for observability but exposed emulator first-render flakiness; revisit only
# with emulator hardening (disable-animations + app warm-up + launch retries).
name: Android (rn-android + android-native)
needs: [decide, resolve_oo_linux, build_oo_linux]
if: >-
always() &&
needs.decide.result == 'success' &&
needs.decide.outputs.run_android == 'true' &&
needs.resolve_oo_linux.result == 'success' &&
(needs.resolve_oo_linux.outputs.reuse == 'true' ||
needs.build_oo_linux.result == 'success')
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
# Test the LATEST SDK on a release event: the mobile-sdk-release dispatch carries {sdk, version};
# a manual run can also override via the sdk/sdk_version inputs. Unset → committed pin (reproducible).
- name: Resolve SDK version override
# Untrusted dispatch payload / manual inputs arrive via env (NEVER interpolated into the
# script body) to avoid shell injection, and the version is format-validated before use.
env:
EVENT_NAME: ${{ github.event_name }}
CP_SDK: ${{ github.event.client_payload.sdk }}
CP_VER: ${{ github.event.client_payload.version }}
IN_SDK: ${{ inputs.sdk }}
IN_VER: ${{ inputs.sdk_version }}
run: |
SDK=''; VER=''
if [ "$EVENT_NAME" = "repository_dispatch" ]; then SDK="$CP_SDK"; VER="$CP_VER";
elif [ "$EVENT_NAME" = "workflow_dispatch" ]; then SDK="$IN_SDK"; VER="$IN_VER"; fi
[ -z "$VER" ] && { echo "no SDK override — using committed pins"; exit 0; }
if ! printf '%s' "$VER" | grep -qE '^[0-9A-Za-z][0-9A-Za-z.-]*$'; then
echo "::error::Refusing unsafe SDK version '$VER' (expected e.g. 0.1.1)"; exit 1
fi
case "$SDK" in
ios) echo "O2_IOS_SDK_VERSION=$VER" >> "$GITHUB_ENV" ;;
android) echo "O2_ANDROID_SDK_VERSION=$VER" >> "$GITHUB_ENV" ;;
react-native|rn) echo "O2_RN_SDK_VERSION=$VER" >> "$GITHUB_ENV" ;;
*) echo "no SDK override" ;;
esac
- name: Enable KVM
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
- uses: actions/setup-java@v6
with: { distribution: temurin, java-version: "17" }
- uses: actions/setup-node@v4
with: { node-version: "20" }
- uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
- name: Install Maestro
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
- name: Download shared Playwright OpenObserve binary
if: needs.resolve_oo_linux.outputs.reuse == 'true'
uses: actions/download-artifact@v5
with:
name: release-ci-binary-${{ github.sha }}
path: oo-bin
github-token: ${{ github.token }}
repository: ${{ github.repository }}
run-id: ${{ needs.resolve_oo_linux.outputs.run_id }}
- name: Download fallback OpenObserve binary
if: needs.resolve_oo_linux.outputs.reuse != 'true'
uses: actions/download-artifact@v4
with:
name: oo-linux-binary
path: oo-bin
- name: Download shared Playwright web UI
if: needs.resolve_oo_linux.outputs.reuse == 'true'
uses: actions/download-artifact@v5
with:
name: frontend-dist-${{ github.sha }}
path: /tmp/dist
github-token: ${{ github.token }}
repository: ${{ github.repository }}
run-id: ${{ needs.resolve_oo_linux.outputs.run_id }}
- name: Download fallback OpenObserve web UI
if: needs.resolve_oo_linux.outputs.reuse != 'true'
uses: actions/download-artifact@v4
with:
name: oo-linux-frontend
path: /tmp/dist
- name: Start OpenObserve (native, from source) + mint RUM token
run: |
chmod +x oo-bin/openobserve
ZO_DATA_DIR="$PWD/oo-data" ./oo-bin/openobserve > oo.log 2>&1 &
for i in $(seq 1 90); do curl -sf http://localhost:5080/healthz && break || sleep 2; done
# GET (not POST): POST /rumtoken ROTATES the token; GET reads the stable one the app must match.
TOKEN=$(curl -s -u "$ZO_ROOT_USER_EMAIL:$ZO_ROOT_USER_PASSWORD" \
http://localhost:5080/api/default/rumtoken | jq -r '.data.rum_token // empty')
if [ -z "$TOKEN" ]; then echo "::error::Could not read rum_token"; cat oo.log; exit 1; fi
# Fail-fast canary: prove the RUM ingest pipeline works before the device suite.
NOW=$(($(date +%s) * 1000))
curl -sf -X POST "http://localhost:5080/rum/v1/default/rum?oo-api-key=$TOKEN&ddsource=react-native" \
-H 'Content-Type: text/plain' \
--data-binary "{\"type\":\"view\",\"service\":\"ci-canary\",\"date\":$NOW,\"session\":{\"id\":\"ci-canary\",\"type\":\"user\"},\"view\":{\"id\":\"v\",\"name\":\"Canary\"},\"source\":\"react-native\"}" \
|| { echo "::error::RUM ingest canary POST failed — aborting before suite"; exit 1; }
echo "RUM ingest canary OK — token + pipeline verified on the runner OpenObserve"
{
# App runs in the emulator and posts to the runner's NATIVE OO via the emulator host
# alias 10.0.2.2:5080 (verified by the EMUPROBE step below). The OO is a native host
# process, not Docker — the emulator can't reach a Docker-published port on the runner.
echo "O2_RUM_HOST=http://10.0.2.2:5080"
echo "O2_RUM_ORG=default"
echo "O2_RUM_TOKEN=$TOKEN"
echo "O2_RUM_ENV=testing"
echo "OO_URL=http://localhost:5080"
} >> "$GITHUB_ENV"
- name: Install suite deps
working-directory: tests/mobile-testing
run: |
npm ci || npm install
# The dashboard UI-login tests (crashed-session, masking) drive a real browser.
npx playwright install --with-deps chromium
# RN app: npm ci runs postinstall → patch-sdk + gen-config (seds App.tsx @gen consts from O2_RUM_*).
- name: Build RN-Android APK
working-directory: tests/mobile-testing/apps/o2-rum-tester
run: |
if [ -n "$O2_RN_SDK_VERSION" ]; then
npm install --legacy-peer-deps \
"@openobserve/mobile-react-native@$O2_RN_SDK_VERSION" \
"@openobserve/mobile-react-native-session-replay@$O2_RN_SDK_VERSION"
else
npm ci --legacy-peer-deps || npm install --legacy-peer-deps
fi
# The merged RN 0.87 dependency bump did not migrate this fixture's React 18 / RN 0.75
# native template. Keep the committed dependency fix, but test the existing template with
# its compatible RN version until that migration lands as a separate change.
npm install --no-save --package-lock=false --legacy-peer-deps react-native@0.75.4
# Release build: embeds the JS bundle (Debug loads it from the Metro dev server, which
# isn't running in CI → blank screen). The release buildType is debug-signed in the RN
# template, so it installs on the emulator without a real keystore.
(cd android && ./gradlew --no-daemon assembleRelease)
# Native app: gradle reads O2_RUM_* via System.getenv into BuildConfig. Uses the setup-gradle
# `gradle` (a committed wrapper also works — `./gradlew` — but this keeps one Gradle across jobs).
- name: Build Android-native APK
working-directory: tests/mobile-testing/apps/o2-native-android
run: gradle --no-daemon assembleDebug
- name: Boot emulator, install apps, run suite
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
with:
api-level: 35
target: google_apis
arch: x86_64
profile: pixel_6
# NB: the emulator-runner executes each line in a SEPARATE shell, so `cd` does not persist —
# the test command must chain `cd && npx` on one line.
script: |
adb reverse tcp:5080 tcp:5080
adb root || true
adb shell "toybox date @$(date +%s)" || adb shell "date $(date +%m%d%H%M%Y.%S)" || true
adb shell 'printf "GET /healthz HTTP/1.0\r\nHost: h\r\n\r\n" | nc -w 5 10.0.2.2 5080' && echo "EMUPROBE: emulator reaches 10.0.2.2:5080 (native OO)" || echo "EMUPROBE: emulator CANNOT reach 10.0.2.2:5080"
adb install -r tests/mobile-testing/apps/o2-rum-tester/android/app/build/outputs/apk/release/app-release.apk
adb install -r tests/mobile-testing/apps/o2-native-android/app/build/outputs/apk/debug/app-debug.apk
cd tests/mobile-testing && BLOB_REPORT_NAME=report-android.zip npx playwright test --project=rn-android --project=android-native
# Diagnostic: dump what actually reached _rumdata (services, counts, and the timestamp range vs
# "now"). Distinguishes "app POST never landed" from "landed but outside the query window"
# (emulator clock skew). Runs even when the suite fails.
- name: Diagnose — _rumdata contents on the runner OpenObserve
if: always()
run: |
NOW_US=$(($(date +%s) * 1000000)); START_US=$((NOW_US - 86400000000))
echo "runner now_us=$NOW_US"
curl -s -u "$OO_USER:$OO_PASS" -X POST "http://localhost:5080/api/default/_search?type=logs" \
-H 'Content-Type: application/json' \
-d "{\"query\":{\"sql\":\"SELECT service, COUNT(*) AS n, MIN(_timestamp) AS mn, MAX(_timestamp) AS mx FROM \\\"_rumdata\\\" GROUP BY service\",\"start_time\":$START_US,\"end_time\":$NOW_US,\"size\":50}}" \
| jq '.hits // .' || true
- name: Upload blob report (merged by merge_reports)
if: always()
uses: actions/upload-artifact@v4
with:
name: blob-report-android
path: tests/mobile-testing/blob-report
if-no-files-found: ignore
# -------------------------------------------------------------------------------------------
# iOS — own OpenObserve built FROM SOURCE on the runner (no macOS release binary exists + no Docker
# on macOS runners); app + suite both use localhost (sim shares the host network). Runs in PARALLEL
# with android (separate instance). Alternatives to hosted macOS runners are all still macOS.
# -------------------------------------------------------------------------------------------
ios:
name: iOS (rn-ios + ios-native)
needs: [decide, build_oo_macos]
if: needs.decide.outputs.run_ios == 'true'
# macos-15 ships Xcode 16.x (RN 0.75-compatible + still includes the Swift compatibility libs the
# SDK references). macos-latest moved to Xcode 26, which removed libswiftCompatibility56 → link error.
runs-on: macos-15
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Resolve SDK version override
# Untrusted dispatch payload / manual inputs arrive via env (NEVER interpolated into the
# script body) to avoid shell injection, and the version is format-validated before use.
env:
EVENT_NAME: ${{ github.event_name }}
CP_SDK: ${{ github.event.client_payload.sdk }}
CP_VER: ${{ github.event.client_payload.version }}
IN_SDK: ${{ inputs.sdk }}
IN_VER: ${{ inputs.sdk_version }}
run: |
SDK=''; VER=''
if [ "$EVENT_NAME" = "repository_dispatch" ]; then SDK="$CP_SDK"; VER="$CP_VER";
elif [ "$EVENT_NAME" = "workflow_dispatch" ]; then SDK="$IN_SDK"; VER="$IN_VER"; fi
[ -z "$VER" ] && { echo "no SDK override — using committed pins"; exit 0; }
if ! printf '%s' "$VER" | grep -qE '^[0-9A-Za-z][0-9A-Za-z.-]*$'; then
echo "::error::Refusing unsafe SDK version '$VER' (expected e.g. 0.1.1)"; exit 1
fi
case "$SDK" in
ios) echo "O2_IOS_SDK_VERSION=$VER" >> "$GITHUB_ENV" ;;
android) echo "O2_ANDROID_SDK_VERSION=$VER" >> "$GITHUB_ENV" ;;
react-native|rn) echo "O2_RN_SDK_VERSION=$VER" >> "$GITHUB_ENV" ;;
*) echo "no SDK override" ;;
esac
# Pin Xcode so a runner-image update can't silently move the toolchain under the alpha SDK.
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
# Pin 16.4 explicitly: macos-15's 'latest-stable' is now Xcode 26, which dropped the Swift
# compatibility libs the SDK's static libs force-load → arm64 link error. 16.x still ships them.
xcode-version: "16.4"
- uses: actions/setup-node@v4
with: { node-version: "20" }
- name: Install Maestro + xcodegen + watchman
run: |
brew install watchman xcodegen jq || true
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
# The darwin OpenObserve binary is compiled ONCE by the build_oo_macos job (in parallel with
# build_oo_linux) and shared as an artifact — this job just downloads and runs it, so iterating
# on the iOS suite never recompiles OpenObserve (same pattern as Android + playwright.yml).
- name: Download OpenObserve binary (built from PR source)
uses: actions/download-artifact@v4
with:
name: oo-macos-binary
path: oo-bin
- name: Download OpenObserve web UI (so /web serves for the dashboard tests)
uses: actions/download-artifact@v4
with:
name: oo-macos-frontend
path: /tmp/dist
- name: Start OpenObserve (native, from source) + mint RUM token
run: |
chmod +x oo-bin/openobserve
ZO_DATA_DIR="$PWD/oo-data" ./oo-bin/openobserve > oo.log 2>&1 &
for i in $(seq 1 90); do curl -sf http://localhost:5080/healthz && break || sleep 2; done
# GET (not POST): POST /rumtoken ROTATES the token each call; the default org already has
# a token at creation, so GET reads the stable one the app must match. Validated locally.
TOKEN=$(curl -s -u "$ZO_ROOT_USER_EMAIL:$ZO_ROOT_USER_PASSWORD" \
http://localhost:5080/api/default/rumtoken | jq -r '.data.rum_token // empty')
if [ -z "$TOKEN" ]; then echo "::error::Could not read rum_token from OpenObserve"; exit 1; fi
# Fail-fast canary: prove the RUM ingest pipeline works before the ~1h device suite.
NOW=$(($(date +%s) * 1000))
curl -sf -X POST "http://localhost:5080/rum/v1/default/rum?oo-api-key=$TOKEN&ddsource=react-native" \
-H 'Content-Type: text/plain' \
--data-binary "{\"type\":\"view\",\"service\":\"ci-canary\",\"date\":$NOW,\"session\":{\"id\":\"ci-canary\",\"type\":\"user\"},\"view\":{\"id\":\"v\",\"name\":\"Canary\"},\"source\":\"react-native\"}" \
|| { echo "::error::RUM ingest canary POST failed — pipeline broken, aborting before suite"; exit 1; }
echo "RUM ingest canary OK — token + pipeline verified on the runner OpenObserve"
{
# iOS simulator shares the host network stack, so it reaches the native OO at localhost.
# (10.0.2.2 is the ANDROID emulator's host alias and is unreachable from an iOS sim —
# using it here silently dropped every upload → 0 rows in _rumdata.)
echo "O2_RUM_HOST=http://127.0.0.1:5080"
echo "O2_RUM_ORG=default"
echo "O2_RUM_TOKEN=$TOKEN"
echo "O2_RUM_ENV=testing"
echo "OO_URL=http://localhost:5080"
} >> "$GITHUB_ENV"
- name: Install suite deps
working-directory: tests/mobile-testing
run: |
npm ci || npm install
# The dashboard UI-login tests (crashed-session, masking) drive a real browser.
npx playwright install --with-deps chromium
- name: Boot an iOS simulator
run: |
# Pick an iPhone specifically — `head -1` of ALL device classes could grab a watchOS/tvOS
# sim, and every iOS test would then fail at install for an unrelated-looking reason.
UDID=$(xcrun simctl list devices available | grep -E 'iPhone' | grep -Eo '[0-9A-F-]{36}' | head -1)
xcrun simctl boot "$UDID" || true
echo "IOS_SIM_UDID=$UDID" >> "$GITHUB_ENV"
# RN iOS: npm ci runs gen-config (seds App.tsx @gen consts → localhost). Allow cleartext to localhost
# for App Transport Security (CI-only plist tweak; committed source stays HTTPS).
# The iOS app builds (CocoaPods + Xcode Release compile of React Native) are the long pole of
# this job (~20min cold). Cache the pods and the Xcode build products so warm runs are minutes.
- name: Cache CocoaPods
uses: actions/cache@v6
with:
path: |
~/Library/Caches/CocoaPods
~/.cocoapods
tests/mobile-testing/apps/o2-rum-tester/ios/Pods
key: pods-${{ runner.os }}-${{ hashFiles('tests/mobile-testing/apps/o2-rum-tester/ios/Podfile.lock') }}
restore-keys: pods-${{ runner.os }}-
- name: Cache Xcode DerivedData (iOS app builds)
uses: actions/cache@v6
with:
path: |
tests/mobile-testing/apps/o2-rum-tester/ios/build
tests/mobile-testing/apps/o2-native-ios/build
key: xcode-dd-${{ runner.os }}-${{ hashFiles('tests/mobile-testing/apps/o2-rum-tester/ios/Podfile.lock', 'tests/mobile-testing/apps/o2-native-ios/project.yml') }}
restore-keys: xcode-dd-${{ runner.os }}-
- name: Build + install RN-iOS app
working-directory: tests/mobile-testing/apps/o2-rum-tester
run: |
if [ -n "$O2_RN_SDK_VERSION" ]; then
npm install --legacy-peer-deps \
"@openobserve/mobile-react-native@$O2_RN_SDK_VERSION" \
"@openobserve/mobile-react-native-session-replay@$O2_RN_SDK_VERSION"
else
npm ci --legacy-peer-deps || npm install --legacy-peer-deps
fi
# See the Android step above: exercise the committed RN 0.75 native template without
# reverting the repository's RN 0.87 dependency/security update.
npm install --no-save --package-lock=false --legacy-peer-deps react-native@0.75.4
/usr/libexec/PlistBuddy -c "Add :NSAppTransportSecurity:NSAllowsLocalNetworking bool true" ios/O2RumTester/Info.plist || true
(cd ios && pod install)
# Release: embeds the JS bundle (Debug expects the Metro dev server → blank screen in CI).
# Simulator builds need no code signing.
xcodebuild -workspace ios/O2RumTester.xcworkspace -scheme O2RumTester \
-configuration Release -sdk iphonesimulator -destination "id=$IOS_SIM_UDID" \
-derivedDataPath ios/build CODE_SIGNING_ALLOWED=NO build
xcrun simctl install "$IOS_SIM_UDID" ios/build/Build/Products/Release-iphonesimulator/O2RumTester.app
# iOS-native: gen-config.sh writes GeneratedConfig.swift (localhost) before xcodegen.
- name: Build + install iOS-native app
working-directory: tests/mobile-testing/apps/o2-native-ios
run: |
if [ -n "$O2_IOS_SDK_VERSION" ]; then
sed -i '' "s/exactVersion:.*/exactVersion: $O2_IOS_SDK_VERSION/" project.yml
fi
O2_RUM_HOST="$O2_RUM_HOST" O2_RUM_ORG="$O2_RUM_ORG" O2_RUM_TOKEN="$O2_RUM_TOKEN" O2_RUM_ENV="$O2_RUM_ENV" bash gen-config.sh
xcodegen generate
xcodebuild -project O2NativeIOS.xcodeproj -scheme O2NativeIOS \
-configuration Debug -sdk iphonesimulator -destination "id=$IOS_SIM_UDID" \
-derivedDataPath build build
xcrun simctl install "$IOS_SIM_UDID" build/Build/Products/Debug-iphonesimulator/O2NativeIOS.app
- name: Run iOS suite
working-directory: tests/mobile-testing
run: BLOB_REPORT_NAME=report-ios.zip npx playwright test --project=rn-ios --project=ios-native
# Diagnostic: what actually reached _rumdata + the timestamp range vs "now". Distinguishes
# "app never uploaded" (0 rows for the services) from "landed outside the query window" (sim
# clock skew — the +5min buffer in ooClient targets this). ios-native uploads fine locally.
- name: Diagnose — _rumdata contents on the runner OpenObserve
if: always()
run: |
NOW_US=$(($(date +%s) * 1000000)); START_US=$((NOW_US - 86400000000))
echo "runner now_us=$NOW_US ; sim clock:"; xcrun simctl spawn booted date || true
curl -s -u "$OO_USER:$OO_PASS" -X POST "http://localhost:5080/api/default/_search?type=logs" \
-H 'Content-Type: application/json' \
-d "{\"query\":{\"sql\":\"SELECT service, COUNT(*) AS n, MIN(_timestamp) AS mn, MAX(_timestamp) AS mx FROM \\\"_rumdata\\\" GROUP BY service\",\"start_time\":$START_US,\"end_time\":$((NOW_US + 600000000)),\"size\":50}}" \
| jq '.hits // .' || true
- name: Upload blob report (merged by merge_reports)
if: always()
uses: actions/upload-artifact@v4
with:
name: blob-report-ios
path: tests/mobile-testing/blob-report
if-no-files-found: ignore
# -------------------------------------------------------------------------------------------
# MERGE REPORTS — always runs (even if a device job failed) so there is ONE browsable HTML report
# of the whole run. Mirrors playwright.yml: download every blob-report-* and `playwright merge-reports`.
# -------------------------------------------------------------------------------------------
merge_reports:
name: Merge Reports
needs: [android, ios]
if: always() && (github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'ready-for-ci') && github.event.pull_request.draft == false))
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20" }
- name: Install suite deps
working-directory: tests/mobile-testing
run: npm ci || npm install
- name: Download all blob reports
uses: actions/download-artifact@v4
with:
path: tests/mobile-testing/all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Merge into one HTML report
working-directory: tests/mobile-testing
run: |
if [ ! -d all-blob-reports ] || [ -z "$(ls -A all-blob-reports 2>/dev/null)" ]; then
echo "::warning::no blob reports found (device jobs may have been skipped)"; exit 0
fi
# -c merge.config.js supplies a single relative testDir so blobs from Linux (Android) and
# macOS (iOS) merge despite their different absolute recording paths. Never fail the job:
# producing the report (pass OR fail data) is the whole point, even when a device job is red.
npx playwright merge-reports -c merge.config.js ./all-blob-reports || echo "::warning::merge-reports reported an error"
test -f playwright-report/index.html && echo "merged report OK" || echo "::warning::no merged index.html produced"
- name: Upload merged HTML report
if: always()
uses: actions/upload-artifact@v4
with:
name: mobile-report-merged
path: tests/mobile-testing/playwright-report
if-no-files-found: ignore