local_build 0.2.1 copy "local_build: ^0.2.1" to clipboard
local_build: ^0.2.1 copied to clipboard

Extensible local Flutter build and release orchestration.

Flutter Local Build #

Flutter Local Build is a command-line tool for building and distributing Flutter applications from a developer machine or CI runner. It supports ordinary single-app repositories, flavor-based apps, and multi-app monorepos from one YAML configuration.

It can:

  • build Android App Bundles and iOS IPA files with Flutter or FVM;
  • create Shorebird releases and patches;
  • resolve the next build number from local pubspecs, Google Play, and App Store Connect;
  • upload artifacts to Google Drive, Google Play, TestFlight, and Firebase App Distribution; and
  • report successful and failed jobs through Slack and SMTP.

Requirements #

  • Dart 3.6 or later;
  • a Flutter SDK available through flutterCommand;
  • Xcode on macOS for iOS builds and TestFlight uploads; and
  • the Shorebird CLI on PATH when using Shorebird.
  • the Firebase CLI on PATH (or configured through command) when using Firebase App Distribution.

Provider accounts and credentials are only required for publishers you configure and use.

Install #

dart pub global activate local_build
local_build --help

If local_build is not found, add Dart's global executable directory to your PATH. Dart prints the appropriate directory when activating the package.

Update or remove the CLI with:

dart pub global activate local_build
dart pub global deactivate local_build

Quick start #

Create .local-build.yaml next to the Flutter application's pubspec.yaml:

version: 1

apps:
  app:
    path: .
    androidPackage: com.example.app
    iosBundleId: com.example.app

profiles:
  development:
    flavor: development
    target: lib/main_development.dart
    dartDefines: config/development.json
  production:
    flavor: production
    target: lib/main_production.dart
    obfuscate: true
    splitDebugInfo: build/symbols
    extraArgs: [--tree-shake-icons]
    androidArgs: [--split-per-abi]
    iosArgs: [--export-method, app-store]

versioning:
  buildNumber:
    strategy: pubspec

Preview the production jobs and generated commands:

local_build build --profile production --dry-run

Build both configured platforms:

local_build build --profile production --platform all --build-name 1.4.0

Commands #

local_build build --profile <name> [options]
local_build patch --profile <name> [options]

build runs flutter build, or shorebird release when --enable-shorebird is supplied. patch always runs shorebird patch.

Command options #

Option Required Default Description
--config <path> No .local-build.yaml YAML configuration file. Relative paths are resolved from the current shell directory.
--profile <name> Yes None Root profile to use. The name must exist under profiles.
--app <name> No All apps Select an app declared under apps. Repeat the option to select multiple apps.
--platform <value> No all Build android, ios, or all. Jobs are only created where the selected app has the corresponding store identifier.
--android-artifact <value> No aab Build Android as an App Bundle (aab) or Android Package (apk). The selected file is passed to Google Drive, Firebase App Distribution, and Play Console. Shorebird Android requires aab.
--build-name <version> No Flutter project value Pass a semantic version such as 1.4.0 to Flutter or Shorebird as --build-name. Flutter Local Build does not calculate build names.
--build-number <number> No Resolved by configuration Positive integer override. It takes precedence over versioning.buildNumber.strategy.
--enable-shorebird No Off Use shorebird release for a build command. It does not change a patch command, which already uses Shorebird.
--publish No Off Run every configured publisher that supports each successfully built artifact.
--firebase-group-aliases <alias> No Publisher setting Override publishers.firebaseAppDistribution.groupAliases for this invocation. Repeat it or provide comma-separated aliases.
--no-notify No Off Disable all configured Slack and SMTP notifications for this invocation.
--dry-run No Off Print build, publishing, and notification plans without executing those actions. See Dry-run behavior.
-h, --help No Off Print top-level usage and examples.

Examples:

# Build and publish an APK for one Android app.
local_build build --profile production --app customer --platform android \
  --android-artifact apk --publish

