In the world of mobile app development, the rapid pace of innovation demands efficient and reliable Continuous Integration and Continuous Deployment (CICD) pipelines. When it comes to building and deploying Flutter apps, harnessing the power of GitHub Actions and CodeMagic can be a game-changer. In this Medium post, we’ll explore essential tips and best practices to supercharge your Flutter CICD workflow, ensuring seamless development, testing, and deployment processes.
This topic became imperative for me due to the unexpectedly high costs I encountered while using GitHub Actions and Codemagic in one of my personal projects. Faced with these financial challenges, I embarked on a journey to discover strategies to optimize and minimize these expenses. In this post, I’ll share my findings and cost-saving techniques to help fellow Flutter developers navigate the budget constraints of CICD pipelines.
What is CICD?
Continuous Integration and Continuous Deployment (CICD) is a software development practice that automates the building, testing, and deployment of code changes, enabling teams to deliver new features and bug fixes more efficiently. For Flutter developers, here are some of CICD tools available to streamline their workflows:

Codemagic: A cloud-based CI/CD tool tailored specifically for Flutter, offering automated build and testing pipelines with support for both Android and iOS platforms.
GitHub Actions: GitHub’s built-in CI/CD platform, seamlessly integrated with your repositories, enabling you to automate workflows and build and deploy Flutter apps with ease.
CircleCI: A popular CI/CD platform that supports Flutter, providing customizable pipelines and integration options with other tools and services.
Fastlane: While not a standalone CI/CD tool, Fastlane is a powerful automation tool that integrates seamlessly with other CICD platforms, allowing you to automate various tasks in your Flutter app deployment process.
Bitrise: A CI/CD platform designed for mobile app development, including Flutter. Bitrise offers a wide range of integrations and workflows to enhance your development pipeline.
In this post, we’ll focus on GitHub Action and Codemagic, with tips how to harness the capabilities of these CICD tools to optimize your Flutter app development, testing, and deployment processes while managing costs effectively.
Pricing Plan
GitHub

GitHub’s pricing structure indeed offers both free and paid options, each tailored to different needs. With a free GitHub account, you can create an unlimited number of public and private repositories, making it an excellent choice for open-source projects and small-scale development. However, the limitation of 2000 build minutes per month on GitHub Actions can become a constraint for more extensive or resource-intensive projects.
For users who require more build minutes, the GitHub Team account, priced at $4 per user per month, offers an increased allocation of 3000 build minutes per month on GitHub Actions. This can be particularly beneficial for teams or developers working on larger and more complex projects, where additional build minutes are essential to maintain efficient CI/CD pipelines.
However, that’s not the only cost per month. GitHub Action comes with a payment system called “Minutes multipliers”, which has different multiplier for each OS
As a Flutter developer who writes mobile app (both Android and iOS apps), this is where the cost increased for me. I need macOS in order to build iOS app, which costs 10 times more than the normal Linux OS. Which means, if I only use macOS for all my GitHub Action pipelines, I have only 300 build minutes per month.
Codemagic
CodeMagic’s offers PAYG (Pay As You Go) payment scheme, which offers flexibility to Flutter developers by providing a base allocation of 500 build minutes per month on both MacOS M1 and Intel VM machines. This allocation is particularly valuable for users who have varying CI/CD needs. However, it’s important to note that any build minutes used beyond the initial 500 will incur an additional cost of $0.095 per minute on Standard macOS M1 VM machine. Details of cost per minute is below.

What causes high cost?
By looking at the pricing plan above, first thing comes to my mind is that our build takes too much time, which is why the cost for us is too high.
Checking on our GitHub Action build time, I noticed that on each success build, the longest build time is 39 minutes, and since they’re all on macOS, that’s 390 minutes for each build.

Another reason I can think of here is that I always use macOS for all jobs, in which some of the task may not need macOS at all. With that thought, now let’s look at some options to avoid a big pocket burn.
Let’s save money!
Step 1: Setup spending limit
Codemagic offers a PAYG payment scheme, therefore there is no spending limit option available for Codemagic.
Luckily, GitHub offers the convenience of setting up a spending limit for your account, allowing you to have better control over your expenses. By navigating to your GitHub Account settings, specifically under “Billing & plans,” you can establish a spending cap that aligns with your budget and development requirements. This feature ensures that you can manage and monitor your GitHub Actions usage while preventing unexpected billing surprises.

Step 2: Avoid unnecessary job run
In both GitHub Actions and CodeMagic, there are provisions to handle the scenario where a new job is triggered in the same workflow while the previous job is still in progress. This situation can lead to unnecessary resource consumption and delays. Fortunately, both platforms offer features to address this issue:
GitHub Actions: You can utilize the concurrency action to cancel a specific job within a workflow if certain conditions are met. This allows you to gracefully terminate jobs that are no longer relevant or necessary, helping you conserve build minutes and maintain workflow efficiency. In your action yaml file, add concurrency as part of your jobs setting, and set cancel-in-progress to true. Then you’re good to go.

