Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ app/core/DeeplinkManager @MetaMask/mobile-pla
scripts/build.sh @MetaMask/mobile-platform
fingerprint.config.js @MetaMask/mobile-platform
builds.yml @MetaMask/mobile-platform
.github/workflows/create-build-branch.yml @MetaMask/mobile-platform
.github/workflows/push-eas-update.yml @MetaMask/mobile-admins
.github/workflows/upload-to-testflight.yml @MetaMask/mobile-admins
scripts/update-expo-channel.js @MetaMask/mobile-admins
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/create-build-branch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Create Build Branch

# Reusable workflow that creates an ephemeral build/<source>-<timestamp> branch
# from a source ref. This avoids pushing version-bump commits directly to
# protected branches (e.g. main).
#
# The caller is responsible for deleting the branch after use.

on:
workflow_call:
inputs:
source_branch:
description: 'Branch, tag, or SHA to branch from'
required: true
type: string
outputs:
build_branch:
description: 'Name of the created ephemeral build branch'
value: ${{ jobs.create.outputs.branch_name }}

jobs:
create:
name: Create build branch
runs-on: ubuntu-latest
outputs:
branch_name: ${{ steps.create-branch.outputs.branch_name }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.source_branch }}
token: ${{ secrets.PR_TOKEN || github.token }}
- name: Create temporary build branch
id: create-branch
env:
SOURCE_BRANCH: ${{ inputs.source_branch }}
run: |
TIMESTAMP=$(date +%s%3N)
SANITIZED=$(echo "$SOURCE_BRANCH" | tr '/' '-')
BRANCH_NAME="build/${SANITIZED}-${TIMESTAMP}"
git checkout -b "$BRANCH_NAME"
git push origin "$BRANCH_NAME"
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
echo "✅ Created build branch: $BRANCH_NAME"
105 changes: 105 additions & 0 deletions .github/workflows/nightly-build-temp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: Nightly Build (temp)

# Runs on a schedule (4 AM UTC) like the old nightly-temp-branch-sync.
# Each build creates its own ephemeral branch via create-build-branch.yml,
# so the persistent chore/temp-nightly branch is no longer needed.
#
# iOS builds are handled end-to-end by upload-to-testflight-temp.yml (build + upload).
# Android builds use create-build-branch.yml + build.yml (build artifacts only).
#
# rc depends on exp within each platform stream to ensure the external version
# service produces sequential numbers (N for exp, N+1 for rc).
# iOS and Android streams run in parallel; their version numbers will differ
# but remain unique, which is all TestFlight / Play Store require.

on:
schedule:
# NOTE: Scheduled workflows ALWAYS run from the default branch (main)
- cron: '0 4 * * *'
workflow_dispatch:

permissions:
contents: write
id-token: write

jobs:
# ── iOS exp: build + TestFlight upload ─────────────────────────────────
ios-exp:
name: Nightly iOS exp
uses: ./.github/workflows/upload-to-testflight-temp.yml
with:
source_branch: main
environment: exp
testflight_group: 'MetaMask BETA & Release Candidates'
secrets: inherit

# ── iOS rc: build + TestFlight upload (after exp for sequential versions) ─
ios-rc:
name: Nightly iOS rc
needs: [ios-exp]
uses: ./.github/workflows/upload-to-testflight-temp.yml
with:
source_branch: main
environment: rc
testflight_group: 'MetaMask BETA & Release Candidates'
secrets: inherit

# ── Android exp: ephemeral branch + build ──────────────────────────────
android-exp-branch:
uses: ./.github/workflows/create-build-branch.yml
with:
source_branch: main
secrets: inherit

android-exp:
name: Nightly Android exp
needs: [android-exp-branch]
uses: ./.github/workflows/build.yml
with:
build_name: main-exp
platform: android
skip_version_bump: false
source_branch: ${{ needs.android-exp-branch.outputs.build_branch }}
secrets: inherit

# ── Android rc: ephemeral branch + build (after exp for sequential versions) ─
android-rc-branch:
needs: [android-exp]
uses: ./.github/workflows/create-build-branch.yml
with:
source_branch: main
secrets: inherit

