Skip to content

Feature/local build workflow setup#12

Merged
digitalnomad91 merged 8 commits intomainfrom
feature/local-build-workflow-setup
Feb 26, 2026
Merged

Feature/local build workflow setup#12
digitalnomad91 merged 8 commits intomainfrom
feature/local-build-workflow-setup

Conversation

@digitalnomad91
Copy link
Copy Markdown
Member

No description provided.

- Create local-android-build.yml workflow for building APKs without Expo.dev
- Builds directly using Gradle on self-hosted runner (your local machine)
- Generates GitHub releases with APK artifacts
- Includes automatic versioning and changelog generation
- Add comprehensive setup & troubleshooting documentation

This allows building APKs without hitting Expo.dev build limits or paying
monthly subscription fees. Uses your own machine as a build runner.

Documentation:
- LOCAL_BUILD_SETUP.md - Complete setup guide with prerequisites
- LOCAL_BUILD_QUICKSTART.md - Quick reference (5-step start)
- LOCAL_BUILD_TROUBLESHOOTING.md - 15+ common issues & solutions
- IMPLEMENTATION_SUMMARY.md - Overview & architecture

Workflow features:
✅ No Expo.dev charges
✅ Full version management
✅ Automatic GitHub releases
✅ Pre-release builds for dev branches
✅ Build metadata tracking
✅ 14-day artifact retention
- System architecture diagram showing data flow
- Runner setup and configuration overview
- Build process timeline and storage info
- Comparison matrices (local vs EAS)
- Decision tree for workflow selection
- Quick reference card for common tasks

Helps visualize the entire system setup and understand how the local
build workflow integrates with GitHub Actions and your machine.
- Quick start guide with immediate action items
- Step-by-step setup for GitHub token and runner
- Build verification and APK installation
- Cost comparison (save -960/year)
- Quick troubleshooting reference
- Example workflow diagram

This is the entry point for users implementing the local build workflow.
Maps all documentation files
Quick start checklist
Cost analysis and comparison
Workflow overview with diagrams
Support and troubleshooting reference

This serves as the main entry point for understanding
and using the entire local build system.
- Fix markdown formatting in LOCAL_BUILD_TROUBLESHOOTING.md
  • Add blank lines before code blocks for better readability
  • Normalize yaml indentation for consistency
  • Improve section spacing

- Add ADB + Expo device management scripts
  • scripts/adb-connect.sh - Full-featured device/emulator selector
    - Handles USB devices and Android Virtual Devices
    - Cleans up offline emulators automatically
    - Launches Expo targeting selected device

  • scripts/adb-connect2.sh - Simplified WSL-safe version
    - Lighter implementation without complex pipes
    - Better bash compatibility
    - Easier to debug and modify

These scripts simplify the workflow of selecting a device and running
Expo dev client, especially useful with the new local build setup.
Copilot AI review requested due to automatic review settings February 26, 2026 00:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a local Android build workflow setup that enables building APKs directly on a self-hosted runner instead of using Expo.dev's cloud build service. The PR aims to provide a cost-effective alternative for Android app builds when developers hit Expo's build limits or want to avoid subscription fees.

Changes:

  • Adds a new GitHub Actions workflow (.github/workflows/local-android-build.yml) for building Android APKs on a self-hosted runner
  • Includes two ADB connection scripts for managing Android device connections and launching Expo builds
  • Provides comprehensive documentation across 6 files covering setup, troubleshooting, architecture, and quick references

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
.github/workflows/local-android-build.yml New self-hosted workflow for local Android builds using Gradle
scripts/adb-connect.sh Script for ADB device management and Expo launching (referenced in package.json)
scripts/adb-connect2.sh Alternative/duplicate ADB script (not referenced anywhere)
GETTING_STARTED.md Quick start guide with 5-step setup process
README_LOCAL_BUILD.md Main overview document with cost comparisons and file references
LOCAL_BUILD_SETUP.md Detailed setup guide with prerequisites and troubleshooting
LOCAL_BUILD_QUICKSTART.md Quick reference card for common commands
LOCAL_BUILD_TROUBLESHOOTING.md Solutions for 15+ common issues
LOCAL_BUILD_ARCHITECTURE.md Visual diagrams and architecture documentation
IMPLEMENTATION_SUMMARY.md Features overview and comparison with EAS builds

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- name: "💾 Commit Version Update"
if: github.ref == 'refs/heads/main'
run: |
git add version.json
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of || true here differs from the existing eas-android-build.yml workflow (lines 71-72), which does not suppress errors. While adding || true is actually a good defensive practice (prevents workflow failure if commit/push fails due to no changes or merge conflicts), this inconsistency means the two workflows handle the same operations differently. Consider updating the existing workflow to match this error handling, or document why they should behave differently.

