Flutter CI/CD Guide with Codemagic

Nitin PoojaryNitin Poojary
13 min read

Introduction

Publishing a Flutter app to both the Play Store and App Store can be a repetitive process. It involves merging code with team members, running tests, performing manual QA, uploading builds, and repeating it all for every release.

Sounds tedious, right?

That's where CI/CD comes in. In this blog, we'll explore how to automate this workflow using Codemagic, so you can spend less time on shipping and more time on building.

What is CI/CD Pipeline?

CI/CD stands for Continuous Integration and Continuous Delivery/Deployment. The aim is to automate the repetitive tasks of software development like building, testing, and deploying with minimal human involvement.

Continuous Integration (CI) involves regularly merging code changes into a shared repository. Whenever a pull request is made, automated tests are run. The code is merged only after all tests pass successfully. If any tests fail, the developer needs to fix the issues and push the changes again. This process helps catch bugs early in development.

Continuous Delivery (CD) starts after the CI phase. Once all tests pass and the code is merged, it can be automatically deployed to a testing or staging environment. Here, additional integration tests are run if they haven't been done yet.

Continuous Deployment goes a step further, changes are not just delivered to staging but are automatically deployed to production, making app updates available to users.

Why CI/CD matters for Flutter Developers?

CI/CD helps you streamline and automate repetitive steps in your Flutter development workflow. Instead of manually testing and distributing builds at each stage, you can:

  • Automatically run tests before each merge to catch issues early

  • Automatically share testing builds with the QA team

  • Upload builds to staging with production credentials for final checks

  • Deploy to production with minimal manual steps

Now that we understand the why let’s dive into how.

Getting started with Codemagic

If you're new to Codemagic, I suggest checking out their official Getting Started Guide. It will guide you through creating an account, connecting your repository, and understanding the user interface.

Once you're comfortable with the interface and how workflows generally work, we'll proceed to set up our Flutter app and create custom workflows.

Don't worry if the documentation seems overwhelming or if you don't understand everything immediately. Things will become clearer as we go through the implementation step-by-step.

Setting Up CI/CD for Your Flutter App Using Codemagic

First, ensure you have a Flutter app hosted on a git hosting provider like GitHub, GitLab, or Bitbucket. I have already created one on GitHub and uploaded a default counter app. You can create a private repository if you prefer.

Home Page

Go to your Codemagic home page. If you haven't created an app yet, you should see something like this:

Click on "Add application," then choose the git hosting provider where your project is hosted. Select the repository and click "Add application."

After successfully connecting to your repository, you should see the app created. Click on the "Finish build setup" button to go to the workflow editor screen. Here, you'll find a default workflow that you can edit to fit your needs. A workflow is simply a series of steps designed to achieve a goal. In this case, the steps include building, testing, and deploying your application. You can create multiple workflows for different purposes, such as testing, staging, and deploying. For now, we will use the Default Workflow.

Build for platforms

Select the platform you want to build for.

Publish updates to user devices

We will keep this option "Disabled." This tool, developed by Shorebird, is called Code Push and is used to deploy app updates directly to users' devices.

Run build on

Here, we can choose the instance for our workflow to run on. In the free plan, we can currently only select macOS M2.

Build triggers

Here, we can set triggers for our workflow.

Trigger on push: This starts a new build whenever you push commits directly or merge pull requests or merge requests on a branch.

Trigger on pull request update: This starts a new build whenever a pull request is created or updated.

Trigger on tag creation: This starts a new build whenever you push a new tag.

Cancel outdated webhook builds: When checked, Codemagic will automatically cancel all ongoing and queued builds triggered by webhooks on push or pull request commit when a more recent build has been triggered for the same branch.

If none of the triggers are checked, it means we have to manually run the workflow when needed. This is especially useful for distribution when you don't want to automatically send updates to users.

Here, you can either enter the exact name of the branch or tag, or use wildcard symbols to match multiple branches or tags. For more advanced matching patterns, please refer to the Wildcard Match Documentation. For now, we will leave this as it is.

Environment variables

This is where we set environment variables for our app. We will discuss this further when we cover the strategy for CI/CD. For now, we will leave this as it is.

Tests

Tests include static code analysis, integration tests, and unit tests. By default, tests are disabled, and for now, we will only enable unit tests.

For integration tests, we can choose a virtual device to run them on.

Build

After all the tests pass, the workflow will create builds of the app. By default, it is set to Debug, but I have chosen Release here. For Android build formats, I have selected APK.

If your project uses Flavors, you can also build for a specific flavor.

Distribution

