Flutter CI & CD at ClickUp

With a growing mobile team and weekly releases, it’s essential to have a reliable CI pipeline. We need to be able to build and test our apps and deploy them to the App Store and Google Play Store. We also maintain internal preview versions to test the latest features.

Learn how we use ClickUp, Fastlane, and GitHub Actions to automate our Continuous Integration (CI) and Continuous Delivery (CD).

The life of a bug 🐜

Let’s start by quickly going over our process of managing and fixing bugs.

  1. A bug (or feature request) gets reported and a task is created in ClickUp
  2. The task is assigned to a developer and fixed in a PR against the staging branch
  3. The CI runs all tests, builds the app, deploys a web preview, and uploads everything to the ClickUp task
  4. Our QA team verifies the fix and if it is good, the task is marked as done
  5. The PR gets merged into staging automatically
  6. The staging branch gets built and deployed to our internal TestFlight
  7. Every Wednesday a release branch is created, built, and tested
  8. On Fridays, we create a release on GitHub and the CI deploys the release to the App Store and Play Store.

A ClickUp task contains everything about the bug. We use Custom Statuses like In Progress or Code Review to keep track of the bug. The CI workflows change the status automatically. Custom Fields contain additional information like who reported the bug, who works on it when it will be released etc.

PR workflow 📜

The first two steps outlined above are not really CI-related, but the third one is interesting…

Our development workflow runs for any PR. It checks lints, formatting and runs all tests before it starts building the Android and iOS artifacts.

name: development


on: pull_request


jobs:
  cancel:
    name: 'Cancel Previous Runs'
    runs-on: ubuntu-latest
    timeout-minutes: 3
    steps:
      - uses: styfle/cancel-workflow-action@0.9.1
        with:
          access_token: ${{ github.token }}


  check:
    name: Check formatting
    runs-on: ubuntu-latest
    needs: cancel
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v2
      - name: Setup Flutter
        uses: subosito/flutter-action@v1
      - name: Install dependencies
        run: flutter pub get
      - name: Check formatting
        run: flutter format --dry-run . --set-exit-if-changed
      - name: Check lints
        run: flutter analyze
      - name: Run tests
        run: flutter test


# build, post task message etc.

When a build is successful, the CI will post a message in the linked task. QA Engineers can go to the PR, download the build artifacts or use the web preview.

The CI will post a message in the linked task

An automated CI message posted in the linked ClickUp task after a successful build

Setting up Flutter on the CI runner 🛠

We use the well-known GitHub action subosito/flutter-action to set up Flutter on the CI. By default, it will install the latest stable Flutter release. To avoid breaking your CI workflows when a new Flutter version is released, you should specify the version manually.

steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v1
  with:
    flutter-version: 2.8.1

If you have multiple workflows, it is better to store the flutter version in a file. We use FLUTTER_VERSION in the root of the repository.

steps:
- uses: actions/checkout@v2
- name: Read FLUTTER_VERSION
  id: get-version
  run: echo "::set-output name=version::$(cat FLUTTER_VERSION)"
- uses: subosito/flutter-action@v1
  with:
    flutter-version: ${{ steps.get-version.outputs.version }}

Another easy solution is to store the Flutter version as GitHub secret and access it using {{ secrets.FLUTER_VERSION }}.

Web Preview 🕸

Thanks to Flutter’s ability to run on the web, we can create a fully functional web preview of pull requests. Using the device_preview package, the device size and settings can be adjusted.

The preview has proven to be very useful and is not only used by our QA team. Designers and Product managers also like it to quickly iterate on new features.

Fully functional web preview in Flutter

via Flutter

How to create a web preview 🐶

To get started, make sure that your app is compatible with Flutter web—not all APIs are supported.

In our app, for example, we needed to disable push notifications and web sockets.

This sample workflow builds a web preview of your Flutter app and uploads it to an S3 bucket. We use an ENABLE_DEVICE_PREVIEW environment variable to disable the device_preview in production.

