name: Compatibility Matrix Testing on: workflow_dispatch: inputs: CMT_MATRIX: description: "A JSON object representing the testing matrix" required: true type: string MOBILE_VERSION: description: "The mattermost mobile version to test" required: true # Cancel overlapping CMT runs for the same ref; matterwick cleans up cancelled servers via workflow_run completed. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true # CMT runs against a freshly-cut build-release-NNN branch, so the app content # never matches main's fingerprint — the S3 build cache would always miss. # CMT's optimization is structural instead: each app (iOS, Android Detox, # Android Maestro) is built ONCE below and reused across every server-version # leg of the matrix via github.run_id artifacts. jobs: calculate-commit-hash: runs-on: ubuntu-24.04 outputs: MOBILE_SHA: ${{ steps.repo.outputs.MOBILE_SHA }} steps: - name: cmt/checkout-mobile uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false ref: ${{ inputs.MOBILE_VERSION }} - name: cmt/calculate-mattermost-sha id: repo run: echo "MOBILE_SHA=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}" # Phase 5 — base TSIO identity for this CMT run. Each matrix leg builds its # own per-job report group (total_reports_expected=1) from this base + tsio-shard-name. cmt-identity: name: Calculate CMT TSIO base identity runs-on: ubuntu-24.04 permissions: contents: read needs: - calculate-commit-hash - prepare-maestro-matrix outputs: tsio-base-config: ${{ steps.id.outputs.tsio-base-config }} tsio-job-configs: ${{ steps.id.outputs.tsio-job-configs }} tsio-job-keys: ${{ steps.id.outputs.tsio-job-keys }} steps: - name: Sparse checkout TSIO job-config helper uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.MOBILE_VERSION }} persist-credentials: false sparse-checkout: | detox/utils/build-tsio-job-config.js - id: id env: GITHUB_REPOSITORY: ${{ github.repository }} MM_SHA: ${{ needs.calculate-commit-hash.outputs.MOBILE_SHA }} MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }} CMT_MATRIX: ${{ inputs.CMT_MATRIX }} MAESTRO_MATRIX: ${{ needs.prepare-maestro-matrix.outputs.matrix }} run: | BRANCH=$(echo "$MOBILE_VERSION" | sed 's#^refs/heads/##; s#^refs/tags/##') BASE=$(jq -nc \ --arg repo "$GITHUB_REPOSITORY" --arg sha "$MM_SHA" --arg run_id "$GITHUB_RUN_ID" \ --arg attempt "$GITHUB_RUN_ATTEMPT" --arg branch "$BRANCH" \ '{repository:$repo, commit_sha:$sha, gh_run_id:$run_id, gh_run_attempt:$attempt, branch:$branch, name:"mobile-release", run_group:"mobile-release", framework:"detox"}') # No status_context — templates expand per tsio-shard-name via buildTsioJobConfig. TSIO_BASE=$(jq -nc --argjson identity "$BASE" \ '{composite_identity: $identity, total_reports_expected: 1}') echo "tsio-base-config=$TSIO_BASE" >> "$GITHUB_OUTPUT" # Per-shard configs so the final rollup can poll every leg's TSIO group. export BASE CMT_MATRIX MAESTRO_MATRIX JOB_CONFIGS=$(node -e ' const {buildTsioJobConfigMap, cmtJobKeys} = require("./detox/utils/build-tsio-job-config.js"); const base = JSON.parse(process.env.BASE); const keys = cmtJobKeys(JSON.parse(process.env.CMT_MATRIX), JSON.parse(process.env.MAESTRO_MATRIX)); process.stdout.write(JSON.stringify(buildTsioJobConfigMap(base, keys))); ') echo "tsio-job-configs=$JOB_CONFIGS" >> "$GITHUB_OUTPUT" JOB_KEYS=$(node -e ' const {cmtJobKeys} = require("./detox/utils/build-tsio-job-config.js"); process.stdout.write(JSON.stringify(cmtJobKeys(JSON.parse(process.env.CMT_MATRIX), JSON.parse(process.env.MAESTRO_MATRIX)))); ') echo "tsio-job-keys=$JOB_KEYS" >> "$GITHUB_OUTPUT" update-initial-status: runs-on: ubuntu-24.04 permissions: contents: read statuses: write needs: - calculate-commit-hash - cmt-identity steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.MOBILE_VERSION }} persist-credentials: false # Pending statuses are posted per matrix leg when each job starts; keep a # single umbrella pending only when MOBILE_SHA is known so the PR/commit # UI shows CMT is running before the first shard uploads. - name: Build TSIO report URL (release group prefix) id: tsio-url env: TSIO_BASE: ${{ needs.cmt-identity.outputs.tsio-base-config }} run: | URL=$(jq -nr --argjson cfg "$TSIO_BASE" --arg base "https://test-io.test.mattermost.com" ' ($cfg.composite_identity) as $id | ($id.repository | split("/") | .[-1]) as $repo | ($id.branch // "main" | sub("^refs/heads/"; "") | sub("^refs/tags/"; "") | gsub("/"; "~")) as $branch | ($id.commit_sha[0:7]) as $sha | "\($base)/reports/\($repo|@uri)/\($branch|@uri)/\($sha)/mobile-release?gh_run_id=\($id.gh_run_id)" ') echo "target_url=$URL" >> "$GITHUB_OUTPUT" - uses: ./.github/actions/update-commit-status if: ${{ needs.calculate-commit-hash.outputs.MOBILE_SHA != '' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: repository_full_name: ${{ github.repository }} commit_sha: ${{ needs.calculate-commit-hash.outputs.MOBILE_SHA }} context: e2e-test/compatibility-matrix-testing description: "Compatibility Matrix Testing for ${{ inputs.MOBILE_VERSION }} version — per-job checks also posted" status: pending target_url: ${{ steps.tsio-url.outputs.target_url }} # Maestro only runs against the latest server version; matrix filtering happens here since job-level if: can't access matrix. prepare-maestro-matrix: runs-on: ubuntu-24.04 outputs: matrix: ${{ steps.filter.outputs.matrix }} steps: - name: Filter CMT matrix to latest server only id: filter env: CMT_MATRIX: ${{ inputs.CMT_MATRIX }} run: | matrix=$(echo "$CMT_MATRIX" | jq -c '{server: [.server[] | select(.latest == true)]}') if [ "$(echo "$matrix" | jq '.server | length')" -eq 0 ]; then matrix=$(echo "$CMT_MATRIX" | jq -c '{server: [.server[-1]]}') fi echo "matrix=$matrix" >> "$GITHUB_OUTPUT" # Provisions license, config and plugins (demo plugin on full-suite site 1 and site 2) before tests. # CMT previously skipped that and only used Matterwick-booted servers, so # interactive_dialog_plugin failed 24× with "Demo plugin is not active". provision-cmt-servers: name: Provision CMT test servers runs-on: ubuntu-24.04 needs: - calculate-commit-hash steps: - name: Checkout Repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.MOBILE_VERSION }} persist-credentials: false - name: Collect unique matrix server URLs id: urls env: CMT_MATRIX: ${{ inputs.CMT_MATRIX }} run: | # Full-suite rows run the server-mutation specs, which need their own site 2; fail here, not after the builds. BAD_ROWS=$(echo "$CMT_MATRIX" | jq -r ' .server[] | select(.latest == true) | . as $row | ["android", "ios"][] | select(($row[. + "_site_2_url"] // "") == "" or $row[. + "_site_2_url"] == $row[. + "_site_1_url"]) | "\($row.version // "unknown"): \(.)_site_2_url is missing or equals \(.)_site_1_url" ') if [ -n "$BAD_ROWS" ]; then echo "::error::Full-suite CMT rows need a distinct site 2 URL per platform:" printf '%s\n' "$BAD_ROWS" exit 1 fi # The demo plugin goes only to full-suite site 1 and site 2 URLs; a shared SITE_3 or a smoke server must not be one of them. DEMO_CONFLICTS=$(echo "$CMT_MATRIX" | jq -r ' . as $m | ([ $m.server[] | select(.latest != true) | .android_site_1_url, .android_site_2_url, .ios_site_1_url, .ios_site_2_url, .site_3_url ] + [ $m.server[] | select(.latest == true) | .site_3_url ] | map(select(. != null and . != ""))) as $other | ([ $m.server[] | select(.latest == true) | .android_site_1_url, .ios_site_1_url ] | map(select(. != null and . != ""))) as $pool | $m.server[] | select(.latest == true) | . as $row | ["android", "ios"][] as $p | ["_site_1_url", "_site_2_url"][] as $k | $row[$p + $k] as $url | select($url != null and $url != "" and (($other | any(. == $url)) or ($k == "_site_2_url" and ($pool | any(. == $url))))) | "\($row.version // "unknown"): \($p)\($k) \($url) is also a pool, shared or smoke server" ') if [ -n "$DEMO_CONFLICTS" ]; then echo "::error::These full-suite URLs are also pool, shared or smoke servers (a site 2 must not be any pool site 1, and the demo plugin must not land on a shared or smoke server):" printf '%s\n' "$DEMO_CONFLICTS" exit 1 fi URLS=$(echo "$CMT_MATRIX" | jq -r ' .server[] | .android_site_1_url, .android_site_2_url, .ios_site_1_url, .ios_site_2_url, .site_3_url | select(. != null and . != "") ' | awk 'NF && !seen[$0]++') if [ -z "$URLS" ]; then echo "::error::CMT_MATRIX contained no server URLs to provision" exit 1 fi # Only full-suite rows run the dialog specs, on pool site 1 and mutation site 2 (smoke rows use smoke_test, which has none). DEMO_URLS=$(echo "$CMT_MATRIX" | jq -r ' .server[] | select(.latest == true) | .android_site_1_url, .android_site_2_url, .ios_site_1_url, .ios_site_2_url | select(. != null and . != "") ' | awk 'NF && !seen[$0]++') echo "Provisioning $(echo "$URLS" | wc -l | tr -d ' ') unique server(s)" { echo "list<> "$GITHUB_OUTPUT" - name: Provision test servers uses: ./.github/actions/provision-e2e-servers env: ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }} ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }} with: server-urls: ${{ steps.urls.outputs.list }} demo-plugin-server-urls: ${{ steps.urls.outputs.demo }} # Input follows the below schema # { # "server": [ # { # "version": "9.6.1", # "android_site_1_url": "https://android-1.example.test", # "android_site_2_url": "https://android-2.example.test", # "ios_site_1_url": "https://ios-1.example.test", # "ios_site_2_url": "https://ios-2.example.test", # "site_3_url": "https://shared-3.example.test" # }, # { # "version": "9.5.2", # "android_site_1_url": "https://android-1-older.example.test", # "android_site_2_url": "https://android-2-older.example.test", # "ios_site_1_url": "https://ios-1-older.example.test", # "ios_site_2_url": "https://ios-2-older.example.test", # "site_3_url": "https://shared-3-older.example.test" # } # ] # } build-ios-simulator: # macos-14 predates Xcode 26 and has none pre-installed, so prepare-ios-build's # Xcode 26 requirement always fails here. e2e-detox-pr.yml and # e2e-maestro-template.yml already build on macos-26 successfully — match that. runs-on: macos-26 needs: - update-initial-status permissions: contents: read actions: write steps: - name: Checkout Repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false ref: ${{ inputs.MOBILE_VERSION }} - name: Prepare iOS Build uses: ./.github/actions/prepare-ios-build - name: Build iOS Simulator env: TAG: "${{ inputs.MOBILE_VERSION }}" GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}" run: bundle exec fastlane ios simulator --env ios.simulator working-directory: ./fastlane - name: Upload iOS Simulator Build uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ios-build-simulator-${{ github.run_id }} path: Mattermost-simulator-*.app.zip build-android-apk: runs-on: ubuntu-latest-8-cores needs: - update-initial-status permissions: contents: read actions: write env: ORG_GRADLE_PROJECT_jvmargs: -Xmx8g steps: - name: Prune Docker to free up space run: docker system prune -af - name: Checkout Repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false ref: ${{ inputs.MOBILE_VERSION }} - name: Prepare Android Build uses: ./.github/actions/prepare-android-build env: STORE_FILE: ${{ secrets.MM_MOBILE_STORE_FILE }} STORE_ALIAS: ${{ secrets.MM_MOBILE_STORE_ALIAS }} STORE_PASSWORD: ${{ secrets.MM_MOBILE_STORE_PASSWORD }} MATTERMOST_BUILD_GH_TOKEN: ${{ secrets.MATTERMOST_BUILD_GH_TOKEN }} - name: Detox build run: | cd detox npm ci --prefer-offline --no-audit --no-fund npm run e2e:android-build - name: Upload Android Build uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: android-build-files-${{ github.run_id }} # Match e2e-detox-pr.yml's glob shape exactly: the wildcard must start # right after "build/" so upload-artifact only strips "android/app/build/" # and preserves the outputs/apk/... subtree. e2e-android-template.yml's # Download APK artifact step (shared by PR + CMT) expects that shape. path: "android/app/build/**/*" build-android-apk-maestro: runs-on: ubuntu-latest-8-cores needs: - update-initial-status permissions: contents: read actions: write env: ORG_GRADLE_PROJECT_jvmargs: -Xmx8g steps: - name: Prune Docker to free up space run: docker system prune -af - name: Checkout Repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false ref: ${{ inputs.MOBILE_VERSION }} - name: Prepare Android Build uses: ./.github/actions/prepare-android-build env: STORE_FILE: ${{ secrets.MM_MOBILE_STORE_FILE }} STORE_ALIAS: ${{ secrets.MM_MOBILE_STORE_ALIAS }} STORE_PASSWORD: ${{ secrets.MM_MOBILE_STORE_PASSWORD }} MATTERMOST_BUILD_GH_TOKEN: ${{ secrets.MATTERMOST_BUILD_GH_TOKEN }} - name: Build Maestro Android APK uses: ./.github/actions/build-android-maestro-apk - name: Upload Maestro Android Build uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: android-maestro-build-files-${{ github.run_id }} path: android/app/build/outputs/apk/release/ # latest entry runs full suite, older entries run smoke only. Job-level if: can't conditionally skip matrix legs, so inputs vary instead. detox-e2e: name: mobile-cmt-ios${{ matrix.server.latest && '-full' || '-smoke' }}-${{ matrix.server.version }} permissions: contents: read # nested template mints an OIDC token for the TSIO upload. id-token: write actions: read statuses: write # Nested template finalize reads PR labels to honour E2E/Override; a # reusable workflow cannot escalate past the caller, so grant it here too. issues: read uses: ./.github/workflows/e2e-ios-template.yml needs: - build-ios-simulator - cmt-identity - provision-cmt-servers strategy: fail-fast: false matrix: ${{ fromJson(inputs.CMT_MATRIX) }} secrets: MM_MOBILE_E2E_ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }} MM_MOBILE_E2E_ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }} MM_MOBILE_E2E_ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }} MM_MOBILE_E2E_ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }} MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX: ${{ secrets.MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX }} MM_MOBILE_E2E_WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }} MM_E2E_MASTER_HEALTH_WEBHOOK_URL: ${{ secrets.MM_E2E_MASTER_HEALTH_WEBHOOK_URL }} MM_E2E_RELEASE_WEBHOOK_URL: ${{ secrets.MM_E2E_RELEASE_WEBHOOK_URL }} MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }} MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }} MM_MOBILE_E2E_WEBHOOK_PUBLIC_BASE_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_PUBLIC_BASE_URL }} MM_MOBILE_E2E_CLOUDFLARED_TUNNEL_TOKEN: ${{ secrets.MM_MOBILE_E2E_CLOUDFLARED_TUNNEL_TOKEN }} with: tsio-config: ${{ needs.cmt-identity.outputs.tsio-base-config }} tsio-shard-name: detox-ios-Server_${{ matrix.server.version }} run-type: "RELEASE" MM_OTHER_PLATFORM_POOL_URL: ${{ matrix.server.android_site_1_url }} MM_TEST_SERVER_URL: ${{ matrix.server.ios_site_1_url }} MM_TEST_SERVER_URL_2: ${{ matrix.server.ios_site_2_url }} MM_TEST_SERVER_URL_3: ${{ matrix.server.site_3_url }} MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }} search_path: ${{ matrix.server.latest && 'detox/e2e/test' || 'detox/e2e/test/products/channels/smoke_test' }} # Full suite matches the PR/main shard count (template default 32); smoke stays single-shard. parallelism: ${{ matrix.server.latest && '32' || '1' }} artifact_run_id: ${{ github.run_id }} detox-ipad-e2e: name: mobile-cmt-ipad-${{ matrix.server.version }} permissions: contents: read id-token: write actions: read statuses: write # Nested template finalize reads PR labels to honour E2E/Override; a # reusable workflow cannot escalate past the caller, so grant it here too. issues: read uses: ./.github/workflows/e2e-ios-template.yml needs: - build-ios-simulator - cmt-identity - provision-cmt-servers strategy: fail-fast: false matrix: ${{ fromJson(inputs.CMT_MATRIX) }} secrets: MM_MOBILE_E2E_ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }} MM_MOBILE_E2E_ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }} MM_MOBILE_E2E_ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }} MM_MOBILE_E2E_ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }} MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX: ${{ secrets.MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX }} MM_MOBILE_E2E_WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }} MM_E2E_MASTER_HEALTH_WEBHOOK_URL: ${{ secrets.MM_E2E_MASTER_HEALTH_WEBHOOK_URL }} MM_E2E_RELEASE_WEBHOOK_URL: ${{ secrets.MM_E2E_RELEASE_WEBHOOK_URL }} MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }} MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }} MM_MOBILE_E2E_WEBHOOK_PUBLIC_BASE_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_PUBLIC_BASE_URL }} MM_MOBILE_E2E_CLOUDFLARED_TUNNEL_TOKEN: ${{ secrets.MM_MOBILE_E2E_CLOUDFLARED_TUNNEL_TOKEN }} with: tsio-config: ${{ needs.cmt-identity.outputs.tsio-base-config }} tsio-shard-name: detox-ipad-Server_${{ matrix.server.version }} run-type: "RELEASE" MM_OTHER_PLATFORM_POOL_URL: ${{ matrix.server.android_site_1_url }} MM_TEST_SERVER_URL: ${{ matrix.server.ios_site_1_url }} MM_TEST_SERVER_URL_2: ${{ matrix.server.ios_site_2_url }} MM_TEST_SERVER_URL_3: ${{ matrix.server.site_3_url }} MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }} ios_device_name: "iPad Pro 13-inch (M5)" ios_device_os_name: "iOS 26.2" search_path: "detox/e2e/test/products/channels/ipad" parallelism: "1" detox_config: "ios.ipad.debug" artifact_run_id: ${{ github.run_id }} # Maestro runs only against the latest server version (no smoke-on-older variant). maestro-ios-e2e: name: mobile-cmt-maestro-ios-${{ matrix.server.version }} permissions: contents: read id-token: write actions: read statuses: write # Nested template finalize reads PR labels to honour E2E/Override; a # reusable workflow cannot escalate past the caller, so grant it here too. issues: read uses: ./.github/workflows/e2e-maestro-template.yml needs: - build-ios-simulator - prepare-maestro-matrix - cmt-identity - provision-cmt-servers strategy: fail-fast: false matrix: ${{ fromJson(needs.prepare-maestro-matrix.outputs.matrix) }} secrets: MM_MOBILE_E2E_ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }} MM_MOBILE_E2E_ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }} MM_MOBILE_E2E_ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }} MM_MOBILE_E2E_ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }} MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }} MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }} MM_MOBILE_E2E_WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }} MM_E2E_MASTER_HEALTH_WEBHOOK_URL: ${{ secrets.MM_E2E_MASTER_HEALTH_WEBHOOK_URL }} MM_E2E_RELEASE_WEBHOOK_URL: ${{ secrets.MM_E2E_RELEASE_WEBHOOK_URL }} with: tsio-config: ${{ needs.cmt-identity.outputs.tsio-base-config }} tsio-shard-name: maestro-ios-Server_${{ matrix.server.version }} platform: ios run-type: "RELEASE" MM_TEST_SERVER_URL: ${{ matrix.server.ios_site_1_url }} MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }} record_tests_in_zephyr: true artifact_run_id: ${{ github.run_id }} detox-android-e2e: name: mobile-cmt-android${{ matrix.server.latest && '-full' || '-smoke' }}-${{ matrix.server.version }} permissions: contents: read id-token: write actions: read statuses: write # Nested template finalize reads PR labels to honour E2E/Override; a # reusable workflow cannot escalate past the caller, so grant it here too. issues: read uses: ./.github/workflows/e2e-android-template.yml needs: - build-android-apk - cmt-identity - provision-cmt-servers strategy: fail-fast: false matrix: ${{ fromJson(inputs.CMT_MATRIX) }} secrets: MM_MOBILE_E2E_ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }} MM_MOBILE_E2E_ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }} MM_MOBILE_E2E_ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }} MM_MOBILE_E2E_ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }} MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX: ${{ secrets.MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX }} MM_MOBILE_E2E_WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }} MM_E2E_MASTER_HEALTH_WEBHOOK_URL: ${{ secrets.MM_E2E_MASTER_HEALTH_WEBHOOK_URL }} MM_E2E_RELEASE_WEBHOOK_URL: ${{ secrets.MM_E2E_RELEASE_WEBHOOK_URL }} MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }} MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }} MM_MOBILE_E2E_WEBHOOK_PUBLIC_BASE_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_PUBLIC_BASE_URL }} MM_MOBILE_E2E_CLOUDFLARED_TUNNEL_TOKEN: ${{ secrets.MM_MOBILE_E2E_CLOUDFLARED_TUNNEL_TOKEN }} with: tsio-config: ${{ needs.cmt-identity.outputs.tsio-base-config }} tsio-shard-name: detox-android-Server_${{ matrix.server.version }} run-android-tests: true run-type: "RELEASE" MM_OTHER_PLATFORM_POOL_URL: ${{ matrix.server.ios_site_1_url }} MM_TEST_SERVER_URL: ${{ matrix.server.android_site_1_url }} MM_TEST_SERVER_URL_2: ${{ matrix.server.android_site_2_url }} MM_TEST_SERVER_URL_3: ${{ matrix.server.site_3_url }} MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }} search_path: ${{ matrix.server.latest && 'detox/e2e/test' || 'detox/e2e/test/products/channels/smoke_test' }} # Full suite matches the PR/main shard count (template default 32); smoke stays single-shard. parallelism: ${{ matrix.server.latest && '32' || '1' }} artifact_run_id: ${{ github.run_id }} # Maestro runs only against the latest server version (no smoke-on-older variant). maestro-android-e2e: name: mobile-cmt-maestro-android-${{ matrix.server.version }} permissions: contents: read id-token: write actions: read statuses: write # Nested template finalize reads PR labels to honour E2E/Override; a # reusable workflow cannot escalate past the caller, so grant it here too. issues: read uses: ./.github/workflows/e2e-maestro-template.yml needs: - build-android-apk-maestro - prepare-maestro-matrix - cmt-identity - provision-cmt-servers strategy: fail-fast: false matrix: ${{ fromJson(needs.prepare-maestro-matrix.outputs.matrix) }} secrets: MM_MOBILE_E2E_ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }} MM_MOBILE_E2E_ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }} MM_MOBILE_E2E_ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }} MM_MOBILE_E2E_ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }} MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }} MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }} MM_MOBILE_E2E_WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }} MM_E2E_MASTER_HEALTH_WEBHOOK_URL: ${{ secrets.MM_E2E_MASTER_HEALTH_WEBHOOK_URL }} MM_E2E_RELEASE_WEBHOOK_URL: ${{ secrets.MM_E2E_RELEASE_WEBHOOK_URL }} with: tsio-config: ${{ needs.cmt-identity.outputs.tsio-base-config }} tsio-shard-name: maestro-android-Server_${{ matrix.server.version }} platform: android run-type: "RELEASE" MM_TEST_SERVER_URL: ${{ matrix.server.android_site_1_url }} MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }} artifact_run_id: ${{ github.run_id }} record_tests_in_zephyr: true update-final-status: runs-on: ubuntu-24.04 if: always() permissions: contents: read statuses: write outputs: upstream-succeeded: ${{ steps.upstream.outputs.succeeded }} needs: - cmt-identity - calculate-commit-hash - provision-cmt-servers - detox-e2e - detox-android-e2e - detox-ipad-e2e - maestro-ios-e2e - maestro-android-e2e steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.MOBILE_VERSION }} persist-credentials: false - name: Resolve upstream matrix outcome id: upstream run: | UPSTREAM="true" for r in "${{ needs.provision-cmt-servers.result }}" "${{ needs.detox-e2e.result }}" "${{ needs.detox-android-e2e.result }}" "${{ needs.detox-ipad-e2e.result }}" "${{ needs.maestro-ios-e2e.result }}" "${{ needs.maestro-android-e2e.result }}"; do case "$r" in success|skipped) ;; *) UPSTREAM="false" ;; esac done echo "succeeded=$UPSTREAM" >> "$GITHUB_OUTPUT" # Umbrella context only — each matrix leg already posted from TSIO. - name: Finalize umbrella CMT commit status from CI job results env: GITHUB_TOKEN: ${{ github.token }} COMMIT_SHA: ${{ needs.calculate-commit-hash.outputs.MOBILE_SHA }} REPO: ${{ github.repository }} TARGET_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} UPSTREAM: ${{ steps.upstream.outputs.succeeded }} run: | if [ "$UPSTREAM" = "true" ]; then STATE=success DESC="Compatibility Matrix Testing completed — see per-job e2e-test/* checks" else STATE=failure DESC="Compatibility Matrix Testing failed — see per-job e2e-test/* checks" fi gh api --method POST "repos/${REPO}/statuses/${COMMIT_SHA}" \ -f state="$STATE" \ -f context="e2e-test/compatibility-matrix-testing" \ -f description="$DESC" \ -f target_url="$TARGET_URL" # Per-shard TSIO groups are finalized in each matrix leg, which leaves channel # notify off so N legs do not spam. This job polls every shard group and posts # one summary — same path as PR/main. After triage, so the message carries its # verdict on each failed test. # Re-run, on fresh devices, the failed specs triage could not clear, against the same server version, so the # channel message can say for certain whether each is a product bug (see e2e-detox-pr.yml plan-rerun). plan-rerun-cmt: name: Plan re-run of failed specs runs-on: ubuntu-24.04 if: '!cancelled() && needs.tsio-triage.result != ''skipped''' needs: - tsio-triage permissions: contents: read outputs: detox-ios-matrix: ${{ steps.plan.outputs.detox-ios-matrix }} detox-android-matrix: ${{ steps.plan.outputs.detox-android-matrix }} detox-ipad-matrix: ${{ steps.plan.outputs.detox-ipad-matrix }} maestro-ios-matrix: ${{ steps.plan.outputs.maestro-ios-matrix }} maestro-android-matrix: ${{ steps.plan.outputs.maestro-android-matrix }} stale: ${{ steps.plan.outputs.stale }} steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.MOBILE_VERSION }} persist-credentials: false sparse-checkout: | detox/utils/rerun-failed.js detox/utils/tsio-channel-notify-rollup.js detox/utils/tsio-report-status.js detox/utils/cmt-channel-notify.js detox/utils/build-tsio-job-config.js detox/utils/cli-args.js - name: Download triage findings continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: e2e-triage-findings-*-a${{ github.run_attempt }} path: triage-findings # A spec that differs from main's is out of date on this release branch: it is not re-run on the old build. - name: Fetch main to compare specs continue-on-error: true run: git fetch --no-tags --depth=1 origin main - name: Plan the re-run id: plan env: CMT_MATRIX: ${{ inputs.CMT_MATRIX }} run: node detox/utils/rerun-failed.js --findings-dir triage-findings --cmt-matrix "$CMT_MATRIX" --compare-ref FETCH_HEAD rerun-detox-e2e: name: rerun-mobile-cmt-ios${{ matrix.server.latest && '-full' || '-smoke' }}-${{ matrix.server.version }} # !cancelled(): a lane failed upstream (that is why there is a re-run), which would skip this job otherwise. if: >- !cancelled() && needs.plan-rerun-cmt.result == 'success' && needs.build-ios-simulator.result == 'success' && needs.provision-cmt-servers.result == 'success' && fromJson(needs.plan-rerun-cmt.outputs.detox-ios-matrix).server[0] != null permissions: contents: read # nested template mints an OIDC token for the TSIO upload. id-token: write actions: read statuses: write # Nested template finalize reads PR labels to honour E2E/Override; a # reusable workflow cannot escalate past the caller, so grant it here too. issues: read uses: ./.github/workflows/e2e-ios-template.yml needs: - plan-rerun-cmt - build-ios-simulator - cmt-identity - provision-cmt-servers strategy: fail-fast: false matrix: ${{ fromJson(needs.plan-rerun-cmt.outputs.detox-ios-matrix) }} secrets: MM_MOBILE_E2E_ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }} MM_MOBILE_E2E_ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }} MM_MOBILE_E2E_ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }} MM_MOBILE_E2E_ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }} MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX: ${{ secrets.MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX }} MM_MOBILE_E2E_WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }} MM_E2E_MASTER_HEALTH_WEBHOOK_URL: ${{ secrets.MM_E2E_MASTER_HEALTH_WEBHOOK_URL }} MM_E2E_RELEASE_WEBHOOK_URL: ${{ secrets.MM_E2E_RELEASE_WEBHOOK_URL }} MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }} MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }} MM_MOBILE_E2E_WEBHOOK_PUBLIC_BASE_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_PUBLIC_BASE_URL }} MM_MOBILE_E2E_CLOUDFLARED_TUNNEL_TOKEN: ${{ secrets.MM_MOBILE_E2E_CLOUDFLARED_TUNNEL_TOKEN }} with: tsio-config: '' tsio-shard-name: '' run-type: "NONE" MM_OTHER_PLATFORM_POOL_URL: ${{ matrix.server.android_site_1_url }} MM_TEST_SERVER_URL: ${{ matrix.server.ios_site_1_url }} MM_TEST_SERVER_URL_2: ${{ matrix.server.ios_site_2_url }} MM_TEST_SERVER_URL_3: ${{ matrix.server.site_3_url }} MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }} search_path: ${{ matrix.server.latest && 'detox/e2e/test' || 'detox/e2e/test/products/channels/smoke_test' }} # Full suite matches the PR/main shard count (template default 32); smoke stays single-shard. parallelism: '1' artifact_run_id: ${{ github.run_id }} spec_files: ${{ matrix.server.specs }} results-artifact: ${{ format('e2e-rerun-results-detox-ios-Server_{0}-a{1}', matrix.server.version, github.run_attempt) }} rerun-detox-ipad-e2e: name: rerun-mobile-cmt-ipad-${{ matrix.server.version }} # !cancelled(): a lane failed upstream (that is why there is a re-run), which would skip this job otherwise. if: >- !cancelled() && needs.plan-rerun-cmt.result == 'success' && needs.build-ios-simulator.result == 'success' && needs.provision-cmt-servers.result == 'success' && fromJson(needs.plan-rerun-cmt.outputs.detox-ipad-matrix).server[0] != null permissions: contents: read id-token: write actions: read statuses: write # Nested template finalize reads PR labels to honour E2E/Override; a # reusable workflow cannot escalate past the caller, so grant it here too. issues: read uses: ./.github/workflows/e2e-ios-template.yml needs: - plan-rerun-cmt - build-ios-simulator - cmt-identity - provision-cmt-servers strategy: fail-fast: false matrix: ${{ fromJson(needs.plan-rerun-cmt.outputs.detox-ipad-matrix) }} secrets: MM_MOBILE_E2E_ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }} MM_MOBILE_E2E_ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }} MM_MOBILE_E2E_ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }} MM_MOBILE_E2E_ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }} MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX: ${{ secrets.MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX }} MM_MOBILE_E2E_WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }} MM_E2E_MASTER_HEALTH_WEBHOOK_URL: ${{ secrets.MM_E2E_MASTER_HEALTH_WEBHOOK_URL }} MM_E2E_RELEASE_WEBHOOK_URL: ${{ secrets.MM_E2E_RELEASE_WEBHOOK_URL }} MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }} MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }} MM_MOBILE_E2E_WEBHOOK_PUBLIC_BASE_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_PUBLIC_BASE_URL }} MM_MOBILE_E2E_CLOUDFLARED_TUNNEL_TOKEN: ${{ secrets.MM_MOBILE_E2E_CLOUDFLARED_TUNNEL_TOKEN }} with: tsio-config: '' tsio-shard-name: '' run-type: "NONE" MM_OTHER_PLATFORM_POOL_URL: ${{ matrix.server.android_site_1_url }} MM_TEST_SERVER_URL: ${{ matrix.server.ios_site_1_url }} MM_TEST_SERVER_URL_2: ${{ matrix.server.ios_site_2_url }} MM_TEST_SERVER_URL_3: ${{ matrix.server.site_3_url }} MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }} ios_device_name: "iPad Pro 13-inch (M5)" ios_device_os_name: "iOS 26.2" search_path: "detox/e2e/test/products/channels/ipad" parallelism: '1' detox_config: "ios.ipad.debug" artifact_run_id: ${{ github.run_id }} spec_files: ${{ matrix.server.specs }} results-artifact: ${{ format('e2e-rerun-results-detox-ipad-Server_{0}-a{1}', matrix.server.version, github.run_attempt) }} # Maestro runs only against the latest server version (no smoke-on-older variant). rerun-detox-android-e2e: name: rerun-mobile-cmt-android${{ matrix.server.latest && '-full' || '-smoke' }}-${{ matrix.server.version }} # !cancelled(): a lane failed upstream (that is why there is a re-run), which would skip this job otherwise. if: >- !cancelled() && needs.plan-rerun-cmt.result == 'success' && needs.build-android-apk.result == 'success' && needs.provision-cmt-servers.result == 'success' && fromJson(needs.plan-rerun-cmt.outputs.detox-android-matrix).server[0] != null permissions: contents: read id-token: write actions: read statuses: write # Nested template finalize reads PR labels to honour E2E/Override; a # reusable workflow cannot escalate past the caller, so grant it here too. issues: read uses: ./.github/workflows/e2e-android-template.yml needs: - plan-rerun-cmt - build-android-apk - cmt-identity - provision-cmt-servers strategy: fail-fast: false matrix: ${{ fromJson(needs.plan-rerun-cmt.outputs.detox-android-matrix) }} secrets: MM_MOBILE_E2E_ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }} MM_MOBILE_E2E_ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }} MM_MOBILE_E2E_ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }} MM_MOBILE_E2E_ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }} MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX: ${{ secrets.MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX }} MM_MOBILE_E2E_WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }} MM_E2E_MASTER_HEALTH_WEBHOOK_URL: ${{ secrets.MM_E2E_MASTER_HEALTH_WEBHOOK_URL }} MM_E2E_RELEASE_WEBHOOK_URL: ${{ secrets.MM_E2E_RELEASE_WEBHOOK_URL }} MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }} MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }} MM_MOBILE_E2E_WEBHOOK_PUBLIC_BASE_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_PUBLIC_BASE_URL }} MM_MOBILE_E2E_CLOUDFLARED_TUNNEL_TOKEN: ${{ secrets.MM_MOBILE_E2E_CLOUDFLARED_TUNNEL_TOKEN }} with: tsio-config: '' tsio-shard-name: '' run-android-tests: true run-type: "NONE" MM_OTHER_PLATFORM_POOL_URL: ${{ matrix.server.ios_site_1_url }} MM_TEST_SERVER_URL: ${{ matrix.server.android_site_1_url }} MM_TEST_SERVER_URL_2: ${{ matrix.server.android_site_2_url }} MM_TEST_SERVER_URL_3: ${{ matrix.server.site_3_url }} MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }} search_path: ${{ matrix.server.latest && 'detox/e2e/test' || 'detox/e2e/test/products/channels/smoke_test' }} # Full suite matches the PR/main shard count (template default 32); smoke stays single-shard. parallelism: '1' artifact_run_id: ${{ github.run_id }} spec_files: ${{ matrix.server.specs }} results-artifact: ${{ format('e2e-rerun-results-detox-android-Server_{0}-a{1}', matrix.server.version, github.run_attempt) }} # Maestro runs only against the latest server version (no smoke-on-older variant). rerun-maestro-ios-e2e: name: rerun-mobile-cmt-maestro-ios-${{ matrix.server.version }} # !cancelled(): a lane failed upstream (that is why there is a re-run), which would skip this job otherwise. if: >- !cancelled() && needs.plan-rerun-cmt.result == 'success' && needs.build-ios-simulator.result == 'success' && needs.provision-cmt-servers.result == 'success' && fromJson(needs.plan-rerun-cmt.outputs.maestro-ios-matrix).server[0] != null permissions: contents: read id-token: write actions: read statuses: write # Nested template finalize reads PR labels to honour E2E/Override; a # reusable workflow cannot escalate past the caller, so grant it here too. issues: read uses: ./.github/workflows/e2e-maestro-template.yml needs: - plan-rerun-cmt - build-ios-simulator - prepare-maestro-matrix - cmt-identity - provision-cmt-servers strategy: fail-fast: false matrix: ${{ fromJson(needs.plan-rerun-cmt.outputs.maestro-ios-matrix) }} secrets: MM_MOBILE_E2E_ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }} MM_MOBILE_E2E_ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }} MM_MOBILE_E2E_ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }} MM_MOBILE_E2E_ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }} MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }} MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }} MM_MOBILE_E2E_WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }} MM_E2E_MASTER_HEALTH_WEBHOOK_URL: ${{ secrets.MM_E2E_MASTER_HEALTH_WEBHOOK_URL }} MM_E2E_RELEASE_WEBHOOK_URL: ${{ secrets.MM_E2E_RELEASE_WEBHOOK_URL }} with: tsio-config: '' tsio-shard-name: '' platform: ios run-type: "NONE" MM_TEST_SERVER_URL: ${{ matrix.server.ios_site_1_url }} MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }} record_tests_in_zephyr: false artifact_run_id: ${{ github.run_id }} flow-path: ${{ matrix.server.specs }} results-artifact: ${{ format('e2e-rerun-results-maestro-ios-Server_{0}-a{1}', matrix.server.version, github.run_attempt) }} rerun-maestro-android-e2e: name: rerun-mobile-cmt-maestro-android-${{ matrix.server.version }} # !cancelled(): a lane failed upstream (that is why there is a re-run), which would skip this job otherwise. if: >- !cancelled() && needs.plan-rerun-cmt.result == 'success' && needs.build-android-apk-maestro.result == 'success' && needs.provision-cmt-servers.result == 'success' && fromJson(needs.plan-rerun-cmt.outputs.maestro-android-matrix).server[0] != null permissions: contents: read id-token: write actions: read statuses: write # Nested template finalize reads PR labels to honour E2E/Override; a # reusable workflow cannot escalate past the caller, so grant it here too. issues: read uses: ./.github/workflows/e2e-maestro-template.yml needs: - plan-rerun-cmt - build-android-apk-maestro - prepare-maestro-matrix - cmt-identity - provision-cmt-servers strategy: fail-fast: false matrix: ${{ fromJson(needs.plan-rerun-cmt.outputs.maestro-android-matrix) }} secrets: MM_MOBILE_E2E_ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }} MM_MOBILE_E2E_ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }} MM_MOBILE_E2E_ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }} MM_MOBILE_E2E_ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }} MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }} MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }} MM_MOBILE_E2E_WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }} MM_E2E_MASTER_HEALTH_WEBHOOK_URL: ${{ secrets.MM_E2E_MASTER_HEALTH_WEBHOOK_URL }} MM_E2E_RELEASE_WEBHOOK_URL: ${{ secrets.MM_E2E_RELEASE_WEBHOOK_URL }} with: tsio-config: '' tsio-shard-name: '' platform: android run-type: "NONE" MM_TEST_SERVER_URL: ${{ matrix.server.android_site_1_url }} MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }} artifact_run_id: ${{ github.run_id }} flow-path: ${{ matrix.server.specs }} results-artifact: ${{ format('e2e-rerun-results-maestro-android-Server_{0}-a{1}', matrix.server.version, github.run_attempt) }} record_tests_in_zephyr: false tsio-channel-notify: name: Post Mattermost CMT channel rollup runs-on: ubuntu-24.04 if: always() && needs.cmt-identity.result == 'success' needs: - cmt-identity - update-final-status - tsio-triage - rerun-detox-e2e - rerun-detox-ipad-e2e - rerun-detox-android-e2e - rerun-maestro-ios-e2e - rerun-maestro-android-e2e - plan-rerun-cmt permissions: contents: read # the channel rollup mints an OIDC token to read each shard's TSIO group. id-token: write steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.MOBILE_VERSION }} persist-credentials: false # This attempt's triage verdicts (none when no shard failed, or triage did not finish). - name: Download triage findings continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: e2e-triage-findings-*-a${{ github.run_attempt }} path: triage-findings # The re-run's result per test (none when nothing was re-run). - name: Download re-run results continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: e2e-rerun-results-*-a${{ github.run_attempt }} path: rerun-results - name: Post Mattermost CMT channel rollup env: JOB_CONFIGS: ${{ needs.cmt-identity.outputs.tsio-job-configs }} TRIAGE_FINDINGS_DIR: triage-findings RERUN_RESULTS_DIR: rerun-results RERUN_STALE: ${{ needs.plan-rerun-cmt.outputs.stale || '{}' }} MATTERMOST_CMT_WEBHOOK_URL: ${{ secrets.MM_E2E_RELEASE_WEBHOOK_URL }} UPSTREAM_SUCCEEDED: ${{ needs.update-final-status.outputs.upstream-succeeded || 'false' }} run: | node detox/utils/tsio-channel-notify-rollup.js \ --job-configs="$JOB_CONFIGS" \ --upstream-succeeded="$UPSTREAM_SUCCEEDED" \ --poll-attempts=6 || true # Instance cleanup is handled by matterwick on workflow_run completed. # Same per-test judging the PR/main lanes get in e2e-detox-pr.yml: without it # every CMT shard is raw any-failure-red (FAILURES > 0 in the template summary # jobs), so a handful of known-flaky failures out of ~650 tests per leg turned # every CMT run red. Report-only (the default) only posts the verdict; # E2E_TRIAGE_MODE=enforce makes the verdict the shard's commit status, exactly # as on PR/main. The workflow run itself stays red on real failures either way. tsio-triage: name: Triage CMT failures (${{ matrix.job-key }}) runs-on: ubuntu-24.04 # The channel message waits for this job: a hang must not hold it for hours (recent runs take under a minute). timeout-minutes: 15 if: >- !cancelled() && (needs.detox-e2e.result == 'failure' || needs.detox-android-e2e.result == 'failure' || needs.detox-ipad-e2e.result == 'failure' || needs.maestro-ios-e2e.result == 'failure' || needs.maestro-android-e2e.result == 'failure') needs: - calculate-commit-hash - cmt-identity - detox-e2e - detox-android-e2e - detox-ipad-e2e - maestro-ios-e2e - maestro-android-e2e permissions: contents: read statuses: write pull-requests: read # Every lane writes the shared e2e-test/triage context, and the toolkit's # status publisher has no compare-and-swap: its status.ts requires every # caller to hold the same non-cancelling, SHA-scoped job concurrency lock # across its reads and writes. max-parallel serializes the matrix entries # within this run without discarding queued entries; the concurrency group # serializes against triage writers from any other run of the same commit. # The workflow-level cancel-in-progress group above does neither. concurrency: group: e2e-test-triage-${{ needs.calculate-commit-hash.outputs.MOBILE_SHA }} cancel-in-progress: false strategy: fail-fast: false max-parallel: 1 matrix: job-key: ${{ fromJSON(needs.cmt-identity.outputs.tsio-job-keys) }} steps: # Checked out from the workflow ref (the build-release-* branch under # test runs no code with the API key beyond this composite's own steps, # which are pinned actions; same pattern as e2e-detox-pr.yml). - name: Checkout triage action continue-on-error: true uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout: .github/actions/e2e-triage/ sparse-checkout-cone-mode: false - name: Resolve CMT triage lanes id: lanes env: JOB_CONFIGS: ${{ needs.cmt-identity.outputs.tsio-job-configs }} run: | echo "contexts=$(echo "$JOB_CONFIGS" | jq -r '[.[].status_context] | join(",")')" >> "$GITHUB_OUTPUT" - name: e2e/triage continue-on-error: true uses: ./.github/actions/e2e-triage with: job-key: ${{ matrix.job-key }} composite-identity: ${{ toJSON(fromJSON(needs.cmt-identity.outputs.tsio-job-configs)[matrix.job-key].composite_identity) }} status-context: ${{ fromJSON(needs.cmt-identity.outputs.tsio-job-configs)[matrix.job-key].status_context }} github-token: ${{ secrets.GITHUB_TOKEN }} anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} triage-lanes: ${{ steps.lanes.outputs.contexts }} # A triage failure must never change the outcome of the CMT run itself. mode: ${{ vars.E2E_TRIAGE_MODE || 'report-only' }}