Suggested change
git add version.json
git add version.json
# Use `|| true` to avoid failing the build when there are no changes to commit
# or when pushing fails (e.g. non-fast-forward). This workflow treats the
# version bump as best-effort so the APK build still succeeds even if these
# Git operations fail. Note: this is intentionally more lenient than the
# behavior in eas-android-build.yml.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +10
branches: ["**"]
paths:
- '.github/workflows/local-android-build.yml' # Allow manual re-runs of this workflow
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow is configured to trigger only when the workflow file itself is modified (line 10). This means regular code pushes to any branch will NOT trigger this workflow, which is inconsistent with the documentation (e.g., README_LOCAL_BUILD.md line 39 states "Builds run automatically on git push"). Either update the trigger to include all code changes (remove the paths filter), or update the documentation to clarify that builds only run on workflow file changes or manual triggers.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,372 @@
# 🚀 Getting Started in 5 Minutes
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation has inconsistent time estimates. The title claims "Getting Started in 5 Minutes" but line 275 states the actual setup takes 15 minutes. Similarly, README_LOCAL_BUILD.md line 17 mentions "5-minute setup" while line 29 says "takes ~15 minutes". Consider updating titles and references to use consistent and realistic time estimates (likely 15-20 minutes for the full setup).

Suggested change
# 🚀 Getting Started in 5 Minutes
# 🚀 Getting Started in 15 Minutes

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +48
- name: "📦 Setup Node.js 22.x"
uses: actions/setup-node@v4
with:
node-version: 22.x
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local-android-build.yml workflow specifies Node.js 22.x, while the existing eas-android-build.yml workflow uses Node.js 20.x. This inconsistency could lead to unexpected behavior or compatibility issues. Consider standardizing on the same Node.js version across both workflows to ensure consistent behavior.

Suggested change
- name: "📦 Setup Node.js 22.x"
uses: actions/setup-node@v4
with:
node-version: 22.x
- name: "📦 Setup Node.js 20.x"
uses: actions/setup-node@v4
with:
node-version: 20.x

Copilot uses AI. Check for mistakes.
## Files Added

### 1. **Workflow File** (Main Implementation)
📄 `.github/workflows/local-android-build.yml` - **The buildworkflow**
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: "buildworkflow" should be "build workflow" (two words).

Suggested change
📄 `.github/workflows/local-android-build.yml` - **The buildworkflow**
📄 `.github/workflows/local-android-build.yml` - **The build workflow**

Copilot uses AI. Check for mistakes.
sudo systemctl status actions.runner.*

# If not running, start it
sudo systemctl start actions.runner.*
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line contains excessive whitespace/padding between "sudo" and "systemctl". The command should be on a single line without the extra spacing. This appears to be a formatting error that could confuse users trying to copy the command.

Suggested change
sudo systemctl start actions.runner.*
sudo systemctl start actions.runner.*

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +200
#!/usr/bin/env bash
set -euo pipefail
shopt -s lastpipe

# ────────────────────────────────────────────
# 🎨 Helper: Styling
# ────────────────────────────────────────────
info() { printf "\033[1;34mℹ️ %s\033[0m\n" "$*"; }
success() { printf "\033[1;32m✅ %s\033[0m\n" "$*"; }
warn() { printf "\033[1;33m⚠️ %s\033[0m\n" "$*"; }
error() { printf "\033[1;31m❌ %s\033[0m\n" "$*"; }

section() {
printf "\n\033[1;35m────────────────────────────────────────────\033[0m\n"
printf "📟 \033[1;1mADB + Expo Device Manager (WSL Safe)\033[0m\n"
printf "\033[1;35m────────────────────────────────────────────\033[0m\n"
}
divider() { printf "\033[1;35m────────────────────────────────────────────\033[0m\n"; }

