Skip to content

Build Mobile App

Build Mobile App #840

Workflow file for this run

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