mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-06 19:00:14 +00:00
* chore(e2e): Fix e2e artifact upload path for CMT workflow (#9955) * chore(e2e): Fix e2e artifact upload path for CMT workflow * test(e2e): Replace MM-T placeholder test keys with real Zephyr Scale IDs - Classification Banner (18 tests): MM-T6196–MM-T6205, MM-T6206–MM-T6208, MM-T6209–MM-T6213 - mm_blocks (incoming webhook + ephemeral): MM-T6214–MM-T6240 (27 tests) - Fixes TM4J test-case linking by ensuring all test titles use real MM-T#### keys - Tightens getTM4JTestCases filter regex from /(MM-T)\w+/ to /MM-T\d+/ to prevent crashes when non-numeric MM-T placeholders slip through before Zephyr linkage - Scan all Xcode_*.app directories and filter for major version >= 26 to future-proof against Xcode 30+ releases (current glob pattern maxes at 29) - Extract version from directory name, validate major >= 26, sort and select highest - Add least-privilege job-level permissions (contents: read) to build jobs (build-ios-simulator, build-android-apk, build-android-apk-maestro) to scope actions/checkout and actions/upload-artifact access * fix: apply CodeRabbit review feedback Require Xcode 26.1+ for iOS prepare, standardize E2E titles to MM-TXXXX_N, and bound Zephyr test-case id matching to complete identifiers. * fix(e2e): Grant actions:write on CMT build jobs for artifact upload Job-level contents:read alone strips upload-artifact permission; match e2e-detox-pr.yml so ios/android/maestro builds can publish artifacts. * feat(e2e): Integrate TSIO reporting (#9925) Retire legacy maestro per-platform contexts from cancel-e2e-runs and document the canonical PR, master, and CMT status context mapping. Wire Detox + Maestro (PR/main/CMT) into Test System IO with a single commit status (e2e/mobile; CMT uses e2e/compatibility-matrix-testing) and groups mobile-pr / mobile-main / cmt-mobile. Upload shard results with correct framework labels, merge Jest shards into TSIO Detox shape, stub missing/dead shards as failures, and post channel summaries after status settles. Matterwick MASTER is treated as MAIN for mobile-main routing. Also harden report/fail paths, stage Maestro screenshots per flow for TSIO galleries, fix a few Detox setup/testID issues, and skip known platform flakes so the pipeline stays actionable while TSIO lands. * ci(e2e): per-job TSIO report groups, commit statuses, and webhook sidecar (#9969) * ci(e2e): per-job TSIO report groups and commit statuses E2E jobs previously shared a single `e2e/mobile` commit status backed by one TSIO report group, so concurrent platform legs raced to finalize the group and clobbered each other's results. Give each Detox/Maestro job its own report group (`mobile-<flow>-<job>`, total_reports_expected=1) and its own commit status context (`detox-ios`, `detox-android`, `detox-ipad`, `maestro-ios`, `maestro-android`); CMT reports per shard plus an umbrella context. Channel notifications move to a single rollup step so N platform legs no longer spam the webhook, and the five-server topology is dispatched with explicit per-platform inputs that still resolve to the existing SITE_*_URL contract consumed by the test config. Also adds the webhook sidecar launcher, Android emulator CI prep, and TSIO screenshot path linking that keeps folder basenames matching Jest full names. The new detox type-check job surfaced pre-existing broken import paths in maestro/fixtures/calls_seed.ts, which pointed at detox/detox/e2e; corrected so the guard passes on its own first run. Note: the CMT matrix now reads per-platform server URLs (`ios_site_1_url`, `android_site_1_url`, `site_3_url`) instead of a single `url`, which needs the matching Matterwick dispatch change to land alongside this. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): aggregate CMT channel rollup from TSIO and wire notify secrets The CMT umbrella posted a placeholder summary built from CI job outcomes (passed:0, failed:0|1, reports:[]), so the release channel message carried no per-leg detail. It now runs the same tsio-channel-notify-rollup.js path as PR/main: cmt-identity emits a per-shard TSIO config map and the rollup polls every shard group, so counts and per-leg rows come from TSIO. The rollup job also needs id-token: write to mint its OIDC token. Webhook secrets used by the per-job TSIO finalize steps were referenced but never declared in the templates' workflow_call.secrets, so GitHub resolved them to empty strings and channel notify silently no-opped. Declare them in the iOS/Android/Maestro templates and forward them from e2e-detox.yml and the CMT callers. The Maestro PR path worked only because its callers use secrets: inherit. Also from review: - split-tests.js emitted two matrix jobs at parallelism=1 when mm_blocks specs were present; merge into one shard instead of exceeding the requested count. - Move the Android ARTIFACT_RUN_ID emptiness check ahead of download-artifact, which consumes it and otherwise fails with an opaque API error. - resolve-tsio-job-config.sh now reuses webhookBucketForReportName instead of re-deriving the bucket prefix with a looser startsWith check. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): add local preflight for CI PRs The webhook-secret and OIDC-permission gaps in this branch were invisible locally and in a green CI run — the affected steps exit 0 and just skip the notify or commit status. Add a script engineers can run before opening a PR that touches .github/ or detox/utils: cd detox && npm run preflight:ci # :ci-fast skips lint + tsc It runs actionlint (which flags secrets not declared in a callee's workflow_call.secrets), scans for exit-code capture broken by a trailing backslash, asserts every job running a TSIO OIDC script declares id-token: write, then runs the detox/utils tests and npm run check. actionlint and the CodeRabbit CLI are optional and skipped with an install hint when absent. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): use mktemp for preflight logs Fixed /tmp log paths can be pre-created as symlinks, so the preflight script would truncate whatever they point at (CWE-377). Create both logs with mktemp and drop them on exit. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): address review feedback on TSIO statuses and sidecar A cancelled E2E run reached no conclusion, so posting `success` on its commit statuses let a PR merge on a check that never ran. Cancelled runs now post `pending`; `success` is reserved for the one deliberate bypass, the E2E/Override label. Commit-status contexts move into the `e2e-test/*` namespace to match the mattermost monorepo, with the prefix owned by build-tsio-job-config.js and mirrored in the cancel action's context list. Legacy unprefixed and `e2e/*` contexts are dropped rather than carried forward. The `preflight:ci` script and its OIDC-permission checker duplicated what actionlint, the unit tests and `npm run check` already do, so they are removed and detox/CLAUDE.md documents the underlying failure modes and the commands to catch them instead. cloudflared and the TSIO CLI are pinned to known-good releases rather than `latest`, and detox/README.md now documents the webhook sidecar: when a tunnel is needed, the WEBHOOK_PUBLIC_BASE_URL / CLOUDFLARED_TUNNEL_TOKEN precedence, and why quick tunnels report callbacks as unreachable. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): detect upstream cancellation and surface status reset failures generate-report runs with always(), so job.status stays "success" when an upstream job is cancelled and TSIO was published as a real failure. Read the generate-specs/e2e-* needs results so a cancelled run is reported as such. In cancel-e2e-runs, only reset the E2E commit statuses for runs that were actually stopped — a run we failed to cancel keeps publishing its own results. Aggregate createCommitStatus failures across every context and fail the action afterwards instead of silently warning, so a stale check cannot go unnoticed. --------- Co-authored-by: Cursor <cursoragent@cursor.com> * fix(e2e): Quarantine proven flakes and harden Detox harness (#9971) * ci(e2e): make E2E/Override waive checks instead of cancelling runs (#9975) * ci(e2e): make E2E/Override waive checks instead of cancelling runs E2E/Override previously cancelled in-flight E2E runs and posted success on their commit statuses. Cancelling destroys the very signal the maintainer might still want, and reporting success for a run that never finished attributes a conclusion to work that never happened. Override now means one thing only: the required e2e-test/* contexts go green so the PR can merge. Runs keep going and keep reporting to TSIO. - Add .github/actions/e2e-override-status to post success (or pending) on all five required e2e-test/* contexts for a SHA. - Label manager posts success on `labeled` and pending on `unlabeled`, so removing the escape hatch re-blocks the PR instead of leaving a stale waiver on the head SHA. It comments the runs it left running rather than killing them, and drops to `actions: read` now that it never cancels. - tsio-report-status re-reads the PR labels before posting a failure. Because runs are no longer cancelled, a result landing after the override was applied would otherwise re-red a check the maintainer deliberately bypassed. The lookup fails closed so an unreadable label list cannot turn a real failure green, and the step itself no longer fails under override. - Guard e2e-cancel-on-label-removal: stripping E2E/Run* is exactly what the Honor E2E/Override step does, so without this the indirect unlabeled event would cancel the runs the redesign is meant to preserve. - e2e-detox-pr posts the success itself when the policy skips E2E, since every downstream job is skipped and a skipped job leaves the required contexts absent, blocking the merge forever. - cancel-e2e-runs drops status_state: a cancelled run has no conclusion to report, so it always resets to pending. - Thread `issues: read` through the reusable workflow callers so finalize can read the labels; a reusable workflow cannot escalate past its caller. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): derive required E2E contexts from one definition The override and cancel actions each carried their own copy of the five required e2e-test/* contexts, kept aligned with build-tsio-job-config.js by comment alone. A job renamed in PR_MAIN_JOBS would silently leave the actions waiving or resetting a context that no longer exists, so a PR either cannot merge or merges while a real context still blocks it. Export E2E_STATUS_CONTEXTS from detox/utils/build-tsio-job-config.js, derived from PR_MAIN_JOBS, and require it from both composite actions. github-script resolves relative requires against the workspace, and every call site already checks out the repo before using the action. Also set persist-credentials: false on the e2e-label-manager checkout, matching the other checkout steps. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): drop the cancel action for native concurrency, keep GitHub calls in Actions Review feedback from @saturninoabril. Superseding an in-flight E2E run is what GitHub's `concurrency` already does, so `e2e-detox-pr.yml` now declares `concurrency: e2e-<ref>` with `cancel-in-progress` instead of cancelling from a step inside the run that supersedes it. The older run is cancelled the moment the new dispatch is created rather than once a runner picks up the first job. That removes the only two remaining callers of `.github/actions/cancel-e2e-runs`, along with the commit statuses it forced to `pending` on cancel — a cancelled run has no conclusion to report, so the state is left to GitHub. Forcing a commit status is now reserved for real test results and the override waiver. The action is deleted. `e2e-cancel-on-label-removal.yml` goes with it: with `E2E/Run` on its way out and override no longer cancelling, removing the label to stop a run has no remaining practical case, and dropping the workflow also removes the back-door cancel guard it needed. The `E2E/Override` lookup moves out of `tsio-report-status.js` into a new `.github/actions/e2e-override-label` composite action, which the finalize steps call and pass in via `--e2e-override`. Talking to GitHub stays in the Actions layer and the reporter is left deciding on its inputs. Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): paginate PR label lookups for E2E/Override Co-authored-by: Cursor <cursoragent@cursor.com> * ci(e2e): re-post E2E/Override success on new commits Override success is SHA-scoped: it is posted only on the head SHA at label-apply time (e2e-label-manager 'labeled' path) and when Matterwick dispatches e2e-detox-pr.yml (post-e2e-override-status). On a push after E2E/Override is applied, refresh-e2e-run-label strips the run labels and returns — Matterwick is not dispatched, so the new head SHA gets no e2e-test/* statuses. With those checks Required, the new commit is blocked until the label is re-toggled. Carry the override onto the new commit: refresh-e2e-run-label now emits an e2e_override output, and when it is set a gated checkout + e2e-override-status step re-posts success on the new head SHA. Reuses the existing e2e-override-status action (same 5 required e2e-test/* contexts, same 'E2E bypassed' description), so it is idempotent with the labeled path. Adds statuses: write / contents: read to the job. Note: refresh-e2e-run-label is gated on base.ref == 'main', so stacked PRs (base != main) are not covered; non-main-base already posts success at dispatch time. --------- Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1138 lines
52 KiB
YAML
1138 lines
52 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: ""
|
|
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
|
|
|
|
# disable_ios_autofill.js (called later during simulator boot) lives in
|
|
# detox/utils/ and requires `shelljs` from detox/package.json. The repo-root
|
|
# `prepare-node-deps` only installs top-level node_modules, so we need a
|
|
# separate install for the detox sub-package or the script throws
|
|
# `Cannot find module 'shelljs'`.
|
|
- 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-00f516729f57d48f36ded55244cfbe358ada6b00-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
|
|
|
|
# Maestro-only: enable Files-app access for channel_bookmark_file picker flows.
|
|
# Must patch Info.plist before simctl install (see e2e-maestro-template comment
|
|
# in the prior Install step).
|
|
- 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"
|
|
|
|
# Same simulator boot / autofill / install sequence as Detox iOS CI.
|
|
# Skip pre-warm: Maestro login.yml uses clearState:true every flow, so a
|
|
# pre-warm launch is discarded immediately and can leave launchd in a
|
|
# bad state on iOS 26 before the first Maestro launchApp.
|
|
- 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
|
|
# Required by detox/maestro/flows/channels/channel_bookmark_file.yml: the
|
|
# iOS document picker can only see files that already exist in the
|
|
# local File Provider storage at maestro launch time. We drop the
|
|
# detox image fixture there so the picker's "On My iPhone > Browse"
|
|
# path can reach it. Android does the equivalent via `adb push` in
|
|
# the Android job below.
|
|
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))"
|
|
|
|
# Gate the suite on connectivity so an infra failure is reported as infra rather
|
|
# than as a test failure in whichever flow happens to run first. CI 30466684108:
|
|
# the simulator's TLS handshake to SITE_1_URL was cancelled during trust evaluation
|
|
# against a valid chain, the app issued zero /api/v4 requests, and the failure
|
|
# surfaced as a home-tab assertion inside attach_logs. Also warms the device's
|
|
# trust evaluation before the reported flows run.
|
|
- name: Pre-flight — simulator can reach test server
|
|
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)
|
|
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
|
|
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)
|
|
# Isolated step: patch the server config to AllowDownloadLogs=false,
|
|
# run the single flow, then restore. Always restore even on failure.
|
|
# Failure here does not fail the job — the main report is the source
|
|
# of truth; this is additive coverage.
|
|
if: always()
|
|
continue-on-error: true
|
|
run: |
|
|
set -a
|
|
source detox/maestro/.maestro-test-env.sh
|
|
set +a
|
|
|
|
# AllowDownloadLogs lives under SupportSettings (not ServiceSettings)
|
|
# in the Mattermost server config — verified during local maestro
|
|
# iteration against SITE_1_URL.
|
|
echo "==> Patching SupportSettings.AllowDownloadLogs=false on ${SITE_1_URL}"
|
|
curl -f -sS --show-error -X PUT \
|
|
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"SupportSettings":{"AllowDownloadLogs":false}}' \
|
|
"${SITE_1_URL}/api/v4/config/patch" > /dev/null
|
|
|
|
# Always restore the default before exiting this step, regardless
|
|
# of whether the maestro run passes.
|
|
trap 'curl -f -sS --show-error -X PUT \
|
|
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"SupportSettings\":{\"AllowDownloadLogs\":true}}" \
|
|
"${SITE_1_URL}/api/v4/config/patch" > /dev/null && \
|
|
echo "==> Restored AllowDownloadLogs=true"' EXIT
|
|
|
|
~/.maestro/bin/maestro test \
|
|
--device "$SIMULATOR_ID" \
|
|
--format junit \
|
|
--output build/maestro-report-T67856_4.xml \
|
|
--test-output-dir build/maestro-artifacts-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: Upload Maestro Artifacts
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: maestro-ios-results-${{ github.run_id }}
|
|
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'));
|
|
}
|
|
"
|
|
|
|
- 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@0661accd8cc9ab9dde6ec93f6f5ad40d06deb0d3 # release-0.14.0 / main@0661acc (#93 unique release tags per run)
|
|
with:
|
|
composite-identity: ${{ toJSON(fromJSON(steps.tsio-cfg.outputs.config).composite_identity) }}
|
|
total-reports-expected: ${{ fromJSON(steps.tsio-cfg.outputs.config).total_reports_expected }}
|
|
framework: 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 }}
|
|
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"
|
|
if [ "${{ job.status }}" = "cancelled" ]; then UPSTREAM="false"; fi
|
|
IDENTITY=$(echo "$TSIO_CONFIG" | jq -c '.composite_identity')
|
|
CONTEXT=$(echo "$TSIO_CONFIG" | jq -r '.status_context // "e2e-test/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" \
|
|
--e2e-override="$E2E_OVERRIDE" \
|
|
--poll-attempts=24
|
|
RC=$?
|
|
echo "tsio-report-status exit=$RC upstream_succeeded=$UPSTREAM context=$CONTEXT"
|
|
if [ "$RC" -ne 0 ] && [ "$UPSTREAM" = "true" ]; then
|
|
echo "::error::TSIO reported test failures for $CONTEXT"
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
|
|
- name: Save 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
|
|
if: always()
|
|
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
|
|
# Match iOS (90). CI 30447839548: apt-get update ~38m then suite cancelled mid leave_call
|
|
# at 60m with 7 passed / 0 failures (SHARD_CANCELLED → STATUS=error).
|
|
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 }}
|
|
|
|
# Moved before node/Maestro/seed: zero deps on checkout beyond the action,
|
|
# and apt-get update can be slow (~38m on run 30447839548).
|
|
- name: Install system dependencies
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y --no-install-recommends libpulse0 scrot ffmpeg xvfb
|
|
|
|
- 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-00f516729f57d48f36ded55244cfbe358ada6b00-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
|
|
|
|
- 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
|
|
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: patch server config to AllowDownloadLogs=false, run
|
|
# the single flow, restore on exit. continue-on-error so the main
|
|
# report stays authoritative.
|
|
if: always()
|
|
continue-on-error: true
|
|
run: |
|
|
set -a
|
|
source detox/maestro/.maestro-test-env.sh
|
|
set +a
|
|
|
|
# AllowDownloadLogs lives under SupportSettings (not ServiceSettings)
|
|
# in the Mattermost server config — verified during local maestro
|
|
# iteration against SITE_1_URL.
|
|
echo "==> Patching SupportSettings.AllowDownloadLogs=false on ${SITE_1_URL}"
|
|
curl -f -sS --show-error -X PUT \
|
|
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"SupportSettings":{"AllowDownloadLogs":false}}' \
|
|
"${SITE_1_URL}/api/v4/config/patch" > /dev/null
|
|
|
|
trap 'curl -f -sS --show-error -X PUT \
|
|
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"SupportSettings\":{\"AllowDownloadLogs\":true}}" \
|
|
"${SITE_1_URL}/api/v4/config/patch" > /dev/null && \
|
|
echo "==> Restored AllowDownloadLogs=true"' EXIT
|
|
|
|
~/.maestro/bin/maestro test \
|
|
--format junit \
|
|
--output build/maestro-report-T67856_4.xml \
|
|
--test-output-dir build/maestro-artifacts-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: Upload Maestro Artifacts
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: maestro-android-results-${{ github.run_id }}
|
|
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'));
|
|
}
|
|
"
|
|
|
|
- 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@0661accd8cc9ab9dde6ec93f6f5ad40d06deb0d3 # release-0.14.0 / main@0661acc (#93 unique release tags per run)
|
|
with:
|
|
composite-identity: ${{ toJSON(fromJSON(steps.tsio-cfg.outputs.config).composite_identity) }}
|
|
total-reports-expected: ${{ fromJSON(steps.tsio-cfg.outputs.config).total_reports_expected }}
|
|
framework: 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 }}
|
|
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"
|
|
if [ "${{ job.status }}" = "cancelled" ]; then UPSTREAM="false"; fi
|
|
IDENTITY=$(echo "$TSIO_CONFIG" | jq -c '.composite_identity')
|
|
CONTEXT=$(echo "$TSIO_CONFIG" | jq -r '.status_context // "e2e-test/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" \
|
|
--e2e-override="$E2E_OVERRIDE" \
|
|
--poll-attempts=24
|
|
RC=$?
|
|
echo "tsio-report-status exit=$RC upstream_succeeded=$UPSTREAM context=$CONTEXT"
|
|
if [ "$RC" -ne 0 ] && [ "$UPSTREAM" = "true" ]; then
|
|
echo "::error::TSIO reported test failures for $CONTEXT"
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
|
|
- name: Save 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
|
|
if: always()
|
|
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
|