android-rc:
name: Nightly Android rc
needs: [android-rc-branch]
uses: ./.github/workflows/build.yml
with:
build_name: main-rc
platform: android
skip_version_bump: false
source_branch: ${{ needs.android-rc-branch.outputs.build_branch }}
secrets: inherit

# ── Cleanup Android ephemeral branches ─────────────────────────────────
# iOS branches are cleaned up by upload-to-testflight-temp.yml internally.
cleanup:
name: Cleanup Android build branches
needs: [android-exp-branch, android-rc-branch, android-exp, android-rc]
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.PR_TOKEN || github.token }}
- name: Delete ephemeral branches
env:
EXP_BRANCH: ${{ needs.android-exp-branch.outputs.build_branch }}
RC_BRANCH: ${{ needs.android-rc-branch.outputs.build_branch }}
run: |
for branch in "$EXP_BRANCH" "$RC_BRANCH"; do
if [ -n "$branch" ]; then
git push origin --delete "$branch" || true
echo "🧹 Deleted: $branch"
fi
done
217 changes: 217 additions & 0 deletions .github/workflows/upload-to-testflight-temp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
name: Upload to TestFlight (temp)

# Dedicated workflow to build iOS (via build.yml) and upload to TestFlight.
# All TestFlight logic lives here; build.yml only builds and uploads artifacts.
#
on:
workflow_call:
inputs:
source_branch:
description: 'Branch, tag, or SHA to build'
required: true
type: string
environment:
description: 'Build environment / track (exp, beta, rc)'
required: true
type: string
testflight_group:
description: 'TestFlight external testing group'
required: false
type: string
default: 'MetaMask BETA & Release Candidates'
workflow_dispatch:
inputs:
source_branch:
description: 'Branch, tag, or SHA to build'
required: true
type: string
default: 'main'
environment:
description: 'Build environment / track'
required: true
type: choice
options:
- exp
- beta
- rc
default: rc
testflight_group:
description: 'TestFlight external testing group'
required: true
type: choice
default: 'MetaMask BETA & Release Candidates'
options:
- 'MetaMask BETA & Release Candidates'
- 'MM Card Team'
- 'Ramp Provider Testing'

# contents: write required by build.yml update-build-version job (version bump)
permissions:
contents: write
id-token: write

jobs:
# Create a temporary branch so the version-bump commit can be pushed without
# hitting branch-protection rules on the source branch (e.g. main).
prepare-build-branch:
uses: ./.github/workflows/create-build-branch.yml
with:
source_branch: ${{ inputs.source_branch }}
secrets: inherit

build:
name: Build iOS (${{ inputs.environment || 'rc' }})
needs: [prepare-build-branch]
uses: ./.github/workflows/build.yml
with:
build_name: main-${{ inputs.environment || 'rc' }}
platform: ios
skip_version_bump: false
source_branch: ${{ needs.prepare-build-branch.outputs.build_branch }}
secrets: inherit

testflight-upload-summary:
name: TestFlight upload summary
needs: [build, prepare-build-branch]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ needs.prepare-build-branch.outputs.build_branch }}
- name: Display TestFlight upload summary
run: |
BUILD_VERSION=$(node -p "require('./package.json').version")
BUILD_NUMBER=$(awk '/versionCode/{print $2}' android/app/build.gradle)
{
echo "### 📲 TestFlight Upload"
echo ""
echo "| Field | Value |"
echo "| --- | --- |"
echo "| **Source branch** | \`${{ inputs.source_branch }}\` |"
echo "| **Build branch** | \`${{ needs.prepare-build-branch.outputs.build_branch }}\` |"
echo "| **Build name** | \`main-${{ inputs.environment || 'rc' }}\` |"
echo "| **Build version** | \`${BUILD_VERSION}\` |"
echo "| **Build number** | \`${BUILD_NUMBER}\` |"
echo "| **TestFlight group** | ${{ inputs.testflight_group || 'MetaMask BETA & Release Candidates' }} |"
echo "| **Workflow ref** | \`${{ github.ref_name }}\` (required for AWS) |"
} >> "$GITHUB_STEP_SUMMARY"

