feat: add share button and share sheet to Token Details page #152184
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| 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: | |
| # Use the default ref (refs/pull/N/merge on pull_request, github.sha otherwise) so the | |
| # fingerprint is computed against the SAME working tree that `build-android-e2e.yml`, | |
| # `build-ios-e2e.yml`, and `run-e2e-workflow.yml` check out and build/test against. | |
| # Posting on `pull_request.head.sha` while fingerprinting the PR head tree would let | |
| # two PRs with identical heads but different merges collide on the cache key — and the | |
| # native build then runs against a different (merge) tree than the one we fingerprinted. | |
| - uses: actions/checkout@v6 | |
| - 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' }} | |
| # 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' }} | |
| 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' }} | |
| - 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' }} | |
| 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' }} | |
| - 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' }} | |
| if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }} | |
| needs: | |
| - get_requirements | |
| strategy: | |
| matrix: | |
| scripts: | |
| - lint | |
| - lint:tsc | |
| - 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 | |
| - 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' }} | |
| 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: ${{ 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 --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 is the latest successful ci/ios-js-bundle-size status on | |
| # github.event.pull_request.base.sha (GitHub's PR base commit for this run), not git merge-base(head, base). | |
| 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_SHA='${{ github.event.pull_request.base.sha }}' | |
| REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" | |
| if [[ -n "$BASE_SHA" ]]; then | |
| echo "Resolving baseline bundle size from ci/ios-js-bundle-size status on base commit ${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/${BASE_SHA}/status" | |
| )"; then | |
| echo "::warning::Failed to fetch commit statuses for base ${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/${BASE_SHA}) for PR base \`${BASE_SHA}\`. | |
| | | MiB | | |
| |---|--:| | |
| | Current bundle | ${CURRENT_MB} | | |
| Baseline would be read from the latest successful \`ci/ios-js-bundle-size\` status description on that base 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 base ref status: ${BASELINE_MB} MB" | |
| else | |
| echo "::warning::No successful ci/ios-js-bundle-size status (or unrecognized description) on base ${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 PR base [\`${BASE_SHA}\`](${REPO_URL}/commit/${BASE_SHA}). | |
| | | MiB | | |
| |---|--:| | |
| | Current bundle | ${CURRENT_MB} | | |
| | Raw description | \`${DESC:-<empty>}\` | | |
| EOF | |
| exit 0 | |
| fi | |
| else | |
| echo "::warning::Pull request base SHA is empty; skipping iOS JS bundle delta check." | |
| append_job_summary <<EOF | |
| ### iOS JS bundle size (PR) | |
| **Result:** skipped — \`github.event.pull_request.base.sha\` was empty. | |
| | | MiB | | |
| |---|--:| | |
| | Current bundle | ${CURRENT_MB} | | |
| 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 PR base [\`${BASE_SHA}\`](${REPO_URL}/commit/${BASE_SHA}). | |
| | | MiB | | |
| |---|--:| | |
| | PR base commit | — | | |
| | 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 | | |
| |---|--:| | |
| | PR base commit | [\`${BASE_SHA}\`](${REPO_URL}/commit/${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. Baseline is **not** from \`git merge-base\`; it is the value posted to the PR base commit by CI. | |
| EOF | |
| - name: Post iOS JS bundle size to commit status | |
| if: ${{ github.ref == 'refs/heads/main' && inputs.runner_provider != 'namespace' }} | |
| 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 Namespace shadow runs: hourly 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' }} | |
| 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 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: ${{ secrets.MOBILE_BUNDLESIZE_TOKEN }} | |
| check-workflows: | |
| name: Check workflows | |
| runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-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' }} | |
| - 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' }} | |
| if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }} | |
| 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' }} | |
| 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: ${{ 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; GitHub-hosted keeps 20480. | |
| - run: yarn test:unit --shard=${{ matrix.shard }}/10${{ inputs.runner_provider == 'namespace' && ' --maxWorkers=33%' || '' }} --forceExit --silent --coverageReporters=json --json --outputFile=tests/results/unit-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-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 both unit and component view 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] | |
| if: ${{ github.event_name != 'merge_group' }} | |
| 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 | |
| shell: bash | |
| run: | | |
| 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" | |
| 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-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 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 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' }} | |
| 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: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 | |
| 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' }} | |
| 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. | |
| if: >- | |
| ${{ | |
| 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 }} | |
| secrets: inherit | |
| permissions: | |
| contents: write | |
| id-token: write | |
| actions: write | |
| pull-requests: 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 }} | |
| 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' | |
| if: >- | |
| ${{ | |
| !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 }} | |
| 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)' | |
| if: >- | |
| ${{ | |
| !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)' | |
| if: >- | |
| ${{ | |
| !cancelled() && | |
| github.ref == 'refs/heads/main' && | |
| (github.event_name == 'push' || github.event_name == 'schedule') && | |
| needs.ios-tests-ready.result == 'success' | |
| }} | |
| permissions: | |
| contents: read | |
| checks: write | |
| id-token: write | |
| needs: [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' }} | |
| 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: ${{ 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' }} | |
| 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 | |
| if: ${{ 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 | |
| if: ${{ 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 | |
| if: ${{ always() && inputs.runner_provider != 'namespace' }} | |
| needs: | |
| - unit-tests | |
| - component-view-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. | |
| if: ${{ always() && !cancelled() }} | |
| 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 | |
| - 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 | |
| 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 }} |