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