# Build and publish all eligible applications and platforms.
local_build build --profile production --publish

# Create a Shorebird Android release.
local_build build --profile production --platform android --enable-shorebird

# Patch one iOS application through Shorebird.
local_build patch --profile production --app customer --platform ios

Configuration file #

The configuration root must be a YAML map. Paths in the following sections are relative to the directory containing the configuration unless noted otherwise.

Root fields #

Field Type Required Default Description
version Integer Yes None Configuration schema version. The only supported value is 1.
flutterCommand List of strings No [flutter] Executable and any fixed arguments used for non-Shorebird builds. It must contain at least one item.
apps Map Yes None Named application definitions. At least one app is required.
profiles Map Yes None Named shared build profiles. --profile selects one of these names.
versioning Map No {} Automatic versioning settings. Currently only buildNumber is read.
publishers Map No {} Artifact-publisher settings for Google Drive, Play Console, TestFlight, and Firebase App Distribution.
notifiers Map No {} Slack and SMTP notification settings. Each notifier is enabled unless its enabled field is false.

Use FVM by changing the Flutter command:

flutterCommand: [fvm, flutter]

flutterCommand is ignored for Shorebird jobs; those invoke the shorebird executable directly.

apps #

Each key under apps is the name accepted by --app.

apps:
  customer:
    path: apps/customer
    androidPackage: com.example.customer
    iosBundleId: com.example.customer
    firebaseAndroidAppId: 1:1234567890:android:abcdef
    firebaseIosAppId: 1:1234567890:ios:abcdef
App field Type Required Default Description
path String No . Flutter application directory, relative to the configuration file. Build commands run from this directory.
androidPackage String For Android jobs None Google Play package name, for example com.example.customer. It may instead come from the selected root or app-specific profile.
iosBundleId String For iOS jobs None App Store bundle identifier. It may instead come from the selected root or app-specific profile.
firebaseAndroidAppId String For Android Firebase uploads None Firebase Android App ID. This is the value shown in Firebase project settings, not the Android package name.
firebaseIosAppId String For iOS Firebase uploads None Firebase iOS App ID. This is the value shown in Firebase project settings, not the bundle identifier.
profiles Map No {} App-specific overrides keyed by a root profile name. These use the same fields as a root profile.

An app may intentionally support only one platform. The selected profile is merged before platform eligibility is checked, so identifiers can live at the app or profile level. If the effective configuration has no identifier for a selected platform, that app/platform is skipped. If the complete selection produces no jobs, the command fails with a configuration error.

profiles #

A root profile describes the arguments shared by every selected app. All profile fields are optional.

profiles:
  production:
    flavor: production
    target: lib/main_production.dart
    dartDefines:
      API_URL: https://api.example.com
      ENABLE_LOGGING: false
    obfuscate: true
    splitDebugInfo: build/symbols
Profile field Type Default Generated argument / behavior
flavor String None --flavor <value>
target String Flutter default --target <value>; normally an app-relative Dart entrypoint.
dartDefines Map, string, or list of strings None Explicit --dart-define values or one or more --dart-define-from-file paths.
obfuscate Boolean false Adds --obfuscate only when the YAML value is exactly true.
splitDebugInfo String None --split-debug-info=<path>; typically used with obfuscate.
extraArgs String or list of strings None Additional arguments appended to both Flutter and Shorebird commands. A string is exactly one argument; use a list to preserve multiple argument boundaries.
androidArgs String or list of strings None Additional arguments appended only to Android commands.
iosArgs String or list of strings None Additional arguments appended only to iOS commands.
androidPackage String App value Overrides the app's package name for this profile. It affects job selection, store lookup, and Google Play publishing, not the Flutter command itself.
iosBundleId String App value Overrides the app's bundle identifier for this profile. It affects job selection, store lookup, and TestFlight publishing, not the Flutter command itself.
firebaseAndroidAppId String App value Overrides the Firebase Android App ID for this profile or tenant.
firebaseIosAppId String App value Overrides the Firebase iOS App ID for this profile or tenant.

