A Practical Guide to Running Expo EAS Builds on Windows

If you’re using Expo EAS for React Native builds, you know that cloud builds can get expensive quickly. But what if you could run EAS builds locally on GitHub Actions and test them before pushing? This post introduces a complete, end-to-end automation pipeline for building Expo apps locally using eas build --local, all orchestrated with GitHub Actions, enhanced with Bash shell scripting, and even PowerShell scripts for Windows developers. The goal? Local development speed, full control, and $0 spent on EAS remote builds unless absolutely necessary. What This Setup Does This configuration automates: Sets up an environment with all required dependencies (Node, Java, Android SDK, etc.) Performs a local Expo build via GitHub Actions and eas build --local Handles GitHub CLI installation and usage Dynamically generates Android keystores and credentials Uploads APK artifacts to GitHub Releases Powers full local build execution using act (the GitHub Actions emulator) Includes PowerShell scripts to run the entire CI/CD workflow locally on Windows It’s a self-contained pipeline that works both in GitHub Actions and in your local terminal. Why Go Local? Using GitHub Actions with local builds can bring down costs. You’ll still get automation, artifact uploading, and continuous delivery, without paying EAS for each build. Bonus: With act and PowerShell, you can run the entire pipeline offline, fully replicating GitHub’s CI/CD in your development machine. Prerequisites Before diving into the setup of the GitHub Actions workflow and local build scripts, ensure the following: A React Native Project Managed by Expo: You need an existing React Native project that uses Expo for easy app building and management. Make sure the project is correctly configured with the necessary Expo and EAS dependencies. GitHub Repository: Ensure your project is hosted on GitHub. You’ll need to push your code to a GitHub repository to leverage GitHub Actions for CI/CD. Basic Understanding of GitHub Actions: Familiarity with how GitHub Actions work, including setting up workflows and actions in the .github/workflows/ directory. You don’t need to be an expert, but understanding the basics will help you troubleshoot and customize the workflow. Access to Expo and GitHub Tokens: You’ll need Expo tokens for authenticating with the Expo API and GitHub tokens for accessing your repository and uploading releases. Be sure to create and configure these tokens properly in the GitHub Secrets section of your repository settings. Local Environment Setup: You should have PowerShell installed on Windows, which will allow you to run the PowerShell scripts locally. The scripts are used to simulate and test the GitHub Actions workflow locally. ACT and Docker: To simulate GitHub Actions runs on your local machine, you’ll need ACT — a command-line tool that runs GitHub Actions workflows locally using Docker. This is crucial for testing and debugging before pushing the changes to GitHub. Make sure both ACT and Docker are installed and configured properly on your system. Once you’ve completed these steps, you’re ready to proceed with setting up the GitHub Actions workflow and local build scripts. Now, let’s go over how the automation works and the code structure. The GitHub Actions Workflow Here’s the core build.yaml workflow: name: Expo on: workflow_dispatch: inputs: os: type: string default: ubuntu-latest platform: type: string default: android profile: type: string default: preview jobs: job: runs-on: ${{ github.event.inputs.os }} timeout-minutes: 30 steps: - name: Setup repo uses: actions/checkout@v4 - name: Setup Act and Env run: | source "./tools.sh" runner "apt-get update >/dev/null 2>&1" runner "apt-get install unzip -y >/dev/null 2>&1" unzip -v runner "apt-get install git -y >/dev/null 2>&1" git --version runner "$(declare -f 'gh_try'); gh_try" gh --version - name: Setup Node uses: actions/setup-node@v2 with: node-version: '18.x' - name: Setup Java uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Test Environment env: GH_TOKEN: ${{ secrets.TOKEN }} run: | adb --version node --version java --version gh repo view --json name,url - name: Install Dependencies run: | yarn --silent > /dev/null 2>&1 - name: Run Dummy Tests run: | yarn add --dev jest --silent > /dev/null 2>&1 yarn test - name: Setup Expo uses: expo/expo-github-action@v7 with: toke

Apr 25, 2025 - 21:35
 0
A Practical Guide to Running Expo EAS Builds on Windows

If you’re using Expo EAS for React Native builds, you know that cloud builds can get expensive quickly. But what if you could run EAS builds locally on GitHub Actions and test them before pushing?

