mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-12 14:10:14 +00:00
* MM-71189 Preserve feature flags in Detox config patches Co-authored-by: mattermost-code <matty-code@mattermost.com> * Guard getFeatureFlags against empty config responses Return an error instead of treating a missing config body as an empty flag set, so a dropped GET does not patch only the requested flags. Co-authored-by: mattermost-code <matty-code@mattermost.com> * Retrigger CI after Detox iOS and iPad E2E flakes Co-authored-by: mattermost-code <matty-code@mattermost.com> * test(e2e): apply one declared server baseline at provisioning Provisioning drives each test server to one declared baseline: the config keys E2E needs and the full FeatureFlags map, written read-modify-write so no flag is reset. Env-pinned keys are reported, not patched, and leftover E2E policies, remote clusters and property fields are removed. Provisioning also disables stray plugins once. * test(e2e): assert the server baseline instead of patching config in specs These specs patched server-wide settings (CRT, archived channels, auto-replies, ...) and restored them while other shards depended on them. They now assert the provisioned baseline instead. * test(e2e): split CI servers into pool and mutation roles; remove the locks Each platform's server A becomes a read-only pool for 27 shards (and iPad and Maestro); server B (and on iOS part of SITE_3) runs one serial mutation shard with the specs in detox/e2e/server_mutation_specs.json. The classification, server and SITE_3 locks go. - split-tests.js places mutation specs by the manifest and packs the pool by duration; a spec that needs B never falls back to the shared SITE_3. - A pool request guard, an ESLint rule and a manifest checker keep server-wide writes off the pool. Mutation shards re-apply the baseline before each spec file. - Mutating tests move to their own spec files; ABAC specs use channel-scoped permission policies; MFA and Terms of Service run on the mutation server. - Pool shards no longer disable provisioned plugins, so the Agents specs run. * fix(e2e): fail a spec when the baseline cannot be applied locally * fix(e2e): keep transport retries for feature-flag and baseline reads Restore the retry in getFeatureFlags, which apiEnableFeatureFlags calls directly, and retry the baseline assertion's GET requests on transport failures. * test(e2e): cover retries of dropped config and baseline reads * test(e2e): apply one declared server baseline at provisioning Provisioning drives each test server to one declared baseline: the config keys E2E needs and the full FeatureFlags map, written read-modify-write so no flag is reset. Env-pinned keys are reported, not patched, and leftover E2E policies, remote clusters and property fields are removed. Provisioning also disables stray plugins once. * test(e2e): assert the server baseline instead of patching config in specs These specs patched server-wide settings (CRT, archived channels, auto-replies, ...) and restored them while other shards depended on them. They now assert the provisioned baseline instead. * fix(e2e): fail a spec when the baseline cannot be applied locally * fix(e2e): keep transport retries for feature-flag and baseline reads Restore the retry in getFeatureFlags, which apiEnableFeatureFlags calls directly, and retry the baseline assertion's GET requests on transport failures. * test(e2e): cover retries of dropped config and baseline reads * E2E: stop burn-on-read spec turning other feature flags off Its partial FeatureFlags patch reset every other flag on the shared server, which turned ChannelAttributes off under channel_attributes on another shard. Enable only the missing flags, as redacted_files and upload_permission do, and do the same for the dialog spec. Also tap the attach button the app actually renders in MM-T339_1. * E2E: route flag-off spec to B on iOS; tap the real attach button in the max-size spec * test(e2e): apply one declared server baseline at provisioning Provisioning drives each test server to one declared baseline: the config keys E2E needs and the full FeatureFlags map, written read-modify-write so no flag is reset. Env-pinned keys are reported, not patched, and leftover E2E policies, remote clusters and property fields are removed. Provisioning also disables stray plugins once. * test(e2e): assert the server baseline instead of patching config in specs These specs patched server-wide settings (CRT, archived channels, auto-replies, ...) and restored them while other shards depended on them. They now assert the provisioned baseline instead. * fix(e2e): fail a spec when the baseline cannot be applied locally * fix(e2e): keep transport retries for feature-flag and baseline reads Restore the retry in getFeatureFlags, which apiEnableFeatureFlags calls directly, and retry the baseline assertion's GET requests on transport failures. * test(e2e): cover retries of dropped config and baseline reads * E2E: stop burn-on-read spec turning other feature flags off Its partial FeatureFlags patch reset every other flag on the shared server, which turned ChannelAttributes off under channel_attributes on another shard. Enable only the missing flags, as redacted_files and upload_permission do, and do the same for the dialog spec. Also tap the attach button the app actually renders in MM-T339_1. * E2E: wait for the results service before retrying a shard upload upload-artifact does not retry network errors (ENOTFOUND, ETIMEDOUT), and the retry fired ~30 ms after the first failure, inside the same DNS outage. A shard whose specs all passed was then reported as "spec did not run". Each retry now waits (bounded, 120 s) for the results host to answer, with a second retry. * E2E: run the specs a killed iOS Jest never started; record why it died When Jest is killed between spec files (exit code = null) it writes no report, and the specs after that point never run. Run only those once in a fresh Jest and merge the results; specs that ran keep their own result, so a spec that crashes Jest stays red. Also sample memory and collect jetsam/crash reports so the next kill names its cause. * E2E: keep the demo plugin off the shared servers; 32 shards The demo plugin posts into every team on every server event and inside every config save, and on our servers each of those posts fails. Only the two dialog specs use it, so install it only on mutation server B, disable it everywhere else (failing provisioning if it stays on), and route both dialog specs to B. Drop the DialogOnlyMode setting, which v0.12.1 does not have. Raise Detox shards 28 -> 32. * E2E: give the demo plugin a settings entry on the servers that install it Demo plugin v0.12.1's ConfigurationWillBeSaved hands the whole config back when it has no settings entry, and that round trip resets settings it does not know, or that are false, to their defaults. With DialogOnlyMode gone from the baseline, fresh B servers had no entry, so ChannelAttributesRequired=true and EnableMobileUpload=false were reverted inside the save. An empty entry keeps the hook out of the save. --------- Co-authored-by: mattermost-code <matty-code@mattermost.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: yasserfaraazkhan <yasser.khan@mattermost.com>
867 lines
38 KiB
YAML
867 lines
38 KiB
YAML
name: Detox Android E2E Tests Template
|
|
|
|
on:
|
|
workflow_call:
|
|
secrets:
|
|
MM_MOBILE_E2E_ADMIN_EMAIL:
|
|
required: false
|
|
MM_MOBILE_E2E_ADMIN_USERNAME:
|
|
required: false
|
|
MM_MOBILE_E2E_ADMIN_PASSWORD:
|
|
required: false
|
|
MM_MOBILE_E2E_ZEPHYR_API_KEY:
|
|
required: false
|
|
MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX:
|
|
required: false
|
|
MM_MOBILE_E2E_WEBHOOK_URL:
|
|
required: false
|
|
# Channel-notify routing for the per-job TSIO finalize step. Callers that use
|
|
# an explicit `secrets:` block must forward these or notify silently no-ops.
|
|
MM_E2E_MASTER_HEALTH_WEBHOOK_URL:
|
|
required: false
|
|
MM_E2E_RELEASE_WEBHOOK_URL:
|
|
required: false
|
|
MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID:
|
|
required: false
|
|
MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY:
|
|
required: false
|
|
# Webhook sidecar. Unset means mm_blocks specs fall back to trycloudflare.
|
|
MM_MOBILE_E2E_WEBHOOK_PUBLIC_BASE_URL:
|
|
required: false
|
|
MM_MOBILE_E2E_CLOUDFLARED_TUNNEL_TOKEN:
|
|
required: false
|
|
inputs:
|
|
MM_TEST_SERVER_URL:
|
|
description: "The test server URL (SITE_1)"
|
|
required: false
|
|
type: string
|
|
MM_TEST_SERVER_URL_2:
|
|
description: "The second test server URL (SITE_2); optional, no automatic fallback"
|
|
required: false
|
|
type: string
|
|
MM_TEST_SERVER_URL_3:
|
|
description: "The third test server URL (SITE_3); optional, no automatic fallback"
|
|
required: false
|
|
type: string
|
|
MM_OTHER_PLATFORM_POOL_URL:
|
|
description: "The other platform's SITE_1 server, used as this platform's pool SITE_2; empty falls back to SITE_1"
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
MM_TEST_USER_NAME:
|
|
description: "The admin username of the test instance"
|
|
required: false
|
|
type: string
|
|
MM_TEST_PASSWORD:
|
|
description: "The admin password of the test instance"
|
|
required: false
|
|
type: string
|
|
MOBILE_VERSION:
|
|
description: "The mobile version to test"
|
|
required: false
|
|
default: ""
|
|
type: string
|
|
run-android-tests:
|
|
description: "Run Android tests"
|
|
required: true
|
|
type: boolean
|
|
run-type:
|
|
type: string
|
|
required: false
|
|
default: "PR"
|
|
record_tests_in_zephyr:
|
|
description: "Record test results in Zephyr, typically for release and main runs"
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
low_bandwidth_mode:
|
|
description: "Enable low bandwidth mode"
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
search_path:
|
|
description: "Path to search for test specs (use for smoke test subset)"
|
|
required: false
|
|
type: string
|
|
default: "detox/e2e/test"
|
|
parallelism:
|
|
description: "Number of parallel test shards"
|
|
required: false
|
|
type: string
|
|
default: "32"
|
|
android_avd_name:
|
|
description: "Android Emulator name"
|
|
required: false
|
|
type: string
|
|
default: "detox_pixel_8"
|
|
android_api_level:
|
|
description: "Android API level"
|
|
required: false
|
|
type: string
|
|
default: "35"
|
|
artifact_run_id:
|
|
description: "Run ID of the workflow that uploaded android-build-files-* (defaults to caller run)"
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
tsio-config:
|
|
description: >
|
|
TSIO reporting config: {"composite_identity":{...},"total_reports_expected":N}.
|
|
Empty disables TSIO upload for this platform.
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
tsio-shard-name:
|
|
description: "Display name for this shard in TSIO (e.g. detox-android-Server_11.9.0-rc3)"
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
durations_key:
|
|
description: >
|
|
Spec-duration manifest key (e.g. detox-android). Enables duration-aware
|
|
shard packing. Empty keeps the even alphabetical split.
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
outputs:
|
|
STATUS:
|
|
value: ${{ jobs.generate-report.outputs.STATUS }}
|
|
TARGET_URL:
|
|
value: ${{ jobs.generate-report.outputs.TARGET_URL }}
|
|
FAILURES:
|
|
value: ${{ jobs.generate-report.outputs.FAILURES }}
|
|
PASSES:
|
|
value: ${{ jobs.generate-report.outputs.PASSES }}
|
|
TOTAL:
|
|
value: ${{ jobs.generate-report.outputs.TOTAL }}
|
|
SKIPPED:
|
|
value: ${{ jobs.generate-report.outputs.SKIPPED }}
|
|
ERRORS:
|
|
value: ${{ jobs.generate-report.outputs.ERRORS }}
|
|
PERCENTAGE:
|
|
value: ${{ jobs.generate-report.outputs.PERCENTAGE }}
|
|
|
|
env:
|
|
AWS_REGION: "us-east-1"
|
|
ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }}
|
|
ADMIN_USERNAME: ${{ inputs.MM_TEST_USER_NAME || secrets.MM_MOBILE_E2E_ADMIN_USERNAME }}
|
|
ADMIN_PASSWORD: ${{ inputs.MM_TEST_PASSWORD || secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }}
|
|
BRANCH: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}
|
|
COMMIT_HASH: ${{ github.sha }}
|
|
DEVICE_NAME: ${{ inputs.android_avd_name }} # This is needed to split tests as same code is used in iOS job
|
|
DEVICE_OS_VERSION: ${{ inputs.android_api_level }}
|
|
DETOX_AWS_S3_BUCKET: "mattermost-detox-report"
|
|
HEADLESS: "true"
|
|
TYPE: ${{ inputs.run-type }}
|
|
PULL_REQUEST: "https://github.com/mattermost/mattermost-mobile/pull/${{ github.event.number }}"
|
|
SITE_1_URL: ${{ inputs.MM_TEST_SERVER_URL }}
|
|
SITE_2_URL: ${{ inputs.MM_TEST_SERVER_URL_2 }}
|
|
SITE_3_URL: ${{ inputs.MM_TEST_SERVER_URL_3 }}
|
|
ZEPHYR_ENABLE: ${{ inputs.record_tests_in_zephyr }}
|
|
JIRA_PROJECT_KEY: "MM"
|
|
ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }}
|
|
ZEPHYR_FOLDER_ID: "3233873"
|
|
TEST_CYCLE_LINK_PREFIX: ${{ secrets.MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX }}
|
|
WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }}
|
|
FAILURE_MESSAGE: "Something has failed"
|
|
IOS: "false"
|
|
RUNNING_E2E: "true"
|
|
AVD_NAME: ${{ inputs.android_avd_name }}
|
|
SDK_VERSION: ${{ inputs.android_api_level }}
|
|
ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id }}
|
|
|
|
jobs:
|
|
generate-specs:
|
|
runs-on: ubuntu-24.04
|
|
outputs:
|
|
specs: ${{ steps.generate-specs.outputs.specs }}
|
|
shard_count: ${{ steps.generate-specs.outputs.shard_count }}
|
|
build_id: ${{ steps.resolve-device.outputs.BUILD_ID }}
|
|
mobile_sha: ${{ steps.resolve-device.outputs.MOBILE_SHA }}
|
|
mobile_ref: ${{ steps.resolve-device.outputs.MOBILE_REF }}
|
|
workflow_hash: ${{ steps.resolve-device.outputs.WORKFLOW_HASH }}
|
|
steps:
|
|
- name: Checkout Repository
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ inputs.MOBILE_VERSION }}
|
|
|
|
- name: Set Build ID
|
|
id: resolve-device
|
|
run: |
|
|
BUILD_ID="${{ github.run_id }}-${{ env.AVD_NAME }}-${{ env.SDK_VERSION}}"
|
|
WORKFLOW_HASH=$(tr -dc a-z0-9 </dev/urandom | head -c 10)
|
|
|
|
## We need that hash to separate the artifacts
|
|
echo "WORKFLOW_HASH=${WORKFLOW_HASH}" >> ${GITHUB_OUTPUT}
|
|
|
|
echo "BUILD_ID=$(echo ${BUILD_ID} | sed 's/ /_/g')" >> ${GITHUB_OUTPUT}
|
|
echo "MOBILE_SHA=$(git rev-parse HEAD)" >> ${GITHUB_OUTPUT}
|
|
echo "MOBILE_REF=$(git rev-parse --abbrev-ref HEAD)" >> ${GITHUB_OUTPUT}
|
|
|
|
# Duration history from the last main-push run; absent → even split.
|
|
# continue-on-error: this job gates the entire test matrix, and shard packing
|
|
# is an optimization — a missing manifest or an AWS hiccup must not stop the
|
|
# run, it must fall back to the even split.
|
|
- name: Restore spec-duration manifest
|
|
if: inputs.durations_key != ''
|
|
continue-on-error: true
|
|
uses: ./.github/actions/s3-spec-durations
|
|
env:
|
|
DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }}
|
|
DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }}
|
|
with:
|
|
key: ${{ inputs.durations_key }}
|
|
bucket: ${{ env.DETOX_AWS_S3_BUCKET }}
|
|
mode: restore
|
|
|
|
# Server B gets a mutation shard when it is distinct from A. Android has no
|
|
# site3 mutation target; its specs whose iosTarget is site3 run on B.
|
|
- name: Resolve mutation server targets
|
|
id: mutation-targets
|
|
env:
|
|
SERVER_A: ${{ inputs.MM_TEST_SERVER_URL }}
|
|
SERVER_B: ${{ inputs.MM_TEST_SERVER_URL_2 }}
|
|
run: |
|
|
# No targets without a distinct second server; split-tests then fails only if a mutation spec is selected.
|
|
TARGETS=""
|
|
if [ -n "$SERVER_B" ] && [ "$SERVER_B" != "$SERVER_A" ]; then
|
|
TARGETS="B"
|
|
fi
|
|
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
|
|
echo "Mutation targets: ${TARGETS:-none}"
|
|
|
|
- name: Generate Test Specs
|
|
id: generate-specs
|
|
uses: ./.github/actions/generate-specs
|
|
with:
|
|
parallelism: ${{ inputs.parallelism }}
|
|
search_path: ${{ inputs.search_path }}
|
|
device_name: ${{ env.AVD_NAME }}
|
|
device_os_version: ${{ env.SDK_VERSION }}
|
|
durations_file: ${{ inputs.durations_key != '' && 'detox/artifacts/spec-durations.json' || '' }}
|
|
platform: android
|
|
mutation_targets: ${{ steps.mutation-targets.outputs.targets }}
|
|
|
|
e2e-android:
|
|
name: ${{ matrix.role == 'mutation' && 'mutation-' || '' }}machine-${{ matrix.runId }}-api-${{ matrix.deviceOsVersion }}
|
|
runs-on: ubuntu-latest-8-cores
|
|
continue-on-error: true
|
|
timeout-minutes: 150
|
|
env:
|
|
ANDROID_HOME: /usr/local/lib/android/sdk
|
|
ANDROID_SDK_ROOT: /usr/local/lib/android/sdk
|
|
needs:
|
|
- generate-specs
|
|
strategy:
|
|
fail-fast: false
|
|
matrix: ${{ fromJSON(needs.generate-specs.outputs.specs) }}
|
|
steps:
|
|
- name: Checkout Repository
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ inputs.MOBILE_VERSION }}
|
|
|
|
# Pool shards pair server A with the other platform's A; mutation shards own server B alone.
|
|
# SITE_3 is also every pool shard's read-only third server, so site3 mutations must not touch login.
|
|
- name: Assign test sites by shard role
|
|
env:
|
|
ROLE: ${{ matrix.role }}
|
|
TARGET: ${{ matrix.target }}
|
|
SERVER_A: ${{ inputs.MM_TEST_SERVER_URL }}
|
|
SERVER_B: ${{ inputs.MM_TEST_SERVER_URL_2 }}
|
|
SERVER_SHARED: ${{ inputs.MM_TEST_SERVER_URL_3 }}
|
|
OTHER_PLATFORM_POOL: ${{ inputs.MM_OTHER_PLATFORM_POOL_URL }}
|
|
run: |
|
|
case "$ROLE" in
|
|
pool)
|
|
SITE_1_URL="$SERVER_A"; SITE_1_LABEL="server-a"
|
|
if [ -n "$OTHER_PLATFORM_POOL" ]; then
|
|
SITE_2_URL="$OTHER_PLATFORM_POOL"; SITE_2_LABEL="other-platform-a"
|
|
else
|
|
# Never SITE_3: on iOS it is also a mutation target.
|
|
echo "::warning::MM_OTHER_PLATFORM_POOL_URL is empty; pool SITE_2 falls back to this shard's own server A"
|
|
SITE_2_URL="$SERVER_A"; SITE_2_LABEL="server-a"
|
|
fi
|
|
SITE_3_URL="$SERVER_SHARED"; SITE_3_LABEL="site3"
|
|
GUARD="enforce"
|
|
;;
|
|
mutation)
|
|
case "$TARGET" in
|
|
B) SITE_1_URL="$SERVER_B"; SITE_1_LABEL="server-b" ;;
|
|
site3) SITE_1_URL="$SERVER_SHARED"; SITE_1_LABEL="site3" ;;
|
|
A) echo "::error::No isolated server for mutation specs (MM_TEST_SERVER_URL_2 missing or equal to MM_TEST_SERVER_URL); they would change state on the pool server"; exit 1 ;;
|
|
*) echo "::error::Unknown mutation target '$TARGET'"; exit 1 ;;
|
|
esac
|
|
SITE_2_URL="$SERVER_A"; SITE_2_LABEL="server-a"
|
|
SITE_3_URL=""; SITE_3_LABEL="unset"
|
|
GUARD="off"
|
|
;;
|
|
*)
|
|
echo "::error::Unknown shard role '$ROLE'"; exit 1 ;;
|
|
esac
|
|
if [ -z "$SITE_1_URL" ]; then
|
|
echo "::error::SITE_1 is empty for role=$ROLE target=$TARGET"; exit 1
|
|
fi
|
|
{
|
|
echo "SITE_1_URL=$SITE_1_URL"
|
|
echo "SITE_2_URL=$SITE_2_URL"
|
|
echo "SITE_3_URL=$SITE_3_URL"
|
|
echo "E2E_SERVER_ROLE=$ROLE"
|
|
echo "E2E_POOL_GUARD=$GUARD"
|
|
} >> "$GITHUB_ENV"
|
|
echo "role=$ROLE target=$TARGET: SITE_1=$SITE_1_LABEL SITE_2=$SITE_2_LABEL SITE_3=$SITE_3_LABEL pool_guard=$GUARD"
|
|
|
|
# Only libpulse0 is a real dependency here — the Android emulator needs it for
|
|
# audio. xvfb is already in the runner image (apt reports "already the newest
|
|
# version"), and it is listed only so a future image change fails loudly.
|
|
- name: Install Dependencies
|
|
timeout-minutes: 6
|
|
run: |
|
|
APT_OPTS=(-o Acquire::Retries=3 -o Acquire::http::Timeout=15 -o DPkg::Lock::Timeout=120)
|
|
PACKAGES=(libpulse0 xvfb)
|
|
sudo apt-get "${APT_OPTS[@]}" install -y "${PACKAGES[@]}" || {
|
|
sudo apt-get "${APT_OPTS[@]}" update
|
|
sudo apt-get "${APT_OPTS[@]}" install -y "${PACKAGES[@]}"
|
|
}
|
|
|
|
- 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
|
|
|
|
- name: ci/prepare-node-deps
|
|
uses: ./.github/actions/prepare-node-deps
|
|
|
|
- name: Cache detox node_modules
|
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
with:
|
|
path: detox/node_modules
|
|
key: ${{ runner.os }}-detox-deps-${{ hashFiles('detox/package-lock.json') }}
|
|
restore-keys: ${{ runner.os }}-detox-deps-
|
|
|
|
- name: Install Detox Dependencies
|
|
run: |
|
|
cd detox
|
|
npm install
|
|
|
|
- name: Set .env with RUNNING_E2E=true
|
|
run: |
|
|
cat > .env <<EOF
|
|
RUNNING_E2E=true
|
|
EOF
|
|
|
|
# Must precede the download: download-artifact consumes ARTIFACT_RUN_ID and
|
|
# fails with an opaque API error when it is empty.
|
|
- name: Verify artifact_run_id was passed by the parent workflow
|
|
env:
|
|
ARTIFACT_RUN_ID: ${{ env.ARTIFACT_RUN_ID }}
|
|
run: |
|
|
if [ -z "${ARTIFACT_RUN_ID}" ]; then
|
|
echo "❌ ARTIFACT_RUN_ID is empty — parent workflow must pass artifact_run_id from build-android-apk"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Download Detox Android build from parent workflow
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: android-build-files-${{ env.ARTIFACT_RUN_ID }}
|
|
repository: ${{ github.repository }}
|
|
run-id: ${{ env.ARTIFACT_RUN_ID }}
|
|
github-token: ${{ github.token }}
|
|
path: android/app/build
|
|
|
|
- name: Verify Detox Android app from parent build
|
|
run: |
|
|
APP_APK="android/app/build/outputs/apk/debug/app-debug.apk"
|
|
TEST_APK="android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk"
|
|
if [ ! -f "$APP_APK" ]; then
|
|
echo "❌ Missing ${APP_APK} from android-build-files-${ARTIFACT_RUN_ID}"
|
|
find android/app/build -name '*.apk' 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
if [ ! -f "$TEST_APK" ]; then
|
|
echo "❌ Missing ${TEST_APK} from android-build-files-${ARTIFACT_RUN_ID}"
|
|
find android/app/build -name '*.apk' 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
echo "✅ Detox will use Android APKs from parent workflow run ${ARTIFACT_RUN_ID}:"
|
|
ls -lh "$APP_APK" "$TEST_APK"
|
|
env:
|
|
ARTIFACT_RUN_ID: ${{ env.ARTIFACT_RUN_ID }}
|
|
|
|
- name: Set up Android SDK
|
|
run: |
|
|
export ANDROID_HOME=/usr/local/lib/android/sdk
|
|
export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/emulator:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools:$PATH
|
|
echo "ANDROID_HOME=$ANDROID_HOME" >> $GITHUB_ENV
|
|
echo "PATH=$PATH" >> $GITHUB_ENV
|
|
|
|
- name: Start Xvfb
|
|
run: |
|
|
Xvfb :99 -screen 0 1920x1080x24 &
|
|
export DISPLAY=:99
|
|
echo "DISPLAY=:99" >> $GITHUB_ENV
|
|
|
|
- name: Accept Android licenses
|
|
run: |
|
|
mkdir -p ~/.android
|
|
yes | sdkmanager --licenses > sdk_licenses_output.txt || true
|
|
|
|
# Check if "All SDK package licenses accepted" appears in output
|
|
if grep -q "All SDK package licenses accepted" sdk_licenses_output.txt; then
|
|
echo "✅ All licenses accepted successfully."
|
|
else
|
|
echo "❌ Licenses not fully accepted."
|
|
cat sdk_licenses_output.txt
|
|
exit 1
|
|
fi
|
|
|
|
# Cache SDK under the workspace (not /usr/local) — tar restore into system
|
|
# SDK dirs often fails with exit code 2 when paths already exist on the image.
|
|
- name: Cache Android SDK components
|
|
id: android-sdk-cache
|
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
with:
|
|
path: .android-sdk-cache
|
|
key: android-sdk-${{ inputs.android_api_level }}-google-apis-x86_64-v2
|
|
|
|
- name: Apply cached Android SDK components
|
|
if: steps.android-sdk-cache.outputs.cache-hit == 'true'
|
|
env:
|
|
API_LEVEL: ${{ inputs.android_api_level }}
|
|
run: |
|
|
for component in platform-tools emulator; do
|
|
if [ -d ".android-sdk-cache/${component}" ]; then
|
|
rm -rf "${ANDROID_HOME:?}/${component}"
|
|
cp -a ".android-sdk-cache/${component}" "${ANDROID_HOME}/"
|
|
fi
|
|
done
|
|
if [ -d ".android-sdk-cache/platforms/android-${API_LEVEL}" ]; then
|
|
rm -rf "${ANDROID_HOME}/platforms/android-${API_LEVEL}"
|
|
mkdir -p "${ANDROID_HOME}/platforms"
|
|
cp -a ".android-sdk-cache/platforms/android-${API_LEVEL}" "${ANDROID_HOME}/platforms/"
|
|
fi
|
|
if [ -d ".android-sdk-cache/system-images/android-${API_LEVEL}" ]; then
|
|
rm -rf "${ANDROID_HOME}/system-images/android-${API_LEVEL}"
|
|
mkdir -p "${ANDROID_HOME}/system-images"
|
|
cp -a ".android-sdk-cache/system-images/android-${API_LEVEL}" "${ANDROID_HOME}/system-images/"
|
|
fi
|
|
|
|
- name: Install Android SDK components
|
|
if: steps.android-sdk-cache.outputs.cache-hit != 'true'
|
|
env:
|
|
API_LEVEL: ${{ inputs.android_api_level }}
|
|
JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }}
|
|
run: |
|
|
if ! [[ "$API_LEVEL" =~ ^[0-9]+$ ]]; then
|
|
echo "Invalid android_api_level: $API_LEVEL"
|
|
exit 1
|
|
fi
|
|
for attempt in 1 2 3; do
|
|
echo "SDK install attempt $attempt..."
|
|
if yes | sdkmanager --install "platform-tools" "emulator" "platforms;android-${API_LEVEL}" "system-images;android-${API_LEVEL};google_apis;x86_64"; then
|
|
echo "SDK install succeeded on attempt $attempt"
|
|
break
|
|
fi
|
|
echo "Attempt $attempt failed, retrying..."
|
|
if [ "$attempt" = "3" ]; then
|
|
echo "SDK install failed after 3 attempts"
|
|
exit 1
|
|
fi
|
|
sleep 10
|
|
done
|
|
mkdir -p .android-sdk-cache/platforms .android-sdk-cache/system-images
|
|
cp -a "${ANDROID_HOME}/platform-tools" .android-sdk-cache/
|
|
cp -a "${ANDROID_HOME}/emulator" .android-sdk-cache/
|
|
cp -a "${ANDROID_HOME}/platforms/android-${API_LEVEL}" .android-sdk-cache/platforms/
|
|
cp -a "${ANDROID_HOME}/system-images/android-${API_LEVEL}" .android-sdk-cache/system-images/
|
|
|
|
# CI AVDs live under detox/.android/avd (see create_android_emulator.sh), not ~/.android/avd.
|
|
- name: Cache Android Emulator & AVD
|
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
with:
|
|
path: |
|
|
detox/.android
|
|
detox/${{ env.AVD_NAME }}_api_${{ inputs.android_api_level }}
|
|
key: android-emulator-${{ runner.os }}-${{ runner.arch }}-${{ env.AVD_NAME }}-api-${{ inputs.android_api_level }}-v3
|
|
restore-keys: |
|
|
android-emulator-${{ runner.os }}-${{ runner.arch }}-${{ env.AVD_NAME }}-api-${{ inputs.android_api_level }}-
|
|
android-emulator-${{ runner.os }}-${{ runner.arch }}-
|
|
|
|
- name: Start mm_blocks webhook sidecar
|
|
if: contains(matrix.specs, 'mm_blocks_')
|
|
continue-on-error: true
|
|
env:
|
|
SITE_1_URL: ${{ env.SITE_1_URL }}
|
|
WEBHOOK_PUBLIC_BASE_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_PUBLIC_BASE_URL }}
|
|
CLOUDFLARED_TUNNEL_TOKEN: ${{ secrets.MM_MOBILE_E2E_CLOUDFLARED_TUNNEL_TOKEN }}
|
|
run: bash detox/scripts/start_webhook_sidecar.sh
|
|
|
|
- name: Create and run Android Emulator
|
|
env:
|
|
MATRIX_SPECS: ${{ matrix.specs }}
|
|
SITE_1_URL: ${{ env.SITE_1_URL }}
|
|
SITE_2_URL: ${{ env.SITE_2_URL }}
|
|
SITE_3_URL: ${{ env.SITE_3_URL }}
|
|
MAX_ATTEMPTS: "2"
|
|
ATTEMPT_TIMEOUT_MIN: "60"
|
|
shell: bash
|
|
# Attempt 2 retries failed/unrun files only. Cold-boots only if the emulator is dead.
|
|
run: |
|
|
chmod +x detox/scripts/run_android_ci_shard.sh detox/create_android_emulator.sh
|
|
# Split on whitespace into an array, then expand quoted: an unquoted
|
|
# $MATRIX_SPECS is also subject to pathname expansion, so a spec path
|
|
# containing a glob character would silently become a different argument
|
|
# list (or vanish). Matches the iOS template's read -ra handling.
|
|
read -ra specs <<< "$MATRIX_SPECS"
|
|
detox/scripts/run_android_ci_shard.sh "$SDK_VERSION" "$AVD_NAME" "${specs[@]}"
|
|
continue-on-error: true # We want to run all the tests
|
|
|
|
# Second attempt for the same reason as the iOS template: losing this upload to a
|
|
# transient ETIMEDOUT reports the shard as having produced no results at all.
|
|
- name: Upload Android Test Report
|
|
id: upload-android-report
|
|
if: always() && hashFiles('detox/artifacts/**') != ''
|
|
continue-on-error: true
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: android-results-${{ needs.generate-specs.outputs.workflow_hash }}-${{ matrix.runId }}
|
|
path: detox/artifacts/
|
|
|
|
# Same two-retry sequence as the iOS template (see there).
|
|
- name: Await results service
|
|
if: |
|
|
!cancelled() &&
|
|
steps.upload-android-report.outcome == 'failure'
|
|
uses: ./.github/actions/await-results-service
|
|
|
|
- name: Upload Android Test Report (retry 1)
|
|
id: upload-android-report-retry
|
|
if: |
|
|
!cancelled() &&
|
|
steps.upload-android-report.outcome == 'failure'
|
|
continue-on-error: true
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: android-results-${{ needs.generate-specs.outputs.workflow_hash }}-${{ matrix.runId }}
|
|
path: detox/artifacts/
|
|
|
|
- name: Await results service (retry 2)
|
|
if: |
|
|
!cancelled() &&
|
|
steps.upload-android-report.outcome == 'failure' &&
|
|
steps.upload-android-report-retry.outcome == 'failure'
|
|
uses: ./.github/actions/await-results-service
|
|
|
|
- name: Upload Android Test Report (retry 2)
|
|
if: |
|
|
!cancelled() &&
|
|
steps.upload-android-report.outcome == 'failure' &&
|
|
steps.upload-android-report-retry.outcome == 'failure'
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: android-results-${{ needs.generate-specs.outputs.workflow_hash }}-${{ matrix.runId }}
|
|
path: detox/artifacts/
|
|
overwrite: true
|
|
|
|
generate-report:
|
|
# TSIO displays gh-job-name as the shard label — keep it aligned with tsio-shard-name.
|
|
name: ${{ inputs.tsio-shard-name != '' && inputs.tsio-shard-name || 'generate-report' }}
|
|
if: always()
|
|
runs-on: ubuntu-24.04
|
|
needs:
|
|
- generate-specs
|
|
- e2e-android
|
|
permissions:
|
|
contents: read
|
|
# test-system-io-report-upload authenticates via the workflow's OIDC
|
|
# token and resolves gh_job_id via the GitHub API.
|
|
id-token: write
|
|
actions: read
|
|
# Per-job TSIO finalize posts detox-android (or CMT <shard>).
|
|
statuses: write
|
|
# Finalize re-reads the PR labels so a result landing after E2E/Override was
|
|
# applied reports success instead of re-blocking the merge.
|
|
issues: read
|
|
outputs:
|
|
TARGET_URL: ${{ steps.set-url.outputs.TARGET_URL }}
|
|
STATUS: ${{ steps.determine-status.outputs.STATUS }}
|
|
FAILURES: ${{ steps.summary.outputs.FAILURES }}
|
|
PASSES: ${{ steps.summary.outputs.PASSES }}
|
|
TOTAL: ${{ steps.summary.outputs.TOTAL }}
|
|
SKIPPED: ${{ steps.summary.outputs.SKIPPED }}
|
|
ERRORS: ${{ steps.summary.outputs.ERRORS }}
|
|
PERCENTAGE: ${{ steps.summary.outputs.PERCENTAGE }}
|
|
steps:
|
|
- name: Checkout Repository
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ inputs.MOBILE_VERSION }}
|
|
|
|
- name: ci/prepare-node-deps
|
|
uses: ./.github/actions/prepare-node-deps
|
|
|
|
- name: Download Android Artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
path: detox/artifacts/
|
|
pattern: android-results-${{ needs.generate-specs.outputs.workflow_hash }}-*
|
|
continue-on-error: true
|
|
|
|
- name: Generate Report Path
|
|
id: s3
|
|
env:
|
|
BUILD_ID: ${{ needs.generate-specs.outputs.build_id }}
|
|
MOBILE_SHA: ${{ needs.generate-specs.outputs.mobile_sha }}
|
|
MOBILE_REF: ${{ needs.generate-specs.outputs.mobile_ref }}
|
|
run: |
|
|
path="${BUILD_ID}-${MOBILE_SHA}-${MOBILE_REF}"
|
|
echo "path=$(echo "${path}" | sed 's/\./-/g')" >> "$GITHUB_OUTPUT"
|
|
|
|
# ── Spec durations for the next run's shard packing ────────────────────
|
|
# Must run before e2e:save-report, which deletes the android-results-* dirs
|
|
# the per-shard jest-results.json live in. Only main-push publishes (a PR
|
|
# must not steer how other branches shard); PR runs still compute it so the
|
|
# step is exercised on every PR that touches this workflow.
|
|
# Re-fetch first: this is a different runner from generate-specs, so without
|
|
# it the EWMA would have no history and collapse to "last run only".
|
|
- name: Restore previous spec-duration manifest
|
|
if: always() && inputs.durations_key != ''
|
|
continue-on-error: true
|
|
uses: ./.github/actions/s3-spec-durations
|
|
env:
|
|
DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }}
|
|
DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }}
|
|
with:
|
|
key: ${{ inputs.durations_key }}
|
|
bucket: ${{ env.DETOX_AWS_S3_BUCKET }}
|
|
mode: restore
|
|
|
|
- name: Build spec-duration manifest
|
|
if: always() && inputs.durations_key != ''
|
|
continue-on-error: true
|
|
env:
|
|
DURATIONS_KEY: ${{ inputs.durations_key }}
|
|
run: |
|
|
node detox/utils/spec-durations.js \
|
|
--dir detox/artifacts \
|
|
--previous detox/artifacts/spec-durations.json \
|
|
--output detox/artifacts/spec-durations.next.json \
|
|
--key "$DURATIONS_KEY" \
|
|
--repo-root "$GITHUB_WORKSPACE"
|
|
|
|
- name: Publish spec-duration manifest
|
|
if: always() && inputs.durations_key != '' && (inputs.run-type == 'MAIN' || inputs.run-type == 'MASTER') && hashFiles('detox/artifacts/spec-durations.next.json') != ''
|
|
continue-on-error: true
|
|
uses: ./.github/actions/s3-spec-durations
|
|
env:
|
|
DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }}
|
|
DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }}
|
|
with:
|
|
key: ${{ inputs.durations_key }}
|
|
bucket: ${{ env.DETOX_AWS_S3_BUCKET }}
|
|
manifest-path: detox/artifacts/spec-durations.next.json
|
|
mode: save
|
|
|
|
- name: Merge per-shard Jest JSON for TSIO
|
|
if: always() && inputs.tsio-config != '' && vars.TSIO_UPLOAD_ENABLED != 'false'
|
|
id: tsio-merge
|
|
env:
|
|
# Built from repo filenames, so it must reach the shell as data, never spliced
|
|
# into the script text: a spec named x'$(...)'.e2e.ts would otherwise execute.
|
|
EXPECTED_SPECS: ${{ needs.generate-specs.outputs.specs }}
|
|
run: |
|
|
set +e
|
|
# TSIO extractDetox expects testFilePath + nested testResults (not Jest CLI
|
|
# name + assertionResults). Convert while merging shard JSON files.
|
|
if ! node detox/utils/merge-jest-results-for-tsio.js \
|
|
--dir detox/artifacts \
|
|
--repo-root "${GITHUB_WORKSPACE}" \
|
|
--expected-specs "$EXPECTED_SPECS" \
|
|
--output detox/artifacts/jest-results-merged.json; then
|
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
echo "No jest-results.json found or merge failed — skipping TSIO upload."
|
|
exit 0
|
|
fi
|
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Write TSIO failure stub when Detox JSON is missing
|
|
if: always() && inputs.tsio-config != '' && vars.TSIO_UPLOAD_ENABLED != 'false' && steps.tsio-merge.outputs.skip == 'true'
|
|
env:
|
|
TSIO_SHARD_NAME: ${{ inputs.tsio-shard-name != '' && inputs.tsio-shard-name || 'generate-report' }}
|
|
run: |
|
|
node detox/utils/write-tsio-failure-stub.mjs \
|
|
--format jest \
|
|
--output detox/artifacts/jest-results-merged.json \
|
|
--job-name "$TSIO_SHARD_NAME" \
|
|
--reason "Detox job failed or was cancelled before jest-results.json was written"
|
|
|
|
- name: Resolve per-job TSIO config
|
|
if: always() && inputs.tsio-config != '' && vars.TSIO_UPLOAD_ENABLED != 'false'
|
|
id: tsio-cfg
|
|
env:
|
|
TSIO_CONFIG: ${{ inputs.tsio-config }}
|
|
TSIO_SHARD_NAME: ${{ inputs.tsio-shard-name }}
|
|
run: |
|
|
CFG=$(bash detox/utils/resolve-tsio-job-config.sh)
|
|
{
|
|
echo "config<<EOF"
|
|
echo "$CFG"
|
|
echo "EOF"
|
|
} >> "$GITHUB_OUTPUT"
|
|
echo "Resolved TSIO job: $(echo "$CFG" | jq -r '.composite_identity.name') context=$(echo "$CFG" | jq -r '.status_context')"
|
|
|
|
- name: Upload report to Test System IO
|
|
if: always() && inputs.tsio-config != '' && vars.TSIO_UPLOAD_ENABLED != 'false' && hashFiles('detox/artifacts/jest-results-merged.json') != '' && steps.tsio-cfg.outputs.config != ''
|
|
uses: mattermost/mattermost-test-system-io/.github/actions/test-system-io-report-upload@0661accd8cc9ab9dde6ec93f6f5ad40d06deb0d3 # release-0.14.0 / main@0661acc (#93 unique release tags per run)
|
|
with:
|
|
composite-identity: ${{ toJSON(fromJSON(steps.tsio-cfg.outputs.config).composite_identity) }}
|
|
total-reports-expected: ${{ fromJSON(steps.tsio-cfg.outputs.config).total_reports_expected }}
|
|
framework: detox
|
|
github-token: ${{ github.token }}
|
|
gh-job-name: ${{ inputs.tsio-shard-name != '' && inputs.tsio-shard-name || 'generate-report' }}
|
|
json-path: detox/artifacts/jest-results-merged.json
|
|
screenshots-dir: detox/artifacts/
|
|
|
|
# Resolved here rather than inside tsio-report-status.js so the GitHub API
|
|
# call stays in the Actions layer and the reporter acts on its inputs.
|
|
- name: Check E2E/Override on the PR
|
|
id: e2e-override
|
|
if: always() && inputs.tsio-config != '' && vars.TSIO_UPLOAD_ENABLED != 'false' && steps.tsio-cfg.outputs.config != ''
|
|
uses: ./.github/actions/e2e-override-label
|
|
with:
|
|
pr_number: ${{ fromJSON(steps.tsio-cfg.outputs.config).composite_identity.gh_pr_number }}
|
|
|
|
- name: Post per-job TSIO commit status
|
|
if: always() && inputs.tsio-config != '' && vars.TSIO_UPLOAD_ENABLED != 'false' && steps.tsio-cfg.outputs.config != ''
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
TSIO_CONFIG: ${{ steps.tsio-cfg.outputs.config }}
|
|
E2E_OVERRIDE: ${{ steps.e2e-override.outputs.override }}
|
|
SPECS_RESULT: ${{ needs.generate-specs.result }}
|
|
E2E_RESULT: ${{ needs.e2e-android.result }}
|
|
JOB_STATUS: ${{ job.status }}
|
|
MATTERMOST_E2E_WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }}
|
|
MATTERMOST_MASTER_HEALTH_WEBHOOK_URL: ${{ secrets.MM_E2E_MASTER_HEALTH_WEBHOOK_URL }}
|
|
MATTERMOST_CMT_WEBHOOK_URL: ${{ secrets.MM_E2E_RELEASE_WEBHOOK_URL }}
|
|
run: |
|
|
set +e
|
|
UPSTREAM="true"
|
|
# This job runs with always(), so job.status stays success when an upstream
|
|
# job is cancelled — read the needs results to detect that.
|
|
if [ "$SPECS_RESULT" = "cancelled" ] ||
|
|
[ "$E2E_RESULT" = "cancelled" ] ||
|
|
[ "$JOB_STATUS" = "cancelled" ]; then
|
|
UPSTREAM="false"
|
|
fi
|
|
IDENTITY=$(echo "$TSIO_CONFIG" | jq -c '.composite_identity')
|
|
CONTEXT=$(echo "$TSIO_CONFIG" | jq -r '.status_context // "e2e-test/detox-android"')
|
|
TOTAL=$(echo "$TSIO_CONFIG" | jq -r '.total_reports_expected // 1')
|
|
node detox/utils/tsio-report-status.js \
|
|
--composite-identity="$IDENTITY" \
|
|
--total-reports-expected="$TOTAL" \
|
|
--github-token="$GITHUB_TOKEN" \
|
|
--context="$CONTEXT" \
|
|
--upstream-succeeded="$UPSTREAM" \
|
|
--e2e-override="$E2E_OVERRIDE" \
|
|
--poll-attempts=24
|
|
RC=$?
|
|
echo "tsio-report-status exit=$RC upstream_succeeded=$UPSTREAM context=$CONTEXT"
|
|
if [ "$RC" -ne 0 ] && [ "$UPSTREAM" = "true" ]; then
|
|
echo "::error::TSIO reported test failures for $CONTEXT"
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
|
|
- name: Save report Detox Dependencies
|
|
id: report-link
|
|
if: always()
|
|
continue-on-error: true
|
|
run: |
|
|
cd detox
|
|
npm ci
|
|
npm run e2e:save-report
|
|
env:
|
|
DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }}
|
|
DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }}
|
|
BUILD_ID: ${{ needs.generate-specs.outputs.build_id }}
|
|
REPORT_PATH: ${{ steps.s3.outputs.path }}
|
|
## These are needed for the MM Webhook report
|
|
COMMIT_HASH: ${{ needs.generate-specs.outputs.mobile_sha }}
|
|
BRANCH: ${{ needs.generate-specs.outputs.mobile_ref }}
|
|
|
|
- name: Calculate failures
|
|
id: summary
|
|
if: always()
|
|
run: |
|
|
if [ -f detox/artifacts/summary.json ]; then
|
|
echo "FAILURES=$(cat detox/artifacts/summary.json | jq .stats.failures)" >> ${GITHUB_OUTPUT}
|
|
echo "PASSES=$(cat detox/artifacts/summary.json | jq .stats.passes)" >> ${GITHUB_OUTPUT}
|
|
echo "SKIPPED=$(cat detox/artifacts/summary.json | jq .stats.skipped)" >> ${GITHUB_OUTPUT}
|
|
echo "TOTAL=$(cat detox/artifacts/summary.json | jq .stats.tests)" >> ${GITHUB_OUTPUT}
|
|
echo "ERRORS=$(cat detox/artifacts/summary.json | jq .stats.errors)" >> ${GITHUB_OUTPUT}
|
|
echo "PERCENTAGE=$(cat detox/artifacts/summary.json | jq .stats.passPercent)" >> ${GITHUB_OUTPUT}
|
|
else
|
|
echo "⚠️ No summary.json found — all test machines likely failed before producing results"
|
|
echo "FAILURES=-1" >> ${GITHUB_OUTPUT}
|
|
echo "PASSES=0" >> ${GITHUB_OUTPUT}
|
|
echo "SKIPPED=0" >> ${GITHUB_OUTPUT}
|
|
echo "TOTAL=0" >> ${GITHUB_OUTPUT}
|
|
echo "ERRORS=0" >> ${GITHUB_OUTPUT}
|
|
echo "PERCENTAGE=0" >> ${GITHUB_OUTPUT}
|
|
fi
|
|
|
|
- name: Set Target URL
|
|
id: set-url
|
|
if: always()
|
|
env:
|
|
S3_BUCKET: ${{ env.DETOX_AWS_S3_BUCKET }}
|
|
REPORT_PATH: ${{ steps.s3.outputs.path }}
|
|
run: |
|
|
echo "TARGET_URL=https://${S3_BUCKET}.s3.amazonaws.com/${REPORT_PATH}/jest-stare/android-report.html" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Determine Status
|
|
id: determine-status
|
|
if: always()
|
|
env:
|
|
FAILURES: ${{ steps.summary.outputs.FAILURES }}
|
|
ERRORS: ${{ steps.summary.outputs.ERRORS }}
|
|
TOTAL: ${{ steps.summary.outputs.TOTAL }}
|
|
run: |
|
|
# Fail on missing/empty reports (infra died before tests) and on real failures.
|
|
if [[ "$FAILURES" == "-1" || "$FAILURES" -gt 0 || "${ERRORS:-0}" -gt 0 || "${TOTAL:-0}" -eq 0 ]]; then
|
|
echo "STATUS=failure" >> ${GITHUB_OUTPUT}
|
|
else
|
|
echo "STATUS=success" >> ${GITHUB_OUTPUT}
|
|
fi
|
|
|
|
- name: Generate Summary
|
|
if: always()
|
|
env:
|
|
TOTAL: ${{ steps.summary.outputs.TOTAL }}
|
|
PASSES: ${{ steps.summary.outputs.PASSES }}
|
|
FAILURES: ${{ steps.summary.outputs.FAILURES }}
|
|
SKIPPED: ${{ steps.summary.outputs.SKIPPED }}
|
|
ERRORS: ${{ steps.summary.outputs.ERRORS }}
|
|
PERCENTAGE: ${{ steps.summary.outputs.PERCENTAGE }}
|
|
TARGET_URL: ${{ steps.set-url.outputs.TARGET_URL }}
|
|
run: |
|
|
echo "| Tests | Passed :white_check_mark: | Failed :x: | Skipped :fast_forward: | Errors :warning: | " >> ${GITHUB_STEP_SUMMARY}
|
|
echo "|:---:|:---:|:---:|:---:|:---:|" >> ${GITHUB_STEP_SUMMARY}
|
|
echo "| ${TOTAL} | ${PASSES} | ${FAILURES} | ${SKIPPED} | ${ERRORS} |" >> ${GITHUB_STEP_SUMMARY}
|
|
echo "" >> ${GITHUB_STEP_SUMMARY}
|
|
echo "You can check the full report [here](${TARGET_URL})" >> ${GITHUB_STEP_SUMMARY}
|
|
echo "There was **${PERCENTAGE}%** success rate." >> ${GITHUB_STEP_SUMMARY}
|
|
|
|
- name: Fail job on test failures
|
|
if: always() && steps.determine-status.outputs.STATUS == 'failure'
|
|
env:
|
|
FAILURES: ${{ steps.summary.outputs.FAILURES }}
|
|
TOTAL: ${{ steps.summary.outputs.TOTAL }}
|
|
ERRORS: ${{ steps.summary.outputs.ERRORS }}
|
|
run: |
|
|
echo "::error::Detox reported test failures (FAILURES=${FAILURES}, TOTAL=${TOTAL}, ERRORS=${ERRORS})"
|
|
exit 1
|