Skip to content

chore(activity): refresh perps activity on focus (#36158) #3344

chore(activity): refresh perps activity on focus (#36158)

chore(activity): refresh perps activity on focus (#36158) #3344

Workflow file for this run

name: ci
on:
push:
branches: [main, 'release/*']
pull_request:
types:
- opened
- reopened
- synchronize
branches-ignore:
- stable
merge_group:
schedule:
# Run the full suite "overnight," once every hour from 2:00am UTC until 6:00am UTC.
# This helps to identy the flaky and failed tests on main branch
- cron: '0 2-6 * * *'
workflow_call:
inputs:
runner_provider:
description: >-
Runner provider. Leave empty to defer to the repo NAMESPACE_RUNNER_*
Actions variables (see .github/CONTRIBUTING.md).
type: string
required: false
default: ''
pr_number:
description: 'PR number (shadow correlation, optional)'
type: string
required: false
head_sha:
description: 'PR head SHA (shadow correlation, optional)'
type: string
required: false
workflow_dispatch:
inputs:
runner_provider:
description: 'Runner provider: inherit uses the repo NAMESPACE_RUNNER_* variables'
required: true
type: choice
options:
- inherit
- current
- namespace
default: inherit
pr_number:
description: 'PR number (shadow correlation, optional)'
required: false
type: string
head_sha:
description: 'PR head SHA (shadow correlation, optional)'
required: false
type: string
concurrency:
# Scheduled runs must not share the main-branch SHA group. A schedule that
# takes longer than the hourly cadence would otherwise replace the previous
# pending run when the next schedule is created.
group: >-
${{ github.workflow }}-${{
github.event_name == 'schedule' && github.run_id ||
(github.ref == 'refs/heads/main' && github.sha || github.ref)
}}
cancel-in-progress: ${{ !(contains(github.ref, 'refs/heads/main') || contains(github.ref, 'refs/heads/stable')) }}
jobs:
get_requirements:
name: Get workflow and job requirements
uses: ./.github/workflows/get-requirements.yml
check-diff:
name: Check diff
# Not an iOS job: setup.mjs skips CocoaPods off darwin, so this does not need
# Xcode. Keep it off NAMESPACE_RUNNER_IOS so it does not consume reduced
# macos/ios-build concurrency.
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux-small' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }}
needs:
- get_requirements
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
- name: Configure Namespace cache
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1
with:
path: |
~/.cache/yarn
.metamask
node_modules
.yarn/cache
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && 'yarn' || '' }}
- name: Install Yarn dependencies with retry
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: yarn install --immutable
- name: Restore .metamask folder
id: restore-metamask
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
uses: actions/cache@v6
with:
path: .metamask
key: .metamask-${{ runner.os }}-${{ hashFiles('package.json', 'yarn.lock') }}
- name: Install Foundry if cache missed
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && steps.restore-metamask.outputs.cache-hit != 'true' }}
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: yarn install:foundryup
- name: Clean state and following up dependencies installation with retry
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
with:
timeout_minutes: 20
max_attempts: 3
retry_wait_seconds: 30
command: yarn setup:github-ci
- name: Require clean working directory
shell: bash
run: |
if ! git diff --exit-code; then
echo "Working tree dirty at end of job"
exit 1
else
echo "No changes detected"
fi
native-build-fingerprint:
name: Compute native build fingerprint
runs-on: ubuntu-latest
if: >-
${{
needs.get_requirements.outputs.skip_everything != 'true' &&
(
needs.get_requirements.outputs.android_e2e_needed == 'true' ||
needs.get_requirements.outputs.ios_e2e_needed == 'true'
)
}}
permissions:
contents: read
statuses: write
needs:
- get_requirements
outputs:
fingerprint: ${{ steps.publish.outputs.fingerprint }}
steps:
# Default: fingerprint the PR merge tree (or push/schedule commit).
- name: Checkout repository
if: ${{ needs.get_requirements.outputs.use_main_builds_for_test_only_prs != 'true' || github.event_name != 'pull_request' }}
uses: actions/checkout@v6
# Test-only PRs reuse main native builds — fingerprint main HEAD so the lookup
# key matches completed ci.yml runs on the main branch.
- name: Checkout main for test-only fingerprint
if: ${{ needs.get_requirements.outputs.use_main_builds_for_test_only_prs == 'true' && github.event_name == 'pull_request' }}
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 1
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: yarn
- name: Install Yarn dependencies with retry
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: yarn install --immutable
- name: Compute native build fingerprint and post build-source-hash
id: publish
uses: ./.github/actions/post-build-source-hash
with:
github-token: ${{ github.token }}
post-status: ${{ needs.get_requirements.outputs.use_main_builds_for_test_only_prs != 'true' }}
# Commit statuses must be posted on a real commit SHA (not the merge ref).
# `find-reusable-build` looks up the status via `run.head_sha` from
# `listWorkflowRuns`, which GitHub reports as the PR head SHA for pull_request
# events and as the pushed/scheduled commit SHA otherwise — so this is the same
# SHA the lookup queries against.
target-sha: ${{ github.event.pull_request.head.sha || github.sha }}
dedupe:
name: Dedupe
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux-small' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }}
needs:
- get_requirements
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
- name: Configure Namespace cache
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1
with:
path: |
~/.cache/yarn
.metamask
node_modules
.yarn/cache
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && 'yarn' || '' }}
- name: Install Yarn dependencies with retry
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: yarn install --immutable
- name: Clean state and following up dependencies installation
run: yarn setup:github-ci --node
- name: Deduplicate dependencies with retry
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: yarn deduplicate
- name: Print error if duplicates found
shell: bash
run: |
if ! git diff --exit-code; then
echo "Duplicate dependencies detected; run 'yarn deduplicate' to remove them"
exit 1
fi
git-safe-dependencies:
name: Run `@lavamoat/git-safe-dependencies`
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux-small' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }}
needs:
- get_requirements
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
- name: Configure Namespace cache
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1
with:
path: |
~/.cache/yarn
.metamask
node_modules
.yarn/cache
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && 'yarn' || '' }}
- name: Install Yarn dependencies with retry
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: yarn install --immutable
- name: Clean state and following up dependencies installation
run: yarn setup:github-ci --node
- name: Run @lavamoat/git-safe-dependencies with retry
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: yarn git-safe-dependencies
scripts:
name: Run `${{ matrix.scripts }}`
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
matrix.profile || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }}
needs:
- get_requirements
strategy:
# `include` with no base axis yields exactly these combinations, and `name`
# already interpolates a matrix value, so the rendered job names stay
# `Run <script>` and the required status checks keep reporting (MCWP-813).
# `profile` is consumed by `runs-on` above. Shape follows peak RAM.
matrix:
include:
# Stay on 8x16: peak RAM above the 8 GB line.
- scripts: lint # 12.3 GB peak
profile: namespace-profile-metamask-ci-linux
- scripts: lint:tsc # 8.9 GB peak
profile: namespace-profile-metamask-ci-linux
# Right-sized to 4x8: worst observed peak <= 5.6 GB.
- scripts: lint:changelog
profile: namespace-profile-metamask-ci-linux-small
- scripts: format:check
profile: namespace-profile-metamask-ci-linux-small
- scripts: test:depcheck
profile: namespace-profile-metamask-ci-linux-small
- scripts: test:tgz-check
profile: namespace-profile-metamask-ci-linux-small
- scripts: constraints
profile: namespace-profile-metamask-ci-linux-small
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
with:
fetch-depth: 2
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
with:
fetch-depth: 2
- name: Configure Namespace cache
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1
with:
path: |
~/.cache/yarn
.metamask
node_modules
.yarn/cache
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && 'yarn' || '' }}
- name: Install Yarn dependencies with retry
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: yarn install --immutable
- name: Clean state and following up dependencies installation
run: yarn setup:github-ci --node
- run: yarn ${{ matrix['scripts'] }}
- name: Require clean working directory
shell: bash
run: |
if ! git diff --exit-code; then
echo "Working tree dirty at end of job"
exit 1
else
echo "No changes detected"
fi
# Advisory only: never fails, never gates merges. Surfaces findings via a
# workflow warning and step summary instead. Exposes `has-advisories` as a
# job output so a follow-up automation can consume it without this job
# needing to know anything about what (if anything) reacts to it.
dependency-audit:
name: Dependency audit (advisory)
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux-small' || 'ubuntu-latest'
}}
outputs:
has-advisories: ${{ steps.audit.outputs.has-advisories }}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }}
needs:
- get_requirements
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
- name: Configure Namespace cache
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1
with:
path: |
~/.cache/yarn
.metamask
node_modules
.yarn/cache
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && 'yarn' || '' }}
- name: Install Yarn dependencies with retry
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: yarn install --immutable
- name: Dependency audit (advisory only, never fails this job)
id: audit
shell: bash
run: |
set -uo pipefail
if yarn audit:ci; then
echo "No production advisories at moderate severity or above."
echo "has-advisories=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "has-advisories=true" >> "$GITHUB_OUTPUT"
echo "::warning title=Dependency audit::Production dependency advisories found at moderate severity or above. This does not block merge. The audit owner is being notified by the dependency-audit-escalation workflow."
{
echo '### Dependency audit'
echo
echo 'Advisories found at moderate severity or above. This check is advisory only and does not block merge.'
echo
echo "The [dependency-audit-escalation](../actions/workflows/dependency-audit-escalation.yml) workflow owns triage and tracking-issue escalation."
} >> "$GITHUB_STEP_SUMMARY"
exit 0
# Only runs when the job above actually found something — most ci.yml runs
# (the overwhelming majority) skip this entirely, so there's no fixed cron
# spinning up a fresh checkout+install+audit for nothing.
dependency-audit-escalation:
name: Dependency audit escalation
needs:
- dependency-audit
if: ${{ needs.dependency-audit.outputs.has-advisories == 'true' }}
# A reusable workflow's own `permissions:` block can only narrow what its
# caller grants here, never widen it — this must be a superset of
# dependency-audit-escalation.yml's own permissions block, or the call
# fails at startup (zero jobs run) rather than at any individual step.
permissions:
contents: write
pull-requests: write
issues: write
id-token: write # required by MetaMask/github-tools/.github/actions/get-token
uses: ./.github/workflows/dependency-audit-escalation.yml
secrets: inherit
js-bundle-size-check:
# Stays on 8x16 (MCWP-813). It is the thinnest margin on the profile — 14.3 GB
# peak of 16 post-#35071, ~11% headroom — but the heap fix took it from a 13.6%
# to a 2.3% failure rate, so the cheap remedy is already applied. Revisit with
# MCWP-812 if failures recur; `ci-linux-large` exists for that.
name: JS bundle size check
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }}
needs:
- get_requirements
permissions:
contents: read
statuses: write
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
with:
fetch-depth: 2
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
with:
fetch-depth: 2
- name: Pin bundle size baseline SHA
if: github.event_name == 'pull_request'
shell: bash
run: |
set -euo pipefail
BASE_REF='${{ github.event.pull_request.base.ref }}'
# PR checkout is the merge ref (fetch-depth: 2); first parent is the base tip.
MERGE_BASE_SHA="$(git rev-parse HEAD^1)"
echo "Merge-base pinned (${BASE_REF}, HEAD): ${MERGE_BASE_SHA}"
echo "MERGE_BASE_SHA=${MERGE_BASE_SHA}" >> "$GITHUB_ENV"
echo "BUNDLE_SIZE_BASE_REF=${BASE_REF}" >> "$GITHUB_ENV"
- name: Configure Namespace cache
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1
with:
path: |
~/.cache/yarn
.metamask
node_modules
.yarn/cache
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && 'yarn' || '' }}
- name: Install Yarn dependencies with retry
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: yarn install --immutable
- name: Clean state and following up dependencies installation
run: yarn setup:github-ci --no-build-android
- name: Generate iOS bundle
run: yarn gen-bundle:ios
env:
# INFRA-3891: sized for a 16 GB runner with no swap. Do not move this inline
# into gen-bundle:ios, an inline value silently shadows it.
NODE_OPTIONS: --max-old-space-size=6144
- name: Check bundle size
if: github.event_name == 'pull_request'
env:
GITHUB_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
# Baseline: ci/ios-js-bundle-size on merge-base pinned at checkout (before bundle build).
append_job_summary() {
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
cat >> "$GITHUB_STEP_SUMMARY"
fi
}
FILE_PATH='ios/main.jsbundle'
CURRENT_MB="$(stat -c %s "$FILE_PATH" | awk '{print $1/1024/1024}')"
echo "Current iOS JS bundle: ${CURRENT_MB} MB"
BASELINE_MB=""
BASE_REF="${BUNDLE_SIZE_BASE_REF}"
MERGE_BASE_SHA="${MERGE_BASE_SHA:-}"
REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}"
if [[ -z "${MERGE_BASE_SHA}" ]]; then
echo "::warning::Merge-base was not pinned at checkout; skipping iOS JS bundle delta check."
append_job_summary <<EOF
### iOS JS bundle size (PR)
**Result:** skipped — merge-base was not available from the checkout step.
| | MiB |
|---|--:|
| Current bundle | ${CURRENT_MB} |
Baseline would be read from the latest successful \`ci/ios-js-bundle-size\` status on the merge-base of HEAD and \`${BASE_REF}\`.
EOF
exit 0
fi
echo "Merge-base (origin/${BASE_REF}, HEAD): ${MERGE_BASE_SHA}"
if ! STATUSES_JSON="$(
curl -fsS \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/commits/${MERGE_BASE_SHA}/status"
)"; then
echo "::warning::Failed to fetch commit statuses for merge-base ${MERGE_BASE_SHA}; skipping iOS JS bundle delta check."
append_job_summary <<EOF
### iOS JS bundle size (PR)
**Result:** skipped — could not load [combined status](${REPO_URL}/commit/${MERGE_BASE_SHA}) for merge-base \`${MERGE_BASE_SHA}\` (\`${BASE_REF}\`).
| | MiB |
|---|--:|
| Current bundle | ${CURRENT_MB} |
Baseline would be read from the latest successful \`ci/ios-js-bundle-size\` status description on that commit (MiB from awk, then an optional trailing semicolon, matching the main-branch post step).
EOF
exit 0
fi
DESC="$(
echo "$STATUSES_JSON" | jq -r '
(.statuses // [])
| map(select(.context == "ci/ios-js-bundle-size" and .state == "success"))
| sort_by(.updated_at // .created_at)
| if length > 0 then .[-1].description else "" end
'
)"
# Status description is "${FILE_SIZE_MB};" from the Post iOS JS bundle size step (awk MiB + ';').
# Regex in a variable: ';' in an unquoted [[ =~ ... ]] pattern is parsed as a command separator (shellcheck SC1072).
_bundle_desc_re='^[[:space:]]*([0-9]+(\.[0-9]*)?)[[:space:]]*;?[[:space:]]*$'
if [[ "$DESC" =~ $_bundle_desc_re ]]; then
BASELINE_MB="${BASH_REMATCH[1]}"
echo "Baseline from merge-base status: ${BASELINE_MB} MB"
else
echo "::warning::No successful ci/ios-js-bundle-size status (or unrecognized description) on merge-base ${MERGE_BASE_SHA}; skipping iOS JS bundle delta check."
append_job_summary <<EOF
### iOS JS bundle size (PR)
**Result:** skipped — no parseable \`ci/ios-js-bundle-size\` success status on merge-base [\`${MERGE_BASE_SHA}\`](${REPO_URL}/commit/${MERGE_BASE_SHA}) (\`${BASE_REF}\`).
| | MiB |
|---|--:|
| Current bundle | ${CURRENT_MB} |
| Raw description | \`${DESC:-<empty>}\` |
EOF
exit 0
fi
if [[ -z "${BASELINE_MB:-}" ]]; then
echo "::warning::Baseline is undefined; skipping iOS JS bundle delta check."
append_job_summary <<EOF
### iOS JS bundle size (PR)
**Result:** skipped — baseline MiB was empty after parsing.
| | MiB |
|---|--:|
| Current bundle | ${CURRENT_MB} |
EOF
exit 0
fi
# awk, not bc: Namespace linux images do not ship bc (GitHub-hosted ubuntu does).
DELTA_MB="$(awk -v c="${CURRENT_MB}" -v b="${BASELINE_MB}" 'BEGIN { print c - b }')"
echo "Delta (current - baseline): ${DELTA_MB} MB"
echo "::notice::iOS JS bundle delta vs baseline: ${DELTA_MB} MB (current ${CURRENT_MB} MB, baseline ${BASELINE_MB} MB)"
if awk -v d="${DELTA_MB}" 'BEGIN { exit !(d > 1) }'; then
echo "::error::JS bundle exceeds baseline by more than 1 MiB (delta ${DELTA_MB} MiB; current ${CURRENT_MB} MiB, baseline ${BASELINE_MB} MiB)."
append_job_summary <<EOF
### iOS JS bundle size (PR)
**Result:** failed — delta is more than 1 MiB above the status baseline on merge-base [\`${MERGE_BASE_SHA}\`](${REPO_URL}/commit/${MERGE_BASE_SHA}) (\`${BASE_REF}\`).
| | MiB |
|---|--:|
| Merge-base (\`${BASE_REF}\` × HEAD) | [\`${MERGE_BASE_SHA}\`](${REPO_URL}/commit/${MERGE_BASE_SHA}) |
| Baseline (from status) | ${BASELINE_MB} |
| Current bundle | ${CURRENT_MB} |
| Delta (current - baseline) | ${DELTA_MB} |
EOF
exit 1
fi
echo "JS bundle size check passed (delta is one or less)."
append_job_summary <<EOF
### iOS JS bundle size (PR)
**Result:** passed
| | MiB |
|---|--:|
| Merge-base (\`${BASE_REF}\` × HEAD) | [\`${MERGE_BASE_SHA}\`](${REPO_URL}/commit/${MERGE_BASE_SHA}) |
| Baseline (from \`ci/ios-js-bundle-size\` status) | ${BASELINE_MB} |
| Current bundle | ${CURRENT_MB} |
| Delta (current - baseline) | ${DELTA_MB} |
Fail threshold: more than 1 MiB above baseline.
EOF
- name: Post iOS JS bundle size to commit status
if: ${{ github.ref == 'refs/heads/main' }}
env:
GITHUB_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
FILE_PATH='ios/main.jsbundle'
# Match scripts/js-bundle-stats.sh: GNU stat byte size, MiB via awk
FILE_SIZE_MB="$(stat -c %s "$FILE_PATH" | awk '{print $1/1024/1024}')"
echo "File size of $FILE_PATH: $FILE_SIZE_MB MB"
COMMIT_SHA='${{ github.event.pull_request.head.sha || github.sha }}'
TARGET_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
BODY="$(jq -n \
--arg state success \
--arg context 'ci/ios-js-bundle-size' \
--arg desc "${FILE_SIZE_MB};" \
--arg target_url "$TARGET_URL" \
'{
state: $state,
context: $context,
description: ($desc | if length > 140 then .[0:140] else . end),
target_url: $target_url
}')"
curl -fsS -X POST \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/statuses/${COMMIT_SHA}" \
-d "$BODY"
- name: Upload iOS bundle (artifact stores)
uses: ./.github/actions/dual-upload-e2e-artifact
with:
name: ios-bundle
path: ios/main.jsbundle
retention-days: 7
runner-provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
ship-js-bundle-size-check:
name: Ship JS bundle size check
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux-small' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
needs: [js-bundle-size-check]
if: ${{ github.ref == 'refs/heads/main' }}
permissions:
id-token: write
contents: read
actions: read
steps:
- name: Get token
id: get-token
uses: MetaMask/github-tools/.github/actions/get-token@v1
with:
token-exchange-url: ${{ vars.TOKEN_EXCHANGE_URL }}
target-repository: MetaMask/mobile_bundlesize_stats
permissions: |
contents: write
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
- name: Download iOS bundle (Namespace)
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
uses: namespace-actions/download-artifact@7cbad919e4b0e09f17e9d6311a444ff002992b5b # v2.0.1
with:
name: ios-bundle
path: ios
- name: Download iOS bundle (current)
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
uses: actions/download-artifact@v7
with:
name: ios-bundle
path: ios
- name: Verify iOS bundle artifact layout
shell: bash
run: |
set -euo pipefail
ls -la ios/
if [[ ! -f ios/main.jsbundle ]]; then
echo "::error::Expected ios/main.jsbundle to be a regular file (artifact layout may be wrong)."
exit 1
fi
- name: Report iOS JS bundle size and commit (main)
run: |
SHORT_SHA="$(git rev-parse --short HEAD)"
FILE_PATH='ios/main.jsbundle'
MB="$(stat -c %s "$FILE_PATH" | awk '{print $1/1024/1024}')"
echo "commit_short=$SHORT_SHA"
echo "::notice::iOS JS bundle (main): ${MB} MiB @ ${SHORT_SHA}"
- name: Push bundle size to mobile_bundlesize_stats repo
run: ./scripts/push-bundle-size.sh
env:
GITHUB_ACTOR: metamaskbot
GITHUB_TOKEN: ${{ steps.get-token.outputs.token }}
check-workflows:
name: Check workflows
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux-small' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }}
needs:
- get_requirements
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
- name: Download actionlint
id: download-actionlint
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/62dc61a45fc95efe8c800af7a557ab0b9165d63b/scripts/download-actionlint.bash) 1.7.1
shell: bash
- name: Check workflow files
run: ${{ steps.download-actionlint.outputs.executable }} -color -config-file .github/actionlint.yaml
shell: bash
deploy-platform-api-docs:
name: Deploy Platform API Docs
needs: check-workflows
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
permissions:
contents: read
id-token: write
uses: ./.github/workflows/deploy-platform-api-docs.yml
# Warms the Namespace cache volume before test shards run. On non-Namespace
# runners uploads a ci-js-deps artifact so consumers can skip their own install.
prepare-ci-js-deps:
name: Prepare CI JS dependencies
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux-small' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }}
needs:
- get_requirements
steps:
# Last job in the Linux cohort still cloning from github.com on Namespace;
# everything else uses the in-region git mirror. Namespace scales network
# bandwidth with machine shape, so an internet clone costs roughly twice
# the wall-clock on the 4x8 profile as it did on 8x16.
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
- uses: ./.github/actions/setup-ci-js-deps
with:
runner_provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
# TEMP: artifact fallback for non-Namespace runners.
# Remove these two steps once Namespace passes the trial and becomes the default.
- name: Pack CI JS deps
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
run: tar -czf ci-js-deps.tar.gz node_modules app/util/termsOfUse/termsOfUseContent.ts
- name: Upload CI JS deps artifact
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
uses: actions/upload-artifact@v7
with:
name: ci-js-deps
path: ci-js-deps.tar.gz
retention-days: 1
compression-level: 0
if-no-files-found: error
unit-tests:
name: Unit tests (${{ matrix.shard }})
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
if: ${{ !cancelled() && needs.get_requirements.result == 'success' && needs.get_requirements.outputs.skip_everything != 'true' }}
needs:
- get_requirements
- prepare-ci-js-deps
strategy:
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
# TEMP: artifact fallback for non-Namespace runners.
# Remove these two steps once Namespace passes the trial and becomes the default.
- name: Download CI JS deps artifact
id: download-ci-js-deps
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && needs.prepare-ci-js-deps.result == 'success' }}
continue-on-error: true
uses: actions/download-artifact@v7
with:
name: ci-js-deps
path: .
- name: Extract CI JS deps
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && needs.prepare-ci-js-deps.result == 'success' && steps.download-ci-js-deps.outcome == 'success' }}
run: tar -xzf ci-js-deps.tar.gz && rm ci-js-deps.tar.gz
- uses: ./.github/actions/setup-ci-js-deps
with:
runner_provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
- name: Prepare results directory
run: mkdir -p tests/results
# The "10" in this command is the total number of shards. It must be kept
# in sync with the length of matrix.shard
# Namespace Linux: --maxWorkers=33% (~3 workers on 8 vCPU) + 12GiB heap.
- run: yarn test:unit --shard=${{ matrix.shard }}/10${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' && ' --maxWorkers=33%' || '' }} --workerIdleMemoryLimit=2GB --forceExit --silent --coverageReporters=json --json --outputFile=tests/results/unit-test-results-${{ matrix.shard }}.json
env:
NODE_OPTIONS: --max_old_space_size=12288
- name: Rename coverage report and extract test count for this shard
shell: bash
run: |
mv ./tests/coverage/coverage-final.json ./tests/coverage/coverage-unit-${{ matrix.shard }}.json
cp tests/results/unit-test-results-${{ matrix.shard }}.json ./tests/coverage/jest-results.json
count=$(jq '(.numPassedTests // 0) + (.numFailedTests // 0)' tests/results/unit-test-results-${{ matrix.shard }}.json)
echo "{\"count\": $count}" > ./tests/coverage/count.json
# Namespace stores the full shard dir for merge; GitHub gets jest-results.json
# only so qa-stats can read it via the GitHub API.
- name: Upload coverage unit shard (artifact stores)
uses: ./.github/actions/dual-upload-e2e-artifact
with:
name: coverage-unit-${{ matrix.shard }}
path: ./tests/coverage/
github-path: ./tests/coverage/jest-results.json
if-no-files-found: error
retention-days: 7
runner-provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
- name: Require clean working directory
shell: bash
run: |
if ! git diff --exit-code; then
echo "Working tree dirty at end of job"
exit 1
else
echo "No changes detected"
fi
# We need to merge unit, component view, and integration tests into a
# single coverage report so the PR coverage threshold calculation is accurate.
merge-unit-and-component-view-tests:
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux-small' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
needs:
- prepare-ci-js-deps
- unit-tests
- component-view-tests
- integration-tests
# Wait for integration shards so coverage can merge into Sonar, but still run
# when integration fails — missing integration artifacts are tolerated below.
if: ${{ always() && github.event_name != 'merge_group' && !cancelled() && needs.prepare-ci-js-deps.result == 'success' && needs.unit-tests.result == 'success' && needs.component-view-tests.result == 'success' }}
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
# TEMP: artifact fallback for non-Namespace runners.
# Remove these two steps once Namespace passes the trial and becomes the default.
- name: Download CI JS deps artifact
id: download-ci-js-deps
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && needs.prepare-ci-js-deps.result == 'success' }}
continue-on-error: true
uses: actions/download-artifact@v7
with:
name: ci-js-deps
path: .
- name: Extract CI JS deps
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && needs.prepare-ci-js-deps.result == 'success' && steps.download-ci-js-deps.outcome == 'success' }}
run: tar -xzf ci-js-deps.tar.gz && rm ci-js-deps.tar.gz
- uses: ./.github/actions/setup-ci-js-deps
with:
runner_provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
- name: Download coverage shards (Namespace)
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
uses: namespace-actions/download-artifact@7cbad919e4b0e09f17e9d6311a444ff002992b5b # v2.0.1
with:
pattern: coverage-*
path: tests/coverage/
- name: Download coverage shards (current)
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
uses: actions/download-artifact@v7
with:
pattern: coverage-*
path: tests/coverage/
- name: Aggregate test counts and gather coverage reports
id: aggregate-coverage
shell: bash
env:
INTEGRATION_TESTS_RESULT: ${{ needs.integration-tests.result }}
run: |
echo "Upstream integration-tests result: ${INTEGRATION_TESTS_RESULT}"
integration_shard_count=$(
find ./tests/coverage -maxdepth 1 -type d -name 'coverage-integration-*' 2>/dev/null | wc -l | tr -d ' '
)
echo "Integration coverage shards present: ${integration_shard_count}"
if [[ "$INTEGRATION_TESTS_RESULT" =~ ^(failure|cancelled)$ ]]; then
echo "::warning::Integration tests did not fully succeed; merging available coverage shards only."
fi
unit_total=0
for file in ./tests/coverage/coverage-unit-*/count.json; do
[ -f "$file" ] || continue
count=$(jq '.count // 0' "$file")
unit_total=$((unit_total + count))
done
echo "{\"unit_test_number\": $unit_total}" > unit-test-stats.json
echo "Unit test count: $unit_total"
cv_total=0
for file in ./tests/coverage/coverage-cv-*/count.json; do
[ -f "$file" ] || continue
count=$(jq '.count // 0' "$file")
cv_total=$((cv_total + count))
done
echo "{\"component_view_test_number\": $cv_total}" > cv-test-stats.json
echo "CV test count: $cv_total"
integration_total=0
for file in ./tests/coverage/coverage-integration-*/count.json; do
[ -f "$file" ] || continue
count=$(jq '.count // 0' "$file")
integration_total=$((integration_total + count))
done
echo "{\"integration_test_number\": $integration_total}" > integration-test-stats.json
echo "Integration test count: $integration_total"
mkdir -p tests/coverage-cv-merged
for file in ./tests/coverage/coverage-cv-*/coverage-cv-*.json; do
[ -f "$file" ] && cp "$file" ./tests/coverage-cv-merged/
done
mkdir -p tests/coverage-integration-merged
for file in ./tests/coverage/coverage-integration-*/coverage-integration-*.json; do
[ -f "$file" ] && cp "$file" ./tests/coverage-integration-merged/
done
integration_json_count=$(
find ./tests/coverage-integration-merged -maxdepth 1 -name '*.json' 2>/dev/null | wc -l | tr -d ' '
)
if [[ "$integration_json_count" -gt 0 ]]; then
echo "has_integration_coverage=true" >> "$GITHUB_OUTPUT"
else
echo "has_integration_coverage=false" >> "$GITHUB_OUTPUT"
echo "No integration coverage artifacts; downstream integration nyc/upload steps will be skipped."
fi
mkdir -p tests/coverage-unit-merged
for file in ./tests/coverage/coverage-unit-*/coverage-unit-*.json; do
[ -f "$file" ] && cp "$file" ./tests/coverage-unit-merged/
done
find ./tests/coverage/coverage-* -name 'coverage-*.json' -exec mv {} ./tests/coverage/ \;
- run: yarn test:merge-coverage
- run: yarn test:validate-coverage
- name: Upload lcov.info (artifact stores)
uses: ./.github/actions/dual-upload-e2e-artifact
with:
name: lcov.info
path: ./tests/merged-coverage/lcov.info
if-no-files-found: error
retention-days: 7
runner-provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
- name: Upload cv-test-stats (artifact stores)
uses: ./.github/actions/dual-upload-e2e-artifact
with:
name: cv-test-stats
path: ./cv-test-stats.json
if-no-files-found: error
retention-days: 7
runner-provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
- name: Upload integration-test-stats (artifact stores)
uses: ./.github/actions/dual-upload-e2e-artifact
with:
name: integration-test-stats
path: ./integration-test-stats.json
if-no-files-found: error
retention-days: 7
runner-provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
- name: Upload unit-test-stats (artifact stores)
uses: ./.github/actions/dual-upload-e2e-artifact
with:
name: unit-test-stats
path: ./unit-test-stats.json
if-no-files-found: error
retention-days: 7
runner-provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
- name: Generate CV test coverage report
run: yarn nyc report --temp-dir ./tests/coverage-cv-merged --report-dir ./tests/coverage-cv-lcov --reporter html --reporter json-summary
- name: Upload cv-test-coverage-html (artifact stores)
uses: ./.github/actions/dual-upload-e2e-artifact
with:
name: cv-test-coverage-html
path: ./tests/coverage-cv-lcov/
if-no-files-found: error
retention-days: 7
runner-provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
- name: Upload cv-test-coverage-summary (artifact stores)
uses: ./.github/actions/dual-upload-e2e-artifact
with:
name: cv-test-coverage-summary
path: ./tests/coverage-cv-lcov/coverage-summary.json
if-no-files-found: error
retention-days: 7
runner-provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
- name: Generate integration test coverage report
if: ${{ steps.aggregate-coverage.outputs.has_integration_coverage == 'true' }}
run: yarn nyc report --temp-dir ./tests/coverage-integration-merged --report-dir ./tests/coverage-integration-lcov --reporter html --reporter json-summary
- name: Upload integration-test-coverage-html (artifact stores)
if: ${{ steps.aggregate-coverage.outputs.has_integration_coverage == 'true' }}
uses: ./.github/actions/dual-upload-e2e-artifact
with:
name: integration-test-coverage-html
path: ./tests/coverage-integration-lcov/
if-no-files-found: error
retention-days: 7
runner-provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
- name: Upload integration-test-coverage-summary (artifact stores)
if: ${{ steps.aggregate-coverage.outputs.has_integration_coverage == 'true' }}
uses: ./.github/actions/dual-upload-e2e-artifact
with:
name: integration-test-coverage-summary
path: ./tests/coverage-integration-lcov/coverage-summary.json
if-no-files-found: error
retention-days: 7
runner-provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
- name: Generate unit test coverage summary
run: yarn nyc report --temp-dir ./tests/coverage-unit-merged --report-dir ./tests/coverage-unit-lcov --reporter json-summary
- name: Upload unit-test-coverage-summary (artifact stores)
uses: ./.github/actions/dual-upload-e2e-artifact
with:
name: unit-test-coverage-summary
path: ./tests/coverage-unit-lcov/coverage-summary.json
if-no-files-found: error
retention-days: 7
runner-provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
- name: Require clean working directory
shell: bash
run: |
if ! git diff --exit-code; then
echo "Working tree dirty at end of job"
exit 1
else
echo "No changes detected"
fi
component-view-tests:
name: Component view tests (${{ matrix.shard }})
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
if: ${{ !cancelled() && needs.get_requirements.result == 'success' && needs.get_requirements.outputs.skip_everything != 'true' }}
needs:
- get_requirements
- prepare-ci-js-deps
strategy:
matrix:
shard: [1, 2, 3]
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
# TEMP: artifact fallback for non-Namespace runners.
# Remove these two steps once Namespace passes the trial and becomes the default.
- name: Download CI JS deps artifact
id: download-ci-js-deps
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && needs.prepare-ci-js-deps.result == 'success' }}
continue-on-error: true
uses: actions/download-artifact@v7
with:
name: ci-js-deps
path: .
- name: Extract CI JS deps
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && needs.prepare-ci-js-deps.result == 'success' && steps.download-ci-js-deps.outcome == 'success' }}
run: tar -xzf ci-js-deps.tar.gz && rm ci-js-deps.tar.gz
- uses: ./.github/actions/setup-ci-js-deps
with:
runner_provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
- name: Prepare results directory
run: mkdir -p tests/results
- run: |
yarn test:view:ci \
--shard=${{ matrix.shard }}/3 \
--json \
--outputFile=tests/results/cv-test-results-${{ matrix.shard }}.json
env:
NODE_OPTIONS: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' && '--max-old-space-size=12288' || '--max-old-space-size=20480' }}
- name: Rename coverage report and extract test count for this shard
shell: bash
run: |
mv ./tests/coverage/coverage-final.json ./tests/coverage/coverage-cv-${{ matrix.shard }}.json
cp tests/results/cv-test-results-${{ matrix.shard }}.json ./tests/coverage/jest-results.json
count=$(jq '(.numPassedTests // 0) + (.numFailedTests // 0)' tests/results/cv-test-results-${{ matrix.shard }}.json)
echo "{\"count\": $count}" > ./tests/coverage/count.json
- name: Upload coverage CV shard (artifact stores)
uses: ./.github/actions/dual-upload-e2e-artifact
with:
name: coverage-cv-${{ matrix.shard }}
path: ./tests/coverage/
github-path: ./tests/coverage/jest-results.json
if-no-files-found: error
retention-days: 7
runner-provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
# Integration tests are optional for the PR gate — they feed coverage into
# merge → Sonar but are not listed in check-all-jobs-pass needs.
integration-tests:
# Renders exactly as GitHub's own matrix suffix did for `name: Integration tests`
# with a single `shard` axis, so the `Integration tests (1)` / `(2)` required
# checks keep reporting. Spelling it out is load-bearing: the auto-suffix joins
# *every* matrix value, so adding `profile` below would otherwise rename these
# checks to `Integration tests (1, namespace-profile-...)` (MCWP-813).
name: Integration tests (${{ matrix.shard }})
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
matrix.profile || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
if: ${{ !cancelled() && needs.get_requirements.result == 'success' && needs.get_requirements.outputs.skip_everything != 'true' }}
needs:
- get_requirements
- prepare-ci-js-deps
strategy:
matrix:
include:
# Shard 1 peaks at 9.3 GB — above the 8 GB line, so it keeps 8x16.
- shard: 1
profile: namespace-profile-metamask-ci-linux
heap_mb: 12288
# Shard 2 peaks at 5.3 GB. On the 4x8 shape the heap ceiling has to come
# down with it: 12288 would let V8 grow past physical RAM before it
# self-limits, and RAM overrun is a hard exit-137. 6144 sits above the
# observed peak, and the profile carries 8 GB of swap as a backstop.
- shard: 2
profile: namespace-profile-metamask-ci-linux-small
heap_mb: 6144
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
# TEMP: artifact fallback for non-Namespace runners.
# Remove these two steps once Namespace passes the trial and becomes the default.
- name: Download CI JS deps artifact
id: download-ci-js-deps
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && needs.prepare-ci-js-deps.result == 'success' }}
continue-on-error: true
uses: actions/download-artifact@v7
with:
name: ci-js-deps
path: .
- name: Extract CI JS deps
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' && needs.prepare-ci-js-deps.result == 'success' && steps.download-ci-js-deps.outcome == 'success' }}
run: tar -xzf ci-js-deps.tar.gz && rm ci-js-deps.tar.gz
- uses: ./.github/actions/setup-ci-js-deps
with:
runner_provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
- name: Prepare results directory
run: mkdir -p tests/results
- run: |
yarn test:integration:ci \
--shard=${{ matrix.shard }}/2 \
--json \
--outputFile=tests/results/integration-test-results-${{ matrix.shard }}.json
env:
NODE_OPTIONS: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' && format('--max-old-space-size={0}', matrix.heap_mb) || '--max-old-space-size=20480' }}
- name: Rename coverage report and extract test count for this shard
shell: bash
run: |
mv ./tests/coverage/coverage-final.json ./tests/coverage/coverage-integration-${{ matrix.shard }}.json
cp tests/results/integration-test-results-${{ matrix.shard }}.json ./tests/coverage/jest-results.json
count=$(jq '(.numPassedTests // 0) + (.numFailedTests // 0)' tests/results/integration-test-results-${{ matrix.shard }}.json)
echo "{\"count\": $count}" > ./tests/coverage/count.json
- name: Upload coverage integration shard (artifact stores)
uses: ./.github/actions/dual-upload-e2e-artifact
with:
name: coverage-integration-${{ matrix.shard }}
path: ./tests/coverage/
github-path: ./tests/coverage/jest-results.json
if-no-files-found: error
retention-days: 7
runner-provider: ${{ env.RESOLVED_RUNNER_PROVIDER }}
smart-e2e-selection:
name: 'Smart E2E Selection'
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux-small' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
if: ${{ needs.get_requirements.outputs.run_smart_e2e_selection == 'true' }}
needs:
- get_requirements
continue-on-error: true
permissions:
contents: read
issues: write
pull-requests: write
outputs:
ai_e2e_test_tags: ${{ steps.e2e-selection.outputs.ai_e2e_test_tags }}
ai_confidence: ${{ steps.e2e-selection.outputs.ai_confidence }}
ai_performance_test_tags: ${{ steps.e2e-selection.outputs.ai_performance_test_tags }}
ai_performance_test_reasoning: ${{ steps.e2e-selection.outputs.ai_performance_test_reasoning }}
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
with:
sparse-checkout: |
.github/actions/smart-e2e-selection
sparse-checkout-cone-mode: false
fetch-depth: 1
- name: Checkout for action definition
uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
with:
sparse-checkout: |
.github/actions/smart-e2e-selection
sparse-checkout-cone-mode: false
fetch-depth: 1
- name: Run Smart E2E Selection
id: e2e-selection
uses: ./.github/actions/smart-e2e-selection
with:
claude-api-key: ${{ secrets.E2E_CLAUDE_API_KEY }}
openai-api-key: ${{ secrets.E2E_OPENAI_API_KEY }}
google-api-key: ${{ secrets.E2E_GEMINI_API_KEY }}
github-token: ${{ github.token }}
pr-number: ${{ github.event.pull_request.number }}
repository: ${{ github.repository }}
post-comment: true
base-ref: ${{ github.event.pull_request.base.ref }}
run-performance-tests-pr:
name: 'Run Performance Tests (PR)'
# Run when:
# - run-performance-tests label is on the PR (forced run, all tests), OR
# - the PR targets main and Smart E2E Selection chose performance tags.
# PRs targeting release/* or stable only run performance tests via the label.
# ai_performance_test_tags == '[]' → AI ran and found no perf-relevant changes: skip.
# ai_performance_test_tags == '' → conservative fallback (AI failed) or run-performance-tests label: run all tests.
# ai_performance_test_tags == '[…]' → specific tags selected: run those tests.
if: >-
${{
github.event_name == 'pull_request' &&
!cancelled() &&
(
needs.get_requirements.outputs.run_performance == 'true' ||
(
github.event.pull_request.base.ref == 'main' &&
needs.smart-e2e-selection.result == 'success' &&
needs.smart-e2e-selection.outputs.ai_performance_test_tags != '[]'
)
)
}}
needs: [get_requirements, smart-e2e-selection, native-build-fingerprint]
uses: ./.github/workflows/run-performance-e2e.yml
with:
performance_tags: ${{ needs.get_requirements.outputs.run_performance != 'true' && needs.smart-e2e-selection.outputs.ai_performance_test_tags || '' }}
performance_tags_reasoning: ${{ needs.get_requirements.outputs.run_performance == 'true' && 'Forced run via run-performance-tests label on PR' || needs.smart-e2e-selection.outputs.ai_performance_test_reasoning }}
build_variant: 'e2e'
branch_name: ${{ github.head_ref }}
source_ref: ${{ github.sha }}
pr_number: ${{ github.event.pull_request.number }}
# Test-only performance runs reuse main APKs when available, with a fresh-build
# fallback; app-changing PRs use normal fingerprint reuse with JS repacking.
reuse_main_builds: ${{ needs.get_requirements.outputs.use_main_builds_for_test_only_prs == 'true' }}
source_fingerprint: ${{ needs.native-build-fingerprint.outputs.fingerprint }}
secrets: inherit
permissions:
contents: write
id-token: write
actions: write
pull-requests: write
statuses: write
build-android-apks:
name: 'Build Android APKs'
if: >-
${{
!cancelled() &&
needs.get_requirements.outputs.android_e2e_needed == 'true' &&
!(fromJSON(needs.smart-e2e-selection.outputs.ai_confidence || '0') >= 85 && needs.smart-e2e-selection.outputs.ai_e2e_test_tags == '[]')
}}
permissions:
contents: read
id-token: write
actions: read
statuses: read
pull-requests: read
needs: [get_requirements, smart-e2e-selection, native-build-fingerprint]
uses: ./.github/workflows/build-android-e2e.yml
with:
build_type: 'main'
metamask_environment: 'e2e'
source-fingerprint: ${{ needs.native-build-fingerprint.outputs.fingerprint }}
runner_provider: >-
${{
inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider ||
vars.NAMESPACE_RUNNER_ANDROID || vars.NAMESPACE_RUNNER_PROVIDER || 'current'
}}
use-main-builds-for-test-only-prs: ${{ needs.get_requirements.outputs.use_main_builds_for_test_only_prs == 'true' }}
secrets: inherit
prepare-e2e-timings:
name: 'Prepare E2E Timings'
# Run in parallel with the build jobs — it is a lightweight API call (~30s).
# Fetches qa-stats timings once (merge-base preferred, latest-main fallback)
# and uploads them as a run-scoped artifact so every shard and every re-run
# uses the exact same frozen snapshot, keeping the bin-pack stable.
runs-on: ubuntu-latest
if: >-
${{
!cancelled() &&
(needs.get_requirements.outputs.android_e2e_needed == 'true' || needs.get_requirements.outputs.ios_e2e_needed == 'true') &&
!(fromJSON(needs.smart-e2e-selection.outputs.ai_confidence || '0') >= 85 && needs.smart-e2e-selection.outputs.ai_e2e_test_tags == '[]')
}}
needs: [get_requirements, smart-e2e-selection]
outputs:
available: ${{ steps.fetch.outputs.available }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- name: Fetch and freeze E2E timings
id: fetch
continue-on-error: true
run: node .github/scripts/qa-automation/stats/e2e-freeze-timings.mjs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
OUTPUT_PATH: ./e2e-timings.json
- name: Upload frozen timings artifact
# continue-on-error so the job does not fail if the artifact already
# exists for this run (i.e. on re-runs). First-write wins — the artifact
# uploaded by the earliest attempt is always the one shards will use.
if: ${{ steps.fetch.outputs.available == 'true' }}
continue-on-error: true
uses: actions/upload-artifact@v7
with:
name: e2e-timings-${{ github.run_id }}
path: ./e2e-timings.json
# Needed so re-runs of old PRs can still download the frozen snapshot
# and keep the same bin-pack, avoiding failed tests landing on a different
# shard that was not re-run, which would make them appear as passed.
# See: https://github.com/MetaMask/metamask-mobile/pull/31217
retention-days: 30
# Detox Android E2E smoke workflow removed — tags fully migrated to Appium
# (run-appium-smoke-tests-android.yml).
build-ios-apps:
name: 'Build iOS Apps'
# Build when the final iOS E2E platform flag is enabled.
if: >-
${{
!cancelled() &&
needs.get_requirements.outputs.ios_e2e_needed == 'true' &&
!(fromJSON(needs.smart-e2e-selection.outputs.ai_confidence || '0') >= 85 && needs.smart-e2e-selection.outputs.ai_e2e_test_tags == '[]')
}}
permissions:
contents: read
id-token: write
actions: read
statuses: read
pull-requests: read
needs: [get_requirements, smart-e2e-selection, native-build-fingerprint]
uses: ./.github/workflows/build-ios-e2e.yml
with:
source-fingerprint: ${{ needs.native-build-fingerprint.outputs.fingerprint }}
runner_provider: >-
${{
inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider ||
vars.NAMESPACE_RUNNER_IOS || vars.NAMESPACE_RUNNER_PROVIDER || 'current'
}}
use-main-builds-for-test-only-prs: ${{ needs.get_requirements.outputs.use_main_builds_for_test_only_prs == 'true' }}
secrets: inherit
appium-smoke-tests-android:
name: 'Appium Smoke Tests (Android)'
if: >-
${{
!cancelled() &&
needs.build-android-apks.result == 'success' &&
(needs.prepare-e2e-timings.result == 'success' ||
needs.prepare-e2e-timings.result == 'skipped' ||
needs.prepare-e2e-timings.result == 'failure')
}}
permissions:
contents: read
checks: write
id-token: write
# Nested smoke workflows need these; callers cannot inherit from this job alone.
# write: prime job persists golden AVD with actions/cache (nested cannot elevate).
actions: write
pull-requests: read
# Include prepare to avoid a timings race; failure/skipped still run (equal-count fallback).
needs: [build-android-apks, smart-e2e-selection, prepare-e2e-timings, get_requirements]
uses: ./.github/workflows/run-appium-smoke-tests-android.yml
with:
build_type: 'main'
metamask_environment: 'e2e'
# Mirrors build-android-apks' resolution chain so this consumer always matches
# the builder's artifact store (GitHub on current, Namespace store on namespace).
runner_provider: >-
${{
inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider ||
vars.NAMESPACE_RUNNER_ANDROID || vars.NAMESPACE_RUNNER_PROVIDER || 'current'
}}
selected_tags: >-
${{
(fromJSON(needs.smart-e2e-selection.outputs.ai_confidence || '0') >= 85 &&
needs.smart-e2e-selection.outputs.ai_e2e_test_tags) ||
'["ALL"]'
}}
changed_spec_files: ${{ needs.get_requirements.outputs.changed_spec_files }}
secrets: inherit
appium-smoke-tests-ios:
name: 'Appium Smoke Tests (iOS)'
# Runs when the final iOS E2E platform flag is enabled.
if: >-
${{
!cancelled() &&
needs.build-ios-apps.result == 'success' &&
(needs.prepare-e2e-timings.result == 'success' ||
needs.prepare-e2e-timings.result == 'skipped' ||
needs.prepare-e2e-timings.result == 'failure') &&
needs.get_requirements.outputs.ios_e2e_needed == 'true'
}}
permissions:
contents: read
checks: write
id-token: write
# Nested smoke workflows need these; callers cannot inherit from this job alone.
actions: read
pull-requests: read
# Include prepare to avoid a timings race; failure/skipped still run (equal-count fallback).
needs: [get_requirements, build-ios-apps, smart-e2e-selection, prepare-e2e-timings]
uses: ./.github/workflows/run-appium-smoke-tests-ios.yml
with:
build_type: 'main'
metamask_environment: 'e2e'
# Mirrors build-ios-apps' resolution chain so this consumer always matches
# the builder's artifact store (GitHub on current, Namespace store on namespace).
runner_provider: >-
${{
inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider ||
vars.NAMESPACE_RUNNER_IOS || vars.NAMESPACE_RUNNER_PROVIDER || 'current'
}}
selected_tags: >-
${{
(fromJSON(needs.smart-e2e-selection.outputs.ai_confidence || '0') >= 85 &&
needs.smart-e2e-selection.outputs.ai_e2e_test_tags) ||
'["ALL"]'
}}
changed_spec_files: ${{ needs.get_requirements.outputs.changed_spec_files }}
secrets: inherit
# Fixture validation — ensures committed E2E fixtures match the live app state schema
# Runs on Android only, so on PRs into main it runs only when Android was requested via
# run-appium-android-tests or skip-smart-e2e-selection. It still runs on PRs into
# release/* that build Android.
validate-e2e-fixtures:
name: 'Validate E2E Fixtures'
if: >-
${{
!cancelled() &&
needs.build-android-apks.result == 'success' &&
(needs.prepare-e2e-timings.result == 'success' ||
needs.prepare-e2e-timings.result == 'skipped' ||
needs.prepare-e2e-timings.result == 'failure')
}}
permissions:
contents: read
checks: write
id-token: write
# Nested run-appium-e2e-workflow requires these; missing grants → ci startup_failure.
actions: read
pull-requests: read
# Include prepare to avoid a timings race; failure/skipped still run (equal-count fallback).
needs: [build-android-apks, smart-e2e-selection, prepare-e2e-timings, get_requirements]
uses: ./.github/workflows/run-appium-e2e-workflow.yml
with:
test-suite-name: validate-e2e-fixtures
platform: android
test_suite_tag: FixtureValidation
split_number: 1
total_splits: 1
test-timeout-minutes: 25
build_type: 'main'
metamask_environment: 'e2e'
# Skip golden AVD cache restore; boot-mode cold keeps intent explicit.
cache-android-golden-snapshot: false
android-emulator-boot-mode: cold
# Mirrors build-android-apps' resolution chain so this consumer always matches
# the builder's artifact store (GitHub on current, Namespace store on namespace).
runner_provider: >-
${{
inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider ||
vars.NAMESPACE_RUNNER_ANDROID || vars.NAMESPACE_RUNNER_PROVIDER || 'current'
}}
secrets: inherit
report-fixture-validation:
name: 'Report Fixture Validation'
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux-small' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
if: ${{ !cancelled() && needs.validate-e2e-fixtures.result != 'skipped' }}
needs: [validate-e2e-fixtures]
permissions:
pull-requests: write
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
# run-appium-e2e-workflow uploads fixture validation results unconditionally via
# actions/upload-artifact (no Namespace-store arm), so results always land in the
# GitHub Actions artifact store — download via actions/* regardless of provider.
- name: Download fixture validation results
continue-on-error: true
uses: actions/download-artifact@v7
with:
name: fixture-validation-results-validate-e2e-fixtures
path: fixture-results/
- name: Report results
env:
RESULTS_PATH: fixture-results
VALIDATION_RESULT: ${{ needs.validate-e2e-fixtures.result }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: node .github/scripts/qa-automation/reporting/e2e-report-fixture-validation.mjs
sonar-cloud:
name: SonarCloud analysis
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
needs: merge-unit-and-component-view-tests
# Run when merged coverage is ready even if optional integration shards failed upstream.
if: ${{ always() && !cancelled() && needs.merge-unit-and-component-view-tests.result == 'success' && github.event_name != 'merge_group' && !github.event.pull_request.head.repo.fork }}
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
with:
fetch-depth: 0 # SonarCloud needs a full checkout to perform necessary analysis
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
with:
fetch-depth: 0 # SonarCloud needs a full checkout to perform necessary analysis
- name: Configure Namespace cache
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1
with:
path: |
~/.cache/yarn
.metamask
node_modules
.yarn/cache
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- name: Download lcov.info (Namespace)
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
uses: namespace-actions/download-artifact@7cbad919e4b0e09f17e9d6311a444ff002992b5b # v2.0.1
with:
name: lcov.info
path: coverage/
- name: Download lcov.info (current)
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
uses: actions/download-artifact@v7
with:
name: lcov.info
path: coverage/
- name: Upload coverage reports to Codecov
if: ${{ always() }}
continue-on-error: true
uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70
- name: SonarCloud Scan
if: ${{ env.HAVE_SONAR_TOKEN == 'true' }}
continue-on-error: true
# This is SonarSource/sonarqube-scan-action@v7.0.0
uses: SonarSource/sonarqube-scan-action@a31c9398be7ace6bbfaf30c0bd5d415f843d45e9
env:
HAVE_SONAR_TOKEN: ${{ secrets.SONAR_TOKEN != '' }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Require clean working directory
shell: bash
run: |
if ! git diff --exit-code; then
echo "Working tree dirty at end of job"
exit 1
else
echo "No changes detected"
fi
# Revert git update-index --no-assume-unchanged for each entry
echo "Reverting assume unchanged for the following paths:"
for path in "${EXCLUDES[@]}"; do
echo "$path"
git update-index --no-assume-unchanged "$path"
done
sonar-cloud-quality-gate-status:
name: SonarCloud quality gate status
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux-small' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
needs: sonar-cloud
if: ${{ always() && !cancelled() && needs.sonar-cloud.result == 'success' && github.event_name != 'merge_group' && !github.event.pull_request.head.repo.fork }}
permissions:
contents: read
issues: read
pull-requests: read
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
- name: Checkout code
uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
- name: SonarCloud Quality Gate Status
id: sonar-status
env:
REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
# Skip step if event is not a PR
if [[ "${{ github.event_name }}" != "pull_request" ]]; then
echo "This job only runs for pull requests."
exit 0
fi
if [ -z "$PR_NUMBER" ]; then
echo "No pull request number found. Failing the check."
exit 1
fi
# Bypass step if skip-sonar-cloud label is found. The `type == "array"`
# guard keeps a GitHub error payload (rate limit, 403) from aborting the
# job: an unreadable label list must not decide the quality gate.
LABELS=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/labels" | \
jq -r 'if type == "array" then .[].name else empty end' 2>/dev/null || true)
if grep -qx 'skip-sonar-cloud' <<< "$LABELS"; then
echo "skip-sonar-cloud label found. Skipping SonarCloud Quality Gate check."
exit 0
fi
PROJECT_KEY="MetaMask_metamask-mobile"
# Poll until SonarCloud has finished ingesting the analysis. OK is final,
# but ERROR is only trusted once it survives several polls: right after the
# scan the API still serves the previous analysis for this PR number, so an
# immediate ERROR is frequently stale and flips to OK within ~2 minutes.
MAX_ATTEMPTS=20
SLEEP_INTERVAL=15
ERROR_STREAK=0
ERROR_STREAK_TO_FAIL=8
for i in $(seq 1 $MAX_ATTEMPTS); do
RESPONSE=$(curl -s -u "$SONAR_TOKEN:" \
"https://sonarcloud.io/api/qualitygates/project_status?projectKey=$PROJECT_KEY&pullRequest=$PR_NUMBER")
echo "SonarCloud API Response (attempt $i/$MAX_ATTEMPTS): $RESPONSE"
STATUS=$(echo "$RESPONSE" | jq -r '.projectStatus.status // empty' 2>/dev/null || true)
if [[ "$STATUS" == "OK" ]]; then
echo "Quality Gate passed."
exit 0
elif [[ "$STATUS" == "ERROR" ]]; then
ERROR_STREAK=$((ERROR_STREAK + 1))
if [[ $ERROR_STREAK -ge $ERROR_STREAK_TO_FAIL ]]; then
echo "Quality Gate failed (ERROR stable across $ERROR_STREAK polls)."
exit 1
fi
echo "Got ERROR ($ERROR_STREAK/$ERROR_STREAK_TO_FAIL), it may be a previous analysis. Retrying in ${SLEEP_INTERVAL}s..."
else
ERROR_STREAK=0
echo "Quality Gate status not yet settled (got: '$STATUS'). Retrying in ${SLEEP_INTERVAL}s..."
fi
if [[ $i -lt $MAX_ATTEMPTS ]]; then
sleep "$SLEEP_INTERVAL"
fi
done
echo "Could not determine Quality Gate status after $MAX_ATTEMPTS attempts."
exit 1
# TEMP: delete the ci-js-deps artifact once all consumers have finished so it
# does not sit in GitHub artifact storage for the full retention-days: 1
# window. Every push re-creates this artifact from scratch, so there is never
# a reason to keep it after the run's consumers are done.
# Remove this job together with the upload/download steps once Namespace
# becomes the default runner.
cleanup-ci-js-deps:
name: Delete CI JS deps artifact
runs-on: ubuntu-latest
if: ${{ always() }}
needs:
- unit-tests
- component-view-tests
- integration-tests
- merge-unit-and-component-view-tests
permissions:
actions: write
steps:
- name: Delete CI JS deps artifact
uses: actions/github-script@v9
with:
script: |
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId,
});
const target = artifacts.data.artifacts.find(a => a.name === 'ci-js-deps');
if (target) {
await github.rest.actions.deleteArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: target.id,
});
core.info(`Deleted artifact ci-js-deps (id=${target.id})`);
} else {
core.info('Artifact ci-js-deps not found (already deleted or never created)');
}
check-all-jobs-pass:
name: Check all jobs pass
# Run the aggregate gate even when optional dependencies are skipped.
# The composite action decides which skipped jobs are acceptable.
if: ${{ always() && !cancelled() }}
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux-small' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
needs:
- get_requirements
- check-diff
- dedupe
- scripts
- unit-tests
- component-view-tests
- merge-unit-and-component-view-tests
- check-workflows
- prepare-ci-js-deps
- js-bundle-size-check
- sonar-cloud-quality-gate-status
- build-android-apks
- build-ios-apps
- appium-smoke-tests-android
- appium-smoke-tests-ios
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ env.RESOLVED_RUNNER_PROVIDER == 'namespace' }}
with:
fetch-depth: 1
sparse-checkout: |
.github/actions/ci-status-gate
- uses: actions/checkout@v6
if: ${{ env.RESOLVED_RUNNER_PROVIDER != 'namespace' }}
with:
fetch-depth: 1
sparse-checkout: |
.github/actions/ci-status-gate
- name: Evaluate CI status
uses: ./.github/actions/ci-status-gate
with:
needs-json: ${{ toJSON(needs) }}
requirement-context-json: ${{ toJSON(needs.get_requirements.outputs) }}
e2e-job-regex: '^(build-android-apks|build-ios-apps|appium-smoke-tests-android|appium-smoke-tests-ios)$'
event-name: ${{ github.event_name }}
is-fork: ${{ github.event.pull_request.head.repo.fork == true }}
log-merge-group-failure:
name: Log merge group failure
runs-on: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current') == 'namespace' &&
'namespace-profile-metamask-ci-linux-small' || 'ubuntu-latest'
}}
env:
# Effective provider for this job. Steps must test this, never inputs.runner_provider
# directly, or a provider set through the repo variables would be ignored.
RESOLVED_RUNNER_PROVIDER: >-
${{
(inputs.runner_provider != '' && inputs.runner_provider != 'inherit' && inputs.runner_provider || vars.NAMESPACE_RUNNER_LINUX || vars.NAMESPACE_RUNNER_PROVIDER || 'current')
}}
# Only run this job if the merge group event fails, skip on forks
if: ${{ github.event_name == 'merge_group' && failure() }}
needs:
- check-all-jobs-pass
steps:
- name: Log merge group failure to Google Sheets
uses: MetaMask/github-tools/.github/actions/log-merge-group-failure@v1
with:
google-application-credentials: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
google-service-account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }}
spreadsheet-id: ${{ secrets.GOOGLE_MERGE_QUEUE_SPREADSHEET_ID }}
sheet-name: ${{ secrets.GOOGLE_MERGE_QUEUE_SHEET_NAME }}