Skip to content

poc: libsodium

poc: libsodium #161277

Workflow file for this run

name: ci
run-name: >-
${{ inputs.pr_number && format('ci [shadow PR #{0} @ {1}]', inputs.pr_number, inputs.head_sha) || '' }}
on:
push:
branches: [main]
pull_request:
types:
- opened
- reopened
- synchronize
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:
type: string
required: false
default: current
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 for this manual trial run
required: true
type: choice
options:
- current
- namespace
- bitrise
default: current
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:
group: ${{ github.workflow }}-${{ 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
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ios-build' || 'macos-latest' }}
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }}
needs:
- get_requirements
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ inputs.runner_provider == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ inputs.runner_provider != 'namespace' }}
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: yarn
- uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb #v1
with:
ruby-version: '3.2.9'
env:
BUNDLE_GEMFILE: ios/Gemfile
- 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
uses: actions/cache@v4
with:
path: .metamask
key: .metamask-${{ hashFiles('package.json', 'yarn.lock') }}
- name: Install Foundry if cache missed
if: 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' }}
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.native_build_needed != 'false' || 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.native_build_needed == 'false' && 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: ${{ inputs.runner_provider != 'namespace' && inputs.runner_provider != 'bitrise' && needs.get_requirements.outputs.native_build_needed == '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 == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' && inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' }}
needs:
- get_requirements
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ inputs.runner_provider == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ inputs.runner_provider != 'namespace' }}
- name: Configure Namespace cache
if: ${{ inputs.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: ${{ inputs.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 == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' && inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' }}
needs:
- get_requirements
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ inputs.runner_provider == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ inputs.runner_provider != 'namespace' }}
- name: Configure Namespace cache
if: ${{ inputs.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: ${{ inputs.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 == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' && inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' }}
needs:
- get_requirements
strategy:
matrix:
scripts:
- lint
- lint:tsc
- lint:changelog
- format:check
- audit:ci
- test:depcheck
- test:tgz-check
- constraints
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ inputs.runner_provider == 'namespace' }}
with:
fetch-depth: 2
- uses: actions/checkout@v6
if: ${{ inputs.runner_provider != 'namespace' }}
with:
fetch-depth: 2
- name: Configure Namespace cache
if: ${{ inputs.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: ${{ inputs.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: Restore ESLint cache
if: ${{ matrix.scripts == 'lint' }}
uses: actions/cache@v4
with:
path: .eslintcache
key: eslintcache-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
eslintcache-${{ runner.os }}-
- name: Restore TypeScript incremental build cache
if: ${{ matrix.scripts == 'lint:tsc' }}
uses: actions/cache@v4
with:
path: tsconfig.tsbuildinfo
key: tsbuildinfo-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
tsbuildinfo-${{ runner.os }}-
- 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
js-bundle-size-check:
name: JS bundle size check
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' && inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' }}
needs:
- get_requirements
permissions:
contents: read
statuses: write
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ inputs.runner_provider == 'namespace' }}
with:
fetch-depth: 2
- uses: actions/checkout@v6
if: ${{ inputs.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 }}'
git fetch origin "${BASE_REF}" --depth=1
if ! MERGE_BASE_SHA="$(git merge-base "origin/${BASE_REF}" HEAD)"; then
echo "::warning::Could not resolve merge-base between HEAD and origin/${BASE_REF}; bundle size delta check will be skipped."
echo "MERGE_BASE_SHA=" >> "$GITHUB_ENV"
else
echo "Merge-base pinned (origin/${BASE_REF}, HEAD): ${MERGE_BASE_SHA}"
echo "MERGE_BASE_SHA=${MERGE_BASE_SHA}" >> "$GITHUB_ENV"
fi
echo "BUNDLE_SIZE_BASE_REF=${BASE_REF}" >> "$GITHUB_ENV"
- name: Configure Namespace cache
if: ${{ inputs.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: ${{ inputs.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:
NODE_OPTIONS: --max_old_space_size=12288
- 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
DELTA_MB="$(echo "${CURRENT_MB} - ${BASELINE_MB}" | bc -l)"
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 (( $(echo "${DELTA_MB} > 1" | bc -l) )); 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' && inputs.runner_provider != 'namespace' && inputs.runner_provider != 'bitrise' }}
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 (Namespace)
if: ${{ inputs.runner_provider == 'namespace' }}
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
with:
name: ios-bundle
path: ios/main.jsbundle
retention-days: 7
- name: Upload iOS bundle (current)
if: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/upload-artifact@v4
with:
name: ios-bundle
path: ios/main.jsbundle
retention-days: 7
ship-js-bundle-size-check:
name: Ship JS bundle size check
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
needs: [js-bundle-size-check]
# Skip on shadow runs (Namespace/Bitrise): cron against main would otherwise
# push duplicate entries to the external mobile_bundlesize_stats repo.
if: ${{ github.ref == 'refs/heads/main' && inputs.runner_provider != 'namespace' && inputs.runner_provider != 'bitrise' }}
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: ${{ inputs.runner_provider == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ inputs.runner_provider != 'namespace' }}
- name: Download iOS bundle (Namespace)
if: ${{ inputs.runner_provider == 'namespace' }}
uses: namespace-actions/download-artifact@7cbad919e4b0e09f17e9d6311a444ff002992b5b # v2.0.1
with:
name: ios-bundle
path: ios
- name: Download iOS bundle (current)
if: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/download-artifact@v4
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 == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' && inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' }}
needs:
- get_requirements
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ inputs.runner_provider == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ inputs.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
# 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 == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: ${{ needs.get_requirements.outputs.skip_everything != 'true' && inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' }}
needs:
- get_requirements
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-ci-js-deps
with:
runner_provider: ${{ inputs.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: ${{ inputs.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: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/upload-artifact@v4
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 == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: ${{ !cancelled() && needs.get_requirements.result == 'success' && needs.get_requirements.outputs.skip_everything != 'true' && inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' }}
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: ${{ inputs.runner_provider == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ inputs.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: ${{ inputs.runner_provider != 'namespace' && needs.prepare-ci-js-deps.result == 'success' }}
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: ci-js-deps
path: .
- name: Extract CI JS deps
if: ${{ inputs.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: ${{ inputs.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${{ inputs.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
- name: Upload coverage unit shard (Namespace)
if: ${{ inputs.runner_provider == 'namespace' }}
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
with:
name: coverage-unit-${{ matrix.shard }}
path: ./tests/coverage/
if-no-files-found: error
retention-days: 7
- name: Upload coverage unit shard (current)
if: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/upload-artifact@v4
with:
name: coverage-unit-${{ matrix.shard }}
path: ./tests/coverage/
if-no-files-found: error
retention-days: 7
- 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 == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
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' && inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' && !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: ${{ inputs.runner_provider == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ inputs.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: ${{ inputs.runner_provider != 'namespace' && needs.prepare-ci-js-deps.result == 'success' }}
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: ci-js-deps
path: .
- name: Extract CI JS deps
if: ${{ inputs.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: ${{ inputs.runner_provider }}
- name: Download coverage shards (Namespace)
if: ${{ inputs.runner_provider == 'namespace' }}
uses: namespace-actions/download-artifact@7cbad919e4b0e09f17e9d6311a444ff002992b5b # v2.0.1
with:
pattern: coverage-*
path: tests/coverage/
- name: Download coverage shards (current)
if: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/download-artifact@v4
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 (Namespace)
if: ${{ inputs.runner_provider == 'namespace' }}
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
with:
name: lcov.info
path: ./tests/merged-coverage/lcov.info
if-no-files-found: error
retention-days: 7
- name: Upload lcov.info (current)
if: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/upload-artifact@v4
with:
name: lcov.info
path: ./tests/merged-coverage/lcov.info
if-no-files-found: error
retention-days: 7
- name: Upload cv-test-stats (Namespace)
if: ${{ inputs.runner_provider == 'namespace' }}
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
with:
name: cv-test-stats
path: ./cv-test-stats.json
if-no-files-found: error
retention-days: 7
- name: Upload cv-test-stats (current)
if: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/upload-artifact@v4
with:
name: cv-test-stats
path: ./cv-test-stats.json
if-no-files-found: error
retention-days: 7
- name: Upload integration-test-stats (Namespace)
if: ${{ inputs.runner_provider == 'namespace' }}
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
with:
name: integration-test-stats
path: ./integration-test-stats.json
if-no-files-found: error
retention-days: 7
- name: Upload integration-test-stats (current)
if: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/upload-artifact@v4
with:
name: integration-test-stats
path: ./integration-test-stats.json
if-no-files-found: error
retention-days: 7
- name: Upload unit-test-stats (Namespace)
if: ${{ inputs.runner_provider == 'namespace' }}
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
with:
name: unit-test-stats
path: ./unit-test-stats.json
if-no-files-found: error
retention-days: 7
- name: Upload unit-test-stats (current)
if: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/upload-artifact@v4
with:
name: unit-test-stats
path: ./unit-test-stats.json
if-no-files-found: error
retention-days: 7
- 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 (Namespace)
if: ${{ inputs.runner_provider == 'namespace' }}
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
with:
name: cv-test-coverage-html
path: ./tests/coverage-cv-lcov/
if-no-files-found: error
retention-days: 7
- name: Upload cv-test-coverage-html (current)
if: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/upload-artifact@v4
with:
name: cv-test-coverage-html
path: ./tests/coverage-cv-lcov/
if-no-files-found: error
retention-days: 7
- name: Upload cv-test-coverage-summary (Namespace)
if: ${{ inputs.runner_provider == 'namespace' }}
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
with:
name: cv-test-coverage-summary
path: ./tests/coverage-cv-lcov/coverage-summary.json
if-no-files-found: error
retention-days: 7
- name: Upload cv-test-coverage-summary (current)
if: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/upload-artifact@v4
with:
name: cv-test-coverage-summary
path: ./tests/coverage-cv-lcov/coverage-summary.json
if-no-files-found: error
retention-days: 7
- 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 (Namespace)
if: ${{ inputs.runner_provider == 'namespace' && steps.aggregate-coverage.outputs.has_integration_coverage == 'true' }}
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
with:
name: integration-test-coverage-html
path: ./tests/coverage-integration-lcov/
if-no-files-found: error
retention-days: 7
- name: Upload integration-test-coverage-html (current)
if: ${{ inputs.runner_provider != 'namespace' && steps.aggregate-coverage.outputs.has_integration_coverage == 'true' }}
uses: actions/upload-artifact@v4
with:
name: integration-test-coverage-html
path: ./tests/coverage-integration-lcov/
if-no-files-found: error
retention-days: 7
- name: Upload integration-test-coverage-summary (Namespace)
if: ${{ inputs.runner_provider == 'namespace' && steps.aggregate-coverage.outputs.has_integration_coverage == 'true' }}
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
with:
name: integration-test-coverage-summary
path: ./tests/coverage-integration-lcov/coverage-summary.json
if-no-files-found: error
retention-days: 7
- name: Upload integration-test-coverage-summary (current)
if: ${{ inputs.runner_provider != 'namespace' && steps.aggregate-coverage.outputs.has_integration_coverage == 'true' }}
uses: actions/upload-artifact@v4
with:
name: integration-test-coverage-summary
path: ./tests/coverage-integration-lcov/coverage-summary.json
if-no-files-found: error
retention-days: 7
- 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 (Namespace)
if: ${{ inputs.runner_provider == 'namespace' }}
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
with:
name: unit-test-coverage-summary
path: ./tests/coverage-unit-lcov/coverage-summary.json
if-no-files-found: error
retention-days: 7
- name: Upload unit-test-coverage-summary (current)
if: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/upload-artifact@v4
with:
name: unit-test-coverage-summary
path: ./tests/coverage-unit-lcov/coverage-summary.json
if-no-files-found: error
retention-days: 7
- 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
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: ${{ !cancelled() && needs.get_requirements.result == 'success' && needs.get_requirements.outputs.skip_everything != 'true' && inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' }}
needs:
- get_requirements
- prepare-ci-js-deps
strategy:
matrix:
shard: [1, 2]
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ inputs.runner_provider == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ inputs.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: ${{ inputs.runner_provider != 'namespace' && needs.prepare-ci-js-deps.result == 'success' }}
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: ci-js-deps
path: .
- name: Extract CI JS deps
if: ${{ inputs.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: ${{ inputs.runner_provider }}
- name: Prepare results directory
run: mkdir -p tests/results
- run: |
yarn test:view:ci \
--shard=${{ matrix.shard }}/2 \
--json \
--outputFile=tests/results/cv-test-results-${{ matrix.shard }}.json
env:
NODE_OPTIONS: ${{ inputs.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 (Namespace)
if: ${{ inputs.runner_provider == 'namespace' }}
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
with:
name: coverage-cv-${{ matrix.shard }}
path: ./tests/coverage/
if-no-files-found: error
retention-days: 7
- name: Upload coverage CV shard (current)
if: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/upload-artifact@v4
with:
name: coverage-cv-${{ matrix.shard }}
path: ./tests/coverage/
if-no-files-found: error
retention-days: 7
# 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:
name: Integration tests
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
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]
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ inputs.runner_provider == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ inputs.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: ${{ inputs.runner_provider != 'namespace' && needs.prepare-ci-js-deps.result == 'success' }}
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: ci-js-deps
path: .
- name: Extract CI JS deps
if: ${{ inputs.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: ${{ inputs.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: ${{ inputs.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-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 (Namespace)
if: ${{ inputs.runner_provider == 'namespace' }}
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
with:
name: coverage-integration-${{ matrix.shard }}
path: ./tests/coverage/
if-no-files-found: error
retention-days: 7
- name: Upload coverage integration shard (current)
if: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/upload-artifact@v4
with:
name: coverage-integration-${{ matrix.shard }}
path: ./tests/coverage/
if-no-files-found: error
retention-days: 7
smart-e2e-selection:
name: 'Smart E2E Selection'
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
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: ${{ inputs.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: ${{ inputs.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: ${{ inputs.runner_provider != 'namespace' && inputs.runner_provider != 'bitrise' }}
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
# - smart-e2e-selection ran and AI selected performance tags.
# 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.
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: >-
${{
inputs.runner_provider != 'bitrise' &&
inputs.runner_provider != 'namespace' &&
github.event_name == 'pull_request' &&
!cancelled() &&
(
needs.get_requirements.outputs.run_performance == 'true' ||
(
needs.smart-e2e-selection.result == 'success' &&
needs.smart-e2e-selection.outputs.ai_performance_test_tags != '[]'
)
)
}}
needs: [get_requirements, smart-e2e-selection]
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 }}
pr_number: ${{ github.event.pull_request.number }}
reuse_main_builds: ${{ needs.get_requirements.outputs.native_build_needed == 'false' }}
secrets: inherit
permissions:
contents: write
id-token: write
actions: write
pull-requests: write
build-android-apks:
name: 'Build Android APKs'
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: >-
${{
inputs.runner_provider != 'bitrise' &&
inputs.runner_provider != 'namespace' &&
!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 }}
reuse-main-builds-only: ${{ needs.get_requirements.outputs.native_build_needed == 'false' }}
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/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@v4
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
e2e-smoke-tests-android:
name: 'Android E2E Smoke Tests'
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: >-
${{
inputs.runner_provider != 'bitrise' &&
inputs.runner_provider != 'namespace' &&
!cancelled() &&
needs.build-android-apks.result == 'success' &&
(needs.prepare-e2e-timings.result == 'success' || needs.prepare-e2e-timings.result == 'failure' || needs.prepare-e2e-timings.result == 'skipped')
}}
permissions:
contents: read
id-token: write
needs:
[
get_requirements,
build-android-apks,
smart-e2e-selection,
prepare-e2e-timings,
]
uses: ./.github/workflows/run-e2e-smoke-tests-android.yml
with:
changed_files: ${{ needs.get_requirements.outputs.changed_files }}
selected_tags: >-
${{
(fromJSON(needs.smart-e2e-selection.outputs.ai_confidence || '0') >= 85 && needs.smart-e2e-selection.outputs.ai_e2e_test_tags) ||
'["ALL"]'
}}
runner_provider: ${{ inputs.runner_provider }}
secrets: inherit
build-ios-apps:
name: 'Build iOS Apps'
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 }}
reuse-main-builds-only: ${{ needs.get_requirements.outputs.native_build_needed == 'false' }}
secrets: inherit
ios-tests-ready:
name: 'iOS Tests Ready'
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
if: ${{ !cancelled() && needs.build-ios-apps.result == 'success' }}
needs: [build-ios-apps]
steps:
- name: iOS build complete
run: echo "Dummy step to better visualize the Android and iOS E2E tests in Github workflow graph"
e2e-smoke-tests-ios:
name: 'iOS E2E Smoke Tests'
if: >-
${{
!cancelled() &&
needs.ios-tests-ready.result == 'success' &&
(needs.prepare-e2e-timings.result == 'success' || needs.prepare-e2e-timings.result == 'failure' || needs.prepare-e2e-timings.result == 'skipped')
}}
permissions:
contents: read
id-token: write
needs:
[
get_requirements,
ios-tests-ready,
smart-e2e-selection,
prepare-e2e-timings,
]
uses: ./.github/workflows/run-e2e-smoke-tests-ios.yml
with:
changed_files: ${{ needs.get_requirements.outputs.changed_files }}
selected_tags: >-
${{
(fromJSON(needs.smart-e2e-selection.outputs.ai_confidence || '0') >= 85 && needs.smart-e2e-selection.outputs.ai_e2e_test_tags) ||
'["ALL"]'
}}
runner_provider: ${{ inputs.runner_provider }}
secrets: inherit
appium-smoke-tests-android:
name: 'Appium Smoke Tests (Android)'
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: >-
${{
inputs.runner_provider != 'bitrise' &&
inputs.runner_provider != 'namespace' &&
!cancelled() &&
needs.build-android-apks.result == 'success'
}}
permissions:
contents: read
checks: write
id-token: write
needs: [build-android-apks, smart-e2e-selection]
uses: ./.github/workflows/run-appium-smoke-tests-android.yml
with:
build_type: 'main'
metamask_environment: 'e2e'
# Appium Android on Cirrus/GitHub-hosted runners until Namespace parity is ready.
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"]'
}}
secrets: inherit
appium-smoke-tests-ios:
name: 'Appium Smoke Tests (iOS)'
# Main push/schedule: always run when the iOS build succeeded.
# PRs: skipped by default; add run-appium-ios-tests, or change page-objects/selectors/locators/framework
# (same smart-selected tags as other E2E jobs).
if: >-
${{
!cancelled() &&
needs.ios-tests-ready.result == 'success' &&
(
(
github.ref == 'refs/heads/main' &&
(github.event_name == 'push' || github.event_name == 'schedule')
) ||
(
github.event_name == 'pull_request' &&
needs.get_requirements.outputs.run_appium_ios == 'true'
)
)
}}
permissions:
contents: read
checks: write
id-token: write
needs: [get_requirements, ios-tests-ready, smart-e2e-selection]
uses: ./.github/workflows/run-appium-smoke-tests-ios.yml
with:
build_type: 'main'
metamask_environment: 'e2e'
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"]'
}}
secrets: inherit
# Fixture validation — ensures committed E2E fixtures match the live app state schema
validate-e2e-fixtures:
name: 'Validate E2E Fixtures'
if: ${{ needs.ios-tests-ready.result == 'success' && github.event_name == 'pull_request' }}
permissions:
contents: read
id-token: write
needs: [get_requirements, ios-tests-ready, smart-e2e-selection]
uses: ./.github/workflows/run-e2e-workflow.yml
with:
test-suite-name: validate-e2e-fixtures
platform: ios
test_suite_tag: 'FixtureValidation'
split_number: 1
total_splits: 1
build_type: 'main'
runner_provider: ${{ inputs.runner_provider }}
secrets: inherit
report-fixture-validation:
name: 'Report Fixture Validation'
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: ${{ inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' && !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: ${{ inputs.runner_provider == 'namespace' }}
- uses: actions/checkout@v6
if: ${{ inputs.runner_provider != 'namespace' }}
- name: Download fixture validation results (Namespace)
if: ${{ inputs.runner_provider == 'namespace' }}
continue-on-error: true
uses: namespace-actions/download-artifact@7cbad919e4b0e09f17e9d6311a444ff002992b5b # v2.0.1
with:
name: test-e2e-main-validate-e2e-fixtures-junit-results
path: fixture-results/
- name: Download fixture validation results (current)
if: ${{ inputs.runner_provider != 'namespace' }}
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: test-e2e-main-validate-e2e-fixtures-junit-results
path: fixture-results/
- name: Report results
if: ${{ inputs.runner_provider != 'namespace' && inputs.runner_provider != 'bitrise' }}
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/e2e-report-fixture-validation.mjs
sonar-cloud:
name: SonarCloud analysis
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
needs: merge-unit-and-component-view-tests
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
# 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' && inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' && github.event_name != 'merge_group' && !github.event.pull_request.head.repo.fork }}
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ inputs.runner_provider == 'namespace' }}
with:
fetch-depth: 0 # SonarCloud needs a full checkout to perform necessary analysis
- uses: actions/checkout@v6
if: ${{ inputs.runner_provider != 'namespace' }}
with:
fetch-depth: 0 # SonarCloud needs a full checkout to perform necessary analysis
- name: Configure Namespace cache
if: ${{ inputs.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: ${{ inputs.runner_provider == 'namespace' }}
uses: namespace-actions/download-artifact@7cbad919e4b0e09f17e9d6311a444ff002992b5b # v2.0.1
with:
name: lcov.info
path: coverage/
- name: Download lcov.info (current)
if: ${{ inputs.runner_provider != 'namespace' }}
uses: actions/download-artifact@v4
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 == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
needs: sonar-cloud
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: ${{ always() && !cancelled() && needs.sonar-cloud.result == 'success' && inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' && github.event_name != 'merge_group' && !github.event.pull_request.head.repo.fork }}
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ inputs.runner_provider == 'namespace' }}
- name: Checkout code
uses: actions/checkout@v6
if: ${{ inputs.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 }}
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
# Bypass step if skip-sonar-cloud label is found
LABEL=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/labels" | \
jq -r '.[] | select(.name=="skip-sonar-cloud") | .name')
if [[ "$LABEL" == "skip-sonar-cloud" ]]; then
echo "skip-sonar-cloud label found. Skipping SonarCloud Quality Gate check."
else
PROJECT_KEY="MetaMask_metamask-mobile"
PR_NUMBER="${{ github.event.pull_request.number }}"
SONAR_TOKEN="${{ secrets.SONAR_TOKEN }}"
if [ -z "$PR_NUMBER" ]; then
echo "No pull request number found. Failing the check."
exit 1
fi
# Poll until SonarCloud has finished ingesting the analysis (status OK or ERROR),
# retrying on NONE/empty which means ingestion is still in progress.
# Retries up to 12 times, 15s apart (~2m45s total), then fails the check on timeout.
MAX_ATTEMPTS=12
SLEEP_INTERVAL=15
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')
if [[ "$STATUS" == "OK" ]]; then
echo "Quality Gate passed."
exit 0
elif [[ "$STATUS" == "ERROR" ]]; then
echo "Quality Gate failed."
exit 1
fi
echo "Quality Gate status not yet settled (got: '$STATUS'). Retrying in ${SLEEP_INTERVAL}s..."
if [[ $i -lt $MAX_ATTEMPTS ]]; then
sleep "$SLEEP_INTERVAL"
fi
done
echo "Could not determine Quality Gate status after $MAX_ATTEMPTS attempts."
exit 1
fi
# 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
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: ${{ always() && inputs.runner_provider != 'namespace' && inputs.runner_provider != 'bitrise' }}
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@v7
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.
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: ${{ always() && !cancelled() && inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' }}
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
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
- e2e-smoke-tests-android
- e2e-smoke-tests-ios
- appium-smoke-tests-android
- appium-smoke-tests-ios
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
if: ${{ inputs.runner_provider == 'namespace' }}
with:
fetch-depth: 1
sparse-checkout: |
.github/actions/ci-status-gate
- uses: actions/checkout@v6
if: ${{ inputs.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|e2e-smoke-tests-android|e2e-smoke-tests-ios|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 == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
# Only run this job if the merge group event fails, skip on forks
# BITRISE/NAMESPACE-SHADOW: skip for iOS-only shadow benchmark (INFRA-3679) — revert to enable full pipeline
if: ${{ inputs.runner_provider != 'bitrise' && inputs.runner_provider != 'namespace' && 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 }}