mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-06 02:40:15 +00:00
* 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>
330 lines
12 KiB
Bash
Executable File
330 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
# Reference: https://wix.github.io/Detox/docs/guide/android-dev-env
|
|
|
|
set -ex
|
|
set -o pipefail
|
|
|
|
SDK_VERSION=${1:-35} # First argument is SDK version
|
|
AVD_BASE_NAME=${2:-"detox_pixel_8"} # Second argument is AVD base name (no api suffix — added below)
|
|
AVD_NAME="${AVD_BASE_NAME}_api_${SDK_VERSION}"
|
|
TEST_FILES=("${@:3}") # Capture all remaining arguments as Detox test files
|
|
EMULATOR_RAM_MB=${MM_ANDROID_EMULATOR_RAM_MB:-3072}
|
|
|
|
setup_avd_home() {
|
|
if [[ "$CI" == "true" ]]; then
|
|
export ANDROID_AVD_HOME=$(pwd)/.android/avd
|
|
mkdir -p "$ANDROID_AVD_HOME"
|
|
fi
|
|
}
|
|
|
|
get_cpu_architecture() {
|
|
if [[ $(uname -p) == 'arm' ]]; then
|
|
echo "arm64-v8a arm64"
|
|
else
|
|
echo "x86_64 x86_64"
|
|
fi
|
|
}
|
|
|
|
create_avd() {
|
|
local cpu_arch_family cpu_arch
|
|
read cpu_arch_family cpu_arch < <(get_cpu_architecture)
|
|
|
|
avdmanager create avd -n "$AVD_NAME" -k "system-images;android-${SDK_VERSION};google_apis;${cpu_arch_family}" -p "$AVD_NAME" -d 'pixel_4_xl'
|
|
|
|
cp -r android_emulator/ "$AVD_NAME/"
|
|
sed -i -e "s|AvdId = change_avd_id|AvdId = ${AVD_NAME}|g" "$AVD_NAME/config.ini"
|
|
sed -i -e "s|avd.ini.displayname = change_avd_displayname|avd.ini.displayname = Detox Pixel 8 API ${SDK_VERSION}|g" "$AVD_NAME/config.ini"
|
|
sed -i -e "s|abi.type = change_type|abi.type = ${cpu_arch_family}|g" "$AVD_NAME/config.ini"
|
|
sed -i -e "s|hw.cpu.arch = change_cpu_arch|hw.cpu.arch = ${cpu_arch}|g" "$AVD_NAME/config.ini"
|
|
sed -i -e "s|image.sysdir.1 = change_to_image_sysdir/|image.sysdir.1 = system-images/android-${SDK_VERSION}/google_apis/${cpu_arch_family}/|g" "$AVD_NAME/config.ini"
|
|
sed -i -e "s|hw.ramSize = .*|hw.ramSize = ${EMULATOR_RAM_MB}|g" "$AVD_NAME/config.ini"
|
|
|
|
echo "Android virtual device successfully created: ${AVD_NAME}"
|
|
}
|
|
|
|
update_existing_avd_memory() {
|
|
local config_path="${AVD_NAME}/config.ini"
|
|
if [[ -f "$config_path" ]]; then
|
|
sed -i -e "s|hw.ramSize = .*|hw.ramSize = ${EMULATOR_RAM_MB}|g" "$config_path"
|
|
fi
|
|
}
|
|
|
|
prepare_avd_for_boot() {
|
|
# Cached AVDs may include snapshot/userdata state that causes
|
|
# "snapshot operation is pending and timeout has expired" on boot.
|
|
rm -rf "${AVD_NAME}/snapshots" 2>/dev/null || true
|
|
rm -f "${AVD_NAME}"/userdata*.img* "${AVD_NAME}"/cache.img* 2>/dev/null || true
|
|
|
|
if [[ "$CI" == "true" ]]; then
|
|
# Restored AVD caches can retain lock files from a crashed prior boot, which
|
|
# makes the emulator exit with "Running multiple emulators with the same AVD".
|
|
rm -f "${AVD_NAME}"/*.lock "${AVD_NAME}"/multiinstance.lock 2>/dev/null || true
|
|
# grep exits 1 when no emulators are listed; pipefail would abort bootstrap.
|
|
while read -r serial; do
|
|
[[ -z "$serial" ]] && continue
|
|
adb -s "$serial" emu kill 2>/dev/null || true
|
|
done < <( (adb devices 2>/dev/null | grep -E '^emulator-' | cut -f1) || true)
|
|
sleep 2
|
|
fi
|
|
}
|
|
|
|
start_adb_server() {
|
|
echo "Restarting ADB server..."
|
|
adb kill-server
|
|
adb start-server
|
|
}
|
|
|
|
start_emulator() {
|
|
echo "Starting the emulator..."
|
|
local emulator_opts="-avd $AVD_NAME -no-snapshot -no-snapshot-load -no-snapshot-save -no-boot-anim -no-audio -gpu off -no-window"
|
|
|
|
if [[ "$CI" == "true" || "$(uname -s)" == "Linux" ]]; then
|
|
emulator $emulator_opts -gpu swiftshader_indirect -accel on -qemu -m "$EMULATOR_RAM_MB" &
|
|
else
|
|
emulator $emulator_opts -gpu guest -verbose -qemu -vnc :0
|
|
fi
|
|
}
|
|
|
|
emulator_process_running() {
|
|
pgrep -f "emulator.*${AVD_NAME}" >/dev/null 2>&1 || pgrep -f 'qemu-system-x86_64' >/dev/null 2>&1
|
|
}
|
|
|
|
wait_for_emulator() {
|
|
if [[ "$CI" != "true" ]]; then return; fi
|
|
|
|
echo "Waiting for emulator to boot..."
|
|
local device_timeout=120
|
|
local device_elapsed=0
|
|
until adb devices | grep -qE '^emulator-[0-9]+\s+device'; do
|
|
if [[ $device_elapsed -ge $device_timeout ]]; then
|
|
echo "Emulator device did not appear within ${device_timeout}s"
|
|
adb devices -l || true
|
|
exit 1
|
|
fi
|
|
if ! emulator_process_running; then
|
|
echo "Emulator process exited before device appeared (check snapshot/cache state)"
|
|
exit 1
|
|
fi
|
|
sleep 5
|
|
device_elapsed=$((device_elapsed + 5))
|
|
done
|
|
|
|
local boot_timeout=300 # 5 minutes max
|
|
local boot_elapsed=0
|
|
until [[ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" == "1" ]]; do
|
|
if [[ $boot_elapsed -ge $boot_timeout ]]; then
|
|
echo "Emulator failed to boot within 5 minutes"
|
|
exit 1
|
|
fi
|
|
if ! emulator_process_running; then
|
|
echo "Emulator process exited during boot"
|
|
exit 1
|
|
fi
|
|
echo "Waiting for emulator to fully boot..."
|
|
sleep 10
|
|
boot_elapsed=$((boot_elapsed + 10))
|
|
done
|
|
echo "Emulator is fully booted."
|
|
|
|
# API 35 emulators take longer to stabilize the instrumentation layer after
|
|
# sys.boot_completed — 15s was insufficient, causing the first Detox app
|
|
# launch to fail with "No activities in stage RESUMED".
|
|
sleep 30
|
|
adb shell pm list packages > /dev/null 2>&1
|
|
echo "Emulator is fully ready."
|
|
}
|
|
|
|
resolve_app_apk() {
|
|
if [[ "${MAESTRO_ANDROID:-}" == "true" ]]; then
|
|
local release_apk="../android/app/build/outputs/apk/release/app-release.apk"
|
|
if [[ -f "$release_apk" ]]; then
|
|
echo "$release_apk"
|
|
return 0
|
|
fi
|
|
echo "Maestro release APK not found at $release_apk" >&2
|
|
ls -la ../android/app/build/outputs/apk/release/ 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
echo "../android/app/build/outputs/apk/debug/app-debug.apk"
|
|
}
|
|
|
|
install_app() {
|
|
local app_apk
|
|
app_apk=$(resolve_app_apk)
|
|
echo "Installing the app from $app_apk..."
|
|
adb install -r "$app_apk"
|
|
if [[ "${MAESTRO_ANDROID:-}" != "true" && "${BOOTSTRAP_ONLY:-}" != "true" ]]; then
|
|
# Install the test/instrumentation APK — required by Detox (reinstallApp: false assumes
|
|
# both the main APK and the test APK are already present on the device).
|
|
adb install -r ../android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
|
|
fi
|
|
adb shell pm list packages | grep "com.mattermost.rnbeta" && echo "App is installed." || echo "App is not installed."
|
|
}
|
|
|
|
grant_android_runtime_permissions() {
|
|
local bundle_id="com.mattermost.rnbeta"
|
|
adb shell pm grant "$bundle_id" android.permission.POST_NOTIFICATIONS 2>/dev/null || true
|
|
adb shell pm grant "$bundle_id" android.permission.RECORD_AUDIO 2>/dev/null || true
|
|
adb shell pm grant "$bundle_id" android.permission.CAMERA 2>/dev/null || true
|
|
adb shell settings put secure show_ime_with_hard_keyboard 0 2>/dev/null || true
|
|
adb shell settings put secure spell_checker_enabled 0 2>/dev/null || true
|
|
adb shell settings put secure auto_text_enabled 0 2>/dev/null || true
|
|
# google_apis images ship Google Autofill; overlays on the server form hide
|
|
# FloatingTextInput testIDs from Maestro (same class of issue as iOS autofill).
|
|
adb shell settings put secure autofill_service null 2>/dev/null || true
|
|
}
|
|
|
|
configure_emulator_for_tests() {
|
|
adb shell settings put global window_animation_scale 0
|
|
adb shell settings put global transition_animation_scale 0
|
|
adb shell settings put global animator_duration_scale 0
|
|
adb shell input keyevent 82 2>/dev/null || true
|
|
adb root 2>/dev/null || true
|
|
adb shell setprop persist.sys.timezone "America/New_York" 2>/dev/null || true
|
|
adb shell setprop sys.timezone "America/New_York" 2>/dev/null || true
|
|
adb shell am broadcast -a android.intent.action.TIMEZONE_CHANGED \
|
|
--ez bypassUserRestrictions true 2>/dev/null || true
|
|
configure_chrome_for_ci
|
|
}
|
|
|
|
configure_chrome_for_ci() {
|
|
adb shell 'mkdir -p /data/local/tmp' 2>/dev/null || true
|
|
adb shell 'echo "chrome --disable-fre --no-first-run --no-default-browser-check" > /data/local/tmp/chrome-command-line' 2>/dev/null || true
|
|
adb shell am start -n com.android.chrome/com.google.android.apps.chrome.Main 2>/dev/null || true
|
|
sleep 3
|
|
adb shell input keyevent 4 2>/dev/null || true
|
|
}
|
|
|
|
push_e2e_fixtures() {
|
|
local fixture="../detox/e2e/support/fixtures/image.png"
|
|
if [[ -f "$fixture" ]]; then
|
|
adb push "$fixture" /sdcard/Download/test_bookmark.png
|
|
echo "Pushed test fixture to /sdcard/Download/test_bookmark.png"
|
|
adb shell am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE \
|
|
-d file:///sdcard/Download/test_bookmark.png 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
start_server() {
|
|
echo "Starting the server..."
|
|
cd ..
|
|
RUNNING_E2E=true npm run start &
|
|
local timeout=120 interval=5 elapsed=0
|
|
|
|
until nc -z localhost 8081; do
|
|
if [[ $elapsed -ge $timeout ]]; then
|
|
echo "Server did not start within 3 minutes."
|
|
exit 1
|
|
fi
|
|
echo "Waiting for server to be ready..."
|
|
sleep $interval
|
|
elapsed=$((elapsed + interval))
|
|
done
|
|
echo "Server is ready."
|
|
|
|
echo "Pre-warming Metro Android bundle before Detox launchApp..."
|
|
local bundle_url="http://127.0.0.1:8081/index.bundle?platform=android&dev=true&minify=false"
|
|
local metro_status_url="http://127.0.0.1:8081/status"
|
|
local status_timeout=120 status_elapsed=0 status_interval=2
|
|
|
|
# Wait for Metro HTTP — does not trigger a bundle build.
|
|
until curl -sf "$metro_status_url" 2>/dev/null | grep -q "packager-status:running"; do
|
|
if [[ $status_elapsed -ge $status_timeout ]]; then
|
|
echo "ERROR: Metro packager did not report running within ${status_timeout}s"
|
|
exit 1
|
|
fi
|
|
echo "Waiting for Metro packager status... (${status_elapsed}s)"
|
|
sleep $status_interval
|
|
status_elapsed=$((status_elapsed + status_interval))
|
|
done
|
|
|
|
# One bundle request only. Polling the bundle URL in a loop starts a fresh Metro
|
|
# build on every curl (CI log: 94% → 0% repeatedly) and never finishes.
|
|
echo "Compiling Android bundle (single request, up to 300s)..."
|
|
local bundle_timeout=300
|
|
local bundle_file
|
|
bundle_file=$(mktemp)
|
|
if ! curl -sf --max-time "$bundle_timeout" "$bundle_url" -o "$bundle_file"; then
|
|
echo "ERROR: failed to download Metro Android bundle within ${bundle_timeout}s"
|
|
rm -f "$bundle_file"
|
|
exit 1
|
|
fi
|
|
|
|
local bundle_bytes
|
|
bundle_bytes=$(wc -c < "$bundle_file" | tr -d ' ')
|
|
if [[ "$bundle_bytes" -lt 500000 ]]; then
|
|
echo "ERROR: Metro Android bundle too small (${bundle_bytes} bytes)"
|
|
head -c 500 "$bundle_file" || true
|
|
rm -f "$bundle_file"
|
|
exit 1
|
|
fi
|
|
if ! grep -q '__d' "$bundle_file"; then
|
|
echo "ERROR: Metro Android bundle missing __d module marker"
|
|
rm -f "$bundle_file"
|
|
exit 1
|
|
fi
|
|
rm -f "$bundle_file"
|
|
echo "Metro Android bundle is ready (${bundle_bytes} bytes)."
|
|
}
|
|
|
|
setup_adb_reverse() {
|
|
echo "Setting up ADB reverse port forwarding..."
|
|
# adb root (in configure_emulator_for_tests) restarts adbd and drops prior reverse mappings.
|
|
adb reverse --remove-all 2>/dev/null || true
|
|
adb reverse tcp:8081 tcp:8081
|
|
if ! adb reverse --list | grep -q 'tcp:8081'; then
|
|
echo "ERROR: adb reverse tcp:8081 is not active after setup"
|
|
adb reverse --list || true
|
|
exit 1
|
|
fi
|
|
echo "adb reverse verified: $(adb reverse --list)"
|
|
}
|
|
|
|
run_detox_tests() {
|
|
echo "Running Detox tests... $@"
|
|
|
|
cd detox
|
|
AVD_NAME="$AVD_NAME" npm run detox:config-gen
|
|
mkdir -p artifacts
|
|
npm run e2e:android-test -- "$@" -- --json --outputFile=artifacts/jest-results.json
|
|
}
|
|
|
|
main() {
|
|
setup_avd_home
|
|
|
|
if ! emulator -list-avds | grep -q "$AVD_NAME"; then
|
|
create_avd
|
|
else
|
|
echo "'${AVD_NAME}' Android virtual device already exists."
|
|
update_existing_avd_memory
|
|
fi
|
|
|
|
prepare_avd_for_boot
|
|
start_adb_server
|
|
start_emulator
|
|
wait_for_emulator
|
|
|
|
if [[ "$CI" == "true" ]]; then
|
|
install_app
|
|
# Maestro uses a release APK with an embedded bundle (mirrors iOS simulator builds).
|
|
if [[ "${MAESTRO_ANDROID:-}" != "true" ]]; then
|
|
start_server
|
|
fi
|
|
grant_android_runtime_permissions
|
|
configure_emulator_for_tests
|
|
if [[ "${MAESTRO_ANDROID:-}" != "true" ]]; then
|
|
setup_adb_reverse
|
|
fi
|
|
push_e2e_fixtures
|
|
fi
|
|
|
|
if [[ "${BOOTSTRAP_ONLY:-}" == "true" ]]; then
|
|
echo "Bootstrap complete (BOOTSTRAP_ONLY=true) — skipping Detox tests"
|
|
exit 0
|
|
fi
|
|
|
|
run_detox_tests "${TEST_FILES[@]}"
|
|
}
|
|
|
|
main
|