Paths passed as profile arguments are interpreted by Flutter or Shorebird from the app's working directory.

Pass-through arguments are supplied directly to the child process without shell parsing or evaluation. Write a flag and its separate value as two list items, or use the tool's --flag=value form:

profiles:
  production:
    extraArgs:
      - --tree-shake-icons
    androidArgs:
      - --analyze-size
      - --code-size-directory
      - build/code-size/android
    iosArgs:
      - --export-method=app-store

extraArgs are appended after Flutter Local Build's generated arguments, followed by the matching platform-specific arguments. The same arguments are also passed to shorebird release and shorebird patch, so only configure flags accepted by every action that uses that profile. Use androidArgs and iosArgs when a flag is platform-specific.

App-specific profile overrides #

Flavor-specific store identifiers and settings can be placed below an app:

apps:
  customer:
    path: apps/customer
    androidPackage: com.example.customer
    iosBundleId: com.example.customer
    profiles:
      development:
        androidPackage: com.example.customer.dev
        iosBundleId: com.example.customer.dev
        dartDefines:
          TENANT: customer

An override is applied only when its key matches the selected root profile. Scalar values replace their root-profile value. Explicit Dart-define maps are merged, with app values winning duplicate keys. Define-file lists are appended. extraArgs, androidArgs, and iosArgs are also appended, with shared-profile arguments first and app-specific arguments last. obfuscate: true in either profile enables obfuscation; an app override cannot turn off obfuscation already enabled by the root profile.

Dart defines #

dartDefines accepts exactly one of three shapes in each profile.

Explicit key-value pairs #

dartDefines:
  API_URL: https://dev.example.com
  ENABLE_LOGGING: true
  RETRY_COUNT: 3

Keys and values are converted to strings and emitted as:

--dart-define=API_URL=https://dev.example.com
--dart-define=ENABLE_LOGGING=true
--dart-define=RETRY_COUNT=3

One define file #

dartDefines: config/development.json

Multiple define files #

dartDefines:
  - config/common.json
  - config/development.env

Each path must be a string and is emitted as --dart-define-from-file=<path>. The file contents and supported formats are interpreted by the installed Flutter or Shorebird version.

One profile node cannot directly mix a map and file list because YAML gives dartDefines one value. A shared root profile and an app-specific override can combine them. Define files are emitted first and explicit values afterward, so explicit values take precedence when Flutter encounters duplicate keys.

Automatic build numbers #

Configure build-number resolution under versioning.buildNumber:

versioning:
  buildNumber:
    strategy: max-stores-plus-one
    fallback: pubspec
Field Accepted values Default Description
strategy manual, pubspec, stores, max-stores-plus-one pubspec Selects how the next number is obtained. stores and max-stores-plus-one are aliases.
fallback pubspec or fail fail Store strategies use local pubspecs if set to pubspec; otherwise lookup errors stop the command. Values other than pubspec behave as fail.

Strategy behavior:

  • manual requires --build-number.
  • pubspec reads version: <name>+<number> from every selected app's pubspec.yaml, takes the highest number, and adds one. Missing files or versions do not contribute; if none contain a number, the result is 1.
  • stores and max-stores-plus-one inspect every selected app/platform, calculate one global maximum, and add one. This prevents collisions across a multi-app release.

Google Play lookup checks uploaded bundles and releases in all tracks. App Store Connect lookup finds the app by bundle identifier and checks all paginated build records. The relevant playConsole or testFlight publisher must be configured for every selected platform.

An explicit --build-number always wins and must be a positive integer. Shorebird patch jobs skip build-number resolution.

Publishers #

Artifact publishers are optional and live under publishers. Merely configuring a publisher does not upload anything; publication also requires --publish. Slack and SMTP live separately under the root-level notifiers section and run by default unless --no-notify is supplied.