We are not covering distribution in this blog. If you want to learn about it, you can refer to Publishing to Google Play and Publishing to App Store.

Notification

Finally, after each step is completed successfully, the workflow can send builds via email and Slack. Enter your email in the "Add recipients" textbox to receive the builds.

Now that everything is set up, let's try pushing a change to the main branch.

Click on "Builds" in the navigation bar on the left, and you should see your build displayed like this.

From the controls on the right, we can stop the build, view it, or choose other options.

By clicking on "View build," we can see more details about the progress and output of the build, as shown below.

As shown in the image above, we can click on a step to view its details, as illustrated below.

After the workflow completes successfully, you'll receive the build at the email addresses you provided.

And just like that, we've set up a simple CI/CD pipeline for a Flutter app using Codemagic. No complex configurations, just the essentials to get things running.

In this example and the one I'll show in the next section, I used the Workflow editor UI to create the workflows. Alternatively, you can create them using a YAML file by placing the codemagic.yaml file at the root of your app and including all the workflow details. To learn more about this, you can refer to building an app with codemagic.yaml.

Now that we've covered the basics, let's look at an example to see how CI/CD can be set up for a real-world Flutter app with multiple branches, environments, and workflows.

CI/CD Strategy for Real World Flutter Projects

In this section, we’ll discuss about the CI/CD workflows to help you understand how real-world Flutter apps might be organized.

For this, we will need four branches in our repository: dev, test, staging, and main (prod), along with five workflows: dev, qa-test, qa-release, staging, and prod. You could use Flutter flavors or environment variables to manage builds for different environments (test, staging, prod). We are going to use environment variables in this example. The flow will be as follows:

Dev Branch Workflow (dev)

  • After completing a feature or bug fix, team members will open a pull request from their feature branch to the dev branch.

  • This triggers the dev workflow, which runs static analysis and unit tests.

  • If tests fail, developers need to fix the issues and push their changes again.

  • Once all tests pass, the pull request can be merged.

  • The dev branch now contains verified code ready to move forward.

Test Branch Workflows (qa-test and qa-release)

  • When a feature is ready for QA testing, a pull request is created from dev to test.

  • This triggers the qa-test workflow, which:

    • Runs unit and integration tests.

    • Ensures the code is stable before merging into test.

  • After the PR is approved and merged into the test branch:

    • The qa-release workflow is triggered.

    • It builds the app using test environment credentials.

    • The resulting build is automatically sent to the QA team via email or Slack.

  • If QA finds issues, fixes are pushed to dev, and the process repeats.

Staging Branch Workflow (staging)

  • Once QA approves the build on the test branch, the changes are merged into the staging branch.

  • This triggers the staging workflow, which:

    • Builds the app using production environment credentials.

    • Uploads the build to Google Play Internal Testing and TestFlight.

    • This step is used for internal verification before public release.

  • If needed, product or stakeholders review the app and provide final feedback.

Production Branch Workflow (release)

  • After the staging build is approved, the changes are merged into the main (or prod) branch.

  • Merging to the main branch does not start any workflow because the prod workflow is set to manual to avoid accidental releases. When this workflow is triggered, it will:

    • Build the app with production environment credentials.

    • Publish the build to the Play Store and App Store.

Flutter App Setup for CI/CD with Codemagic

To demonstrate the CI/CD setup, I'm using a simple Flutter app hosted on GitHub. The app simply displays text showing the name of the environment it's running in.

You can view the source code here: GitHub Repo

Once you're familiar with the code from the repo, we'll move on to setting up Codemagic to automate the build, test, and deployment process.

Setting Up Workflows

In Codemagic, on the workflow editor screen, you'll find "Workflow settings" on the right side. Click "Duplicate workflow" to create a new workflow with the same settings as the current one. You can then rename the workflow. Create a total of five workflows and name them dev, qa-test, qa-release, staging, and prod.

After completing these steps, you should be able to see this.

Workflow Configuration

We will now set up the workflows based on the strategy we discussed earlier.

dev branch Workflow setup

For the dev branch, we'll set up a dev workflow that starts when a pull request is opened or updated, targeting the dev branch. This workflow runs analysis and unit tests to ensure the new changes are safe to merge.

Select “Run tests only”

Update the build triggers to listen for pull requests with the "dev" branch as the "Target" branch.

Before adding environment variables, let's first see how they can be used in a Flutter app. There are two ways to do this:

  • Using a .env file: Create a .env file at the root of the project. You can access the values in your app using packages like flutter_dotenv or envied.

  • Through the —dart-define argument: When running the Flutter app through the terminal, you can add environment variables like this:

flutter run --dart-define=KEY1=VALUE1--dart-define=KEY2=VALUE2

Alternatively, if you usually run the app using Ctrl + F5, you can edit the launch.json file. If you don't see a launch.json file in VS Code, you can create one as shown below:

Inside launch.json, add the args key as shown below:

    {
      "name": "app_name",
      "request": "launch",
      "type": "dart",
      "args": ["--dart-define=KEY1=VALUE1"]
    },

The variables added this way can be accessed with String.fromEnvironment('KEY1').

Codemagic uses the second method in its workflow, which is through --dart-define. Personally, I prefer using a .env file. If you checked out the GitHub Repo I shared earlier, you might have noticed that I used the envied package with a path to the .env file. However, that approach doesn't work and causes an error during the workflow because it can't find the .env file. So, I experimented and found another solution.

Just above the "Tests" phase, you'll see a "+" symbol. Click on it.

As it states, we can add our custom scripts between phases, either after a specific phase is completed or before a phase starts. In the "Pre-test script" section, I added the following script:

touch .env
echo "ENV_NAME=DEV" > .env

This script creates the .env file with some values in it, and like all scripts, it runs at the root level of the project on the virtual machine. If you want to use a .env file like I do, you'll need to do something similar. Otherwise, you can use --dart-define without any issues. To use environment variables with --dart-define, add the environment variables directly using the UI, as shown below.

Add environment variables with these details:

  • Variable name: ENV_NAME

  • Variable value: DEV

Check Secret if you want to hide the value in the UI and build logs, and to prevent editing of the variable.

Enable both tests, but we don't want to run integration tests; we are only interested in running Flutter analyze and unit tests.

That's it! We have configured the dev workflow.

test branch Workflow setup

This branch will trigger two workflows:

qa-test: This starts when you create or update a PR from the dev branch to the test branch. It runs all tests, including analysis, unit, and integration tests.

qa-release: This starts on merge. It creates a build using test environment credentials and sends it to the QA team.

For qa-test select “Run tests only” and for qa-release select the build platforms.

For the qa-test workflow, select "Trigger on pull request update." Since we need to run this workflow when creating or updating a PR from dev to test, set dev as the source branch and test as the target branch.

For the qa-release workflow, select "Trigger on push." Since we need to run this workflow on merge, watch the test branch.

If you are using a .env file, add the script mentioned above before the "Test" phase in both workflows. Otherwise, add the environment variables through the UI.

Enable all the tests for qa-test optionally you can do the same for qa-release workflow.

For the qa-release workflow, in the "Build" phase, I will select the APK build format with "Release" mode.

For the qa-release workflow, in the "Notifications" phase, add your email address where you want to receive the build.

We have set up the qa-test and qa-release workflows.

staging branch Workflow setup

This branch will trigger the staging workflow, which creates the build using production environment credentials and uploads it to Google Play Internal testing and TestFlight. We will not upload the builds to stores; instead, we will share the build using “Notifications.” If you want to learn more, you can refer to Publishing to Google Play and Publishing to App Store.

Based on that, the only differences here compared to qa-release will be the trigger branch and the environment variables.

Select "Trigger on push" and watch the staging branch.

Add the environment variable.

We have configured the staging workflow.

main branch Workflow setup

This branch will trigger the prod workflow, which creates the build using production environment credentials and uploads it to the Google Play Store and App Store. Since we are not uploading the build to the stores, this workflow will be the same as the staging workflow, except we will not add any triggers here. This is because the workflow is manual to prevent accidental releases. Remove all triggers if any exist; there's no need to watch any branch or tags.

To test your setup:

  • Create a feature branch, make a small change, and open a PR to the dev branch.

  • Observe the dev workflow running and confirm your changes.

  • Follow the process by merging from devteststagingmain through PRs and observe your configured workflows in action.

Check Codemagic’s build logs and the build artifacts you receive via email to ensure each step works as expected.

Conclusion

If you've made it this far, thank you for reading!

We started with the basics of CI/CD, explored how it fits into a Flutter development workflow, and created a complete Codemagic setup from scratch, covering everything from testing PRs to sending builds for QA and getting ready for production.

You now have a working example that you can adapt and extend for your own Flutter apps.
CI/CD might seem overwhelming at first, but once it's set up, it becomes a powerful tool for shipping quickly and confidently.

Have feedback or suggestions? I'd love to hear from you.

10
Subscribe to my newsletter

Read articles from Nitin Poojary directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nitin Poojary
Nitin Poojary