Build Mobile App #840
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: Build Mobile App | |
| on: | |
| workflow_call: | |
| inputs: | |
| build_name: | |
| required: true | |
| type: string | |
| platform: | |
| required: true | |
| type: string # android, ios, or both | |
| source_branch: | |
| description: 'Branch, tag, or SHA to build' | |
| required: true | |
| type: string | |
| version: | |
| description: >- | |
| Optional semver (e.g. 7.43.0) to apply inline on each runner via set-semvar-version.sh | |
| without committing or pushing. Used by post-release-cut-testflight to stamp an x+2 | |
| version onto a main checkout without creating an ephemeral branch. | |
| required: false | |
| type: string | |
| default: '' | |
| build_number: | |
| description: >- | |
| Optional (workflow_call only). From generate-build-version.yml. When non-empty, each matrix | |
| build runner applies scripts/set-build-version.sh locally before building. Omit when | |
| source_branch already contains the desired build number (e.g. commit-hash from | |
| commit-build-version.yml). | |
| required: false | |
| type: string | |
| default: '' | |
| upload_to_sentry: | |
| description: 'If true, enable Sentry CLI upload of JS source maps and native debug symbols during the build' | |
| required: false | |
| type: boolean | |
| default: false | |
| runner_provider: | |
| description: Runner provider forwarded from the caller | |
| required: false | |
| type: string | |
| default: current | |
| outputs: | |
| build_name: | |
| description: 'build_name input passed to this workflow' | |
| value: ${{ inputs.build_name }} | |
| platform: | |
| description: 'platform input (android, ios, or both)' | |
| value: ${{ inputs.platform }} | |
| source_branch_input: | |
| description: 'source_branch input passed to this workflow' | |
| value: ${{ inputs.source_branch }} | |
| checkout_ref: | |
| description: 'Git ref used for checkout (same as source_branch)' | |
| value: ${{ jobs.emit-build-metadata.outputs.checkout_ref }} | |
| built_commit_sha: | |
| description: 'Resolved commit SHA at checkout_ref after build succeeded' | |
| value: ${{ jobs.emit-build-metadata.outputs.built_commit_sha }} | |
| semantic_version: | |
| description: 'package.json version at the built commit' | |
| value: ${{ jobs.emit-build-metadata.outputs.semantic_version }} | |
| android_version_code: | |
| description: 'android/app/build.gradle versionCode at the built commit' | |
| value: ${{ jobs.emit-build-metadata.outputs.android_version_code }} | |
| ios_version_code: | |
| description: 'ios/MetaMask.xcodeproj/project.pbxproj CURRENT_PROJECT_VERSION at the built commit' | |
| value: ${{ jobs.emit-build-metadata.outputs.ios_version_code }} | |
| workflow_dispatch: | |
| inputs: | |
| source_branch: | |
| description: 'Branch, tag, or SHA to build' | |
| required: true | |
| type: string | |
| default: 'main' | |
| build_name: | |
| required: true | |
| type: choice | |
| options: | |
| - main-prod | |
| - main-beta | |
| - main-rc | |
| - main-test | |
| - main-e2e | |
| - main-exp | |
| - main-dev | |
| - main-dev-expo | |
| - flask-prod | |
| - flask-test | |
| - flask-e2e | |
| - flask-dev | |
| platform: | |
| required: true | |
| type: choice | |
| options: [android, ios, both] | |
| upload_to_sentry: | |
| description: 'Upload JS source maps and native debug symbols to Sentry during the build (requires Sentry auth in the build environment)' | |
| required: false | |
| type: boolean | |
| default: false | |
| runner_provider: | |
| description: Runner provider for this manual trial run | |
| required: true | |
| type: choice | |
| options: | |
| - current | |
| - namespace | |
| default: current | |
| permissions: | |
| contents: read | |
| id-token: write | |
| jobs: | |
| # Load config | |
| prepare: | |
| if: ${{ always() && !failure() && !cancelled() }} | |
| runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }} | |
| outputs: | |
| github_environment: ${{ steps.config.outputs.github_environment }} | |
| secrets_json: ${{ steps.config.outputs.secrets_json }} | |
| tap_and_pay_sdk_repo_ssh: ${{ steps.config.outputs.tap_and_pay_sdk_repo_ssh }} | |
| signing_aws_role: ${{ steps.config.outputs.signing_aws_role }} | |
| signing_aws_secret: ${{ steps.config.outputs.signing_aws_secret }} | |
| signing_android_keystore_path: ${{ steps.config.outputs.signing_android_keystore_path }} | |
| script_name: ${{ steps.config.outputs.script_name }} | |
| checkout_ref_for_setup: ${{ inputs.source_branch }} | |
| should_cache: ${{ startsWith(github.ref_name, 'release/') || github.event_name == 'workflow_dispatch' }} | |
| steps: | |
| - uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1 | |
| if: ${{ inputs.runner_provider == 'namespace' }} | |
| with: | |
| fetch-depth: 1 | |
| ref: ${{ inputs.source_branch }} | |
| - uses: actions/checkout@v4 | |
| if: ${{ inputs.runner_provider != 'namespace' }} | |
| with: | |
| fetch-depth: 1 | |
| ref: ${{ inputs.source_branch }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: '.nvmrc' | |
| cache: 'yarn' | |
| - run: yarn install --immutable | |
| - name: Apply version override locally | |
| if: ${{ inputs.version != '' }} | |
| env: | |
| VERSION: ${{ inputs.version }} | |
| run: | | |
| bash scripts/set-semvar-version.sh "$VERSION" | |
| echo "Version bumped locally to $VERSION (no commit)" | |
| - run: node scripts/validate-build-config.js | |
| - name: Load config | |
| id: config | |
| run: | | |
| node -e " | |
| const yaml = require('js-yaml'); | |
| const fs = require('fs'); | |
| const config = yaml.load(fs.readFileSync('builds.yml', 'utf8')); | |
| const build = config.builds['${{ inputs.build_name }}']; | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, 'github_environment=' + build.github_environment + '\n'); | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, 'secrets_json=' + JSON.stringify(build.secrets || {}) + '\n'); | |
| const env = build.env || {}; | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, 'tap_and_pay_sdk_repo_ssh=' + (env.TAP_AND_PAY_SDK_REPO_SSH || '') + '\n'); | |
| const signing = build.signing; | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, 'signing_aws_role=' + (signing ? signing.aws_role || '' : '') + '\n'); | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, 'signing_aws_secret=' + (signing ? signing.aws_secret || '' : '') + '\n'); | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, 'signing_android_keystore_path=' + (signing && signing.android_keystore_path ? signing.android_keystore_path : '') + '\n'); | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, 'script_name=' + (build.script_name || '${{ inputs.build_name }}') + '\n'); | |
| " | |
| # Setup dependencies (no secrets) - uses reusable workflow on platform-specific runner | |
| setup-dependencies: | |
| name: Setup Dependencies (${{ matrix.platform }}) | |
| needs: [prepare] | |
| if: ${{ always() && !failure() && !cancelled() }} | |
| strategy: | |
| matrix: | |
| platform: ${{ inputs.platform == 'both' && fromJSON('["android", "ios"]') || fromJSON(format('["{0}"]', inputs.platform)) }} | |
| uses: ./.github/workflows/setup-node-modules.yml | |
| with: | |
| ref: ${{ needs.prepare.outputs.checkout_ref_for_setup }} | |
| fetch-depth: 1 | |
| checkout-submodules: true | |
| platform: ${{ matrix.platform }} | |
| build_name: ${{ inputs.build_name }} | |
| use-cache: ${{ needs.prepare.outputs.should_cache == 'true' }} | |
| use-tarball: ${{ needs.prepare.outputs.should_cache != 'true' }} | |
| upload-artifact: ${{ needs.prepare.outputs.should_cache != 'true' }} | |
| artifact-name: node-modules-${{ inputs.build_name }}-${{ matrix.platform }} | |
| artifact-retention-days: 1 | |
| runner_provider: ${{ inputs.runner_provider }} | |
| # Build | |
| build: | |
| needs: [prepare, setup-dependencies] | |
| if: ${{ always() && !failure() && !cancelled() }} | |
| timeout-minutes: 120 # Must allow build step (115min) + setup; comment at Build step documents this | |
| strategy: | |
| matrix: | |
| platform: ${{ inputs.platform == 'both' && fromJSON('["android", "ios"]') || fromJSON(format('["{0}"]', inputs.platform)) }} | |
| # Android: Cirrus lg (large) runner for 8GB Gradle heap; iOS: Cirrus macOS Tahoe (has Xcode 26.x) | |
| runs-on: ${{ inputs.runner_provider == 'namespace' && (matrix.platform == 'ios' && 'namespace-profile-metamask-ios-build' || 'namespace-profile-metamask-android-build') || (matrix.platform == 'ios' && 'ghcr.io/cirruslabs/macos-runner:tahoe-xl' || 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-lg') }} | |
| environment: ${{ needs.prepare.outputs.github_environment }} | |
| env: | |
| GRADLE_USER_HOME: /home/admin/_work/.gradle | |
| CACHE_GENERATION: v1 | |
| steps: | |
| - uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1 | |
| if: ${{ inputs.runner_provider == 'namespace' }} | |
| with: | |
| ref: ${{ inputs.source_branch }} | |
| submodules: recursive | |
| - uses: actions/checkout@v4 | |
| if: ${{ inputs.runner_provider != 'namespace' }} | |
| with: | |
| ref: ${{ inputs.source_branch }} | |
| submodules: recursive | |
| - name: Apply version override locally | |
| if: ${{ inputs.version != '' }} | |
| env: | |
| VERSION: ${{ inputs.version }} | |
| run: | | |
| bash scripts/set-semvar-version.sh "$VERSION" | |
| echo "Version bumped locally to $VERSION (no commit)" | |
| - name: Apply build number locally | |
| if: ${{ inputs.build_number != '' }} | |
| env: | |
| BUILD_NUMBER: ${{ inputs.build_number }} | |
| run: | | |
| if [ -z "$BUILD_NUMBER" ]; then | |
| echo "::error::build_number input was non-empty in expression but empty at runtime; aborting." | |
| exit 1 | |
| fi | |
| ./scripts/set-build-version.sh "$BUILD_NUMBER" | |
| echo "Bump applied locally (no commit). Build number: $BUILD_NUMBER" | |
| # Node first so we can download/extract/verify; no yarn install here. | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: '.nvmrc' | |
| # release/* branches and manual workflow_dispatch: restore node_modules from GitHub Actions cache (fast, no artifact download). | |
| - name: Compute node_modules cache key | |
| id: cache-key | |
| if: ${{ needs.prepare.outputs.should_cache == 'true' }} | |
| run: | | |
| NODE_VERSION=$(node --version | sed 's/v//') | |
| KEY="node-modules-${{ env.CACHE_GENERATION }}-${{ inputs.build_name }}-${{ matrix.platform }}-${RUNNER_OS}-${NODE_VERSION}-${{ hashFiles('yarn.lock', 'patches/**', 'scripts/inpage-bridge/**', 'scripts/build-inpage-bridge.sh') }}" | |
| echo "key=${KEY}" >> "$GITHUB_OUTPUT" | |
| echo "Cache key: ${KEY}" | |
| - name: Restore node_modules cache | |
| if: ${{ needs.prepare.outputs.should_cache == 'true' }} | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: | | |
| node_modules | |
| app/util/termsOfUse/termsOfUseContent.ts | |
| app/core/InpageBridgeWeb3.js | |
| scripts/inpage-bridge/dist | |
| key: ${{ steps.cache-key.outputs.key }} | |
| fail-on-cache-miss: true | |
| # All other branches: download tarball artifact from setup-dependencies job. | |
| - name: Download node_modules tarball | |
| if: ${{ needs.prepare.outputs.should_cache != 'true' }} | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: node-modules-${{ inputs.build_name }}-${{ matrix.platform }} | |
| - name: Ensure zstd is available | |
| if: ${{ needs.prepare.outputs.should_cache != 'true' }} | |
| shell: bash | |
| run: | | |
| if command -v zstd >/dev/null 2>&1; then | |
| echo "✅ zstd present: $(zstd --version)" | |
| elif [ "$RUNNER_OS" = "macOS" ]; then | |
| echo "Installing zstd via Homebrew..." | |
| brew install zstd | |
| else | |
| echo "Installing zstd via apt..." | |
| sudo apt-get update -qq && sudo apt-get install -y -qq zstd | |
| fi | |
| - name: Extract tarball (preserves symlinks) | |
| if: ${{ needs.prepare.outputs.should_cache != 'true' }} | |
| run: | | |
| echo "📦 Extracting tarball..." | |
| tar --use-compress-program 'zstd -d' -xf node-modules-${{ matrix.platform }}.tar.zst | |
| rm node-modules-${{ matrix.platform }}.tar.zst | |
| echo "✅ Tarball extracted" | |
| - name: Verify artifacts and symlinks | |
| run: | | |
| echo "✅ Verifying artifacts..." | |
| if [ ! -d "node_modules" ]; then | |
| echo "❌ node_modules not found" | |
| exit 1 | |
| fi | |
| if [ ! -f "app/core/InpageBridgeWeb3.js" ]; then | |
| echo "❌ InpageBridgeWeb3.js not found" | |
| exit 1 | |
| fi | |
| echo "📦 node_modules size: $(du -sh node_modules | cut -f1)" | |
| # Verify symlinks in .bin directory | |
| echo "🔍 Checking .bin symlinks..." | |
| if [ -e "node_modules/.bin/react-native" ]; then | |
| echo "✅ react-native symlink exists and target is valid" | |
| find node_modules/.bin -maxdepth 1 -name 'react-native' -ls | |
| else | |
| echo "❌ react-native symlink missing or broken" | |
| find node_modules/.bin -maxdepth 1 -ls | head -10 | |
| exit 1 | |
| fi | |
| # Check React Native scripts | |
| if [ -d "node_modules/react-native/scripts" ]; then | |
| echo "✅ React Native scripts found" | |
| else | |
| echo "❌ React Native scripts missing" | |
| exit 1 | |
| fi | |
| echo "✅ All artifacts verified with working symlinks!" | |
| # iOS only: Ruby + CocoaPods + Xcode (no yarn install; node_modules from cache). Signing is in Configure signing certificates below. | |
| - name: Setup Ruby (iOS) | |
| if: matrix.platform == 'ios' | |
| uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb #v1 | |
| with: | |
| ruby-version: '3.2.9' | |
| working-directory: ios | |
| bundler-cache: true | |
| - name: Select Xcode (iOS) | |
| if: matrix.platform == 'ios' | |
| run: | | |
| sudo xcodes select 26.3 | |
| xcodebuild -version | |
| # TapAndPay SDK Setup (Android only) | |
| - name: Clone TapAndPay SDK | |
| if: | | |
| matrix.platform == 'android' && | |
| needs.prepare.outputs.tap_and_pay_sdk_repo_ssh != '' | |
| run: | | |
| if [ -z "$TAP_AND_PAY_SDK_SSH_KEY" ]; then | |
| echo "⚠️ TAP_AND_PAY_SDK_SSH_KEY not set, skipping TapAndPay SDK clone" | |
| exit 0 | |
| fi | |
| mkdir -p ~/.ssh | |
| echo "$TAP_AND_PAY_SDK_SSH_KEY" | base64 -d | tr -d '\r' > ~/.ssh/tap_and_pay_key | |
| echo "" >> ~/.ssh/tap_and_pay_key | |
| chmod 600 ~/.ssh/tap_and_pay_key | |
| trap 'rm -f ~/.ssh/tap_and_pay_key' EXIT | |
| ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null | |
| eval "$(ssh-agent -s)" | |
| ssh-add ~/.ssh/tap_and_pay_key | |
| echo "📦 Cloning TapAndPay SDK into android/libs/..." | |
| git clone --depth 1 "$TAP_AND_PAY_SDK_REPO_SSH" /tmp/tap-and-pay-sdk | |
| mkdir -p android/libs | |
| cp -r /tmp/tap-and-pay-sdk/* android/libs/ | |
| rm -rf /tmp/tap-and-pay-sdk | |
| ssh-add -D | |
| eval "$(ssh-agent -k)" | |
| echo "✅ TapAndPay SDK installed to android/libs/" | |
| env: | |
| TAP_AND_PAY_SDK_SSH_KEY: ${{ secrets.TAP_AND_PAY_SDK_SSH_KEY }} | |
| TAP_AND_PAY_SDK_REPO_SSH: ${{ needs.prepare.outputs.tap_and_pay_sdk_repo_ssh }} | |
| - name: Apply build config | |
| run: | | |
| # Load env vars from builds.yml (this step only) | |
| eval "$(node scripts/apply-build-config.js ${{ inputs.build_name }} --export)" | |
| # Persist to GITHUB_ENV so later steps (e.g. Build) see CONFIGURATION, IS_SIM_BUILD, etc. | |
| node scripts/apply-build-config.js ${{ inputs.build_name }} --export-github-env >> "$GITHUB_ENV" | |
| # Generate build-env.json capturing actual env values used in this build (non-critical) | |
| node scripts/apply-build-config.js ${{ inputs.build_name }} --write-build-env || true | |
| - name: Validate secrets | |
| env: | |
| CONFIG_SECRETS: ${{ needs.prepare.outputs.secrets_json }} | |
| SECRETS_JSON: ${{ toJSON(secrets) }} | |
| run: node scripts/validate-secrets-from-config.js | |
| # iOS: Install Pods here so generated paths match this runner (setup-node-modules skips pod install with --no-install-pods). | |
| - name: Install CocoaPods dependencies (iOS) | |
| if: matrix.platform == 'ios' | |
| env: | |
| BUNDLE_GEMFILE: ios/Gemfile | |
| uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 | |
| with: | |
| timeout_minutes: 10 | |
| max_attempts: 3 | |
| retry_wait_seconds: 30 | |
| command: | | |
| node -e "require('fs').writeFileSync('ios/debug.xcconfig',''); require('fs').writeFileSync('ios/release.xcconfig','');" | |
| yarn pod:install | |
| - name: Set secrets | |
| env: | |
| CONFIG_SECRETS: ${{ needs.prepare.outputs.secrets_json }} | |
| ALL_SECRETS: ${{ toJSON(secrets) }} | |
| run: node scripts/set-secrets-from-config.js | |
| # Android only: minimal env (SDK is pre-installed on Cirrus runner) | |
| - name: Set Android environment variables | |
| if: matrix.platform == 'android' | |
| run: | | |
| { | |
| echo "ANDROID_HOME=/opt/android-sdk" | |
| echo "ANDROID_SDK_ROOT=/opt/android-sdk" | |
| } >> "$GITHUB_ENV" | |
| - name: Setup Java | |
| if: matrix.platform == 'android' | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: '17' | |
| distribution: 'temurin' | |
| - name: Apply release Gradle config (Android) | |
| if: matrix.platform == 'android' | |
| run: cp android/gradle.properties.release android/gradle.properties | |
| - name: Restore Gradle dependencies from branch cache (Android) | |
| if: matrix.platform == 'android' | |
| id: gradle-cache-restore | |
| uses: cirruslabs/cache@bba69c6578b863ad0398ad40567bd2ef70290fe0 # v4 | |
| with: | |
| path: | | |
| ~/_work/.gradle/caches | |
| ~/_work/.gradle/wrapper | |
| key: gradle-release-${{ github.ref_name }}-${{ env.CACHE_GENERATION }}-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} | |
| - name: Restore Gradle dependencies from main cache (Android) | |
| if: matrix.platform == 'android' && steps.gradle-cache-restore.outputs.cache-hit != 'true' && github.ref_name != 'main' | |
| uses: cirruslabs/cache/restore@bba69c6578b863ad0398ad40567bd2ef70290fe0 # v4 | |
| with: | |
| path: | | |
| ~/_work/.gradle/caches | |
| ~/_work/.gradle/wrapper | |
| key: gradle-release-main-${{ env.CACHE_GENERATION }}-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} | |
| # iOS: Clean up any existing keychains from previous runs | |
| - name: Clean up existing keychains | |
| if: matrix.platform == 'ios' && needs.prepare.outputs.signing_aws_role != '' | |
| run: | | |
| KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" | |
| if [ -f "$KEYCHAIN_PATH" ]; then | |
| echo "🧹 Removing existing keychain..." | |
| security delete-keychain "$KEYCHAIN_PATH" || true | |
| fi | |
| # Signing: uses role + secret from builds.yml (skipped when builds.yml omits signing, e.g. simulator-only flask-dev) | |
| - name: Configure signing certificates | |
| if: needs.prepare.outputs.signing_aws_role != '' | |
| uses: ./.github/actions/configure-signing | |
| with: | |
| aws-role-to-assume: ${{ needs.prepare.outputs.signing_aws_role }} | |
| aws-region: 'us-east-2' | |
| platform: ${{ matrix.platform }} | |
| aws-secret-name: ${{ needs.prepare.outputs.signing_aws_secret }} | |
| android-keystore-path: ${{ needs.prepare.outputs.signing_android_keystore_path }} | |
| # iOS: Configure Node path for Xcode build scripts (React Native Codegen) | |
| - name: Configure Node path for Xcode | |
| if: matrix.platform == 'ios' | |
| run: | | |
| NODE_BINARY=$(command -v node) | |
| echo "Node binary: $NODE_BINARY" | |
| echo "export NODE_BINARY=\"$NODE_BINARY\"" > ios/.xcode.env.local | |
| echo "✅ Created ios/.xcode.env.local" | |
| cat ios/.xcode.env.local | |
| node --version | |
| # Sentry: build.sh defaults SENTRY_DISABLE_AUTO_UPLOAD=true; set false only when opted in. | |
| - name: Configure Sentry upload | |
| env: | |
| UPLOAD_TO_SENTRY: ${{ inputs.upload_to_sentry }} | |
| run: | | |
| if [ "$UPLOAD_TO_SENTRY" = "true" ]; then | |
| echo "SENTRY_DISABLE_AUTO_UPLOAD=false" >> "$GITHUB_ENV" | |
| echo "✅ Sentry auto-upload enabled for this build" | |
| else | |
| echo "SENTRY_DISABLE_AUTO_UPLOAD=true" >> "$GITHUB_ENV" | |
| echo "ℹ️ Sentry auto-upload disabled" | |
| fi | |
| # Build with retry logic. Timeouts: 55min per attempt, 115min total for step, 120min job | |
| - name: Build ${{ matrix.platform }} | |
| timeout-minutes: 115 | |
| env: | |
| # Must match Apply build config / artifact naming so build.sh loads the same | |
| # builds.yml entry (e.g. main-e2e-bs-with-srp), not generic main-e2e (sim-only). | |
| BUILD_CONFIG_NAME: ${{ inputs.build_name }} | |
| GIT_BRANCH: ${{ inputs.source_branch }} | |
| # React Native 0.81's ReactAndroid/build.gradle.kts requests CMake 3.30.5 | |
| # via `System.getenv("CMAKE_VERSION") ?: "3.30.5"`. The self-hosted runner | |
| # only ships CMake 3.22.1 in /opt/android-sdk/cmake/ and AGP cannot auto- | |
| # download missing components, causing CXX1300. RN's CMakeLists.txt files | |
| # only require >= 3.13, so 3.22.1 is fully sufficient. | |
| CMAKE_VERSION: '3.22.1' | |
| uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 | |
| with: | |
| timeout_minutes: 55 | |
| max_attempts: 2 | |
| retry_wait_seconds: 60 | |
| command: | | |
| # AGP can leave a partial ndk/<version> directory after a failed | |
| # download (corrupt zip → "Error on ZipFile unknown archive"). The | |
| # next attempt then fails immediately with CXX1101 (missing | |
| # source.properties) instead of re-downloading. Clear incomplete | |
| # installs and SDK temp zips before each attempt — same pattern as | |
| # .github/actions/setup-e2e-env for system images. | |
| if [ "${{ matrix.platform }}" = "android" ]; then | |
| SDK_ROOT="${ANDROID_SDK_ROOT:-/opt/android-sdk}" | |
| NDK_ROOT="$SDK_ROOT/ndk" | |
| if [ -d "$NDK_ROOT" ]; then | |
| for ndk_dir in "$NDK_ROOT"/*; do | |
| if [ -d "$ndk_dir" ] && [ ! -f "$ndk_dir/source.properties" ]; then | |
| echo "Removing incomplete NDK install: $ndk_dir" | |
| rm -rf "$ndk_dir" | |
| fi | |
| done | |
| fi | |
| if [ -d "$SDK_ROOT/.temp" ]; then | |
| echo "Clearing Android SDK temp downloads under $SDK_ROOT/.temp" | |
| find "$SDK_ROOT/.temp" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + || true | |
| fi | |
| fi | |
| export NODE_OPTIONS='--max-old-space-size=4096' | |
| export METRO_MAX_WORKERS='4' | |
| SCRIPT_BASE="${{ needs.prepare.outputs.script_name }}" | |
| SCRIPT_NAME="build:${{ matrix.platform }}:${SCRIPT_BASE//-/:}" | |
| yarn "$SCRIPT_NAME" | |
| # Android Play Store checks: Gradle lint (all main release builds) + bundletool AAB validation (production only). | |
| # The lint + Slack report run for every main release build so RC gets Slack feedback. | |
| # bundletool cache is gated on production because non-prod builds don't produce an AAB. | |
| - name: Cache bundletool (Android Play bundle validation) | |
| id: bundletool-cache-play-check | |
| if: >- | |
| success() && | |
| matrix.platform == 'android' && | |
| env.CONFIGURATION != 'Debug' && | |
| env.METAMASK_BUILD_TYPE == 'main' && | |
| env.METAMASK_ENVIRONMENT == 'production' | |
| uses: actions/cache@v4 | |
| with: | |
| path: ${{ runner.temp }}/bundletool-all.jar | |
| key: bundletool-1.18.3-jar | |
| - name: Android Play Store lint and bundle validation (prodRelease, non-blocking) | |
| if: >- | |
| success() && | |
| matrix.platform == 'android' && | |
| env.CONFIGURATION != 'Debug' && | |
| env.METAMASK_BUILD_TYPE == 'main' | |
| env: | |
| SENTRY_DISABLE_AUTO_UPLOAD: true | |
| run: node scripts/android-play-store-check-slack.mjs | |
| - name: Upload Android Play Store Slack report (main-rc only) | |
| if: >- | |
| success() && | |
| matrix.platform == 'android' && | |
| inputs.build_name == 'main-rc' && | |
| env.CONFIGURATION != 'Debug' && | |
| env.METAMASK_BUILD_TYPE == 'main' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-play-store-check-slack | |
| path: android-play-store-check-slack.md | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| # Rename build artifacts (ios_simulator_path / ios_ipa_path / ios_archive_path / android_*_path outputs) | |
| - name: Rename ${{ matrix.platform }} artifacts | |
| if: success() | |
| id: rename | |
| run: node scripts/rename-artifacts.js ${{ matrix.platform }} | |
| # Upload build artifacts (only if build succeeded) | |
| - name: Upload iOS simulator app | |
| if: matrix.platform == 'ios' && env.IS_SIM_BUILD == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ios-app-${{ inputs.build_name }} | |
| path: ${{ steps.rename.outputs.ios_simulator_path }} | |
| if-no-files-found: error | |
| - name: Upload iOS IPA | |
| if: matrix.platform == 'ios' && (env.IS_SIM_BUILD != 'true' || env.IS_DEVICE_BUILD == 'true') | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ios-ipa-${{ inputs.build_name }} | |
| path: ${{ steps.rename.outputs.ios_ipa_path }} | |
| if-no-files-found: error | |
| - name: Upload iOS xcarchive | |
| if: matrix.platform == 'ios' && (env.IS_SIM_BUILD != 'true' || env.IS_DEVICE_BUILD == 'true') | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ios-xcarchive-${{ inputs.build_name }} | |
| path: ${{ steps.rename.outputs.ios_archive_path }} | |
| if-no-files-found: error | |
| - name: Upload iOS sourcemap | |
| if: matrix.platform == 'ios' && (env.IS_SIM_BUILD != 'true' || env.IS_DEVICE_BUILD == 'true') | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ios-sourcemaps-${{ inputs.build_name }} | |
| path: ${{ steps.rename.outputs.ios_sourcemap_path }} | |
| if-no-files-found: warn | |
| # Dev builds (CONFIGURATION=Debug): APK + test APK (for E2E) | |
| - name: Upload Android APK (dev) | |
| if: matrix.platform == 'android' && env.CONFIGURATION == 'Debug' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-apk-${{ inputs.build_name }} | |
| path: ${{ steps.rename.outputs.android_apk_path }} | |
| if-no-files-found: error | |
| - name: Upload Android test APK (dev) | |
| if: matrix.platform == 'android' && env.CONFIGURATION == 'Debug' && steps.rename.outputs.android_test_apk_path != '' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-test-apk-${{ inputs.build_name }} | |
| path: ${{ steps.rename.outputs.android_test_apk_path }} | |
| if-no-files-found: warn | |
| # Release builds: APK and AAB as separate artifacts | |
| - name: Upload Android APK (release) | |
| if: matrix.platform == 'android' && env.CONFIGURATION != 'Debug' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-apk-${{ inputs.build_name }} | |
| path: ${{ steps.rename.outputs.android_apk_path }} | |
| if-no-files-found: error | |
| - name: Upload Android AAB | |
| if: >- | |
| matrix.platform == 'android' && | |
| env.CONFIGURATION != 'Debug' && | |
| env.METAMASK_ENVIRONMENT == 'production' && | |
| steps.rename.outputs.android_aab_path != '' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-aab-${{ inputs.build_name }} | |
| path: ${{ steps.rename.outputs.android_aab_path }} | |
| if-no-files-found: error | |
| - name: Upload Android sourcemaps | |
| if: matrix.platform == 'android' && env.CONFIGURATION != 'Debug' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-sourcemaps-${{ inputs.build_name }} | |
| path: ${{ steps.rename.outputs.android_sourcemap_dir }} | |
| if-no-files-found: warn | |
| - name: Upload build environment JSON | |
| if: success() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-env-${{ inputs.build_name }}-${{ matrix.platform }} | |
| path: build-env.json | |
| if-no-files-found: warn | |
| # Single fan-in job so workflow_call outputs work with matrix `build` (matrix jobs cannot feed workflow outputs directly). | |
| emit-build-metadata: | |
| name: Emit build metadata | |
| needs: [prepare, build] | |
| if: ${{ !failure() && !cancelled() && needs.prepare.result == 'success' && needs.build.result == 'success' }} | |
| runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }} | |
| outputs: | |
| checkout_ref: ${{ steps.meta.outputs.checkout_ref }} | |
| built_commit_sha: ${{ steps.meta.outputs.built_commit_sha }} | |
| semantic_version: ${{ steps.meta.outputs.semantic_version }} | |
| android_version_code: ${{ steps.meta.outputs.android_version_code }} | |
| ios_version_code: ${{ steps.meta.outputs.ios_version_code }} | |
| steps: | |
| - uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1 | |
| if: ${{ inputs.runner_provider == 'namespace' }} | |
| with: | |
| fetch-depth: 1 | |
| ref: ${{ needs.prepare.outputs.checkout_ref_for_setup }} | |
| - uses: actions/checkout@v4 | |
| if: ${{ inputs.runner_provider != 'namespace' }} | |
| with: | |
| fetch-depth: 1 | |
| ref: ${{ needs.prepare.outputs.checkout_ref_for_setup }} | |
| - name: Apply version override locally | |
| if: ${{ inputs.version != '' }} | |
| env: | |
| VERSION: ${{ inputs.version }} | |
| run: | | |
| bash scripts/set-semvar-version.sh "$VERSION" | |
| echo "Version bumped locally to $VERSION (no commit)" | |
| - name: Apply build number locally | |
| if: ${{ inputs.build_number != '' }} | |
| env: | |
| BUILD_NUMBER: ${{ inputs.build_number }} | |
| run: ./scripts/set-build-version.sh "$BUILD_NUMBER" | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: '.nvmrc' | |
| - name: Read metadata from tree at built ref | |
| id: meta | |
| run: | | |
| REF="${{ needs.prepare.outputs.checkout_ref_for_setup }}" | |
| { | |
| echo "checkout_ref=$REF" | |
| echo "built_commit_sha=$(git rev-parse HEAD)" | |
| } >> "$GITHUB_OUTPUT" | |
| ./scripts/get-build-metadata.sh --ci |