Skip to content
Reference

Configuration Reference

Every key in Shipfile.yml, with defaults and examples.

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 keyEnvironment variable
app.schemeSHIPIT_APP__SCHEME
app.bundle_idSHIPIT_APP__BUNDLE_ID
app.team_idSHIPIT_APP__TEAM_ID
app_store_connect.key_idSHIPIT_APP_STORE_CONNECT__KEY_ID
app_store_connect.issuer_idSHIPIT_APP_STORE_CONNECT__ISSUER_ID
build.configurationSHIPIT_BUILD__CONFIGURATION
archive.export_methodSHIPIT_ARCHIVE__EXPORT_METHOD
code_signing.git_urlSHIPIT_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

KeyTypeDescription
workspacestringPath to .xcworkspace
projectstringPath to .xcodeproj (used when workspace is nil)
schemestringXcode scheme for build and test
bundle_idstringApp bundle identifier. Optional if it can be inferred from Xcode build settings
team_idstringApple 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 or PRODUCT_BUNDLE_IDENTIFIER in build settings
  • team_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:

  1. Open Users and Access.
  2. Open Integrations.
  3. Open App Store Connect API.
  4. Create or select an API key.
  5. Copy Key ID and Issuer ID.
  6. Download the .p8 file and use it via private_key or key_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.

KeyTypeEnv VarDescription
key_idstringASC_KEY_IDAPI key ID
issuer_idstringASC_ISSUER_IDIssuer ID
key_pathstring—Path to .p8 key file
private_keystringASC_PRIVATE_KEYRaw .p8 contents

code_signing

KeyTypeDefaultDescription
typestringvaultautomatic, vault, or manual
storagestringgitgit, s3, or gcs
git_urlstring—URL of the certificate repo
app_identifierstring—Bundle ID for matching
profile_typestring—appstore, adhoc, development
p12_pathstring—Local .p12 certificate path (manual signing)
p12_base64string—Base64-encoded .p12 for CI / manual signing
p12_passwordstring—.p12 certificate password
provisioning_profile_pathstring—Local .mobileprovision path (manual signing)
provisioning_profile_base64string—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

KeyTypeDefaultDescription
configurationstringReleaseBuild configuration
derived_data_pathstring—Custom derived data path
xcargsmap{}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).

KeyTypeDefaultDescription
export_methodstringapp-storeapp-store, ad-hoc, enterprise, development
include_symbolsbooltrueInclude dSYMs
include_bitcodebool—Include bitcode (deprecated Xcode 14+)
output_pathstring—Output path for .xcarchive

export

KeyTypeDescription
archive_pathstringPath to .xcarchive
output_directorystringOutput directory for IPA

testflight

KeyTypeDefaultDescription
skip_waiting_for_build_processingboolfalseSkip Apple's processing wait
distribute_externalboolfalseDistribute to external testers
groupslist[]Beta group names
changelogstring—Release notes

screenshots

KeyTypeDefaultDescription
deviceslist[]Simulator device names
localeslist[en-US]Locale identifiers
schemestring—Screenshot UI test scheme
output_directorystring./screenshotsOutput directory

metadata

KeyTypeDefaultDescription
directorystring./metadataMetadata directory
submit_for_reviewboolfalseSubmit for review
automatic_releaseboolfalseAuto-release after approval
phased_releaseboolfalsePhased release

versioning

ShipItSwifty follows Apple's two-version model:

Plist keyFormatMeaning
CFBundleShortVersionStringMAJOR.MINOR.PATCHUser-facing marketing version (e.g. 2.4.1). Bumped manually for releases.
CFBundleVersionplain integerInternal 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.

KeyTypeDefaultDescription
strategystringsequentialsequential — adds 1 each run (guarantees a plain integer). timestamp — uses YYYYMMDDHHmm format.
sourcestringxcodeprojxcodeproj — 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_pathstring—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_keystring—Key for the build number in the spec file (YAML, e.g. settings.CURRENT_PROJECT_VERSION) or .xcconfig (e.g. APP_BUILD_NUMBER).
marketing_keystring—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 root

