Skip to content

Push OTA Update

Push OTA Update #196

Workflow file for this run

name: Push OTA Update
on:
workflow_dispatch:
inputs:
commit_hash:
description: 'Commit hash to publish'
required: true
type: string
pr_number:
description: 'PR number associated with the commit'
required: true
type: string
base_branch:
description: 'Base ref to compare fingerprints against (branch name like "main" or tag name like "v7.61.6")'
required: true
type: string
message:
description: 'EAS update message'
required: true
type: string
channel:
description: 'OTA channel to push update to (exp, rc, production)'
required: true
type: choice
options:
- exp
- rc
- production
default: exp
permissions:
contents: read
id-token: write
env:
TARGET_COMMIT_HASH: ${{ inputs.commit_hash }}
TARGET_PR_NUMBER: ${{ inputs.pr_number }}
BASE_BRANCH_REF: ${{ inputs.base_branch }}
UPDATE_MESSAGE: ${{ inputs.message }}
TARGET_CHANNEL: ${{ inputs.channel }}
jobs:
setup-dependencies:
name: Setup Dependencies (PR)
needs:
- validate-pr
uses: ./.github/workflows/setup-node-modules.yml
with:
ref: ${{ inputs.commit_hash }}
fetch-depth: 0
upload-artifact: true
artifact-name: node-modules-eas-update-pr
artifact-retention-days: 1
setup-dependencies-base:
name: Setup Dependencies (Base)
needs:
- validate-pr
uses: ./.github/workflows/setup-node-modules.yml
with:
ref: ${{ inputs.base_branch }}
fetch-depth: 0
upload-artifact: true
artifact-name: node-modules-eas-update-base
artifact-retention-days: 1
fingerprint-comparison:
name: Compare Expo Fingerprints
needs:
- setup-dependencies
- setup-dependencies-base
runs-on: ubuntu-latest
outputs:
branch_fingerprint: ${{ steps.branch_fingerprint.outputs.fingerprint }}
main_fingerprint: ${{ steps.main_fingerprint.outputs.fingerprint }}
fingerprints_equal: ${{ steps.compare.outputs.equal }}
env:
PR_ARTIFACT_NAME: ${{ needs.setup-dependencies.outputs.artifact-name }}
BASE_ARTIFACT_NAME: ${{ needs.setup-dependencies-base.outputs.artifact-name }}
steps:
- name: Checkout target commit
uses: actions/checkout@v4
with:
ref: ${{ env.TARGET_COMMIT_HASH }}
fetch-depth: 0
- name: Checkout base branch snapshot
uses: actions/checkout@v4
with:
ref: ${{ env.BASE_BRANCH_REF }}
path: main
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Validate artifact compatibility (PR commit)
run: |
NODE_VERSION=$(node --version | sed 's/v//')
OS_NAME=$(echo "$RUNNER_OS" | tr '[:upper:]' '[:lower:]')
EXPECTED_ARTIFACT="node-modules-eas-update-pr-node${NODE_VERSION}-${OS_NAME}"
echo "🔍 Validating PR artifact compatibility..."
echo " Expected artifact: $EXPECTED_ARTIFACT"
echo " Actual artifact: ${{ env.PR_ARTIFACT_NAME }}"
if [ "$EXPECTED_ARTIFACT" != "${{ env.PR_ARTIFACT_NAME }}" ]; then
echo "::error title=Artifact Incompatibility::Node version or OS mismatch detected!"
echo "❌ The node_modules artifact was built with different Node version or OS"
echo " This could cause issues with native node modules"
echo " Expected: $EXPECTED_ARTIFACT"
echo " Actual: ${{ env.PR_ARTIFACT_NAME }}"
exit 1
fi
echo "✅ PR artifact compatibility validated"
- name: Download node_modules artifact (PR commit)
uses: actions/download-artifact@v4
with:
name: ${{ env.PR_ARTIFACT_NAME }}
- name: Restore executable permissions
run: |
echo "🔧 Restoring executable permissions..."
find node_modules/.bin -type f -exec chmod +x {} \; 2>/dev/null || true
find node_modules -type f -name "*.node" -exec chmod +x {} \; 2>/dev/null || true
find node_modules -path "*/bin/*" -type f -exec chmod +x {} \; 2>/dev/null || true
find node_modules -path "*/sdks/*" -type f -exec chmod +x {} \; 2>/dev/null || true
echo "✅ Permissions restored"
- name: Verify downloaded artifacts
run: |
echo "✅ Verifying downloaded artifacts..."
if [ ! -d "node_modules" ]; then
echo "❌ node_modules directory not found"
exit 1
fi
if [ ! -f "app/core/InpageBridgeWeb3.js" ]; then
echo "❌ InpageBridgeWeb3.js not found in artifact"
exit 1
fi
echo "✅ Artifacts verified"
- name: Generate fingerprint (target commit)
id: branch_fingerprint
run: |
echo "🧬 Generating fingerprint for current branch..."
FINGERPRINT=$(yarn fingerprint:generate)
echo "fingerprint=$FINGERPRINT" >> "$GITHUB_OUTPUT"
echo "Target PR fingerprint: $FINGERPRINT"
- name: Validate artifact compatibility (base branch)
run: |
NODE_VERSION=$(node --version | sed 's/v//')
OS_NAME=$(echo "$RUNNER_OS" | tr '[:upper:]' '[:lower:]')
EXPECTED_ARTIFACT="node-modules-eas-update-base-node${NODE_VERSION}-${OS_NAME}"
echo "🔍 Validating base branch artifact compatibility..."
echo " Expected artifact: $EXPECTED_ARTIFACT"
echo " Actual artifact: ${{ env.BASE_ARTIFACT_NAME }}"
if [ "$EXPECTED_ARTIFACT" != "${{ env.BASE_ARTIFACT_NAME }}" ]; then
echo "::error title=Artifact Incompatibility::Node version or OS mismatch detected!"
echo "❌ The node_modules artifact was built with different Node version or OS"
echo " This could cause issues with native node modules"
echo " Expected: $EXPECTED_ARTIFACT"
echo " Actual: ${{ env.BASE_ARTIFACT_NAME }}"
exit 1
fi
echo "✅ Base branch artifact compatibility validated"
- name: Download node_modules artifact (base branch)
uses: actions/download-artifact@v4
with:
name: ${{ env.BASE_ARTIFACT_NAME }}
path: main
- name: Restore executable permissions (base branch)
working-directory: main
run: |
echo "🔧 Restoring executable permissions..."
find node_modules/.bin -type f -exec chmod +x {} \; 2>/dev/null || true
find node_modules -type f -name "*.node" -exec chmod +x {} \; 2>/dev/null || true
find node_modules -path "*/bin/*" -type f -exec chmod +x {} \; 2>/dev/null || true
find node_modules -path "*/sdks/*" -type f -exec chmod +x {} \; 2>/dev/null || true
echo "✅ Permissions restored"
- name: Generate fingerprint (base branch)
id: main_fingerprint
working-directory: main
run: |
echo "🧬 Generating fingerprint for base branch (${BASE_BRANCH_REF})..."
FINGERPRINT=$(yarn fingerprint:generate)
echo "fingerprint=$FINGERPRINT" >> "$GITHUB_OUTPUT"
echo "Base branch fingerprint: $FINGERPRINT"
- name: Compare fingerprints
id: compare
env:
BRANCH_FP: ${{ steps.branch_fingerprint.outputs.fingerprint }}
MAIN_FP: ${{ steps.main_fingerprint.outputs.fingerprint }}
run: |
if [ -z "$BRANCH_FP" ] || [ -z "$MAIN_FP" ]; then
echo "❌ Fingerprint generation failed." >&2
exit 1
fi
if [ "$BRANCH_FP" = "$MAIN_FP" ]; then
echo "✅ Fingerprints match. No native changes detected."
echo "equal=true" >> "$GITHUB_OUTPUT"
else
echo "⚠️ Fingerprints differ. Native changes detected."
echo "equal=false" >> "$GITHUB_OUTPUT"
fi
echo "Target PR fingerprint: $BRANCH_FP"
echo "Base branch fingerprint: $MAIN_FP"
- name: Record fingerprint summary
env:
BRANCH_FP: ${{ steps.branch_fingerprint.outputs.fingerprint }}
MAIN_FP: ${{ steps.main_fingerprint.outputs.fingerprint }}
MATCHES: ${{ steps.compare.outputs.equal }}
TARGET_COMMIT_HASH: ${{ env.TARGET_COMMIT_HASH }}
BASE_BRANCH_REF: ${{ env.BASE_BRANCH_REF }}
run: |
{
echo "### Expo Fingerprint Comparison"
echo ""
echo "- Target commit (\`$TARGET_COMMIT_HASH\`) fingerprint: \`$BRANCH_FP\`"
echo "- Base branch (\`$BASE_BRANCH_REF\`) fingerprint: \`$MAIN_FP\`"
echo "- Match: \`$MATCHES\`"
} >> "$GITHUB_STEP_SUMMARY"
validate-pr:
name: Validate PR and Commit
runs-on: ubuntu-latest
outputs:
pr_number: ${{ steps.validate-pr.outputs.pr_number }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate PR contains commit
id: validate-pr
env:
COMMIT_HASH: ${{ env.TARGET_COMMIT_HASH }}
PR_NUMBER: ${{ env.TARGET_PR_NUMBER }}
BASE_BRANCH: ${{ env.BASE_BRANCH_REF }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
.github/scripts/validate-pr-commit.sh
approval:
name: Require OTA Update Approval
needs:
- fingerprint-comparison
- validate-pr
if: ${{ needs.fingerprint-comparison.outputs.fingerprints_equal == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Await approval from mobile platform team
uses: op5dev/require-team-approval@dfd7b8b9a88bf82a955c103f7e19642b0411aecd
with:
team: mobile-platform
pr-number: ${{ needs.validate-pr.outputs.pr_number }}
token: ${{ secrets.METAMASK_MOBILE_ORG_READ_TOKEN }}
push-update:
name: Push EAS Update
runs-on: ubuntu-latest
environment: ${{ inputs.channel == 'exp' && 'build-exp' || inputs.channel == 'rc' && 'build-rc' || 'build-production' }}
needs:
- fingerprint-comparison
- approval
- validate-pr
- setup-dependencies
if: >
needs.fingerprint-comparison.outputs.fingerprints_equal == 'true' &&
needs.approval.result == 'success'
env:
ARTIFACT_NAME: ${{ needs.setup-dependencies.outputs.artifact-name }}
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
EXPO_PROJECT_ID: ${{ secrets.EXPO_PROJECT_ID }}
EXPO_CHANNEL: ${{ vars.EXPO_CHANNEL }}
GIT_BRANCH: ${{ github.ref_name }}
BRIDGE_USE_DEV_APIS: 'true'
RAMP_INTERNAL_BUILD: 'true'
SEEDLESS_ONBOARDING_ENABLED: 'true'
MM_NOTIFICATIONS_UI_ENABLED: 'true'
MM_SECURITY_ALERTS_API_ENABLED: 'true'
MM_REMOVE_GLOBAL_NETWORK_SELECTOR: 'true'
FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN: ${{ secrets.FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN }}
FEATURES_ANNOUNCEMENTS_SPACE_ID: ${{ secrets.FEATURES_ANNOUNCEMENTS_SPACE_ID }}
SEGMENT_WRITE_KEY: ${{ secrets.SEGMENT_WRITE_KEY }}
SEGMENT_PROXY_URL: ${{ secrets.SEGMENT_PROXY_URL }}
SEGMENT_DELETE_API_SOURCE_ID: ${{ secrets.SEGMENT_DELETE_API_SOURCE_ID }}
SEGMENT_REGULATIONS_ENDPOINT: ${{ secrets.SEGMENT_REGULATIONS_ENDPOINT }}
MM_SENTRY_DSN: ${{ secrets.MM_SENTRY_DSN }}
MM_SENTRY_AUTH_TOKEN: ${{ secrets.MM_SENTRY_AUTH_TOKEN }}
IOS_GOOGLE_CLIENT_ID: ${{ secrets.IOS_GOOGLE_CLIENT_ID }}
IOS_GOOGLE_REDIRECT_URI: ${{ secrets.IOS_GOOGLE_REDIRECT_URI }}
ANDROID_APPLE_CLIENT_ID: ${{ secrets.ANDROID_APPLE_CLIENT_ID }}
ANDROID_GOOGLE_CLIENT_ID: ${{ secrets.ANDROID_GOOGLE_CLIENT_ID }}
ANDROID_GOOGLE_SERVER_CLIENT_ID: ${{ secrets.ANDROID_GOOGLE_SERVER_CLIENT_ID }}
GOOGLE_SERVICES_B64_IOS: ${{ secrets.GOOGLE_SERVICES_B64_IOS }}
GOOGLE_SERVICES_B64_ANDROID: ${{ secrets.GOOGLE_SERVICES_B64_ANDROID }}
MM_INFURA_PROJECT_ID: ${{ secrets.MM_INFURA_PROJECT_ID }}
MM_BRANCH_KEY_LIVE: ${{ secrets.MM_BRANCH_KEY_LIVE }}
MM_BRANCH_KEY_TEST: ${{ secrets.MM_BRANCH_KEY_TEST }}
MM_CARD_BAANX_API_CLIENT_KEY: ${{ secrets.MM_CARD_BAANX_API_CLIENT_KEY }}
WALLET_CONNECT_PROJECT_ID: ${{ secrets.WALLET_CONNECT_PROJECT_ID }}
MM_FOX_CODE: ${{ secrets.MM_FOX_CODE }}
FCM_CONFIG_API_KEY: ${{ secrets.FCM_CONFIG_API_KEY }}
FCM_CONFIG_AUTH_DOMAIN: ${{ secrets.FCM_CONFIG_AUTH_DOMAIN }}
FCM_CONFIG_STORAGE_BUCKET: ${{ secrets.FCM_CONFIG_STORAGE_BUCKET }}
FCM_CONFIG_PROJECT_ID: ${{ secrets.FCM_CONFIG_PROJECT_ID }}
FCM_CONFIG_MESSAGING_SENDER_ID: ${{ secrets.FCM_CONFIG_MESSAGING_SENDER_ID }}
FCM_CONFIG_APP_ID: ${{ secrets.FCM_CONFIG_APP_ID }}
FCM_CONFIG_MEASUREMENT_ID: ${{ secrets.FCM_CONFIG_MEASUREMENT_ID }}
QUICKNODE_MAINNET_URL: ${{ secrets.QUICKNODE_MAINNET_URL }}
QUICKNODE_ARBITRUM_URL: ${{ secrets.QUICKNODE_ARBITRUM_URL }}
QUICKNODE_AVALANCHE_URL: ${{ secrets.QUICKNODE_AVALANCHE_URL }}
QUICKNODE_BASE_URL: ${{ secrets.QUICKNODE_BASE_URL }}
QUICKNODE_LINEA_MAINNET_URL: ${{ secrets.QUICKNODE_LINEA_MAINNET_URL }}
QUICKNODE_MONAD_URL: ${{ secrets.QUICKNODE_MONAD_URL }}
QUICKNODE_OPTIMISM_URL: ${{ secrets.QUICKNODE_OPTIMISM_URL }}
QUICKNODE_POLYGON_URL: ${{ secrets.QUICKNODE_POLYGON_URL }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ env.TARGET_COMMIT_HASH }}
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Validate artifact compatibility
run: |
NODE_VERSION=$(node --version | sed 's/v//')
OS_NAME=$(echo "$RUNNER_OS" | tr '[:upper:]' '[:lower:]')
EXPECTED_ARTIFACT="node-modules-eas-update-pr-node${NODE_VERSION}-${OS_NAME}"
echo "🔍 Validating artifact compatibility..."
echo " Expected artifact: $EXPECTED_ARTIFACT"
echo " Actual artifact: ${{ env.ARTIFACT_NAME }}"
if [ "$EXPECTED_ARTIFACT" != "${{ env.ARTIFACT_NAME }}" ]; then
echo "::error title=Artifact Incompatibility::Node version or OS mismatch detected!"
echo "❌ The node_modules artifact was built with different Node version or OS"
echo " This could cause issues with native node modules"
echo " Expected: $EXPECTED_ARTIFACT"
echo " Actual: ${{ env.ARTIFACT_NAME }}"
exit 1
fi
echo "✅ Artifact compatibility validated"
- name: Download node_modules artifact
uses: actions/download-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}
- name: Restore executable permissions
run: |
echo "🔧 Restoring executable permissions..."
find node_modules/.bin -type f -exec chmod +x {} \; 2>/dev/null || true
find node_modules -type f -name "*.node" -exec chmod +x {} \; 2>/dev/null || true
find node_modules -path "*/bin/*" -type f -exec chmod +x {} \; 2>/dev/null || true
find node_modules -path "*/sdks/*" -type f -exec chmod +x {} \; 2>/dev/null || true
echo "✅ Permissions restored"
- name: Verify downloaded artifacts
run: |
echo "✅ Verifying downloaded artifacts..."
if [ ! -d "node_modules" ]; then
echo "❌ node_modules directory not found"
exit 1
fi
if [ ! -f "app/core/InpageBridgeWeb3.js" ]; then
echo "❌ InpageBridgeWeb3.js not found in artifact"
exit 1
fi
echo "📦 node_modules size: $(du -sh node_modules | cut -f1)"
echo "✅ Artifacts verified"
- name: Determine signing secret name
shell: bash
env:
TARGET: ${{ inputs.channel }}
run: |
case "$TARGET" in
exp)
SECRET_NAME="metamask-exp-expo-signer"
;;
rc)
SECRET_NAME="metamask-rc-expo-signer"
;;
production)
SECRET_NAME="metamask-prod-expo-signer"
;;
*)
echo "❌ Unknown target: $TARGET"
exit 1
;;
esac
echo "AWS_SIGNING_CERT_SECRET_NAME=$SECRET_NAME" >> "$GITHUB_ENV"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: 'us-east-2'
- name: Fetch secret and export as environment variables
shell: bash
run: |
echo "🔐 Fetching secret from Secrets Manager..."
secret_json=$(aws secretsmanager get-secret-value \
--region 'us-east-2' \
--secret-id "${AWS_SIGNING_CERT_SECRET_NAME}" \
--query SecretString \
--output text)
keys=$(echo "$secret_json" | jq -r 'keys[]')
for key in $keys; do
value=$(echo "$secret_json" | jq -r --arg k "$key" '.[$k]')
echo "::add-mask::$value"
echo "$key=$(printf '%s' "$value")" >> "$GITHUB_ENV"
echo "✅ Set secret for key: $key"
done
- name: Display configuration
run: |
TARGET_RUNTIME_VERSION=$(node -p "require('./package.json').version")
echo "🔧 Configuration:"
echo " Channel: ${EXPO_CHANNEL:-<not set>}"
echo " Channel (vars.EXPO_CHANNEL): ${{ vars.EXPO_CHANNEL }}"
echo " Message: ${UPDATE_MESSAGE:-<not set>}"
echo " Runtime Version (target): ${TARGET_RUNTIME_VERSION}"
- name: Build & Push EAS Update via build.sh
env:
SKIP_TRANSFORM_LINT: 'true'
# Increase Node heap to avoid OOM during Expo export in CI
NODE_OPTIONS: '--max_old_space_size=8192'
# Disable LavaMoat sandbox to prevent duplicate bundle executions in CI
EXPO_NO_LAVAMOAT: '1'
run: |
echo "📦 Configuring EXPO key..."
if [[ -z "$EXPO_KEY_PRIV_B64" ]]; then
echo "⚠️ EXPO_KEY_PRIV_B64 is not set. Skipping keystore decoding."
exit 1
fi
# Decode the key
EXPO_KEY_PRIV=$(echo "$EXPO_KEY_PRIV_B64" | base64 --decode)
export EXPO_KEY_PRIV
echo "✅ Expo key decoded and exported"
echo "🚀 Pushing EAS update for channel: ${TARGET_CHANNEL}"
case "${TARGET_CHANNEL}" in
exp)
yarn run build:expo-update:main:exp
;;
rc)
yarn run build:expo-update:main:rc
;;
production)
yarn run build:expo-update:main:prod
;;
*)
echo "❌ Unsupported TARGET_CHANNEL: ${TARGET_CHANNEL}" >&2
exit 1
;;
esac
- name: Update summary
if: success()
run: |
{
echo "### ✅ EAS Update Published Successfully"
echo
echo "**Channel:** \`${EXPO_CHANNEL:-<not set>}\`"
echo "**Message:** ${UPDATE_MESSAGE:-<not set>}"
echo
echo "Users on the \`${EXPO_CHANNEL:-<not set>}\` channel will receive this update on their next app launch."
} >> "$GITHUB_STEP_SUMMARY"
- name: Update summary on failure
if: failure()
run: |
{
echo "### ❌ EAS Update Failed"
echo
echo "Check the logs above for error details."
} >> "$GITHUB_STEP_SUMMARY"
fingerprint-mismatch:
name: Fingerprint Mismatch Guard
needs: fingerprint-comparison
if: ${{ needs.fingerprint-comparison.outputs.fingerprints_equal != 'true' }}
runs-on: ubuntu-latest
steps:
- name: Fail on native changes
run: |
echo "::error title=Fingerprint mismatch::Current branch fingerprint differs from main. Native changes detected; aborting workflow."
echo "Current fingerprint: ${{ needs.fingerprint-comparison.outputs.branch_fingerprint }}"
echo "Main fingerprint: ${{ needs.fingerprint-comparison.outputs.main_fingerprint }}"
exit 1