This document outlines the migration path from Bitrise to GitHub Actions using the centralized builds.yml configuration.
┌─────────────────────────────────────────────────────────────────────────────┐
│ Phase 1: builds.yml for config ✅ COMPLETE │
├─────────────────────────────────────────────────────────────────────────────┤
│ Phase 1.5: Parallel validation in Bitrise ✅ COMPLETE │
├─────────────────────────────────────────────────────────────────────────────┤
│ Phase 2: Remove env remapping from build.sh ✅ COMPLETE │
├─────────────────────────────────────────────────────────────────────────────┤
│ Phase 3: Add store deployment workflows 📍 NEXT │
├─────────────────────────────────────────────────────────────────────────────┤
│ Phase 4: Deprecate Bitrise ⏳ PENDING │
└─────────────────────────────────────────────────────────────────────────────┘
Note: E2E test workflows already exist in GitHub Actions (see
.github/workflows/run-e2e-*.yml), so no migration needed for E2E.
Status: Complete
What was done:
- Created
builds.yml(project root) as single source of truth - Created
scripts/apply-build-config.jsto load and export config - Created
scripts/validate-build-config.jsfor CI validation - Created
scripts/set-secrets-from-config.jsfor secret mapping - Created
.github/workflows/build.ymlGitHub Actions workflow - Added
remote_feature_flagssection for build-time defaults of LaunchDarkly flags - Updated
RemoteFeatureFlagControllerto seed defaults fromREMOTE_FEATURE_FLAG_DEFAULTS - Refactored selectors (Perps, Earn) to use build-time defaults instead of hardcoded fallbacks
Files:
builds.yml # Build configuration (project root; env vars, secrets, code fencing, remote_feature_flags)
.github/
├── builds.README.md # Architecture documentation
└── workflows/
└── build.yml # GitHub Actions workflow
scripts/
├── apply-build-config.js # Loads config, exports env vars + REMOTE_FEATURE_FLAG_DEFAULTS
├── validate-build-config.js # Validates config structure
└── set-secrets-from-config.js # Maps GitHub Secrets → env vars
app/core/Engine/controllers/
└── remote-feature-flag-controller-init.ts # Seeds build-time defaults
app/components/UI/Perps/selectors/featureFlags/
└── index.ts # Updated to use build-time defaults
app/components/UI/Earn/selectors/featureFlags/
└── index.ts # Updated to use build-time defaults
Pattern: Single anchor with production defaults, override in dev/exp builds (same as _servers).
# builds.yml
_remote_feature_flags: &remote_feature_flags # Single anchor with prod defaults
perpsPerpTradingEnabled: false
earnPooledStakingEnabled: true
builds:
main-prod:
remote_feature_flags: *remote_feature_flags # Use defaults
main-dev:
remote_feature_flags:
<<: *remote_feature_flags
perpsPerpTradingEnabled: true # Override for devFlow:
─────────────────────────────────────────────────────────────────────────
builds.yml (build time) → REMOTE_FEATURE_FLAG_DEFAULTS (JSON env var)
→ RemoteFeatureFlagController seeds defaults
→ LaunchDarkly OVERRIDES at runtime
→ Selectors read from remoteFeatureFlags
Benefits:
- Single source of truth for feature flag defaults (one anchor)
- Explicit overrides show exactly what differs from production
- Removed ~50+ hardcoded
process.env.MM_*checks from selectors - LaunchDarkly still works with version gating
Status: Next
Goal: Run both old (remapping functions) and new (builds.yml) configuration in parallel within Bitrise builds. Compare outputs to validate the new config produces identical results before trusting it.
- Zero risk - Bitrise still uses old remapping for actual builds
- Early detection - Catches config mismatches before Phase 2
- Builds confidence - Team sees "✅ Config matches" in every build
- Audit trail - Bitrise logs show comparison results
The script has been created at scripts/verify-build-config.js. It:
- Automatically detects build name from
METAMASK_BUILD_TYPE+METAMASK_ENVIRONMENT - Compares env vars, secret mappings, code fencing, and remote feature flags
- Supports
--strictmode (exit with error on mismatch) and--verbosemode
# Test locally
METAMASK_BUILD_TYPE=main METAMASK_ENVIRONMENT=production node scripts/verify-build-config.js --verbose
# Output shows what matches and what differs from builds.ymlIn bitrise.yml, add a verification step after the existing remapping but before the actual build.
The script auto-detects the build name from METAMASK_BUILD_TYPE and METAMASK_ENVIRONMENT:
# After existing remapping runs (sets env vars the old way)
# Add this step BEFORE the actual build step
- script@1:
title: Verify builds.yml config matches
inputs:
- content: |
#!/bin/bash
# Don't use set -e initially - we want to control exit behavior
echo "╔════════════════════════════════════════════════════════════╗"
echo "║ Phase 1.5: Parallel Validation ║"
echo "║ Comparing Bitrise env vars with builds.yml config ║"
echo "╚════════════════════════════════════════════════════════════╝"
# Run verification (auto-detects build from METAMASK_BUILD_TYPE + METAMASK_ENVIRONMENT)
# Week 1-2: Run without --strict (warnings only)
# Week 3+: Add --strict to fail builds on mismatch
node scripts/verify-build-config.js --verbose
# Uncomment below when ready for strict mode:
# node scripts/verify-build-config.js --strictWhere to add this step:
- Find workflows that call
remapMainProdEnvVariables,remapMainDevEnvVariables, etc. - Add the verification step AFTER the remapping script, BEFORE
generateIosBinaryorgenerateAndroidBinary
Week 1: Add verification step with warnings only (don't fail builds)
- Change process.exit(1) to process.exit(0) temporarily
- Monitor logs for mismatches
Week 2: Fix any mismatches found in builds.yml
Week 3: Enable strict mode (fail on mismatch)
- Restore process.exit(1)
Week 4: If all builds pass validation for 1 week, proceed to Phase 2
-
verify-build-config.jscreated and tested locally - Verification step added to Bitrise build workflows:
_android_build_template(main Android builds)_ios_build_template(main iOS builds)ios_e2e_build(iOS E2E builds)_android_e2e_build_template(Android E2E builds)
- 1+ week of builds passing with "✅ Config verification PASSED"
- No mismatches detected in any build variant
- Team confident to proceed to Phase 2
Environment Variables:
| Variable | Why Critical |
|---|---|
METAMASK_ENVIRONMENT |
Build identity - wrong = undefined behavior |
METAMASK_BUILD_TYPE |
Build identity - wrong = undefined behavior |
PORTFOLIO_API_URL |
API endpoint - wrong = API errors |
SECURITY_ALERTS_API_URL |
Security features - wrong = alerts broken |
RAMPS_ENVIRONMENT |
Ramps feature - wrong = payment issues |
IS_TEST |
Test mode flags - wrong = unexpected state |
Secret Mappings:
| Secret | Why Critical |
|---|---|
SEGMENT_WRITE_KEY |
Analytics - wrong mapping = data in wrong project |
MM_SENTRY_DSN |
Error tracking - wrong mapping = errors lost |
IOS_GOOGLE_CLIENT_ID |
Auth - wrong mapping = login fails |
MM_CARD_BAANX_API_CLIENT_KEY |
Card feature - wrong mapping = API errors |
Also Validated:
- Code fencing features (what code is included/excluded)
- Remote feature flag defaults (build-time LaunchDarkly defaults)
Status: Complete
Goal: Replace 200+ lines of remapXxxEnvVariables() functions in build.sh with a single config load from builds.yml.
-
Added
loadBuildConfig()function tobuild.shthat:- Constructs build name from
METAMASK_BUILD_TYPE+METAMASK_ENVIRONMENT(e.g.,main-prod) - Normalizes environment names (
production→prod) - Calls
apply-build-config.jswith--exportflag - Evaluates the exported environment variables
- Constructs build name from
-
Removed 12 remapping functions (~200 lines):
remapEnvVariable()remapMainDevEnvVariables()remapMainProdEnvVariables()remapMainBetaEnvVariables()remapMainReleaseCandidateEnvVariables()remapMainExperimentalEnvVariables()remapMainTestEnvVariables()remapMainE2EEnvVariables()remapFlaskProdEnvVariables()remapFlaskTestEnvVariables()remapFlaskE2EEnvVariables()remapEnvVariableQA()
-
Replaced switch/case logic (~35 lines) with a single call to
loadBuildConfig() -
Added QA builds to
builds.yml:qa-prod- QA production buildqa-dev- QA development build
loadBuildConfig() {
local build_type="$1"
local environment="$2"
# Normalize environment name (production -> prod)
local normalized_env="$environment"
case "$environment" in
production) normalized_env="prod" ;;
esac
# Construct build name (e.g., main-prod, flask-dev)
local build_name="${build_type}-${normalized_env}"
echo "📦 Loading configuration from builds.yml for '${build_name}'..."
# Load config using apply-build-config.js
local config_output
config_output=$(node "${__DIRNAME__}/apply-build-config.js" "${build_name}" --export 2>&1)
local exit_code=$?
if [ $exit_code -ne 0 ]; then
echo "❌ Failed to load build configuration"
echo "Error: ${config_output}"
return 1
fi
# Apply the configuration (exports environment variables)
eval "$config_output"
echo "✅ Configuration loaded from builds.yml"
return 0
}The old 3-argument format still works:
# Old format (still supported)
./scripts/build.sh android main production
# New format (recommended)
./scripts/build.sh android main-prod- Added
loadBuildConfig()function tobuild.sh - Removed 12
remapXxxEnvVariables()functions (~200 lines) - Replaced switch/case remapping logic with single
loadBuildConfig()call - Added QA builds to
builds.yml(qa-prod,qa-dev) - Maintained backward compatibility with old 3-argument format
# Test each build variant
./scripts/build.sh android main production # Old format
./scripts/build.sh android main-prod # Also works (normalized internally)
./scripts/build.sh ios flask dev
./scripts/build.sh ios qa production
# Verify error handling
./scripts/build.sh android invalid-build # Should fail with clear errorGoal: Replace Bitrise store deployment pipelines with GitHub Actions.
.github/workflows/
├── build.yml # ✅ Exists
├── deploy-android-play.yml # ⏳ Deploy to Play Store
├── deploy-ios-testflight.yml # ⏳ Deploy to TestFlight
├── deploy-ios-appstore.yml # ⏳ Deploy to App Store
└── release.yml # ⏳ Full release pipeline
| Bitrise Step | GitHub Actions Equivalent |
|---|---|
deploy-to-bitrise-io |
actions/upload-artifact@v4 |
google-play-deploy |
r0adkll/upload-google-play@v1 |
deploy-to-itunesconnect-deliver |
apple-actions/upload-testflight-build@v1 |
fastlane |
ruby/setup-ruby + fastlane |
# .github/workflows/deploy-android-play.yml
name: Deploy Android to Play Store
on:
workflow_call:
inputs:
build_name:
required: true
type: string
track:
required: true
type: string # internal, alpha, beta, production
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download APK artifact
uses: actions/download-artifact@v4
with:
name: android-${{ inputs.build_name }}
- name: Upload to Play Store
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}
packageName: io.metamask
releaseFiles: app-prod-release.aab
track: ${{ inputs.track }}Goal: Fully transition to GitHub Actions and remove Bitrise.
- All build variants working in GitHub Actions
- Store deployments working
- E2E tests running in GitHub Actions (already complete)
- Nightly builds configured
- Team trained on new workflows
- Monitoring/alerting set up
- Documentation updated
-
bitrise.ymlremoved from repository
Week 1-2: Run both systems in parallel (builds)
Week 3-4: Run both systems in parallel (deployments)
Week 5: Disable Bitrise triggers, monitor GitHub Actions
Week 6: Remove bitrise.yml, update documentation
# Local development
./scripts/build.sh android main-dev
./scripts/build.sh ios flask-dev
# CI (GitHub Actions)
# Triggered via workflow_dispatch or workflow_call# Check config is valid
node scripts/validate-build-config.js
# Preview env vars for a build
node scripts/apply-build-config.js main-prod --export- Add to
builds.yml - Add to workflow dropdown in
.github/workflows/build.yml - Add package.json script (optional)
- Test:
./scripts/build.sh <platform> <new-build-name>
If issues arise during migration:
- Phase 2 rollback: Revert build.sh changes, keep old remapping functions
- Phase 3 rollback: Keep Bitrise pipelines active, disable GitHub Actions deployment workflows
- Phase 4 rollback: Re-enable Bitrise from git history
All phases are designed to be reversible with minimal impact.