Framework

Flutter CI/CD you can self-host

One pipeline from a merged pull request to TestFlight and Play, running flutter build ipa and flutter build appbundle on Macs you own — with your FVM pin honoured and your lockfile enforced.

The pipeline, step by step

  • flutter/bootstrappub get with lockfile enforcement, honouring the FVM or SDK pin
  • flutter/analyzestatic analysis, with fatal-infos if you want it
  • flutter/testflutter test, shardable across runners
  • flutter/buildIPA or AAB — capability and credential needs follow the platform input
  • flutter/integration-testintegration_test on a simulator or emulator

Every one of these is in the step library, with the credentials it can ask for and whether it may run on a fork’s pull request — generated from the file the server itself reads.

The SDK pin is read, not assumed

.fvmrc or the constraint in pubspec.yaml decides which Flutter installs, per job, cached on the runner. A fleet does not need every version preinstalled, and two projects on different channels do not fight.

Obfuscation and split debug info, kept

--obfuscate with --split-debug-info produces a symbol mapping that is useless if it is thrown away with the build directory. It is collected as an artifact, so a crash report three weeks later still resolves to a line of your Dart.

One project, both platforms, one release

The iOS job runs on a Mac and the Android job runs wherever there is an Android SDK, in the same run, producing one release with both artifacts attached. Which is what a Flutter release actually is, and is awkward to express in tools that model a pipeline as a single linear script.

What a build produces

IPA, AAB, dSYM, mapping.txt, and the split-debug-info symbols.

Symbol files are collected on every build rather than on the ones somebody remembers to configure. A crash report that arrives three weeks later resolving to a memory address is a crash report nobody can act on, and the difference is a build-time decision that cannot be made retroactively.