require_cmd() {
command -v "$1" >/dev/null 2>&1 || { error "Missing required command: $1"; exit 1; }
}

# ────────────────────────────────────────────
# 🧠 Detect USB & emulator devices
# ────────────────────────────────────────────
USB_DEVICES=()
EMU_DEVICES=()
ALL_DEVICES=()

get_devices() {
USB_DEVICES=()
EMU_DEVICES=()

# Drop the header line safely; do NOT let grep/sed failure trip set -e
local adb_out
adb_out="$(adb devices -l 2>/dev/null | sed '1d' || true)"

while IFS= read -r line; do
# Skip empty/whitespace-only lines
[[ -z "${line//[[:space:]]/}" ]] && continue

# serial + status are always first two fields
local serial status rest model device_name entry
serial="$(awk '{print $1}' <<<"$line" || true)"
status="$(awk '{print $2}' <<<"$line" || true)"
[[ -z "$serial" || -z "$status" ]] && continue

# Everything after the 2nd field
rest="$(cut -d' ' -f3- <<<"$line" 2>/dev/null || true)"

# Extract model:XYZ if present (no grep -P dependency)
model="$(sed -n 's/.*model:\([^[:space:]]*\).*/\1/p' <<<"$rest" | head -n1 || true)"

device_name="${model:-$serial}"
entry="$serial::$status::$device_name"

if [[ "$serial" == emulator-* ]]; then
EMU_DEVICES+=("$entry")
else
USB_DEVICES+=("$entry")
fi
done <<<"$adb_out"
}

# ────────────────────────────────────────────
# 🔐 Ensure ADB server is running
# ────────────────────────────────────────────
start_adb_if_needed() {
section
require_cmd adb
require_cmd npx

info "Checking ADB server..."
# Starting the server is safe even if it is already running
adb start-server >/dev/null 2>&1 || true
success "ADB server is responsive."
}

# ────────────────────────────────────────────
# 📱 Device Selector Prompt
# ────────────────────────────────────────────
SELECTED_SERIAL=""
SELECTED_NAME=""
SELECTED_STATUS=""

prompt_device_choice() {
divider
info "Select a device to launch Expo:"

local i serial status name icon
for i in "${!ALL_DEVICES[@]}"; do
IFS='::' read -r serial status name <<<"${ALL_DEVICES[$i]}"
icon="📱"
[[ "$serial" == emulator-* ]] && icon="🖥️"
printf " %d) %s %s (%s) [%s]\n" "$((i + 1))" "$icon" "$serial" "$name" "$status"
done

local choice_raw choice_idx max="${#ALL_DEVICES[@]}"
while true; do
read -rp "🚀 Enter choice [1-${max}]: " choice_raw
[[ "$choice_raw" =~ ^[0-9]+$ ]] || { warn "Enter a number from 1 to ${max}."; continue; }
(( choice_raw >= 1 && choice_raw <= max )) || { warn "Out of range. Enter 1..${max}."; continue; }

choice_idx=$((choice_raw - 1))
IFS='::' read -r SELECTED_SERIAL SELECTED_STATUS SELECTED_NAME <<<"${ALL_DEVICES[$choice_idx]}"
break
done
}

# ────────────────────────────────────────────
# ⏳ Wait for device to be online (with timeout if available)
# ────────────────────────────────────────────
wait_for_selected_device() {
divider
info "Waiting for ${SELECTED_SERIAL} to be online..."

if command -v timeout >/dev/null 2>&1; then
# adb syntax: adb -s SERIAL wait-for-device
timeout 90s adb -s "$SELECTED_SERIAL" wait-for-device || {
error "Timed out waiting for ${SELECTED_SERIAL}."
exit 1
}
else
adb -s "$SELECTED_SERIAL" wait-for-device
fi

# Confirm state
local state
state="$(adb -s "$SELECTED_SERIAL" get-state 2>/dev/null || true)"
if [[ "$state" != "device" ]]; then
warn "Device state is '${state:-unknown}'. If this is 'unauthorized', accept the RSA prompt on the device."
else
success "${SELECTED_SERIAL} is online."
fi
}

