Skip to content

Commit daa9ce1

Browse files
feat: Proposal 1 - YAML base config with inheritance
- Add base.yml with shared configuration (servers, features, common secrets) - Add environment-specific configs (prod, rc, exp) that extend base - Add merge-config.js script to merge base + environment configs - Add set-secrets-from-config.js helper for secret mapping - Add build.yml workflow that uses merged configurations - Add js-yaml dependency for YAML parsing - Add README explaining the configuration structure This proposal uses a base configuration file with inheritance pattern, allowing shared values to be defined once and environment-specific overrides to be minimal and clear. Co-authored-by: tomas.santos <[email protected]>
1 parent 7bb07c9 commit daa9ce1

File tree

9 files changed

+494
-0
lines changed

9 files changed

+494
-0
lines changed

.github/workflows/build.yml

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
name: Build Mobile App
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
environment:
7+
required: true
8+
type: string
9+
platform:
10+
required: true
11+
type: choice
12+
options: [android, ios, both]
13+
build_type:
14+
required: true
15+
type: choice
16+
options: [main, flask]
17+
default: main
18+
workflow_dispatch:
19+
inputs:
20+
environment:
21+
required: true
22+
type: choice
23+
options: [prod, rc, exp]
24+
platform:
25+
required: true
26+
type: choice
27+
options: [android, ios, both]
28+
build_type:
29+
required: true
30+
type: choice
31+
options: [main, flask]
32+
default: main
33+
34+
jobs:
35+
load-environment-config:
36+
runs-on: ubuntu-latest
37+
outputs:
38+
config: ${{ steps.load-config.outputs.config }}
39+
requires_approval: ${{ steps.load-config.outputs.requires_approval }}
40+
github_environment: ${{ steps.load-config.outputs.github_environment }}
41+
secrets_mapping: ${{ steps.load-config.outputs.secrets_mapping }}
42+
steps:
43+
- uses: actions/checkout@v4
44+
45+
- name: Setup Node.js
46+
uses: actions/setup-node@v4
47+
with:
48+
node-version: '20'
49+
50+
- name: Install dependencies
51+
run: yarn install --immutable
52+
53+
- name: Merge and load environment configuration
54+
id: load-config
55+
uses: actions/github-script@v7
56+
with:
57+
script: |
58+
const { mergeConfigs } = require('./scripts/merge-config.js');
59+
const env = '${{ inputs.environment }}';
60+
const config = mergeConfigs(env);
61+
62+
// Extract secrets mapping
63+
const secretsMapping = JSON.stringify(config.secrets || {});
64+
65+
return {
66+
config: JSON.stringify(config),
67+
requires_approval: config.environment.requires_approval,
68+
github_environment: config.environment.github_environment,
69+
secrets_mapping: secretsMapping
70+
};
71+
72+
approval:
73+
if: needs.load-environment-config.outputs.requires_approval == 'true'
74+
needs: load-environment-config
75+
runs-on: ubuntu-latest
76+
environment: ${{ needs.load-environment-config.outputs.github_environment }}
77+
steps:
78+
- name: Wait for approval
79+
run: echo "Approval required for ${{ inputs.environment }} builds"
80+
81+
build:
82+
needs:
83+
- load-environment-config
84+
- approval
85+
if: always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped')
86+
strategy:
87+
matrix:
88+
platform: ${{ inputs.platform == 'both' && fromJSON('["android", "ios"]') || fromJSON(format('["{0}"]', inputs.platform)) }}
89+
runs-on: ${{ matrix.platform == 'ios' && 'macos-latest' || 'ubuntu-latest' }}
90+
environment: ${{ needs.load-environment-config.outputs.github_environment }}
91+
steps:
92+
- uses: actions/checkout@v4
93+
94+
- name: Setup Node.js
95+
uses: actions/setup-node@v4
96+
with:
97+
node-version: '20'
98+
cache: 'yarn'
99+
100+
- name: Install dependencies
101+
run: yarn install --immutable
102+
103+
- name: Setup project
104+
run: yarn setup:github-ci
105+
106+
- name: Apply environment configuration
107+
id: apply-config
108+
uses: actions/github-script@v7
109+
with:
110+
script: |
111+
const { mergeConfigs } = require('./scripts/merge-config.js');
112+
const env = '${{ inputs.environment }}';
113+
const config = mergeConfigs(env);
114+
115+
// Set non-secret environment variables from merged config
116+
const outputs = {};
117+
118+
// Server URLs
119+
Object.entries(config.servers).forEach(([key, value]) => {
120+
const envVar = key.toUpperCase();
121+
outputs[envVar] = value;
122+
core.exportVariable(envVar, value);
123+
});
124+
125+
// Feature flags
126+
Object.entries(config.features).forEach(([key, value]) => {
127+
const envVar = key.toUpperCase();
128+
outputs[envVar] = value.toString();
129+
core.exportVariable(envVar, value.toString());
130+
});
131+
132+
// Build configuration
133+
core.exportVariable('METAMASK_ENVIRONMENT', config.environment.name);
134+
core.exportVariable('METAMASK_BUILD_TYPE', '${{ inputs.build_type }}');
135+
136+
// Output for other steps
137+
Object.entries(outputs).forEach(([key, value]) => {
138+
core.setOutput(key.toLowerCase().replace(/_/g, '_'), value);
139+
});
140+
141+
- name: Set secrets as environment variables
142+
env:
143+
CONFIG_SECRETS: ${{ needs.load-environment-config.outputs.secrets_mapping }}
144+
run: |
145+
# Note: GitHub Actions doesn't allow dynamic secret access
146+
# Secrets must be configured in the GitHub environment and accessed directly
147+
# This step loads the mapping and sets env vars using the secrets context
148+
# For each secret in the mapping, we need to access it via ${{ secrets.SECRET_NAME }}
149+
# In a real implementation, you would need to explicitly list each secret
150+
# or use a composite action that handles this mapping
151+
echo "Secrets mapping loaded. Secrets should be configured in GitHub environment settings."
152+
echo "$CONFIG_SECRETS" | jq -r 'to_entries[] | "export \(.key)=\"${{ secrets.\(.value) }}\""' || true
153+
154+
- name: Build ${{ matrix.platform }}
155+
run: |
156+
./scripts/build.sh ${{ matrix.platform }} ${{ inputs.build_type }} ${{ inputs.environment }}

