flutter_distribute_manager 2.0.0
flutter_distribute_manager: ^2.0.0 copied to clipboard
Ship Flutter apps to QA in one command. Builds Android APK and iOS IPA, then uploads to Google Drive (via rclone) and Diawi with shareable install links. CI/CD ready.
Changelog #
2.0.0 #
A full audit of the build, distribution and runtime layers. Several defects here meant the tool shipped the wrong artifact, or hung, rather than failing — the worst kind of bug for a release tool. Test suite grew from 100 to 361.
Why this is a major release. The behavioural changes are not backwards compatible, even though almost all of them are corrections:
- The tool now refuses to ship artifacts it previously shipped. Stale IPAs
left by a failed export, and debug APKs sitting in
flutter-apk/, were being uploaded to QA; both are now rejected. A pipeline that (unknowingly) depended on one of those files being produced will now fail instead. --export-method release-testingwas the default and was never a value Flutter accepts, so every iOS build without a Team ID failed at the usage check. The default now works, which means iOS builds that used to fail immediately will now actually run.- Exit codes are more specific: a missing binary or bad
--flutter-pathis now 78 rather than 255, "built but no artifact found" is 70 rather than 1, and--versionin a non-global position no longer exits 0. - A filesystem error while taking the build lock no longer reports "another build is already running" (exit 69); it warns and builds unlocked.
rcloneis now invoked with--retries 1, since its own retries were multiplying with the tool's retry policy for up to nine full uploads.ProcessRunner.capture/streamandDriveUploadergained members; anything implementing those interfaces must be updated.
Shipped the wrong artifact #
- iOS: a stale IPA could be uploaded to QA.
flutter build ipaexits 0 when the archive succeeds butxcodebuild -exportArchivefails, and it never cleansbuild/ios/ipa/. After a signing failure the tool picked up a previous IPA, stamped it with the current timestamp and shipped it. Builds now record a start time and reject anything older; export failure is detected in Flutter's output. - Android: debug APKs were shipped alongside release ones. The Flutter
Gradle plugin copies every variant into one flat
flutter-apk/directory and never deletes anything, so any developer who had runflutter runhad anapp-debug.apksitting there. Artifact discovery is now variant- and flavor-aware, and rejects stale files. --flavorwas passed to the build but ignored when locating artifacts, so a multi-flavor project could ship the wrong flavor.- Release builds silently signed with the debug keystore — the Flutter template's default — now warned about before the build starts.
iOS builds that could never succeed #
--export-method release-testingis not a value Flutter accepts (app-store,ad-hoc,development,enterprise), and it was the default. Every iOS build without a Team ID failed with a usage error.- Hardcoded
signingStyle: automaticbroke every manual-signing project. - ExportOptions.plist: XML is now escaped,
destinationis set, App Store exports setuploadSymbolsandmanageAppVersionAndBuildNumber: false, and the obsoletecompileBitcodekey is gone.enterprisewas missing from the export-method model entirely. - Xcode presence was checked with
which xcodebuild, which succeeds with Command Line Tools only; the build then died minutes later.
Hangs #
- The child process's stdin was never closed, so anything that reads it —
sudo, CocoaPods, a Gradle prompt, an SDK licence question — hung forever with no timeout. - A daemon inheriting the pipe blocked the run after a successful build. Gradle and adb keep stdout open, so waiting for end-of-stream meant waiting for the daemon to exit. Now a bounded post-exit drain.
- A single non-UTF-8 byte killed the CLI mid-build with an unhandled
FormatExceptionand exit 255 — acaféin a path was enough. Decoding is now lenient.
Distribution #
- The Diawi API token leaked into logs and stack traces (the poll URI
carries it, and
ClientException.toString()includes the URI). Redacted. The notification webhook URL leaked the same way. - Discord notifications always failed — the payload used Slack's
textfield; Discord requirescontent. Slack, Discord and Teams are now detected and formatted per dialect. - A 401 or 413 re-uploaded the whole IPA three times. 4xx is no longer
retried (except 408/425/429), and
Retry-Afteris honoured. rclone's own--retries 3multiplied with the retry policy for up to 9 full uploads; now pinned to 1. - The Drive progress bar never worked and destroyed error messages. It
required a
Transferred:prefix that--stats-one-linedoes not emit, so every progress line fell into the 20-line error tail and evicted the real error within ten seconds. - No HTTP timeouts on Diawi — a stalled connection hung the CLI after the build. Poll bound raised from 3 minutes, and a timeout now names the job id instead of stranding an uploaded build.
- A failed
rclone linkwas silent; QA got a summary with no link and no reason. AddedDriveUploader.preflight()to validate the remote and its token before a build is paid for.
Runtime and config #
- A filesystem error was reported as "another build is already running",
which no amount of waiting fixes. Lock paths are now canonicalized, so
-d appand-d app/no longer take separate locks on one project. - One unknown config field discarded the entire config, silently losing the saved Diawi token and rclone remote.
ProjectConfig.save()crashed a finished run on a read-only checkout, after the uploads, so the user never saw their links.- Trailing YAML comments were parsed into the app name and version, landing in artifact filenames and Drive paths.
- The project is now found by walking up from a subdirectory.
--versionanywhere in the arguments exited 0, so a CI step with a mistyped flag passed silently.- The saved Diawi token was echoed to the terminal by
configandinit; the config file is nowchmod 600. - FVM detection understood only the
.fvm/flutter_sdklayout.
1.1.1 #
Bug fixes from an adversarial review of 1.1.0. No API or flag changes.
- Build lock moved out of
build/— it lived inbuild/.fdm.lock, whichflutter cleandeletes, allowing a second concurrent build to slip in. The lock now lives in a stable OS-temp path keyed by the project, survives a clean, and no longer unlinks on release (closing an unlink race) or NUL-pads its pid stamp. flutter cleanfailures are now surfaced instead of being swallowed (the auto-retry path no longer silently rebuilds against a dirty cache).- More accurate exit codes — "no artifacts" and artifact-staging I/O
failures report build (70); invalid
--retryreports usage (64). - fvm version reporting now resolves from the project directory, and a warning is shown when an fvm-pinned project falls back to a global Flutter.
gradleDeclaresAbiFiltersno longer false-positives onabiFiltersinside block/inline comments.- Build-failure signing hint no longer fires on benign debug-keystore log lines.
- Bundled global flags (e.g.
-vq doctor) are now routed correctly instead of being treated as a positional tobuild.
1.1.0 #
Real-world hardening from testing on a large, fvm-pinned production app. All existing flags remain backward-compatible.
Added #
- fvm support — the tool now detects fvm-pinned projects (
.fvmrc,.fvm/,.fvm/fvm_config.json) and invokes the project's pinned Flutter viafvm flutter …, so version-pinned apps build correctly with no manualPATHexport. New--flutter-pathoverrides the executable explicitly. --clean— runflutter cleanbefore building. The tool also auto-cleans and retries once when it detects a corrupted Kotlin cache.- Concurrent-build lock — a per-project OS lock (
build/.fdm.lock) refuses a second simultaneous build, preventing Gradle/Kotlin cache corruption. --flavor— passed through toflutter build.--quiet/-q— silence non-essential output (build chatter, detail lines); errors, warnings and the summary still show.- Distinct exit codes for CI: build failure (
70), upload failure (69), missing configuration (78).
Improved #
--split-per-abi↔ndk.abiFiltersconflict — when an app declaresabiFilters, the tool builds a single APK by default (with a notice) instead of failing after a multi-minute build. An explicit--split-per-abi/--no-split-per-abistill wins.- Actionable failure hints — known Gradle/Kotlin/signing/SDK/v1-embedding failures now print a one-line fix instead of just raw output.
doctor— fvm-aware Flutter check (green when resolvable via fvm), verifies the configured rclone remote exists, and reports resolved Flutter/Java versions.init— empty Diawi-token / webhook answers now skip with a clear notice.- Pre-build summary shows the resolved Flutter toolchain (and fvm), plus a
reminder that
--environmentonly labels artifacts and sets--dart-define=ENV=….
1.0.1 #
Documentation and metadata polish (no functional changes).
- Reworked README and example walkthrough for clarity.
- Tightened the package description.
- Updated LICENSE copyright holder.
1.0.0 #
Initial release.
Build & distribute #
build(default command): build release Android APK (--split-per-abi) and iOS IPA for one or both platforms.- Environment tagging (DEV / UAT / PROD) with timestamped artifact names, also
passed to the build as
--dart-define=ENV=…. - Google Drive uploads via rclone with an automatic
App/Year/Month/Environmentfolder structure and case-insensitive folder matching to avoid duplicates. - Diawi integration for iOS one-tap install links.
Professional touches #
- Resilient uploads — Drive and Diawi transfers retry with exponential
backoff (
--retry). - Live progress bar — rclone transfers render as a branded
[████░░░] 64% 42 MiB / 66 MiB 4.2 MiB/s ETA 6smeter. - Per-step timing — build/upload durations reported in the summary.
--json— machine-readable result on stdout for CI pipelines.--dry-run— preview the full plan without building or uploading.- Team notifications — post a build summary to a Slack/Discord/generic
webhook (
--notify-url). - Box-drawing UI and a summary table for a polished terminal experience.
Setup & diagnostics #
init: one-time setup wizard (install rclone, authenticate Drive, save Diawi token and notification webhook).doctor: environment & connectivity health checks, including a macOS Gatekeeper section that lists quarantined binaries and the exactxattrcommand to clear them.config: interactive configuration editor with full reset.
Everything else #
- Interactive prompts plus a fully non-interactive mode (
--yes) for CI/CD. - Per-machine and per-project configuration files.
- A reusable library API (
package:flutter_distribute_manager/…).