Automatically build your Flutter app APKs with Travis-CI

Automatically build your Flutter app APKs with Travis-CI

·

7 min read

When attempting to setup Travis-CI, I saw more than a few articles on running Flutter tests with Travis, but none for actually building and obtaining a compiled APK as we wanted.

Whenever changes are pushed to the GitHub repository a new build is automatically generated by Travis and a link to the APK on WeTransfer is posted to a Discord channel for supporters and beta testers allowing them to comment on the new build.

In this article I’m going to comment on how we achieved this workflow, as well as giving some general advice.

Step 1: Create Travis Configuration

Start by creating the following directory structure. You only need to include the .travis/ directory (also in the root) if you plan to add additional build scripts to your project.

your_project_root/
    - .travis.yml
    - .travis/
        - utils/

In your .travis.yml you need to add some basic boilerplate configuration: I didn’t set the language to Android because it didn’t seem to setup the SDK correctly, plus using node as the language allowed us to easily set up additional build scripts.

os: linux

# The Ubuntu Trusty release on Travis is known to
# have oraclejdk8 available. For some reason, we couldn't
# get this to work with other distributions/releases.
dist: trusty
jdk: oraclejdk8

# We set language to Node.js (JavaScript) for the sake
# of making creating utility scripts later on easier.
language: node_js
node_js:
  - "12"

env:
  global:
    - ANDROID_SDK_ROOT=/opt/android

sudo: required

addons:
  apt:
    # Flutter depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18
    sources:
      - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version
    packages:
      - lib32stdc++6 # https://github.com/flutter/flutter/issues/6207
      - libstdc++6
      - curl

cache:
  directories:
    - $HOME/.pub-cache
    - node_modules

Next, we need to setup our initial before_script stage. This will execute commands to setup Gradle, the Android SDK and Flutter.

before_script:
  # Setup gradle.
  - wget https://services.gradle.org/distributions/gradle-4.10.3-bin.zip
  - unzip -qq gradle-4.10.3-bin.zip
  - export GRADLE_HOME=`pwd`/gradle-4.10.3
  - export PATH=$GRADLE_HOME/bin:$PATH

  # (Quick fix: Silence sdkmanager warning)
  - mkdir -p /home/travis/.android
  - echo 'count=0' > /home/travis/.android/repositories.cfg

  # Download and setup Android SDK tools.
  - wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip
  - mkdir android-sdk-tools
  - unzip -qq sdk-tools-linux-4333796.zip -d android-sdk-tools
  - export PATH=`pwd`/android-sdk-tools/tools/bin:$PATH
  - mkdir -p $ANDROID_SDK_ROOT

  # This will install the Android SDK 28 using the previously installed SDK tools.
  - yes | sdkmanager --sdk_root=$ANDROID_SDK_ROOT "tools" "build-tools;28.0.3" "extras;android;m2repository" > /dev/null
  - export PATH=${ANDROID_SDK_ROOT}/tools/bin:$PATH

  # List sdkmanager packages
  # (useful when checking the logs)
  - sdkmanager --list

  # Clone Flutter
  # We clone the Flutter beta branch. You should clone whatever branch
  # you know works for building production apps.
  # If in doubt, you are advised to use the stable branch of Flutter
  # for production apps and you would do this by changing -b beta to -b stable
  # but we started the project before stable existed and whilst beta has always
  # worked reasonably well for us and we find stable is usually too outdated
  # and has too many missing framework features.
  - git clone https://github.com/flutter/flutter.git -b beta --depth 1

  # Add Flutter to the PATH environment variable.
  - export PATH=`pwd`/flutter/bin:`pwd`/flutter/bin/cache/dart-sdk/bin:$PATH

Finally, to get our fundamental configuration working, we of course have to actually execute flutter build. Which we do in the script stage.

script:
  # Prints the flutter version
  # (allows you to ensure, for each build, that Flutter is set up correctly.)
  - flutter doctor -v

  # Run Flutter build
  - ./flutter/bin/flutter build apk

Step 2: Upload the finished build to WeTransfer

