15 Automating Test Executions & Build Distributions¶
Software development is always an iterative process. You’ll always need to update your app with bug fixes to your existing features and new features to improve your project. Mobile development in Flutter is no different. Quite a few repetitive tasks are connected to both bug fixes and feature development. You have to test the code by running automated tests, create a build for all the supported platforms and upload just-created builds to some platform where other team members, the QA team and end-users can test and use them. This can be very time-consuming. Therefore, in this chapter, you’ll learn how to automate these tests.
In software development, CI/CD are practices dealing with automating operating activities so you can focus on development activities. CI stands for continuous integration and covers automated test execution and building code artifacts such as Android builds. On the other hand, CD stands for continuous delivery, which primarily focuses on deploying code artifacts.
Automating all repetitive processes sounds great, but one important question must be answered to make it happen: How will the system know when to execute automated tests and build and deploy the apps? Keep reading to find out.
In this chapter, you’ll:
- Learn how to use GitHub Actions to set up and execute CI/CD for your Flutter project.
- Learn how to automate test execution.
- Learn how to automate building mobile apps for both iOS and Android.
- Become familiar with different development workflows and dive deeper into Gitflow.
Throughout this chapter, you’ll work on the starter project from this chapter’s assetsfolder. This chapter requires basic knowledge of Git. Therefore, if you don’t have experience using it, check out the Git Apprentice book.
Note
If you’re having trouble running the app, you might have forgotten to propagate the configurations you did in the first chapter’s starter project to the following chapters’ materials. If that’s the case, please revisit Chapter 1, “Setting up Your Environment”.
If you want to use the final project for this app, you still have to follow along with this chapter, as it requires a few additional steps to work. These include creating a GitHub repository, adding GitHub secrets, and generating access tokens and certificates for GitHub.
Software Development Workflows¶
The debate over the best way to accomplish something never ends. Therefore, this section will cover the software development workflows that are most commonly used in the developer community. When working on a project alone, it often feels like you don’t need strictly defined workflows. But when more people join your team or you try to automate some parts of your work, sticking to a workflow becomes much more important.
When developing software, you most often encounter two development workflows — Gitflow and trunk-based development.
Gitflow¶
The Gitflow development workflow uses multiple long-lived branches. To better understand the following explanation, look at the scheme below:
When the project starts, the main branch is created. Alternatively, it can also be called the master branch. From this branch, the develop branch is created and used as the stable source of code. This is a starting point for any feature branch. Feature branches are created from the develop branch and are used as long as a specific feature is in development. When the work is done, the feature branch is merged back to the develop branch, usually with the help of the pull request.
When a sufficient number of features are created, or a new release is scheduled, the release branch is created from the develop branch. From this point on, no new features should be added to this branch. The purpose of this branch is to prepare the necessary documentation for the new features, create release notes and resolve smaller bug fixes. When this is completed, you merge this branch to the main branch, where the code for the new release will live. To ensure that you have the latest stable version of your codebase on your develop branch, you have to merge the main branch back to develop.
You’ll deal with one more type of branch if you choose Gitflow as your software development workflow — hotfix branches. These branches are created directly from the main branch. When you detect a bug in your latest release, you’ll create a hotfix branch, where you’ll resolve the issue. This will be merged directly back to the main branch, where a new release will be created. Once again, the main branch will be merged back to develop.
You may notice that this isn’t an ideal methodology if you want to achieve efficient continuous integration and delivery. Some feature or release branches might exist for a longer period. Meanwhile, other developers might merge the features they’re working on back to the develop branch. Therefore, you’ll have to deal with issues such as merge conflicts. On the other hand, it requires quite some synchronizing between your team members to find the right time to create a release. As releases aren’t made very often, the internal testers won’t be able to provide efficient feedback on the work that’s been done since the last release.
To get rid of this issue, developers usually also set up a custom CI/CD for the develop branch. Here, all the newly created features are built and deployed to the development environment for internal testing immediately after merging. You’ll create one such automation in the following sections.
Trunk-based Development¶
A trunk-based development workflow, on the other hand, keeps the stable code on the main branch. Work is divided into very small batches, which are developed on short-lived branches. Those branches are merged back to the main branch quickly, ensuring the building of code on a daily basis. This allows internal testers to provide developers with regular feedback, which contributes to more agile work. Despite merging often, this doesn’t solve the problems that occur due to merge conflicts, which occur during the phases of code reviews.
The graphical representation looks more like this:
This methodology gives developers more freedom when scheduling the rollout of the new version, as the code on the main branch is always stable and ready for deployment.
Setting up CI/CD for a Flutter App¶
The workflow you choose for your app’s development will definitely affect the CI/CD pipeline, despite the general concepts always staying the same. With that being said, you’ll start by creating a new repository on GitHub named wonder_words:
To do so, you need a GitHub account. For this specific project, it doesn’t make much difference whether you choose the private or public option. It’s worth mentioning that a free private account offers a limited number of build minutes per month, whereas a public account is unlimited. Still, it’s suggested that you create a private repository, as the number of build minutes provided for it will be sufficient for this example. You also don’t need to create any of the suggested files, as your starter project already contains them.
When you’ve successfully created a new repository, this is what you’ll see:
As you can see in the image above, you have quite a few options to add a project to your GitHub repository. For this example, use the following commands in the terminal at the root of the starter project:
git init
git remote add origin <REPOSITORY_URL>
git branch -M main
git add -f *
git commit -m 'Initial commit'
git push -u origin main
Note
Make sure to replace
Next, create and push a new branch named develop using the following commands in the terminal:
git checkout -b develop
git push -u origin develop
With that, you set the base structure of the Gitflow software development workflow.
Creating Your First GitHub Action Job¶
Now, you can finally open the project and start programming. Start by creating two nested folders at the root of the project .github/workflows, and add the file named cicd.yml. Your folder structure should look something like this:
Note
If you’re following the chapter using Android Studio, it might ask if you want to add the newly created file to the .gitignore. You must not do that.
Open the file, and paste the following code into it:
# 1
name: Test, build and deploy
# 2
on:
pull_request:
branches:
- develop
push:
branches:
- develop
# 3
permissions: read-all
# 4
jobs:
# TODO: Remove the example job after testing
# 5
example:
name: Example of a job
# 6
runs-on: ubuntu-latest
# 7
steps:
# 8
- name: Echo text
run: echo This\ is\ my\ first\ Github\ actions\ job
Note
Make sure to use the correct indentation for the code above. Otherwise, it might not work correctly.
Here’s what the code above does:
- Defines the name for this specific workflow.
- Specifies the events on which to execute this workflow. In this case, it will run when the pull request is merged to the develop branch as well as when new code is pushed to the develop branch.
- Defines the permission scope.
- Lists the jobs that will run in this workflow.
- Defines the unique identifier for the job. Jobs from a single workflow run in parallel unless you define that one job is a prerequisite for another one. This is where those unique identifiers come in handy.
- Specifies the OS for the machine on which the job will run.
- Lists the steps that will run in a predefined order.
- Defines the name of the step and command that will be executed.
To see this in action, commit the changes done using the “Example GitHub actions job added” commit message and push it.
Note
Make sure the authorization you’re using for this Git repository has a “workflow” scope. If it doesn’t, GitHub won’t let you push the changes.
Go back to the GitHub page. Under the Actions tab, this is what you’ll see:
When the workflow completes, this is how the tab will look:
If you click the workflow that just completed, you’ll see a list with the jobs run as a part of this workflow:
By selecting a specific job, you can check the details of what’s been going on:
This might feel useless at the moment but is crucial when the job fails. You may notice that, despite the fact that you defined only one step for this specific job, it executed multiple steps. This is because a few steps run before and after the steps you’ve defined. They set up and clean the machine on which the job is running.
Automating Test Execution¶
Now, as you understand the basics, you can start to move to the real deal. Locate # TODO: Remove the example job after testing
, and replace the code below it with the following:
test:
name: Test
runs-on: ubuntu-latest
steps:
# 1
- name: Clone flutter repository with master channel
uses: subosito/flutter-action@v2
with:
channel: master
# 2
- name: Run flutter doctor
run: flutter doctor -v
# 3
- name: Checkout code
uses: actions/checkout@v2
# 4
- name: Get all packages and test
run: make get && make testing
# TODO: add a job for building Android app
The code above:
- Clones the Flutter, which is necessary to execute the tests.
- Runs Flutter doctor, which will install all the necessary development tools.
- Checks out the code.
- Fetches the missing packages and runs make test, which will run tests in all packages.
When you commit and push the changes, you should see output very similar to before. By checking the details of this job, you should be able to see whether any tests weren’t successful.
Automating Android Builds¶
As your tests have run through successfully, you may proceed with building the apps. You’ll start with the Android app. To do so, replace # TODO: add a job for building Android app
with the following code snippet:
android:
name: Build Android
runs-on: ubuntu-latest
steps:
# 1
- name: Clone flutter repository with master channel
uses: subosito/flutter-action@v2
with:
channel: master
- name: Run flutter doctor
run: flutter doctor -v
- name: Checkout code
uses: actions/checkout@v2
# 2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
# 3
- name: Clean and fetch packages
run: make clean && make get
#4
- name: Build apk
run: flutter build apk
# TODO: add deployment logic
Very similarly to the previous job, you:
- Clone Flutter, run Flutter doctor and check out the code.
- Install Java JDK, which is required to build your app.
- Clean the repository and fetch packages. Additionally, you could add
make lint
andmake format
commands here. They would throw an error and cancel the build in cases where the app doesn’t obey the rules of linting or is incorrectly formatted. But the better option would be to use Git hooks with Flutter Lefthook, which would run those two commands before committing. In the case of any issues, it wouldn’t let the developer push the changes to the remote. This is a good practice, as it resolves formatting and linting issues before the build. - Finally, builds the Android APK.
Having built the APK doesn’t help you much if you aren’t able to access it. To do so, you need to deploy the app.
To deploy both apps, you’ll use Firebase App Distribution.
Deploying Android App to Firebase App Distribution¶
Before continuing with coding, you need to take care of a few things. Go to your Firebase console, and locate the App Distribution tab in the left-hand sidebar. When opening it, this is what you should see:
In the drop-down menu, choose the Android app and click Get started. It’ll take you to the screen where you can upload your Android app and distribute it to your testers:
Continue to the Testers & Groups tab. Add the testers who’ll test your app, and add them into groups. For now, just create one group that you’ll use primarily for Android testing:
Next, install Firebase tools by running the following command in the home directory:
npm install -g firebase-tools
Note
Using npm to install Firebase tools is the easiest approach. If you don’t have npm installed on your computer, see alternative options in Firebase’s documentation.
By running the following command, you can log in to your Firebase account and receive the Firebase CLI token, which you’ll need in the next step:
firebase login:ci
Quite a bit of vulnerable information is connected with the deployment of your app, which shouldn’t just be hardcoded in your sources, especially if you use a public repository. But there’s a good workaround for securely saving your secrets — you have to set them as environment variables. To achieve that, go back to your repository on the GitHub website, and go to the Settings tab:
In the sidebar, locate the Secrets / Actions tab, and click New repository secret. It’ll take you to the next screen, where you’ll define FIREBASE_CLI_TOKEN as a secret name and add the token you received in the previous step after running the firebase login:ci
command as a value:
You’ll repeat the process two more times to store the TESTERS_GROUPS variable of the comma-separated list of groups you defined as testers group before and FIREBASE_APP_ID_ANDROID, which you can find in the Android settings of the Firebase console:
Under the App ID name:
When you add all the missing secrets, this is what the list of secrets should look like:
Now, you can finally complete the Android deployment by replacing # TODO: add deployment logic
with:
- name: Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID_ANDROID }}
token: ${{ secrets.FIREBASE_CLI_TOKEN }}
groups: ${{ secrets.TESTERS_GROUPS }}
file: build/app/outputs/flutter-apk/app-release.apk
The previous code sets the secrets as well as the Android file path as parameters; everything else is handled by the action on its own. After the deployment successfully completes and after committing and pushing the changes, an app is added in the Firebase App Distribution and mail is sent to the testers:
Automating iOS Builds and Deployment¶
Unfortunately, iOS build and deployment automation is much more complicated than Android. To build the app, you’re required to sign it with Apple developer certificates. The process is very time-consuming and requires installing the whole set of tools. Because this topic is almost broad enough for its own book, check the following video course covering building and deploying iOS apps using fastlane.
Key Points¶
- By automating test execution, software building and deployment, you can save a lot of time by avoiding repetitive work.
- The practice of automating operational tasks is called CI/CD, which stands for continuous integration and continuous delivery.
- Multiple workflows make continuous integration and delivery more efficient. The most common are Gitflow and trunk-based development.
- GitHub Actions is a great tool to help you execute automated testing, software building and deployment.
- To make your life easier, use fastlane when implementing a pipeline for iOS app building. Check this video course to see what fastlane offers.
Where to Go From Here?¶
CI/CD is a very big topic; therefore, it’s worth exploring a bit deeper than was explained in this chapter. Take a look at how you can use fastlane to deploy both Android and iOS to Google Play and the App Store. Nevertheless, your end users will download your app from the stores, not from the Firebase App Distribution, which can be very convenient for an internal testing team. Also, the current implementation of the pipeline for Android has a smaller flaw; therefore, think about how you could implement an automatic build number incrementation for Android too.
On the other hand, you should look at what can be done when the jobs or workflows are completed. No information is reported if any issues appeared or any tests failed. Think of how to efficiently add build reports to your CI/CD.