I love automating the publishing process for my Expo apps using GitHub. I already use GitHub for version control, it's the perfect tool for managing builds, especially when I work across different operating systems like Windows and Ubuntu.

Building Android apps isn’t supported on Windows. Luckily, I can just push to GitHub and let it do the work.

Before publishing my app to production, I always test it. That's what test tracks are for. My app is first deployed to the test track for internal review before going live.

My workflow:

  • Create a feature branch and do work on it
  • MR to main after lint and tests pass
  • When main is merged, a release workflow is triggered
  • Release workflow creates a new MR with a bumped version. I need to approve it.
  • If approved, a new merge to main is made and release to test track is triggered

Here's what I do with a diagram:

publish expo to play store

What do you need

Keys are set in Github repository Settings -> Secrets and variables -> New Repository Secret.

Setup eas.json build profiles

You need eas.json configuration because eas will be called locally, and there is just one profile that you need to build for production, but actually you need 3 environments. I like to call them development, preview and production. Example file:

{
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "env": {
        
      }
    },
    "preview": {
      "distribution": "internal",
      "env": {
       
      }
    },
    "production": {
      "env": {
      }
    }
  },
  "cli": {
    "version": ">= 9.0.0",
    "appVersionSource": "local"
  }
}
  • development profile for android creates an apk with bundled expo dev client that you can install on your smartphone and emulator, and then you can run expo on your machine and preview the app while you develop and change the code.
  • production profile for android is your app built as an aab which you can manually or automatically upload to test tracks.
  • preview profile is almost the same as the production profile, it's just apk instead of aab extension. It's purely your app built as apk.

appVersionSource local means that versioning of the app is handled locally.

Given that I have three build profiles, my build scripts are:

"build:android-dev": "eas build -p android --local --profile development --output app-dev.apk",
"build:android-preview": "eas build -p android --local --profile preview --output app-preview.apk",
"build:android": "NODE_ENV=production expo prebuild --clean && eas build -p android --local --profile production --output app-release.aab",
 

Environment variables

env is everything that you have in .env file that is read while you're running expo. You need to explicitly specify environment variables in the eas.json. Don't put anything there that isn't supposed to be public.

Maybe obvious or maybe not, but you can have different environment variables per profile.

Internal Test Track

I forgot how this goes, but I remember it being easy. So, if you’ve created a new app and want to distribute it via the Internal Test Track, you need to configure the internal test track in the Google Play Console. Go to the Google Play Console and log in. Select or create your app, complete the initial setup if necessary. Go to internal test track and fill in the internal tester emails. Otherwise you won't be able to install it on your device. Testers just need to open the invitation link and enroll in the testing program.

GitHub Workflow

Check that you've set the necessary environment variables in repository settings.

This workflow works in 2 phases. First it detects that the version needs to be bumped. If you want to know more about automating version bumping, I recommend that you read my tips on bumping expo. I need to approve the version bump merge request. Then the second phase is triggered, and release to internal test track is called:

name: Release Please

on:
  push:
    branches:
      - main

permissions:
  contents: write
  pull-requests: write

jobs:
  release-please:
    runs-on: ubuntu-latest
    outputs:
      app--tag_name: ${{ steps.release.outputs.app--tag_name}}
      app--release_created: ${{ steps.release.outputs.app--release_created}}
    steps:
      - uses: actions/checkout@v4
      - uses: googleapis/release-please-action@v4
        id: release
        with:
          token:  ${{ secrets.GITHUB_TOKEN }}
          config-file: release-please-config.json
          manifest-file: .release-please-manifest.json
  release2play-internal-test-track:
    needs: release-please
    runs-on: ubuntu-latest
    if: ${{needs.release-please.outputs.app--release_created}}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - uses: actions/cache@v4
        with:
          path: node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
      - run: npm i
      - name: 🏗 Setup EAS
        uses: expo/expo-github-action@v8
        with:
          expo-version: latest
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
          packager: npm
      - name: Build aab
        run: npm run build:android
      - name: Upload to Play Store Internal Testing
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.PLAY_CONSOLE_SERVICE_ACCOUNT_JSON }}
          packageName: package.name
          releaseFiles: app-release.aab
          track: internal
          status: draft
          inAppUpdatePriority: 2
      

This workflow will build the production aab and upload it to internal test track. Test track will be in a draft status. It doesn't need to be, but I like to manually approve my test track publications.

You can also publish to GitHub releases

You might need a development apk and your machine might not be suited for building the apk. Maybe you're working from Windows or whatever the reason, you can use GitHub to build it. You can store your apk in GitHub releases. I created a manual dispatch workflow that builds and uploads the apk. Then I can download and install it on my usb connected device or emulator with:

adb install app-dev.apk

In case of an error while installing, uninstall the package first:

adb uninstall your.package

Workflow that publishes apk to github releases looks like:

name: Release DEV

on:
  workflow_dispatch:

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup-node-environment
        with:
          node-version: '20'
          cache-path: 'app/node_modules'
          cache-key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
          project: 'app'
      - uses: ./.github/actions/setup-expo
        with:
          expo-token: ${{ secrets.EXPO_TOKEN }}
      - name: Build development
        run: npm run build:android-dev
      - name: Create GitHub Release
        id: create_release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: v${{ github.run_number }}
          release_name: Snapshot Release v${{ github.run_number }} - ${{ github.ref_name }}
          body: |
            WIP development pre-release
          draft: false
          prerelease: true
       - name: Upload apk to GitHub Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: gh release upload ${{ steps.create_release.outputs.upload_url }} ./app-dev.apk

Reusable Expo workflow

In this workflow you might have noticed there are some custom actions like ./.github/actions/setup-expo. This kind of action is called a composite action and I use this pattern often to minimize repetition and make the already big workflow more readable:

name: "Setup Expo"
description: "Sets up java 17 and expo env"

inputs:
  expo-token:
    description: 'Expo Token'
    required: true
    default: ''

runs:
  using: 'composite'
  steps:
    - name: Set up Java 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'adopt'
    - name: 🏗 Setup EAS
      uses: expo/expo-github-action@v8
      with:
        expo-version: latest
        eas-version: latest
        token: ${{ inputs.expo-token }}
        packager: npm

Action is composed of 2 steps. If I didn't set up java 17 jvm, I'd get an error like:

    Could not resolve com.google.firebase:firebase-crashlytics-gradle:3.0.0.
    #[RUN_GRADLEW]      Required by:
    #[RUN_GRADLEW]          project :
    #[RUN_GRADLEW]       > Dependency requires at least JVM runtime version 17. This build uses a Java 11 JVM.
 

If you write your own github actions, I'd recommend that you make use of this pattern. It makes workflow configurations easier to maintain.

When using the workflow to publish to github releases, workflow needs permissions to read and write. You can either set it up in repository settings or set permissions in a workflow file.

allow github workflows to read and write
https://yourgithubrepo/settings/actions

GitHub: https://github.com/amarjanica/react-native-sqlite-expo-demo

Youtube Video: https://youtu.be/StMaBOjYOZU