build/environments/README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Environment Configuration Management
2+
3+
This directory contains YAML configuration files for different build environments (prod, rc, exp).
4+
5+
## Structure
6+
7+
- `base.yml` - Common configuration shared across all environments
8+
- `prod/config.yml` - Production-specific overrides
9+
- `rc/config.yml` - Release Candidate-specific overrides
10+
- `exp/config.yml` - Experimental-specific overrides
11+
12+
## How It Works
13+
14+
1. **Base Configuration**: `base.yml` contains all shared configuration (servers, features, common secrets)
15+
2. **Environment Overrides**: Each environment directory contains a `config.yml` that only defines what's different
16+
3. **Merging**: The `scripts/merge-config.js` script merges base.yml with the environment-specific config.yml
17+
18+
## Usage
19+
20+
### Merge configurations
21+
22+
```bash
23+
node scripts/merge-config.js <environment>
24+
```
25+
26+
Example:
27+
```bash
28+
node scripts/merge-config.js prod
29+
```
30+
31+
This outputs the merged JSON configuration.
32+
33+
### In GitHub Actions
34+
35+
The `.github/workflows/build.yml` workflow automatically:
36+
1. Loads and merges the configuration for the specified environment
37+
2. Sets non-secret environment variables (servers, features)
38+
3. Maps secrets to GitHub secrets (requires secrets to be configured in GitHub environment settings)
39+
40+
## Secret Management
41+
42+
**Important**: GitHub Actions doesn't allow dynamic secret access for security reasons. Secrets must be:
43+
44+
1. Configured in GitHub repository/environment settings
45+
2. Explicitly referenced in the workflow using `${{ secrets.SECRET_NAME }}`
46+
47+
The configuration files map environment variable names to GitHub secret names. For example:
48+
- `MM_SENTRY_DSN: "MM_SENTRY_DSN"` means the env var `MM_SENTRY_DSN` should use the GitHub secret `MM_SENTRY_DSN`
49+
50+
To use secrets in the workflow, you'll need to either:
51+
- Explicitly list each secret in the workflow file
52+
- Use a composite action that handles the mapping
53+
- Configure secrets in the GitHub environment and access them directly
54+
55+
## Benefits
56+
57+
- **DRY**: Shared configuration defined once in `base.yml`
58+
- **Maintainable**: Update base.yml to change all environments
59+
- **Clear**: Each env config only shows what's different
60+
- **Scalable**: Easy to add new environments