This post introduces a complete, end-to-end automation pipeline for building Expo apps locally using eas build --local, all orchestrated with GitHub Actions, enhanced with Bash shell scripting, and even PowerShell scripts for Windows developers. The goal? Local development speed, full control, and $0 spent on EAS remote builds unless absolutely necessary.

What This Setup Does

This configuration automates:

  • Sets up an environment with all required dependencies (Node, Java, Android SDK, etc.)
  • Performs a local Expo build via GitHub Actions and eas build --local
  • Handles GitHub CLI installation and usage
  • Dynamically generates Android keystores and credentials
  • Uploads APK artifacts to GitHub Releases
  • Powers full local build execution using act (the GitHub Actions emulator)
  • Includes PowerShell scripts to run the entire CI/CD workflow locally on Windows

It’s a self-contained pipeline that works both in GitHub Actions and in your local terminal.

Why Go Local?

Using GitHub Actions with local builds can bring down costs. You’ll still get automation, artifact uploading, and continuous delivery, without paying EAS for each build.

Bonus: With act and PowerShell, you can run the entire pipeline offline, fully replicating GitHub’s CI/CD in your development machine.

Prerequisites

Before diving into the setup of the GitHub Actions workflow and local build scripts, ensure the following:

  • A React Native Project Managed by Expo: You need an existing React Native project that uses Expo for easy app building and management. Make sure the project is correctly configured with the necessary Expo and EAS dependencies.
  • GitHub Repository: Ensure your project is hosted on GitHub. You’ll need to push your code to a GitHub repository to leverage GitHub Actions for CI/CD.
  • Basic Understanding of GitHub Actions: Familiarity with how GitHub Actions work, including setting up workflows and actions in the .github/workflows/ directory. You don’t need to be an expert, but understanding the basics will help you troubleshoot and customize the workflow.
  • Access to Expo and GitHub Tokens: You’ll need Expo tokens for authenticating with the Expo API and GitHub tokens for accessing your repository and uploading releases. Be sure to create and configure these tokens properly in the GitHub Secrets section of your repository settings.
  • Local Environment Setup: You should have PowerShell installed on Windows, which will allow you to run the PowerShell scripts locally. The scripts are used to simulate and test the GitHub Actions workflow locally.
  • ACT and Docker: To simulate GitHub Actions runs on your local machine, you’ll need ACT — a command-line tool that runs GitHub Actions workflows locally using Docker. This is crucial for testing and debugging before pushing the changes to GitHub. Make sure both ACT and Docker are installed and configured properly on your system.

Once you’ve completed these steps, you’re ready to proceed with setting up the GitHub Actions workflow and local build scripts. Now, let’s go over how the automation works and the code structure.

The GitHub Actions Workflow

Here’s the core build.yaml workflow:

name: Expo 

on:
  workflow_dispatch:
    inputs:
      os:
        type: string
        default: ubuntu-latest
      platform:
        type: string
        default: android
      profile:
        type: string
        default: preview

jobs:
  job:
    runs-on: ${{ github.event.inputs.os }}
    timeout-minutes: 30  

    steps:
      - name: Setup repo
        uses: actions/checkout@v4

      - name: Setup Act and Env
        run: |
          source "./tools.sh"
          runner "apt-get update >/dev/null 2>&1"
          runner "apt-get install unzip -y >/dev/null 2>&1"
          unzip -v
          runner "apt-get install git -y >/dev/null 2>&1"
          git --version
          runner "$(declare -f 'gh_try'); gh_try"
          gh --version

      - name: Setup Node
        uses: actions/setup-node@v2
        with:
          node-version: '18.x'

      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Setup Android SDK
        uses: android-actions/setup-android@v3

      - name: Test Environment
        env:
          GH_TOKEN: ${{ secrets.TOKEN }}
        run: |
          adb --version
          node --version
          java --version
          gh repo view --json name,url

      - name: Install Dependencies
        run: |
          yarn --silent > /dev/null 2>&1

      - name: Run Dummy Tests
        run: |
          yarn add --dev jest --silent > /dev/null 2>&1
          yarn test

      - name: Setup Expo
        uses: expo/expo-github-action@v7
        with:
          token: ${{ secrets.EXPO_TOKEN }}
          eas-version: latest
          expo-version: latest

      - run: eas whoami

      - name: EAS Init
        run: |
          eas init --non-interactive --force
          [ ! -f eas.json ] && echo '{
            "build": {
              "preview": {
                "developmentClient": false,
                "distribution": "internal",
                "android": {
                  "credentialsSource": "local",
                  "buildType": "apk"
                }
              }
            }
          }' > eas.json

      - name: Create Keystore
        run: |
          source "./tools.sh"
          key_create "com.spicytech.test2"

      - name: EAS Local Build
        run: |
          EAS_NO_VCS=1 eas build --local \
            --non-interactive \
            --output=./app-build.apk \
            --platform=${{ github.event.inputs.platform }} \
            --profile=${{ github.event.inputs.profile }}

      - name: Upload Artifact
        uses: actions/upload-artifact@v4
        with:
          name: ArtifactName
          path: app-build.apk

      - name: Create GitHub Release
        env:
          GH_TOKEN: ${{ secrets.TOKEN }}
        run: |
          source "./tools.sh"
          release_with_artifact '${{ github.repository }}' '${{ github.actor }}' "XYZ" app-build.apk