Every publisher and notifier accepts enabled, a boolean that defaults to true:

publishers:
  playConsole:
    enabled: false
    serviceAccountEnv: LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT
    track: internal

When enabled: false, Flutter Local Build ignores that publisher or notifier completely. It does not create its publisher/notifier, read its credentials, or use it as a store build-number source. This allows configuration to remain in the file while being temporarily inactive. Omitting enabled is equivalent to enabled: true.

Google service-account credentials #

Google Drive and Google Play share the same credential format:

serviceAccountEnv: LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT

The field names an environment variable—it is not the credential itself. The variable may contain the service-account JSON text or a path to its JSON file. Use an absolute file path in CI and when running the command outside the config directory. The default variable name is LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT.

The service account must have access to the target Drive folder or Play Console application as appropriate.

publishers.googleDrive #

Uploads every successful Android and iOS artifact to one Drive folder.

publishers:
  googleDrive:
    enabled: true
    serviceAccountEnv: LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT
    folderId: 1AbCdEfGhIjKlMnOp
Field Required Default Description
enabled No true Set to false to ignore this publisher, including its credentials and uploads.
folderId Yes None Google Drive destination folder ID. The folder must be shared with the service account.
serviceAccountEnv No LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT Name of the environment variable containing Google credentials or a credentials-file path.

The uploaded object keeps the artifact's filename.

publishers.playConsole #

Supports Android artifacts only. It creates a Play edit, uploads the AAB/APK, assigns it to a track as a completed release, and commits the edit.

publishers:
  playConsole:
    enabled: true
    serviceAccountEnv: LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT
    track: internal
Field Required Default Description
enabled No true Set to false to ignore Play uploads and Google Play build-number lookup.
serviceAccountEnv No LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT Name of the environment variable containing Google credentials or a credentials-file path.
track No internal Google Play target track, such as internal, alpha, beta, or production. Custom track names are passed through unchanged.

The Play package name comes from the selected app's effective androidPackage, including any profile override.

The Play release name is <build-name>+<build-number>, such as 1.0.0+128. When --build-name is omitted, the name is read from that app's pubspec.yaml, so each app in a monorepo keeps its own version name. The CLI also adds an en-US release note in the form Local <profile> build <build-name>+<build-number>.

publishers.testFlight #

Supports iOS artifacts only and uploads through xcrun altool. Uploading requires macOS and Xcode.

publishers:
  testFlight:
    enabled: true
    keyIdEnv: APP_STORE_CONNECT_KEY_ID
    issuerIdEnv: APP_STORE_CONNECT_ISSUER_ID
    privateKeyEnv: APP_STORE_CONNECT_PRIVATE_KEY_PATH
Field Required Default environment-variable name Description
enabled No true Set to false to ignore TestFlight uploads and App Store build-number lookup.
keyIdEnv No APP_STORE_CONNECT_KEY_ID Environment variable containing the App Store Connect API key ID.
issuerIdEnv No APP_STORE_CONNECT_ISSUER_ID Environment variable containing the issuer ID.
privateKeyEnv No APP_STORE_CONNECT_PRIVATE_KEY_PATH Environment variable containing the private-key value described below.

For TestFlight publication, privateKeyEnv must resolve to the path of the App Store Connect .p8 file because its parent directory is supplied to xcrun through API_PRIVATE_KEYS_DIR. An absolute path is recommended.

For App Store build-number lookup, the same variable may instead contain either the EC private-key PEM text or a path to the key. If the same configuration is used for both lookup and publication, use a file path so both operations work.

The effective iosBundleId selects the App Store Connect application during build-number lookup. TestFlight upload itself uses the signed IPA metadata.

publishers.firebaseAppDistribution #

Uploads Android AAB/APK and iOS IPA artifacts with the Firebase CLI. Install the Firebase CLI and authenticate it, or provide a service-account credential file. Each app (or its selected profile override) needs the matching firebaseAndroidAppId or firebaseIosAppId.