The 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 valueChangesLeaves untouched
buildCFBundleVersion (integer)CFBundleShortVersionString
patchPatch segment of CFBundleShortVersionString; resets CFBundleVersion to 1Major and minor segments
minorMinor segment; resets patch and CFBundleVersion to 1Major segment
majorMajor 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 json

Safe 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: testflight

The git action's operation: commit stages 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 Connect

Use 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:

  1. xcodebuild -showBuildSettings — reads MARKETING_VERSION and CURRENT_PROJECT_VERSION directly from Xcode build settings. This is the primary path for modern Xcode projects.
  2. xcrun agvtool — fallback for projects that have VERSIONING_SYSTEM = apple-generic but not the newer MARKETING_VERSION key.
  3. plutil on Info.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_failure passes xcodebuild's built-in -retry-tests-on-failure for a one-pass iOS retry.
  • rerun_failed_tests is ShipIt's own selective rerun. It collects the failures, reruns only those tests, and reports flaky versus persistent failures in the TestRunReport.
  • infrastructure_retry reruns 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.

OptionTypeDefaultDescription
destinationslistauto-discoveredOne 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>.
destinationstring—Legacy single-destination string, promoted to a one-element destinations list. Prefer destinations.
schemestringapp.schemeXcode scheme containing the test targets.
configurationstringDebugBuild configuration used for test compilation.
enable_code_coveragebool—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_pathstring—Explicit initial bundle path for a single-plan, single-destination run. An existing bundle at the path is removed first.
test_planstring—Named .xctestplan to run.
test_planslist—Ordered plans over the same test targets that share one build. Exclusive with test_plan.
test_products_pathstringunique run directoryNew .xctestproducts path for the shared build.
legacy_combined_testboolfalseUse the previous single xcodebuild test invocation instead of build-once-test-many.
serialboolfalseForce -parallel-testing-enabled NO.
skip_macro_validationboolfalsePass -skipMacroValidation while building the tests.
erase_simulatorboolfalseErase the resolved simulator before running. Refuses a device that is already booted.
only_testinglist—Restrict to specific targets or test cases. Each entry maps to -only-testing.
skip_testinglist—Skip specific targets or test cases. Each entry maps to -skip-testing.
retry_on_failureboolfalsePass -retry-tests-on-failure to retry each failing test once.
rerun_failed_testsobject—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_retryobject—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_pathstring—Path to write the structured JSON TestRunReport.
modulestringandroid.moduleAndroid/KMP. Gradle module whose tests should run.
kindstringunitAndroid only. unit for JVM tests (no device), instrumented for on-device tests, e2e for end-to-end.
scopestringautoAndroid 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_variantstringdebugAndroid only. Overrides android.build_variant for this test step.
taskstring—Android only. Explicit Gradle task name (e.g. testDebugUnitTest). Overrides variant-based task selection.
devicesobject—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: true to keep the older single xcodebuild test invocation.
  • Select a test workflow by name with the top-level test_workflow key. 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: root

For 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 optionTypeDefaultDescription
package_pathstring.Swift package directory.
scratch_pathstring—Isolated SwiftPM build storage.
filterstring—Test selector regex.
skipstring—Excluded test selector regex.
environmentmap—Extra environment for the test run, including snapshot-testing controls.
enable_code_coveragebool—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_workersinteger—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_testsobject—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_retryobject—Retry transient SwiftPM execution failures. Same fields as the test action.
output_directorystring—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 optionTypeDefaultDescription
pathslist—Paths to lint recursively.
configurationstring—Path to a swift-format configuration file.
report_pathstring—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.

