mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-05 18:38:54 +00:00
fix(e2e): stabilize Detox and Maestro test suite Restore main features, and harden the E2E harness against flaky UI, network, retry, synchronization, and classification-lock failures. Stabilize channel creation, saved messages, bookmarks, search, deep links, keyboard dismissal, and post verification. Bound network, transport, health-check, synchronization, and lock retries by elapsed time. Fix iOS and Android Maestro/Detox races and flaky interaction flows. Restore Session Attributes and deep-link fixes lost during merge. Improve test isolation, global-setting cleanup, and Jest/TSIO reporting. Enforce Maestro flow-header validation in local and CI checks.
1101 lines
46 KiB
YAML
1101 lines
46 KiB
YAML
# Matterwick E2E entry point (workflow_dispatch only).
|
|
#
|
|
# Matterwick dispatches this file via:
|
|
# POST /repos/{owner}/{repo}/actions/workflows/e2e-detox-pr.yml/dispatches
|
|
# and watches workflow_run "completed" events where workflow name is "E2E".
|
|
#
|
|
# Despite the `-pr` filename this is the entry point for *every* Matterwick E2E
|
|
# dispatch, not just PRs: run_type selects the flavour (PR, MAIN/MASTER for
|
|
# main-branch pushes, RELEASE). Renaming the file would break the dispatch URL
|
|
# above, so it needs a coordinated Matterwick change.
|
|
#
|
|
# Newer dispatches cancel older in-flight runs on the same branch via the workflow
|
|
# `concurrency` group below, so the latest SHA starts immediately instead of
|
|
# queueing behind a long run. MAIN/MASTER dispatches are keyed per commit instead,
|
|
# so consecutive merges to main never cancel each other.
|
|
#
|
|
# Phase 1 — builds & setup (parallel):
|
|
# build-ios-simulator, build-android-apk-maestro, build-android-apk (Detox debug APK),
|
|
# provision-servers, validate-maestro-flow-headers
|
|
# Phase 2 — tests (parallel, after phase 1):
|
|
# run-detox-ios / run-detox-android → e2e-detox.yml
|
|
# run-maestro-ios / run-maestro-android → e2e-maestro-pr.yml (downloads phase-1 artifacts via artifact_run_id)
|
|
name: E2E
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
MOBILE_VERSION:
|
|
description: "The mobile app commit SHA to test"
|
|
required: true
|
|
type: string
|
|
PLATFORM:
|
|
description: "Mobile platform to test: ios, android, or both"
|
|
required: false
|
|
default: "both"
|
|
type: choice
|
|
options:
|
|
- ios
|
|
- android
|
|
- both
|
|
ANDROID_SITE_1_URL:
|
|
description: "URL of Android test server A (provisioned by Matterwick)"
|
|
required: true
|
|
type: string
|
|
ANDROID_SITE_2_URL:
|
|
description: "URL of Android test server B (provisioned by Matterwick)"
|
|
required: true
|
|
type: string
|
|
IOS_SITE_1_URL:
|
|
description: "URL of iOS test server A (provisioned by Matterwick)"
|
|
required: true
|
|
type: string
|
|
IOS_SITE_2_URL:
|
|
description: "URL of iOS test server B (provisioned by Matterwick)"
|
|
required: true
|
|
type: string
|
|
SITE_3_URL:
|
|
description: "URL of the shared fifth Mattermost test server"
|
|
required: true
|
|
type: string
|
|
pr_number:
|
|
description: "PR number to remove E2E label from after tests complete"
|
|
required: false
|
|
type: string
|
|
run_type:
|
|
description: "Run type for test reporting (PR/RELEASE/MAIN). MASTER is accepted as an alias for MAIN (Matterwick main-push)."
|
|
required: false
|
|
default: "PR"
|
|
type: string
|
|
version_name:
|
|
description: "Branch or version ref under test (e.g. feature-branch, main, release-2.40). Used as TSIO branch — not pr-{number}."
|
|
required: false
|
|
type: string
|
|
|
|
# Default least-privilege token; jobs that upload artifacts, post statuses, or
|
|
# manage PR labels declare the extra scopes they need.
|
|
permissions:
|
|
contents: read
|
|
# test-system-io-report-upload (in the nested detox templates) authenticates
|
|
# via the workflow's OIDC token and resolves gh_job_id via the GitHub API.
|
|
# Reusable workflows cannot escalate beyond the caller, so the root caller
|
|
# must grant these.
|
|
id-token: write
|
|
actions: read
|
|
|
|
# One E2E run per branch for PRs, but per-COMMIT for main/release dispatches:
|
|
# Matterwick dispatches MAIN/MASTER runs with ref=main (matterwick
|
|
# server/push_events.go:319), so keying on github.ref alone would make a later
|
|
# merge cancel an earlier commit's still-running E2E — and a cancelled run never
|
|
# writes its build-cache entry, which is the scope PRs read from.
|
|
#
|
|
# For a PR the group stays per-branch (github.ref is the PR head branch, unique
|
|
# per PR), so a newer Matterwick dispatch still supersedes the one already in
|
|
# flight — cancelled by GitHub as the new run is created, which is earlier and
|
|
# simpler than cancelling from a step inside the new run.
|
|
concurrency:
|
|
group: e2e-${{ (inputs.run_type == 'MAIN' || inputs.run_type == 'MASTER') && inputs.MOBILE_VERSION || github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
# Changing this switches the iOS build path. Bump CACHE_VERSION in
|
|
# .github/actions/compute-e2e-build-fingerprint/action.yml or cached
|
|
# binaries built under the previous INTUNE setting will be served.
|
|
INTUNE_ENABLED: 'false'
|
|
DETOX_AWS_S3_BUCKET: "mattermost-detox-report"
|
|
|
|
jobs:
|
|
# Compute content fingerprints for S3 build caching.
|
|
compute-build-fingerprints:
|
|
name: Compute build fingerprints
|
|
runs-on: ubuntu-24.04
|
|
outputs:
|
|
ios: ${{ steps.ios.outputs.fingerprint }}
|
|
android-detox: ${{ steps.android-detox.outputs.fingerprint }}
|
|
android-maestro: ${{ steps.android-maestro.outputs.fingerprint }}
|
|
cache-scope: ${{ steps.scope.outputs.scope }}
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ inputs.MOBILE_VERSION }}
|
|
persist-credentials: false
|
|
|
|
# Cache trust scope. Only main-push runs write the `main` scope that every
|
|
# other run reads; a PR writes (and additionally reads) only `pr-<n>`, so
|
|
# one PR's build can never be served to main or to another PR.
|
|
- name: Resolve cache trust scope
|
|
id: scope
|
|
env:
|
|
RUN_TYPE: ${{ inputs.run_type || 'PR' }}
|
|
PR_NUMBER: ${{ inputs.pr_number }}
|
|
VERSION_NAME: ${{ inputs.version_name }}
|
|
REF_NAME: ${{ github.ref_name }}
|
|
run: |
|
|
set -eu
|
|
# Matterwick still sends MASTER for main-branch pushes; treat as MAIN.
|
|
if [ "$RUN_TYPE" = "MAIN" ] || [ "$RUN_TYPE" = "MASTER" ]; then
|
|
scope=main
|
|
elif [ -n "$PR_NUMBER" ]; then
|
|
# Digits only — a crafted pr_number must not be able to escape the prefix.
|
|
pr=$(printf '%s' "$PR_NUMBER" | tr -cd '0-9')
|
|
if [ -z "$pr" ]; then
|
|
echo "::error::pr_number '$PR_NUMBER' contains no digits — cannot build a safe cache scope."
|
|
exit 1
|
|
fi
|
|
scope="pr-${pr}"
|
|
else
|
|
raw="${VERSION_NAME:-$REF_NAME}"
|
|
slug=$(printf '%s' "$raw" | tr -c 'a-zA-Z0-9._-' '-' | cut -c1-60)
|
|
scope="branch-${slug}"
|
|
fi
|
|
echo "scope=$scope" >> "$GITHUB_OUTPUT"
|
|
echo "Cache trust scope: $scope"
|
|
|
|
- name: Compute iOS fingerprint
|
|
id: ios
|
|
uses: ./.github/actions/compute-e2e-build-fingerprint
|
|
with:
|
|
platform: ios
|
|
|
|
- name: Compute Android Detox fingerprint
|
|
id: android-detox
|
|
uses: ./.github/actions/compute-e2e-build-fingerprint
|
|
with:
|
|
platform: android-detox
|
|
|
|
- name: Compute Android Maestro fingerprint
|
|
id: android-maestro
|
|
uses: ./.github/actions/compute-e2e-build-fingerprint
|
|
with:
|
|
platform: android-maestro
|
|
|
|
resolve-e2e-policy:
|
|
name: Resolve E2E policy (Override label)
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
issues: read
|
|
outputs:
|
|
e2e_override: ${{ steps.policy.outputs.e2e_override }}
|
|
skip_reason: ${{ steps.policy.outputs.skip_reason }}
|
|
steps:
|
|
# Per-branch cancellation used to happen here via cancel-e2e-runs (removed —
|
|
# see the workflow-level `concurrency:` group above, which supersedes it via
|
|
# native GitHub Actions cancellation). That step also guarded main-branch runs
|
|
# against cancelling each other; the concurrency group keys MAIN/MASTER by
|
|
# MOBILE_VERSION for the same reason, since Matterwick dispatches those with
|
|
# ref=main.
|
|
- name: Check PR labels for E2E/Override
|
|
id: policy
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
env:
|
|
PR_NUMBER: ${{ inputs.pr_number }}
|
|
MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }}
|
|
with:
|
|
script: |
|
|
const prInput = parseInt(process.env.PR_NUMBER, 10);
|
|
let prNumber = Number.isFinite(prInput) ? prInput : null;
|
|
|
|
if (!prNumber && process.env.MOBILE_VERSION) {
|
|
try {
|
|
const {data: prs} = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
commit_sha: process.env.MOBILE_VERSION,
|
|
});
|
|
const openPr = prs.find((pr) => pr.state === 'open');
|
|
if (openPr) {
|
|
prNumber = openPr.number;
|
|
core.info(`Resolved PR #${prNumber} from commit ${process.env.MOBILE_VERSION}`);
|
|
}
|
|
} catch (err) {
|
|
core.warning(`Could not resolve PR from commit: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
if (!prNumber) {
|
|
core.info('No PR number available — assuming E2E/Override is not set.');
|
|
core.setOutput('e2e_override', 'false');
|
|
core.setOutput('skip_reason', '');
|
|
return;
|
|
}
|
|
|
|
const {data: pr} = await github.rest.pulls.get({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: prNumber,
|
|
});
|
|
if (pr.base.ref !== 'main') {
|
|
core.info(`PR #${prNumber} targets ${pr.base.ref}, not main — skipping E2E.`);
|
|
core.setOutput('e2e_override', 'true');
|
|
core.setOutput('skip_reason', 'non-main-base');
|
|
return;
|
|
}
|
|
|
|
const {data: labels} = await github.rest.issues.listLabelsOnIssue({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: prNumber,
|
|
});
|
|
const override = labels.some((label) => label.name === 'E2E/Override');
|
|
core.info(`PR #${prNumber} E2E/Override=${override}`);
|
|
core.setOutput('e2e_override', override ? 'true' : 'false');
|
|
core.setOutput('skip_reason', override ? 'override-label' : '');
|
|
|
|
# Every other job is skipped when the policy says "no E2E", and a skipped job
|
|
# posts nothing — the required e2e-test/* contexts would stay absent and block
|
|
# the merge forever. Post success here so the override actually lets it merge.
|
|
# Also covers the dispatch race where the label lands after Matterwick dispatched.
|
|
# Idempotent with the label manager, which posts the same statuses on `labeled`.
|
|
post-e2e-override-status:
|
|
name: Publish success on required E2E contexts (override)
|
|
runs-on: ubuntu-24.04
|
|
needs: resolve-e2e-policy
|
|
if: >-
|
|
needs.resolve-e2e-policy.outputs.e2e_override == 'true'
|
|
&& (inputs.run_type == 'PR' || inputs.run_type == 'MAIN' || inputs.run_type == 'MASTER')
|
|
permissions:
|
|
contents: read
|
|
statuses: write
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
persist-credentials: false
|
|
- uses: ./.github/actions/e2e-override-status
|
|
with:
|
|
commit_sha: ${{ inputs.MOBILE_VERSION }}
|
|
description: >-
|
|
${{ needs.resolve-e2e-policy.outputs.skip_reason == 'non-main-base'
|
|
&& 'E2E skipped — PR does not target main'
|
|
|| 'e2e tests overridden' }}
|
|
target_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
|
|
calculate-tsio-identity:
|
|
name: Calculate TSIO per-job identities
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
needs: resolve-e2e-policy
|
|
if: needs.resolve-e2e-policy.outputs.e2e_override != 'true'
|
|
outputs:
|
|
job-configs: ${{ steps.id.outputs.job-configs }}
|
|
job-keys: ${{ steps.id.outputs.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
|
|
|
|
- name: Resolve branch ref for TSIO display URL
|
|
id: branch
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
env:
|
|
VERSION_NAME: ${{ inputs.version_name }}
|
|
PR_NUMBER: ${{ inputs.pr_number }}
|
|
RUN_TYPE: ${{ inputs.run_type || 'PR' }}
|
|
with:
|
|
script: |
|
|
const versionName = (process.env.VERSION_NAME || '').trim();
|
|
if (versionName) {
|
|
core.setOutput('branch', versionName.replace(/^refs\/heads\//, '').replace(/^refs\/tags\//, ''));
|
|
return;
|
|
}
|
|
const runType = (process.env.RUN_TYPE || 'PR').toUpperCase();
|
|
// Matterwick still sends MASTER for main-branch pushes; treat as MAIN.
|
|
if (runType === 'MAIN' || runType === 'MASTER') {
|
|
core.setOutput('branch', 'main');
|
|
return;
|
|
}
|
|
const prInput = parseInt(process.env.PR_NUMBER, 10);
|
|
if (Number.isFinite(prInput)) {
|
|
const {data: pr} = await github.rest.pulls.get({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: prInput,
|
|
});
|
|
core.setOutput('branch', pr.head.ref);
|
|
return;
|
|
}
|
|
core.setOutput('branch', context.ref_name || 'main');
|
|
|
|
- id: id
|
|
env:
|
|
PLATFORM: ${{ inputs.PLATFORM }}
|
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
MM_SHA: ${{ inputs.MOBILE_VERSION }}
|
|
PR_NUMBER: ${{ inputs.pr_number }}
|
|
RUN_TYPE: ${{ inputs.run_type || 'PR' }}
|
|
BRANCH: ${{ steps.branch.outputs.branch }}
|
|
run: |
|
|
# Matterwick main-push still sends MASTER; canonicalize to MAIN → mobile-main.
|
|
if [ "${RUN_TYPE}" = "MASTER" ]; then
|
|
RUN_TYPE=MAIN
|
|
fi
|
|
RUN_TYPE_LOWER=$(echo "${RUN_TYPE}" | tr '[:upper:]' '[:lower:]')
|
|
RUN_NAME="mobile-${RUN_TYPE_LOWER}"
|
|
if [ -n "${PR_NUMBER}" ]; then
|
|
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" --arg pr "$PR_NUMBER" --arg name "$RUN_NAME" \
|
|
'{repository:$repo, commit_sha:$sha, gh_run_id:$run_id, gh_run_attempt:$attempt, branch:$branch, name:$name, run_group:$name, gh_pr_number:$pr, framework:"detox"}')
|
|
else
|
|
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" --arg name "$RUN_NAME" \
|
|
'{repository:$repo, commit_sha:$sha, gh_run_id:$run_id, gh_run_attempt:$attempt, branch:$branch, name:$name, run_group:$name, framework:"detox"}')
|
|
fi
|
|
export BASE PLATFORM
|
|
JOB_CONFIGS=$(node -e '
|
|
const {buildTsioJobConfigMap, jobKeysForPlatform} = require("./detox/utils/build-tsio-job-config.js");
|
|
const base = JSON.parse(process.env.BASE);
|
|
const keys = jobKeysForPlatform(process.env.PLATFORM);
|
|
process.stdout.write(JSON.stringify(buildTsioJobConfigMap(base, keys)));
|
|
')
|
|
echo "job-configs=$JOB_CONFIGS" >> "$GITHUB_OUTPUT"
|
|
echo "job-keys=$(echo "$JOB_CONFIGS" | jq -r 'keys | join(",")')" >> "$GITHUB_OUTPUT"
|
|
|
|
update-initial-tsio-status:
|
|
name: Set pending per-job TSIO commit statuses
|
|
runs-on: ubuntu-24.04
|
|
needs:
|
|
- resolve-e2e-policy
|
|
- calculate-tsio-identity
|
|
if: >-
|
|
needs.resolve-e2e-policy.outputs.e2e_override != 'true'
|
|
&& needs.calculate-tsio-identity.result == 'success'
|
|
&& (inputs.run_type == 'PR' || inputs.run_type == 'MAIN' || inputs.run_type == 'MASTER')
|
|
permissions:
|
|
contents: read
|
|
statuses: write
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ inputs.MOBILE_VERSION }}
|
|
persist-credentials: false
|
|
- name: Post pending status for each platform job
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
JOB_CONFIGS: ${{ needs.calculate-tsio-identity.outputs.job-configs }}
|
|
COMMIT_SHA: ${{ inputs.MOBILE_VERSION }}
|
|
REPO: ${{ github.repository }}
|
|
TARGET_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
run: |
|
|
echo "$JOB_CONFIGS" | jq -c 'to_entries[]' | while read -r entry; do
|
|
CTX=$(echo "$entry" | jq -r '.value.status_context')
|
|
NAME=$(echo "$entry" | jq -r '.value.composite_identity.name')
|
|
gh api --method POST "repos/${REPO}/statuses/${COMMIT_SHA}" \
|
|
-f state=pending \
|
|
-f context="$CTX" \
|
|
-f description="Mobile E2E (${NAME}) — waiting for TSIO report" \
|
|
-f target_url="$TARGET_URL"
|
|
done
|
|
|
|
resolve-server-topology:
|
|
name: Resolve test server topology
|
|
runs-on: ubuntu-24.04
|
|
needs: resolve-e2e-policy
|
|
if: needs.resolve-e2e-policy.outputs.e2e_override != 'true'
|
|
outputs:
|
|
android-site-1-url: ${{ steps.topology.outputs.android-site-1-url }}
|
|
android-site-2-url: ${{ steps.topology.outputs.android-site-2-url }}
|
|
ios-site-1-url: ${{ steps.topology.outputs.ios-site-1-url }}
|
|
ios-site-2-url: ${{ steps.topology.outputs.ios-site-2-url }}
|
|
site-3-url: ${{ steps.topology.outputs.site-3-url }}
|
|
steps:
|
|
- name: Resolve five-server inputs
|
|
id: topology
|
|
env:
|
|
ANDROID_SITE_1_URL: ${{ inputs.ANDROID_SITE_1_URL }}
|
|
ANDROID_SITE_2_URL: ${{ inputs.ANDROID_SITE_2_URL }}
|
|
IOS_SITE_1_URL: ${{ inputs.IOS_SITE_1_URL }}
|
|
IOS_SITE_2_URL: ${{ inputs.IOS_SITE_2_URL }}
|
|
SITE_3_URL: ${{ inputs.SITE_3_URL }}
|
|
run: |
|
|
for name in ANDROID_SITE_1_URL ANDROID_SITE_2_URL IOS_SITE_1_URL IOS_SITE_2_URL SITE_3_URL; do
|
|
if [ -z "${!name}" ]; then
|
|
echo "::error::$name is required for the five-server topology"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
{
|
|
echo "android-site-1-url=$ANDROID_SITE_1_URL"
|
|
echo "android-site-2-url=$ANDROID_SITE_2_URL"
|
|
echo "ios-site-1-url=$IOS_SITE_1_URL"
|
|
echo "ios-site-2-url=$IOS_SITE_2_URL"
|
|
echo "site-3-url=$SITE_3_URL"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
build-ios-simulator:
|
|
if: >-
|
|
!cancelled()
|
|
&& needs.resolve-e2e-policy.outputs.e2e_override != 'true'
|
|
&& (inputs.PLATFORM == 'ios' || inputs.PLATFORM == 'both')
|
|
# Runner / Xcode image is not in the git tree. Changing this label (or the
|
|
# Xcode on it) without bumping CACHE_VERSION in
|
|
# compute-e2e-build-fingerprint/action.yml serves stale cached binaries.
|
|
runs-on: macos-26
|
|
needs:
|
|
- resolve-e2e-policy
|
|
- compute-build-fingerprints
|
|
permissions:
|
|
contents: read
|
|
actions: write
|
|
steps:
|
|
- name: Checkout Repository
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ inputs.MOBILE_VERSION }}
|
|
|
|
# S3 build cache — skip rebuild when fingerprint matches (test-only PRs hit the cache seeded by main-push runs).
|
|
- name: Check for cached iOS build
|
|
id: cache-check
|
|
continue-on-error: true
|
|
uses: ./.github/actions/s3-build-cache
|
|
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 }}
|
|
with:
|
|
fingerprint: ${{ needs.compute-build-fingerprints.outputs.ios }}
|
|
scope: ${{ needs.compute-build-fingerprints.outputs.cache-scope }}
|
|
platform: ios
|
|
bucket: ${{ env.DETOX_AWS_S3_BUCKET }}
|
|
cache-path: 'Mattermost-simulator-*.app.zip'
|
|
mode: restore
|
|
|
|
- name: Skip build if cache hit
|
|
if: steps.cache-check.outputs.cache-hit == 'true'
|
|
run: echo "SKIP_BUILD=true" >> "$GITHUB_ENV"
|
|
|
|
- name: Prepare iOS Build
|
|
if: env.SKIP_BUILD != 'true'
|
|
uses: ./.github/actions/prepare-ios-build
|
|
with:
|
|
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
|
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
|
|
|
- name: Set .env with RUNNING_E2E=true
|
|
if: env.SKIP_BUILD != 'true'
|
|
run: echo "RUNNING_E2E=true" > .env
|
|
|
|
- name: Build iOS Simulator
|
|
if: env.SKIP_BUILD != 'true'
|
|
env:
|
|
TAG: ${{ inputs.MOBILE_VERSION }}
|
|
GITHUB_TOKEN: ${{ secrets.MM_MOBILE_GITHUB_TOKEN }}
|
|
run: bundle exec fastlane ios simulator --env ios.simulator
|
|
working-directory: ./fastlane
|
|
|
|
# Always upload artifact (from cache or fresh build) so test jobs can download it.
|
|
- name: Upload iOS Simulator Build
|
|
if: always() && hashFiles('Mattermost-simulator-*.app.zip') != ''
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: ios-build-simulator-${{ github.run_id }}
|
|
path: Mattermost-simulator-*.app.zip
|
|
|
|
- name: Save iOS build to S3
|
|
if: env.SKIP_BUILD != 'true' && hashFiles('Mattermost-simulator-*.app.zip') != ''
|
|
continue-on-error: true
|
|
uses: ./.github/actions/s3-build-cache
|
|
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 }}
|
|
with:
|
|
fingerprint: ${{ needs.compute-build-fingerprints.outputs.ios }}
|
|
scope: ${{ needs.compute-build-fingerprints.outputs.cache-scope }}
|
|
platform: ios
|
|
bucket: ${{ env.DETOX_AWS_S3_BUCKET }}
|
|
cache-path: 'Mattermost-simulator-*.app.zip'
|
|
mode: save
|
|
|
|
build-android-apk:
|
|
if: >-
|
|
!cancelled()
|
|
&& needs.resolve-e2e-policy.outputs.e2e_override != 'true'
|
|
&& (inputs.PLATFORM == 'android' || inputs.PLATFORM == 'both')
|
|
# Runner image is not in the git tree — bump CACHE_VERSION in
|
|
# compute-e2e-build-fingerprint/action.yml when changing this label.
|
|
runs-on: ubuntu-latest-8-cores
|
|
needs:
|
|
- resolve-e2e-policy
|
|
- compute-build-fingerprints
|
|
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ inputs.MOBILE_VERSION }}
|
|
|
|
# S3 build cache — skip rebuild when fingerprint matches.
|
|
- name: Check for cached Android Detox build
|
|
id: cache-check
|
|
continue-on-error: true
|
|
uses: ./.github/actions/s3-build-cache
|
|
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 }}
|
|
with:
|
|
fingerprint: ${{ needs.compute-build-fingerprints.outputs.android-detox }}
|
|
scope: ${{ needs.compute-build-fingerprints.outputs.cache-scope }}
|
|
platform: android-detox
|
|
bucket: ${{ env.DETOX_AWS_S3_BUCKET }}
|
|
cache-path: 'android/app/build'
|
|
mode: restore
|
|
|
|
- name: Skip build if cache hit
|
|
if: steps.cache-check.outputs.cache-hit == 'true'
|
|
run: echo "SKIP_BUILD=true" >> "$GITHUB_ENV"
|
|
|
|
- name: Prepare Android Build
|
|
if: env.SKIP_BUILD != 'true'
|
|
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 }}
|
|
|
|
# Cache Gradle dependencies — same key as e2e-detox-release.yml so the
|
|
# PR + release workflows share cache. Saves 3-5 min on warm runs by
|
|
# skipping gradle plugin resolution + Maven dep downloads.
|
|
- name: Cache Gradle dependencies
|
|
if: env.SKIP_BUILD != 'true'
|
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
with:
|
|
path: /home/runner/.gradle/caches/modules-2/
|
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
restore-keys: ${{ runner.os }}-gradle-
|
|
|
|
- name: Detox build
|
|
if: env.SKIP_BUILD != 'true'
|
|
run: |
|
|
cd detox
|
|
npm ci --prefer-offline --no-audit --no-fund
|
|
npm run e2e:android-build
|
|
|
|
# Always upload artifact (from cache or fresh build) so test jobs can download it.
|
|
- name: Upload Android Build
|
|
if: always() && hashFiles('android/app/build/**/*') != ''
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: android-build-files-${{ github.run_id }}
|
|
path: "android/app/build/**/*"
|
|
|
|
- name: Save Android Detox build to S3
|
|
if: env.SKIP_BUILD != 'true' && hashFiles('android/app/build/**/*') != ''
|
|
continue-on-error: true
|
|
uses: ./.github/actions/s3-build-cache
|
|
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 }}
|
|
with:
|
|
fingerprint: ${{ needs.compute-build-fingerprints.outputs.android-detox }}
|
|
scope: ${{ needs.compute-build-fingerprints.outputs.cache-scope }}
|
|
platform: android-detox
|
|
bucket: ${{ env.DETOX_AWS_S3_BUCKET }}
|
|
cache-path: 'android/app/build'
|
|
mode: save
|
|
|
|
build-android-apk-maestro:
|
|
if: >-
|
|
!cancelled()
|
|
&& needs.resolve-e2e-policy.outputs.e2e_override != 'true'
|
|
&& (inputs.PLATFORM == 'android' || inputs.PLATFORM == 'both')
|
|
# Runner image is not in the git tree — bump CACHE_VERSION in
|
|
# compute-e2e-build-fingerprint/action.yml when changing this label.
|
|
runs-on: ubuntu-latest-8-cores
|
|
needs:
|
|
- resolve-e2e-policy
|
|
- compute-build-fingerprints
|
|
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ inputs.MOBILE_VERSION }}
|
|
|
|
# S3 build cache — skip rebuild when fingerprint matches.
|
|
- name: Check for cached Android Maestro build
|
|
id: cache-check
|
|
continue-on-error: true
|
|
uses: ./.github/actions/s3-build-cache
|
|
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 }}
|
|
with:
|
|
fingerprint: ${{ needs.compute-build-fingerprints.outputs.android-maestro }}
|
|
scope: ${{ needs.compute-build-fingerprints.outputs.cache-scope }}
|
|
platform: android-maestro
|
|
bucket: ${{ env.DETOX_AWS_S3_BUCKET }}
|
|
cache-path: 'android/app/build/outputs/apk/release/'
|
|
mode: restore
|
|
|
|
- name: Skip build if cache hit
|
|
if: steps.cache-check.outputs.cache-hit == 'true'
|
|
run: echo "SKIP_BUILD=true" >> "$GITHUB_ENV"
|
|
|
|
- name: Prepare Android Build
|
|
if: env.SKIP_BUILD != 'true'
|
|
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
|
|
if: env.SKIP_BUILD != 'true'
|
|
uses: ./.github/actions/build-android-maestro-apk
|
|
|
|
# Always upload artifact (from cache or fresh build) so test jobs can download it.
|
|
- name: Upload Maestro Android Build
|
|
if: always() && hashFiles('android/app/build/outputs/apk/release/*.apk') != ''
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: android-maestro-build-files-${{ github.run_id }}
|
|
path: android/app/build/outputs/apk/release/
|
|
|
|
- name: Save Android Maestro build to S3
|
|
if: env.SKIP_BUILD != 'true' && hashFiles('android/app/build/outputs/apk/release/*.apk') != ''
|
|
continue-on-error: true
|
|
uses: ./.github/actions/s3-build-cache
|
|
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 }}
|
|
with:
|
|
fingerprint: ${{ needs.compute-build-fingerprints.outputs.android-maestro }}
|
|
scope: ${{ needs.compute-build-fingerprints.outputs.cache-scope }}
|
|
platform: android-maestro
|
|
bucket: ${{ env.DETOX_AWS_S3_BUCKET }}
|
|
cache-path: 'android/app/build/outputs/apk/release/'
|
|
mode: save
|
|
|
|
provision-servers:
|
|
name: Provision test servers
|
|
needs:
|
|
- resolve-e2e-policy
|
|
- resolve-server-topology
|
|
if: >-
|
|
needs.resolve-e2e-policy.outputs.e2e_override != 'true'
|
|
runs-on: ubuntu-24.04
|
|
steps:
|
|
- name: Checkout Repository
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ inputs.MOBILE_VERSION }}
|
|
persist-credentials: false
|
|
|
|
- name: Collect server URLs
|
|
id: urls
|
|
env:
|
|
ANDROID_SITE_1_URL: ${{ needs.resolve-server-topology.outputs.android-site-1-url }}
|
|
ANDROID_SITE_2_URL: ${{ needs.resolve-server-topology.outputs.android-site-2-url }}
|
|
IOS_SITE_1_URL: ${{ needs.resolve-server-topology.outputs.ios-site-1-url }}
|
|
IOS_SITE_2_URL: ${{ needs.resolve-server-topology.outputs.ios-site-2-url }}
|
|
SITE_3_URL: ${{ needs.resolve-server-topology.outputs.site-3-url }}
|
|
run: |
|
|
{
|
|
echo "list<<EOF"
|
|
printf '%s\n' "$ANDROID_SITE_1_URL" "$ANDROID_SITE_2_URL" "$IOS_SITE_1_URL" "$IOS_SITE_2_URL" "$SITE_3_URL"
|
|
echo "EOF"
|
|
} >> "$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 }}
|
|
|
|
# iOS and Android Detox jobs are separate so each platform starts as soon as its
|
|
# own build finishes — Android does not wait for build-ios-simulator.
|
|
run-detox-ios:
|
|
name: Detox iOS E2E
|
|
needs:
|
|
- resolve-e2e-policy
|
|
- calculate-tsio-identity
|
|
- build-ios-simulator
|
|
- provision-servers
|
|
- resolve-server-topology
|
|
permissions:
|
|
contents: read
|
|
statuses: write
|
|
actions: read
|
|
id-token: write
|
|
# Finalize re-reads the PR labels for E2E/Override; a reusable workflow
|
|
# cannot escalate past the caller, so grant it here too.
|
|
issues: read
|
|
if: |
|
|
!cancelled() &&
|
|
needs.resolve-e2e-policy.outputs.e2e_override != 'true' &&
|
|
(inputs.PLATFORM == 'ios' || inputs.PLATFORM == 'both') &&
|
|
needs.provision-servers.result == 'success' &&
|
|
(needs.build-ios-simulator.result == 'success' || needs.build-ios-simulator.result == 'skipped')
|
|
uses: ./.github/workflows/e2e-detox.yml
|
|
with:
|
|
MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }}
|
|
PLATFORM: ios
|
|
ANDROID_SITE_1_URL: ${{ needs.resolve-server-topology.outputs.android-site-1-url }}
|
|
ANDROID_SITE_2_URL: ${{ needs.resolve-server-topology.outputs.android-site-2-url }}
|
|
IOS_SITE_1_URL: ${{ needs.resolve-server-topology.outputs.ios-site-1-url }}
|
|
IOS_SITE_2_URL: ${{ needs.resolve-server-topology.outputs.ios-site-2-url }}
|
|
SITE_3_URL: ${{ needs.resolve-server-topology.outputs.site-3-url }}
|
|
run_type: ${{ inputs.run_type || 'PR' }}
|
|
artifact_run_id: ${{ github.run_id }}
|
|
tsio-job-configs: ${{ needs.calculate-tsio-identity.outputs.job-configs }}
|
|
secrets: inherit
|
|
|
|
run-detox-android:
|
|
name: Detox Android E2E
|
|
needs:
|
|
- resolve-e2e-policy
|
|
- calculate-tsio-identity
|
|
- build-android-apk
|
|
- provision-servers
|
|
- resolve-server-topology
|
|
permissions:
|
|
contents: read
|
|
statuses: write
|
|
actions: read
|
|
id-token: write
|
|
# Finalize re-reads the PR labels for E2E/Override; a reusable workflow
|
|
# cannot escalate past the caller, so grant it here too.
|
|
issues: read
|
|
if: |
|
|
!cancelled() &&
|
|
needs.resolve-e2e-policy.outputs.e2e_override != 'true' &&
|
|
(inputs.PLATFORM == 'android' || inputs.PLATFORM == 'both') &&
|
|
needs.provision-servers.result == 'success' &&
|
|
(needs.build-android-apk.result == 'success' || needs.build-android-apk.result == 'skipped')
|
|
uses: ./.github/workflows/e2e-detox.yml
|
|
with:
|
|
MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }}
|
|
PLATFORM: android
|
|
ANDROID_SITE_1_URL: ${{ needs.resolve-server-topology.outputs.android-site-1-url }}
|
|
ANDROID_SITE_2_URL: ${{ needs.resolve-server-topology.outputs.android-site-2-url }}
|
|
IOS_SITE_1_URL: ${{ needs.resolve-server-topology.outputs.ios-site-1-url }}
|
|
IOS_SITE_2_URL: ${{ needs.resolve-server-topology.outputs.ios-site-2-url }}
|
|
SITE_3_URL: ${{ needs.resolve-server-topology.outputs.site-3-url }}
|
|
run_type: ${{ inputs.run_type || 'PR' }}
|
|
artifact_run_id: ${{ github.run_id }}
|
|
tsio-job-configs: ${{ needs.calculate-tsio-identity.outputs.job-configs }}
|
|
secrets: inherit
|
|
|
|
validate-maestro-flow-headers:
|
|
name: Validate Maestro flow headers
|
|
needs: resolve-e2e-policy
|
|
if: >-
|
|
needs.resolve-e2e-policy.outputs.e2e_override != 'true'
|
|
runs-on: ubuntu-24.04
|
|
steps:
|
|
- name: Checkout Repository
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ inputs.MOBILE_VERSION }}
|
|
|
|
- name: Validate flow header contract
|
|
run: bash detox/maestro/scripts/validate-flow-headers.sh
|
|
|
|
# Maestro orchestration lives in e2e-maestro-pr.yml so Detox and Maestro CI
|
|
# can evolve independently. Maestro downloads ios-build-simulator-* and
|
|
# android-maestro-build-files-* (release APK with embedded bundle) from this
|
|
# workflow run via artifact_run_id.
|
|
# iOS and Android are separate jobs so each platform starts as soon as its
|
|
# own build finishes — Android does not wait for build-ios-simulator.
|
|
run-maestro-ios:
|
|
name: Maestro iOS E2E
|
|
needs:
|
|
- resolve-e2e-policy
|
|
- calculate-tsio-identity
|
|
- build-ios-simulator
|
|
- provision-servers
|
|
- validate-maestro-flow-headers
|
|
- resolve-server-topology
|
|
permissions:
|
|
contents: read
|
|
statuses: write
|
|
actions: read
|
|
id-token: write
|
|
# Finalize re-reads the PR labels for E2E/Override; a reusable workflow
|
|
# cannot escalate past the caller, so grant it here too.
|
|
issues: read
|
|
if: |
|
|
!cancelled() &&
|
|
needs.resolve-e2e-policy.outputs.e2e_override != 'true' &&
|
|
(inputs.PLATFORM == 'ios' || inputs.PLATFORM == 'both') &&
|
|
needs.provision-servers.result == 'success' &&
|
|
needs.validate-maestro-flow-headers.result == 'success' &&
|
|
(needs.build-ios-simulator.result == 'success' || needs.build-ios-simulator.result == 'skipped')
|
|
uses: ./.github/workflows/e2e-maestro-pr.yml
|
|
with:
|
|
MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }}
|
|
PLATFORM: ios
|
|
ANDROID_SITE_1_URL: ${{ needs.resolve-server-topology.outputs.android-site-1-url }}
|
|
ANDROID_SITE_2_URL: ${{ needs.resolve-server-topology.outputs.android-site-2-url }}
|
|
IOS_SITE_1_URL: ${{ needs.resolve-server-topology.outputs.ios-site-1-url }}
|
|
IOS_SITE_2_URL: ${{ needs.resolve-server-topology.outputs.ios-site-2-url }}
|
|
SITE_3_URL: ${{ needs.resolve-server-topology.outputs.site-3-url }}
|
|
run_type: ${{ inputs.run_type || 'PR' }}
|
|
artifact_run_id: ${{ github.run_id }}
|
|
record_tests_in_zephyr: ${{ inputs.run_type == 'RELEASE' || inputs.run_type == 'MAIN' || inputs.run_type == 'MASTER' }}
|
|
tsio-job-configs: ${{ needs.calculate-tsio-identity.outputs.job-configs }}
|
|
secrets: inherit
|
|
|
|
run-maestro-android:
|
|
name: Maestro Android E2E
|
|
needs:
|
|
- resolve-e2e-policy
|
|
- calculate-tsio-identity
|
|
- build-android-apk-maestro
|
|
- provision-servers
|
|
- validate-maestro-flow-headers
|
|
- resolve-server-topology
|
|
permissions:
|
|
contents: read
|
|
statuses: write
|
|
actions: read
|
|
id-token: write
|
|
# Finalize re-reads the PR labels for E2E/Override; a reusable workflow
|
|
# cannot escalate past the caller, so grant it here too.
|
|
issues: read
|
|
if: |
|
|
!cancelled() &&
|
|
needs.resolve-e2e-policy.outputs.e2e_override != 'true' &&
|
|
(inputs.PLATFORM == 'android' || inputs.PLATFORM == 'both') &&
|
|
needs.provision-servers.result == 'success' &&
|
|
needs.validate-maestro-flow-headers.result == 'success' &&
|
|
(needs.build-android-apk-maestro.result == 'success' || needs.build-android-apk-maestro.result == 'skipped')
|
|
uses: ./.github/workflows/e2e-maestro-pr.yml
|
|
with:
|
|
MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }}
|
|
PLATFORM: android
|
|
ANDROID_SITE_1_URL: ${{ needs.resolve-server-topology.outputs.android-site-1-url }}
|
|
ANDROID_SITE_2_URL: ${{ needs.resolve-server-topology.outputs.android-site-2-url }}
|
|
IOS_SITE_1_URL: ${{ needs.resolve-server-topology.outputs.ios-site-1-url }}
|
|
IOS_SITE_2_URL: ${{ needs.resolve-server-topology.outputs.ios-site-2-url }}
|
|
SITE_3_URL: ${{ needs.resolve-server-topology.outputs.site-3-url }}
|
|
run_type: ${{ inputs.run_type || 'PR' }}
|
|
artifact_run_id: ${{ github.run_id }}
|
|
record_tests_in_zephyr: ${{ inputs.run_type == 'RELEASE' || inputs.run_type == 'MAIN' || inputs.run_type == 'MASTER' }}
|
|
tsio-job-configs: ${{ needs.calculate-tsio-identity.outputs.job-configs }}
|
|
secrets: inherit
|
|
|
|
e2e-remove-matterwick-label:
|
|
name: Remove Matterwick E2E label from PR
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
contents: read
|
|
issues: write
|
|
pull-requests: write
|
|
needs:
|
|
- resolve-e2e-policy
|
|
- run-detox-ios
|
|
- run-detox-android
|
|
- run-maestro-ios
|
|
- run-maestro-android
|
|
# Strip E2E/Run* after tests finish unless Maestro was externally cancelled
|
|
# (leave the label so the suite can be re-triggered).
|
|
if: |
|
|
!cancelled() &&
|
|
needs.resolve-e2e-policy.outputs.e2e_override != 'true' &&
|
|
contains(fromJSON('["success","failure","skipped"]'), needs.run-detox-ios.result) &&
|
|
contains(fromJSON('["success","failure","skipped"]'), needs.run-detox-android.result) &&
|
|
contains(fromJSON('["success","failure","skipped"]'), needs.run-maestro-ios.result) &&
|
|
contains(fromJSON('["success","failure","skipped"]'), needs.run-maestro-android.result)
|
|
steps:
|
|
- name: e2e/remove-label-from-pr
|
|
continue-on-error: true
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
github-token: ${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}
|
|
script: |
|
|
const owner = context.repo.owner;
|
|
const repo = context.repo.repo;
|
|
const runLabels = ['E2E/Run', 'E2E/Run-iOS', 'E2E/Run-Android'];
|
|
|
|
let prNumber = parseInt(process.env.PR_NUMBER, 10);
|
|
if (!Number.isFinite(prNumber) && process.env.MOBILE_VERSION) {
|
|
const {data: prs} = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
|
owner,
|
|
repo,
|
|
commit_sha: process.env.MOBILE_VERSION,
|
|
});
|
|
const openPr = prs.find((pr) => pr.state === 'open');
|
|
if (openPr) {
|
|
prNumber = openPr.number;
|
|
console.log(`Resolved PR #${prNumber} from commit ${process.env.MOBILE_VERSION}`);
|
|
}
|
|
}
|
|
if (!Number.isFinite(prNumber)) {
|
|
const branch = process.env.BRANCH;
|
|
if (branch) {
|
|
const {data: prs} = await github.rest.pulls.list({
|
|
owner,
|
|
repo,
|
|
head: `${owner}:${branch}`,
|
|
state: 'open',
|
|
per_page: 1,
|
|
});
|
|
if (prs.length > 0) {
|
|
prNumber = prs[0].number;
|
|
}
|
|
}
|
|
}
|
|
if (!Number.isFinite(prNumber)) {
|
|
core.warning('No PR found — skipping label removal');
|
|
return;
|
|
}
|
|
|
|
const {data: workflows} = await github.rest.actions.listRepoWorkflows({owner, repo});
|
|
const e2eWorkflow = workflows.workflows.find((workflow) => workflow.name === 'E2E');
|
|
if (e2eWorkflow) {
|
|
// Cache PR lookups per head_sha to avoid quadratic API calls.
|
|
const prsForSha = new Map();
|
|
const lookupPRsForSha = async (sha) => {
|
|
if (!sha) return [];
|
|
if (prsForSha.has(sha)) return prsForSha.get(sha);
|
|
try {
|
|
const {data} = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
|
owner,
|
|
repo,
|
|
commit_sha: sha,
|
|
});
|
|
const numbers = (data || []).map((p) => p.number);
|
|
prsForSha.set(sha, numbers);
|
|
return numbers;
|
|
} catch (err) {
|
|
core.warning(`listPullRequestsAssociatedWithCommit failed for ${sha}: ${err.message}`);
|
|
prsForSha.set(sha, []);
|
|
return [];
|
|
}
|
|
};
|
|
|
|
for (const status of ['queued', 'in_progress', 'waiting']) {
|
|
const {data: activeRuns} = await github.rest.actions.listWorkflowRuns({
|
|
owner,
|
|
repo,
|
|
workflow_id: e2eWorkflow.id,
|
|
status,
|
|
branch: process.env.BRANCH,
|
|
});
|
|
for (const run of activeRuns.workflow_runs) {
|
|
if (run.id === context.runId) {
|
|
continue;
|
|
}
|
|
// Fall back to per-SHA PR lookup when run.pull_requests is empty (fork PRs / webhook timing).
|
|
let belongsToPR;
|
|
if (run.pull_requests?.length) {
|
|
belongsToPR = run.pull_requests.some((pr) => pr.number === prNumber);
|
|
} else {
|
|
const numbers = await lookupPRsForSha(run.head_sha);
|
|
belongsToPR = numbers.includes(prNumber);
|
|
}
|
|
if (belongsToPR) {
|
|
core.info(`PR #${prNumber} has another active E2E run — skipping label removal (run ${run.id}).`);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`Removing E2E labels from PR #${prNumber} (workflow conclusion: ${process.env.WORKFLOW_CONCLUSION || 'unknown'})`);
|
|
for (const label of runLabels) {
|
|
try {
|
|
await github.rest.issues.removeLabel({
|
|
issue_number: prNumber,
|
|
owner,
|
|
repo,
|
|
name: label,
|
|
});
|
|
console.log(`Removed ${label}`);
|
|
} catch (err) {
|
|
if (err.status !== 404) {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
env:
|
|
PR_NUMBER: ${{ inputs.pr_number }}
|
|
MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }}
|
|
BRANCH: ${{ github.ref_name }}
|
|
WORKFLOW_CONCLUSION: ${{ job.status }}
|
|
|
|
# Per-platform TSIO commit statuses are finalized inside each Detox/Maestro
|
|
# generate-report / maestro job after that job's upload (total_reports_expected=1).
|
|
# No combined e2e/mobile finalizer — avoids cross-job clobber and pending races.
|
|
|
|
# One Mattermost channel rollup (best-effort). Per-job finalize leaves channel
|
|
# notify off so N legs do not spam; this job posts a single summary.
|
|
tsio-channel-notify:
|
|
name: Post Mattermost E2E channel rollup
|
|
runs-on: ubuntu-24.04
|
|
if: >-
|
|
always() &&
|
|
needs.resolve-e2e-policy.outputs.e2e_override != 'true' &&
|
|
needs.calculate-tsio-identity.result == 'success' &&
|
|
(inputs.run_type == 'PR' || inputs.run_type == 'MAIN' || inputs.run_type == 'MASTER')
|
|
needs:
|
|
- resolve-e2e-policy
|
|
- calculate-tsio-identity
|
|
- run-detox-ios
|
|
- run-detox-android
|
|
- run-maestro-ios
|
|
- run-maestro-android
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ inputs.MOBILE_VERSION }}
|
|
persist-credentials: false
|
|
sparse-checkout: |
|
|
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: Aggregate per-job TSIO groups and notify channel
|
|
env:
|
|
JOB_CONFIGS: ${{ needs.calculate-tsio-identity.outputs.job-configs }}
|
|
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: |
|
|
UPSTREAM="true"
|
|
for r in "${{ needs.run-detox-ios.result }}" "${{ needs.run-detox-android.result }}" "${{ needs.run-maestro-ios.result }}" "${{ needs.run-maestro-android.result }}"; do
|
|
case "$r" in
|
|
success|skipped) ;;
|
|
*) UPSTREAM="false" ;;
|
|
esac
|
|
done
|
|
node detox/utils/tsio-channel-notify-rollup.js \
|
|
--job-configs="$JOB_CONFIGS" \
|
|
--upstream-succeeded="$UPSTREAM" \
|
|
--poll-attempts=6 || true
|