build/environments/base.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Base configuration shared across all environments
2+
# Environment-specific configs will inherit and override as needed
3+
4+
# Server URLs (same for all environments)
5+
servers:
6+
portfolio_api: "https://portfolio.api.cx.metamask.io"
7+
security_alerts_api: "https://security-alerts.api.cx.metamask.io"
8+
ramp_eligibility: "https://on-ramp-content.api.cx.metamask.io"
9+
ramp_tokens: "https://on-ramp-cache.api.cx.metamask.io"
10+
auth_service: "https://auth-service.api.cx.metamask.io"
11+
rewards_api: "https://rewards.api.cx.metamask.io"
12+
decoding_api: "https://signature-insights.api.cx.metamask.io"
13+
14+
# Build configuration (same for all)
15+
build:
16+
android:
17+
package_name: "io.metamask"
18+
ios:
19+
scheme: "MetaMask"
20+
bundle_id: "io.metamask"
21+
export_method: "enterprise"
22+
23+
# Feature flags (same for all)
24+
features:
25+
bridge_use_dev_apis: false
26+
ramp_internal_build: false
27+
seedless_onboarding_enabled: true
28+
mm_notifications_ui_enabled: true
29+
mm_security_alerts_api_enabled: true
30+
31+
# Common secrets that are the same across environments
32+
# These map to the same GitHub Secret names
33+
common_secrets:
34+
MM_SENTRY_AUTH_TOKEN: "MM_SENTRY_AUTH_TOKEN"
35+
GOOGLE_SERVICES_B64_IOS: "GOOGLE_SERVICES_B64_IOS"
36+
GOOGLE_SERVICES_B64_ANDROID: "GOOGLE_SERVICES_B64_ANDROID"
37+
MM_INFURA_PROJECT_ID: "MM_INFURA_PROJECT_ID"
38+
WALLET_CONNECT_PROJECT_ID: "WALLET_CONNECT_PROJECT_ID"
39+
MM_FOX_CODE: "MM_FOX_CODE"
40+
FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN: "FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN"
41+
FEATURES_ANNOUNCEMENTS_SPACE_ID: "FEATURES_ANNOUNCEMENTS_SPACE_ID"
42+
FCM_CONFIG_API_KEY: "FCM_CONFIG_API_KEY"
43+
FCM_CONFIG_AUTH_DOMAIN: "FCM_CONFIG_AUTH_DOMAIN"
44+
FCM_CONFIG_STORAGE_BUCKET: "FCM_CONFIG_STORAGE_BUCKET"
45+
FCM_CONFIG_PROJECT_ID: "FCM_CONFIG_PROJECT_ID"
46+
FCM_CONFIG_MESSAGING_SENDER_ID: "FCM_CONFIG_MESSAGING_SENDER_ID"
47+
FCM_CONFIG_APP_ID: "FCM_CONFIG_APP_ID"
48+
FCM_CONFIG_MEASUREMENT_ID: "FCM_CONFIG_MEASUREMENT_ID"
49+
QUICKNODE_MAINNET_URL: "QUICKNODE_MAINNET_URL"
50+
QUICKNODE_ARBITRUM_URL: "QUICKNODE_ARBITRUM_URL"
51+
QUICKNODE_AVALANCHE_URL: "QUICKNODE_AVALANCHE_URL"
52+
QUICKNODE_BASE_URL: "QUICKNODE_BASE_URL"
53+
QUICKNODE_LINEA_MAINNET_URL: "QUICKNODE_LINEA_MAINNET_URL"
54+
QUICKNODE_MONAD_URL: "QUICKNODE_MONAD_URL"
55+
QUICKNODE_OPTIMISM_URL: "QUICKNODE_OPTIMISM_URL"
56+
QUICKNODE_POLYGON_URL: "QUICKNODE_POLYGON_URL"