OptionTypeDefaultDescription
inputslist—Result artifacts to read. The CLI's --input is repeatable.
input_formatstringdetectedxcresult, junit, swift, flutter, jest, shipit, or manifest. Ambiguous input needs an override.
runnerstringfrom the artifactThe 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_systemstringfrom the Shipfilenative, kmp, flutter, or react_native, for results read outside a Shipfile. A framework is a build system, never a platform.
coverage_inputslist—Coverage artifacts to keep alongside the results.
coverage_formatstring—swift, lcov, jacoco, or kover.
evidence_pathslist—Extra screenshots, videos, logs, and native artifacts to copy into an export.
export_directorystring—New directory for a portable export with originals, extracted evidence, a manifest, and an index with relative links.
formatstringtextOutput format hint: text, json, or markdown.
platformstringresolved platformios or android override when reading outside a Shipfile-backed context.
xcresult_pathstringauto-discoverExplicit .xcresult bundle. Falls back to ./build/<scheme>-tests.xcresult.
report_pathstringauto-discoverExplicit JUnit XML report directory. Falls back to common Gradle build/test-results/... locations.
failed_onlyboolfalseOnly include failed and errored tests in the output.
include_passedbooltrueInclude passed tests in the output.
report_output_pathstring—Write the structured JSON report to disk.

