Configuration Reference
Every key in Shipfile.yml, with defaults and examples.
On this page
Shipfile.yml is only the default config filename. ShipIt reads ./Shipfile.yml when --shipfile is omitted, but any YAML file path works with --shipfile <path>.
For config-backed CLI commands, the selected file must exist. A missing ./Shipfile.yml or missing --shipfile <path> target causes the command to exit with code 2.
Priority Order
CLI flags > SHIPIT_* env vars > .env file > config file > Built-in defaults
.env file auto-loading
ShipIt automatically loads a .env file from the same directory as Shipfile.yml before resolving configuration. Variables already in the process environment take precedence and are never overwritten.
Supported formats: KEY=VALUE, export KEY=VALUE, quoted values, and # comments. Use shipit generate to create a pre-populated .env file; it is automatically added to .gitignore.
Environment variable naming convention
Shipfile YAML keys map to env vars by upper-casing and joining path segments with _, prefixed with SHIPIT_. Nested keys use __ (double-underscore) as the section separator.
| Shipfile key | Environment variable |
|---|---|
app.scheme | SHIPIT_APP__SCHEME |
app.bundle_id | SHIPIT_APP__BUNDLE_ID |
app.team_id | SHIPIT_APP__TEAM_ID |
app_store_connect.key_id | SHIPIT_APP_STORE_CONNECT__KEY_ID |
app_store_connect.issuer_id | SHIPIT_APP_STORE_CONNECT__ISSUER_ID |
build.configuration | SHIPIT_BUILD__CONFIGURATION |
archive.export_method | SHIPIT_ARCHIVE__EXPORT_METHOD |
code_signing.git_url | SHIPIT_CODE_SIGNING__GIT_URL |
Secrets bypass: ASC_KEY_ID, ASC_ISSUER_ID, ASC_PRIVATE_KEY, ASC_PRIVATE_KEY_PATH, and VAULT_PASSWORD are also read directly (without the SHIPIT_ prefix) for compatibility with standard CI secret naming.
Top-Level Structure
platform:
app:
app_store_connect:
code_signing:
build:
archive:
export:
testflight:
screenshots:
metadata:
versioning:
project_generation:
notifications:
test_workflow:
workflows:
custom_actions:
# Platform-specific overrides (merged on top of shared config)
ios:
android:app
| Key | Type | Description |
|---|---|---|
workspace | string | Path to .xcworkspace |
project | string | Path to .xcodeproj (used when workspace is nil) |
scheme | string | Xcode scheme for build and test |
bundle_id | string | App bundle identifier. Optional if it can be inferred from Xcode build settings |
team_id | string | Apple Developer Team ID. Optional if it can be inferred from Xcode build settings |
Where to find these values manually:
bundle_id: your target's bundle identifier / application ID in Xcode Signing settings orPRODUCT_BUNDLE_IDENTIFIERin build settingsteam_id: the 10-character Apple Developer Team ID for the team selected in Xcode Signing, also shown in the Apple Developer account / Certificates, IDs & Profiles
app_store_connect
For App Store Connect API actions, provide key_id, issuer_id, and exactly one private key source: private_key or key_path.
issuer_id is not inside the .p8 file. Copy it from the App Store Connect API Keys page.
You can omit this whole section if you only use local Xcode actions such as build, test, archive, or export.
Find these values in App Store Connect:
- Open
Users and Access. - Open
Integrations. - Open
App Store Connect API. - Create or select an API key.
- Copy
Key IDandIssuer ID. - Download the
.p8file and use it viaprivate_keyorkey_path.
If ShipIt cannot infer app.team_id, use the team selected in Xcode for the app target. The value you need is the Apple Developer Team ID, not the App Store Connect issuer ID.
| Key | Type | Env Var | Description |
|---|---|---|---|
key_id | string | ASC_KEY_ID | API key ID |
issuer_id | string | ASC_ISSUER_ID | Issuer ID |
key_path | string | — | Path to .p8 key file |
private_key | string | ASC_PRIVATE_KEY | Raw .p8 contents |
code_signing
| Key | Type | Default | Description |
|---|---|---|---|
type | string | vault | automatic, vault, or manual |
storage | string | git | git, s3, or gcs |
git_url | string | — | URL of the certificate repo |
app_identifier | string | — | Bundle ID for matching |
profile_type | string | — | appstore, adhoc, development |
p12_path | string | — | Local .p12 certificate path (manual signing) |
p12_base64 | string | — | Base64-encoded .p12 for CI / manual signing |
p12_password | string | — | .p12 certificate password |
provisioning_profile_path | string | — | Local .mobileprovision path (manual signing) |
provisioning_profile_base64 | string | — | Base64-encoded .mobileprovision for CI / manual signing |
Set VAULT_PASSWORD env var for the encrypted repo passphrase.
With type: automatic, ShipIt passes -allowProvisioningUpdates to Xcode build/export operations and writes signingStyle=automatic into export options.
With type: manual, shipit sign sync installs the configured .p12 certificate and provisioning profile. Local paths are preferred when they exist; base64 values are used as a CI fallback.
build
| Key | Type | Default | Description |
|---|---|---|---|
configuration | string | Release | Build configuration |
derived_data_path | string | — | Custom derived data path |
xcargs | map | {} | Extra flags for xcodebuild |
archive
Top-level archive keys apply to iOS archives. Android archive options are set per workflow step (see archive action options).
| Key | Type | Default | Description |
|---|---|---|---|
export_method | string | app-store | app-store, ad-hoc, enterprise, development |
include_symbols | bool | true | Include dSYMs |
include_bitcode | bool | — | Include bitcode (deprecated Xcode 14+) |
output_path | string | — | Output path for .xcarchive |
export
| Key | Type | Description |
|---|---|---|
archive_path | string | Path to .xcarchive |
output_directory | string | Output directory for IPA |
testflight
| Key | Type | Default | Description |
|---|---|---|---|
skip_waiting_for_build_processing | bool | false | Skip Apple's processing wait |
distribute_external | bool | false | Distribute to external testers |
groups | list | [] | Beta group names |
changelog | string | — | Release notes |
screenshots
| Key | Type | Default | Description |
|---|---|---|---|
devices | list | [] | Simulator device names |
locales | list | [en-US] | Locale identifiers |
scheme | string | — | Screenshot UI test scheme |
output_directory | string | ./screenshots | Output directory |
metadata
| Key | Type | Default | Description |
|---|---|---|---|
directory | string | ./metadata | Metadata directory |
submit_for_review | bool | false | Submit for review |
automatic_release | bool | false | Auto-release after approval |
phased_release | bool | false | Phased release |
versioning
ShipItSwifty follows Apple's two-version model:
| Plist key | Format | Meaning |
|---|---|---|
CFBundleShortVersionString | MAJOR.MINOR.PATCH | User-facing marketing version (e.g. 2.4.1). Bumped manually for releases. |
CFBundleVersion | plain integer | Internal build number (e.g. 317). Auto-incremented on every beta run. |
The versioning section controls how CFBundleVersion is incremented. CFBundleShortVersionString is only changed when the version action is called with bump: major, bump: minor, or bump: patch.
| Key | Type | Default | Description |
|---|---|---|---|
strategy | string | sequential | sequential — adds 1 each run (guarantees a plain integer). timestamp — uses YYYYMMDDHHmm format. |
source | string | xcodeproj | xcodeproj — reads MARKETING_VERSION/CURRENT_PROJECT_VERSION from xcodebuild -showBuildSettings; falls back to agvtool, then plutil on Info.plist. asc — reads current build number from App Store Connect. project_spec — reads/writes version values in a YAML spec file (XcodeGen/Tuist). xcconfig — reads/writes named keys in an .xcconfig file (set spec_path to the file and marketing_key/build_key to the variable names), so a $(VAR) indirection from the project survives. kmp — reads/writes versionName/versionCode in gradle.properties. gradle — reads/writes directly in build.gradle.kts or build.gradle; default for Android projects. pubspec — reads/writes the version: X.Y.Z+B key in pubspec.yaml; default for Flutter projects. |
spec_path | string | — | Path to the version source file. Required for source: project_spec (e.g. project.yml) and source: xcconfig (e.g. Config/Version.xcconfig). For source: pubspec it defaults to pubspec.yaml at the project root. |
build_key | string | — | Key for the build number in the spec file (YAML, e.g. settings.CURRENT_PROJECT_VERSION) or .xcconfig (e.g. APP_BUILD_NUMBER). |
marketing_key | string | — | Key for the marketing version in the spec file (YAML, e.g. settings.MARKETING_VERSION) or .xcconfig (e.g. APP_MARKETING_VERSION). |
source: pubspec (Flutter)
Flutter treats the pubspec version: key as the single source of truth: flutter build stamps CFBundleShortVersionString, CFBundleVersion, versionName, and versionCode from it on every build, so bumping the native project files directly wouldn't stick. pubspec is already the default source when the resolved build system is Flutter. Set it only to override the file location:
versioning:
source: pubspec # default for build_system: flutter
spec_path: pubspec.yaml # optional; defaults to pubspec.yaml at the project rootThe part before + is the marketing version and the part after is the build number, so version: 1.2.3+45 reads as marketing 1.2.3, build 45. With no +build suffix the build number reads as 0, and the first bump: build writes +1. Only the top-level version: key is rewritten; comments and the rest of the file are preserved.
version action options
The version action accepts a bump option that selects which counter to change:
bump value | Changes | Leaves untouched |
|---|---|---|
build | CFBundleVersion (integer) | CFBundleShortVersionString |
patch | Patch segment of CFBundleShortVersionString; resets CFBundleVersion to 1 | Major and minor segments |
minor | Minor segment; resets patch and CFBundleVersion to 1 | Major segment |
major | Major segment; resets minor, patch, and CFBundleVersion to 1 | — |
Version dry-run preview
Use --dry-run to see the computed before/after version values without writing anything:
shipit version --bump build --dry-run
shipit version --bump patch --dry-run --output jsonSafe bump ordering
When a workflow bumps the marketing version (patch/minor/major) and the build number in the same run, place the marketing version bump first. A marketing version bump resets the build number to 1, so running it after a build bump would discard the incremented value. Pair each version step with an immediate git commit so a late-stage failure leaves a clean rollback target.
workflows:
beta:
- action: test
- action: archive
- action: version # bump only after build succeeds
options: { bump: build }
- action: git # commit immediately so git is the rollback mechanism
options:
operation: commit # stages all changes (git add -A) then commits
commit_message: "chore: bump build number [skip ci]"
- action: export
- action: testflightThe
gitaction'soperation: commitstages every change in the working tree (git add -A) before committing — there is no per-file option. In CI this is usually what you want, since the runner starts from a clean checkout and the only modified files are the version bump. If you have unrelated local changes, commit or stash them first.
Beta workflow pattern (local-only, Apple semantics)
For a local beta that increments the build number, runs tests, then archives and exports:
versioning:
strategy: sequential # CFBundleVersion stays a plain integer
source: xcodeproj # reads/writes MARKETING_VERSION + CURRENT_PROJECT_VERSION in .xcodeproj
workflows:
beta:
- action: version
options:
bump: build # increments CFBundleVersion only; marketing version unchanged
- action: test
options:
destinations:
- "platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2"
retry_on_failure: true # retry each failing test once before surfacing a failure
# only_testing: # narrow to specific targets, e.g. [NovalingoTests/CoreTests]
# skip_testing: # skip slow targets, e.g. [NovalingoUITests]
- action: archive
- action: export # exports IPA locally; no upload to App Store ConnectUse xcodebuild -showdestinations -scheme <Scheme> (or shipit ai session --goal local) to
discover destination strings that are valid on your machine before filling in the destinations array.
You can list multiple destinations to run tests on several simulators or devices in one step.
How source: xcodeproj is resolved:
xcodebuild -showBuildSettings— readsMARKETING_VERSIONandCURRENT_PROJECT_VERSIONdirectly from Xcode build settings. This is the primary path for modern Xcode projects.xcrun agvtool— fallback for projects that haveVERSIONING_SYSTEM = apple-genericbut not the newerMARKETING_VERSIONkey.plutilonInfo.plist— last resort for legacy projects.
To add a TestFlight upload later, append a testflight step and uncomment the app_store_connect section.
test action options
The test action is configured inline in a workflow step. It does not have a top-level Shipfile section.
Three options retry tests, and they solve different problems:
retry_on_failurepasses xcodebuild's built-in-retry-tests-on-failurefor a one-pass iOS retry.rerun_failed_testsis ShipIt's own selective rerun. It collects the failures, reruns only those tests, and reports flaky versus persistent failures in theTestRunReport.infrastructure_retryreruns the whole invocation after a transient tool, simulator, or emulator failure — a simulator launch crash, an Android emulator disconnect, a Flutter tool crash, or a JS worker failure.
The last two have separate budgets. An infrastructure retry never spends a rerun attempt, and a selective rerun never spends an infrastructure attempt. Each execution is recorded as its own attempt in the report.
shipit generate asks whether generated test steps should enable infrastructure_retry. shipit ai session includes the same guidance in the agent prompt so AI-assisted workflow creation asks the same question.
Every test run writes a TestRunReport (schema version 2). See Test Results & Workflows for what it contains.
| Option | Type | Default | Description |
|---|---|---|---|
destinations | list | auto-discovered | One or more xcodebuild destination strings. Each entry gets its own test-without-building pass, and pass/fail/skip counts are aggregated. When omitted, ShipIt discovers an iPhone simulator, or fails with the list of what it found. Discover valid values with xcodebuild -showdestinations -scheme <Scheme>. |
destination | string | — | Legacy single-destination string, promoted to a one-element destinations list. Prefer destinations. |
scheme | string | app.scheme | Xcode scheme containing the test targets. |
configuration | string | Debug | Build configuration used for test compilation. |
enable_code_coverage | bool | — | Collect coverage in each native result bundle. ShipIt saves a unique bundle per plan, destination, and attempt under build/test-runs (or the workflow evidence root), with coverage retained inside it. |
result_bundle_path | string | — | Explicit initial bundle path for a single-plan, single-destination run. An existing bundle at the path is removed first. |
test_plan | string | — | Named .xctestplan to run. |
test_plans | list | — | Ordered plans over the same test targets that share one build. Exclusive with test_plan. |
test_products_path | string | unique run directory | New .xctestproducts path for the shared build. |
legacy_combined_test | bool | false | Use the previous single xcodebuild test invocation instead of build-once-test-many. |
serial | bool | false | Force -parallel-testing-enabled NO. |
skip_macro_validation | bool | false | Pass -skipMacroValidation while building the tests. |
erase_simulator | bool | false | Erase the resolved simulator before running. Refuses a device that is already booted. |
only_testing | list | — | Restrict to specific targets or test cases. Each entry maps to -only-testing. |
skip_testing | list | — | Skip specific targets or test cases. Each entry maps to -skip-testing. |
retry_on_failure | bool | false | Pass -retry-tests-on-failure to retry each failing test once. |
rerun_failed_tests | object | — | Selectively rerun only the failed tests. Fields: enabled and max_attempts (default 2, counting the initial run; 1 disables reruns, and a recovered run stops early). Supported for native iOS (per plan, destination, and configuration), Android JVM unit tests (kind: unit), Flutter, React Native (Jest), and SwiftPM. A test that fails again fails the step. A test that recovers passes and is reported as flaky. |
infrastructure_retry | object | — | Retry the whole test invocation after a transient infrastructure failure. The block's presence enables it. Fields: max_attempts (default 3, including the first), initial_delay_seconds (default 2), and max_delay_seconds (default 60). Recommended: { max_attempts: 3, initial_delay_seconds: 2, max_delay_seconds: 30 }. |
report_path | string | — | Path to write the structured JSON TestRunReport. |
module | string | android.module | Android/KMP. Gradle module whose tests should run. |
kind | string | unit | Android only. unit for JVM tests (no device), instrumented for on-device tests, e2e for end-to-end. |
scope | string | auto | Android only. Gradle task scope: module qualifies the task with the module path; root runs the root-level task (cascades across all modules). When kind: instrumented and scope is unset, defaults to root. Set scope: module explicitly for single-module behaviour. |
build_variant | string | debug | Android only. Overrides android.build_variant for this test step. |
task | string | — | Android only. Explicit Gradle task name (e.g. testDebugUnitTest). Overrides variant-based task selection. |
devices | object | — | Android only. Device config for instrumented runs. Fields: strategy (none/connected/named_emulators/managed), emulators (AVD names), group (managed device group), prompt_locally (interactive selection outside CI). |
Native iOS: build once, test many
By default ShipIt builds the tests once into an .xctestproducts bundle, then runs every plan and destination with test-without-building, writing a separate result bundle for each. Retries reuse the same products.
workflows:
tests:
- action: test
options:
test_plans: [Unit, UI]
destinations:
- "platform=iOS Simulator,name=iPhone 17"
rerun_failed_tests: { enabled: true, max_attempts: 2 }
infrastructure_retry: { max_attempts: 3, initial_delay_seconds: 2, max_delay_seconds: 30 }- Plans run independently. A failing plan doesn't stop the others, but a failed shared build blocks all of them.
- Each plan and destination is its own entry in the report's
destinations, carrying that configuration's language and region. - If xcodebuild reports a simulator clone failure, ShipIt falls back to serial execution once per destination without rebuilding. Ordinary assertion failures never trigger the fallback.
- Set
legacy_combined_test: trueto keep the older singlexcodebuild testinvocation. - Select a test workflow by name with the top-level
test_workflowkey. See Test workflows.
Destination string format
platform=iOS Simulator,name=<SimulatorName>,OS=<Version>
platform=iOS,name=<DeviceName>
platform=macOS
Run xcodebuild -showdestinations -scheme <Scheme> (with -workspace or -project as appropriate)
to list all valid destination strings for your scheme on the current machine. Alternatively, run
shipit ai session --goal local which calls destination discovery automatically and includes the
results in nextQuestion so an AI agent can ask which destinations to use.
Android examples:
workflows:
beta:
steps:
# JVM unit tests for a single module
- action: test
options:
kind: unit
build_variant: debug
# Instrumented tests — defaults to scope:root for multi-module projects
- action: test
options:
kind: instrumented
devices:
strategy: named_emulators
emulators: [Pixel_9_API_35]
# Explicit root-level aggregate task
- action: test
options:
task: testDebugUnitTest
scope: rootFor Android aggregate tasks, ShipItSwifty falls back to JUnit XML reports under build/test-results/<taskName>/ when Gradle stdout does not include a test summary.
swift-test and swift-format actions
SwiftPM packages use two dedicated actions, which are ordinary workflow steps. Select a test workflow with the top-level test_workflow key (or SHIPIT_TEST_WORKFLOW); shipit test --workflow <name> overrides it.
test_workflow: tests
workflows:
format:
- action: swift-format
options: { paths: [Sources, Tests] }
tests:
- action: swift-test
options:
enable_code_coverage: true
rerun_failed_tests: { enabled: true, max_attempts: 2 }
infrastructure_retry: { max_attempts: 3, initial_delay_seconds: 2, max_delay_seconds: 30 }swift-test option | Type | Default | Description |
|---|---|---|---|
package_path | string | . | Swift package directory. |
scratch_path | string | — | Isolated SwiftPM build storage. |
filter | string | — | Test selector regex. |
skip | string | — | Excluded test selector regex. |
environment | map | — | Extra environment for the test run, including snapshot-testing controls. |
enable_code_coverage | bool | — | Collect full-suite coverage. SwiftPM writes no coverage file when a test fails, so ShipIt recomputes it from the raw profiles right after the first attempt, before a rerun adds its own. |
parallel_workers | integer | — | Maximum XCTest worker processes (swift test --num-workers). 1 runs XCTest cases one at a time, for suites that share state. Swift Testing parallelism is set in code with .serialized. |
rerun_failed_tests | object | — | enabled and max_attempts (default 2, counting the initial run). Reruns reuse the built products (--skip-build). A recovered test passes and stays marked flaky. |
infrastructure_retry | object | — | Retry transient SwiftPM execution failures. Same fields as the test action. |
output_directory | string | — | New directory for per-attempt results, logs, and attachments. |
swift-test saves Swift Testing events, attachments, stdout/stderr, and a final report.json.
swift-format option | Type | Default | Description |
|---|---|---|---|
paths | list | — | Paths to lint recursively. |
configuration | string | — | Path to a swift-format configuration file. |
report_path | string | — | Save the lint stdout/stderr. |
swift-format is strict and non-mutating: it lints and never rewrites files.
Packages that import UIKit
A package whose sources import a framework the host lacks, such as UIKit, without a canImport or os(...) guard can't run under swift test. shipit inspect project --output json reports this under swiftPackage, and shipit generate --goal local (or shipit ai session --goal local) suggests a test step instead of swift-test. That step uses the scheme Xcode generates for the package directory — the package name when it has at most one product, <name>-Package when it has several — and runs xcodebuild on a simulator without app.project or app.workspace.
test-results action options
Use test-results to read test-result artifacts without running tests: xcresult bundles, JUnit XML (Gradle, KMP, and swift test), Swift Testing events, Flutter machine events, Jest JSON, and ShipIt reports or portable exports. Explicit inputs work without a Shipfile. The options below follow the schema printed by shipit schema --output json.
| Option | Type | Default | Description |
|---|---|---|---|
inputs | list | — | Result artifacts to read. The CLI's --input is repeatable. |
input_format | string | detected | xcresult, junit, swift, flutter, jest, shipit, or manifest. Ambiguous input needs an override. |
runner | string | from the artifact | The tool that produced the results: xcodebuild, gradle, swift-test, flutter-test, or jest. Needed for JUnit XML, which Gradle and swift test both write. |
build_system | string | from the Shipfile | native, kmp, flutter, or react_native, for results read outside a Shipfile. A framework is a build system, never a platform. |
coverage_inputs | list | — | Coverage artifacts to keep alongside the results. |
coverage_format | string | — | swift, lcov, jacoco, or kover. |
evidence_paths | list | — | Extra screenshots, videos, logs, and native artifacts to copy into an export. |
export_directory | string | — | New directory for a portable export with originals, extracted evidence, a manifest, and an index with relative links. |
format | string | text | Output format hint: text, json, or markdown. |
platform | string | resolved platform | ios or android override when reading outside a Shipfile-backed context. |
xcresult_path | string | auto-discover | Explicit .xcresult bundle. Falls back to ./build/<scheme>-tests.xcresult. |
report_path | string | auto-discover | Explicit JUnit XML report directory. Falls back to common Gradle build/test-results/... locations. |
failed_only | bool | false | Only include failed and errored tests in the output. |
include_passed | bool | true | Include passed tests in the output. |
report_output_path | string | — | Write the structured JSON report to disk. |
The CLI flags don't map one to one onto these keys: on the command line,
--reportis the JUnit directory (the action'sreport_path), and--report-pathis where the JSON report is written (the action'sreport_output_path).
Archive action options
The archive action accepts inline step options for both iOS and Android:
| Option | Type | Default | Description |
|---|---|---|---|
scheme | string | app.scheme | Xcode scheme to archive (iOS). |
configuration | string | Release | Build configuration. |
export_method | string | app-store | Distribution method (iOS only): app-store, ad-hoc, development, enterprise. |
output_path | string | — | Output .xcarchive path (iOS) or .aab path (Android). |
include_symbols | bool | true | Include dSYMs (iOS only). |
module | string | android.module | Gradle module to bundle (Android). |
build_variant | string | release | Gradle bundle variant (Android). |
flavor | string | — | Product flavor name. Combined with build_variant for Gradle task names. Also used by Flutter and React Native archives. |
scope | string | module | Android Gradle task scope: module or root. |
gradle_properties | object | {} | Additional -P key=value properties passed to the Gradle bundle task (Android). |
infrastructure_retry | object | — | Retry the iOS archive for transient Apple provisioning-server failures that occur when -allowProvisioningUpdates contacts Apple's servers. Presence enables retries; omit to disable. Permanent failures (missing certificate, invalid team ID) are never retried. Recommended: { max_attempts: 3, initial_delay_seconds: 5, max_delay_seconds: 30 }. |
project_generation
Configuration for automatic project file generation (XcodeGen, Tuist) before Xcode-dependent steps.
| Key | Type | Default | Description |
|---|---|---|---|
tool | string | — | Generation tool: xcodegen or tuist |
command | string | — | Custom shell command (overrides tool) |
spec_path | string | — | Path to the project spec file (e.g. project.yml for XcodeGen) |
output_project | string | — | Expected output .xcodeproj path (used to verify generation succeeded) |
auto_generate | bool | true | Automatically run generation before Xcode-dependent actions. Set to false to skip. |
When project_generation.tool is configured, Workflow.run() automatically generates the Xcode project before the first Xcode-dependent step (build, test, archive, etc.), including steps reached through nested custom_actions. Generation is skipped when the output project already exists.
notifications.slack
| Key | Type | Description |
|---|---|---|
webhook_url | string | Incoming webhook URL (use ${SLACK_WEBHOOK_URL}) |
channel | string | Default Slack channel |
on_success | bool | Notify on success |
on_failure | bool | Notify on failure |
workflows
Workflows can be defined as a plain array of steps (legacy) or as an object with optional workflow-level overrides:
# Legacy format: plain array of steps
workflows:
beta:
- action: archive
- action: testflight
# New format: object with workflow-level overrides
workflows:
release:
build_variant: prodRelease
steps:
- action: archive
- action: play-store
options:
track: productionWorkflow-level overrides
| Key | Type | Description |
|---|---|---|
build_variant | string | Overrides android.build_variant for all steps in this workflow. Used by actions that auto-discover paths. |
flavor | string | Overrides android.gradle_properties.flavor for all steps in this workflow. |
steps | array | Required when using the object format. The ordered sequence of action steps. |
continue_on_failure | bool | Optional, default false. Opt-in for workflows made of independent checks: a failing step doesn't stop the remaining steps, but the workflow still finishes as failed. Set it next to steps (object format), not on a step. See Test workflows and artifacts. |
Each step:
| Key | Description |
|---|---|
action | Registered action name |
options | Optional key-value map passed to the action |
when | Optional condition. Reserved tokens are substituted, then the result must be truthy (true/1/yes) for the step to run; otherwise the step is skipped (status skipped) and the workflow continues. |
artifacts | Optional list of files to publish from the step, each with name, paths, and retention_days. Works on built-in and custom-action steps. See Test workflows and artifacts. |
Use these AI-oriented commands to inspect the supported workflow surface:
swift run shipit ai session --goal beta
swift run shipit schema --output json
swift run shipit inspect project --output json
swift run shipit generate --goal beta
swift run shipit validate yml --shipfile ./Shipfile.yml --output jsonai session always emits JSON and is intended for AI/tooling integrations rather than direct human consumption.
Workflow notes:
exportcan write an IPA intoexport.output_directory.testflightanduploadcan reuse that exported IPA automatically when they run afterexportin the same workflow and no explicitipa/ipa_pathis set.validatereports an error whentestflightoruploadhas no explicit IPA and no prior export context.
Test workflows and artifacts
A test workflow is an ordinary workflow made of test steps. The top-level test_workflow key (or SHIPIT_TEST_WORKFLOW) names the one shipit test runs by default, and shipit test --workflow <name> picks one explicitly. Workflow selection is exclusive with direct test options.
test_workflow: tests
workflows:
tests:
continue_on_failure: true # workflow-level: run the tests even if formatting fails
steps:
- action: swift-format
- action: swift-test
options:
output_directory: "build/results/{{run_id}}"
artifacts:
- name: results
paths: ["build/results/{{run_id}}"]
retention_days: 14artifacts declares files a step produces that should be published with the run.
nameuses letters, numbers, underscores, and hyphens.pathstakes files, directories, or globs (*,?, and**), andretention_daysis 1–90.- Test runs already keep their evidence under
build/workflow-artifacts/<run_id>/, which is staged and uploaded as a whole. Declare artifacts only for files a step writes elsewhere. - Report and result locations a step publishes (
report_path,output_directory,result_bundle_path) are collected automatically. Build products such as IPAs, AABs, APKs, and archives are never collected unless you list them. - Evidence is gathered after a failed step too. A missing optional match becomes a diagnostic, not a failure.
continue_on_failure: true is a workflow-level key, set next to steps in the object format. It is opt-in, for independent checks that should all run. The default stays fail-fast, and any failed step still makes the whole workflow fail. Only artifacts (and when) go on an individual step.
{{run_id}} resolves to the current workflow run's ID in a step's options and artifact paths, keeping the output of separate runs apart. It is available to top-level steps only. See Workflow tokens & step conditions.
Turn a test workflow into a CI job with shipit ci export.
firebase-app-distribution action options
Use the firebase-app-distribution workflow action to upload a signed IPA, APK, or AAB to Firebase App Distribution. It accepts credentials only from an environment variable or a local service-account file; never put service-account JSON in Shipfile.yml.
| Option | Type | Default | Description |
|---|---|---|---|
app_id | string | — | Required. Firebase app ID, for example 1:1234567890:ios:abc123. |
artifact_path | string | — | Required. Path to the signed .ipa, .apk, or .aab artifact. |
groups | list | [] | Firebase tester group aliases. At least one group or tester is required. |
testers | list | [] | Individual tester email addresses. |
release_notes | string | — | Plain-text notes shown to testers. |
service_account_path | string | — | Local service-account JSON file. Overrides environment credentials. |
dry_run | bool | false | Validate the planned distribution without uploading. |
timeout_seconds | int | 300 | Maximum time to wait for Firebase to process the upload. |
The action resolves credentials in this order: service_account_path, FIREBASE_SERVICE_ACCOUNT_JSON (raw JSON), then GOOGLE_APPLICATION_CREDENTIALS (path to a service-account JSON file). The Firebase app ID platform is checked against the artifact before upload, so an iOS app ID requires an IPA and an Android app ID requires an APK or AAB.
workflows:
beta:
- action: archive
- action: export
options:
output_directory: ./build/firebase
- action: firebase-app-distribution
options:
app_id: ${FIREBASE_IOS_QA_APP_ID}
artifact_path: ./build/firebase/MyApp.ipa
groups: [qa-testers]
release_notes: "Build {{build_number}}"See Firebase App Distribution for setup and Android examples.
- Human output from
shipit run <workflow>now prints per-step summaries when available, including test pass/fail/skip counts, version strings, and archive artifact filenames.
custom_actions
User-defined composite actions — reusable, parameterized sequences of built-in actions (or other custom actions). Use these to avoid duplicating step blocks across workflows (for example sharing test → archive → export between beta and adhoc).
custom_actions:
<composite-name>:
description: "Human-readable summary (optional)."
parameters:
<param-name>:
type: string # string | bool | int | number | array | object | any
required: true # defaults to true when no default is set
default: <value> # optional fallback when call site omits the value
description: "..."
steps:
- action: <registered-action-or-other-composite>
options:
<option>: "{{param.<param-name>}}"Invocation is identical to a built-in action — a workflow step uses action: <composite-name> and forwards call-site values via its options: block:
custom_actions:
build_and_sign:
description: "Test, archive, and export the app with a chosen export method."
parameters:
method:
type: string
required: true
steps:
- action: test
- action: archive
- action: export
options:
method: "{{param.method}}"
workflows:
beta:
- action: build_and_sign
options:
method: app-store
- action: testflight
adhoc:
- action: build_and_sign
options:
method: ad-hoc
- action: notifyParameter reference syntax: {{param.NAME}}
Inside a step's options: block, string values may reference declared parameters using {{param.NAME}}. This delimiter is chosen to stay out of the way of every common template system that surrounds a Shipfile:
| System | Template syntax | Safe alongside {{param.X}}? |
|---|---|---|
Shipfile ${ENV_VAR} expansion | ${FOO} | ✅ Different delimiter. Env expansion runs before composite substitution, so you can freely mix: key: "{{param.track}}-${DEPLOY_ENV}". |
| GitHub Actions | ${{ env.X }}, ${{ secrets.X }} | ✅ Different delimiter (leading $ + double braces). |
| CircleCI parameters | << parameters.X >> | ✅ Different delimiter. |
| Xcode Cloud / shell | $VAR, ${VAR} | ✅ Different delimiter. |
Rules:
- Only string leaves in a step's
options:are scanned. Keys and non-string scalars are untouched. - When a string is exactly
{{param.NAME}}, the underlying typed JSON value is substituted (bool,int,array,object). When it appears inline with other text, the value is stringified. - Unknown parameter references cause
shipit validate ymlto report an error rather than silently expanding to an empty string.
Validation rules
shipit validate yml enforces:
- Custom action names must not collide with built-in action names.
steps:must contain at least one step.- Each step's
action:must resolve to a built-in or another declared custom action. - Each
{{param.NAME}}reference inside a step's options must match a declared parameter. - Required parameters must be supplied at the call site (or have a
default:). - The graph of composite-to-composite references must be acyclic.
AI-session integration
shipit ai session includes the user's custom actions in the generated agent prompt (name, description, and declared parameters). Agents are instructed to prefer invoking an existing composite over duplicating its step sequence in a new workflow.
Workflow tokens & step conditions
Top-level workflow steps support reserved interpolation tokens and an optional when: condition. Together they let a single shipit run bump the version, distribute, then commit and tag the release — no shell or jq glue required.
Reserved tokens
| Token | Resolves to |
|---|---|
{{version}} | The marketing version (e.g. 1.2.3), from a prior version step or the current versioning source. |
{{build_number}} | The build number (e.g. 42). |
{{run_id}} | The current workflow run's unique ID. Use it to scope output and artifact paths per run. |
{{version_changed}} | true/false — whether the marketing version changed (false for a build-only bump, or when no version step ran). |
Rules:
- Tokens are substituted into string leaves of a step's
options:and into itswhen:value, at run time. - Values come from the most recent
versionstep's result. Before anyversionstep runs they are seeded best-effort from the current versioning source (and{{version_changed}}isfalse). {{run_id}}doesn't depend on aversionstep: it is generated when the workflow starts.- An unknown or not-yet-resolved token is left literal (and logged) rather than expanding to empty.
- These are distinct from composite
{{param.NAME}}references and from Shipfile${ENV_VAR}expansion. Resolution is scoped to top-level workflow steps; tokens insidecustom_actionssubsteps are not resolved in this release.
when: conditions
A step may carry when: "<expr>". Reserved tokens are substituted first, then the result is evaluated for truthiness: true, 1, or yes (case-insensitive) run the step; anything else — including an unresolved literal token — skips it (recorded with status skipped; the workflow continues). v1 supports a single truthy token, not operators or comparisons.
Skips and workflows marked continue_on_failure: true are the only exceptions to fail-fast workflow semantics. Any other failing step still aborts the workflow, and a continue_on_failure workflow with a failed step still finishes as failed.
Example: tag the released version in one shipit run
workflows:
release:
- action: version
options: { bump: patch } # or { bump: ${RELEASE_BUMP} } to drive from CI
- action: archive
- action: play-store
options: { track: production }
- action: git
options:
operation: commit
commit_message: "chore: release v{{version}} (build {{build_number}})"
- action: git
when: "{{version_changed}}" # skipped on build-only bumps
options:
operation: tag
tag_name: "v{{version}}"
- action: git
options:
operation: push
push_tags: trueshipit generate --goal release scaffolds this commit/tag/push tail for you — for both iOS and Android — and offers to drive the bump from CI via ${RELEASE_BUMP}. CI prerequisites the generator prints: a full-history checkout, write permission on the repo, and a configured git identity (user.name / user.email).
Environment Variables Summary
| Variable | Maps To |
|---|---|
ASC_KEY_ID | app_store_connect.key_id |
ASC_ISSUER_ID | app_store_connect.issuer_id |
ASC_PRIVATE_KEY | app_store_connect.private_key |
ASC_PRIVATE_KEY_PATH | app_store_connect.key_path |
VAULT_PASSWORD | Code signing passphrase |
SLACK_WEBHOOK_URL | notifications.slack.webhook_url |
SHIPIT_SCHEME | app.scheme |
SHIPIT_BUNDLE_ID | app.bundle_id |
SHIPIT_TEAM_ID | app.team_id |
android
Platform-specific Android configuration. These values are merged on top of shared config when --platform android is active or the platform is auto-detected as Android.
| Key | Type | Default | Env Var | Description |
|---|---|---|---|---|
build_system | string | native | SHIPIT_ANDROID__BUILD_SYSTEM | Build system: native, flutter, react_native, or kmp. See Build systems. |
module | string | app | — | Gradle module to build/bundle |
build_variant | string | release | — | Gradle build variant (e.g. release, debug) |
build_type | string | aab | — | aab (Android App Bundle) or apk |
scope | string | module | — | Default Gradle task scope: module qualifies tasks with the module; root runs from the Gradle root. |
test_kind | string | unit | — | Default test kind: unit (JVM tests), instrumented (on-device), e2e (end-to-end). |
package_name | string | — | SHIPIT_ANDROID__PACKAGE_NAME | Android application ID (e.g. com.example.app) |
keystore_path | string | — | — | Path to the release keystore |
keystore_password | string | — | ANDROID_KEYSTORE_PASSWORD | Keystore password |
keystore_alias | string | — | ANDROID_KEY_ALIAS | Key alias in the keystore |
key_password | string | — | ANDROID_KEY_PASSWORD | Key password |
rollout_fraction | float | — | — | Staged rollout fraction (0.0–1.0), for production track |
gradle_properties | map | {} | — | Extra -P key=value properties passed to Gradle |
gradlew_path | string | — | SHIPIT_ANDROID__GRADLEW_PATH | Explicit path to the gradlew script (auto-detected when omitted) |
gradle_project_dir | string | Shipfile directory | SHIPIT_ANDROID__GRADLE_PROJECT_DIR | Directory containing the Gradle root project |
AndroidCLI (preview, opt-in)
AndroidCLI complements the Gradle/ADB pipeline and does not build release artifacts. Use shipit android-cli for direct commands; workflow actions require explicit opt-in:
android:
cli:
enabled: true
executable_path: android
sdk_path: /opt/android-sdk| Key | Type | Default | Environment variable | Description |
|---|---|---|---|---|
cli.enabled | bool | false | SHIPIT_ANDROID__CLI__ENABLED | Enables AndroidCLI-backed workflow actions |
cli.executable_path | string | android | SHIPIT_ANDROID__CLI__EXECUTABLE_PATH | Executable name or path; bare names use PATH, while relative paths resolve from the Shipfile |
cli.sdk_path | string | — | SHIPIT_ANDROID__CLI__SDK_PATH | SDK path forwarded through AndroidCLI's global --sdk option |
ShipIt requires AndroidCLI 1.0 or newer when enabled. It never downloads, initializes, updates, or installs skills automatically. Device- or environment-changing workflow operations require allow_mutation: true. AndroidCLI is preview software and may report basic command/option telemetry; emulator management is currently unavailable on Windows.
| gradle_flags | array | [] | — | Extra Gradle flags such as --stacktrace |
Google Play credentials
Set these environment variables for Google Play upload actions:
| Variable | Description |
|---|---|
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON | Full JSON content of the service account key file |
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH | Path to the service account key JSON file |
Lookup notes:
package_nameis your AndroidapplicationId, usually fromapp/build.gradleorapp/build.gradle.ktsplay-storesteps require atrackoption (or--trackon the CLI) to target a Play Console track such asinternal,alpha,beta, orproduction- Create the service account in Google Cloud, enable the
Google Play Developer API, then invite that service account in Play ConsoleUsers and permissions - Give the service account Play Console permissions that match the rollout you want to perform
Env var naming: Android keystore variables support both the short form (
ANDROID_KEYSTORE_PASSWORD,ANDROID_KEY_PASSWORD,ANDROID_KEY_ALIAS) and the standardSHIPIT_form (SHIPIT_ANDROID__KEYSTORE_PASSWORD,SHIPIT_ANDROID__KEY_PASSWORD,SHIPIT_ANDROID__KEY_ALIAS). The short forms are checked as a compatibility fallback when theSHIPIT_names are not set.
ios
Platform-specific iOS overrides. Merged on top of shared config when --platform ios is active. All top-level app, code_signing, archive, export, testflight, and screenshots keys can appear here to override the shared defaults for iOS only.
| Key | Type | Default | Env Var | Description |
|---|---|---|---|---|
build_system | string | native | SHIPIT_IOS__BUILD_SYSTEM | native, flutter, react_native, or kmp. See Build systems. |
kmp_shared_module | string | shared | SHIPIT_IOS__KMP_SHARED_MODULE | Gradle module containing KMP framework/test tasks |
kmp_build_target | string | IosSimulatorArm64 | SHIPIT_IOS__KMP_BUILD_TARGET | KMP target linked before local xcodebuild build |
kmp_archive_target | string | IosArm64 | SHIPIT_IOS__KMP_ARCHIVE_TARGET | KMP target linked before xcodebuild archive |
kmp_test_task | string | iosSimulatorArm64Test | SHIPIT_IOS__KMP_TEST_TASK | KMP Gradle task used by shipit test --platform ios |
scheme | string | — | SHIPIT_IOS__SCHEME | iOS-specific Xcode scheme override |
workspace | string | — | SHIPIT_IOS__WORKSPACE | iOS-specific Xcode workspace override |
project | string | — | SHIPIT_IOS__PROJECT | iOS-specific Xcode project override |
ios:
build_system: native # native (default) | flutter | react_native | kmp
scheme: MyApp
archive:
export_method: app-storeBuild systems
build_system is orthogonal to platform: a single Kotlin Multiplatform or Flutter source tree can produce both an iOS .ipa and an Android .aab.
| Value | iOS compilation | Android compilation |
|---|---|---|
native | xcodebuild | gradlew assemble / bundle |
kmp | link KMP shared framework via Gradle → xcodebuild | gradlew :androidApp:assembleRelease / bundleRelease |
flutter | flutter build ipa (IPA at build/ios/ipa/) | flutter build apk / flutter build appbundle |
react_native | Metro bundle → xcodebuild archive + export | npx react-native build-android --tasks bundleRelease |
Auto-detection
When build_system is unset, ShipItSwifty resolves it automatically:
| Project marker | Detected value |
|---|---|
pubspec.yaml with a flutter: key | flutter |
package.json declaring react-native in dependencies | react_native |
build.gradle.kts applying kotlin("multiplatform") | kmp |
| None of the above | native |
Flutter
- iOS:
flutter build ipawrites the IPA tobuild/ios/ipa/, so there is no separateexportstep. Omitexportfrom a Flutter iOS workflow. - Android:
buildrunsflutter build apk;archiverunsflutter build appbundle. - Versioning:
pubspecis the defaultversioning.source, becauseflutter buildstamps the native projects frompubspec.yaml. - Tests:
testrunsflutter testwith machine output. Reruns select failed tests by name, and results report theflutter-testrunner withbuildSystem: flutter.
React Native
- iOS:
archiverunsxcodebuild archiveon the Xcode project React Native or Expo generated, thenexportandtestflightwork as for a native app. - Android:
archiverunsnpx react-native build-android --tasks bundleRelease, falling back to a directgradlew :app:bundleReleaseon an Expo app without the React Native CLI. - Tests and lint:
testandlintrun the package manager'stestandlintscripts. Jest results are collected as JSON and supportrerun_failed_tests. - Run
shipitfrom the directory that holdspackage.json. See the React Native Quickstart.
KMP example
ios:
build_system: kmp
scheme: iosApp
workspace: iosApp/iosApp.xcworkspace
kmp_shared_module: shared
kmp_build_target: IosSimulatorArm64
kmp_archive_target: IosArm64
android:
build_system: kmp
module: androidApp
gradle_project_dir: .
versioning:
source: kmp
spec_path: gradle.propertiescoverage action options
The coverage action reads coverage artifacts — it does not generate them. Without input_format it auto-discovers an iOS .xcresult or an Android JaCoCo report; with it, it reads a saved report from any supported tool. The options below follow the schema printed by shipit schema --output json.
| Option | Type | Default | Description |
|---|---|---|---|
input_format | string | — | Read a portable report: swift (SwiftPM LLVM JSON), lcov, jacoco, or kover. Pair with report_path. |
runner | string | — | Label for an lcov file: flutter-test or jest. LCOV doesn't say which tool wrote it, so without this the reported runner is unknown. Ignored for other formats. |
source_roots | list | — | Source directories to include (Swift and LCOV inputs). |
exclude_previews | bool | — | Exclude SwiftUI preview regions from file coverage. |
minimum_coverage | number | — | Required line coverage percentage (0–100). Fails below it, and fails outright on zero executable lines. |
xcresult_path | string | auto-discovered | Explicit .xcresult bundle (iOS). |
report_path | string | auto-discovered | Explicit JaCoCo XML report (Android), or the saved report to read with input_format. |
first_party_only | bool | true | Suppress test bundles, SPM dependencies, and known vendor frameworks. |
show_targets | bool | — | Show the per-target (iOS) or per-module (Android) breakdown. |
show_files | bool | — | Show the per-file breakdown. |
show_functions | bool | — | Show the per-function breakdown (iOS only). |
include_targets | list | — | Include only these targets or modules (overrides the first-party filter). |
exclude_targets | list | — | Exclude these targets or modules. |
sort | string | coverage | coverage (lowest first, most actionable) or name. |
limit | integer | — | Cap the number of entries shown. |
summary | bool | — | Print a single overall line. |
format | string | text | text, json, or markdown. |
A SwiftPM or LCOV file doesn't say where the tests ran, so its platform is unknown, and reports from different runs are never summed. Kotlin/Native and JavaScript coverage aren't available through these formats.
workflows:
tests:
- action: swift-test
options: { enable_code_coverage: true }
- action: coverage
options:
input_format: swift
report_path: .build/debug/codecov/MyPackage.json
source_roots: [Sources]
exclude_previews: true
minimum_coverage: 70