apps:
  customer:
    path: apps/customer
    androidPackage: com.example.customer
    iosBundleId: com.example.customer
    firebaseAndroidAppId: 1:1234567890:android:abcdef
    firebaseIosAppId: 1:1234567890:ios:abcdef

publishers:
  firebaseAppDistribution:
    enabled: true
    command: [npx, firebase]
    serviceAccountEnv: LOCAL_BUILD_FIREBASE_SERVICE_ACCOUNT
    groupAliases: [internal-testers, qa]
    releaseNotesFile: release-notes/firebase.txt
    testers: [developer@example.com]
    testDevices:
      - model=shiba,version=34,locale=en,orientation=portrait
    testUsername: qa@example.com
    testPasswordEnv: FIREBASE_TEST_PASSWORD
    testUsernameResource: username
    testPasswordResource: password
    testNonBlocking: false
    resultsBucket: gs://my-test-results
    debug: false
Field Type Default Description
enabled Boolean true Set to false to ignore Firebase uploads.
command String or list firebase Firebase CLI executable and optional fixed arguments. For example, [npx, firebase].
serviceAccountEnv String GOOGLE_APPLICATION_CREDENTIALS Name of an environment variable containing the absolute path to a Google credential JSON file. The path is forwarded to the Firebase CLI as GOOGLE_APPLICATION_CREDENTIALS. If the default variable is unset, the Firebase CLI may use its existing login or Application Default Credentials.
groupAliases String or list None Tester group aliases. A string is comma-separated. Overridden by --firebase-group-aliases. Cannot be combined with groupsFile.
groupsFile String None Config-relative file containing comma-separated group aliases. Cannot be combined with groupAliases.
testers String or list None Tester email addresses. A string is comma-separated. Cannot be combined with testersFile.
testersFile String None Config-relative file containing comma-separated tester emails. Cannot be combined with testers.
releaseNotes String None Inline release notes. Cannot be combined with releaseNotesFile.
releaseNotesFile String None Config-relative plain-text release-notes file. Cannot be combined with releaseNotes.
debug Boolean false Enable verbose Firebase CLI output.
testDevices String or list None Automated App Testing device specifications. Separate multiple inline specifications with semicolons or use a YAML list. Cannot be combined with testDevicesFile.
testDevicesFile String None Config-relative file containing automated-test device specifications. Cannot be combined with testDevices.
testUsername String None Username used for automatic login during an automated test.
testPasswordEnv String None Environment-variable name containing the automated-test password. The secret is not read during a dry run. Cannot be combined with testPasswordFile.
testPasswordFile String None Config-relative password file passed to Firebase. Cannot be combined with testPasswordEnv.
testUsernameResource String None Android resource name of the login username field.
testPasswordResource String None Android resource name of the login password field.
testNonBlocking Boolean false Start automated tests without waiting for their results.
resultsBucket String None Google Cloud Storage bucket used for automated-test results.

For a one-off group selection, the CLI value replaces the configured aliases:

local_build build --profile production --app customer --platform android \
  --publish --firebase-group-aliases internal-testers,qa

File paths in this publisher are resolved relative to .local-build.yaml. Direct and file forms of the same setting are mutually exclusive, so choose one of releaseNotes/releaseNotesFile, testers/testersFile, groupAliases/groupsFile, testDevices/testDevicesFile, and testPasswordEnv/testPasswordFile.

Notifiers #

Slack and SMTP configurations live under the root-level notifiers map, not under publishers.

notifiers.slack #

notifiers:
  slack:
    enabled: true
    webhookEnv: LOCAL_BUILD_SLACK_WEBHOOK
Field Required Default Description
enabled No true Set to false to disable Slack without deleting its settings.
webhookEnv No LOCAL_BUILD_SLACK_WEBHOOK Name of the environment variable containing a Slack incoming-webhook URL.

Slack sends one message after each successful job and one message for a failed job. A non-2xx webhook response fails the command.

