mattermost-mobile-upstream/detox/scripts/run_android_ci_shard.sh
yasser khan 93b1863b58
Some checks are pending
ci / lint-typecheck (push) Waiting to run
ci / detox-lint-typecheck (push) Waiting to run
ci / test (push) Waiting to run
ci(e2e): triage red Detox and Maestro runs against test history (#10172)
* ci(e2e): triage red Detox and Maestro runs against test history

When a Detox or Maestro lane is red, the toolkit's e2e-triage action checks
each failure against TSIO history (flaky or broken on main, the same failure on
other PRs, environment errors, or caused by this PR) and adds its verdict to the
lane's status and the job summary. Report-only unless E2E_TRIAGE_MODE is enforce.

- .github/actions/e2e-triage: the per-lane triage steps (evidence download,
  answers cache, toolkit e2e-triage) as one composite, checked out from the
  dispatch ref so the branch under test never runs with the API key.
- Each lane uploads what it saw at a failure (screenshots, the step it
  stopped at, the server's answer) as triage evidence.
- The TSIO status links name the run only when both the run id and the
  attempt are known, so triage writes only on statuses its run owns.

Toolkit pinned to bdce26e.

* ci(e2e): pin the toolkit to 24bf208; use the renamed e2e-test-* actions

The toolkit renamed e2e-triage to e2e-test-triage and e2e-status to
e2e-test-status; inputs are unchanged. Triage now holds a failure repeated on
this PR's last commits and never on the base branch as out of date (merge the
base branch and re-run) instead of clearing it, and the status line names each
kind: flaky, broken on main, caused by PR, out of date, needs investigation.

* ci(e2e): pin the toolkit to main (419b0a9)

Toolkit #5 merged; this pins its merge on main instead of the PR branch.

* ci(e2e): pin the toolkit to main (b3ca677)

Toolkit #6 merged: a run flagged as a whole now says why once instead of repeating "investigation required".

* ci(e2e): pin the toolkit to e2e-triage-judge-flagged-runs (f1f5d54, toolkit#8)

* ci(e2e): pin the toolkit to e2e-triage-judge-flagged-runs (77c3048, toolkit#8)

* ci(e2e): use the toolkit's main while triage is observed

toolkit#8 merged (b7757e9). Following review, consumers track main so
toolkit changes need no bump here; pin a hash again once stable.

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2026-10-26 10:26:45 +05:30

182 lines
6.5 KiB
Bash
Executable File

#!/bin/bash
# Run one Android Detox shard. On failure, retry only failed/unrun files.
# Keep the emulator if it is still healthy; cold-boot only when it is not.
set -u
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DETOX_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
SDK_VERSION="${1:?SDK version required}"
AVD_NAME="${2:?AVD name required}"
shift 2
SHARD_SPECS=("$@")
if (( ${#SHARD_SPECS[@]} == 0 )); then
echo "No specs provided"
exit 1
fi
MAX_ATTEMPTS="${MAX_ATTEMPTS:-2}"
ATTEMPT_TIMEOUT_MIN="${ATTEMPT_TIMEOUT_MIN:-60}"
RESULTS="${DETOX_DIR}/artifacts/jest-results.json"
ATTEMPT1_RESULTS="${DETOX_DIR}/artifacts/jest-results-attempt1.json"
FAILED_SPECS_JS="${DETOX_DIR}/utils/failed-jest-specs.js"
# Jest --outputFile can miss the shutdown flush while jest-stare still wrote
# android-data.json (CI 33915931136 machine-2) — and `timeout` killing an attempt
# always lands there. Recovering it BETWEEN attempts is what makes the retry
# correct: listRetrySpecs treats a missing report as "retry the whole shard", so a
# timed-out attempt 1 re-runs specs that already passed, and mergeJestResultsPreferLater
# then lets the retry's result win. That is how channel_summary and settings — both
# green at 08:47 — were reported as 8 failures (CI 34099282816 machine-2).
# Returns 0 when a usable report exists afterwards, 1 when there is nothing to promote.
promote_jest_stare_results() {
if [ -f "$RESULTS" ]; then
return 0
fi
local stare="${DETOX_DIR}/artifacts/jest-stare/android-data.json"
if [ -f "$stare" ]; then
echo "==> Detox left no jest-results.json — promoting jest-stare/android-data.json"
cp "$stare" "$RESULTS"
return 0
fi
return 1
}
write_missing_results_stub() {
if promote_jest_stare_results; then
return
fi
echo "==> Detox left no jest-results.json — writing shard stub"
node "${DETOX_DIR}/utils/write-tsio-failure-stub.mjs" \
--format jest \
--output "$RESULTS" \
--job-name "${AVD_NAME:-android-shard}" \
--reason "Detox exited before writing jest-results.json (specs: ${SHARD_SPECS[*]})"
}
trap write_missing_results_stub EXIT
emulator_healthy() {
adb devices 2>/dev/null | grep -qE '^emulator-[0-9]+\s+device' || return 1
[[ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" == "1" ]] || return 1
adb shell pm list packages 2>/dev/null | grep -q 'com.mattermost.rnbeta' || return 1
}
# Every test in attempt 1 failed and none passed: the app never came up on this
# emulator. CI 37082736971 machine-11 logged "Detox can't seem to connect to the
# test app" on every launch while adb, boot and the installed package all looked
# fine, and reusing that emulator failed the retry the same way. Cold-boot instead.
attempt1_never_reached_app() {
[[ -f "${ATTEMPT1_RESULTS}" ]] || return 1
node -e '
const r = require(process.argv[1]);
process.exit((r.numFailedTests ?? 0) > 0 && (r.numPassedTests ?? 0) === 0 ? 0 : 1);
' "${ATTEMPT1_RESULTS}" 2>/dev/null
}
kill_emulator() {
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
}
run_detox_attempt() {
local tests_only="${1:-false}"
shift
(
cd "${DETOX_DIR}"
chmod +x ./create_android_emulator.sh
if [[ "${tests_only}" == "true" ]]; then
TESTS_ONLY=true CI=true timeout "${ATTEMPT_TIMEOUT_MIN}m" \
./create_android_emulator.sh "${SDK_VERSION}" "${AVD_NAME}" "$@"
else
CI=true timeout "${ATTEMPT_TIMEOUT_MIN}m" \
./create_android_emulator.sh "${SDK_VERSION}" "${AVD_NAME}" "$@"
fi
)
}
merge_attempt_results() {
if [[ ! -f "${ATTEMPT1_RESULTS}" ]]; then
return 0
fi
if [[ ! -f "${RESULTS}" ]]; then
mv "${ATTEMPT1_RESULTS}" "${RESULTS}"
return 0
fi
node "${FAILED_SPECS_JS}" \
--attempt1 "${ATTEMPT1_RESULTS}" \
--attempt2 "${RESULTS}" \
--output "${RESULTS}"
}
attempt=1
while (( attempt <= MAX_ATTEMPTS )); do
echo "==> Attempt ${attempt}/${MAX_ATTEMPTS} (timeout ${ATTEMPT_TIMEOUT_MIN}m)"
if (( attempt == 1 )); then
run_detox_attempt false "${SHARD_SPECS[@]}"
rc=$?
# Recover the results of the suites that did finish before deciding what to
# retry — a `timeout` kill (exit 124) never lets Jest write --outputFile.
promote_jest_stare_results || true
else
retry_out="$(node "${FAILED_SPECS_JS}" \
--results "${RESULTS}" \
--shard-specs "${SHARD_SPECS[*]}")"
node_rc=$?
retry_specs=()
if (( node_rc == 0 )) && [[ -n "${retry_out}" ]]; then
while IFS= read -r line; do
[[ -n "${line}" ]] && retry_specs+=("${line}")
done <<< "${retry_out}"
fi
if (( ${#retry_specs[@]} == 0 )); then
echo "==> Could not determine failed specs — retrying the whole shard"
retry_specs=("${SHARD_SPECS[@]}")
fi
echo "==> Retrying ${#retry_specs[@]} spec(s): ${retry_specs[*]}"
if [[ -f "${RESULTS}" ]]; then
mv "${RESULTS}" "${ATTEMPT1_RESULTS}"
fi
if emulator_healthy && ! attempt1_never_reached_app; then
echo "==> Emulator healthy — resetting app and retrying failed specs only"
run_detox_attempt true "${retry_specs[@]}"
rc=$?
if (( rc == 2 )); then
echo "==> Emulator went unhealthy during retry — cold boot"
kill_emulator
run_detox_attempt false "${retry_specs[@]}"
rc=$?
fi
else
echo "==> Emulator unhealthy, or no test passed in attempt 1 — cold boot, then retrying failed specs only"
kill_emulator
run_detox_attempt false "${retry_specs[@]}"
rc=$?
fi
# Same recovery for the retry: a second timeout must not throw away the
# suites it did finish, which merge_attempt_results would otherwise drop.
promote_jest_stare_results || true
merge_attempt_results
fi
if (( rc == 0 )); then
echo "==> Attempt ${attempt} succeeded"
exit 0
fi
echo "==> Attempt ${attempt} failed with exit ${rc}"
if (( attempt < MAX_ATTEMPTS )); then
echo "==> Will retry failed/unrun specs (not the whole shard)"
fi
attempt=$(( attempt + 1 ))
done
echo "==> All ${MAX_ATTEMPTS} attempts failed"
exit 1