# ────────────────────────────────────────────
# 🚀 Launch Expo with selected device
# ────────────────────────────────────────────
launch_expo() {
divider
printf "🎯 Selected: \033[1m%s (%s)\033[0m [%s]\n" "$SELECTED_SERIAL" "$SELECTED_NAME" "$SELECTED_STATUS"

# Optional cleanup: try to kill a known zombie emulator-5554 if present
if adb devices 2>/dev/null | awk '{print $1,$2}' | grep -qE '^emulator-5554[[:space:]]+offline$' ; then
warn "Attempting to kill zombie emulator: emulator-5554"
adb -s emulator-5554 emu kill >/dev/null 2>&1 || true
fi

wait_for_selected_device

divider
info "Launching Expo on ${SELECTED_SERIAL}..."
export EXPO_ANDROID_DEVICE_ID="$SELECTED_SERIAL"

# Use expo run:android targeting the chosen device
npx expo run:android --device "$SELECTED_SERIAL"
}

# ────────────────────────────────────────────
# 🧠 Main logic
# ────────────────────────────────────────────
start_adb_if_needed

divider
info "Detecting connected devices..."
get_devices

if [[ "${#USB_DEVICES[@]}" -eq 0 ]]; then
warn "No USB devices found."
else
info "USB Devices detected:"
for dev in "${USB_DEVICES[@]}"; do
IFS='::' read -r serial status name <<<"$dev"
printf " 📱 %s [%s]\n" "$serial" "$status"
done
fi

divider
if [[ "${#EMU_DEVICES[@]}" -eq 0 ]]; then
warn "No emulators running."
else
info "Available Android Virtual Devices:"
for dev in "${EMU_DEVICES[@]}"; do
IFS='::' read -r serial status name <<<"$dev"
printf " 🖥️ %s [%s]\n" "$serial" "$status"
done
fi

ALL_DEVICES=("${USB_DEVICES[@]}" "${EMU_DEVICES[@]}")

if [[ "${#ALL_DEVICES[@]}" -eq 0 ]]; then
divider
error "No available devices to select."
exit 1
fi

prompt_device_choice
launch_expo No newline at end of file
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script (adb-connect2.sh) appears to be a duplicate or alternative version of adb-connect.sh with similar functionality. However, it's not referenced anywhere in the codebase (package.json only references adb-connect.sh). Consider removing this file if it's not needed, or documenting its purpose and when it should be used instead of adb-connect.sh. Having two similar scripts without clear differentiation can lead to confusion and maintenance issues.

Copilot uses AI. Check for mistakes.
- Auto-detect Android SDK at common locations (~\nAndroid/Sdk, ~/Library/Android/sdk)
- Export ANDROID_HOME from detection step for use in subsequent steps
- Add fallback ANDROID_HOME in build step
- Better error messages for SDK detection failures
- Ensures gradle can locate Android SDK on self-hosted runners

This fixes the 'SDK location not found' error when running on
self-hosted GitHub Actions runners that don't have ANDROID_HOME
explicitly set in the shell environment.
echo "RELEASE_TITLE=${RELEASE_TITLE}" >> $GITHUB_OUTPUT

- name: '🎉 Publish GitHub Release'
uses: softprops/action-gh-release@v2

Check warning

Code scanning / CodeQL

Unpinned tag for a non-immutable Action in workflow Medium

Unpinned 3rd party Action '🏗️ Local Android Build (Self-Hosted)' step
Uses Step
uses 'softprops/action-gh-release' with ref 'v2', not a pinned commit hash
echo "RELEASE_TITLE=${RELEASE_TITLE}" >> $GITHUB_OUTPUT

- name: '🎉 Publish GitHub Release'
uses: softprops/action-gh-release@v2

Check warning

Code scanning / CodeQL

Unpinned tag for a non-immutable Action in workflow Medium

Unpinned 3rd party Action '🚀 EAS Android Build & Smart Release' step
Uses Step
uses 'softprops/action-gh-release' with ref 'v2', not a pinned commit hash
@digitalnomad91 digitalnomad91 merged commit 8fea3fa into main Feb 26, 2026
3 checks passed
@digitalnomad91 digitalnomad91 deleted the feature/local-build-workflow-setup branch February 26, 2026 02:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants