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 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 — computes the TSIO composite identity for this CMT run and the # per-group totals. Detox runs the FULL CMT_MATRIX (all server versions) across # 3 platforms (ios/android/ipad) → total = 3 × (#servers). Maestro runs only # the latest server version(s) across 2 platforms (ios/android) → total = # 2 × (#latest servers). The orchestrator (update-final-status) and the # per-platform uploads both consume these outputs. cmt-identity: name: Calculate CMT TSIO composite identity runs-on: ubuntu-24.04 permissions: contents: read needs: - calculate-commit-hash outputs: composite-identity-json: ${{ steps.id.outputs.composite-identity-json }} tsio-config: ${{ steps.id.outputs.tsio-config }} total-reports-expected: ${{ steps.id.outputs.total-reports-expected }} steps: - id: id env: CMT_MATRIX: ${{ inputs.CMT_MATRIX }} GITHUB_REPOSITORY: ${{ github.repository }} MM_SHA: ${{ needs.calculate-commit-hash.outputs.MOBILE_SHA }} MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }} run: | DETOX_SERVERS=$(echo "$CMT_MATRIX" | jq '.server | length') MAESTRO_SERVERS=$(echo "$CMT_MATRIX" | jq '[.server[] | select(.latest == true)] | length') if [ "$MAESTRO_SERVERS" -eq 0 ]; then MAESTRO_SERVERS=1; fi DETOX_TOTAL=$(( 3 * DETOX_SERVERS )) MAESTRO_TOTAL=$(( 2 * MAESTRO_SERVERS )) TOTAL=$(( DETOX_TOTAL + MAESTRO_TOTAL )) echo "total-reports-expected=$TOTAL" >> "$GITHUB_OUTPUT" 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"}') echo "composite-identity-json=$BASE" >> "$GITHUB_OUTPUT" TSIO_CONFIG=$(jq -nc --argjson identity "$BASE" --argjson total "$TOTAL" \ '{composite_identity: $identity, total_reports_expected: $total}') echo "tsio-config=$TSIO_CONFIG" >> "$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 status uses the TSIO display URL (report fills in as shards upload). - name: Build TSIO report URL id: tsio-url env: IDENTITY: ${{ needs.cmt-identity.outputs.composite-identity-json }} run: | URL=$(jq -nr --argjson id "$IDENTITY" --arg base "https://test-io.test.mattermost.com" ' ($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 | ($id.run_group // $id.name // "mobile-release") as $name | "\($base)/reports/\($repo|@uri)/\($branch|@uri)/\($sha)/\($name|@uri)?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/compatibility-matrix-testing description: "Compatibility Matrix Testing for ${{ inputs.MOBILE_VERSION }} version" 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" # Input follows the below schema # { # "server": [ # { # "version": "9.6.1", # "url": "https://delivery-cmt-8467830017-9-6-1.test.mattermost.cloud/" # }, # { # "version": "9.5.2", # "url": "https://delivery-cmt-8467830017-9-5-2.test.mattermost.cloud/" # } # ] # } 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 uses: ./.github/workflows/e2e-ios-template.yml needs: - build-ios-simulator - cmt-identity 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_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 }} with: tsio-config: ${{ needs.cmt-identity.outputs.tsio-config }} tsio-shard-name: detox-ios-Server_${{ matrix.server.version }} run-type: "RELEASE" MM_TEST_SERVER_URL: ${{ matrix.server.url }} MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }} search_path: ${{ matrix.server.latest && 'detox/e2e/test' || 'detox/e2e/test/products/channels/smoke_test' }} parallelism: ${{ matrix.server.latest && '10' || '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 uses: ./.github/workflows/e2e-ios-template.yml needs: - build-ios-simulator - cmt-identity 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_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 }} with: tsio-config: ${{ needs.cmt-identity.outputs.tsio-config }} tsio-shard-name: detox-ipad-Server_${{ matrix.server.version }} run-type: "RELEASE" MM_TEST_SERVER_URL: ${{ matrix.server.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 uses: ./.github/workflows/e2e-maestro-template.yml needs: - build-ios-simulator - prepare-maestro-matrix - cmt-identity 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 }} with: tsio-config: ${{ needs.cmt-identity.outputs.tsio-config }} tsio-shard-name: maestro-ios-Server_${{ matrix.server.version }} platform: ios run-type: "RELEASE" MM_TEST_SERVER_URL: ${{ matrix.server.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 uses: ./.github/workflows/e2e-android-template.yml needs: - build-android-apk - cmt-identity 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_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 }} with: tsio-config: ${{ needs.cmt-identity.outputs.tsio-config }} tsio-shard-name: detox-android-Server_${{ matrix.server.version }} run-android-tests: true run-type: "RELEASE" MM_TEST_SERVER_URL: ${{ matrix.server.url }} MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }} search_path: ${{ matrix.server.latest && 'detox/e2e/test' || 'detox/e2e/test/products/channels/smoke_test' }} parallelism: ${{ matrix.server.latest && '10' || '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 uses: ./.github/workflows/e2e-maestro-template.yml needs: - build-android-apk-maestro - prepare-maestro-matrix - cmt-identity 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 }} with: tsio-config: ${{ needs.cmt-identity.outputs.tsio-config }} tsio-shard-name: maestro-android-Server_${{ matrix.server.version }} platform: android run-type: "RELEASE" MM_TEST_SERVER_URL: ${{ matrix.server.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 # posts the combined commit status. statuses: write # mints the OIDC token used to recover the TSIO report group ids. id-token: write actions: read needs: - cmt-identity - calculate-commit-hash - detox-e2e - detox-android-e2e - detox-ipad-e2e - maestro-ios-e2e - maestro-android-e2e steps: - name: Sparse checkout TSIO status utility uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ inputs.MOBILE_VERSION }} persist-credentials: false sparse-checkout: | detox/utils/tsio-report-status.js detox/utils/cmt-channel-notify.js detox/utils/cli-args.js - name: Post TSIO commit status from unified CMT report group env: GITHUB_TOKEN: ${{ github.token }} COMPOSITE_IDENTITY: ${{ needs.cmt-identity.outputs.composite-identity-json }} TOTAL_REPORTS_EXPECTED: ${{ needs.cmt-identity.outputs.total-reports-expected }} # Incoming webhook for the Mobile CMT/RC results channel (separate from # PR MM_MOBILE_E2E_WEBHOOK_URL and main MM_E2E_MASTER_HEALTH_WEBHOOK_URL). # Optional — when unset, TSIO status still flips and channel notify is skipped. MATTERMOST_CMT_WEBHOOK_URL: ${{ secrets.MM_E2E_RELEASE_WEBHOOK_URL }} run: | set +e UPSTREAM="true" for r in "${{ 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 node detox/utils/tsio-report-status.js \ --composite-identity="$COMPOSITE_IDENTITY" \ --total-reports-expected="$TOTAL_REPORTS_EXPECTED" \ --github-token="$GITHUB_TOKEN" \ --context="e2e/compatibility-matrix-testing" \ --upstream-succeeded="$UPSTREAM" \ --poll-attempts=24 RC=$? echo "tsio-report-status exit=$RC upstream_succeeded=$UPSTREAM" if [ "$RC" -ne 0 ] && [ "$UPSTREAM" = "true" ]; then echo "::error::TSIO reported test failures (exit $RC) with all upstream jobs succeeded." exit 1 fi exit 0 # Instance cleanup is handled by matterwick on workflow_run completed.