Codemagic: Codemagic offers a similar feature through its user interface. You can configure your workflows to automatically cancel queued or running builds if a newer build is triggered. This ensures that you only utilize resources for the most up-to-date and relevant builds, optimizing your build minute consumption. In Codemagic workflow editor, under Build triggers, make sure you tick on Cancel outdated webhook builds

Step 3: Avoid unnecessary trigger
Indeed, in software development projects, not all files or folders are directly related to the technical implementation, and there are times when you don’t want your CI/CD pipeline to trigger based on certain changes. Both GitHub Actions and Codemagic offer options to control pipeline triggers effectively by specifying the conditions under which a pipeline should or should not run.
GitHub Actions: You can use path filters in your workflow configuration to restrict pipeline triggers to specific folders or files. By specifying paths or paths-ignore, you can ensure that your pipeline only runs when relevant code or files are modified, avoiding unnecessary builds triggered by unrelated changes.
paths: Trigger when and only when there are changes in these files or folders
paths-ignore: do not trigger when there are changes in these files or directories

Similar to paths and paths-ignore setting, you can also use branches and branches-ignore setting for the same purpose.
branches: Trigger when and only when the PR is targeting these branches
branches-ignore: do not trigger when the PR is targeting these branches
WARNING #1: there’s currently issue on paths-ignore, which does not work as expected. For more information on the issue, please visit https://github.com/actions/runner/issues/2324
WARNING #2: path filtering and branch filtering may block your PR if your PR status check depends on these pipeline.

Codemagic: Codemagic also provides configuration options to control pipeline triggers. You can define which branches or tags should be considered for pipeline execution, ensuring that your pipeline is selective about which changes it responds to.
In Codemagic workflow editor, simply enter the name of your targeted branch or tag, decide whether it is a source or target branch/tag, and click Add pattern button. And you’re all set.

Step 4: Run checks on cheaper OS instance (GitHub Action only)
In a conscious effort to optimize resource usage and minimize costs, I’ve made a strategic decision to break my CI/CD pipeline into two distinct jobs, each tailored to different needs. The first job, executed on a Linux machine, takes care of essential tasks like validation and analysis, which are platform-agnostic and don’t require the macOS environment. By utilizing Linux for these tasks, I can leverage the cost-effective build minutes offered by GitHub Actions.

The second job, reserved for macOS, is selectively triggered only when it’s absolutely necessary — specifically, when changes occur in the ios folder. This targeted approach ensures that I utilize macOS resources sparingly, as macOS build minutes are significantly more expensive. By employing an if check that monitors the iOS folder for changes, I’ve implemented an intelligent automation mechanism that minimizes the chances of running the macOS job unnecessarily.

In the picture above, you can see that I first check if there are any changes in ios folder in this branch, compare to the main branch (name is dev), and assign the output into param name can_run (step #1). Later on, in step Install Flutter (#2), and also step build iOS no codesign (#3) I added an if condition, and check if env.can_run is not empty. Which means these steps will only run if can_run is not empty.
This division of labor in my CI/CD pipeline not only helps control costs but also streamlines the workflow, ensuring that macOS resources are allocated efficiently for iOS-specific tasks only.
Step 5: Further reduce build time where possible
In order to further reduce build time, I’ve implemented some other strategies to further optimize the CI/CD pipeline and reduce build times effectively. By pushing changes for Flutter localization files directly into the codebase, I’ve eliminated the need to run the flutter pub run intl_utils:generate command in the pipeline, streamlining the process and saving valuable build minutes.

Additionally, I’ve taken a proactive approach to avoid running build_runner in the pipeline wherever possible. While Hive and fluttergen integration posed minimal challenges, handling the multitude of generated files from Mockito became a significant concern. I had over 400 test files, which means there are over 400 mocks files generated, although the actual number of mock files is not 400. My solution is to consolidate all Mockito-generated mocks into a single file. By condensing all @GenerateMocks into a single file, I’ve not only simplified my codebase, but also significantly reduced the time and resources needed for the build process.

However, I’ve later on moved to use Mocktail to replace Mockito as it totally does not require build_runner at all. I still follow the same approach from Mockito for my Mocktail implementation

These optimizations helped to efficiency and resource management, ensuring that my Flutter CI/CD pipeline operates at its peak performance while minimizing unnecessary build times and costs.
Conclusion
In conclusion, my journey to optimize our Flutter CI/CD pipeline has been a great improvement to my project and also my money. By breaking the pipeline into two jobs, selectively utilizing macOS resources, and implementing if checks, I’ve not only minimized costs but also enhanced the overall efficiency of our development workflow.
Furthermore, the decision to push intl and build_runner generated files into the codebase has significantly reduced build times, allowing us to focus on what truly matters: delivering high-quality Flutter applications.
I hope that these optimization strategies will prove valuable to our fellow Flutter developers. As the mobile development landscape continues to evolve, it’s essential to adapt and find innovative ways to enhance our workflows. By sharing our experiences and solutions, I hope to empower the community to navigate the world of CI/CD with greater efficiency, cost-effectiveness, and confidence. Together, we can continue to build exceptional Flutter apps that delight users and exceed expectations.
Comments