notifiers.smtp #

notifiers:
  smtp:
    enabled: true
    events: both
    host: smtp.example.com
    port: 587
    security: starttls
    usernameEnv: LOCAL_BUILD_SMTP_USERNAME
    passwordEnv: LOCAL_BUILD_SMTP_PASSWORD
    from: builds@example.com
    fromName: Flutter Local Build
    to:
      - developer@example.com
      - release-team@example.com
    cc: engineering@example.com
    bcc: [audit@example.com]
    subjectPrefix: "[Flutter Local Build]"
Field Type Required Default Description
enabled Boolean No true Set to false to disable SMTP without deleting its settings. No other SMTP fields are required while disabled.
events String No both success, failure, both, or none. neither is accepted as an alias for none.
host String Yes when sending None SMTP server hostname.
port Integer or numeric string No 587 SMTP port from 1 through 65535.
security String No starttls starttls, ssl, or insecure. Use insecure only for a trusted local relay.
usernameEnv String No No authentication Name of the environment variable containing the SMTP username.
passwordEnv String No No authentication Name of the environment variable containing the SMTP password.
from String Yes when sending None Sender email address.
fromName String No None Display name for the sender.
to String or list of strings Yes when sending None At least one recipient. A string may contain comma-separated addresses.
cc String or list of strings No None Carbon-copy recipients. A string may contain comma-separated addresses.
bcc String or list of strings No None Blind-carbon-copy recipients. A string may contain comma-separated addresses.
subjectPrefix String No None Text placed before generated success/failure subjects. Blank text is ignored.

SMTP sends plain-text messages. Success messages contain the app, platform, profile, version values, and artifact path. Failure messages contain the job identity and error. Fields marked "when sending" are not accessed for events disabled by events or during a dry run.

Publishing behavior #

With --publish, every applicable configured publisher runs in this order:

Publisher Android iOS
Google Drive Yes Yes
Google Play Yes No
TestFlight No Yes
Firebase App Distribution Yes Yes

Android format support is:

Publisher AAB APK
Google Drive Yes Yes
Google Play Yes Yes
Firebase App Distribution Yes Yes

--android-artifact selects one Android output for the invocation. The same artifact is sent to all applicable enabled publishers; it does not build a different format per publisher. The default is aab, which preserves prior behavior. TestFlight is unaffected because iOS always produces an IPA.

If multiple publishers support an artifact, all of them run. Jobs are processed sequentially in configuration order, with Android before iOS for each app. A build, upload, or notification error stops the remaining work and returns a non-zero exit code.

Shorebird patches are delivered by Shorebird and cannot be combined with --publish. Shorebird releases may use the configured artifact publishers.

Notifications #

Slack and SMTP are independent. Both run when both are configured. Successful notifications are sent after publishing completes for an artifact. On a job error, failure notifications run immediately and the original error is then reported.

Use --no-notify to disable all notifiers for one invocation. To keep SMTP configured but silence it persistently, use events: none. Slack has no event filter; remove its configuration or use --no-notify when it should not send.

Dry-run behavior #

--dry-run prints generated build commands, prospective uploads, and notification summaries without running Flutter, Shorebird, uploads, Slack, or SMTP. It uses conventional artifact paths in its output.

Build-number resolution happens before execution. Consequently, a dry run with the stores or max-stores-plus-one strategy still calls Google Play and/or App Store Connect. Use --build-number, the pubspec strategy, or a separate dry-run configuration when a completely offline preview is required.

Artifact discovery #

Flutter Android builds use build appbundle for --android-artifact aab and build apk for --android-artifact apk; iOS builds use build ipa. After a real build, Flutter Local Build searches recursively below:

  • build/app/outputs/bundle for the most recently modified .aab; and
  • build/app/outputs/flutter-apk for the most recently modified .apk; and
  • build/ios/ipa for the most recently modified .ipa.