build/environments/exp/config.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Extends base.yml - only define what's different
2+
3+
environment:
4+
name: exp
5+
display_name: Experimental
6+
requires_approval: false
7+
approval_team: null
8+
github_environment: build-exp
9+
10+
# Override build config for Exp
11+
build:
12+
android:
13+
keystore: "mainExp"
14+
signing_config: "mainExp"
15+
16+
# Exp-specific secrets (extends common_secrets from base)
17+
secrets:
18+
# Segment/Analytics - QA secrets for Exp
19+
SEGMENT_WRITE_KEY: "SEGMENT_WRITE_KEY_QA"
20+
SEGMENT_PROXY_URL: "SEGMENT_PROXY_URL_QA"
21+
SEGMENT_DELETE_API_SOURCE_ID: "SEGMENT_DELETE_API_SOURCE_ID_QA"
22+
SEGMENT_REGULATIONS_ENDPOINT: "SEGMENT_REGULATIONS_ENDPOINT_QA"
23+
24+
# Sentry
25+
MM_SENTRY_DSN: "MM_SENTRY_DSN_TEST"
26+
27+
# OAuth - UAT secrets for Exp (Main)
28+
IOS_GOOGLE_CLIENT_ID: "MAIN_IOS_GOOGLE_CLIENT_ID_UAT"
29+
IOS_GOOGLE_REDIRECT_URI: "MAIN_IOS_GOOGLE_REDIRECT_URI_UAT"
30+
ANDROID_APPLE_CLIENT_ID: "MAIN_ANDROID_APPLE_CLIENT_ID_UAT"
31+
ANDROID_GOOGLE_CLIENT_ID: "MAIN_ANDROID_GOOGLE_CLIENT_ID_UAT"
32+
ANDROID_GOOGLE_SERVER_CLIENT_ID: "MAIN_ANDROID_GOOGLE_SERVER_CLIENT_ID_UAT"
33+
34+
# Web3Auth
35+
WEB3AUTH_NETWORK: "MAIN_WEB3AUTH_NETWORK_PROD"
36+
37+
# Other Exp-specific
38+
MM_CARD_BAANX_API_CLIENT_KEY: "MM_CARD_BAANX_API_CLIENT_KEY_UAT"

