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.

Summarize this article with AI ClickUp Brain not only saves you precious time by instantly summarizing articles, it also leverages AI to connect your tasks, docs, people, and more, streamlining your workflow like never before.
ClickUp Brain
Avatar of person using AI Summarize this article for me please

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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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.
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.
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v1
with:
flutter-version: 2.8.1
steps: - uses: actions/checkout@v2 - uses: subosito/flutter-action@v1 with: flutter-version: 2.8.1
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 }}
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 }}
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 }}.

Summarize this article with AI ClickUp Brain not only saves you precious time by instantly summarizing articles, it also leverages AI to connect your tasks, docs, people, and more, streamlining your workflow like never before.
ClickUp Brain
Avatar of person using AI Summarize this article for me please

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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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(),
),
)
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(), ), )
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.

Summarize this article with AI ClickUp Brain not only saves you precious time by instantly summarizing articles, it also leverages AI to connect your tasks, docs, people, and more, streamlining your workflow like never before.
ClickUp Brain
Avatar of person using AI Summarize this article for me please

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 🍏

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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
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
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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']
}
}
}
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'] } } }
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
- name: Decode Android Keystore
uses: timheuer/base64-to-file@v1.1
with:
fileName: 'android/app/key.jks'
encodedString: ${{ secrets.ANDROID_STORE_CONTENT }}
- name: Decode Android Keystore uses: timheuer/base64-to-file@v1.1 with: fileName: 'android/app/key.jks' encodedString: ${{ secrets.ANDROID_STORE_CONTENT }}
- name: Decode Android Keystore
  uses: timheuer/base64-to-file@v1.1
  with:
    fileName: 'android/app/key.jks'
    encodedString: ${{ secrets.ANDROID_STORE_CONTENT }}
Summarize this article with AI ClickUp Brain not only saves you precious time by instantly summarizing articles, it also leverages AI to connect your tasks, docs, people, and more, streamlining your workflow like never before.
ClickUp Brain
Avatar of person using AI Summarize this article for me please

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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
name: pre-release
on:
push:
branches:
- 'release/v*'
# build, test, and post slack message
name: pre-release on: push: branches: - 'release/v*' # build, test, and post slack message
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
name: prod
on:
release:
types:
- created
# build and deplo
name: prod on: release: types: - created # build and deplo
name: prod


on:
  release:
    types:
      - created


# build and deplo
Summarize this article with AI ClickUp Brain not only saves you precious time by instantly summarizing articles, it also leverages AI to connect your tasks, docs, people, and more, streamlining your workflow like never before.
ClickUp Brain
Avatar of person using AI Summarize this article for me please

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.

Summarize this article with AI ClickUp Brain not only saves you precious time by instantly summarizing articles, it also leverages AI to connect your tasks, docs, people, and more, streamlining your workflow like never before.
ClickUp Brain
Avatar of person using AI Summarize this article for me please

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! 🦄

Everything you need to stay organized and get work done.
clickup product image
Sign up for FREE and start using ClickUp in seconds!
Please enter valid email address