If the directory or matching artifact does not exist, the job fails. Dry runs and patch jobs report the conventional paths build/app/outputs/bundle/release/app-release.aab, build/app/outputs/flutter-apk/app-release.apk, and build/ios/ipa/app.ipa without checking that files exist.

Complete multi-app example #

version: 1
flutterCommand: [fvm, flutter]

apps:
  customer:
    path: apps/customer
    androidPackage: com.example.customer
    iosBundleId: com.example.customer
    profiles:
      development:
        androidPackage: com.example.customer.dev
        iosBundleId: com.example.customer.dev
        dartDefines:
          TENANT: customer
  vendor:
    path: apps/vendor
    androidPackage: com.example.vendor
    iosBundleId: com.example.vendor
    profiles:
      development:
        androidPackage: com.example.vendor.dev
        iosBundleId: com.example.vendor.dev
        dartDefines:
          TENANT: vendor

profiles:
  development:
    flavor: development
    target: lib/main_development.dart
    dartDefines: config/development.json
  production:
    flavor: production
    target: lib/main_production.dart
    dartDefines:
      API_URL: https://api.example.com
    obfuscate: true
    splitDebugInfo: build/symbols

versioning:
  buildNumber:
    strategy: max-stores-plus-one
    fallback: pubspec

publishers:
  googleDrive:
    serviceAccountEnv: LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT
    folderId: your-drive-folder-id
  playConsole:
    serviceAccountEnv: LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT
    track: internal
  testFlight:
    keyIdEnv: APP_STORE_CONNECT_KEY_ID
    issuerIdEnv: APP_STORE_CONNECT_ISSUER_ID
    privateKeyEnv: APP_STORE_CONNECT_PRIVATE_KEY_PATH
notifiers:
  slack:
    webhookEnv: LOCAL_BUILD_SLACK_WEBHOOK
  smtp:
    events: failure
    host: smtp.example.com
    port: 587
    security: starttls
    usernameEnv: LOCAL_BUILD_SMTP_USERNAME
    passwordEnv: LOCAL_BUILD_SMTP_PASSWORD
    from: builds@example.com
    to: [release-team@example.com]

Build both apps for Android while overriding the automatically resolved number:

local_build build \
  --profile production \
  --app customer \
  --app vendor \
  --platform android \
  --build-name 1.4.0 \
  --build-number 104 \
  --publish

Credentials and security #

Store credentials in environment variables or a secret manager. Never commit service-account JSON, App Store private keys, SMTP passwords, tokens, or Slack webhooks. Prefer absolute paths for credential files because those paths are read by the CLI process, not resolved relative to each app.

Use narrowly scoped provider accounts, grant Drive access only to the target folder, and limit Play/App Store permissions to the applications and actions required by the build workflow.

To report a vulnerability, use the repository's private security advisory form.

Extending Flutter Local Build #

The Dart package exports the build models and provider interfaces. A fork can add a Publisher, Notifier, or BuildNumberSource implementation and register it in the provider registry. Keep provider-specific credentials and API behavior outside the planner and executor so the CLI remains portable.

See the architecture guide and contribution guide.

Troubleshooting #

  • Unknown profile or app: verify names exactly match the YAML keys.
  • No build jobs: the selected apps lack androidPackage/iosBundleId for the selected platform, or the selection is empty.
  • Missing environment variable: configuration fields such as serviceAccountEnv contain variable names, not secret values.
  • Artifact not found: confirm the Flutter flavor writes an .aab or .ipa below the documented output directories.
  • Store lookup fails in a preview: store-based build-number resolution also runs during --dry-run; use fallback: pubspec or an explicit number.
  • TestFlight key cannot be found: use an absolute .p8 path for publication and ensure Xcode's command-line tools are installed.

Argument and configuration errors return exit code 64. Failed builds, publishing operations, notifications, and unexpected errors return a non-zero exit code and print details to standard error.

Project #

Flutter Local Build is maintained by Pycify. Contributor: GabbyGreat.