|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +set -euo pipefail |
| 4 | + |
| 5 | +# Install the latest expo-dev-build Android APK from GitHub Actions artifacts. |
| 6 | +# Replaces the Runway-bucket flow with a direct `gh run download` from |
| 7 | +# .github/workflows/expo-dev-build.yml. |
| 8 | +# |
| 9 | +# The Android artifact uploads a single APK (no zipping required), so we just |
| 10 | +# copy it to a stable path under build/. |
| 11 | +# |
| 12 | +# Verify step by step: |
| 13 | +# Step 1 (Resolve): Run with --skipInstall and watch the resolved run id / artifact size. |
| 14 | +# Step 2 (Download): Inspect build/gh-expo-dev-build/android/*.apk and build/app-prod-debug.apk. |
| 15 | +# Step 3 (Install): Run with --skip-download to install build/app-prod-debug.apk on a booted emulator. |
| 16 | + |
| 17 | +RED='\033[0;31m' |
| 18 | +GREEN='\033[0;32m' |
| 19 | +YELLOW='\033[1;33m' |
| 20 | +BLUE='\033[0;34m' |
| 21 | +NC='\033[0m' |
| 22 | + |
| 23 | +REPO="MetaMask/metamask-mobile" |
| 24 | +WORKFLOW="expo-dev-build.yml" |
| 25 | +ARTIFACT_NAME="android-apk-main-dev-expo" |
| 26 | +PACKAGE_ID="io.metamask" |
| 27 | + |
| 28 | +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 29 | +readonly REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" |
| 30 | +readonly BUILD_DIR="$REPO_ROOT/build" |
| 31 | +readonly DOWNLOAD_DIR="$BUILD_DIR/gh-expo-dev-build/android" |
| 32 | +readonly STABLE_APK="$BUILD_DIR/app-prod-debug.apk" |
| 33 | + |
| 34 | +if [[ "$(pwd)" != "$REPO_ROOT" ]]; then |
| 35 | + echo -e "${RED}❌ This script must be run from the repository root${NC}" |
| 36 | + echo -e "${YELLOW}Current directory: $(pwd)${NC}" |
| 37 | + echo -e "${YELLOW}Expected directory: $REPO_ROOT${NC}" |
| 38 | + echo -e "${YELLOW}Run: cd $REPO_ROOT && yarn install:android:gh-expo-dev${NC}" |
| 39 | + exit 1 |
| 40 | +fi |
| 41 | + |
| 42 | +UNINSTALL=false |
| 43 | +SKIP_DOWNLOAD=false |
| 44 | +SKIP_INSTALL=false |
| 45 | +BRANCH="main" |
| 46 | +RUN_ID="" |
| 47 | + |
| 48 | +DOWNLOAD_SUCCESS=false |
| 49 | + |
| 50 | +cleanup() { |
| 51 | + if [[ "$DOWNLOAD_SUCCESS" == true && -d "$DOWNLOAD_DIR" && "$DOWNLOAD_DIR" == "$BUILD_DIR"/* ]]; then |
| 52 | + rm -rf "$DOWNLOAD_DIR" |
| 53 | + fi |
| 54 | +} |
| 55 | +trap cleanup EXIT |
| 56 | + |
| 57 | +while [[ $# -gt 0 ]]; do |
| 58 | + case $1 in |
| 59 | + --uninstall) |
| 60 | + UNINSTALL=true |
| 61 | + shift |
| 62 | + ;; |
| 63 | + --skip-download) |
| 64 | + SKIP_DOWNLOAD=true |
| 65 | + shift |
| 66 | + ;; |
| 67 | + --skipInstall) |
| 68 | + SKIP_INSTALL=true |
| 69 | + shift |
| 70 | + ;; |
| 71 | + --branch) |
| 72 | + if [[ -z "${2:-}" ]]; then |
| 73 | + echo -e "${RED}❌ --branch requires a value${NC}" |
| 74 | + exit 1 |
| 75 | + fi |
| 76 | + BRANCH="$2" |
| 77 | + shift 2 |
| 78 | + ;; |
| 79 | + --run) |
| 80 | + if [[ -z "${2:-}" ]]; then |
| 81 | + echo -e "${RED}❌ --run requires a value${NC}" |
| 82 | + exit 1 |
| 83 | + fi |
| 84 | + RUN_ID="$2" |
| 85 | + shift 2 |
| 86 | + ;; |
| 87 | + *) |
| 88 | + echo -e "${RED}Unknown option: $1${NC}" |
| 89 | + echo "Usage: $0 [--skip-download] [--skipInstall] [--uninstall] [--branch <name>] [--run <id>]" |
| 90 | + exit 1 |
| 91 | + ;; |
| 92 | + esac |
| 93 | +done |
| 94 | + |
| 95 | +require_cmd() { |
| 96 | + if ! command -v "$1" &> /dev/null; then |
| 97 | + echo -e "${RED}❌ $1 is required but not installed${NC}" |
| 98 | + echo -e "${YELLOW}$2${NC}" |
| 99 | + exit 1 |
| 100 | + fi |
| 101 | +} |
| 102 | + |
| 103 | +download_latest_app() { |
| 104 | + echo -e "${BLUE}━━━ Step 1: Resolving expo-dev-build run ━━━${NC}" |
| 105 | + |
| 106 | + require_cmd gh "Install with: brew install gh (then run: gh auth login)" |
| 107 | + require_cmd jq "Install with: brew install jq" |
| 108 | + |
| 109 | + if ! gh auth status &> /dev/null; then |
| 110 | + echo -e "${RED}❌ gh CLI is not authenticated${NC}" |
| 111 | + echo -e "${YELLOW}Run: gh auth login${NC}" |
| 112 | + exit 1 |
| 113 | + fi |
| 114 | + |
| 115 | + if [[ -n "$RUN_ID" ]]; then |
| 116 | + if [[ ! "$RUN_ID" =~ ^[0-9]+$ ]]; then |
| 117 | + echo -e "${RED}❌ Invalid run id: $RUN_ID (must be numeric)${NC}" |
| 118 | + exit 1 |
| 119 | + fi |
| 120 | + echo -e "${GREEN}✓ Using explicit run id: $RUN_ID${NC}" |
| 121 | + else |
| 122 | + echo -e "${BLUE}Looking up latest successful run on '$BRANCH' for $WORKFLOW...${NC}" |
| 123 | + RUN_ID=$(gh run list \ |
| 124 | + --repo "$REPO" \ |
| 125 | + --workflow "$WORKFLOW" \ |
| 126 | + --branch "$BRANCH" \ |
| 127 | + --status success \ |
| 128 | + --limit 1 \ |
| 129 | + --json databaseId \ |
| 130 | + --jq '.[0].databaseId' || true) |
| 131 | + |
| 132 | + if [[ -z "$RUN_ID" || "$RUN_ID" == "null" ]]; then |
| 133 | + echo -e "${RED}❌ No successful '$WORKFLOW' runs found on branch '$BRANCH'${NC}" |
| 134 | + echo -e "${YELLOW}Trigger one via the workflow_dispatch UI or push to '$BRANCH', or pass --run <id>.${NC}" |
| 135 | + exit 1 |
| 136 | + fi |
| 137 | + echo -e "${GREEN}✓ Latest successful run id: $RUN_ID${NC}" |
| 138 | + fi |
| 139 | + |
| 140 | + echo -e "${BLUE}━━━ Step 2: Looking up artifact '$ARTIFACT_NAME' ━━━${NC}" |
| 141 | + local artifact_info |
| 142 | + artifact_info=$(gh api "repos/$REPO/actions/runs/$RUN_ID/artifacts" --paginate \ |
| 143 | + --jq ".artifacts[] | select(.name==\"$ARTIFACT_NAME\")" || true) |
| 144 | + |
| 145 | + if [[ -z "$artifact_info" ]]; then |
| 146 | + echo -e "${RED}❌ Artifact '$ARTIFACT_NAME' not found in run $RUN_ID${NC}" |
| 147 | + echo -e "${YELLOW}Inspect: https://github.com/$REPO/actions/runs/$RUN_ID${NC}" |
| 148 | + exit 1 |
| 149 | + fi |
| 150 | + |
| 151 | + local expired |
| 152 | + expired=$(echo "$artifact_info" | jq -r '.expired') |
| 153 | + if [[ "$expired" == "true" ]]; then |
| 154 | + echo -e "${RED}❌ Artifact '$ARTIFACT_NAME' has expired for run $RUN_ID${NC}" |
| 155 | + echo -e "${YELLOW}Re-run the workflow or pass --run <id> for a newer run.${NC}" |
| 156 | + exit 1 |
| 157 | + fi |
| 158 | + |
| 159 | + local artifact_size_bytes |
| 160 | + artifact_size_bytes=$(echo "$artifact_info" | jq -r '.size_in_bytes') |
| 161 | + local artifact_size_mb=$((artifact_size_bytes / 1024 / 1024)) |
| 162 | + echo -e "${GREEN}✓ Artifact size: ${artifact_size_mb}MB${NC}" |
| 163 | + echo -e "${BLUE}🔗 https://github.com/$REPO/actions/runs/$RUN_ID${NC}" |
| 164 | + |
| 165 | + echo -e "${BLUE}━━━ Step 3: Downloading $ARTIFACT_NAME ━━━${NC}" |
| 166 | + rm -rf "$DOWNLOAD_DIR" |
| 167 | + mkdir -p "$DOWNLOAD_DIR" |
| 168 | + |
| 169 | + if ! gh run download "$RUN_ID" --repo "$REPO" --name "$ARTIFACT_NAME" --dir "$DOWNLOAD_DIR"; then |
| 170 | + echo -e "${RED}❌ Failed to download artifact${NC}" |
| 171 | + exit 1 |
| 172 | + fi |
| 173 | + |
| 174 | + # The Android artifact is uploaded as a single .apk (no zip), so we expect exactly one file. |
| 175 | + local apk_count |
| 176 | + apk_count=$(find "$DOWNLOAD_DIR" -maxdepth 1 -type f -name "*.apk" 2>/dev/null | wc -l | tr -d ' ') |
| 177 | + if [[ "$apk_count" != "1" ]]; then |
| 178 | + echo -e "${RED}❌ Expected exactly one .apk in $DOWNLOAD_DIR, found $apk_count${NC}" |
| 179 | + find "$DOWNLOAD_DIR" -maxdepth 2 -type f |
| 180 | + exit 1 |
| 181 | + fi |
| 182 | + |
| 183 | + local downloaded_apk |
| 184 | + downloaded_apk=$(find "$DOWNLOAD_DIR" -maxdepth 1 -type f -name "*.apk" | head -1) |
| 185 | + echo -e "${GREEN}✓ Downloaded: $downloaded_apk${NC}" |
| 186 | + |
| 187 | + echo -e "${BLUE}━━━ Step 4: Copying to stable path ━━━${NC}" |
| 188 | + cp -f "$downloaded_apk" "$STABLE_APK" |
| 189 | + echo -e "${GREEN}✓ Copied to $STABLE_APK${NC}" |
| 190 | + DOWNLOAD_SUCCESS=true |
| 191 | +} |
| 192 | + |
| 193 | +if [[ "$SKIP_DOWNLOAD" == false ]]; then |
| 194 | + download_latest_app |
| 195 | +fi |
| 196 | + |
| 197 | +if [[ "$SKIP_INSTALL" == true ]]; then |
| 198 | + echo -e "${GREEN}✓ Download complete. Installation skipped (--skipInstall). APK at $STABLE_APK${NC}" |
| 199 | + exit 0 |
| 200 | +fi |
| 201 | + |
| 202 | +echo -e "${BLUE}━━━ Step 5: Installing on emulator/device ━━━${NC}" |
| 203 | + |
| 204 | +require_cmd adb "Ensure Android SDK platform-tools are on PATH." |
| 205 | + |
| 206 | +BOOTED_DEVICES=$(adb devices | grep -E '^[^ ]+\s+device$' | awk '{print $1}' || true) |
| 207 | +if [[ -z "$BOOTED_DEVICES" ]]; then |
| 208 | + echo -e "${RED}❌ No emulator/device in 'device' state.${NC}" |
| 209 | + echo -e "${YELLOW}Start an Android emulator and run: adb devices${NC}" |
| 210 | + exit 1 |
| 211 | +fi |
| 212 | + |
| 213 | +echo -e "${GREEN}✓ Device(s):${NC}" |
| 214 | +echo "$BOOTED_DEVICES" | while read -r dev; do echo " $dev"; done |
| 215 | + |
| 216 | +if [[ ! -f "$STABLE_APK" ]]; then |
| 217 | + echo -e "${RED}❌ No APK found at $STABLE_APK${NC}" |
| 218 | + echo -e "${YELLOW}Run without --skip-download to download first${NC}" |
| 219 | + exit 1 |
| 220 | +fi |
| 221 | + |
| 222 | +if [[ "$UNINSTALL" == true ]]; then |
| 223 | + echo -e "${YELLOW}Uninstalling existing app...${NC}" |
| 224 | + adb uninstall "$PACKAGE_ID" 2>/dev/null || true |
| 225 | + echo -e "${GREEN}✓ Uninstall complete${NC}" |
| 226 | +fi |
| 227 | + |
| 228 | +echo -e "${BLUE}Installing APK on device...${NC}" |
| 229 | +set +e |
| 230 | +INSTALL_OUTPUT=$(adb install -r "$STABLE_APK" 2>&1) |
| 231 | +INSTALL_EXIT=$? |
| 232 | +set -e |
| 233 | + |
| 234 | +echo "$INSTALL_OUTPUT" |
| 235 | +if [[ $INSTALL_EXIT -ne 0 ]]; then |
| 236 | + if [[ "$INSTALL_OUTPUT" == *"INSTALL_FAILED_UPDATE_INCOMPATIBLE"* ]]; then |
| 237 | + echo -e "${YELLOW}Existing app was signed with a different key. Uninstall it first:${NC}" |
| 238 | + echo -e " yarn install:android:gh-expo-dev --skip-download --uninstall" |
| 239 | + fi |
| 240 | + exit $INSTALL_EXIT |
| 241 | +fi |
| 242 | + |
| 243 | +echo -e "${GREEN}✓ Successfully installed app on emulator/device.${NC}" |
0 commit comments