DEV Community

Justus Soh
Justus Soh

Posted on

Using GitHub Actions to seamlessly deploy Expo applications (Part 2)

Continuation

In part 1 of this series, I went through some of the pre-requisites to use Expo with GitHub actions and demonstrated how to set up your first action. If you want to learn more about how to run tests and publish branches, you can check out part 1 here!

In this section, I will explain the purpose of versioning and how to build the application using the Expo turtle service for the distribution of the application.

Importance of versioning

Why should we version? Is it necessary? How will this improve user experience? As the project grows, the number of dependencies grows and gets more difficult to manage. Eventually, one might find it difficult to upgrade without breaking contracts with other services. Version control is a simple way we can manage these dependencies with a common set of rules so that everyone can be on the same page. This is Semantic Versioning and follows a given rule in the format Major.Minor.Patch. Major changes include breaking API changes, Minor changes add functionality that is backward compatible and Patch adds bug fix.

The proper use of versioning could bring about a slew of advantages to the application. In-app versioning could allow for easy debugging of reported bugs by verifying code base and app version. Configuring over the air updates (OTA) to only send minor and patch updates would prevent breaking changes from bricking your application. This would also allow for different major versions of the application to existing and be maintained for users who haven't grab the latest version from the respective application stores.

In my implementation, I will specifically be using the GitHub release tag to trigger my workflows and also specify the name of the tag to match prod-Major.Minor.Patch so as to inform the action which release-channels to deploy the application to.

There are a lot of things going on in the code. I will first provide the code followed by a detailed explanation of some choices I have made.

name: Create Release
on:
  push:
    tags:
      - "prod-[1-9]+.[0-9]+.[0-9]+" # Push events to matching prod-*, i.e.prod-20.15.10

jobs:
  deploy_prod:
    name: Deploy To Production
    needs: test
    runs-on: ubuntu-latest
    outputs:
      releaseChannel: ${{ steps.releaseChannel.outputs.releaseChannel }}
      latestBinaryVersion: ${{ steps.latestBinaryVersion.outputs.version }}
    steps:
      - uses: actions/checkout@v2
        with:
          ref: ${{ github.head_ref }}
      - name: Fetch Tags
        run: |
          git fetch --prune --unshallow --tags -f
      - uses: actions/setup-node@v1
        with:
          node-version: 12.x
      - uses: expo/expo-github-action@v5
        with:
          expo-packager: npm
          expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
          expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
          expo-cache: true
      - uses: rlespinasse/github-slug-action@v2.x
      - name: Generate Release Channel # Release Channels are named prod-<Major Release>, i.e. prod-1, prod-3
        id: releaseChannel
        run: |
          RELEASE_CHANNEL=$(echo ${{ env.GITHUB_REF_SLUG }} | sed -r 's/\.[0-9]+\.[0-9]+$//')
          echo "::set-output name=releaseChannel::$RELEASE_CHANNEL"
      - name: Install Packages
        run: npm install
      - name: Get Latest Binary Version # Binary Version will be x.x.x based on the latest tag
        id: latestBinaryVersion
        run: |
          # Release tag finds the lastest tag in the tree branch - i.e. prod-x.x.x
          RELEASE_TAG=$(echo $(git describe --tags --abbrev=0))
          # Using param substitution, we output x.x.x instead
          echo "::set-output name=version::${RELEASE_TAG#*-}"
      - name: Echo Version Details
        run: |
          echo Build number is $GITHUB_RUN_NUMBER
          echo Latest release is ${{ steps.latestBinaryVersion.outputs.version }}
      - name: Expo Publish Channel
        run: expo publish --non-interactive --release-channel=${{ steps.releaseChannel.outputs.releaseChannel }}

In the first few lines, we can see that this action will only occur if a tag with name prod-x.x.x is pushed. This gives us the flexibility to run the action either with the git command-line tool or the GitHub GUI create release page.

Next, we can take a closer look at the step Fetch Tag and Get Latest Binary Version. Normally if we were to just grab the latest tag from the repo, the latest release tag will be grabbed, so instead, I decided to make this fail-proof through only taking the latest tag from that branch we are referencing.

A buildNumber also must be provided to the app.json for the app to be built. In this case, I have chosen to use the GitHub actions run number (it is the small number beside the name of each run on the actions tab of the repo which counts the unique times the action is played not including reruns, you can find more info here).

GitHub run number

In the case of Create Release out buildNumber will be 3

I also altered the release-channel for Expo to be represented as prod-Major grabbing only the major version from the latest tag as we want to route the OTA updates to those channels. This also allows us to do hotfixes for older versions of the application by creating release on another branch so nothing will be broken.

Writing releases

Being able to see and track changes from release to release is a game-changer. However, the hassle of creating a changelog for release to release brings about the stress of missing out on important changes. Fear not more, the community has written many some actions which we can utilize to help us generate a changelog from the past commits, this way no changes will be missed out and we can integrate it directly into the job. In my case, I have chosen metcalfc/changelog-generator because it's simple and gets the job done. After this is done, we will be using another community action ncipollo/release-action to create or update the existing release tag.

The example job is as follows:

create_release:
    name: Create Release
    needs: deploy_prod
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: rlespinasse/github-slug-action@v2.x
      - name: Generate Changelog
        id: changelog
        uses: metcalfc/changelog-generator@v0.4.0
        with:
          myToken: ${{ secrets.GITHUB_TOKEN }}
          base-ref: 'prod-0'
      - name: Creating Release
        uses: ncipollo/release-action@v1
        with:
          body: |
            Changes in this Release: 
            ${{ steps.changelog.outputs.changelog }}
          token: ${{ secrets.GITHUB_TOKEN }}
          name: Release ${{ env.GITHUB_REF_SLUG }}
          allowUpdates: true

Building and Distribution

The last and most important step in this would be building and distributing the application to the masses. The build time for the application is usually dependent on the number of projects queuing for the Expo build service, this process is usually quite lengthy (20 to 30 mins). Imagine waiting for about an hour just to build both Android and iOS binaries before every release, that would be a pain. But thankfully for us, we can write an action every release automatically and upload the binaries on the release itself or even the respective app stores! There are generally two approaches when it comes to using the free Expo build service that is to either wait for the service to complete before grabbing the URL or generated binaries or using a hook to send a POST request to a server when the build job is done. For simplicity and not wanting to spin up job listening for hooks in GitHub actions, I decided to go with the former option.

Here is a code snippet of how the job may look like for an android apk package, iOS would just be a mirror running build:ios instead:

 build_android:
    needs: [deploy_prod, create_release]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: rlespinasse/github-slug-action@v2.x
      - uses: expo/expo-github-action@v5
        with:
          expo-packager: npm
          expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
          expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
          expo-cache: true
      - name: Install Packages
        run: npm install
      - name: Build Android Release
        env:
          APP_BUILD_VERSION: ${{ github.run_number }}
          APP_BINARY_VERSION: ${{ needs.deploy_prod.outputs.latestBinaryVersion }}
        run: |
          expo build:android --release-channel=${{ needs.deploy_prod.outputs.releaseChannel }} > buildLogAndroid.txt
          cat buildLogAndroid.txt
      - name: Parse Asset URL
        id: androidUrl
        run: |
          ASSET_URL=$(cat buildLogAndroid.txt | tail | egrep -o 'https?://expo\.io/artifacts/[^ ]+')
          echo The android url is $ASSET_URL
          echo "::set-output name=assetUrl::$ASSET_URL"
      - name: Download APK Asset
        run: wget -O example-${{ env.GITHUB_REF_SLUG }}.apk ${{ steps.androidUrl.outputs.assetUrl }}
      - name: Upload Release Asset
        uses: svenstaro/upload-release-action@v2
        with:
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          file: ./example-${{ env.GITHUB_REF_SLUG }}.apk
          asset_name: example-${{ env.GITHUB_REF_SLUG }}.apk
          tag: ${{ github.ref }}

One challenge I faced was grabbing the .apk asset when the build was complete. In some cases, using the expo url:apk would yield the wrong result as only the latest build would not be updated, hence I decided to stick to using good old trusty regex to solve the problem. After which the uploading of assets to the release was smooth sailing.

The final output of the release should look something like this! Concise with all the parts we need.

release snapshot

Improvements

Some improvements we could make would be to use a community action like GitHub Tag Bump to store the latest version as a tag and automatically bump up the version base on the PR description, but that will be a story for another time. Another area for improvement would be to automatically add the upload the built .apk and .ipa binaries to the app stores directly. If you would like to do that you can read more about the process here. There are definitely many other areas of improvement and I'll continue working on the repo to add functionality to the CI/CD workflow.

Conclusion

Overall, it has been a fun journey planning out how I wanted the workflow to run and how it should be implemented. Also, documenting the process was enjoyable so that others can learn from and use GitHub Actions for themselves. I want to give a shout out to my mentor Sebastian as well for guiding me in the process and reviewing the same jobs and workflows for another project.

You can check out the entire repo here! Do fork it and play around with it however you like.

Expo working together with GitHub Actions starter

This is the example explained in a dev.to post, more details will be available soon.

Top comments (0)