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 pinflutter/analyzestatic analysis, with fatal-infos if you want itflutter/testflutter test, shardable across runnersflutter/buildIPA or AAB — capability and credential needs follow the platform inputflutter/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.