ci [shadow PR #30163 @ 92e47fde6b2fb87c7fc203a207246a16adbb84f6] #137635
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' | |
| run: 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 | |
| post-build-source-hash: | |
| name: Post build-source-hash commit status | |
| 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: Post build-source-hash commit status | |
| if: ${{ inputs.runner_provider != 'namespace' }} | |
| id: publish | |
| uses: ./.github/actions/post-build-source-hash | |
| with: | |
| github-token: ${{ github.token }} | |
| # 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 | |
| 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 | |
| if: ${{ inputs.runner_provider != 'namespace' && needs.prepare-ci-js-deps.result == 'success' }} | |
| 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' }} | |
| 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: ${{ !cancelled() && 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 | |
| if: ${{ inputs.runner_provider != 'namespace' && needs.prepare-ci-js-deps.result == 'success' }} | |
| 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' }} | |
| 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 | |
| if: ${{ inputs.runner_provider != 'namespace' && needs.prepare-ci-js-deps.result == 'success' }} | |
| 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' }} | |
| 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 }} | |
| 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 }} | |
| 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') >= 80 && 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, post-build-source-hash] | |
| uses: ./.github/workflows/build-android-e2e.yml | |
| with: | |
| build_type: 'main' | |
| metamask_environment: 'e2e' | |
| source-fingerprint: ${{ needs.post-build-source-hash.outputs.fingerprint }} | |
| runner_provider: ${{ inputs.runner_provider }} | |
| secrets: inherit | |
| e2e-smoke-tests-android: | |
| name: 'Android E2E Smoke Tests' | |
| if: ${{ !cancelled() && needs.build-android-apks.result == 'success' }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| needs: [get_requirements, build-android-apks, smart-e2e-selection] | |
| 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') >= 80 && 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') >= 80 && 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, post-build-source-hash] | |
| uses: ./.github/workflows/build-ios-e2e.yml | |
| with: | |
| source-fingerprint: ${{ needs.post-build-source-hash.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' }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| needs: [get_requirements, ios-tests-ready, smart-e2e-selection] | |
| 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') >= 80 && needs.smart-e2e-selection.outputs.ai_e2e_test_tags) || | |
| '["ALL"]' | |
| }} | |
| runner_provider: ${{ inputs.runner_provider }} | |
| 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: ${{ !cancelled() && 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: ${{ !cancelled() && 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 | |
| sleep 30 | |
| PROJECT_KEY="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 | |
| RESPONSE=$(curl -s -u "$SONAR_TOKEN:" \ | |
| "https://sonarcloud.io/api/qualitygates/project_status?projectKey=$PROJECT_KEY&pullRequest=$PR_NUMBER") | |
| echo "SonarCloud API Response: $RESPONSE" | |
| STATUS=$(echo "$RESPONSE" | jq -r '.projectStatus.status') | |
| if [[ "$STATUS" == "ERROR" ]]; then | |
| echo "Quality Gate failed." | |
| exit 1 | |
| elif [[ "$STATUS" == "OK" ]]; then | |
| echo "Quality Gate passed." | |
| else | |
| echo "Could not determine Quality Gate status." | |
| exit 1 | |
| fi | |
| fi | |
| 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 | |
| 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)$' | |
| 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 }} |