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.
- A bug (or feature request) gets reported and a task is created in ClickUp
- The task is assigned to a developer and fixed in a PR against the staging branch
- The CI runs all tests, builds the app, deploys a web preview, and uploads everything to the ClickUp task
- Our QA team verifies the fix and if it is good, the task is marked as done
- The PR gets merged into staging automatically
- The staging branch gets built and deployed to our internal TestFlight
- Every Wednesday a release branch is created, built, and tested
- 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.
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.
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:
- fastlane match to create and store iOS keys and profiles in a GitHub repository
- build_app to build iOS and Android apps
- upload_to_testflight and deliver to deploy iOS builds
- upload_to_play_store to deploy Android builds
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.
- We recommend using the Cancel Workflow Action to cancel all previous instances of the workflow
- If you are looking for an easy and maintainable solution to generate sequential build numbers, try the Build Number Generator. You can also use the
GITHUB_RUN_ID
but that cannot be customized - Check out the ClickUp GitHub app to see branches, commits, and GitHub status right in your ClickUp tasks. Use ClickUp Automations or ClickUp’s public API for advanced automations
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! 🦄