Obviously, a key part of using Travis to build the compiled APK is being able to obtain the compiled APK. For this we use WeTransfer, a well-designed free (temporary) file sharing service with an API; exactly what we need.

Register an account for the WeTransfer Public API (developers.wetransfer.com) and under My apps, click ‘Create new application’. Enter your application’s details and copy the API key.

Create an environment variable for your key. Go to the Travis page for your repository and click ‘More options’; in the dropdown choose ‘Settings’ and under environment variables add a new environment variable called WT_API_KEY and paste in the key you copied previously. Do not enable the option to ‘Display value in log file’, this will expose your WeTransfer API key to anyone viewing your build logs.

after_success:
  # Export commit info
  - export AUTHOR_NAME=`git log -1 "$TRAVIS_COMMIT" --pretty="%aN"`
  - export COMMITTER_NAME=`git log -1 "$TRAVIS_COMMIT" --pretty="%cN"`
  - export COMMIT_SUBJECT=`git log -1 "$TRAVIS_COMMIT" --pretty="%s"`
  - export COMMIT_MESSAGE=`git log -1 "$TRAVIS_COMMIT" --pretty="%b"`
  # Upload to WeTransfer
  - npm install --save @wetransfer/js-sdk
  - export BUILD_OUTPUT_URL=`node ./.travis/utils/runUpload.js`

In order to give the file a clear name, we extract useful commit information from git (note; using the Travis commit reference.)

*$TRAVIS_COMMIT* is an automatic environment variable, exported by Travis at the start of a build. It refers to the git commit hash that is currently being built by Travis.

Then, we install the WeTransfer SDK using npm and call our own script:

const APPLICATION_NAME = "ApolloTV";

const fs = require('fs');
const createWTClient = require('@wetransfer/js-sdk');

const commit = process.env.TRAVIS_COMMIT.substring(0, 6);
const jobName = process.env.TRAVIS_JOB_NUMBER;
const buildName = process.env.TRAVIS_BUILD_NUMBER;
const title = process.env.COMMIT_SUBJECT;
const author = process.env.AUTHOR_NAME;
const message = `Job: ${jobName}, Build: ${buildName}\n\n${title} (${author})`;

(async function(){
  const wtClient = await createWTClient(process.env.WT_API_KEY, {
    logger: {
      level: 'error'
    }
  });

  const appBinaryContent = await new Promise((resolve, reject) => {
    fs.readFile(
      './build/app/outputs/apk/release/app-release.apk',
      (error, data) => {
        if(error) return reject(error);
        resolve(data);
      }
    );
  });

  const transfer = await wtClient.transfer.create({
    message: message,

    files: [
      {
        name: `${APPLICATION_NAME} Build ${buildName} - ${commit}.apk`,
        size: appBinaryContent.length,
        content: appBinaryContent,
      }
    ]
  });

  // This is required; this is so you can obtain the URL in your Travis/bash scripts.
  console.log(transfer.url);
})();

As you can see, it just uploads the file in the Flutter build output directory and prints the URL, from there you’re ready to go with your compiled APK.

If you want to stop here, you can just echo the URL for the APK and then you’re able to download the builds.

Step 3: Execute pre and post build web-hooks

We have web-hooks that send a message to a channel in our Discord server when a build has started or ended.We have web-hooks that send a message to a channel in our Discord server when a build has started or ended.

We find it’s pretty helpful to be alerted when a new build starts and finishes, however we also wanted to give our supporters access to cutting-edge builds as soon as they’re pushed to GitHub and we can use our web-hooks to notify supporters when a new build is starting as well as when it is finished and available to download.

before_install:
  # Execute Travis prebuild webhook.
  - ./.travis/10_prebuild.sh $WEBHOOK_URL

after_success:
  # Execute success procedure of postbuild webhook script.
  - ./.travis/40_postbuild.sh success $WEBHOOK_URL $BUILD_OUTPUT_URL

after_failure:
  # Execute failure procedure of postbuild webhook script.
  - ./.travis/40_postbuild.sh failure $WEBHOOK_URL

You may have noticed above that we have given numeric prefixes to each of our build scripts. This is because in our full configuration, we have further scripts to prepare our app build. This includes injecting a configuration into the app source code and checking translations. You can check out our actual build configuration on the Kamino GitHub repository.

In Discord, click the cog to edit a channel and then click ‘Webhooks’. Choose ‘Create Webhook’, give it a name such as ‘Travis Build’ and copy the URL, then click Save.

In Travis, create an environment variable (see Step 2 for instructions on how to do this) for your web-hook URL. Again, do not enable ‘display value in log file’, this will allow anyone to send messages to your Discord channel with the web-hook.

Finally, you need to create the prebuild and postbuild scripts; these should go in the .travis folder in the root of your project (that you created in Step 1):

#!/bin/bash
TIMESTAMP=$(date -u +%FT%TZ)

AUTHOR_NAME="$(git log -1 "$TRAVIS_COMMIT" --pretty="%aN")"
COMMITTER_NAME="$(git log -1 "$TRAVIS_COMMIT" --pretty="%cN")"
COMMIT_SUBJECT="$(git log -1 "$TRAVIS_COMMIT" --pretty="%s")"
COMMIT_MESSAGE="$(git log -1 "$TRAVIS_COMMIT" --pretty="%b")"

WEBHOOK_DATA='{
    "username": "ApolloTV (Travis)",
    "content": "A build has started.\n\n[Job #'"$TRAVIS_JOB_NUMBER"' (Build #'"$TRAVIS_BUILD_NUMBER"') '"$STATUS_MESSAGE"' ('"$AUTHOR_NAME"') - '"$TRAVIS_REPO_SLUG"'\n\n'"$COMMIT_SUBJECT"']('"$TRAVIS_BUILD_WEB_URL"')"
}'

(curl --fail --progress-bar -H Content-Type:application/json -d "$WEBHOOK_DATA" "$1" \
&& echo -e "\\n[Webhook]: Successfully sent the webhook.") || echo -e "\\n[Webhook]: Unable to send webhook."
#!/bin/bash
STATUS="$1"

TIMESTAMP=$(date -u +%FT%TZ)

if [ "$STATUS" = "success" ]; then
    EMBED_COLOR=3066993
    STATUS_MESSAGE="Passed"
elif [ "$STATUS" = "failure" ]; then
    EMBED_COLOR=15158332
    STATUS_MESSAGE="Failed"
fi

WEBHOOK_DATA='{
  "username": "ApolloTV (Travis)",
  "content": "@BuildNotify A new build has completed",
  "embeds": [ {
    "color": '$EMBED_COLOR',
    "author": {
      "name": "Job #'"$TRAVIS_JOB_NUMBER"' (Build #'"$TRAVIS_BUILD_NUMBER"') '"$STATUS_MESSAGE"' - '"$TRAVIS_REPO_SLUG"'",
      "url": "'"$TRAVIS_BUILD_WEB_URL"'"
    },
    "title": "'"$COMMIT_SUBJECT"'",
    "url": "'"$URL"'",
    "description": "**Build Information**: '"${COMMIT_MESSAGE//$'\n'/ }"\\n\\n"$CREDITS"'",
    "fields": [
      {
        "name": "Commit",
        "value": "'"[${TRAVIS_COMMIT:0:7}](https://github.com/$TRAVIS_REPO_SLUG/commit/$TRAVIS_COMMIT)"'",
        "inline": true
      },
      {
        "name": "Branch",
        "value": "'"[$TRAVIS_BRANCH](https://github.com/$TRAVIS_REPO_SLUG/tree/$TRAVIS_BRANCH)"'",
        "inline": true
      },
      {
        "name": "Files",
        "value": "'"[Download APK]($BUILD_OUTPUT_URL)"'"
      }
    ],
    "timestamp": "'"$TIMESTAMP"'"
  } ]
}'

(curl --fail --progress-bar -H Content-Type:application/json -d "$WEBHOOK_DATA" "$2" \
&& echo -e "\\n[Webhook]: Successfully sent the webhook.") || echo -e "\\n[Webhook]: Unable to send webhook."

Feel free to use and customise these scripts as you wish.