Development Tools

Flutter CI/CD with GitHub Actions: Automate Build, Test, and Release

Set up a real Flutter CI/CD pipeline in GitHub Actions — running tests and analysis on every PR, and building signed Android and iOS release artifacts — with working YAML you can adapt.

July 19, 20269 min readDeval Joshi
FlutterCI/CDGitHub ActionsDevOpsAutomationAndroidiOS

Flutter CI/CD with GitHub Actions: Automate Build, Test, and Release

For a long time my “CI pipeline” was: run flutter test locally, run flutter analyze, forget one of them half the time because I was in a hurry, and occasionally merge a PR with a failing test because I didn’t feel like waiting for it. That’s not a pipeline, it’s a habit, and habits skip steps under deadline pressure. What actually fixes it is making the checks run somewhere that isn’t optional — a CI server that blocks the merge if something’s broken.

This is the GitHub Actions setup I use: automated checks on every pull request, plus a release workflow that builds signed artifacts so “cutting a release” doesn’t mean running a ten-step manual checklist from memory.

Part 1: Testing and analysis on every pull request

This is the workflow that matters most day-to-day — it’s what catches a broken test or a lint failure before it merges to main, not after.

A few things worth calling out, because they’re easy to get subtly wrong:

Pin the Flutter version. flutter-version: '3.24.0', not 'stable'. If you track stable, your CI can start failing overnight because a new Flutter release changed something, with zero code changes on your end to explain it. Bump the pinned version deliberately when you’re ready, not as a surprise.

--set-exit-if-changed on dart format. Without this flag, the formatting check runs but doesn’t actually fail the build if formatting is wrong — it just reformats silently and exits 0. The flag is what turns “formatting is off” into a failed check that blocks merge.

Codecov needs a token now, even for public repos. Versions of codecov-action before v4 allowed tokenless uploads for public repositories; v4 requires CODECOV_TOKEN regardless. Grab it from your repo’s Codecov settings and add it as a GitHub secret, or the upload step will fail silently in a way that’s confusing to debug later.

Run this on every PR, not just pushes to main. The whole value is catching problems before merge. A workflow that only runs after code is already on main is a smoke detector that goes off after the house has burned down.

Part 2: Building a signed Android release

This is where most people’s first attempt at Flutter CI/CD stalls out — signing needs a keystore file and passwords that can’t live in your repo. GitHub’s encrypted secrets are the answer.

First, generate your keystore locally if you haven’t already (this part happens once, on your machine, not in CI):

Base64-encode it so it can be stored as a GitHub secret:

Add these as repository secrets in GitHub (Settings → Secrets and variables → Actions): KEYSTORE_BASE64, KEYSTORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD.

This triggers on a version tag (git tag v1.4.0 && git push --tags) rather than every push to main — you don’t want a signed release artifact built on every commit, only when you’ve deliberately decided to cut one. The keystore only ever exists inside the ephemeral CI runner, decoded from the secret at build time, and is gone when the job ends.

Part 3: Building an iOS release

iOS signing needs macOS runners and Apple’s certificate/provisioning profile setup, which is more moving parts than Android’s single keystore file.

That temporary keychain dance (security create-keychain, unlock-keychain, import) is unavoidable boilerplate — it’s how macOS code signing works regardless of CI provider, not something GitHub Actions makes more complicated than it needs to be. You’ll also need an ios/ExportOptions.plist checked into your repo describing your team ID and provisioning method, which you can generate once from Xcode’s own archive/export flow and reuse.

What I’d actually set up, in order

If you’re starting from nothing, don’t try to build all of this in one sitting:

  1. PR checks first (Part 1). This is the highest-value, lowest-effort piece — it catches broken code before merge, and there’s no signing complexity to fight through.
  2. Android release build once you’re tagging releases regularly and tired of running flutter build appbundle by hand.
  3. iOS release build last — it’s the most fiddly to set up correctly, and if you’re only shipping Android for now, there’s no reason to front-load that pain.

Skip straight to fully automated App Store/Play Store submission (fastlane supply/deliver lanes) only once the build pipeline itself is solid — automating deployment of a flaky build just means you ship broken releases faster.

Frequently asked questions

Do I need paid GitHub Actions minutes for this? Public repositories get GitHub Actions minutes free; private repos get a monthly free allowance before billing kicks in. iOS builds specifically use macOS runners, which consume minutes at a higher multiplier than Linux runners — worth knowing before you set every workflow to run on every push.

Can I use Codemagic or Bitrise instead of GitHub Actions? Yes, and both have more Flutter-specific tooling out of the box (some of the signing steps above are a checkbox instead of raw YAML). GitHub Actions is worth it mainly if you want everything — code, CI, and releases — in one place with no third-party account.

Should tests run on every commit or just pull requests? Both, generally — PR checks catch issues before merge, and it’s still reasonable to also run the same suite on direct pushes to main as a safety net, though PRs should be where the real gate is.

How do I avoid committing secrets by accident in these workflows? Everything sensitive above goes through ${{ secrets.NAME }}, never hardcoded in the YAML — GitHub encrypts these and masks their values in logs automatically. Never echo a secret directly to stdout for debugging; even masked, it’s a habit worth not forming.


Setting up CI/CD for a Flutter app and hitting a signing or workflow issue? Get in touch — happy to help debug it.