mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-04 18:08:54 +00:00
* 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. ---------
1561 lines
77 KiB
YAML
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
|