mattermost-mobile-upstream/.github/workflows/e2e-maestro-template.yml
yasser khan 3e1331248c
Some checks are pending
ci / lint-typecheck (push) Waiting to run
ci / detox-lint-typecheck (push) Waiting to run
ci / test (push) Waiting to run
ci(e2e): say whether each main-run failure is new, still failing, flaky or a setup problem (#10284)
* ci(e2e): say whether each main-run failure is new, still failing, flaky or a setup problem, from the triage verdict

The channel message judged failures from their error text, beside the E2E triage
jobs that judge the same failures from TSIO history, and the two disagreed: on
main run 40764686027 it listed tests that passed on retry as needing a look. The
message now waits for triage and reads each run's findings. On main, a test that
passed every earlier main run and fails now is new, with the changes merged since
the last main run named (TSIO's previous run, GitHub's compare); a run where most
tests failed at once is called out first. Runs with no triage verdict keep the
error-text judgement.

* ci(e2e): judge from the error text each failed test triage named no verdict for, and say how many were not checked

* ci(e2e): count each test once per run, so a test that failed and then passed its retry is flaky, not a failure

* ci(e2e): a main-run failure triage calls flaky or a test problem is not blamed on new changes; a same-commit re-run says nothing was merged; a long PR list says it is partial

* Fix TSIO current report selection

Co-authored-by: yasserfaraazkhan <15979783+yasserfaraazkhan@users.noreply.github.com>

* Fix lint comment spacing

Co-authored-by: yasserfaraazkhan <15979783+yasserfaraazkhan@users.noreply.github.com>

* ci(e2e): update the TSIO upload action, so a failed screenshot upload no longer drops a shard's results

* ci(e2e): an earlier attempt of the same main run is not the previous run, so a re-run still lists the changes merged since the last run

* ci(e2e): drop the note from the TSIO upload action pin comment

* ci(e2e): a re-run replaces the first attempt's triage findings, so the channel message never posts stale verdicts

* ci(e2e): the main-run message says in a few lines which failures may be product bugs and which are not, and posts nothing when none may be

* ci(e2e): main and CMT call a failure a product bug only when it fails again on a fresh device, and clear what the error text proves is not the app

* ci(e2e): a re-run hands Android Detox and Maestro results to the channel message too

* ci(e2e): CMT re-runs start after a failed lane; a spec that differs from main, or a feature the server has off, is not a product bug

* ci(e2e): the CMT message plans the re-run as the planner job does, leaving out tests that differ from main

* ci(e2e): one channel post per run, and a test's name can never mention the channel

* ci(e2e): a re-run keeps the run's own report and runs only the failed flows

* ci(e2e): a PR run posts to the channel only when something failed

* ci(e2e): an E2E/Override PR keeps its statuses on a new push; a few simpler lines in the channel notify

* ci(e2e): exclude isolated report-problem flows from reruns and prevent label/action concurrency collisions

<!-- coderabbit-ai-generated:v1 -->

* ci(e2e): pass support paths to E2E triage

* ci(e2e): pin TSIO report-upload to release-0.16.0 (main@3212731)

Brings the TSIO action pin up to the latest release, which includes
mattermost-test-system-io#129, #130 and #124. The report-upload action itself
is unchanged between the two pins.

* ci(e2e): re-run E2E jobs once when a lane is red only on infrastructure

Triage (rerun-attempts: 1, with actions: read to read the failed jobs) asks for
one re-run when a lane is red only because jobs died or reports never arrived;
a build or test-server failure holds it. e2e-tests-rerun.yml carries the re-run
out once the run finishes. CMT keeps its own re-run of failed specs.

---------
2026-11-03 07:45:59 +05:30

1561 lines
77 KiB
YAML

name: Maestro E2E Tests Template
on:
workflow_call:
inputs:
platform:
description: "Platform to test on: ios or android"
required: false
type: string
default: "ios"
flow-path:
description: "Path to the Maestro flow(s) to run (defaults differ by platform)"
required: false
type: string
default: ""
MM_TEST_SERVER_URL:
description: "The test server URL"
required: false
type: string
MOBILE_VERSION:
description: "The mobile version to test"
required: false
default: ""
type: string
run-type:
description: "Type of run (PR, MAIN, etc.)"
required: false
type: string
default: "PR"
artifact_run_id:
description: "GitHub run ID of the workflow that uploaded ios-build-simulator-* (defaults to this run)"
required: false
type: string
default: ""
results-artifact:
description: >
Artifact name for this run's Maestro JUnit report: a re-run of failed flows (flow-path),
with an empty tsio-config and run-type NONE, reports only to the channel message.
required: false
type: string
default: ""
record_tests_in_zephyr:
description: "Record Maestro test results in Zephyr Scale (release/main runs)"
required: false
type: boolean
default: false
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. maestro-ios-Server_11.9.0-rc3)"
required: false
type: string
default: ""
secrets:
MM_MOBILE_E2E_ADMIN_EMAIL:
required: true
MM_MOBILE_E2E_ADMIN_USERNAME:
required: true
MM_MOBILE_E2E_ADMIN_PASSWORD:
required: true
MM_MOBILE_E2E_ZEPHYR_API_KEY:
required: false
MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID:
required: false
MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY:
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_MOBILE_E2E_WEBHOOK_URL:
required: false
MM_E2E_MASTER_HEALTH_WEBHOOK_URL:
required: false
MM_E2E_RELEASE_WEBHOOK_URL:
required: false
outputs:
STATUS:
value: ${{ jobs.e2e-maestro-ios.outputs.STATUS || jobs.e2e-maestro-android.outputs.STATUS }}
TARGET_URL:
value: ${{ jobs.e2e-maestro-ios.outputs.TARGET_URL || jobs.e2e-maestro-android.outputs.TARGET_URL }}
FAILURES:
value: ${{ jobs.e2e-maestro-ios.outputs.FAILURES || jobs.e2e-maestro-android.outputs.FAILURES }}
PASSES:
value: ${{ jobs.e2e-maestro-ios.outputs.PASSES || jobs.e2e-maestro-android.outputs.PASSES }}
TOTAL:
value: ${{ jobs.e2e-maestro-ios.outputs.TOTAL || jobs.e2e-maestro-android.outputs.TOTAL }}
SKIPPED:
value: ${{ jobs.e2e-maestro-ios.outputs.SKIPPED || jobs.e2e-maestro-android.outputs.SKIPPED }}
PERCENTAGE:
value: ${{ jobs.e2e-maestro-ios.outputs.PERCENTAGE || jobs.e2e-maestro-android.outputs.PERCENTAGE }}
CANCELLED:
value: ${{ jobs.e2e-maestro-ios.outputs.CANCELLED || jobs.e2e-maestro-android.outputs.CANCELLED }}
# Least-privilege token: maestro jobs only check out code and talk to the test
# server / AWS via dedicated secrets — they never use GITHUB_TOKEN for writes.
# actions: read is required to download build artifacts from the parent workflow run.
permissions:
contents: read
actions: read
# test-system-io-report-upload authenticates via the workflow's OIDC token
# and resolves gh_job_id via the GitHub API; reusable workflows cannot
# escalate beyond the caller, so this must be granted at every level.
id-token: write
# Per-job TSIO finalize posts <platform> commit statuses (detox-/maestro-*).
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
env:
ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }}
ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }}
ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }}
SITE_1_URL: ${{ inputs.MM_TEST_SERVER_URL }}
TYPE: ${{ inputs.run-type }}
DETOX_AWS_S3_BUCKET: "mattermost-detox-report"
AWS_REGION: "us-east-1"
# iOS .app artifact is uploaded by e2e-detox-pr.yml under ios-build-simulator-<run_id>.
# ios-build-simulator-<run_id>. When this template is nested, that run_id is
# the parent's, not this workflow's.
ARTIFACT_RUN_ID: ${{ inputs.artifact_run_id || github.run_id }}
# Keep Maestro device targets identical to detox/.detoxrc.json and e2e-*-template.yml.
E2E_IOS_DEVICE_NAME: iPhone 17 Pro
E2E_IOS_DEVICE_OS_VERSION: iOS 26.2
E2E_ANDROID_AVD_NAME: detox_pixel_8
E2E_ANDROID_API_LEVEL: "35"
RUNNING_E2E: "true"
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"
BRANCH: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}
COMMIT_HASH: ${{ inputs.MOBILE_VERSION || github.sha }}
jobs:
e2e-maestro-ios:
# TSIO shard label = job name; prefer tsio-shard-name (e.g. maestro-ios-e2e).
name: ${{ inputs.tsio-shard-name != '' && inputs.tsio-shard-name || 'Maestro E2E Tests (iOS)' }}
if: ${{ inputs.platform == 'ios' || inputs.platform == '' }}
runs-on: macos-26
timeout-minutes: 90
outputs:
STATUS: ${{ steps.determine-status.outputs.STATUS }}
TARGET_URL: ${{ steps.set-url.outputs.TARGET_URL }}
FAILURES: ${{ steps.summary.outputs.FAILURES }}
PASSES: ${{ steps.summary.outputs.PASSES }}
TOTAL: ${{ steps.summary.outputs.TOTAL }}
SKIPPED: ${{ steps.summary.outputs.SKIPPED }}
PERCENTAGE: ${{ steps.summary.outputs.PERCENTAGE }}
ERRORS: ${{ steps.summary.outputs.ERRORS }}
CANCELLED: ${{ steps.shard-cancelled.outputs.CANCELLED }}
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: Install Detox Dependencies
run: cd detox && npm ci --prefer-offline --no-audit --no-fund
- name: Cache Maestro
id: maestro-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.maestro
key: maestro-c8c361be1cc89b2e742f185466a302a22778be6b-macos
- name: Install Maestro CLI
if: steps.maestro-cache.outputs.cache-hit != 'true'
run: |
MAESTRO_VERSION=$(jq -r .version detox/maestro/maestro-version.json)
MAESTRO_COMMIT=$(jq -r .commit detox/maestro/maestro-version.json)
echo "Installing Maestro ${MAESTRO_VERSION} (pinned to commit ${MAESTRO_COMMIT})"
export MAESTRO_VERSION
curl -fsSL "https://get.maestro.mobile.dev" | bash
MAESTRO_BIN="$HOME/.maestro/bin/maestro"
if [ ! -f "$MAESTRO_BIN" ]; then
echo "Error: Maestro binary not found at $MAESTRO_BIN"
exit 1
fi
echo "Maestro installed successfully at $MAESTRO_BIN"
"$MAESTRO_BIN" --version
- name: Verify Maestro installation
run: ~/.maestro/bin/maestro --version
- name: Download iOS Simulator Build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ios-build-simulator-${{ env.ARTIFACT_RUN_ID }}
# Reusable workflows have their own github.run_id; when Maestro is
# Reusable workflows have their own github.run_id; artifacts live on the parent e2e-detox-pr.yml run.
run-id: ${{ env.ARTIFACT_RUN_ID }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: mobile-artifacts
- name: Unzip iOS Simulator Build
run: |
unzip -o mobile-artifacts/*.zip -d mobile-artifacts/
rm -f mobile-artifacts/*.zip
- name: Patch iOS app Info.plist for file-sharing flows
run: |
APP_PATH=$(find mobile-artifacts -name "*.app" -maxdepth 2 | head -1)
if [ -z "$APP_PATH" ]; then
echo "Error: Could not find .app bundle in mobile-artifacts/"
ls -la mobile-artifacts/
exit 1
fi
plutil -replace UIFileSharingEnabled -bool true "$APP_PATH/Info.plist" 2>/dev/null || \
plutil -insert UIFileSharingEnabled -bool true "$APP_PATH/Info.plist" 2>/dev/null || true
plutil -replace LSSupportsOpeningDocumentsInPlace -bool true "$APP_PATH/Info.plist" 2>/dev/null || \
plutil -insert LSSupportsOpeningDocumentsInPlace -bool true "$APP_PATH/Info.plist" 2>/dev/null || true
echo "UIFileSharingEnabled patched in $APP_PATH/Info.plist"
- name: Pre-boot iOS Simulator (Detox-compatible)
env:
DEVICE_NAME: ${{ env.E2E_IOS_DEVICE_NAME }}
DEVICE_OS_VERSION: ${{ env.E2E_IOS_DEVICE_OS_VERSION }}
PREBOOT_SKIP_PREWARM: "1"
run: bash detox/scripts/preboot_ios_simulator.sh
# Old inline Maestro boot waited for SpringBoard via listapps before tests.
- name: Wait for Simulator UI readiness
run: |
for i in $(seq 1 30); do
xcrun simctl listapps "$SIMULATOR_ID" &>/dev/null && exit 0
sleep 2
done
echo "Warning: listapps did not succeed within 60s"
- name: Place test fixture file in app Documents (channel_bookmark_file flow)
run: |
FIXTURE="detox/e2e/support/fixtures/image.png"
if [ ! -f "$FIXTURE" ]; then
echo "Fixture file not found at $FIXTURE — skipping document setup"
exit 0
fi
APP_DATA=$(xcrun simctl get_app_container "$SIMULATOR_ID" com.mattermost.rnbeta data 2>/dev/null || true)
if [ -z "$APP_DATA" ]; then
echo "App data container not found — app may not be installed yet; skipping"
exit 0
fi
DOCS_DIR="$APP_DATA/Documents"
mkdir -p "$DOCS_DIR"
cp "$FIXTURE" "$DOCS_DIR/test_bookmark.png"
echo "Copied test fixture to $DOCS_DIR/test_bookmark.png"
- name: Set simulator timezone for timezone flow
run: |
# MM-T1325: set timezone before any flow runs; launchApp:stopApp:true in the flow
# will restart the app so it picks up the new timezone.
# 'xcrun simctl timezone' was removed in Xcode 26; 'defaults write' doesn't affect
# [NSTimeZone localTimeZone] either. 'launchctl setenv TZ' sets it in launchd so
# all new child processes (including the restarted app) inherit the correct timezone.
xcrun simctl spawn "$SIMULATOR_ID" launchctl setenv TZ "America/New_York"
echo "Simulator timezone set to America/New_York"
- name: Seed test data
run: |
cd detox/maestro
npm ci --prefer-offline --no-audit --no-fund
npx tsx fixtures/seed.ts
env:
SITE_1_URL: ${{ env.SITE_1_URL }}
ADMIN_EMAIL: ${{ env.ADMIN_EMAIL }}
ADMIN_USERNAME: ${{ env.ADMIN_USERNAME }}
ADMIN_PASSWORD: ${{ env.ADMIN_PASSWORD }}
- name: Push test fixture into iOS simulator
continue-on-error: true
run: |
set +e
UDID=$(xcrun simctl list devices booted 2>/dev/null | grep -oE '[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}' | head -1)
if [ -z "$UDID" ]; then
echo "Warning: no booted iOS simulator found; skipping fixture push"
exit 0
fi
echo "Resolved booted simulator UDID: $UDID"
LOCAL_STORAGE=$(find "$HOME/Library/Developer/CoreSimulator/Devices/$UDID/data/Containers/Shared/AppGroup" \
-name ".com.apple.mobile_container_manager.metadata.plist" \
-exec grep -l "FileProvider.LocalStorage" {} \; 2>/dev/null | xargs -I{} dirname {} | head -1)
if [ -n "$LOCAL_STORAGE" ]; then
mkdir -p "$LOCAL_STORAGE/File Provider Storage"
cp detox/e2e/support/fixtures/image.png "$LOCAL_STORAGE/File Provider Storage/test_bookmark.png"
echo "Pushed test fixture to $LOCAL_STORAGE/File Provider Storage/test_bookmark.png"
else
echo "Warning: FileProvider.LocalStorage container not found; channel_bookmark_file.yml may fail"
fi
- name: Start iOS Simulator device log capture
if: always()
run: |
mkdir -p build
# Filter sim log to Mattermost/WebRTC processes only.
xcrun simctl spawn "$SIMULATOR_ID" log stream \
--level=debug \
--predicate 'process == "Mattermost" OR process == "MattermostRN" OR senderImagePath CONTAINS "WebRTC" OR senderImagePath CONTAINS "calls-native"' \
> build/sim-device.log 2> build/sim-device.err &
echo $! > build/sim-device.pid
echo "==> Device log capture started (pid $(cat build/sim-device.pid))"
- name: Pre-flight — simulator can reach test server
id: maestro-ios-preflight
env:
MAESTRO_DRIVER_STARTUP_TIMEOUT: "180000"
run: |
set -a
source detox/maestro/.maestro-test-env.sh
set +a
for attempt in 1 2 3; do
if "$HOME/.maestro/bin/maestro" --device "$SIMULATOR_ID" test \
--env "SITE_1_URL=$SITE_1_URL" \
--env "MAESTRO_APP_ID=${MAESTRO_APP_ID:-com.mattermost.rnbeta}" \
detox/maestro/preflight/_connect_check.yml; then
echo "==> Pre-flight connect OK (attempt $attempt)"
exit 0
fi
echo "==> Pre-flight connect failed (attempt $attempt/3)"
done
echo "::error::Simulator could not connect to $SITE_1_URL after 3 attempts. This is an environment failure (TLS/DNS/server), not a test failure — check build/sim-device.log for NSURLErrorDomain -1200 or 'Cancelled during verify block' before touching any flow."
exit 1
- name: Run Maestro Flows (iOS)
# CI 33936010053: this step ran 64m then the runner died (completed_at
# null, logs BlobNotFound). always() TSIO stub/upload never ran, so
# e2e-test/maestro-ios stayed pending. Cap the step so those steps run.
timeout-minutes: 50
continue-on-error: true
env:
FLOW_PATH: ${{ inputs.flow-path }}
E2E_IOS_DEVICE_NAME: ${{ env.E2E_IOS_DEVICE_NAME }}
E2E_IOS_DEVICE_OS_VERSION: ${{ env.E2E_IOS_DEVICE_OS_VERSION }}
MAESTRO_DRIVER_STARTUP_TIMEOUT: "180000"
run: |
if [ -n "${FLOW_PATH:-}" ] && ! printf '%s' "$FLOW_PATH" | grep -Eq '^[A-Za-z0-9_./ -]+$'; then
echo "Invalid flow-path input: $FLOW_PATH"
exit 1
fi
set -a
source detox/maestro/.maestro-test-env.sh
set +a
# MM-T3261 (report_problem_action.yml) requires the link variant: the
# default paid-tier path opens the native mail composer, which cannot
# open on iOS simulators (no Mail accounts) — the app then shows an
# error alert and the report screen never dismisses. Patch it to the
# link variant (report URL opens in the browser) and always restore.
trap 'curl -f -sS --show-error -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"SupportSettings\":{\"ReportAProblemType\":\"default\"}}" \
"${SITE_1_URL}/api/v4/config/patch" > /dev/null && \
echo "==> Restored ReportAProblemType=default"' EXIT
echo "==> Patching SupportSettings.ReportAProblemType=link on ${SITE_1_URL}"
curl -f -sS --show-error -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"SupportSettings":{"ReportAProblemType":"link"}}' \
"${SITE_1_URL}/api/v4/config/patch" > /dev/null
chmod +x detox/maestro/scripts/run_ci_batches.sh
detox/maestro/scripts/run_ci_batches.sh --platform ios --device "$SIMULATOR_ID"
- name: Stop iOS Simulator device log capture
if: always()
run: |
if [ -f build/sim-device.pid ]; then
pid=$(cat build/sim-device.pid)
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
echo "==> Device log capture stopped (pid $pid)"
fi
# Print log tail for quick failure scans without downloading the artifact.
if [ -f build/sim-device.log ]; then
sz=$(wc -c < build/sim-device.log)
echo "==> build/sim-device.log size=${sz}B"
echo "==> Last 100 lines of sim-device.log:"
tail -100 build/sim-device.log || true
echo "==> Lines matching calls/webrtc/peer/timed-out:"
grep -iE 'calls:|RTCPeer|WebRTC|peer connect|unable to get media|wired ?headset|timed out waiting' build/sim-device.log | head -100 || true
fi
- name: Run MM-T67856_4 with AllowDownloadLogs=false (iOS)
# A re-run of failed flows runs only those (flow-path).
if: always() && inputs.results-artifact == '' && hashFiles('detox/maestro/.maestro-test-env.sh') != '' && steps.maestro-ios-preflight.conclusion == 'success'
continue-on-error: true
run: |
set -a
source detox/maestro/.maestro-test-env.sh
set +a
# The flow flips SupportSettings.AllowDownloadLogs=false in its own
# onFlowStart hook and restores true in onFlowComplete
# (detox/maestro/fixtures/set_allow_download_logs.js), so this step
# only runs it; a local `maestro test` of the same file does the same.
# The always() restore step below stays as the safety net for runner
# hard-kills, which never reach onFlowComplete.
# Confirm the client config actually serves false instead of assuming a 200
# means the write took: an installation that supplies this setting through an
# environment variable answers 200 and keeps its own value, and the flow then
# asserts against a toggle the server never turned off. Same treatment
# ReportAProblemType already gets below.
echo "==> Patching SupportSettings.AllowDownloadLogs=false on ${SITE_1_URL}"
CONFIG_DIAGNOSIS_FILE="$RUNNER_TEMP/config-diagnosis.txt" bash detox/maestro/scripts/apply_client_config_value.sh \
"${SITE_1_URL}" "$ADMIN_TOKEN" '{"SupportSettings":{"AllowDownloadLogs":false}}' \
AllowDownloadLogs false || {
rc=$?
# 3 = the installation forbids the write, so the flow's pre-condition cannot
# be created here; record a skip rather than a red. Anything else is real.
if [ "$rc" -eq 3 ]; then
bash detox/maestro/scripts/write_maestro_skip_stub.sh \
build/maestro-report-T67856_4.xml \
detox/maestro/flows/account/attach_logs_disabled_when_download_logs_off.yml \
"AllowDownloadLogs=false is not writable on this server (set by environment variable, patch silently ignored); flow cannot run here"
exit 0
fi
bash detox/maestro/scripts/write_maestro_failure_stub.sh \
build/maestro-report-T67856_4.xml \
detox/maestro/flows/account/attach_logs_disabled_when_download_logs_off.yml \
"AllowDownloadLogs=false never reached the client config; the flow's pre-condition was not in force: $(cat "$RUNNER_TEMP/config-diagnosis.txt" 2>/dev/null)"
exit 1
}
bash detox/maestro/scripts/reset_ios_app.sh "$SIMULATOR_ID" com.mattermost.rnbeta || {
bash detox/maestro/scripts/write_maestro_failure_stub.sh \
build/maestro-report-T67856_4.xml \
detox/maestro/flows/account/attach_logs_disabled_when_download_logs_off.yml \
"The app could not be reinstalled for a clean state before the flow"
exit 1
}
~/.maestro/bin/maestro test \
--device "$SIMULATOR_ID" \
--env MAESTRO_APP_RESET_BY_RUNNER=true \
--format junit \
--output build/maestro-report-T67856_4.xml \
--test-output-dir build/maestro-artifacts-T67856_4 \
--debug-output build/maestro-debug-T67856_4 \
--flatten-debug-output \
--env SITE_1_URL="${SITE_1_URL}" \
--env TEST_USER_EMAIL="${TEST_USER_EMAIL}" \
--env TEST_USER_PASSWORD="${TEST_USER_PASSWORD}" \
--env TEST_CHANNEL_NAME="${TEST_CHANNEL_NAME}" \
--env TEST_TEAM_NAME="${TEST_TEAM_NAME}" \
--env ADMIN_TOKEN="${ADMIN_TOKEN}" \
--env MAESTRO_APP_ID="com.mattermost.rnbeta" \
detox/maestro/flows/account/attach_logs_disabled_when_download_logs_off.yml
- name: Run MM-T3261_1 with ReportAProblemType=link (iOS)
# Isolated step: patch ReportAProblemType=link plus the report link, run the
# link-mode flow, restore the default on exit. The flow has no onFlowStart hook
# of its own, so the patch and the trap restore live here. continue-on-error so
# the main batch report stays authoritative.
# A re-run of failed flows runs only those (flow-path).
if: always() && inputs.results-artifact == '' && hashFiles('detox/maestro/.maestro-test-env.sh') != '' && steps.maestro-ios-preflight.conclusion == 'success'
continue-on-error: true
run: |
set -a
source detox/maestro/.maestro-test-env.sh
set +a
trap 'curl -f -sS --show-error -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"SupportSettings\":{\"ReportAProblemType\":\"default\"}}" \
"${SITE_1_URL}/api/v4/config/patch" > /dev/null && \
echo "==> Restored ReportAProblemType=default"' EXIT
# Patch and then confirm the client config actually serves it. Two config writes
# close together can lose the later one: in run 34344304929 the mail step's trap
# restored 'default' 58 ms before this step patched, the app observed 'default',
# and handleReport() emailed instead of opening the browser.
echo "==> Patching SupportSettings.ReportAProblemType=link on ${SITE_1_URL}"
CONFIG_DIAGNOSIS_FILE="$RUNNER_TEMP/config-diagnosis.txt" bash detox/maestro/scripts/apply_client_config_value.sh \
"${SITE_1_URL}" "$ADMIN_TOKEN" '{"SupportSettings":{"ReportAProblemType":"link","ReportAProblemLink":"https://mattermost.com/pl/report_a_problem_licensed"}}' \
ReportAProblemType link || {
rc=$?
# 3 = this installation forbids the write (run 34452126763 proved it is set by an
# environment variable, so config/patch is silently ignored); the flow's
# pre-condition cannot be created here, so record a skip rather than a red.
# Anything else is a real failure and stays red.
if [ "$rc" -eq 3 ]; then
bash detox/maestro/scripts/write_maestro_skip_stub.sh \
build/maestro-report-T3261_1.xml \
detox/maestro/flows/account/report_problem_link_mode.yml \
"ReportAProblemType=link is not writable on this server (set by environment variable, patch silently ignored); flow cannot run here"
exit 0
fi
bash detox/maestro/scripts/write_maestro_failure_stub.sh \
build/maestro-report-T3261_1.xml \
detox/maestro/flows/account/report_problem_link_mode.yml \
"ReportAProblemType=link never reached the client config; the flow's pre-condition was not in force: $(cat "$RUNNER_TEMP/config-diagnosis.txt" 2>/dev/null)"
exit 1
}
bash detox/maestro/scripts/reset_ios_app.sh "$SIMULATOR_ID" com.mattermost.rnbeta || {
bash detox/maestro/scripts/write_maestro_failure_stub.sh \
build/maestro-report-T3261_1.xml \
detox/maestro/flows/account/report_problem_link_mode.yml \
"The app could not be reinstalled for a clean state before the flow"
exit 1
}
~/.maestro/bin/maestro test \
--device "$SIMULATOR_ID" \
--env MAESTRO_APP_RESET_BY_RUNNER=true \
--format junit \
--output build/maestro-report-T3261_1.xml \
--test-output-dir build/maestro-artifacts-T3261_1 \
--debug-output build/maestro-debug-T3261_1 \
--flatten-debug-output \
--env SITE_1_URL="${SITE_1_URL}" \
--env TEST_USER_EMAIL="${TEST_USER_EMAIL}" \
--env TEST_USER_PASSWORD="${TEST_USER_PASSWORD}" \
--env TEST_CHANNEL_NAME="${TEST_CHANNEL_NAME}" \
--env TEST_TEAM_NAME="${TEST_TEAM_NAME}" \
--env ADMIN_TOKEN="${ADMIN_TOKEN}" \
--env MAESTRO_APP_ID="com.mattermost.rnbeta" \
detox/maestro/flows/account/report_problem_link_mode.yml
- name: Restore AllowDownloadLogs baseline (self-healing)
if: always() && hashFiles('detox/maestro/.maestro-test-env.sh') != ''
continue-on-error: true
run: |
set -a
source detox/maestro/.maestro-test-env.sh
set +a
# # Standalone restore independent of the dedicated T67856_4 step:
# its in-step EXIT trap does not fire on runner hard-kills and is
# skipped entirely when the step is skipped, while transient restore
# failures are masked by continue-on-error. Setting true when already
# true is a harmless no-op, which keeps this safe when multiple jobs
# share one server and a concurrent job re-flipped the flag.
echo "==> Restoring SupportSettings.AllowDownloadLogs=true on ${SITE_1_URL}"
# Not curl --retry: a timed-out patch is still applied, and resending queues more saves behind it.
bash detox/maestro/scripts/apply_client_config_value.sh \
"${SITE_1_URL}" "$ADMIN_TOKEN" '{"SupportSettings":{"AllowDownloadLogs":true}}' \
AllowDownloadLogs true && \
echo "==> Restored AllowDownloadLogs=true"
- name: Upload Maestro Artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
# A re-run of failed flows runs in the same workflow run: its own name.
name: maestro-ios-results-${{ github.run_id }}${{ inputs.results-artifact != '' && '-rerun' || '' }}
path: build/
- name: Parse Maestro Report
id: summary
if: always()
run: |
cd detox
npm ci --prefer-offline --no-audit --no-fund
node -e "
const path = require('path');
const {parseMaestroReport, generateMaestroHtmlReport, mergeMaestroBatchReportsFromDir, prepareMaestroScreenshotsForTsio} = require('./utils/maestro_report');
const buildDir = path.resolve('..', 'build');
const xmlPath = path.join(buildDir, 'maestro-report.xml');
mergeMaestroBatchReportsFromDir(buildDir, xmlPath);
const summary = parseMaestroReport(xmlPath);
const fs = require('fs');
if (summary) {
const s = summary.stats;
const output = [
'FAILURES=' + s.failures,
'PASSES=' + s.passes,
'TOTAL=' + s.tests,
'ERRORS=' + s.errors,
'SKIPPED=' + s.skipped,
'PERCENTAGE=' + s.passPercent,
];
output.forEach(l => fs.appendFileSync(process.env.GITHUB_OUTPUT, l + '\n'));
// Generate a rich HTML report (stats + per-flow drill-down with
// inline screenshots + log links). Lives next to maestro-artifacts/
// so relative <img src> URLs resolve when viewed from S3.
generateMaestroHtmlReport({
xmlPath: xmlPath,
artifactsDir: path.join(buildDir, 'maestro-artifacts'),
outputPath: path.join(buildDir, 'maestro-report.html'),
platform: 'iOS',
commitSha: process.env.GITHUB_SHA || '',
});
// Per-flow folders so TSIO LinkScreenshots can match full_title.
prepareMaestroScreenshotsForTsio({
xmlPath: xmlPath,
artifactsDir: path.join(buildDir, 'maestro-artifacts'),
outputDir: path.join(buildDir, 'maestro-artifacts-tsio'),
repoRoot: path.resolve('..'),
});
} else {
console.log('No Maestro report found — marking as failure');
['FAILURES=1','PASSES=0','TOTAL=0','ERRORS=1','SKIPPED=0','PERCENTAGE=0','NO_REPORT=true'].forEach(l => fs.appendFileSync(process.env.GITHUB_OUTPUT, l + '\n'));
}
"
# Evidence for the triage job in e2e-detox-pr.yml: each failed flow's failure
# screenshot, the step it stopped at and the errors the app logged meanwhile.
# A re-run of failed flows hands its per-flow results to the channel message.
- name: Upload re-run results
if: always() && inputs.results-artifact != '' && hashFiles('build/maestro-report.xml') != ''
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.results-artifact }}
path: build/maestro-report.xml
retention-days: 1
- name: Collect triage evidence
if: always() && inputs.tsio-shard-name != '' && hashFiles('build/maestro-report.xml') != ''
continue-on-error: true
run: node detox/utils/maestro-triage-evidence.js --report build/maestro-report.xml --build build --device-log build/sim-device.log --dir build/triage-evidence --out build/triage-evidence/evidence.json
- name: Upload triage evidence
id: upload-evidence
if: always() && inputs.tsio-shard-name != '' && hashFiles('build/triage-evidence/*.json') != ''
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: triage-evidence-${{ inputs.tsio-shard-name }}-${{ github.run_id }}
path: build/triage-evidence/
overwrite: true
# The artifact service times out now and then ("Failed to CreateArtifact:
# ETIMEDOUT") and upload-artifact does not retry, which leaves triage with
# no evidence for this lane.
- name: Upload triage evidence (retry)
if: always() && steps.upload-evidence.outcome == 'failure'
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: triage-evidence-${{ inputs.tsio-shard-name }}-${{ github.run_id }}
path: build/triage-evidence/
overwrite: true
- name: Write TSIO failure stub when Maestro report is missing
if: always() && inputs.tsio-config != '' && vars.TSIO_UPLOAD_ENABLED != 'false' && hashFiles('build/maestro-report.xml') == ''
env:
TSIO_SHARD_NAME: ${{ inputs.tsio-shard-name != '' && inputs.tsio-shard-name || 'Maestro E2E Tests (iOS)' }}
run: |
node detox/utils/write-tsio-failure-stub.mjs \
--format junit \
--output build/maestro-report.xml \
--job-name "$TSIO_SHARD_NAME" \
--reason "Maestro job failed or was cancelled before maestro-report.xml 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')"
# TSIO accepts framework=maestro (JUnit XML) since mattermost-test-system-io#76.
- name: Upload report to Test System IO
if: always() && inputs.tsio-config != '' && vars.TSIO_UPLOAD_ENABLED != 'false' && hashFiles('build/maestro-report.xml') != '' && steps.tsio-cfg.outputs.config != ''
uses: mattermost/mattermost-test-system-io/.github/actions/test-system-io-report-upload@3212731263d782a3993dbdf4c8c54f143e69a7b2 # release-0.16.0 / main@3212731
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: maestro
github-token: ${{ github.token }}
gh-job-name: ${{ inputs.tsio-shard-name != '' && inputs.tsio-shard-name || 'Maestro E2E Tests (iOS)' }}
# Action input is named json-path; Maestro uploads JUnit XML bytes.
json-path: build/maestro-report.xml
screenshots-dir: build/maestro-artifacts-tsio
# 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 }}
# The runs e2e-detox-pr.yml triages (its tsio-triage job), so a red lane
# says triage is coming instead of looking final.
ANNOUNCE_TRIAGE: ${{ (inputs.run-type == 'PR' || inputs.run-type == 'MAIN' || inputs.run-type == 'MASTER') && vars.E2E_TRIAGE_MODE == 'enforce' }}
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"
CANCELLED="false"
if [ "${{ job.status }}" = "cancelled" ]; then UPSTREAM="false"; CANCELLED="true"; fi
IDENTITY=$(echo "$TSIO_CONFIG" | jq -c '.composite_identity')
CONTEXT=$(echo "$TSIO_CONFIG" | jq -r '.status_context // "e2e-test/maestro-ios"')
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" \
--cancelled="$CANCELLED" \
--e2e-override="$E2E_OVERRIDE" \
--announce-triage="$ANNOUNCE_TRIAGE" \
--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 Maestro results to Zephyr Scale
if: always() && inputs.record_tests_in_zephyr
run: npm run e2e:save-maestro-zephyr -- ../build/maestro-report.xml
working-directory: detox
env:
ZEPHYR_ENABLE: "true"
IOS: "true"
BUILD_ID: maestro-ios-${{ github.run_id }}
- name: Upload Maestro Report to S3
# A re-run of failed flows reports only to the channel message, not over the run's report.
if: always() && inputs.results-artifact == ''
run: |
cd detox
node -e "
const {saveArtifacts} = require('./utils/artifacts');
const fse = require('fs-extra');
fse.copySync('../build', './artifacts/maestro-ios', {overwrite: true});
saveArtifacts('maestro-ios').then(r => {
if (r && r.success) console.log('Uploaded:', r.reportLink);
}).catch(e => console.log('S3 upload skipped:', e.message));
"
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 }}
DETOX_AWS_S3_BUCKET: ${{ env.DETOX_AWS_S3_BUCKET }}
REPORT_PATH: maestro-ios-${{ github.run_id }}
- name: Set Target URL
id: set-url
if: always()
env:
S3_BUCKET: ${{ env.DETOX_AWS_S3_BUCKET }}
run: |
# Point at the HTML report generated above. It's uploaded inside
# detox/artifacts/maestro-ios/, which lands at the maestro-ios/
# path segment under the S3 prefix.
echo "TARGET_URL=https://${S3_BUCKET:-mattermost-detox-report}.s3.amazonaws.com/maestro-ios-${{ github.run_id }}/maestro-ios/maestro-report.html" >> "$GITHUB_OUTPUT"
- name: Record job cancellation
id: shard-cancelled
if: always()
run: |
if [[ "${{ job.status }}" == "cancelled" ]]; then
echo "CANCELLED=true" >> "$GITHUB_OUTPUT"
else
echo "CANCELLED=false" >> "$GITHUB_OUTPUT"
fi
- name: Determine Status
id: determine-status
if: always()
env:
FAILURES: ${{ steps.summary.outputs.FAILURES }}
ERRORS: ${{ steps.summary.outputs.ERRORS }}
TOTAL: ${{ steps.summary.outputs.TOTAL }}
NO_REPORT: ${{ steps.summary.outputs.NO_REPORT }}
SHARD_CANCELLED: ${{ steps.shard-cancelled.outputs.CANCELLED }}
run: |
if [[ "${SHARD_CANCELLED}" == "true" ]]; then
echo "STATUS=error" >> "$GITHUB_OUTPUT"
elif [[ "${FAILURES:-0}" == "-1" || "${FAILURES:-0}" -gt 0 || "${ERRORS:-0}" -gt 0 || "${TOTAL:-0}" -eq 0 || "${NO_REPORT:-}" == "true" ]]; then
echo "STATUS=failure" >> "$GITHUB_OUTPUT"
else
echo "STATUS=success" >> "$GITHUB_OUTPUT"
fi
- name: Generate Summary
if: always()
run: |
echo "### Maestro iOS E2E Results" >> "$GITHUB_STEP_SUMMARY"
echo "| Tests | Passed :white_check_mark: | Failed :x: | Skipped :fast_forward: |" >> "$GITHUB_STEP_SUMMARY"
echo "|:---:|:---:|:---:|:---:|" >> "$GITHUB_STEP_SUMMARY"
echo "| ${{ steps.summary.outputs.TOTAL }} | ${{ steps.summary.outputs.PASSES }} | ${{ steps.summary.outputs.FAILURES }} | ${{ steps.summary.outputs.SKIPPED }} |" >> "$GITHUB_STEP_SUMMARY"
- name: Fail job on test failures
if: always() && (steps.determine-status.outputs.STATUS == 'failure' || steps.determine-status.outputs.STATUS == 'error')
env:
STATUS: ${{ steps.determine-status.outputs.STATUS }}
FAILURES: ${{ steps.summary.outputs.FAILURES }}
run: |
echo "::error::Maestro reported test failures (STATUS=${STATUS}, FAILURES=${FAILURES})"
exit 1
e2e-maestro-android:
# TSIO shard label = job name; prefer tsio-shard-name (e.g. maestro-android-e2e).
name: ${{ inputs.tsio-shard-name != '' && inputs.tsio-shard-name || 'Maestro E2E Tests (Android)' }}
if: ${{ inputs.platform == 'android' }}
runs-on: ubuntu-latest-8-cores
timeout-minutes: 90
outputs:
STATUS: ${{ steps.determine-status.outputs.STATUS }}
TARGET_URL: ${{ steps.set-url.outputs.TARGET_URL }}
FAILURES: ${{ steps.summary.outputs.FAILURES }}
PASSES: ${{ steps.summary.outputs.PASSES }}
TOTAL: ${{ steps.summary.outputs.TOTAL }}
SKIPPED: ${{ steps.summary.outputs.SKIPPED }}
PERCENTAGE: ${{ steps.summary.outputs.PERCENTAGE }}
CANCELLED: ${{ steps.shard-cancelled.outputs.CANCELLED }}
steps:
- name: Checkout Repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ inputs.MOBILE_VERSION }}
- name: Install system 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 --no-install-recommends "${PACKAGES[@]}" || {
sudo apt-get "${APT_OPTS[@]}" update
sudo apt-get "${APT_OPTS[@]}" install -y --no-install-recommends "${PACKAGES[@]}"
}
- name: ci/prepare-node-deps
uses: ./.github/actions/prepare-node-deps
- name: Cache Maestro
id: maestro-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.maestro
key: maestro-c8c361be1cc89b2e742f185466a302a22778be6b-linux
- name: Install Maestro CLI
if: steps.maestro-cache.outputs.cache-hit != 'true'
run: |
MAESTRO_VERSION=$(jq -r .version detox/maestro/maestro-version.json)
MAESTRO_COMMIT=$(jq -r .commit detox/maestro/maestro-version.json)
echo "Installing Maestro ${MAESTRO_VERSION} (pinned to commit ${MAESTRO_COMMIT})"
export MAESTRO_VERSION
curl -fsSL "https://get.maestro.mobile.dev" | bash
MAESTRO_BIN="$HOME/.maestro/bin/maestro"
if [ ! -f "$MAESTRO_BIN" ]; then
echo "Error: Maestro binary not found at $MAESTRO_BIN"
exit 1
fi
echo "Maestro installed successfully at $MAESTRO_BIN"
"$MAESTRO_BIN" --version
- name: Verify Maestro installation
run: ~/.maestro/bin/maestro --version
- name: Download Maestro Android Build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: android-maestro-build-files-${{ env.ARTIFACT_RUN_ID }}
run-id: ${{ env.ARTIFACT_RUN_ID }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: android/app/build/outputs/apk/release
- name: Seed test data
run: |
cd detox/maestro
npm ci --prefer-offline --no-audit --no-fund
npx tsx fixtures/seed.ts
env:
SITE_1_URL: ${{ env.SITE_1_URL }}
ADMIN_EMAIL: ${{ env.ADMIN_EMAIL }}
ADMIN_USERNAME: ${{ env.ADMIN_USERNAME }}
ADMIN_PASSWORD: ${{ env.ADMIN_PASSWORD }}
- 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: Set up Android SDK
run: |
export ANDROID_HOME=/usr/local/lib/android/sdk
export ANDROID_SDK_ROOT=/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 "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" >> $GITHUB_ENV
echo "PATH=$PATH" >> $GITHUB_ENV
- name: Start Xvfb
run: |
Xvfb :99 -screen 0 1920x1080x24 &
echo "DISPLAY=:99" >> $GITHUB_ENV
- name: Accept Android licenses
run: |
mkdir -p ~/.android
yes | sdkmanager --licenses > sdk_licenses_output.txt || true
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
# Same SDK cache strategy as e2e-android-template.yml (API 35, workspace cache).
- name: Cache Android SDK components
id: android-sdk-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .android-sdk-cache
key: android-sdk-${{ env.E2E_ANDROID_API_LEVEL }}-google-apis-x86_64-v2
- name: Apply cached Android SDK components
if: steps.android-sdk-cache.outputs.cache-hit == 'true'
run: |
API_LEVEL='${{ env.E2E_ANDROID_API_LEVEL }}'
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:
JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }}
run: |
API_LEVEL='${{ env.E2E_ANDROID_API_LEVEL }}'
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/
- name: Cache Android Emulator & AVD
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
detox/.android
detox/${{ env.E2E_ANDROID_AVD_NAME }}_api_${{ env.E2E_ANDROID_API_LEVEL }}
key: android-emulator-${{ runner.os }}-${{ runner.arch }}-${{ env.E2E_ANDROID_AVD_NAME }}-api-${{ env.E2E_ANDROID_API_LEVEL }}-v3
restore-keys: |
android-emulator-${{ runner.os }}-${{ runner.arch }}-${{ env.E2E_ANDROID_AVD_NAME }}-api-${{ env.E2E_ANDROID_API_LEVEL }}-
android-emulator-${{ runner.os }}-${{ runner.arch }}-
# Retry only the boot (infra failures); test failures must not be auto-retried.
- name: Bootstrap Android emulator
env:
BOOTSTRAP_ONLY: "true"
MAESTRO_ANDROID: "true"
JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }}
API_LEVEL: ${{ env.E2E_ANDROID_API_LEVEL }}
AVD_NAME: ${{ env.E2E_ANDROID_AVD_NAME }}
MAX_ATTEMPTS: "2"
ATTEMPT_TIMEOUT_MIN: "15"
shell: bash
run: |
set +e
attempt=1
while (( attempt <= MAX_ATTEMPTS )); do
echo "==> Boot attempt ${attempt}/${MAX_ATTEMPTS} (timeout ${ATTEMPT_TIMEOUT_MIN}m)"
timeout "${ATTEMPT_TIMEOUT_MIN}m" bash -c '
cd detox
chmod +x ./create_android_emulator.sh
CI=true ./create_android_emulator.sh "$API_LEVEL" "$AVD_NAME"
'
rc=$?
if (( rc == 0 )); then
echo "==> Boot succeeded on attempt ${attempt}"
exit 0
fi
echo "==> Boot attempt ${attempt} failed with exit ${rc}"
if (( attempt < MAX_ATTEMPTS )); then
echo "==> Cleaning up before retry"
adb -s emulator-5554 emu kill 2>/dev/null || true
pkill -9 -f qemu-system 2>/dev/null || true
pkill -9 -f emulator 2>/dev/null || true
sleep 5
fi
attempt=$(( attempt + 1 ))
done
echo "==> All ${MAX_ATTEMPTS} boot attempts failed"
exit 1
# The iOS job has captured a device log since it was written; the Android job never has,
# so an Android Maestro failure has no record of what the OS did. That gap is why
# report_problem_link_mode could not be diagnosed across three runs: the command trace
# proves the "Report a problem" tap completed and the app stayed put, but with no logcat
# there is no way to tell whether the browser intent was fired, resolved, or foregrounded.
- name: Start Android logcat capture
run: |
mkdir -p build
adb logcat -c || true
# Full UTC dates, so triage evidence can match app errors to the flow that failed.
adb logcat -v threadtime -v year -v UTC > build/android-logcat.log 2> build/android-logcat.err &
echo $! > build/android-logcat.pid
echo "==> logcat capture started (pid $(cat build/android-logcat.pid))"
- name: Run Maestro Flows (Android)
continue-on-error: true
env:
FLOW_PATH: ${{ inputs.flow-path }}
MAESTRO_DRIVER_STARTUP_TIMEOUT: "180000"
run: |
if [ -n "${FLOW_PATH:-}" ] && ! printf '%s' "$FLOW_PATH" | grep -Eq '^[A-Za-z0-9_./ -]+$'; then
echo "Invalid flow-path input: $FLOW_PATH"
exit 1
fi
set -a
source detox/maestro/.maestro-test-env.sh
set +a
# Same link-mode patch as the iOS batch — see the iOS step's comment.
trap 'curl -f -sS --show-error -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"SupportSettings\":{\"ReportAProblemType\":\"default\"}}" \
"${SITE_1_URL}/api/v4/config/patch" > /dev/null && \
echo "==> Restored ReportAProblemType=default"' EXIT
echo "==> Patching SupportSettings.ReportAProblemType=link on ${SITE_1_URL}"
curl -f -sS --show-error -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"SupportSettings":{"ReportAProblemType":"link"}}' \
"${SITE_1_URL}/api/v4/config/patch" > /dev/null
chmod +x detox/maestro/scripts/run_ci_batches.sh
detox/maestro/scripts/run_ci_batches.sh --platform android
- name: Run MM-T67856_4 with AllowDownloadLogs=false (Android)
# Isolated step: the flow patches AllowDownloadLogs=false in onFlowStart and
# restores it in onFlowComplete, so it must not share a batch with attach_logs.yml.
# continue-on-error keeps the main batch report authoritative.
# Skip when seed never wrote the env file (e.g. inactive Matterwick site).
# A re-run of failed flows runs only those (flow-path).
if: always() && inputs.results-artifact == '' && hashFiles('detox/maestro/.maestro-test-env.sh') != ''
continue-on-error: true
run: |
set -a
source detox/maestro/.maestro-test-env.sh
set +a
# The flow flips SupportSettings.AllowDownloadLogs=false in its own
# onFlowStart hook and restores true in onFlowComplete
# (detox/maestro/fixtures/set_allow_download_logs.js), so this step
# only runs it; a local `maestro test` of the same file does the same.
# The always() restore step below stays as the safety net for runner
# hard-kills, which never reach onFlowComplete.
# Confirm the client config actually serves false instead of assuming a 200
# means the write took: an installation that supplies this setting through an
# environment variable answers 200 and keeps its own value, and the flow then
# asserts against a toggle the server never turned off. Same treatment
# ReportAProblemType already gets below.
echo "==> Patching SupportSettings.AllowDownloadLogs=false on ${SITE_1_URL}"
CONFIG_DIAGNOSIS_FILE="$RUNNER_TEMP/config-diagnosis.txt" bash detox/maestro/scripts/apply_client_config_value.sh \
"${SITE_1_URL}" "$ADMIN_TOKEN" '{"SupportSettings":{"AllowDownloadLogs":false}}' \
AllowDownloadLogs false || {
rc=$?
# 3 = the installation forbids the write, so the flow's pre-condition cannot
# be created here; record a skip rather than a red. Anything else is real.
if [ "$rc" -eq 3 ]; then
bash detox/maestro/scripts/write_maestro_skip_stub.sh \
build/maestro-report-T67856_4.xml \
detox/maestro/flows/account/attach_logs_disabled_when_download_logs_off.yml \
"AllowDownloadLogs=false is not writable on this server (set by environment variable, patch silently ignored); flow cannot run here"
exit 0
fi
bash detox/maestro/scripts/write_maestro_failure_stub.sh \
build/maestro-report-T67856_4.xml \
detox/maestro/flows/account/attach_logs_disabled_when_download_logs_off.yml \
"AllowDownloadLogs=false never reached the client config; the flow's pre-condition was not in force: $(cat "$RUNNER_TEMP/config-diagnosis.txt" 2>/dev/null)"
exit 1
}
~/.maestro/bin/maestro test \
--format junit \
--output build/maestro-report-T67856_4.xml \
--test-output-dir build/maestro-artifacts-T67856_4 \
--debug-output build/maestro-debug-T67856_4 \
--flatten-debug-output \
--platform android \
--env SITE_1_URL="${SITE_1_URL}" \
--env TEST_USER_EMAIL="${TEST_USER_EMAIL}" \
--env TEST_USER_PASSWORD="${TEST_USER_PASSWORD}" \
--env TEST_CHANNEL_NAME="${TEST_CHANNEL_NAME}" \
--env TEST_TEAM_NAME="${TEST_TEAM_NAME}" \
--env ADMIN_TOKEN="${ADMIN_TOKEN}" \
--env MAESTRO_APP_ID="com.mattermost.rnbeta" \
detox/maestro/flows/account/attach_logs_disabled_when_download_logs_off.yml
- name: Restore AllowDownloadLogs baseline (self-healing)
if: always() && hashFiles('detox/maestro/.maestro-test-env.sh') != ''
continue-on-error: true
run: |
set -a
source detox/maestro/.maestro-test-env.sh
set +a
# # Android copy of the iOS restore step — see that step for the
# rationale (trap-less hard-kill and skip-path coverage).
echo "==> Restoring SupportSettings.AllowDownloadLogs=true on ${SITE_1_URL}"
# Not curl --retry: a timed-out patch is still applied, and resending queues more saves behind it.
bash detox/maestro/scripts/apply_client_config_value.sh \
"${SITE_1_URL}" "$ADMIN_TOKEN" '{"SupportSettings":{"AllowDownloadLogs":true}}' \
AllowDownloadLogs true && \
echo "==> Restored AllowDownloadLogs=true"
- name: Run MM-T3261_2 with ReportAProblemType=email (Android)
# Isolated step: clear Gmail, patch ReportAProblemType=email, run the
# mail-mode flow, restore on exit. Android-only — iOS simulators have no
# Mail app. continue-on-error so the main report stays authoritative.
# A re-run of failed flows runs only those (flow-path).
if: always() && inputs.results-artifact == '' && hashFiles('detox/maestro/.maestro-test-env.sh') != ''
continue-on-error: true
run: |
set -a
source detox/maestro/.maestro-test-env.sh
set +a
echo "==> Clearing Gmail so MM-T3261_2 hits first-run onboarding"
adb shell pm clear com.google.android.gm || true
trap 'curl -f -sS --show-error -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"SupportSettings\":{\"ReportAProblemType\":\"default\"}}" \
"${SITE_1_URL}/api/v4/config/patch" > /dev/null && \
echo "==> Restored ReportAProblemType=default"' EXIT
# ReportAProblemMail is required: the server rejects ReportAProblemType=email
# without a valid address (model/config.go IsValid — CI 34290629488 got HTTP 400
# here and the flow never ran). On patch failure, record the flow as failed so
# it stays in the merged report instead of vanishing from the count.
# Patch and then confirm the client config actually serves it. Two config writes
# close together can lose the later one: in run 34344304929 the mail step's trap
# restored 'default' 58 ms before this step patched, the app observed 'default',
# and handleReport() emailed instead of opening the browser.
echo "==> Patching SupportSettings.ReportAProblemType=email on ${SITE_1_URL}"
CONFIG_DIAGNOSIS_FILE="$RUNNER_TEMP/config-diagnosis.txt" bash detox/maestro/scripts/apply_client_config_value.sh \
"${SITE_1_URL}" "$ADMIN_TOKEN" '{"SupportSettings":{"ReportAProblemType":"email","ReportAProblemMail":"e2e-report-a-problem@example.com"}}' \
ReportAProblemType email || {
rc=$?
# 3 = this installation forbids the write (run 34452126763 proved it is set by an
# environment variable, so config/patch is silently ignored); the flow's
# pre-condition cannot be created here, so record a skip rather than a red.
# Anything else is a real failure and stays red.
if [ "$rc" -eq 3 ]; then
bash detox/maestro/scripts/write_maestro_skip_stub.sh \
build/maestro-report-T3261_2.xml \
detox/maestro/flows/account/report_problem_mail_mode.yml \
"ReportAProblemType=email is not writable on this server (set by environment variable, patch silently ignored); flow cannot run here"
exit 0
fi
bash detox/maestro/scripts/write_maestro_failure_stub.sh \
build/maestro-report-T3261_2.xml \
detox/maestro/flows/account/report_problem_mail_mode.yml \
"ReportAProblemType=email never reached the client config; the flow's pre-condition was not in force: $(cat "$RUNNER_TEMP/config-diagnosis.txt" 2>/dev/null)"
exit 1
}
~/.maestro/bin/maestro test \
--format junit \
--output build/maestro-report-T3261_2.xml \
--test-output-dir build/maestro-artifacts-T3261_2 \
--debug-output build/maestro-debug-T3261_2 \
--flatten-debug-output \
--platform android \
--env SITE_1_URL="${SITE_1_URL}" \
--env TEST_USER_EMAIL="${TEST_USER_EMAIL}" \
--env TEST_USER_PASSWORD="${TEST_USER_PASSWORD}" \
--env TEST_CHANNEL_NAME="${TEST_CHANNEL_NAME}" \
--env TEST_TEAM_NAME="${TEST_TEAM_NAME}" \
--env ADMIN_TOKEN="${ADMIN_TOKEN}" \
--env MAESTRO_APP_ID="com.mattermost.rnbeta" \
detox/maestro/flows/account/report_problem_mail_mode.yml
- name: Run MM-T3261_1 with ReportAProblemType=link (Android)
# Isolated step: patch ReportAProblemType=link plus the report link, run the
# link-mode flow, restore the default on exit. The flow has no onFlowStart hook
# of its own, so the patch and the trap restore live here. continue-on-error so
# the main batch report stays authoritative.
# A re-run of failed flows runs only those (flow-path).
if: always() && inputs.results-artifact == '' && hashFiles('detox/maestro/.maestro-test-env.sh') != ''
continue-on-error: true
run: |
set -a
source detox/maestro/.maestro-test-env.sh
set +a
trap 'curl -f -sS --show-error -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"SupportSettings\":{\"ReportAProblemType\":\"default\"}}" \
"${SITE_1_URL}/api/v4/config/patch" > /dev/null && \
echo "==> Restored ReportAProblemType=default"' EXIT
# Patch and then confirm the client config actually serves it. Two config writes
# close together can lose the later one: in run 34344304929 the mail step's trap
# restored 'default' 58 ms before this step patched, the app observed 'default',
# and handleReport() emailed instead of opening the browser.
echo "==> Patching SupportSettings.ReportAProblemType=link on ${SITE_1_URL}"
CONFIG_DIAGNOSIS_FILE="$RUNNER_TEMP/config-diagnosis.txt" bash detox/maestro/scripts/apply_client_config_value.sh \
"${SITE_1_URL}" "$ADMIN_TOKEN" '{"SupportSettings":{"ReportAProblemType":"link","ReportAProblemLink":"https://mattermost.com/pl/report_a_problem_licensed"}}' \
ReportAProblemType link || {
rc=$?
# 3 = this installation forbids the write (run 34452126763 proved it is set by an
# environment variable, so config/patch is silently ignored); the flow's
# pre-condition cannot be created here, so record a skip rather than a red.
# Anything else is a real failure and stays red.
if [ "$rc" -eq 3 ]; then
bash detox/maestro/scripts/write_maestro_skip_stub.sh \
build/maestro-report-T3261_1.xml \
detox/maestro/flows/account/report_problem_link_mode.yml \
"ReportAProblemType=link is not writable on this server (set by environment variable, patch silently ignored); flow cannot run here"
exit 0
fi
bash detox/maestro/scripts/write_maestro_failure_stub.sh \
build/maestro-report-T3261_1.xml \
detox/maestro/flows/account/report_problem_link_mode.yml \
"ReportAProblemType=link never reached the client config; the flow's pre-condition was not in force: $(cat "$RUNNER_TEMP/config-diagnosis.txt" 2>/dev/null)"
exit 1
}
~/.maestro/bin/maestro test \
--format junit \
--output build/maestro-report-T3261_1.xml \
--test-output-dir build/maestro-artifacts-T3261_1 \
--debug-output build/maestro-debug-T3261_1 \
--flatten-debug-output \
--platform android \
--env SITE_1_URL="${SITE_1_URL}" \
--env TEST_USER_EMAIL="${TEST_USER_EMAIL}" \
--env TEST_USER_PASSWORD="${TEST_USER_PASSWORD}" \
--env TEST_CHANNEL_NAME="${TEST_CHANNEL_NAME}" \
--env TEST_TEAM_NAME="${TEST_TEAM_NAME}" \
--env ADMIN_TOKEN="${ADMIN_TOKEN}" \
--env MAESTRO_APP_ID="com.mattermost.rnbeta" \
detox/maestro/flows/account/report_problem_link_mode.yml
- name: Stop Android logcat capture
if: always()
run: |
if [ -f build/android-logcat.pid ]; then
pid=$(cat build/android-logcat.pid)
kill "$pid" 2>/dev/null || true
echo "==> logcat capture stopped (pid $pid)"
fi
if [ -f build/android-logcat.log ]; then
echo "==> logcat size: $(wc -c < build/android-logcat.log) bytes"
fi
- name: Upload Maestro Artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
# A re-run of failed flows runs in the same workflow run: its own name.
name: maestro-android-results-${{ github.run_id }}${{ inputs.results-artifact != '' && '-rerun' || '' }}
path: build/
- name: Parse Maestro Report
id: summary
if: always()
run: |
cd detox
npm ci --prefer-offline --no-audit --no-fund
node -e "
const path = require('path');
const {parseMaestroReport, generateMaestroHtmlReport, mergeMaestroBatchReportsFromDir, prepareMaestroScreenshotsForTsio} = require('./utils/maestro_report');
const buildDir = path.resolve('..', 'build');
const xmlPath = path.join(buildDir, 'maestro-report.xml');
mergeMaestroBatchReportsFromDir(buildDir, xmlPath);
const summary = parseMaestroReport(xmlPath);
const fs = require('fs');
if (summary) {
const s = summary.stats;
const output = [
'FAILURES=' + s.failures,
'PASSES=' + s.passes,
'TOTAL=' + s.tests,
'ERRORS=' + s.errors,
'SKIPPED=' + s.skipped,
'PERCENTAGE=' + s.passPercent,
];
output.forEach(l => fs.appendFileSync(process.env.GITHUB_OUTPUT, l + '\n'));
// Generate a rich HTML report (stats + per-flow drill-down with
// inline screenshots + log links). See maestro iOS notes above.
generateMaestroHtmlReport({
xmlPath: xmlPath,
artifactsDir: path.join(buildDir, 'maestro-artifacts'),
outputPath: path.join(buildDir, 'maestro-report.html'),
platform: 'Android',
commitSha: process.env.GITHUB_SHA || '',
});
// Per-flow folders so TSIO LinkScreenshots can match full_title.
prepareMaestroScreenshotsForTsio({
xmlPath: xmlPath,
artifactsDir: path.join(buildDir, 'maestro-artifacts'),
outputDir: path.join(buildDir, 'maestro-artifacts-tsio'),
repoRoot: path.resolve('..'),
});
} else {
console.log('No Maestro report found — marking as failure');
['FAILURES=1','PASSES=0','TOTAL=0','ERRORS=1','SKIPPED=0','PERCENTAGE=0','NO_REPORT=true'].forEach(l => fs.appendFileSync(process.env.GITHUB_OUTPUT, l + '\n'));
}
"
# Evidence for the triage job in e2e-detox-pr.yml: each failed flow's failure
# screenshot, the step it stopped at and the errors the app logged meanwhile.
# A re-run of failed flows hands its per-flow results to the channel message.
- name: Upload re-run results
if: always() && inputs.results-artifact != '' && hashFiles('build/maestro-report.xml') != ''
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.results-artifact }}
path: build/maestro-report.xml
retention-days: 1
- name: Collect triage evidence
if: always() && inputs.tsio-shard-name != '' && hashFiles('build/maestro-report.xml') != ''
continue-on-error: true
run: node detox/utils/maestro-triage-evidence.js --report build/maestro-report.xml --build build --device-log build/android-logcat.log --dir build/triage-evidence --out build/triage-evidence/evidence.json
- name: Upload triage evidence
id: upload-evidence
if: always() && inputs.tsio-shard-name != '' && hashFiles('build/triage-evidence/*.json') != ''
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: triage-evidence-${{ inputs.tsio-shard-name }}-${{ github.run_id }}
path: build/triage-evidence/
overwrite: true
# The artifact service times out now and then ("Failed to CreateArtifact:
# ETIMEDOUT") and upload-artifact does not retry, which leaves triage with
# no evidence for this lane.
- name: Upload triage evidence (retry)
if: always() && steps.upload-evidence.outcome == 'failure'
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: triage-evidence-${{ inputs.tsio-shard-name }}-${{ github.run_id }}
path: build/triage-evidence/
overwrite: true
- name: Write TSIO failure stub when Maestro report is missing
if: always() && inputs.tsio-config != '' && vars.TSIO_UPLOAD_ENABLED != 'false' && hashFiles('build/maestro-report.xml') == ''
env:
TSIO_SHARD_NAME: ${{ inputs.tsio-shard-name != '' && inputs.tsio-shard-name || 'Maestro E2E Tests (Android)' }}
run: |
node detox/utils/write-tsio-failure-stub.mjs \
--format junit \
--output build/maestro-report.xml \
--job-name "$TSIO_SHARD_NAME" \
--reason "Maestro job failed or was cancelled before maestro-report.xml 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')"
# TSIO accepts framework=maestro (JUnit XML) since mattermost-test-system-io#76.
- name: Upload report to Test System IO
if: always() && inputs.tsio-config != '' && vars.TSIO_UPLOAD_ENABLED != 'false' && hashFiles('build/maestro-report.xml') != '' && steps.tsio-cfg.outputs.config != ''
uses: mattermost/mattermost-test-system-io/.github/actions/test-system-io-report-upload@3212731263d782a3993dbdf4c8c54f143e69a7b2 # release-0.16.0 / main@3212731
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: maestro
github-token: ${{ github.token }}
gh-job-name: ${{ inputs.tsio-shard-name != '' && inputs.tsio-shard-name || 'Maestro E2E Tests (Android)' }}
# Action input is named json-path; Maestro uploads JUnit XML bytes.
json-path: build/maestro-report.xml
screenshots-dir: build/maestro-artifacts-tsio
# 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 }}
# The runs e2e-detox-pr.yml triages (its tsio-triage job), so a red lane
# says triage is coming instead of looking final.
ANNOUNCE_TRIAGE: ${{ (inputs.run-type == 'PR' || inputs.run-type == 'MAIN' || inputs.run-type == 'MASTER') && vars.E2E_TRIAGE_MODE == 'enforce' }}
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"
CANCELLED="false"
if [ "${{ job.status }}" = "cancelled" ]; then UPSTREAM="false"; CANCELLED="true"; fi
IDENTITY=$(echo "$TSIO_CONFIG" | jq -c '.composite_identity')
CONTEXT=$(echo "$TSIO_CONFIG" | jq -r '.status_context // "e2e-test/maestro-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" \
--cancelled="$CANCELLED" \
--e2e-override="$E2E_OVERRIDE" \
--announce-triage="$ANNOUNCE_TRIAGE" \
--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 Maestro results to Zephyr Scale
if: always() && inputs.record_tests_in_zephyr
run: npm run e2e:save-maestro-zephyr -- ../build/maestro-report.xml
working-directory: detox
env:
ZEPHYR_ENABLE: "true"
IOS: "false"
BUILD_ID: maestro-android-${{ github.run_id }}
- name: Upload Maestro Report to S3
# A re-run of failed flows reports only to the channel message, not over the run's report.
if: always() && inputs.results-artifact == ''
run: |
cd detox
node -e "
const {saveArtifacts} = require('./utils/artifacts');
const fse = require('fs-extra');
fse.copySync('../build', './artifacts/maestro-android', {overwrite: true});
saveArtifacts('maestro-android').then(r => {
if (r && r.success) console.log('Uploaded:', r.reportLink);
}).catch(e => console.log('S3 upload skipped:', e.message));
"
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 }}
DETOX_AWS_S3_BUCKET: ${{ env.DETOX_AWS_S3_BUCKET }}
REPORT_PATH: maestro-android-${{ github.run_id }}
- name: Set Target URL
id: set-url
if: always()
env:
S3_BUCKET: ${{ env.DETOX_AWS_S3_BUCKET }}
run: |
# Point at the HTML report generated above.
echo "TARGET_URL=https://${S3_BUCKET:-mattermost-detox-report}.s3.amazonaws.com/maestro-android-${{ github.run_id }}/maestro-android/maestro-report.html" >> "$GITHUB_OUTPUT"
- name: Record shard cancellation
id: shard-cancelled
if: always()
run: |
if [[ "${{ job.status }}" == "cancelled" ]]; then
echo "CANCELLED=true" >> "$GITHUB_OUTPUT"
else
echo "CANCELLED=false" >> "$GITHUB_OUTPUT"
fi
- name: Determine Status
id: determine-status
if: always()
env:
FAILURES: ${{ steps.summary.outputs.FAILURES }}
ERRORS: ${{ steps.summary.outputs.ERRORS }}
TOTAL: ${{ steps.summary.outputs.TOTAL }}
NO_REPORT: ${{ steps.summary.outputs.NO_REPORT }}
SHARD_CANCELLED: ${{ steps.shard-cancelled.outputs.CANCELLED }}
run: |
if [[ "${SHARD_CANCELLED}" == "true" ]]; then
echo "STATUS=error" >> "$GITHUB_OUTPUT"
elif [[ "${FAILURES:-0}" == "-1" || "${FAILURES:-0}" -gt 0 || "${ERRORS:-0}" -gt 0 || "${TOTAL:-0}" -eq 0 || "${NO_REPORT:-}" == "true" ]]; then
echo "STATUS=failure" >> "$GITHUB_OUTPUT"
else
echo "STATUS=success" >> "$GITHUB_OUTPUT"
fi
- name: Generate Summary
if: always()
run: |
echo "### Maestro Android E2E Results" >> "$GITHUB_STEP_SUMMARY"
echo "| Tests | Passed :white_check_mark: | Failed :x: | Skipped :fast_forward: |" >> "$GITHUB_STEP_SUMMARY"
echo "|:---:|:---:|:---:|:---:|" >> "$GITHUB_STEP_SUMMARY"
echo "| ${{ steps.summary.outputs.TOTAL }} | ${{ steps.summary.outputs.PASSES }} | ${{ steps.summary.outputs.FAILURES }} | ${{ steps.summary.outputs.SKIPPED }} |" >> "$GITHUB_STEP_SUMMARY"
- name: Fail job on test failures
if: always() && (steps.determine-status.outputs.STATUS == 'failure' || steps.determine-status.outputs.STATUS == 'error')
env:
STATUS: ${{ steps.determine-status.outputs.STATUS }}
FAILURES: ${{ steps.summary.outputs.FAILURES }}
run: |
echo "::error::Maestro reported test failures (STATUS=${STATUS}, FAILURES=${FAILURES})"
exit 1