# Pulls App Store Connect API keys from AWS Secrets Manager (OIDC).
# Workflow must run from main; build uses the temporary build branch.
upload-ios-testflight:
name: Upload iOS to TestFlight
needs: [build, testflight-upload-summary]
runs-on: ghcr.io/cirruslabs/macos-runner:sequoia-xl
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Ruby (iOS)
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb #v1
with:
ruby-version: '3.2.9'
working-directory: ios
bundler-cache: true

- name: Download iOS IPA artifact
uses: actions/download-artifact@v4
with:
name: ios-ipa-main-${{ inputs.environment || 'rc' }}

- name: Find IPA path
id: ipa
run: |
IPA=$(find . -name '*.ipa' -type f | head -1)
if [ -z "$IPA" ]; then
echo "::error::No .ipa file found in artifact"
exit 1
fi
case "$IPA" in /*) ABS="$IPA" ;; *) ABS="$PWD/$IPA" ;; esac
echo "path=$ABS" >> "$GITHUB_OUTPUT"

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_APPLE_TESTFLIGHT }}
aws-region: 'us-east-2'

- name: Fetch Apple API keys from AWS Secrets Manager
run: |
echo "🔐 Fetching App Store Connect API keys from Secrets Manager..."
secret_id="metamask-mobile-main-apple-api-keys"
secret_json=$(aws secretsmanager get-secret-value \
--region 'us-east-2' \
--secret-id "$secret_id" \
--query SecretString \
--output text)

for key in APP_STORE_CONNECT_API_KEY_ISSUER_ID APP_STORE_CONNECT_API_KEY_KEY_ID; do
value=$(echo "$secret_json" | jq -r --arg k "$key" '.[$k] // empty')
if [ -z "$value" ]; then
echo "::error::Missing key in secret: $key"
exit 1
fi
echo "::add-mask::$value"
echo "${key}=${value}" >> "$GITHUB_ENV"
done

key=APP_STORE_CONNECT_API_KEY_KEY_CONTENT
value=$(echo "$secret_json" | jq -r --arg k "$key" '.[$k] // empty')
if [ -z "$value" ]; then
echo "::error::Missing key in secret: $key"
exit 1
fi
while IFS= read -r line || [ -n "$line" ]; do
[ -n "$line" ] && echo "::add-mask::$line"
done <<< "$(printf '%s\n' "$value")"

delim="APPLEP8$(openssl rand -hex 16)"
{
printf '%s<<%s\n' "$key" "$delim"
printf '%s\n' "$value"
printf '%s\n' "$delim"
} >> "$GITHUB_ENV"

echo "✅ Apple API keys loaded from AWS"

- name: Setup App Store Connect API Key
run: |
bash scripts/setup-app-store-connect-api-key.sh \
"$APP_STORE_CONNECT_API_KEY_ISSUER_ID" \
"$APP_STORE_CONNECT_API_KEY_KEY_ID" \
"$APP_STORE_CONNECT_API_KEY_KEY_CONTENT"

- name: Upload to TestFlight
run: |
bash scripts/upload-to-testflight.sh \
"github_actions_main-${{ inputs.environment || 'rc' }}" \
"${{ inputs.source_branch }}" \
"${{ steps.ipa.outputs.path }}" \
"${{ inputs.testflight_group || 'MetaMask BETA & Release Candidates' }}"

- name: Cleanup API Key
if: always()
run: |
rm -f ios/AuthKey.p8
echo "🧹 Cleaned up API key file"

cleanup-build-branch:
name: Cleanup build branch
needs: [prepare-build-branch, upload-ios-testflight]
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.PR_TOKEN || github.token }}
- name: Delete temporary build branch
env:
BRANCH: ${{ needs.prepare-build-branch.outputs.build_branch }}
run: |
if [ -n "$BRANCH" ]; then
git push origin --delete "$BRANCH" || true
echo "🧹 Deleted build branch: $BRANCH"
fi
Loading