local_build 0.1.0
local_build: ^0.1.0 copied to clipboard
Extensible local Flutter build and release orchestration.
Local Build #
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, and TestFlight; 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
PATHwhen using Shorebird.
Provider accounts and credentials are only required for integrations 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
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. |
--build-name <version> |
No | Flutter project value | Pass a semantic version such as 1.4.0 to Flutter or Shorebird as --build-name. 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. |
--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 one Android app.
local_build build --profile production --app customer --platform android
# 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. |
integrations |
Map | No | {} |
Publisher and notifier settings. Each configured integration 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
| 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. |
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. |
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. |
Paths passed as profile arguments are interpreted by Flutter or Shorebird from the app's working directory.
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.
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:
manualrequires--build-number.pubspecreadsversion: <name>+<number>from every selected app'spubspec.yaml, takes the highest number, and adds one. Missing files or versions do not contribute; if none contain a number, the result is1.storesandmax-stores-plus-oneinspect 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 integration 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.
Integrations #
All integrations are optional and live under integrations. Merely configuring
a publisher does not upload anything; publication also requires --publish.
Configured notifiers run by default unless --no-notify is supplied.
Every integration accepts enabled, a boolean that defaults to true:
integrations:
playConsole:
enabled: false
serviceAccountEnv: LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT
track: internal
When enabled: false, Local Build ignores that integration completely. It does
not create its publisher or 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.
integrations.googleDrive #
Uploads every successful Android and iOS artifact to one Drive folder.
integrations:
googleDrive:
enabled: true
serviceAccountEnv: LOCAL_BUILD_GOOGLE_SERVICE_ACCOUNT
folderId: 1AbCdEfGhIjKlMnOp
| Field | Required | Default | Description |
|---|---|---|---|
enabled |
No | true |
Set to false to ignore this integration, 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.
integrations.playConsole #
Supports Android artifacts only. It creates a Play edit, uploads the AAB, assigns it to a track as a completed release, and commits the edit.
integrations:
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.
integrations.testFlight #
Supports iOS artifacts only and uploads through xcrun altool. Uploading
requires macOS and Xcode.
integrations:
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.
integrations.slack #
integrations:
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.
integrations.smtp #
integrations:
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: Local Build
to:
- developer@example.com
- release-team@example.com
cc: engineering@example.com
bcc: [audit@example.com]
subjectPrefix: "[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 |
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; iOS builds use build ipa.
After a real build, Local Build searches recursively below:
build/app/outputs/bundlefor the most recently modified.aab; andbuild/ios/ipafor 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 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
integrations:
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
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 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/iosBundleIdfor the selected platform, or the selection is empty. - Missing environment variable: configuration fields such as
serviceAccountEnvcontain variable names, not secret values. - Artifact not found: confirm the Flutter flavor writes an
.aabor.ipabelow the documented output directories. - Store lookup fails in a preview: store-based build-number resolution also
runs during
--dry-run; usefallback: pubspecor an explicit number. - TestFlight key cannot be found: use an absolute
.p8path 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 #
Local Build is maintained by Pycify. Contributor: GabbyGreat.