flutter_distribute_manager 3.0.0
flutter_distribute_manager: ^3.0.0 copied to clipboard
Ship Flutter apps to QA in one command: build the Android APK and iOS IPA, then upload to Google Drive (via rclone) and Diawi with shareable install links.
π flutter_distribute_manager #
Build your Flutter app and hand QA a working link β in one command. Android APK + iOS IPA β uploaded to Google Drive and Diawi automatically.
No Google Cloud project. No OAuth client to register. No manual Xcode archiving. No dragging files into shared folders.
$ flutter_distribute_manager
ββββββββββββββββββββββββββββββββββββββββββββββββ
flutter_distribute_manager Β· Build Β· Ship
ββββββββββββββββββββββββββββββββββββββββββββββββ
Project : my_app v1.4.2
? Which platform do you want to build?
1) Android (APK) (default)
2) iOS (IPA)
3) Android + iOS
β Built my_app_UAT_2026_06_19_0930_arm64-v8a.apk (24 MB)
[ββββββββββββββββββββ] 100% 24 MB / 24 MB 8.1 MB/s
β Uploaded to Google Drive
Share links:
Drive Β· my_app_UAT_2026_06_19_0930_arm64-v8a.apk
https://drive.google.com/file/d/β¦
π Contents #
| What it replaces Β· Why not fastlane? | Decide if this is for you |
| Install Β· Quick start | Get running in 3 minutes |
| Commands Β· Build flags | Everyday reference |
| CI/CD Β· JSON output Β· Exit codes | Automate it |
| How it works Β· Configuration | Understand it |
| Troubleshooting Β· FAQ | When something breaks |
π€ What it replaces #
Shipping a build to QA by hand is seven boring steps, every single time:
| Doing it by hand | With flutter_distribute_manager |
|---|---|
flutter build apk β wait β hunt for the file in build/β¦ |
One command builds it for you |
| Drag the APK into a Google Drive folder | Uploaded automatically |
| Right-click β Get shareable link β copy | Link printed in your terminal |
| Flat Drive folder β chaos after 10 builds | Organised App/Year/Month/ENV/ folders |
| "Which build is this β DEV or PROD?" | Environment stamped into every filename |
| Android and iOS are two separate chores | Both in a single run |
| Paste the link into Slack yourself | Optional auto-notification to Slack/Discord/Teams |
cd your_flutter_app
flutter_distribute_manager
That's the whole workflow.
π₯ Why not fastlane or Codemagic? #
They're excellent β and heavier. This tool is deliberately the small one.
| flutter_distribute_manager | fastlane | Codemagic / Bitrise | |
|---|---|---|---|
| Setup time | ~3 minutes, one command | Ruby, Gemfile, Fastfile, lanes | Sign-up, connect repo, YAML |
| Runs on your laptop | β Yes, that's the point | β Yes | β Cloud-only (paid minutes) |
| Config to write | None β it asks you | A Fastfile per project | A codemagic.yaml per project |
| Cost | Free (your Drive quota) | Free | Free tier, then per-minute |
| Google Drive delivery | β Built in, folder-organised | Plugin + service account | β Not typically |
| Needs a Google Cloud project | β No β rclone signs you in | β Service-account JSON | β Usually |
| Play Store / App Store submit | β Not its job | β Yes | β Yes |
| Code signing management | Passes through to Flutter | β match/sigh | β Managed |
Use this when you want QA builds out of your hands and into a tester's, right now, without standing up a pipeline. Use fastlane/Codemagic when you're automating store submissions, managing signing certificates across a team, or need a hosted build farm.
They compose fine, too β plenty of people use this for daily QA drops and fastlane for release day.
β What you'll need #
| Requirement | What it's for | How to get it |
|---|---|---|
| Flutter & Dart (Dart β₯ 3.5) | Building the app | flutter.dev/get-started |
| rclone | Uploading to Google Drive | init offers to install it via your package manager |
| Xcode (macOS only) | Building iOS IPAs | Mac App Store β full Xcode, not just the CLI tools |
| Diawi token (optional) | iOS one-tap install links | diawi.com β Account β API |
You do not need a Google Cloud project, an OAuth client ID, or a service-account key. rclone signs you in through your browser.
π» Platform support #
| Android build | iOS build | Drive upload | Diawi | |
|---|---|---|---|---|
| macOS | β | β | β | β |
| Linux | β | β (needs macOS + Xcode) | β | β |
| Windows | β | β (needs macOS + Xcode) | β | β |
π₯ Install #
dart pub global activate flutter_distribute_manager
Make sure the global bin directory is on your PATH (add to ~/.zshrc or ~/.bashrc):
export PATH="$PATH":"$HOME/.pub-cache/bin"
Verify:
flutter_distribute_manager --version
flutter_distribute_manager --help
β‘ Quick start (3 steps) #
# 1οΈβ£ One-time setup β installs rclone, connects Google Drive, saves your Diawi token
flutter_distribute_manager init
# 2οΈβ£ Check everything is wired up correctly
flutter_distribute_manager doctor
# 3οΈβ£ From inside your Flutter project β build & ship
cd path/to/your/flutter_app
flutter_distribute_manager
The tool detects your project, asks for platform + environment, builds, uploads, and prints the share links.
π‘ Not sure what a command will do? Add
--dry-run. It prints the exact plan, checks that Flutter and Google Drive are actually reachable, and exits non-zero if the real run would fail β without building anything.
π§ Commands #
| Command | What it does |
|---|---|
flutter_distribute_manager |
Build & ship (the default command, interactive). |
flutter_distribute_manager init |
One-time setup: rclone, Google Drive auth, Diawi token, webhook. |
flutter_distribute_manager doctor |
Health check: tools, config, Drive connectivity (+ macOS Gatekeeper). Exits 1 if anything is broken. |
flutter_distribute_manager config |
View or edit saved configuration (interactive or scriptable). |
flutter_distribute_manager reset |
Remove saved configuration and start over. |
Every command supports --help.
π© Build flags #
Every prompt has a flag, so any build is scriptable.
| Flag | Alias | Default | Description |
|---|---|---|---|
--platform |
-p |
ask β android |
android, ios, or both. |
--environment |
-e |
ask β DEV |
DEV, UAT, or PROD. |
--app-dir |
-d |
current dir | Flutter project path (searches parent dirs too). |
--app-name |
-n |
pubspec name: |
Label used in artifact file names. |
--flavor |
none | Build flavor passed through to flutter build. |
|
--flutter-path |
auto | Explicit Flutter executable (overrides PATH/fvm detection). | |
--clean |
off | Run flutter clean before building. |
|
--[no-]split-per-abi |
on | Split Android APKs per ABI. | |
--team-id |
-t |
ask / saved | Apple Developer Team ID (iOS). |
--export-method |
release-testing |
development, release-testing, ad-hoc, enterprise, app-store. |
|
--scheme |
Runner |
Recorded for reference; Flutter picks the scheme from --flavor. |
|
--upload-drive |
saved / ask | Upload artifacts to Google Drive. | |
--upload-diawi |
ask if token set | Upload the iOS IPA to Diawi. | |
--diawi-token |
saved | Diawi API token. Prefer FDM_DIAWI_TOKEN. |
|
--notify-url |
saved | Slack/Discord/Teams webhook. Prefer FDM_NOTIFY_URL. |
|
--retry |
3 |
Upload attempts before giving up. | |
--skip-build / --upload-only |
off | Reuse the last build instead of rebuilding. | |
--dry-run |
off | Print the plan and validate readiness; build nothing. | |
--json |
off | Emit a machine-readable JSON result on stdout. | |
--yes |
-y |
off | Assume defaults; never prompt (CI/CD). |
--quiet |
-q |
off | Silence build chatter and detail lines. |
--verbose |
-v |
off | Verbose logging. |
--version |
Print version. |
β οΈ
--yeschanges the defaults. With no terminal (or with-y), the tool never asks:--platformand--environmentfall back to whatever is saved in.flutter_distribute_manager.json, and then toandroid/DEV. Pass both explicitly in CI so a fresh checkout builds what you expect.
π Secrets via environment #
A token passed as a command-line argument is visible to every user on the
machine (ps auxww) and lands in shell history and CI command echoes. Prefer:
| Variable | Replaces |
|---|---|
FDM_DIAWI_TOKEN |
--diawi-token |
FDM_NOTIFY_URL |
--notify-url |
Precedence is flag β environment β saved config.
π Reset flags #
| Flag | Alias | Description |
|---|---|---|
| (no flag) | Soft reset β deletes machine + project config. Drive stays connected. | |
--all |
Full reset β also deletes the rclone remote (disconnects Google Drive). | |
--app-dir |
-d |
Project whose config to remove (default: current dir). |
--yes |
-y |
Skip the confirmation prompt (for CI). |
flutter_distribute_manager reset # keep the Google sign-in
flutter_distribute_manager reset --all # disconnect Drive too
Interactively, reset lists exactly what it will remove and asks you to type
reset to confirm β nothing is deleted by accident. If --all cannot actually
remove the remote, it says so and exits non-zero rather than claiming success.
βοΈ Config flags (scriptable) #
config is interactive by default, but every field has a flag so a
provisioning script or CI image can configure the tool without a terminal:
flutter_distribute_manager config --show # current values
flutter_distribute_manager config --show --json # machine-readable
flutter_distribute_manager config --set-remote gdrive \
--set-folder "Flutter Builds"
flutter_distribute_manager config --upload-to-drive # on by default
flutter_distribute_manager config --set-diawi-token - # clear it
Secrets are never printed back β --show reports set / ***set***, never
the value.
π€ CI/CD #
One-time: get credentials onto the runner #
--upload-drive needs an rclone remote and this tool's config. On a fresh
runner neither exists, and init is interactive β so provision them from
secrets instead. Run init once locally, then copy
~/.config/rclone/rclone.conf into a CI secret (it contains the Drive OAuth
refresh token β treat it as a credential).
GitHub Actions #
name: Ship to QA
on:
workflow_dispatch:
push:
tags: ['v*']
jobs:
ship:
runs-on: ubuntu-latest # use macos-latest to also build iOS
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with: { channel: stable }
- name: Install rclone
run: sudo apt-get update && sudo apt-get install -y rclone
- name: Restore credentials
env:
RCLONE_CONF: ${{ secrets.RCLONE_CONF }}
run: |
mkdir -p ~/.config/rclone
printf '%s' "$RCLONE_CONF" > ~/.config/rclone/rclone.conf
chmod 600 ~/.config/rclone/rclone.conf
- name: Install the CLI
run: |
dart pub global activate flutter_distribute_manager
echo "$HOME/.pub-cache/bin" >> "$GITHUB_PATH"
- name: Configure
run: |
flutter_distribute_manager config \
--set-remote gdrive \
--set-folder "Flutter Builds"
- name: Preflight
run: flutter_distribute_manager -p android --upload-drive --dry-run -y
- name: Build & ship
env:
FDM_NOTIFY_URL: ${{ secrets.SLACK_WEBHOOK }}
run: |
flutter_distribute_manager \
--platform android \
--environment UAT \
--app-name MyApp \
--upload-drive \
--yes --json > result.json
cat result.json
- uses: actions/upload-artifact@v4
if: always()
with:
name: build-result
path: result.json
π‘ The Preflight step is worth its 5 seconds:
--dry-runexits non-zero when Flutter is missing or the Drive token has expired, so you find out before paying for a 15-minute build.
Any other CI #
flutter_distribute_manager \
--platform both -e UAT --app-name MyApp \
--upload-drive --yes --json
All human-readable output goes to stderr, so stdout carries only JSON.
π JSON output schema #
--json writes exactly one JSON document to stdout:
{
"tool": "flutter_distribute_manager",
"success": true, // false when an upload failed (the build still succeeded)
"dryRun": false,
"app": "MyApp",
"version": "1.4.2+7", // straight from pubspec.yaml
"environment": "UAT", // DEV | UAT | PROD
"platform": "both", // android | ios | both
"artifacts": [
{
"name": "MyApp_UAT_2026_06_19_0930_arm64-v8a.apk",
"platform": "android", // android | ios
"sizeBytes": 25165824,
"path": "/abs/path/build/distribute/MyApp_UAT_β¦.apk",
"abi": "arm64-v8a" // omitted when not a per-ABI split
}
],
"links": [
{ "service": "Drive", "artifact": "MyApp_UAT_β¦.apk", "url": "https://β¦" },
{ "service": "Diawi", "artifact": "MyApp_UAT_β¦.ipa", "url": "https://i.diawi.com/β¦" }
],
"warnings": [ // non-fatal degradations; see below
"iOS was skipped: builds require macOS with Xcode (this machine is linux)."
],
"timingsMs": { "androidBuild": 182000, "driveUpload": 9400 }
}
warnings is the field to watch. A run can succeed while quietly doing
less than you asked β iOS skipped on a Linux runner, Drive upload with no
shareable link, Diawi requested with no token. success: true with a non-empty
warnings array means "shipped, but not everything you asked for."
# Fail the pipeline on any degradation, not just on a non-zero exit:
jq -e '.warnings | length == 0' result.json
Even when an upload fails, the JSON document is still written (with
"success": false) so you never lose the artifact paths and the links that did
succeed.
π’ Exit codes #
| Code | Meaning | Typical cause |
|---|---|---|
0 |
Success. | β |
1 |
Generic failure. | doctor found problems; an unexpected filesystem error. |
64 |
Bad command-line usage. | Unknown command/flag, bad value, no Flutter project, missing --app-dir. |
69 |
Upload failed, or a build is already running for this project. | rclone/Diawi failure; the concurrency lock is held. |
70 |
The build itself failed. | Gradle/Xcode error; no artifact produced; an unexpected crash. |
78 |
Missing or invalid configuration. | Flutter not found, Drive not set up or unreachable, --dry-run blockers. |
130 |
Interrupted (Ctrl-C). | Standard 128 + SIGINT. |
π How files land on Google Drive #
Flutter Builds/ β your root folder (set during init)
βββ MyApp/ β --app-name
βββ 2026/
βββ June/
βββ DEV/
βββ UAT/
β βββ MyApp_UAT_2026_06_19_0930_arm64-v8a.apk
βββ PROD/
Folder matching is case-insensitive, so re-runs reuse the same folders instead of piling up duplicates.
π¬ How it works #
No magic, and nothing you couldn't do by hand:
ββ 1. Detect βββββββββββββββββββββββββββββββββββββββββββββββββ
β Walk up from --app-dir for a pubspec.yaml with Flutter. β
β Resolve the toolchain: --flutter-path β fvm β PATH. β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββ 2. Pre-flight βββββββββββββββββββββββββββββββββββββββββββββ
β Take a per-project lock (two builds corrupt Gradle caches).β
β Prove rclone works AND the Drive remote actually answers β
β β before you pay for the build, not after. β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββ 3. Build ββββββββββββββββββββββββββββββββββββββββββββββββββ
β flutter build apk --release [--split-per-abi] β
β flutter build ipa --release --export-method β¦ β
β Failures are matched against known signatures and turned β
β into an actionable hint (a corrupt Kotlin cache is even β
β cleaned and retried once, automatically). β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββ 4. Stage ββββββββββββββββββββββββββββββββββββββββββββββββββ
β Copy only *this* build's artifacts into build/distribute/ β
β as MyApp_UAT_2026_06_19_0930_arm64-v8a.apk. β
β (flutter-apk/ keeps every variant ever built β picking the β
β wrong one is how a debug APK reaches QA.) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββ 5. Distribute βββββββββββββββββββββββββββββββββββββββββββββ
β rclone copy β Drive (App/Year/Month/ENV), then rclone link.β
β IPA β Diawi, polled until the install link is ready. β
β Retries use backoff; permanent errors are not retried. β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββ 6. Report βββββββββββββββββββββββββββββββββββββββββββββββββ
β Summary table + share links, --json document, and an β
β optional Slack/Discord/Teams post. Sent on failure too. β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βοΈ Configuration #
| Scope | Location | Stores |
|---|---|---|
| Machine | ~/.config/flutter_distribute_manager/config.json( %APPDATA%\flutter_distribute_manager\config.json on Windows) |
rclone remote, Drive folder, Diawi token, notify webhook. |
| Project | .flutter_distribute_manager.json in the project root |
last platform, app name, Team ID, scheme, export method, environment. |
The machine config holds API tokens, so on macOS/Linux it is written user-only
(chmod 600) via an atomic temp-file rename β there is no window in which it
sits world-readable, and an interrupted write cannot truncate a good config.
XDG_CONFIG_HOME is honoured.
Add .flutter_distribute_manager.json to .gitignore if you'd rather not
commit your per-project choices.
π― fvm-pinned projects #
If your app is pinned with fvm, you don't need a global flutter on your
PATH. The tool detects fvm (.fvmrc, .fvm/, or .fvm/fvm_config.json) and
runs the project's pinned SDK via fvm flutter β¦ automatically β so builds use
the exact version your project expects.
- It also finds a vendored SDK (
.fvm/flutter_sdk,.fvm/versions/<v>,$FVM_CACHE_PATH,~/fvm/versions/<v>) whenfvmitself isn't onPATH. doctorreportsFlutter (fvm 3.x.x)when it resolves via fvm.- If the project is pinned but nothing fvm-shaped is found, the run warns and records a warning in the result rather than silently using a different SDK.
- Override detection anytime with
--flutter-path /path/to/flutter.
π Troubleshooting #
command not found: flutter_distribute_manager after installing
The global bin directory isn't on your PATH:
export PATH="$PATH":"$HOME/.pub-cache/bin" # add to ~/.zshrc or ~/.bashrc
source ~/.zshrc
macOS shows a security popup for flutter / dart / rclone (Gatekeeper)
Run flutter_distribute_manager doctor β it lists any quarantined binaries and
prints the exact command to clear each one:
xattr -d com.apple.quarantine /path/to/binary
rclone remote β¦ not found / Google Drive not reachable
Your sign-in expired or the remote was renamed:
rclone config reconnect <remote>: # refresh the token
flutter_distribute_manager init # or redo setup
flutter_distribute_manager doctor # confirm
Note the remote name is bare β gdrive, not gdrive:.
iOS build fails immediately with an export-method error
Pass a value Flutter accepts: development, release-testing (the default β
exported as Xcode's ad-hoc), ad-hoc, enterprise, or app-store. For
signed builds also pass --team-id.
"Xcode command-line tools" are installed but the iOS build still can't archive
The Command Line Tools alone are not enough β you need full Xcode, and the toolchain has to point at it:
# Install Xcode from the App Store first, then:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept
(xcode-select --install installs the Command Line Tools, which is the thing
that isn't sufficient β it will not fix this.)
Noisy FINE: / SLVR: / IO : lines on the very first run
That's pub recompiling its snapshot right after
dart pub global activate β not this tool. It disappears on the next run. Use
--quiet to trim build chatter, or --json for clean machine-readable stdout.
A build "hangs"
Almost always a signing prompt, an SDK licence question, or a CocoaPods/Gradle prompt waiting on input. Run once interactively to answer it; the tool closes child stdin and bounds output draining so daemons can't wedge the run.
"Another build is already running for this project"
A per-project lock stops two concurrent builds corrupting the shared Gradle/Kotlin caches. It's an OS file lock, so it clears automatically when the other process exits β including if it crashes. If no other build exists, check the message: a setup problem with the temp directory is reported differently and the build continues unlocked.
The wrong APK got uploaded
build/app/outputs/flutter-apk/ accumulates every variant ever built and
Flutter never cleans it. The tool filters by build mode, flavor and timestamp β
but if you use flavors, pass --flavor with --skip-build too, otherwise
"the last build" means whichever variant is newest.
β FAQ #
Do I need a Google Cloud project or OAuth credentials?
No. rclone handles Google Drive sign-in in your browser during init.
Where is my Diawi token stored, and is it safe?
In the machine config file, written user-only (chmod 600) on macOS/Linux via
an atomic rename. It is never echoed to the terminal (entry is hidden), never
printed by config --show, and is redacted from logs and error messages β
including the ones Diawi itself echoes back.
Can I upload without rebuilding?
Yes β --skip-build (alias --upload-only). Add --flavor if you use flavors.
Does it work on Windows / Linux?
Yes for Android builds and Drive uploads. iOS builds require macOS + Xcode. On
Linux/Windows, --platform ios fails immediately with a clear message (exit
78) β and under --dry-run it is reported as a blocker, so the JSON document
is still emitted. --platform both builds Android and records a warning about
the skipped iOS in warnings.
How do I change a saved setting?
flutter_distribute_manager config (interactive), config --set-β¦ (scripted),
or override per-run with the matching flag.
Is this safe to run in CI?
Yes. Pass every value as a flag plus --yes for zero prompts, and --json for
a parseable result. Use --dry-run as a preflight gate. Distinct exit codes
(see above) let a pipeline react.
Does it upload my source code anywhere? No. It uploads only the built artifacts, only to the destinations you enable, and it has no telemetry.
π§© Programmatic use #
The building blocks are also exported as a library:
import 'dart:io';
import 'package:flutter_distribute_manager/flutter_distribute_manager.dart';
Future<void> main() async {
final project = FlutterProject.find(Directory.current.path);
if (project == null) {
stderr.writeln('No Flutter project here.');
exit(64);
}
final artifacts = await AndroidBuilder(const ProcessRunner(), Logger())
.build(project, environment: ReleaseEnvironment.uat);
for (final artifact in artifacts) {
print('${artifact.name} ${artifact.humanReadableSize}');
}
}
See example/ for a full walkthrough.
π€ Contributing #
Issues and PRs welcome at github.com/Ashutoshtripathi980/flutter_distribute_manager.
dart pub get
dart analyze # must be clean
dart test # 450 tests
π License #
MIT Β© 2026 Ashutosh Tripathi.