The CLI flags don't map one to one onto these keys: on the command line, --report is the JUnit directory (the action's report_path), and --report-path is where the JSON report is written (the action's report_output_path).

Archive action options

The archive action accepts inline step options for both iOS and Android:

OptionTypeDefaultDescription
schemestringapp.schemeXcode scheme to archive (iOS).
configurationstringReleaseBuild configuration.
export_methodstringapp-storeDistribution method (iOS only): app-store, ad-hoc, development, enterprise.
output_pathstring—Output .xcarchive path (iOS) or .aab path (Android).
include_symbolsbooltrueInclude dSYMs (iOS only).
modulestringandroid.moduleGradle module to bundle (Android).
build_variantstringreleaseGradle bundle variant (Android).
flavorstring—Product flavor name. Combined with build_variant for Gradle task names. Also used by Flutter and React Native archives.
scopestringmoduleAndroid Gradle task scope: module or root.
gradle_propertiesobject{}Additional -P key=value properties passed to the Gradle bundle task (Android).
infrastructure_retryobject—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.

KeyTypeDefaultDescription
toolstring—Generation tool: xcodegen or tuist
commandstring—Custom shell command (overrides tool)
spec_pathstring—Path to the project spec file (e.g. project.yml for XcodeGen)
output_projectstring—Expected output .xcodeproj path (used to verify generation succeeded)
auto_generatebooltrueAutomatically 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

KeyTypeDescription
webhook_urlstringIncoming webhook URL (use ${SLACK_WEBHOOK_URL})
channelstringDefault Slack channel
on_successboolNotify on success
on_failureboolNotify 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: production

Workflow-level overrides

KeyTypeDescription
build_variantstringOverrides android.build_variant for all steps in this workflow. Used by actions that auto-discover paths.
flavorstringOverrides android.gradle_properties.flavor for all steps in this workflow.
stepsarrayRequired when using the object format. The ordered sequence of action steps.
continue_on_failureboolOptional, 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:

KeyDescription
actionRegistered action name
optionsOptional key-value map passed to the action
whenOptional 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.
artifactsOptional 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 json

ai session always emits JSON and is intended for AI/tooling integrations rather than direct human consumption.

Workflow notes:

  • export can write an IPA into export.output_directory.
  • testflight and upload can reuse that exported IPA automatically when they run after export in the same workflow and no explicit ipa / ipa_path is set.
  • validate reports an error when testflight or upload has 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: 14

artifacts declares files a step produces that should be published with the run.

  • name uses letters, numbers, underscores, and hyphens. paths takes files, directories, or globs (*, ?, and **), and retention_days is 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.

OptionTypeDefaultDescription
app_idstring—Required. Firebase app ID, for example 1:1234567890:ios:abc123.
artifact_pathstring—Required. Path to the signed .ipa, .apk, or .aab artifact.
groupslist[]Firebase tester group aliases. At least one group or tester is required.
testerslist[]Individual tester email addresses.
release_notesstring—Plain-text notes shown to testers.
service_account_pathstring—Local service-account JSON file. Overrides environment credentials.
dry_runboolfalseValidate the planned distribution without uploading.
timeout_secondsint300Maximum 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: notify

Parameter 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:

SystemTemplate syntaxSafe 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 yml to 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

TokenResolves 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 its when: value, at run time.
  • Values come from the most recent version step's result. Before any version step runs they are seeded best-effort from the current versioning source (and {{version_changed}} is false).
  • {{run_id}} doesn't depend on a version step: 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 inside custom_actions substeps 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: true

shipit 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

VariableMaps To
ASC_KEY_IDapp_store_connect.key_id
ASC_ISSUER_IDapp_store_connect.issuer_id
ASC_PRIVATE_KEYapp_store_connect.private_key
ASC_PRIVATE_KEY_PATHapp_store_connect.key_path
VAULT_PASSWORDCode signing passphrase
SLACK_WEBHOOK_URLnotifications.slack.webhook_url
SHIPIT_SCHEMEapp.scheme
SHIPIT_BUNDLE_IDapp.bundle_id
SHIPIT_TEAM_IDapp.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.

KeyTypeDefaultEnv VarDescription
build_systemstringnativeSHIPIT_ANDROID__BUILD_SYSTEMBuild system: native, flutter, react_native, or kmp. See Build systems.
modulestringapp—Gradle module to build/bundle
build_variantstringrelease—Gradle build variant (e.g. release, debug)
build_typestringaab—aab (Android App Bundle) or apk
scopestringmodule—Default Gradle task scope: module qualifies tasks with the module; root runs from the Gradle root.
test_kindstringunit—Default test kind: unit (JVM tests), instrumented (on-device), e2e (end-to-end).
package_namestring—SHIPIT_ANDROID__PACKAGE_NAMEAndroid application ID (e.g. com.example.app)
keystore_pathstring——Path to the release keystore
keystore_passwordstring—ANDROID_KEYSTORE_PASSWORDKeystore password
keystore_aliasstring—ANDROID_KEY_ALIASKey alias in the keystore
key_passwordstring—ANDROID_KEY_PASSWORDKey password
rollout_fractionfloat——Staged rollout fraction (0.0–1.0), for production track
gradle_propertiesmap{}—Extra -P key=value properties passed to Gradle
gradlew_pathstring—SHIPIT_ANDROID__GRADLEW_PATHExplicit path to the gradlew script (auto-detected when omitted)
gradle_project_dirstringShipfile directorySHIPIT_ANDROID__GRADLE_PROJECT_DIRDirectory 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
KeyTypeDefaultEnvironment variableDescription
cli.enabledboolfalseSHIPIT_ANDROID__CLI__ENABLEDEnables AndroidCLI-backed workflow actions
cli.executable_pathstringandroidSHIPIT_ANDROID__CLI__EXECUTABLE_PATHExecutable name or path; bare names use PATH, while relative paths resolve from the Shipfile
cli.sdk_pathstring—SHIPIT_ANDROID__CLI__SDK_PATHSDK 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:

VariableDescription
GOOGLE_PLAY_SERVICE_ACCOUNT_JSONFull JSON content of the service account key file
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATHPath to the service account key JSON file

Lookup notes:

  • package_name is your Android applicationId, usually from app/build.gradle or app/build.gradle.kts
  • play-store steps require a track option (or --track on the CLI) to target a Play Console track such as internal, alpha, beta, or production
  • Create the service account in Google Cloud, enable the Google Play Developer API, then invite that service account in Play Console Users 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 standard SHIPIT_ form (SHIPIT_ANDROID__KEYSTORE_PASSWORD, SHIPIT_ANDROID__KEY_PASSWORD, SHIPIT_ANDROID__KEY_ALIAS). The short forms are checked as a compatibility fallback when the SHIPIT_ 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.

KeyTypeDefaultEnv VarDescription
build_systemstringnativeSHIPIT_IOS__BUILD_SYSTEMnative, flutter, react_native, or kmp. See Build systems.
kmp_shared_modulestringsharedSHIPIT_IOS__KMP_SHARED_MODULEGradle module containing KMP framework/test tasks
kmp_build_targetstringIosSimulatorArm64SHIPIT_IOS__KMP_BUILD_TARGETKMP target linked before local xcodebuild build
kmp_archive_targetstringIosArm64SHIPIT_IOS__KMP_ARCHIVE_TARGETKMP target linked before xcodebuild archive
kmp_test_taskstringiosSimulatorArm64TestSHIPIT_IOS__KMP_TEST_TASKKMP Gradle task used by shipit test --platform ios
schemestring—SHIPIT_IOS__SCHEMEiOS-specific Xcode scheme override
workspacestring—SHIPIT_IOS__WORKSPACEiOS-specific Xcode workspace override
projectstring—SHIPIT_IOS__PROJECTiOS-specific Xcode project override
ios:
  build_system: native # native (default) | flutter | react_native | kmp
  scheme: MyApp
archive:
  export_method: app-store

Build 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.

ValueiOS compilationAndroid compilation
nativexcodebuildgradlew assemble / bundle
kmplink KMP shared framework via Gradle → xcodebuildgradlew :androidApp:assembleRelease / bundleRelease
flutterflutter build ipa (IPA at build/ios/ipa/)flutter build apk / flutter build appbundle
react_nativeMetro bundle → xcodebuild archive + exportnpx react-native build-android --tasks bundleRelease

Auto-detection

When build_system is unset, ShipItSwifty resolves it automatically:

Project markerDetected value
pubspec.yaml with a flutter: keyflutter
package.json declaring react-native in dependenciesreact_native
build.gradle.kts applying kotlin("multiplatform")kmp
None of the abovenative

Flutter

  • iOS: flutter build ipa writes the IPA to build/ios/ipa/, so there is no separate export step. Omit export from a Flutter iOS workflow.
  • Android: build runs flutter build apk; archive runs flutter build appbundle.
  • Versioning: pubspec is the default versioning.source, because flutter build stamps the native projects from pubspec.yaml.
  • Tests: test runs flutter test with machine output. Reruns select failed tests by name, and results report the flutter-test runner with buildSystem: flutter.

React Native

  • iOS: archive runs xcodebuild archive on the Xcode project React Native or Expo generated, then export and testflight work as for a native app.
  • Android: archive runs npx react-native build-android --tasks bundleRelease, falling back to a direct gradlew :app:bundleRelease on an Expo app without the React Native CLI.
  • Tests and lint: test and lint run the package manager's test and lint scripts. Jest results are collected as JSON and support rerun_failed_tests.
  • Run shipit from the directory that holds package.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.properties

coverage 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.

OptionTypeDefaultDescription
input_formatstring—Read a portable report: swift (SwiftPM LLVM JSON), lcov, jacoco, or kover. Pair with report_path.
runnerstring—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_rootslist—Source directories to include (Swift and LCOV inputs).
exclude_previewsbool—Exclude SwiftUI preview regions from file coverage.
minimum_coveragenumber—Required line coverage percentage (0–100). Fails below it, and fails outright on zero executable lines.
xcresult_pathstringauto-discoveredExplicit .xcresult bundle (iOS).
report_pathstringauto-discoveredExplicit JaCoCo XML report (Android), or the saved report to read with input_format.
first_party_onlybooltrueSuppress test bundles, SPM dependencies, and known vendor frameworks.
show_targetsbool—Show the per-target (iOS) or per-module (Android) breakdown.
show_filesbool—Show the per-file breakdown.
show_functionsbool—Show the per-function breakdown (iOS only).
include_targetslist—Include only these targets or modules (overrides the first-party filter).
exclude_targetslist—Exclude these targets or modules.
sortstringcoveragecoverage (lowest first, most actionable) or name.
limitinteger—Cap the number of entries shown.
summarybool—Print a single overall line.
formatstringtexttext, 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