feat(rewards): add Perps Trading campaign participant outcome #134116
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: ci | |
| on: | |
| push: | |
| branches: [main] | |
| 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 * * *' | |
| 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: macos-latest | |
| if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }} | |
| needs: | |
| - get_requirements | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - 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: 10 | |
| 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 | |
| dedupe: | |
| name: Dedupe | |
| runs-on: ubuntu-latest | |
| if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }} | |
| needs: | |
| - get_requirements | |
| steps: | |
| - 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: 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: ubuntu-latest | |
| if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }} | |
| needs: | |
| - get_requirements | |
| steps: | |
| - 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: 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: 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: actions/checkout@v6 | |
| with: | |
| fetch-depth: 2 | |
| - 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: 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: ubuntu-latest | |
| if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }} | |
| needs: | |
| - get_requirements | |
| permissions: | |
| contents: read | |
| statuses: write | |
| steps: | |
| - 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: 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' }} | |
| 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 | |
| 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: ubuntu-latest | |
| needs: [js-bundle-size-check] | |
| if: ${{ github.ref == 'refs/heads/main' }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Download iOS bundle | |
| 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: ubuntu-latest | |
| if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }} | |
| needs: | |
| - get_requirements | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - 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 | |
| unit-tests: | |
| name: Unit tests (${{ matrix.shard }}) | |
| runs-on: ubuntu-latest | |
| if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }} | |
| needs: | |
| - get_requirements | |
| strategy: | |
| matrix: | |
| shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] | |
| steps: | |
| - 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: Clean state and following up dependencies installation | |
| run: yarn setup:github-ci --node | |
| - 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 | |
| - run: yarn test:unit --shard=${{ matrix.shard }}/10 --forceExit --silent --coverageReporters=json --json --outputFile=tests/results/unit-test-results-${{ matrix.shard }}.json | |
| env: | |
| NODE_OPTIONS: --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 | |
| - 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: ubuntu-latest | |
| needs: [unit-tests, component-view-tests] | |
| if: ${{ !cancelled() && github.event_name != 'merge_group' }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Restore node_modules cache | |
| id: cache-node-modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| node_modules | |
| .yarn/install-state.gz | |
| key: ${{ runner.os }}-node-modules-${{ hashFiles('yarn.lock') }} | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache: yarn | |
| - name: Install Yarn dependencies with retry | |
| if: steps.cache-node-modules.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 --immutable | |
| - name: Clean state and following up dependencies installation | |
| run: yarn setup:github-ci --node | |
| - 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 | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: lcov.info | |
| path: ./tests/merged-coverage/lcov.info | |
| if-no-files-found: error | |
| retention-days: 7 | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: cv-test-stats | |
| path: ./cv-test-stats.json | |
| if-no-files-found: error | |
| retention-days: 7 | |
| - 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 | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: cv-test-coverage-html | |
| path: ./tests/coverage-cv-lcov/ | |
| if-no-files-found: error | |
| retention-days: 7 | |
| - 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 | |
| - 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: ubuntu-latest | |
| if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }} | |
| needs: | |
| - get_requirements | |
| strategy: | |
| matrix: | |
| shard: [1, 2] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Restore node_modules cache | |
| id: cache-node-modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| node_modules | |
| .yarn/install-state.gz | |
| key: ${{ runner.os }}-node-modules-${{ hashFiles('yarn.lock') }} | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache: yarn | |
| - name: Install Yarn dependencies with retry | |
| if: steps.cache-node-modules.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 --immutable | |
| - name: Clean state and following up dependencies installation | |
| run: yarn setup:github-ci --node | |
| - 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: --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 | |
| - 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: 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: | |
| - name: Checkout for action definition | |
| uses: actions/checkout@v6 | |
| 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 }} | |
| 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 | |
| needs: [get_requirements, smart-e2e-selection] | |
| uses: ./.github/workflows/build-android-e2e.yml | |
| with: | |
| build_type: 'main' | |
| metamask_environment: 'e2e' | |
| keystore_target: 'qa' | |
| 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"]' | |
| }} | |
| 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 | |
| needs: [get_requirements, smart-e2e-selection] | |
| uses: ./.github/workflows/build-ios-e2e.yml | |
| secrets: inherit | |
| ios-tests-ready: | |
| name: 'iOS Tests Ready' | |
| runs-on: 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"]' | |
| }} | |
| 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' | |
| metamask_environment: 'qa' | |
| secrets: inherit | |
| report-fixture-validation: | |
| name: 'Report Fixture Validation' | |
| runs-on: ubuntu-latest | |
| if: ${{ !cancelled() && needs.validate-e2e-fixtures.result != 'skipped' }} | |
| needs: [validate-e2e-fixtures] | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Download fixture validation results | |
| 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 | |
| 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: 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: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 # SonarCloud needs a full checkout to perform necessary analysis | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - 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: ubuntu-latest | |
| needs: sonar-cloud | |
| if: ${{ !cancelled() && github.event_name != 'merge_group' && !github.event.pull_request.head.repo.fork }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - 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: ubuntu-latest | |
| needs: | |
| - get_requirements | |
| - check-diff | |
| - dedupe | |
| - scripts | |
| - unit-tests | |
| - component-view-tests | |
| - check-workflows | |
| - 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: actions/checkout@v6 | |
| 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: 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 }} |