Photo by Pathum Danthanarayana on Unsplash

Deploying Your Flutter App with Github Actions

Jay Whitsitt

--

When starting a new project, it’s not always top-of-mind to speed up or simplify the release cycle. Creating an idea is more exciting, but automating deployment, even if just for personal testing prior to publicly releasing, can help productivity. Coding is great, but waiting for builds to finish takes up valuable time.

If you use GitHub for your Flutter codebase repository, GitHub Actions can be a powerful tool for Continuous Deployment. Follow the steps below with an existing Flutter repository to speed up your development cycle.

Setup Distribution Platforms

There are multiple app distribution platforms available. These steps will cover using the Play Store and TestFlight for distribution. The builds uploaded can then be easily promoted to production for public release.

Play Store

Visit the Google Play Console.

Google Play Console — Click the “Create app” button

If you haven’t already, you’ll need to create a developer account and verify your identity. (New account verification can take a couple of days and is required for distribution, even to internal tracks.)

If you haven’t created your app listing yet, you’ll need to do that as well. Click the “Create app” button in the top-right and fill out the form on the next page. In order to release the app, Google some information to categorize it and provide details to users.

At least one build must be released manually before the Play API will allow automated releases. This is because Google needs to know the application ID to match up with uploaded bundles and the API can only create draft releases for apps in a draft status. Releasing to production or any testing track satisfies both of these requirements. The easiest option is to release to a closed testing track with no testers.

Google Play Console — Manually publish one release to production or a testing track

The exact process and requirements to release occasionally change. Be sure you’ve added all the required items in the “Main store listing” page and provided any necessary information on the “App content” page. If anything is missing, an error will say which data needs to be provided prior to releasing. Check out Google’s documentation on making a new release for the most accurate details: https://support.google.com/googleplay/android-developer/answer/9859348

App Store Connect / TestFlight

Distributing using TestFlight requires an app listing be created in App Store Connect. Access requires a paid developer account. Once the app entry is created with the appropriate bundle ID, package uploads will be successful with the proper authentication.

App Store Connect — TestFlight tab

Automatic releases to TestFlight can only happen when export compliance is properly setup. Export and encryption regulation compliance can be declared on the TestFlight tab of the app listing on App Store Connect or in the builds Info.plist file. A build status of “Missing Compliance” means compliance declaration has to be given for that specific build before it is available. Adding the appropriate values to the app’s Info.plist file allows the build to be released automatically.

View Apple’s documentation on how to declare and manage export and encryption regulation compliance in your Info.plist: https://developer.apple.com/documentation/security/complying_with_encryption_export_regulations

GitHub Actions Workflows

When code is pushed to GitHub, it’s possible to trigger scripts to run based on certain criteria to build, sign, upload, and release app packages. The process is completely customizable.

Processes are broken up into workflows, each with its own yaml file. Each workflow has jobs, some of which can be dependent on each other if desired. Each job has steps which can be explicit command line scripts or actions published by the community. GitHub’s documentation can be found on their website: https://docs.github.com/actions

Before customizing any Continuous Deployment or Continuous Integration system, it’s usually good to run the same commands locally. Code signing is often a pain point and is much easier to troubleshoot locally rather than debugging remote machines without direct access to any generated log files.

Complete working examples can be found here: https://github.com/GDGKansasCity/flutter_ci/tree/master/.github/workflows

Creating Workflows

A new workflow can be created either by creating a file in the .github/workflows/ folder or on the Actions tab for the GitHub repo.

GitHub Actions tab

Each workflow has its own trigger(s). For incremental builds, pushes to the main development branch should be sufficient. Specific file paths can be ignored by using the paths-ignore property. Manual invocation requires the workflow_dispatch trigger be added. This can be configured with the following:

on:
push:
branches: [develop]
paths-ignore: ['ios/**', 'test/**', '.github/workflows/ios.yml']
workflow_dispatch:

Each workflow lists each job. For this use-case, only one is required, although there are more complex ways to divide up steps and dependencies if desired. The job will define a list of steps that are a combination of explicit bash scripts and third-party actions created by community developers.

Android

Creating an Android app bundle is usually done with the Flutter CLI. A local development environment has to be setup prior to building. The remote machine running the GitHub Actions workflow needs the same setup before the Flutter CLI works as expected.

To start, the code needs to be checked out. This can be done with the following. After the trigger section mentioned above, the list of jobs is declared with the jobs keyword. Next, a job named “build” can be declared by

jobs:
build:

Each job has a runs-on property to declare which OS and version is requires.

jobs:
build:
runs-on: ubuntu-latest

Also within the defined job, the list of steps is declared with the steps keyword.

jobs:
build:
runs-on: ubuntu-latest
steps:

Each step starts with a and is either a community action or bash script. To start, the repo needs to be checked out onto the machine.

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

Java, the Flutter SDK, and Dart are required to build the Android module. Utilizing actions already made for this greatly simplifies the implementation.

      - uses: actions/setup-java@v1
with:
java-version: '12.x'
- uses: subosito/flutter-action@v1
with:
channel: 'stable'

The machine also needs the code signing information available. Sensitive data can be provided to actions using GitHub repo secrets. These are added on the repo Settings tab on the Secrets page. For this example, the app will expect a keystore.jks file in the android/app folder and key.properties file in the android folder with the signing information. Since the keystore is in a binary file, the base64 representation will be the value of the secret, then the script step will decode the base64 string back into a binary file.

      - name: Setup signing
run: |
echo "${{ secrets.ANDROID_KEY_PROPERTIES }}" > android/key.properties
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks

Now that code signing is setup, the app can be built. The same commands that are ran locally to build should be used here as well. The only difference here is that the path to the signed bundle will need to be used in a later step. To do that

  1. Values can be passed between steps by passing a specific string format to echo
  2. The step should have a custom id value to reference the output value

The following step will build the bundle and add the file path to the output values. It also uses the git commit count as the build number which requires a full git fetch, not just a shallow fetch. Adding the appropriate properties to the checkout action will do that.

    steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-java@v1
# ...
- name: flutter build
id: build
run: |
flutter pub get
flutter build appbundle \
--build-number=`git rev-list --count HEAD`
bundlePath="$(pwd)/build/app/outputs/bundle/release/app-release.aab"
echo "::set-output name=bundlePath::$bundlePath"

Optionally, files can be added to workflow runs. If upload to the Play API succeeds, files can be downloaded from the Play Console, but in case of a failure, it may be beneficial to archive this for a time. The default retention policy is 90 days but it is customizable.

      - uses: actions/upload-artifact@v2
with:
path: ${{ steps.build.outputs.bundlePath }}
name: app-release.aab

The final step is to upload the bundle using the Play API. Luckily, an action exists for this. The action requires a Google Cloud service account key with access to the Play API. Follow these instructions to get the credentials: https://developers.google.com/android-publisher/getting_started

The action’s version 1.0.16 has an issue with apps that aren’t released to production yet, so 1.0.15 should be used if the app is still in “draft” status until the issue is fixed.

      - name: Upload to Play
uses: r0adkll/upload-google-play@v1.0.15
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_UPLOAD_SERVICE_ACCOUNT_JSON }}
packageName: com.jaywhitsitt.flutterapp
releaseFiles: ${{ steps.build.outputs.bundlePath }}
track: internal

A fully functional example can be found here: https://github.com/GDGKansasCity/flutter_ci/blob/master/.github/workflows/android.yml

Pushing to the branch defined in the on: section will now run all the above steps, ending with the upload to the Play API.

iOS

Coming soon…

--

--

Jay Whitsitt

Mobile developer, community organizer, and Kansas City native