build-preview:
  name: Build preview
  runs-on: ubuntu-latest
  env:
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  steps:
    - uses: actions/checkout@v2
    - name: Setup Flutter
      uses: subosito/flutter-action@v1
    - name: Install dependencies
      run: flutter pub get
    - name: Build web
      run: flutter build web --release --dart-define ENABLE_DEVICE_PREVIEW=true
    - name: Fix base href
      uses: jacobtomlinson/gha-find-replace@0.1.4
      with:
        find: '<base href="/">'
        replace: '<base href="/flutter-preview-${{ github.head_ref }}/">'
        include: build/web/index.html
    - name: Deploy to S3
      uses: reggionick/s3-deploy@v3
      with:
        folder: build/web
        bucket: yourbucket/flutter-preview-${{ github.head_ref }}
        bucket-region: us-west-2
        no-cache: true

The Fix base href step is needed because the preview will not be at the root of the bucket.

And some code to conditionally enable the device_preview.

import 'package:device_preview/device_preview.dart';


const _preview = bool.fromEnvironment('ENABLE_DEVICE_PREVIEW', defaultValue: false);


void main() => runApp(
  DevicePreview(
    enabled: _preview,
    builder: (context) => MyApp(),
  ),
)

Environment variables are a powerful tool and allow Flutter’s tree shaking algorithm to drop debug code for release builds.

Fastlane 💨

Fastlane greatly simplifies building, signing, and deploying Flutter apps. It manages our certificates, provisioning profiles, and other settings. We use GitHub secrets to store passwords and tokens securely.

Useful Fastlane actions:

Sample iOS dev build 🍏

platform :ios do
  desc "Create a dev build"
  lane :build_dev do
    setup_ci
    api_key = app_store_connect_api_key(
      key_id: ENV["IOS_KEY_ID"],
      issuer_id: ENV["IOS_ISSUER_ID"],
      key_content: ENV["IOS_APP_STORE_API_KEY"],
    )
    match(
      type: "development",
      app_identifier: ["com.example.mobile"],
      api_key: api_key,
      git_basic_authorization: Base64.strict_encode64(ENV["GH_USER_TOKEN"])
    )
    build_app(
      workspace: "Runner.xcworkspace",
      scheme: "Runner",
      export_method: "development",
      export_options: {
        method: "development",
        compileBitcode: false,
        provisioningProfiles: { 
          "com.example.mobile" => "match Development com.example.mobile"
        }
      }
    )
  end
end

Don’t forget setup_ci it will save you from weird errors 👾. Learn more about Fastlane for Flutter apps.

Android signing 🔒

The easiest way to sign Android release builds securely is to store the tokens as GitHub secrets and use environment variables and a temporary key.jks created by the CI:

android {
  // ...
  signingConfigs {
      release {
          storeFile file('key.jks')
          storePassword System.env['ANDROID_STORE_PASSWORD']
          keyAlias System.env['ANDROID_KEY_ALIAS']
          keyPassword System.env['ANDROID_KEY_PASSWORD'] 
      }
  }
}

We store the key.jks as base64 encoded string in Github Secrets and decode it in the workflow:

- name: Decode Android Keystore
  uses: timheuer/base64-to-file@v1.1
  with:
    fileName: 'android/app/key.jks'
    encodedString: ${{ secrets.ANDROID_STORE_CONTENT }}

Release & prod workflow 🚀

The pre-release workflow runs for branches that start with release/v. It strips all debugging and internal code to make sure we test the same code that will be released.

name: pre-release


on:
  push:
    branches:
      - 'release/v*'


# build, test, and post slack message

Additionally, the pre-release workflow posts to various Slack channels to notify QA and marketing teams about a new release using incoming webhooks.

After everything is tested thoroughly, we create a release on GitHub that triggers the prod workflow. It builds and signs the App with production certificates and sends it to the App Store.

name: prod


on:
  release:
    types:
      - created


# build and deplo

More tricks for your CI 🦾

If you use the push trigger for GitHub Actions, you will likely run into problems if there are multiple pushes to the same branch in quick succession. More than one instance of the workflow will start and eat build minutes or cause other problems.

Summary 🍩

Building your CI is a fun process. It’s easy to get started and you can evolve it as you go. Our CI lives after one of ClickUp’s Core Values: Progress towards perfection. We’re constantly working on CI improvements for our QA and engineering teams.

The combination of ClickUp, GitHub Actions, and Fastlane is very powerful and allows building a flexible and fully automated CI/CD pipeline in less than an hour. Give it a try!

We have many cool topics in the pipeline, so keep checking out the ClickUp Engineering blog! 🦄

Questions? Comments? We're here for you 24/7 at help@clickup.com!

Sign up for FREE
and start using ClickUp in seconds!
Please enter valid email address