Building standalone Expo apps with Github Actions

Some time ago, I wrote an article, how to build standalone apps with Expo. It’s not a very complicated process, but if you lack knowledge about DevOps, you could be a bit confused. So, I was looking for a solution: how to make it easier. Unexpectedly, Github extended a helping hand by providing Github Actions - easy to configure set for CI and CD for projects based on their repositories. I’ve checked it, and it works great!

Check the working example on my repository: Expo standalone with GitHub Actions

Why Github Actions are so helpful?

Github released their Actions for public beta tests in August 2019. From that time, they’ve updated it to version 2.x and filled with many useful features. From building a mobile application point of view, they allow using a macOS runner, which is necessary to build IPA packages for iOS. Thanks to that, we can run all processes of building standalone applications without any other scripts.

How does it work? Github allows running building pipeline, triggered by one of many actions (like pull request, merge, comment, etc.). We define our actions in the workflow file, and we describe, step by step, how we want to build our application.

Building apps - crucial steps

In building standalone mobile applications based on Expo, we have to go through a few important steps.

Export and publish source code.

All our sources have to be exported to a public server (with SSL). This host will serve sources for our Over-The-Air updates. The application will check it on every launch (or however we set it up).

So, let’s start from the beginning and go through the first part of our workflow file:

jobs:
  export:
    name: Export assets
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-node@v1
        with:
          node-version: 12.x

      - uses: expo/expo-github-action@v5
        with:
          expo-version: 3.x
          expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
          expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}

      - run: yarn install

      - run: expo export --public-url ${{ secrets.ASSETS_URL }}

      - uses: sebastianpopp/ftp-action@v2.0.0
        with:
          host: ${{ secrets.ASSETS_FTP_HOST }}
          user: ${{ secrets.ASSETS_FTP_USER }}
          password: ${{ secrets.ASSETS_FTP_PASSWORD }}
          localDir: dist
          remoteDir: ${{ secrets.ASSETS_FTP_REMOTE_DIR }}

As you can see, we define the first job export, where we do all these things. We use expo/expo-github-action dependency, which is an official set of actions for Expo. With its help, we sign in to expo servers with our Expo credentials.

Next, we run installing dependencies with yarn and call expo export... command to minify and prepare sources for publishing.

The last step is to upload this package on an FTP server (we use for it another, simple, dependency: sebastianpopp/ftp-action@v2.0.0).

Building .apk

After a few minutes of exporting and uploading sources to FTP, we can build an Android package.

jobs:
  (...)
  apk:
    name: Build .apk file
    runs-on: ubuntu-latest
    needs: export
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-node@v1
        with:
          node-version: 12.x

      - run: ./.github/scripts/decrypt_secret.sh
        env:
          SECRET_FILES_PASSWORD: ${{ secrets.SECRET_FILES_PASSWORD }}

      - uses: actions/cache@v2
        id: turtle-cache
        with:
          path: ~/.turtle
          key: ${{ runner.os }}-turtle

      - run: |
          yarn install
          npm install -g turtle-cli
          turtle build:android -o ./build.apk --keystore-path ./secrets/build.keystore --keystore-alias ${{ secrets.ANDROID_KEYSTORE_ALIAS }} --public-url ${{ secrets.ASSETS_URL }}/android-index.json -t apk
        env:
          EXPO_ANDROID_KEY_PASSWORD: ${{ secrets.EXPO_ANDROID_KEY_PASSWORD }}
          EXPO_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.EXPO_ANDROID_KEYSTORE_PASSWORD }}
      - name: Archive .apk build
        uses: actions/upload-artifact@v1
        with:
          name: build.apk
          path: ./build.apk

At the very beginning, we noticed we need export to be finished (needs: export). Next, we go through the standard process, where we run turtle script. In this file, we see two extraordinary places:

  • caching - we use it to reuse downloaded SDKs and NDKs, necessary for building APK file; it speeds up the whole building process
  • decrypting secrets - as you may see, we use decrypt_secret.sh file in which we run bash’s gpg commands to get our secret files

At the very end of this process, we save our package in ./build.apk file, which pops up on the screen when the building is finished.

Building .ipa

The last thing is to build an iOS package. It could be run simultaneously with building apk, but as in the previous step, we have to wait for export job anyway.

Let’s take a look at the second build:

jobs:
  ipa:
    name: Build .ipa file
    runs-on: macos-latest
    needs: export
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-node@v1
        with:
          node-version: 12.x

      - run: ./.github/scripts/decrypt_secret.sh
        env:
          SECRET_FILES_PASSWORD: ${{ secrets.SECRET_FILES_PASSWORD }}

      - uses: actions/cache@v2
        id: turtle-cache
        with:
          path: ~/.turtle
          key: ${{ runner.os }}-turtle

      - run: |
          yarn install
          npm install -g turtle-cli
          turtle build:ios -o build.ipa --team-id ${{ secrets.APPLE_TEAM_ID }} --dist-p12-path ./secrets/cert.p12 --provisioning-profile-path ./secrets/profile.mobileprovision --public-url ${{ secrets.ASSETS_URL }}/ios-index.json
        env:
          EXPO_IOS_DIST_P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
      - name: Archive .ipa build
        uses: actions/upload-artifact@v1
        with:
          name: build.ipa
          path: ./build.ipa

It seems to be even simpler than .apk. And it actually is! The only thing which could bother is the number of certificates to prepare.

To proper build, we need at least a .p12 certificate file and provisioning profile. Both of them we have to create through App Store Connect panel and on macOS.

Speaking of macOS, keep in mind that for .ipa build, you have to use macos-latest runner, which is required.

Summary and example

The whole building process is described in the separate post - if you want details, you definitely should go there. Here we prepared the simplest version of it by using Github Actions. This service is sufficient to set up the whole process with any other exceptions we want. We are limited by our imagination.

I share this example on my Github repository, where I list all needed secrets and explain how to encrypt secret files. Feel free to extend it or fork it into your project!