build/environments/prod/config.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Extends base.yml - only define what's different
2+
3+
environment:
4+
name: production
5+
display_name: Production
6+
requires_approval: true
7+
approval_team: mobile-platform
8+
github_environment: build-production
9+
10+
# Override build config for production
11+
build:
12+
android:
13+
keystore: "mainProd"
14+
signing_config: "mainProd"
15+
package_name: "io.metamask" # Inherited from base, but explicit here
16+
17+
# Production-specific secrets (extends common_secrets from base)
18+
secrets:
19+
# Segment/Analytics - Production
20+
SEGMENT_WRITE_KEY: "SEGMENT_WRITE_KEY"
21+
SEGMENT_PROXY_URL: "SEGMENT_PROXY_URL"
22+
SEGMENT_DELETE_API_SOURCE_ID: "SEGMENT_DELETE_API_SOURCE_ID"
23+
SEGMENT_REGULATIONS_ENDPOINT: "SEGMENT_REGULATIONS_ENDPOINT"
24+
25+
# Sentry
26+
MM_SENTRY_DSN: "MM_SENTRY_DSN"
27+
28+
# OAuth - Production (Main)
29+
IOS_GOOGLE_CLIENT_ID: "IOS_GOOGLE_CLIENT_ID"
30+
IOS_GOOGLE_REDIRECT_URI: "IOS_GOOGLE_REDIRECT_URI"
31+
ANDROID_APPLE_CLIENT_ID: "ANDROID_APPLE_CLIENT_ID"
32+
ANDROID_GOOGLE_CLIENT_ID: "ANDROID_GOOGLE_CLIENT_ID"
33+
ANDROID_GOOGLE_SERVER_CLIENT_ID: "ANDROID_GOOGLE_SERVER_CLIENT_ID"
34+
35+
# Other production-specific
36+
MM_BRANCH_KEY_LIVE: "MM_BRANCH_KEY_LIVE"
37+
MM_CARD_BAANX_API_CLIENT_KEY: "MM_CARD_BAANX_API_CLIENT_KEY"

build/environments/rc/config.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Extends base.yml - only define what's different
2+
3+
environment:
4+
name: rc
5+
display_name: Release Candidate
6+
requires_approval: false
7+
approval_team: null
8+
github_environment: build-rc
9+
10+
# Override build config for RC
11+
build:
12+
android:
13+
keystore: "mainRc"
14+
signing_config: "mainRc"
15+
16+
# RC-specific secrets (extends common_secrets from base)
17+
secrets:
18+
# Segment/Analytics - QA secrets for RC
19+
SEGMENT_WRITE_KEY: "SEGMENT_WRITE_KEY_QA"
20+
SEGMENT_PROXY_URL: "SEGMENT_PROXY_URL_QA"
21+
SEGMENT_DELETE_API_SOURCE_ID: "SEGMENT_DELETE_API_SOURCE_ID_QA"
22+
SEGMENT_REGULATIONS_ENDPOINT: "SEGMENT_REGULATIONS_ENDPOINT_QA"
23+
24+
# Sentry
25+
MM_SENTRY_DSN: "MM_SENTRY_DSN_TEST"
26+
27+
# OAuth - Production secrets for RC (Main)
28+
IOS_GOOGLE_CLIENT_ID: "MAIN_IOS_GOOGLE_CLIENT_ID_PROD"
29+
IOS_GOOGLE_REDIRECT_URI: "MAIN_IOS_GOOGLE_REDIRECT_URI_PROD"
30+
ANDROID_APPLE_CLIENT_ID: "MAIN_ANDROID_APPLE_CLIENT_ID_PROD"
31+
ANDROID_GOOGLE_CLIENT_ID: "MAIN_ANDROID_GOOGLE_CLIENT_ID_PROD"
32+
ANDROID_GOOGLE_SERVER_CLIENT_ID: "MAIN_ANDROID_GOOGLE_SERVER_CLIENT_ID_PROD"
33+
34+
# Other RC-specific
35+
MM_CARD_BAANX_API_CLIENT_KEY: "MM_CARD_BAANX_API_CLIENT_KEY_PROD"

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,7 @@
599599
"jest": "^29.7.0",
600600
"jest-junit": "^15.0.0",
601601
"jetifier": "2.0.0",
602+
"js-yaml": "^4.1.0",
602603
"koa": "^2.14.2",
603604
"lint-staged": "10.5.4",
604605
"listr2": "^8.0.2",

0 commit comments

Comments
 (0)