Helper Bash Functions (tools.sh)

The following functions power setup, fallback installs, keystore generation, and GitHub Release management:

runner() {
  CMD="$*"
  if command -v sudo >/dev/null 2>&1; then
    sudo bash -c "$CMD"
  else
    bash -c "$CMD"
  fi
}

gh_try() {
  if ! command -v gh >/dev/null 2>&1; then
    echo "Installing GitHub CLI..."
    apt-get install -y gnupg curl
    curl -fsSL https://cli.github.com/... | gpg --dearmor -o /usr/share/keyrings/...
    echo "deb ..." | tee /etc/apt/sources.list.d/github-cli.list
    apt-get update
    apt-get install -y gh
  else
    gh auth status || true
    gh repo view || true
  fi
}

key_create() {
  keytool -genkey -v -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 \
    -storepass KEYSTORE_PASSWORD -keypass KEY_PASSWORD \
    -alias KEY_ALIAS -keystore keystore.jks \
    -dname "CN=${1},OU=,O=,L=,S=,C=US"
  cat < credentials.json
{
  "android": {
    "keystore": {
      "keystorePath": "keystore.jks",
      "keystorePassword": "KEYSTORE_PASSWORD",
      "keyAlias": "KEY_ALIAS",
      "keyPassword": "KEY_PASSWORD"
    }
  }
}
EOF
}

release_with_artifact() {
  repo=$1
  actor=$2
  tag_name=$3
  artifact=$4
  git config user.name "$actor"
  git config user.email "$actor+$actor@users.noreply.github.com"
  gh release delete "$tag_name" --repo "$repo" --yes 2>/dev/null || true
  git fetch --tags
  git push --delete origin "$tag_name" 2>/dev/null || true
  git tag --delete "$tag_name" 2>/dev/null || true
  gh release create "$tag_name" --repo "$repo" --title "Releasing ${tag_name}" --notes "Released at '$(date)' by '$actor'"
  gh release upload "$tag_name" "$artifact" --repo "$repo"
}

PowerShell Script for Local Execution (Windows)

This is a PowerShell script to run the workflow locally using act, simulating GitHub Actions:

function INSTALL_EXECUTE 
{
    $REPO = "https://:@github.com//.git"
    $WORKFLOW = "build.yaml"

    git.exe clone $REPO $FolderName
    Set-Location $FolderName

    act.exe `
        --secret "TOKEN=$TOKEN" `
        --secret "EXPO_TOKEN=$EXPO_TOKEN" `
        --artifact-server-path $EXECUTION_PATH `
        --platform windows-2019=-self-hosted `
        --workflows .github\workflows\$WORKFLOW `
        -e ..\event.json

    Set-Location $EXECUTION_PATH
}

with

{
    "event_name": "workflow_dispatch",
    "inputs": {
      "os": "ubuntu-latest",
      "platform": "android",
      "profile": "preview"
    }
}

Conclusion

This setup gives you all the power of CI/CD, without the cost of cloud builds. It’s fully customizable, and especially great for solo devs and indie projects. The ability to run it all locally — on Linux via Bash or on Windows via PowerShell — makes it a game-changer.

If this helped you streamline your pipeline, give a star to the repo, share it with your team, and don’t forget to feed the GitHub algorithm.

Bon appétit!!

References

Inspired by *Don’t Pay for EAS! How to Set Up an EAS Local Build on GitHub Actions*